Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ACME "dns-account-01" challenge #7387

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d341dc5
Add DNS-ACCOUNT-01 support.
sheurich Jan 8, 2024
1c0653f
Initial tests for DNS-ACCOUNT-01.
sheurich Mar 14, 2024
72c4b09
Upgrade eggsampler/acme/v3 to v3.5.0 for DNS-ACCOUNT-01 support
sheurich Mar 15, 2024
bcec8c4
test(integration): add TestDNSAccountChallenge
sheurich Mar 15, 2024
4489f43
fix(validateDNSAccount01): reject unsupported scopes
sheurich Mar 16, 2024
14ea785
test(dns-account-01): additional unit tests
sheurich Mar 16, 2024
22d2210
test(pa): add dns-account-01 to wildcard challenges
sheurich Mar 16, 2024
0718a30
test(dns-account-01): add to core and ra unit tests
sheurich Mar 16, 2024
ece427a
feat(load-generator): add DNS-ACCOUNT-01 challenge strategy
sheurich Mar 16, 2024
e0fce1b
feat(ra): Allow DNS-ACCOUNT-01 authorizations to be reused for wildcards
sheurich Mar 16, 2024
eb7c094
rename
sheurich Mar 16, 2024
a948e01
fix(TestDNSAccountChallenge): only run test in config-next
sheurich Mar 16, 2024
9e5e388
Merge branch 'main' into add-dns-account-01
sheurich Mar 18, 2024
492ce0f
clean up challenge policy logic
sheurich Mar 18, 2024
4ea3634
fix(test): add dns-01+dns-account-01 wildcard policy test
sheurich Mar 18, 2024
b2f90f7
doc(model): explain challTypeToUint and uintToChallType
sheurich Mar 19, 2024
ac488a5
bundle authz `Scope` into core.Challenge
sheurich Mar 19, 2024
dcc624e
Merge branch 'main' into add-dns-account-01
sheurich Mar 19, 2024
4f7a8a1
Merge branch 'main' into add-dns-account-01
sheurich Mar 19, 2024
18d89dc
Merge branch 'main' into add-dns-account-01
sheurich Mar 22, 2024
a6f87c4
- Add AccountURL and Scope to core.Authorization and protobufs
sheurich Mar 20, 2024
ab01878
lint
sheurich Mar 22, 2024
528f6de
fix(akamai-purger): deep copy batch of purge entries
sheurich Mar 22, 2024
67cfe74
remove AccountURL from Authorization; pass using Challenge instead
sheurich Mar 27, 2024
d2fe3e0
fix: use jws protected header for KeyID
sheurich Mar 27, 2024
c941420
fix: use challenge pointer for setting AccountURL in WebFrontEndImpl.…
sheurich Mar 27, 2024
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
32 changes: 17 additions & 15 deletions bdns/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,37 @@ type MockClient struct {

// LookupTXT is a mock
func (mock *MockClient) LookupTXT(_ context.Context, hostname string) ([]string, ResolverAddrs, error) {
if hostname == "_acme-challenge.servfail.com" {
// The hostname prefix of "_vrr7uudrklshxb6l._acme-host-challenge"
// corresponds to `host` Scope dns-account-01 validation with an
// Account URL of `https://example.com/acme/acct/1`.

switch hostname {
case "_acme-challenge.servfail.com":
return nil, ResolverAddrs{"MockClient"}, fmt.Errorf("SERVFAIL")
}
if hostname == "_acme-challenge.good-dns01.com" {
case "_acme-challenge.good-dns01.com",
"_vrr7uudrklshxb6l._acme-host-challenge.good-dns01.com":
// base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
// + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"))
// expected token + test account jwk thumbprint
return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == "_acme-challenge.wrong-dns01.com" {
case "_acme-challenge.wrong-dns01.com",
"_vrr7uudrklshxb6l._acme-host-challenge.wrong-dns01.com":
return []string{"a"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == "_acme-challenge.wrong-many-dns01.com" {
case "_acme-challenge.wrong-many-dns01.com":
return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == "_acme-challenge.long-dns01.com" {
case "_acme-challenge.long-dns01.com":
return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, nil
}
if hostname == "_acme-challenge.no-authority-dns01.com" {
case "_acme-challenge.no-authority-dns01.com":
// base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
// + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"))
// expected token + test account jwk thumbprint
return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil
}
// empty-txts.com always returns zero TXT records
if hostname == "_acme-challenge.empty-txts.com" {
case "_acme-challenge.empty-txts.com",
"_vrr7uudrklshxb6l._acme-host-challenge.empty-txts.com":
return []string{}, ResolverAddrs{"MockClient"}, nil
default:
return []string{"hostname"}, ResolverAddrs{"MockClient"}, nil
}
return []string{"hostname"}, ResolverAddrs{"MockClient"}, nil
}

// makeTimeoutError returns a a net.OpError for which Timeout() returns true.
Expand Down
9 changes: 8 additions & 1 deletion cmd/akamai-purger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"math"
"os"
"slices"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -193,6 +194,8 @@ func (ap *akamaiPurger) purgeBatch(batch [][]string) error {
return nil
}

// takeBatch returns a slice containing the next batch of entries from the purge stack.
// It copies at most entriesPerBatch entries from the top of the stack into a new slice which is returned.
func (ap *akamaiPurger) takeBatch() [][]string {
ap.Lock()
defer ap.Unlock()
Expand All @@ -211,7 +214,11 @@ func (ap *akamaiPurger) takeBatch() [][]string {
}

batchBegin := stackSize - batchSize
batch := ap.toPurge[batchBegin:]
batchEnd := stackSize
batch := make([][]string, batchSize)
for i, entry := range ap.toPurge[batchBegin:batchEnd] {
batch[i] = slices.Clone(entry)
}
ap.toPurge = ap.toPurge[:batchBegin]
return batch
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,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 dns-account-01 tls-alpn-01,endkeys"`
}

// CheckChallenges checks whether the list of challenges in the PA config
Expand Down
7 changes: 7 additions & 0 deletions core/challenges.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ func DNSChallenge01(token string) Challenge {
return newChallenge(ChallengeTypeDNS01, token)
}

// DNSAccountChallenge01 constructs a dns-account-01 challenge.
func DNSAccountChallenge01(token string) Challenge {
return newChallenge(ChallengeTypeDNSAccount01, token)
}

// TLSALPNChallenge01 constructs a tls-alpn-01 challenge.
func TLSALPNChallenge01(token string) Challenge {
return newChallenge(ChallengeTypeTLSALPN01, token)
Expand All @@ -33,6 +38,8 @@ func NewChallenge(kind AcmeChallenge, token string) (Challenge, error) {
return HTTPChallenge01(token), nil
case ChallengeTypeDNS01:
return DNSChallenge01(token), nil
case ChallengeTypeDNSAccount01:
return DNSAccountChallenge01(token), nil
case ChallengeTypeTLSALPN01:
return TLSALPNChallenge01(token), nil
default:
Expand Down
1 change: 1 addition & 0 deletions core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestChallenges(t *testing.T) {

test.Assert(t, ChallengeTypeHTTP01.IsValid(), "Refused valid challenge")
test.Assert(t, ChallengeTypeDNS01.IsValid(), "Refused valid challenge")
test.Assert(t, ChallengeTypeDNSAccount01.IsValid(), "Refused valid challenge")
test.Assert(t, ChallengeTypeTLSALPN01.IsValid(), "Refused valid challenge")
test.Assert(t, !AcmeChallenge("nonsense-71").IsValid(), "Accepted invalid challenge")
}
Expand Down
33 changes: 27 additions & 6 deletions core/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,31 @@ type AcmeChallenge string

// These types are the available challenges
const (
ChallengeTypeHTTP01 = AcmeChallenge("http-01")
ChallengeTypeDNS01 = AcmeChallenge("dns-01")
ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01")
ChallengeTypeHTTP01 = AcmeChallenge("http-01")
ChallengeTypeDNS01 = AcmeChallenge("dns-01")
ChallengeTypeDNSAccount01 = AcmeChallenge("dns-account-01")
ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01")
)

// IsValid tests whether the challenge is a known challenge
func (c AcmeChallenge) IsValid() bool {
switch c {
case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01:
case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeDNSAccount01, ChallengeTypeTLSALPN01:
return true
default:
return false
}
}

// AuthorizationScope defines the scope of an authorization.
// This is used to determine challenge validation behavior.
type AuthorizationScope string

const (
AuthorizationScopeHost = AuthorizationScope("host")
AuthorizationScopeWildcard = AuthorizationScope("wildcard")
)

// OCSPStatus defines the state of OCSP for a domain
type OCSPStatus string

Expand Down Expand Up @@ -192,9 +202,15 @@ type Challenge struct {
// For the V2 API the "URI" field is deprecated in favour of URL.
URL string `json:"url,omitempty"`

// Used by http-01, tls-sni-01, tls-alpn-01 and dns-01 challenges
// Used by http-01, tls-sni-01, tls-alpn-01, dns-01 and dns-account-01 challenges
Token string `json:"token,omitempty"`

// The scope of the authorization. Used by dns-account-01 challenge.
Scope AuthorizationScope `json:"scope,omitempty"`

// Account URL. Used by dns-account-01 challenge.
AccountURL string `json:"accountUrl,omitempty"`

// The expected KeyAuthorization for validation of the challenge. Populated by
// the RA prior to passing the challenge to the VA. For legacy reasons this
// field is called "ProvidedKeyAuthorization" because it was initially set by
Expand Down Expand Up @@ -256,7 +272,7 @@ func (ch Challenge) RecordsSane() bool {
ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
return false
}
case ChallengeTypeDNS01:
case ChallengeTypeDNS01, ChallengeTypeDNSAccount01:
if len(ch.ValidationRecord) > 1 {
return false
}
Expand Down Expand Up @@ -372,6 +388,11 @@ type Authorization struct {
// as part of the authorization, the identifier we store in the database
// can contain an asterisk.
Wildcard bool `json:"wildcard,omitempty" db:"-"`

// The scope of the authorization. This is used internally for challenge
// validation (e.g. dns-account-01) but not stored in the database
// or represented externally.
Scope AuthorizationScope `json:"scope,omitempty" db:"-"`
}

// FindChallengeByStringID will look for a challenge matching the given ID inside
Expand Down
2 changes: 1 addition & 1 deletion core/objects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestChallengeSanityCheck(t *testing.T) {
}`), &accountKey)
test.AssertNotError(t, err, "Error unmarshaling JWK")

types := []AcmeChallenge{ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01}
types := []AcmeChallenge{ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeDNSAccount01, ChallengeTypeTLSALPN01}
for _, challengeType := range types {
chall := Challenge{
Type: challengeType,
Expand Down
Loading
Loading