@@ -13,8 +13,7 @@ import (
1313 "github.com/letsencrypt/boulder/identifier"
1414)
1515
16- type dnsPersistIssueValue struct {
17- issuerDomain string
16+ type dnsPersistIssueValueParams struct {
1817 accountURI string
1918 policy string
2019 persistUntil time.Time
@@ -26,111 +25,106 @@ func isWSP(r rune) bool {
2625 return r == '\t' || r == ' '
2726}
2827
29- // parseDNSPersistIssueValue parses the raw parameter segments of an RFC 8659
30- // issue-value from a dns-persist-01 TXT record. It returns an error if any
31- // recognized parameter is malformed or duplicated.
32- func parseDNSPersistIssueValue (issuerDomainName string , paramsRaw []string ) (* dnsPersistIssueValue , error ) {
33- result := & dnsPersistIssueValue {issuerDomain : issuerDomainName }
28+ // parseDNSPersistRecord parses a raw TXT record string per RFC 8659 issue-value
29+ // syntax. It returns the normalized issuer-domain-name and parsed parameters on
30+ // success, or an error if the record is malformed.
31+ func parseDNSPersistRecord (record string ) (string , * dnsPersistIssueValueParams , error ) {
32+ parts := strings .Split (record , ";" )
33+ rawIssuer := strings .TrimFunc (parts [0 ], isWSP )
34+ if rawIssuer == "" {
35+ return "" , nil , errors .New ("empty issuer-domain-name" )
36+ }
37+
38+ normalizedIssuer , err := core .NormalizeIssuerDomainName (rawIssuer )
39+ if err != nil {
40+ return "" , nil , fmt .Errorf ("malformed issuer-domain-name %q: %w" , rawIssuer , err )
41+ }
42+
43+ params := & dnsPersistIssueValueParams {}
3444
3545 seenTags := make (map [string ]bool )
46+ seenTag := func (tag string ) bool {
47+ if seenTags [tag ] {
48+ return true
49+ }
50+ seenTags [tag ] = true
51+ return false
52+ }
3653
37- for _ , param := range paramsRaw {
54+ for _ , param := range parts [ 1 :] {
3855 // Clean optional WSP from the parameter.
3956 param = strings .TrimFunc (param , isWSP )
4057 if param == "" {
41- return nil , errors .New ("empty parameter or trailing semicolon provided" )
58+ return normalizedIssuer , nil , errors .New ("empty parameter or trailing semicolon provided" )
4259 }
4360
4461 // Capture each tag=value pair.
4562 tagValue := strings .SplitN (param , "=" , 2 )
4663 if len (tagValue ) != 2 {
47- return nil , fmt .Errorf ("malformed parameter %q should be tag=value pair" , param )
64+ return normalizedIssuer , nil , fmt .Errorf ("malformed parameter %q should be tag=value pair" , param )
4865 }
4966 tag := strings .TrimFunc (tagValue [0 ], isWSP )
5067 value := strings .TrimFunc (tagValue [1 ], isWSP )
5168 if tag == "" {
52- return nil , fmt .Errorf ("malformed parameter %q, empty tag" , param )
69+ return normalizedIssuer , nil , fmt .Errorf ("malformed parameter %q, empty tag" , param )
5370 }
5471
55- // Per the RFC 8659, matching of tags is case insensitive; canonicalize
56- // before checking whether the tag is recognized.
57- canonicalTag := strings .ToLower (tag )
58-
59- switch canonicalTag {
60- case "accounturi" , "policy" , "persistuntil" :
61- // Recognized tag — fall through to validation below.
62- default :
63- // Per draft-ietf-acme-dns-persist-00, "the server MUST ignore any
64- // parameter within the issue-value that has an unrecognized tag."
65- continue
66- }
67- if seenTags [canonicalTag ] {
68- return nil , fmt .Errorf ("duplicate parameter %q" , tag )
69- }
70- seenTags [canonicalTag ] = true
71-
7272 // Ensure values contain no whitespace, control, or non-ASCII
7373 // characters.
7474 for _ , r := range value {
7575 if (r >= 0x21 && r <= 0x3A ) || (r >= 0x3C && r <= 0x7E ) {
7676 continue
7777 }
78- return nil , fmt .Errorf ("malformed value %q for tag %q" , value , tag )
78+ return normalizedIssuer , nil , fmt .Errorf ("malformed value %q for tag %q" , value , tag )
7979 }
8080
81+ // Per the RFC 8659, matching of tags is case insensitive; canonicalize
82+ // before checking whether the tag is recognized.
83+ canonicalTag := strings .ToLower (tag )
84+
8185 switch canonicalTag {
8286 case "accounturi" :
87+ if seenTag (canonicalTag ) {
88+ return normalizedIssuer , nil , fmt .Errorf ("duplicate parameter %q" , tag )
89+ }
8390 if value == "" {
84- return nil , fmt .Errorf ("empty value provided for mandatory accounturi" )
91+ return normalizedIssuer , nil , fmt .Errorf ("empty value provided for mandatory accounturi" )
8592 }
86- result .accountURI = value
93+ params .accountURI = value
8794
8895 case "policy" :
89- result .policy = value
96+ if seenTag (canonicalTag ) {
97+ return normalizedIssuer , nil , fmt .Errorf ("duplicate parameter %q" , tag )
98+ }
99+ params .policy = value
90100
91101 case "persistuntil" :
102+ if seenTag (canonicalTag ) {
103+ return normalizedIssuer , nil , fmt .Errorf ("duplicate parameter %q" , tag )
104+ }
92105 persistUntilVal , err := strconv .ParseInt (value , 10 , 64 )
93106 if err != nil {
94- return nil , fmt .Errorf ("malformed persistUntil timestamp %q" , value )
107+ return normalizedIssuer , nil , fmt .Errorf ("malformed persistUntil timestamp %q" , value )
95108 }
96- result .persistUntil = time .Unix (persistUntilVal , 0 ).UTC ()
97- }
98- }
99- return result , nil
100- }
109+ params .persistUntil = time .Unix (persistUntilVal , 0 ).UTC ()
101110
102- // parseDNSPersistRecord parses a raw TXT record string into a
103- // dnsPersistIssueValue. It returns ("", nil, nil) if the record's
104- // issuer-domain-name is empty or does not match allowedIssuer.
105- func parseDNSPersistRecord (record string , allowedIssuer string ) (string , * dnsPersistIssueValue , error ) {
106- // Split into issuer-domain-name and parameters per RFC 8659.
107- parts := strings .Split (record , ";" )
108- receivedIssuer := strings .TrimFunc (parts [0 ], isWSP )
109- if receivedIssuer == "" {
110- return "" , nil , nil
111- }
112-
113- normalizedIssuer , err := core .NormalizeIssuerDomainName (receivedIssuer )
114- if err != nil || normalizedIssuer != allowedIssuer {
115- return "" , nil , nil
111+ default :
112+ // Per draft-ietf-acme-dns-persist-00, "the server MUST ignore any
113+ // parameter within the issue-value that has an unrecognized tag."
114+ continue
115+ }
116116 }
117-
118- params , err := parseDNSPersistIssueValue (receivedIssuer , parts [1 :])
119- if err != nil {
120- return receivedIssuer , nil , err
117+ if params .accountURI == "" {
118+ return normalizedIssuer , nil , errors .New ("missing mandatory accounturi parameter" )
121119 }
122- return receivedIssuer , params , nil
120+ return normalizedIssuer , params , nil
123121}
124122
125123// checkDNSPersistRecord checks whether a parsed dns-persist-01 record
126124// authorizes issuance for the given account URI and wildcard status at the
127- // given time. It returns nil if the record authorizes issuance, a
128- // berrors.Malformed error for syntax problems, or a berrors.Unauthorized error
129- // for authorization failures.
130- func checkDNSPersistRecord (params * dnsPersistIssueValue , validAccountURI string , wildcardName bool , validatedAt time.Time ) error {
131- if params .accountURI == "" {
132- return berrors .MalformedError ("missing mandatory accountURI parameter" )
133- }
125+ // given time. It returns nil if the record authorizes issuance, or a
126+ // berrors.Unauthorized error for authorization failures.
127+ func checkDNSPersistRecord (params * dnsPersistIssueValueParams , validAccountURI string , wildcardName bool , validatedAt time.Time ) error {
134128 if params .accountURI != validAccountURI {
135129 return berrors .UnauthorizedError ("accounturi mismatch: expected %q, got %q" , validAccountURI , params .accountURI )
136130 }
@@ -175,26 +169,24 @@ func (va *ValidationAuthorityImpl) validateDNSPersist01(ctx context.Context, ide
175169 for _ , rr := range txts .Final {
176170 record := strings .Join (rr .Txt , "" )
177171
178- receivedIssuer , params , err := parseDNSPersistRecord (record , va . issuerDomain )
172+ receivedIssuer , params , err := parseDNSPersistRecord (record )
179173 if err != nil {
180- syntaxErrs = append (syntaxErrs , fmt .Sprintf (
181- "Parsing DNS-PERSIST-01 challenge TXT record with issuer-domain-name %q: %s" , receivedIssuer , err ))
174+ if receivedIssuer == va .issuerDomain {
175+ syntaxErrs = append (syntaxErrs , fmt .Sprintf (
176+ "Parsing DNS-PERSIST-01 challenge TXT record with issuer-domain-name %q: %s" , receivedIssuer , err ))
177+ }
178+ // Record is malformed, skip.
182179 continue
183180 }
184- if params == nil {
185- // Record didn't match our issuer domain , skip.
181+ if receivedIssuer != va . issuerDomain {
182+ // Record is well-formed but for a different CA , skip.
186183 continue
187184 }
188185
189186 err = checkDNSPersistRecord (params , validAccountURI , wildcardName , validatedAt )
190187 if err != nil {
191- msg := fmt .Sprintf (
192- "Parsing DNS-PERSIST-01 challenge TXT record with issuer-domain-name %q: %s" , receivedIssuer , err )
193- if errors .Is (err , berrors .Malformed ) {
194- syntaxErrs = append (syntaxErrs , msg )
195- } else {
196- authorizationErrs = append (authorizationErrs , msg )
197- }
188+ authorizationErrs = append (authorizationErrs , fmt .Sprintf (
189+ "Checking DNS-PERSIST-01 challenge TXT record with issuer-domain-name %q: %s" , receivedIssuer , err ))
198190 continue
199191 }
200192
0 commit comments