Skip to content

Commit 654c1ad

Browse files
committed
Specify UDP-first PQ certificate retrieval
Require PQ-capable clients retrieving certificates over UDP to pad their queries for the complete rollover certificate set. Clients cannot know whether a rotation is in progress, and padding only for the steady-state set would temporarily downgrade them to classical certificates during every rotation. Recommend trying the rollover-sized UDP query first because fragmented UDP works on most paths, with TCP fallback after failure, timeout, or a truncated response.
1 parent 88f386a commit 654c1ad

3 files changed

Lines changed: 67 additions & 6 deletions

File tree

‎draft-denis-dprive-dnscrypt.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ To initiate a DNSCrypt session, a client transmits an ordinary unencrypted `TXT`
457457

458458
A client MAY use UDP or TCP according to local policy.
459459

460-
A common strategy is to try UDP first and retry over TCP after failure, timeout, or truncation.
460+
Clients SHOULD try UDP first and retry over TCP after failure, timeout, or truncation. A PQ-capable client uses the rollover-sized padding target defined in Certificate Retrieval Amplification; although this can cause IP fragmentation, fragmented UDP works on most paths. A client on a path already known to block fragmented UDP MAY use TCP immediately.
461461

462462
A resolver MUST serve certificates over both UDP and TCP; the UDP response MAY be truncated, with the TC flag set, to direct the client to TCP. Deployed clients begin certificate retrieval over UDP and only retry over TCP after a truncated response.
463463

@@ -982,7 +982,7 @@ For compatibility with deployed DNSCrypt v2 clients and resolvers, a resolver MA
982982

983983
A resolver MUST NOT add PQ certificates or other large certificate records to a UDP certificate response unless the complete response is no larger than the request that triggered it.
984984

985-
A client that wants a PQ certificate over UDP therefore pads its certificate query to at least the size of the expected response, exactly as a query that carries a ciphertext is already large enough to cover its response.
985+
A client retrieving PQ certificates over UDP therefore MUST pad its certificate query to at least the size of the expected response, exactly as a query that carries a ciphertext is already large enough to cover its response.
986986

987987
The advertised EDNS(0) {{!RFC6891}} UDP payload size is a fragmentation-avoidance hint and MUST NOT be used as the amplification limit, because a spoofed query can advertise an arbitrarily large buffer while remaining small.
988988

@@ -995,7 +995,7 @@ For relay-compatible PQ deployments, resolvers SHOULD use the following certific
995995

996996
Resolvers SHOULD NOT exceed the rollover certificate set over UDP, and SHOULD keep certificate responses below 4096 octets.
997997

998-
A client cannot tell from outside whether a rotation is in progress, so a client that wants PQ certificates over UDP across rotations SHOULD pad its certificate query to cover the rollover set rather than only the steady-state set; otherwise it silently drops to the classical certificate for the duration of every rollover window.
998+
A client cannot tell from outside whether a rotation is in progress. A client retrieving PQ certificates over UDP therefore MUST pad its certificate query to cover the rollover set rather than only the steady-state set; otherwise it silently drops to the classical certificate for the duration of every rollover window. This padding exceeds common fragmentation-avoidance targets, but clients SHOULD try UDP first because fragmented UDP works on most paths, and retry over TCP only after timeout or truncation.
999999

10001000
If the certificate response containing the PQ set would be larger than the request, the resolver MUST NOT return that oversized PQ response over UDP; it SHOULD instead return the classical certificate response with the TC flag set, so a classical-only client can proceed while a PQ-capable client learns that more certificates may be available.
10011001

@@ -1217,13 +1217,15 @@ If the client connects to the relay over TCP, the relay still forwards the inner
12171217

12181218
Certificate retrieval through a relay is different: the relay forwards the certificate query to the resolver over UDP and forwards a matching certificate response back to the client, and it applies the same response-size check, dropping any response larger than the query it relayed.
12191219

1220-
A client that wants a PQ certificate over UDP through a relay therefore pads its certificate query to at least the size of the expected certificate response, as described in Certificate Retrieval Amplification, so the response passes both the resolver's and the relay's anti-amplification check.
1220+
A client retrieving a PQ certificate over UDP through a relay therefore MUST pad its certificate query to at least the size of the expected certificate response, as described in Certificate Retrieval Amplification, so the response passes both the resolver's and the relay's anti-amplification check.
12211221

12221222
A client that does not, or cannot, pad far enough receives only the certificates that fit within the inner UDP query size, typically a classical certificate with the TC flag set; if even that response is larger than the inner query, the relay drops it.
12231223

12241224
Retrying over TCP to an Anonymized DNSCrypt relay does not make the relay use TCP upstream: relays still forward the inner certificate query to the resolver over UDP.
12251225

1226-
Therefore, a client that retrieves certificates through a relay MUST make the inner certificate query large enough for the expected UDP certificate response, or retrieve certificates directly from the resolver over TCP before using the relay for encrypted queries.
1226+
Nevertheless, carrying a rollover-sized inner certificate query over the client-to-relay TCP connection avoids IP fragmentation on that leg while giving the relay a large enough request to forward upstream and a sufficient anti-amplification budget for the response. The inner query remains EDNS(0)-padded to the expected UDP response size; only its transport to the relay changes.
1227+
1228+
Therefore, after a rollover-sized UDP attempt times out or returns a truncated certificate response, a client retrieving certificates through a relay SHOULD retry over TCP to the relay with an inner query large enough for the expected UDP certificate response. Alternatively, it can retrieve certificates directly from the resolver over TCP before using the relay for encrypted queries, with the associated privacy tradeoff.
12271229

12281230
## PQ Downgrade Protection
12291231

‎reference-implementation/certificate_retrieval.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Sequence
66

77
from constants import (
8+
ANON_MAGIC,
89
CERTIFICATE_RECORD_TTL,
910
DNS_CLASS_IN,
1011
DNS_HEADER_SIZE,
@@ -18,9 +19,13 @@
1819
__all__ = [
1920
"build_certificate_response",
2021
"certificate_query",
22+
"certificate_query_for_transport",
2123
"serve_certificates",
2224
]
2325

26+
ANONYMIZED_DNSCRYPT_HEADER_SIZE = len(ANON_MAGIC) + 16 + 2
27+
CERTIFICATE_ROLLOVER_QUERY_SIZE = 3200
28+
2429

2530
def _dns_name(name: str) -> bytes:
2631
"""Encode a domain name as a sequence of length-prefixed labels."""
@@ -102,7 +107,7 @@ def certificate_query(
102107
) -> bytes:
103108
"""Build an unencrypted TXT certificate query, optionally EDNS(0)-padded.
104109
105-
A client that wants the larger PQ certificates over UDP pads the query to at
110+
A client retrieving the larger PQ certificates over UDP pads the query to at
106111
least the expected response size, so the response stays within the request and
107112
passes the anti-amplification check at the resolver and at any relay.
108113
"""
@@ -130,6 +135,37 @@ def certificate_query(
130135
return header + question + additional
131136

132137

138+
def certificate_query_for_transport(
139+
provider_name: str,
140+
query_id: bytes = b"\x00\x00",
141+
*,
142+
over_tcp: bool,
143+
via_relay: bool,
144+
expected_relay_response_size: int = CERTIFICATE_ROLLOVER_QUERY_SIZE,
145+
udp_payload_size: int = CERTIFICATE_ROLLOVER_QUERY_SIZE,
146+
) -> bytes:
147+
"""Build a certificate query with transport-appropriate padding.
148+
149+
The initial UDP query covers the rollover certificate set, including the
150+
Anonymized DNSCrypt prefix when present. Direct TCP needs no amplification
151+
padding. TCP to a relay still carries a padded inner query because the relay
152+
forwards it upstream over UDP and applies the query length as its response
153+
budget.
154+
"""
155+
156+
if over_tcp:
157+
padded_length = expected_relay_response_size if via_relay else 0
158+
else:
159+
relay_overhead = ANONYMIZED_DNSCRYPT_HEADER_SIZE if via_relay else 0
160+
padded_length = udp_payload_size - relay_overhead
161+
if padded_length <= 0:
162+
raise ValueError("UDP payload size cannot hold the relay header")
163+
query = certificate_query(provider_name, query_id, padded_length)
164+
if not over_tcp and len(query) + relay_overhead > udp_payload_size:
165+
raise ValueError("UDP payload size is too small for the certificate query")
166+
return query
167+
168+
133169
def build_certificate_response(
134170
request: bytes,
135171
certificates: Sequence[bytes],

‎reference-implementation/test_dnscrypt.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,29 @@ def test_certificate_retrieval_amplification(self):
299299
1338,
300300
)
301301

302+
# Initial UDP retrieval covers the rollover set, including the relay prefix.
303+
direct_udp = d.certificate_query_for_transport(
304+
provider_name, over_tcp=False, via_relay=False
305+
)
306+
self.assertEqual(len(direct_udp), 3200)
307+
relay_udp = d.certificate_query_for_transport(
308+
provider_name, over_tcp=False, via_relay=True
309+
)
310+
self.assertEqual(
311+
len(d.anonymized_dnscrypt_query("192.0.2.1", 443, relay_udp)), 3200
312+
)
313+
314+
# Direct TCP needs no padding. TCP to a relay keeps the inner query large
315+
# enough for the relay's UDP response budget.
316+
direct_tcp = d.certificate_query_for_transport(
317+
provider_name, over_tcp=True, via_relay=False
318+
)
319+
self.assertEqual(direct_tcp, base_query)
320+
relay_tcp = d.certificate_query_for_transport(
321+
provider_name, over_tcp=True, via_relay=True
322+
)
323+
self.assertEqual(len(relay_tcp), 3200)
324+
302325
# A query padded past the response carries the PQ certificate over UDP.
303326
padded = d.certificate_query(provider_name, padded_length=1600)
304327
served = d.serve_certificates(padded, [classical], [pq], over_udp=True)

0 commit comments

Comments
 (0)