Skip to content

Commit 67e3acb

Browse files
Addressing comments
1 parent 59f468c commit 67e3acb

2 files changed

Lines changed: 197 additions & 159 deletions

File tree

va/dns_persist.go

Lines changed: 68 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)