Skip to content
Open
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 type challenge, or DNS-Account-01 if the feature is enabled.
// 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 &&
(!features.Get().DNSAccount01Enabled || 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
92 changes: 92 additions & 0 deletions ra/ra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2120,6 +2120,98 @@ 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()

features.Set(features.Config{DNSAccount01Enabled: true})
defer features.Reset()

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 for the wildcard domain. NewOrder should recognize that
// the existing valid authorization with DNS-Account-01 challenge can be
// reused for this wildcard domain request.
order, err := ra.NewOrder(ctx, orderReq)
test.AssertNotError(t, err, "NewOrder failed to reuse wildcard authz with DNS-Account-01")
// The order should contain exactly one authorization (the reused one)
test.AssertEquals(t, len(order.V2Authorizations), 1)
// The authorization ID should match the mock authz we provided (ID "1")
test.AssertEquals(t, order.V2Authorizations[0], int64(1))
}

// TestNewOrderAuthzReuseDNSAccount01Disabled checks that the RA rejects
// wildcard authorization reuse with DNS-Account-01 when the feature is disabled.
func TestNewOrderAuthzReuseDNSAccount01Disabled(t *testing.T) {
_, _, ra, _, _, registration, cleanUp := initAuthorities(t)
defer cleanUp()

// Feature flag is NOT set - DNSAccount01Enabled defaults to false

ctx := context.Background()
idents := identifier.ACMEIdentifiers{identifier.NewDNS("*.zombo.com")}

// Use a mock SA that returns a 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(),
}

// NewOrder should reject the DNS-Account-01 authz when feature is disabled
_, err := ra.NewOrder(ctx, orderReq)
test.AssertError(t, err, "NewOrder should reject DNS-Account-01 when feature disabled")
test.AssertContains(t, err.Error(), "SA.GetAuthorizations returned a DNS wildcard authz (1) with invalid challenge(s)")
}

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