Compare commits

...

1 Commits

Author SHA1 Message Date
Shiloh Heurich 473b4059c4
feat: Add core definitions for dns-account-01 (#8140)
## Summary

This PR introduces the foundational components required to support
the `dns-account-01` challenge type, as specified in draft-ietf-acme-dns-account-label-00:
https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/.

It focuses only on core definitions and SA support. PA/VA/RA logic will be in
a follow-up change.

Core Definitions & Logic:
- //core/objects.go: Added `ChallengeTypeDNSAccount01` constant and
  updated validation methods
- //core/challenges.go: Added `DNSAccountChallenge01` constructor
  and factory support

Storage Authority (SA) Support:
- //sa/model.go: Added `dns-account-01` to challenge type mappings

Testing:
- //core/*_test.go: Basic definition and validation tests
- //sa/sa_test.go: Database round-trip tests for `dns-account-01`
  challenges

Dependencies:
- Updated github.com/eggsampler/acme/v3 to release version v3.6.2
2025-07-29 09:27:04 -07:00
10 changed files with 69 additions and 14 deletions

View File

@ -25,6 +25,11 @@ func TLSALPNChallenge01(token string) Challenge {
return newChallenge(ChallengeTypeTLSALPN01, token) return newChallenge(ChallengeTypeTLSALPN01, token)
} }
// DNSAccountChallenge01 constructs a dns-account-01 challenge.
func DNSAccountChallenge01(token string) Challenge {
return newChallenge(ChallengeTypeDNSAccount01, token)
}
// NewChallenge constructs a challenge of the given kind. It returns an // NewChallenge constructs a challenge of the given kind. It returns an
// error if the challenge type is unrecognized. // error if the challenge type is unrecognized.
func NewChallenge(kind AcmeChallenge, token string) (Challenge, error) { func NewChallenge(kind AcmeChallenge, token string) (Challenge, error) {
@ -35,6 +40,8 @@ func NewChallenge(kind AcmeChallenge, token string) (Challenge, error) {
return DNSChallenge01(token), nil return DNSChallenge01(token), nil
case ChallengeTypeTLSALPN01: case ChallengeTypeTLSALPN01:
return TLSALPNChallenge01(token), nil return TLSALPNChallenge01(token), nil
case ChallengeTypeDNSAccount01:
return DNSAccountChallenge01(token), nil
default: default:
return Challenge{}, fmt.Errorf("unrecognized challenge type %q", kind) return Challenge{}, fmt.Errorf("unrecognized challenge type %q", kind)
} }

View File

@ -32,12 +32,16 @@ func TestChallenges(t *testing.T) {
dns01 := DNSChallenge01(token) dns01 := DNSChallenge01(token)
test.AssertNotError(t, dns01.CheckPending(), "CheckConsistencyForClientOffer returned an error") test.AssertNotError(t, dns01.CheckPending(), "CheckConsistencyForClientOffer returned an error")
dnsAccount01 := DNSAccountChallenge01(token)
test.AssertNotError(t, dnsAccount01.CheckPending(), "CheckConsistencyForClientOffer returned an error")
tlsalpn01 := TLSALPNChallenge01(token) tlsalpn01 := TLSALPNChallenge01(token)
test.AssertNotError(t, tlsalpn01.CheckPending(), "CheckConsistencyForClientOffer returned an error") test.AssertNotError(t, tlsalpn01.CheckPending(), "CheckConsistencyForClientOffer returned an error")
test.Assert(t, ChallengeTypeHTTP01.IsValid(), "Refused valid challenge") test.Assert(t, ChallengeTypeHTTP01.IsValid(), "Refused valid challenge")
test.Assert(t, ChallengeTypeDNS01.IsValid(), "Refused valid challenge") test.Assert(t, ChallengeTypeDNS01.IsValid(), "Refused valid challenge")
test.Assert(t, ChallengeTypeTLSALPN01.IsValid(), "Refused valid challenge") test.Assert(t, ChallengeTypeTLSALPN01.IsValid(), "Refused valid challenge")
test.Assert(t, ChallengeTypeDNSAccount01.IsValid(), "Refused valid challenge")
test.Assert(t, !AcmeChallenge("nonsense-71").IsValid(), "Accepted invalid challenge") test.Assert(t, !AcmeChallenge("nonsense-71").IsValid(), "Accepted invalid challenge")
} }

View File

@ -53,15 +53,16 @@ type AcmeChallenge string
// These types are the available challenges // These types are the available challenges
const ( const (
ChallengeTypeHTTP01 = AcmeChallenge("http-01") ChallengeTypeHTTP01 = AcmeChallenge("http-01")
ChallengeTypeDNS01 = AcmeChallenge("dns-01") ChallengeTypeDNS01 = AcmeChallenge("dns-01")
ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01") ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01")
ChallengeTypeDNSAccount01 = AcmeChallenge("dns-account-01")
) )
// IsValid tests whether the challenge is a known challenge // IsValid tests whether the challenge is a known challenge
func (c AcmeChallenge) IsValid() bool { func (c AcmeChallenge) IsValid() bool {
switch c { switch c {
case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01: case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01, ChallengeTypeDNSAccount01:
return true return true
default: default:
return false return false
@ -228,7 +229,7 @@ func (ch Challenge) RecordsSane() bool {
(ch.ValidationRecord[0].AddressUsed == netip.Addr{}) || len(ch.ValidationRecord[0].AddressesResolved) == 0 { (ch.ValidationRecord[0].AddressUsed == netip.Addr{}) || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
return false return false
} }
case ChallengeTypeDNS01: case ChallengeTypeDNS01, ChallengeTypeDNSAccount01:
if len(ch.ValidationRecord) > 1 { if len(ch.ValidationRecord) > 1 {
return false return false
} }

View File

@ -59,7 +59,7 @@ func TestChallengeSanityCheck(t *testing.T) {
}`), &accountKey) }`), &accountKey)
test.AssertNotError(t, err, "Error unmarshaling JWK") test.AssertNotError(t, err, "Error unmarshaling JWK")
types := []AcmeChallenge{ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01} types := []AcmeChallenge{ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01, ChallengeTypeDNSAccount01}
for _, challengeType := range types { for _, challengeType := range types {
chall := Challenge{ chall := Challenge{
Type: challengeType, Type: challengeType,
@ -152,6 +152,8 @@ func TestChallengeStringID(t *testing.T) {
test.AssertEquals(t, ch.StringID(), "iFVMwA") test.AssertEquals(t, ch.StringID(), "iFVMwA")
ch.Type = ChallengeTypeHTTP01 ch.Type = ChallengeTypeHTTP01
test.AssertEquals(t, ch.StringID(), "0Gexug") test.AssertEquals(t, ch.StringID(), "0Gexug")
ch.Type = ChallengeTypeDNSAccount01
test.AssertEquals(t, ch.StringID(), "8z2wSg")
} }
func TestFindChallengeByType(t *testing.T) { func TestFindChallengeByType(t *testing.T) {

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.29.17 github.com/aws/aws-sdk-go-v2/config v1.29.17
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0
github.com/aws/smithy-go v1.22.4 github.com/aws/smithy-go v1.22.4
github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941 github.com/eggsampler/acme/v3 v3.6.2
github.com/go-jose/go-jose/v4 v4.1.0 github.com/go-jose/go-jose/v4 v4.1.0
github.com/go-logr/stdr v1.2.2 github.com/go-logr/stdr v1.2.2
github.com/go-sql-driver/mysql v1.9.1 github.com/go-sql-driver/mysql v1.9.1

4
go.sum
View File

@ -70,8 +70,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941 h1:CnQwymLMJ3MSfjbZQ/bpaLfuXBZuM3LUgAHJ0gO/7d8= github.com/eggsampler/acme/v3 v3.6.2 h1:gvyZbQ92wNQLDASVftGpHEdFwPSfg0+17P0lLt09Tp8=
github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= github.com/eggsampler/acme/v3 v3.6.2/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=

View File

@ -416,15 +416,17 @@ func modelToOrder(om *orderModel) (*corepb.Order, error) {
} }
var challTypeToUint = map[string]uint8{ var challTypeToUint = map[string]uint8{
"http-01": 0, "http-01": 0,
"dns-01": 1, "dns-01": 1,
"tls-alpn-01": 2, "tls-alpn-01": 2,
"dns-account-01": 3,
} }
var uintToChallType = map[uint8]string{ var uintToChallType = map[uint8]string{
0: "http-01", 0: "http-01",
1: "dns-01", 1: "dns-01",
2: "tls-alpn-01", 2: "tls-alpn-01",
3: "dns-account-01",
} }
var identifierTypeToUint = map[string]uint8{ var identifierTypeToUint = map[string]uint8{

View File

@ -2632,6 +2632,36 @@ func TestGetValidAuthorizations2(t *testing.T) {
aaa = am.ID aaa = am.ID
} }
var dac int64
{
tokenStr := core.NewToken()
token, err := base64.RawURLEncoding.DecodeString(tokenStr)
test.AssertNotError(t, err, "computing test authorization challenge token")
profile := "test"
attempted := challTypeToUint[string(core.ChallengeTypeDNSAccount01)]
attemptedAt := fc.Now()
vr, _ := json.Marshal([]core.ValidationRecord{})
am := authzModel{
IdentifierType: identifierTypeToUint[string(identifier.TypeDNS)],
IdentifierValue: "aaa",
RegistrationID: 3,
CertificateProfileName: &profile,
Status: statusToUint[core.StatusValid],
Expires: fc.Now().Add(24 * time.Hour),
Challenges: 1 << challTypeToUint[string(core.ChallengeTypeDNSAccount01)],
Attempted: &attempted,
AttemptedAt: &attemptedAt,
Token: token,
ValidationError: nil,
ValidationRecord: vr,
}
err = sa.dbMap.Insert(context.Background(), &am)
test.AssertNotError(t, err, "failed to insert valid authz with dns-account-01")
dac = am.ID
}
for _, tc := range []struct { for _, tc := range []struct {
name string name string
regID int64 regID int64
@ -2648,6 +2678,14 @@ func TestGetValidAuthorizations2(t *testing.T) {
validUntil: fc.Now().Add(time.Hour), validUntil: fc.Now().Add(time.Hour),
wantIDs: []int64{aaa}, wantIDs: []int64{aaa},
}, },
{
name: "happy path, dns-account-01 challenge",
regID: 3,
identifiers: []*corepb.Identifier{identifier.NewDNS("aaa").ToProto()},
profile: "test",
validUntil: fc.Now().Add(time.Hour),
wantIDs: []int64{dac},
},
{ {
name: "different identifier type", name: "different identifier type",
regID: 1, regID: 1,

View File

@ -60,10 +60,11 @@ boulder_setup:
-git clone --depth 1 https://github.com/letsencrypt/boulder.git $(BOULDER_PATH) -git clone --depth 1 https://github.com/letsencrypt/boulder.git $(BOULDER_PATH)
(cd $(BOULDER_PATH); git checkout -f main && git reset --hard HEAD && git pull -q) (cd $(BOULDER_PATH); git checkout -f main && git reset --hard HEAD && git pull -q)
make boulder_stop make boulder_stop
(cd $(BOULDER_PATH); docker compose run --rm bsetup)
# runs an instance of boulder # runs an instance of boulder
boulder_start: boulder_start:
docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f docker-compose.boulder-temp.yml up -d docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.yml up -d
# waits until boulder responds # waits until boulder responds
boulder_wait: boulder_wait:

2
vendor/modules.txt vendored
View File

@ -140,7 +140,7 @@ github.com/cespare/xxhash/v2
# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
## explicit ## explicit
github.com/dgryski/go-rendezvous github.com/dgryski/go-rendezvous
# github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941 # github.com/eggsampler/acme/v3 v3.6.2
## explicit; go 1.11 ## explicit; go 1.11
github.com/eggsampler/acme/v3 github.com/eggsampler/acme/v3
# github.com/felixge/httpsnoop v1.0.4 # github.com/felixge/httpsnoop v1.0.4