nx_dns_txt.nx source
↩ module page · 194 lines · 7903 B
1// nx_dns_txt.nx -- TXT + NS DNS response parsing for ACME DNS-01
2// propagation polling (RFC 1035 §3.3.11 NS, §3.3.14 TXT).
3//
4// nx_dns.nx ships an A-record answer-section walker (nx_dns_parse_response_a)
5// but ACME DNS-01 propagation checking needs two more record types:
6// - TXT: is the _acme-challenge value visible yet? (the challenge proof)
7// - NS: who are the AUTHORITATIVE nameservers for the zone? (so we can
8// query them DIRECTLY and dodge a public resolver's stale cache of
9// the previous run's TXT set -- exactly what Let's Encrypt checks).
10//
11// Pure byte parsing over a caller-supplied response buffer -- composes
12// nx_dns's bounded name-skip (anti-cycle) + endian helpers. No syscalls.
13//
14// license_tier: ORIGINAL (composes nx_dns RFC 1035 primitives)
15// genealogy_id: international-research-sources/ietf/rfc_1035
16
17import "nx_syscalls.nx"
18import "nx_dns.nx"
19
20// Sealed verdicts (distinct band so they never collide with NX_DNS_VERDICT_*).
21const NX_DNS_TXT_OK: i64 = 1 // parsed; (caller inspects found flag)
22const NX_DNS_TXT_BAD_HEADER: i64 = 2
23const NX_DNS_TXT_TXID_MISMATCH: i64 = 3
24const NX_DNS_TXT_NOT_RESPONSE: i64 = 4
25const NX_DNS_TXT_TRUNCATED: i64 = 5
26const NX_DNS_TXT_RCODE_NONZERO: i64 = 6 // NXDOMAIN / SERVFAIL / ...
27const NX_DNS_TXT_PARSE_FAIL: i64 = 7
28
29// Walk the answer section of a DNS response and test whether ANY TXT
30// record's content EQUALS want[0..want_len). RFC 1035 §3.3.14: TXT rdata
31// is one-or-more <character-string>s, each = 1 length byte + that many
32// bytes. The ACME _acme-challenge value is a single 43-byte base64url
33// character-string, so we compare each char-string to `want`.
34//
35// Returns NX_DNS_TXT_OK and sets found[0]=1 iff the value is present (0 if
36// the name resolves but the value isn't there yet), or a non-OK verdict on
37// a malformed / NXDOMAIN / spoofed (tx mismatch) response.
38func nx_dns_txt_contains(
39 buf: *u8, n: i64, expected_tx_id: i64,
40 want: *u8, want_len: i64,
41 found: *i64
42) -> i64 {
43 found[0] = 0
44 if n < NX_DNS_HEADER_LEN { return NX_DNS_TXT_BAD_HEADER }
45 let tx: i64 = nx_dns_get_u16_be(buf, 0)
46 if tx != (expected_tx_id & 0xffff) { return NX_DNS_TXT_TXID_MISMATCH }
47 let flags: i64 = nx_dns_get_u16_be(buf, 2)
48 if (flags & NX_DNS_FLAG_QR) == 0 { return NX_DNS_TXT_NOT_RESPONSE }
49 if (flags & NX_DNS_FLAG_TC) != 0 { return NX_DNS_TXT_TRUNCATED }
50 let rcode: i64 = flags & NX_DNS_RCODE_MASK
51 if rcode != 0 { return NX_DNS_TXT_RCODE_NONZERO }
52 let qdcount: i64 = nx_dns_get_u16_be(buf, 4)
53 let ancount: i64 = nx_dns_get_u16_be(buf, 6)
54 if qdcount > NX_DNS_MAX_RECORDS { return NX_DNS_TXT_BAD_HEADER }
55 if ancount > NX_DNS_MAX_RECORDS { return NX_DNS_TXT_BAD_HEADER }
56
57 var o: i64 = NX_DNS_HEADER_LEN
58 // skip the question section
59 var qi: i64 = 0
60 while qi < qdcount {
61 let an: i64 = nx_dns_skip_name(buf, n, o)
62 if an < 0 { return NX_DNS_TXT_PARSE_FAIL }
63 if an + 4 > n { return NX_DNS_TXT_BAD_HEADER }
64 o = an + 4
65 qi = qi + 1
66 }
67 // walk the answers
68 var ai: i64 = 0
69 while ai < ancount {
70 let an2: i64 = nx_dns_skip_name(buf, n, o)
71 if an2 < 0 { return NX_DNS_TXT_PARSE_FAIL }
72 if an2 + 10 > n { return NX_DNS_TXT_BAD_HEADER }
73 let rtype: i64 = nx_dns_get_u16_be(buf, an2)
74 let rdlength: i64 = nx_dns_get_u16_be(buf, an2 + 8)
75 let rdata_off: i64 = an2 + 10
76 if rdata_off + rdlength > n { return NX_DNS_TXT_BAD_HEADER }
77 if rtype == NX_DNS_TYPE_TXT {
78 // iterate the character-strings inside this TXT rdata
79 var cs: i64 = rdata_off
80 let rdend: i64 = rdata_off + rdlength
81 while cs < rdend {
82 let slen: i64 = buf[cs] & 0xff
83 let sstart: i64 = cs + 1
84 if sstart + slen > rdend { cs = rdend } // malformed -> stop this rdata
85 else {
86 if slen == want_len {
87 var eq: i64 = 1
88 var k: i64 = 0
89 while k < want_len {
90 if (buf[sstart + k] & 0xff) != (want[k] & 0xff) { eq = 0; k = want_len }
91 else { k = k + 1 }
92 }
93 if eq == 1 { found[0] = 1 }
94 }
95 cs = sstart + slen
96 }
97 }
98 }
99 o = rdata_off + rdlength
100 ai = ai + 1
101 }
102 return NX_DNS_TXT_OK
103}
104
105// Extract the FIRST NS hostname from a response's ANSWER or AUTHORITY
106// section into out (NUL-terminated, dotted). RFC 1035 §3.3.11: NS rdata is
107// a single <domain-name> (may use compression pointers back into the
108// packet). Returns the hostname length (>0) or 0 if none / malformed.
109//
110// We decode the name manually (following one level of compression) rather
111// than reuse skip_name, because we need the BYTES, not just the end offset.
112func nx_dns_decode_name_at(buf: *u8, n: i64, start: i64, out: *u8, out_cap: i64) -> i64 {
113 var o: i64 = 0
114 var cur: i64 = start
115 var hops: i64 = 0
116 var first_label: i64 = 1
117 while hops < NX_DNS_MAX_PTR_HOPS {
118 if cur < 0 { return 0 }
119 if cur >= n { return 0 }
120 let b: i64 = buf[cur] & 0xff
121 if b == 0 { return o } // end of name
122 let high2: i64 = b & 0xc0
123 if high2 == 0xc0 {
124 if cur + 1 >= n { return 0 }
125 let lo: i64 = buf[cur + 1] & 0xff
126 cur = ((b & 0x3f) << 8) | lo
127 hops = hops + 1
128 } else {
129 if high2 != 0 { return 0 } // reserved label type
130 if b > 63 { return 0 }
131 if cur + 1 + b > n { return 0 }
132 if first_label == 0 { if o < out_cap { out[o] = 0x2e as u8; o = o + 1 } } // '.'
133 first_label = 0
134 var i: i64 = 0
135 while i < b {
136 if o < out_cap - 1 { out[o] = buf[cur + 1 + i]; o = o + 1 }
137 i = i + 1
138 }
139 cur = cur + 1 + b
140 }
141 }
142 return 0 // pointer loop budget exhausted
143}
144
145func nx_dns_first_ns(
146 buf: *u8, n: i64, expected_tx_id: i64,
147 out: *u8, out_cap: i64
148) -> i64 {
149 if n < NX_DNS_HEADER_LEN { return 0 }
150 let tx: i64 = nx_dns_get_u16_be(buf, 0)
151 if tx != (expected_tx_id & 0xffff) { return 0 }
152 let flags: i64 = nx_dns_get_u16_be(buf, 2)
153 if (flags & NX_DNS_FLAG_QR) == 0 { return 0 }
154 let qdcount: i64 = nx_dns_get_u16_be(buf, 4)
155 let ancount: i64 = nx_dns_get_u16_be(buf, 6)
156 let nscount: i64 = nx_dns_get_u16_be(buf, 8)
157 if qdcount > NX_DNS_MAX_RECORDS { return 0 }
158 if ancount > NX_DNS_MAX_RECORDS { return 0 }
159 if nscount > NX_DNS_MAX_RECORDS { return 0 }
160
161 var o: i64 = NX_DNS_HEADER_LEN
162 var qi: i64 = 0
163 while qi < qdcount {
164 let an: i64 = nx_dns_skip_name(buf, n, o)
165 if an < 0 { return 0 }
166 if an + 4 > n { return 0 }
167 o = an + 4
168 qi = qi + 1
169 }
170 // Scan ANSWER then AUTHORITY records for the first NS rdata.
171 let total: i64 = ancount + nscount
172 var ri: i64 = 0
173 while ri < total {
174 let an2: i64 = nx_dns_skip_name(buf, n, o)
175 if an2 < 0 { return 0 }
176 if an2 + 10 > n { return 0 }
177 let rtype: i64 = nx_dns_get_u16_be(buf, an2)
178 let rdlength: i64 = nx_dns_get_u16_be(buf, an2 + 8)
179 let rdata_off: i64 = an2 + 10
180 if rdata_off + rdlength > n { return 0 }
181 if rtype == NX_DNS_TYPE_NS {
182 let nl: i64 = nx_dns_decode_name_at(buf, n, rdata_off, out, out_cap)
183 if nl > 0 { return nl }
184 }
185 o = rdata_off + rdlength
186 ri = ri + 1
187 }
188 return 0
189}
190
191// Compile-only smoke. Real proof in nx_acme_dns01_propagation_gate.nx.
192func main() -> i64 {
193 return 0
194}