Add support for the draft dns-persist-01 challenge#536
Add support for the draft dns-persist-01 challenge#536beautifulentropy merged 5 commits intomainfrom
Conversation
|
re: linter failure, let's disable the perfsprint linter? Pebble isn't super perf-sensitive and on 1.26.0, fmt.Errorf is equivalent |
6facf40 to
e16ebf7
Compare
e16ebf7 to
bcf9af7
Compare
| // trimWSP trims RFC 8659 whitespace characters (space and tab) from the | ||
| // beginning and end of a string. | ||
| func trimWSP(s string) string { | ||
| return strings.TrimFunc(s, func(r rune) bool { | ||
| return r == ' ' || r == '\t' | ||
| }) | ||
| } | ||
|
|
||
| // splitIssuerDomainName splits an RFC 8659 issue-value into issuer-domain-name | ||
| // and raw parameter segments. It returns zero values when issuer-domain-name is | ||
| // missing. | ||
| func splitIssuerDomainName(raw string) (string, []string) { | ||
| // Split into issuer-domain-name and parameters. | ||
| parts := strings.Split(raw, ";") | ||
| if len(parts) == 0 { | ||
| return "", nil | ||
| } | ||
| // Parse issuer-domain-name. | ||
| issuerDomainName := trimWSP(parts[0]) | ||
| if issuerDomainName == "" { | ||
| return "", nil | ||
| } | ||
| return issuerDomainName, parts[1:] | ||
| } |
There was a problem hiding this comment.
I don't love splitting these two functions like this, but I'm not seeing a better way to peek at the issuer-domain-name, without performing a second split. So, it feels like if we're going to split we might as well return the issuer-domain-name string and the remainder []string. That way we don't have to do that work again. Let me know if you have other alternatives.
jsha
left a comment
There was a problem hiding this comment.
Some optional nits; otherwise good.
| // We know if this record was intended for us but it is malformed, | ||
| // we can continue checking other records but we should report the | ||
| // syntax error if no other record authorizes the challenge. |
There was a problem hiding this comment.
Checking the spec, it seems not fully specified whether a CA should ignore malformed records that are intended for it.
Closest seems to be:
4.1.1. Coexistence of Records
When multiple TXT records are present at the same DNS label (e.g., _validation-persist.example.com), each record functions as an independent authorization for the specified issuer. This follows a similar pattern to CAA records [RFC8659], where multiple records at the same label are permissible.
But honestly I think it would be better to just error on any malformed record that has your issuer; or maybe any malformed record at all. I'll file an issue on the spec. Let's keep the behavior here as-is, pending any spec changes.
There was a problem hiding this comment.
Considering those statements:
Validate Record: If a matching record is found, the CA proceeds to validate it according to the requirements in this specification, including verifying the accounturi and persistUntil parameters.
CAs SHOULD return a malformed error (as defined in [RFC8555]) when the TXT record has invalid syntax, such as duplicate parameters, invalid timestamp format in the persistUntil parameter, missing mandatory accounturi parameter, or other syntactic violations of the record format specified in this document.
I would read that as "all intended records are validated", but you are right that it isn't clear what to do then.
It is a bigger question with regards to how multiple records that are intended for the same CA are handled,
like if there are two records that are applicable, one is expired and other is not, then expired one should be ignored logically,
if there is a wildcard record and a record that isn't a wildcard then for wildcard validation only one of them is applicable,
or there could be two records with different account urls,
and probably doing the same with malformed records just for consistency makes sense.
or there could be two records with different account urls
In particular this is an important use case to allow in my opinion.
kanashimia
left a comment
There was a problem hiding this comment.
I verified that it works on my client, code overall seems okay.
As it seems to me there is no just in time validation as described in https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-persist#name-just-in-time-validation, returning status doesn't seem to be set to "valid".
It is optional, maybe makes sense to implement it too?
For context I relied on it in my client to not print "please set this DNS record to the following value" logs when DNS record is already validated.
|
Just an observation but when there are multiple? records for same CA and one? of them is invalid, for example when uri is wrong, |
This is very interesting! Wrong in what sense, syntax (malformed) or authorization? |
Authorization, like if you add a dns record, then restart pebble without restarting dns server, so you add another record, now there are two records with different accounturis, but I think it may happen in other cases possibly. |
|
Ahh, @beautifulentropy I think it is just because there is the same fixed sized buffer for all records and it overflows (not really an overflow in C sense, but I mean buffer is too small). Ok, from what I see it is due to how miekg/dns handles requests, it seems like a pretty low level library, and it uses 512 UDP buffer by default, it doesn't retry over TCP or sets EDNS0 for you, so I think the solution here is retry over TCP or set Edns0 to a larger value, but I think retrying over TCP is better, it seems challtestsrv needs to be changed too, to include truncate flag. |
|
Ok, here is a patch, please test that there was a problem before and wasn't after applying: Details
As that requires changes to multiple repos please apply yourself, I made it by modifying vendor dir so beware, thanks! Also I don't know the reasoning behind Compress = false there but Truncate can set compression to true. EDIT: EDNS0 advertised udp buffer size shouldn't be used for TCP (duh) from what I understand, I updated the patch here. This patch isn't really specific to dns-persist-01. |
Awesome, if you're open to it, we'd gladly accept a PR for this fix. I'd take the patch and post one myself but I'd rather you get the credit. |
UDP buffer is only 512 bytes by default, it is pretty much never enough for the cases when there are multiple DNS records in a single response, so the responses are cut at arbitrary points which causes problems. This fixes that by truncating responses at record boundaries and setting TC bit, DNS clients should then read TC bit and retry over TCP which allows for a 64KB buffer. See docs: https://pkg.go.dev/github.com/miekg/dns#Msg.Truncate Was originally found in letsencrypt/pebble#536 (comment)
UDP buffer is only 512 bytes by default, it is pretty much never enough for the cases when there are multiple DNS records in a single response, so the responses are cut at arbitrary points which causes problems. This fixes that by truncating responses at record boundaries and setting TC bit, DNS clients should then read TC bit and retry over TCP which allows for a 64KB buffer. See docs: https://pkg.go.dev/github.com/miekg/dns#Msg.Truncate Was originally found in letsencrypt/pebble#536 (comment)
UDP buffer is only 512 bytes by default, it is pretty much never enough for the cases when there are multiple DNS records in a single response, so the responses are cut at arbitrary points which causes problems. This fixes that by truncating responses at record boundaries and setting TC bit, DNS clients should then read TC bit and retry over TCP which allows for a 64KB buffer. Truncate function also enables compression if message doesn't fit in the buffer uncompressed but fits if it is compressed. See docs: https://pkg.go.dev/github.com/miekg/dns#Msg.Truncate Was originally found in letsencrypt/pebble#536 (comment)
TXT records can be quite large, with is a problem for DNS challenges, they don't fit in the 512 buffer especially if there are multiple. DNS servers can increase the buffer size through EDNS0 extension, and retry over TCP if that wasn't enough. DNS server should truncate response if it doesn't fit in the buffer, and set TC (Truncate) bit, then DNS client should retry over TCP. 1220 value for UDP buffer size was chosen based on this recommendation: https://www.isc.org/blogs/dns-flag-day-2020-2/ This changes only TXT request, I left other methods untouched. But it would make sense to do the same logic for all requests, by building some small abstraction around miekg/dns for requests. Also see docs for Exchange function: https://pkg.go.dev/github.com/miekg/dns#Client.Exchange Was originally found in letsencrypt#536 (comment)
TXT records can be quite large, with is a problem for DNS challenges, they don't fit in the 512 buffer especially if there are multiple. DNS servers can increase the buffer size through EDNS0 extension, and retry over TCP if that wasn't enough. DNS server should truncate response if it doesn't fit in the buffer, and set TC (Truncate) bit, then DNS client should retry over TCP. 1220 value for UDP buffer size was chosen based on this recommendation: https://www.isc.org/blogs/dns-flag-day-2020-2/ This changes only TXT request, I left other methods untouched. But it would make sense to do the same logic for all requests, by building some small abstraction around miekg/dns for requests. Also see docs for Exchange function: https://pkg.go.dev/github.com/miekg/dns#Client.Exchange Was originally found in letsencrypt#536 (comment)
TXT records can be quite large, with is a problem for DNS challenges, they don't fit in the 512 buffer especially if there are multiple. DNS servers can increase the buffer size through EDNS0 extension, and retry over TCP if that wasn't enough. For the pebble puposes there isn't much reason to try UDP first, it is possible to just always query DNS records over TCP, so we do. Also see docs for Exchange function: https://pkg.go.dev/github.com/miekg/dns#Client.Exchange Was originally found in letsencrypt#536 (comment)
TXT records can be quite large, with is a problem for DNS challenges, they don't fit in the 512 buffer especially if there are multiple. DNS servers can increase the buffer size through EDNS0 extension, and retry over TCP if that wasn't enough. For the pebble purposes there isn't much reason to try UDP first, it is possible to just always query DNS records over TCP, so we do. Was originally found in letsencrypt#536 (comment)
Adds support for draft specification of the dns-perist-01 ACME challenge type.
Draft specification can be found here and the BRs text covering this challenge can be found here.
Part of letsencrypt/boulder#8527