Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ra/ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions ra/ra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
90 changes: 90 additions & 0 deletions test/integration/dns_account_01_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Loading