diff --git a/ra/ra.go b/ra/ra.go index 8e92a095faa..824ff6b1129 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -2239,10 +2239,12 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New } // If the identifier is a wildcard DNS name, it must have exactly one - // DNS-01 type challenge. The PA guarantees this at order creation time, - // but we verify again to be safe. + // DNS-01 or DNS-Account-01 type challenge. The PA guarantees this at + // order creation time, but we verify again to be safe. if ident.Type == identifier.TypeDNS && strings.HasPrefix(ident.Value, "*.") && - (len(authz.Challenges) != 1 || authz.Challenges[0].Type != core.ChallengeTypeDNS01) { + (len(authz.Challenges) != 1 || + (authz.Challenges[0].Type != core.ChallengeTypeDNS01 && + authz.Challenges[0].Type != core.ChallengeTypeDNSAccount01)) { return nil, berrors.InternalServerError( "SA.GetAuthorizations returned a DNS wildcard authz (%s) with invalid challenge(s)", authz.ID) diff --git a/ra/ra_test.go b/ra/ra_test.go index 997aa4f31cb..ace57670085 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -2120,6 +2120,46 @@ func TestNewOrderAuthzReuseSafety(t *testing.T) { test.AssertContains(t, err.Error(), "SA.GetAuthorizations returned a DNS wildcard authz (1) with invalid challenge(s)") } +// TestNewOrderAuthzReuseDNSAccount01 checks that the RA correctly allows reuse +// of a wildcard authorization with a DNS-Account-01 challenge. +func TestNewOrderAuthzReuseDNSAccount01(t *testing.T) { + _, _, ra, _, _, registration, cleanUp := initAuthorities(t) + defer cleanUp() + + ctx := context.Background() + idents := identifier.ACMEIdentifiers{identifier.NewDNS("*.zombo.com")} + + // Use a mock SA that returns a valid DNS-Account-01 authz for wildcard + expires := time.Now().Add(24 * time.Hour) + ra.SA = &mockSAWithAuthzs{ + authzs: []*core.Authorization{ + { + ID: "1", + Identifier: identifier.NewDNS("*.zombo.com"), + RegistrationID: registration.Id, + Status: "valid", + Expires: &expires, + Challenges: []core.Challenge{ + { + Type: core.ChallengeTypeDNSAccount01, + Status: core.StatusValid, + Token: core.NewToken(), + }, + }, + }, + }, + } + + orderReq := &rapb.NewOrderRequest{ + RegistrationID: registration.Id, + Identifiers: idents.ToProtoSlice(), + } + + // Create an order - it should succeed with DNS-Account-01 wildcard reuse + _, err := ra.NewOrder(ctx, orderReq) + test.AssertNotError(t, err, "NewOrder failed to reuse wildcard authz with DNS-Account-01") +} + func TestNewOrderWildcard(t *testing.T) { _, _, ra, _, _, registration, cleanUp := initAuthorities(t) defer cleanUp() diff --git a/test/integration/dns_account_01_test.go b/test/integration/dns_account_01_test.go index 5f2d72661b1..e5bb3d2c129 100644 --- a/test/integration/dns_account_01_test.go +++ b/test/integration/dns_account_01_test.go @@ -361,3 +361,93 @@ func TestDNSAccount01WildcardDomain(t *testing.T) { } } } + +func TestDNSAccount01WildcardAuthorizationReuse(t *testing.T) { + t.Parallel() + + if os.Getenv("BOULDER_CONFIG_DIR") == "test/config" { + t.Skip("Test requires dns-account-01 to be enabled") + } + + // Use same domain for both orders to trigger authorization reuse + domain := random_domain() + wildcardDomain := fmt.Sprintf("*.%s", domain) + + c, err := makeClient() + if err != nil { + t.Fatalf("creating client: %s", err) + } + + idents := []acme.Identifier{{Type: "dns", Value: wildcardDomain}} + + // First order: Create and complete DNS-Account-01 challenge + order1, err := c.Client.NewOrder(c.Account, idents) + if err != nil { + t.Fatalf("creating first order: %s", err) + } + + authzURL := order1.Authorizations[0] + auth1, err := c.Client.FetchAuthorization(c.Account, authzURL) + if err != nil { + t.Fatalf("fetching first authorization: %s", err) + } + + chal, ok := auth1.ChallengeMap[acme.ChallengeTypeDNSAccount01] + if !ok { + t.Fatal("dns-account-01 challenge not offered by server") + } + + _, err = testSrvClient.AddDNSAccount01Response(c.Account.URL, domain, chal.KeyAuthorization) + if err != nil { + t.Fatalf("adding DNS response: %s", err) + } + t.Cleanup(func() { + _, _ = testSrvClient.RemoveDNSAccount01Response(c.Account.URL, domain) + }) + + chal, err = c.Client.UpdateChallenge(c.Account, chal) + if err != nil { + t.Fatalf("updating challenge: %s", err) + } + + // Wait for authorization to become valid + auth1, err = c.Client.FetchAuthorization(c.Account, authzURL) + if err != nil { + t.Fatalf("fetching first authorization after challenge update: %s", err) + } + + if auth1.Status != "valid" { + t.Fatalf("expected first authorization status to be 'valid', got '%s'", auth1.Status) + } + + // Second order: Should reuse the existing authorization + order2, err := c.Client.NewOrder(c.Account, idents) + if err != nil { + t.Fatalf("creating second order: %s", err) + } + + if len(order2.Authorizations) != 1 { + t.Fatalf("expected 1 authorization in second order, got %d", len(order2.Authorizations)) + } + + authzURL2 := order2.Authorizations[0] + auth2, err := c.Client.FetchAuthorization(c.Account, authzURL2) + if err != nil { + t.Fatalf("fetching second authorization: %s", err) + } + + // Verify reuse occurred: same authorization URL + if authzURL != authzURL2 { + t.Fatalf("expected same authorization URL, got different: %s != %s", authzURL, authzURL2) + } + + // Verify authorization is already valid (no re-validation needed) + if auth2.Status != "valid" { + t.Fatalf("expected reused authorization status to be 'valid', got '%s'", auth2.Status) + } + + // Verify authorization still has DNS-Account-01 challenge + if _, ok := auth2.ChallengeMap[acme.ChallengeTypeDNSAccount01]; !ok { + t.Fatal("expected reused authorization to have dns-account-01 challenge") + } +}