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
39 changes: 39 additions & 0 deletions bdns/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,43 @@ type MockClient struct {

// LookupTXT is a mock
func (mock *MockClient) LookupTXT(_ context.Context, hostname string) ([]string, ResolverAddrs, error) {
// Use the example account-specific label prefix derived from
// "https://example.com/acme/acct/ExampleAccount"
const accountLabelPrefix = "_ujmmovf2vn55tgye._acme-challenge"

if hostname == accountLabelPrefix+".servfail.com" {
// Mirror dns-01 servfail behaviour
return nil, ResolverAddrs{"MockClient"}, fmt.Errorf("SERVFAIL")
}
if hostname == accountLabelPrefix+".good-dns01.com" {
// Mirror dns-01 good record
// base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
// + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"))
return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == accountLabelPrefix+".wrong-dns01.com" {
// Mirror dns-01 wrong record
return []string{"a"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == accountLabelPrefix+".wrong-many-dns01.com" {
// Mirror dns-01 wrong-many record
return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == accountLabelPrefix+".long-dns01.com" {
// Mirror dns-01 long record
return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == accountLabelPrefix+".no-authority-dns01.com" {
// Mirror dns-01 no-authority good record
// base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
// + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"))
return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == accountLabelPrefix+".empty-txts.com" {
// Mirror dns-01 zero TXT records
return []string{}, ResolverAddrs{"MockClient"}, nil
}

if hostname == "_acme-challenge.servfail.com" {
return nil, ResolverAddrs{"MockClient"}, fmt.Errorf("SERVFAIL")
}
Expand Down Expand Up @@ -48,6 +85,8 @@ func (mock *MockClient) LookupTXT(_ context.Context, hostname string) ([]string,
if hostname == "_acme-challenge.empty-txts.com" {
return []string{}, ResolverAddrs{"MockClient"}, nil
}

// Default fallback
return []string{"hostname"}, ResolverAddrs{"MockClient"}, nil
}

Expand Down
1 change: 1 addition & 0 deletions cmd/boulder-va/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func main() {
clk,
logger,
c.VA.AccountURIPrefixes,
c.VA.DNSAccountChallengeURIPrefix,
va.PrimaryPerspective,
"",
policy.IsReservedIP)
Expand Down
2 changes: 1 addition & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ type SMTPConfig struct {
// it should offer.
type PAConfig struct {
DBConfig `validate:"-"`
Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01,endkeys"`
Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01 dns-account-01,endkeys"`
Identifiers map[identifier.IdentifierType]bool `validate:"omitempty,dive,keys,oneof=dns ip,endkeys"`
}

Expand Down
1 change: 1 addition & 0 deletions cmd/remoteva/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func main() {
clk,
logger,
c.RVA.AccountURIPrefixes,
c.RVA.DNSAccountChallengeURIPrefix,
c.RVA.Perspective,
c.RVA.RIR,
policy.IsReservedIP)
Expand Down
6 changes: 6 additions & 0 deletions features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ type Config struct {
// StoreARIReplacesInOrders causes the SA to store and retrieve the optional
// ARI replaces field in the orders table.
StoreARIReplacesInOrders bool

// DNSAccount01Enabled controls support for the dns-account-01 challenge
// type. When enabled, the server can offer and validate this challenge
// during certificate issuance. This flag must be set to true in the
// RA, VA, and WFE2 services for full functionality.
DNSAccount01Enabled bool
}

var fMu = new(sync.RWMutex)
Expand Down
23 changes: 16 additions & 7 deletions policy/pa.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/iana"
"github.com/letsencrypt/boulder/identifier"
blog "github.com/letsencrypt/boulder/log"
Expand Down Expand Up @@ -577,20 +578,28 @@ func (pa *AuthorityImpl) checkHostLists(domain string) error {
func (pa *AuthorityImpl) ChallengeTypesFor(ident identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) {
switch ident.Type {
case identifier.TypeDNS:
// If the identifier is for a DNS wildcard name we only provide a DNS-01
// challenge, to comply with the BRs Sections 3.2.2.4.19 and 3.2.2.4.20
// stating that ACME HTTP-01 and TLS-ALPN-01 are not suitable for validating
// Wildcard Domains.
// If the identifier is for a DNS wildcard name we only provide DNS-01
// or DNS-ACCOUNT-01 challenges, to comply with the BRs Sections 3.2.2.4.19
// and 3.2.2.4.20 stating that ACME HTTP-01 and TLS-ALPN-01 are not
// suitable for validating Wildcard Domains.
if strings.HasPrefix(ident.Value, "*.") {
return []core.AcmeChallenge{core.ChallengeTypeDNS01}, nil
challenges := []core.AcmeChallenge{core.ChallengeTypeDNS01}
if features.Get().DNSAccount01Enabled {
challenges = append(challenges, core.ChallengeTypeDNSAccount01)
}
return challenges, nil
}

// Return all challenge types we support for non-wildcard DNS identifiers.
return []core.AcmeChallenge{
challenges := []core.AcmeChallenge{
core.ChallengeTypeHTTP01,
core.ChallengeTypeDNS01,
core.ChallengeTypeTLSALPN01,
}, nil
}
if features.Get().DNSAccount01Enabled {
challenges = append(challenges, core.ChallengeTypeDNSAccount01)
}
return challenges, nil
case identifier.TypeIP:
// Only HTTP-01 and TLS-ALPN-01 are suitable for IP address identifiers
// per RFC 8738, Sec. 4.
Expand Down
161 changes: 114 additions & 47 deletions policy/pa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import (

func paImpl(t *testing.T) *AuthorityImpl {
enabledChallenges := map[core.AcmeChallenge]bool{
core.ChallengeTypeHTTP01: true,
core.ChallengeTypeDNS01: true,
core.ChallengeTypeTLSALPN01: true,
core.ChallengeTypeHTTP01: true,
core.ChallengeTypeDNS01: true,
core.ChallengeTypeTLSALPN01: true,
core.ChallengeTypeDNSAccount01: true,
}

enabledIdentifiers := map[identifier.IdentifierType]bool{
Expand Down Expand Up @@ -445,56 +446,122 @@ func TestChallengeTypesFor(t *testing.T) {
t.Parallel()
pa := paImpl(t)

testCases := []struct {
name string
ident identifier.ACMEIdentifier
wantChalls []core.AcmeChallenge
wantErr string
}{
{
name: "dns",
ident: identifier.NewDNS("example.com"),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeTLSALPN01,
t.Run("DNSAccount01Enabled=true", func(t *testing.T) {
features.Set(features.Config{DNSAccount01Enabled: true})
t.Cleanup(features.Reset)

testCases := []struct {
name string
ident identifier.ACMEIdentifier
wantChalls []core.AcmeChallenge
wantErr string
}{
{
name: "dns",
ident: identifier.NewDNS("example.com"),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeHTTP01,
core.ChallengeTypeDNS01,
core.ChallengeTypeTLSALPN01,
core.ChallengeTypeDNSAccount01,
},
},
},
{
name: "dns wildcard",
ident: identifier.NewDNS("*.example.com"),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeDNS01,
{
name: "dns wildcard",
ident: identifier.NewDNS("*.example.com"),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeDNS01,
core.ChallengeTypeDNSAccount01,
},
},
},
{
name: "ip",
ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01,
{
name: "ip",
ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01,
},
},
},
{
name: "invalid",
ident: identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"},
wantErr: "unrecognized identifier type",
},
}
{
name: "invalid",
ident: identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"},
wantErr: "unrecognized identifier type",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
challs, err := pa.ChallengeTypesFor(tc.ident)
for _, tc := range testCases {
tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
challs, err := pa.ChallengeTypesFor(tc.ident)

if len(tc.wantChalls) != 0 {
test.AssertNotError(t, err, "should have succeeded")
test.AssertDeepEquals(t, challs, tc.wantChalls)
}
if len(tc.wantChalls) != 0 {
test.AssertNotError(t, err, "should have succeeded")
test.AssertDeepEquals(t, challs, tc.wantChalls)
}

if tc.wantErr != "" {
test.AssertError(t, err, "should have errored")
test.AssertContains(t, err.Error(), tc.wantErr)
}
})
}
if tc.wantErr != "" {
test.AssertError(t, err, "should have errored")
test.AssertContains(t, err.Error(), tc.wantErr)
}
})
}
})

t.Run("DNSAccount01Enabled=false", func(t *testing.T) {
features.Set(features.Config{DNSAccount01Enabled: false})
t.Cleanup(features.Reset)

testCases := []struct {
name string
ident identifier.ACMEIdentifier
wantChalls []core.AcmeChallenge
wantErr string
}{
{
name: "dns",
ident: identifier.NewDNS("example.com"),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeHTTP01,
core.ChallengeTypeDNS01,
core.ChallengeTypeTLSALPN01,
// DNSAccount01 excluded
},
},
{
name: "wildcard",
ident: identifier.NewDNS("*.example.com"),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeDNS01,
// DNSAccount01 excluded
},
},
{
name: "ip",
ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
wantChalls: []core.AcmeChallenge{
core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01,
},
},
}

for _, tc := range testCases {
tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
challs, err := pa.ChallengeTypesFor(tc.ident)

if len(tc.wantChalls) != 0 {
test.AssertNotError(t, err, "should have succeeded")
test.AssertDeepEquals(t, challs, tc.wantChalls)
}

if tc.wantErr != "" {
test.AssertError(t, err, "should have errored")
test.AssertContains(t, err.Error(), tc.wantErr)
}
})
}
})
}

// TestMalformedExactBlocklist tests that loading a YAML policy file with an
Expand Down
Loading
Loading