/src/systemd/src/resolve/resolved-dns-trust-anchor.c
Line | Count | Source |
1 | | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | | |
3 | | #include "sd-messages.h" |
4 | | |
5 | | #include "alloc-util.h" |
6 | | #include "conf-files.h" |
7 | | #include "constants.h" |
8 | | #include "dns-answer.h" |
9 | | #include "dns-domain.h" |
10 | | #include "dns-rr.h" |
11 | | #include "extract-word.h" |
12 | | #include "fd-util.h" |
13 | | #include "fileio.h" |
14 | | #include "hexdecoct.h" |
15 | | #include "log.h" |
16 | | #include "nulstr-util.h" |
17 | | #include "parse-util.h" |
18 | | #include "resolved-dns-dnssec.h" |
19 | | #include "resolved-dns-trust-anchor.h" |
20 | | #include "set.h" |
21 | | #include "string-util.h" |
22 | | #include "strv.h" |
23 | | |
24 | | DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( |
25 | | dns_answer_hash_ops_by_key, |
26 | | DnsResourceKey, |
27 | | dns_resource_key_hash_func, |
28 | | dns_resource_key_compare_func, |
29 | | DnsAnswer, |
30 | | dns_answer_unref); |
31 | | |
32 | | static const char trust_anchor_dirs[] = CONF_PATHS_NULSTR("dnssec-trust-anchors.d"); |
33 | | |
34 | | /* The second DS RR from https://data.iana.org/root-anchors/root-anchors.xml, retrieved February 2017 */ |
35 | | static const uint8_t root_digest2[] = |
36 | | { 0xE0, 0x6D, 0x44, 0xB8, 0x0B, 0x8F, 0x1D, 0x39, 0xA9, 0x5C, 0x0B, 0x0D, 0x7C, 0x65, 0xD0, 0x84, |
37 | | 0x58, 0xE8, 0x80, 0x40, 0x9B, 0xBC, 0x68, 0x34, 0x57, 0x10, 0x42, 0x37, 0xC7, 0xF8, 0xEC, 0x8D }; |
38 | | |
39 | | static const uint8_t root_digest3[] = |
40 | | { 0x68, 0x3D, 0x2D, 0x0A, 0xCB, 0x8C, 0x9B, 0x71, 0x2A, 0x19, 0x48, 0xB2, 0x7F, 0x74, 0x12, 0x19, |
41 | | 0x29, 0x8D, 0x0A, 0x45, 0x0D, 0x61, 0x2C, 0x48, 0x3A, 0xF4, 0x44, 0xA4, 0xC0, 0xFB, 0x2B, 0x16 }; |
42 | | |
43 | 0 | static bool dns_trust_anchor_knows_domain_positive(DnsTrustAnchor *d, const char *name) { |
44 | 0 | assert(d); |
45 | | |
46 | | /* Returns true if there's an entry for the specified domain |
47 | | * name in our trust anchor */ |
48 | |
|
49 | 0 | return |
50 | 0 | hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DNSKEY, name)) || |
51 | 0 | hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DS, name)); |
52 | 0 | } |
53 | | |
54 | | static int add_root_ksk( |
55 | | DnsAnswer *answer, |
56 | | DnsResourceKey *key, |
57 | | uint16_t key_tag, |
58 | | uint8_t algorithm, |
59 | | uint8_t digest_type, |
60 | | const void *digest, |
61 | 0 | size_t digest_size) { |
62 | |
|
63 | 0 | _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; |
64 | 0 | int r; |
65 | |
|
66 | 0 | rr = dns_resource_record_new(key); |
67 | 0 | if (!rr) |
68 | 0 | return -ENOMEM; |
69 | | |
70 | 0 | rr->ds.key_tag = key_tag; |
71 | 0 | rr->ds.algorithm = algorithm; |
72 | 0 | rr->ds.digest_type = digest_type; |
73 | 0 | rr->ds.digest_size = digest_size; |
74 | 0 | rr->ds.digest = memdup(digest, rr->ds.digest_size); |
75 | 0 | if (!rr->ds.digest) |
76 | 0 | return -ENOMEM; |
77 | | |
78 | 0 | r = dns_answer_add(answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL); |
79 | 0 | if (r < 0) |
80 | 0 | return r; |
81 | | |
82 | 0 | return 0; |
83 | 0 | } |
84 | | |
85 | 0 | static int dns_trust_anchor_add_builtin_positive(DnsTrustAnchor *d) { |
86 | 0 | _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; |
87 | 0 | _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; |
88 | 0 | int r; |
89 | |
|
90 | 0 | assert(d); |
91 | | |
92 | | /* Only add the built-in trust anchor if there's neither a DS nor a DNSKEY defined for the root domain. That |
93 | | * way users have an easy way to override the root domain DS/DNSKEY data. */ |
94 | 0 | if (dns_trust_anchor_knows_domain_positive(d, ".")) |
95 | 0 | return 0; |
96 | | |
97 | 0 | key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_DS, ""); |
98 | 0 | if (!key) |
99 | 0 | return -ENOMEM; |
100 | | |
101 | 0 | answer = dns_answer_new(2); |
102 | 0 | if (!answer) |
103 | 0 | return -ENOMEM; |
104 | | |
105 | | /* Add the currently valid RRs from https://data.iana.org/root-anchors/root-anchors.xml */ |
106 | 0 | r = add_root_ksk(answer, key, 20326, DNSSEC_ALGORITHM_RSASHA256, DNSSEC_DIGEST_SHA256, root_digest2, sizeof(root_digest2)); |
107 | 0 | if (r < 0) |
108 | 0 | return r; |
109 | 0 | r = add_root_ksk(answer, key, 38696, DNSSEC_ALGORITHM_RSASHA256, DNSSEC_DIGEST_SHA256, root_digest3, sizeof(root_digest3)); |
110 | 0 | if (r < 0) |
111 | 0 | return r; |
112 | | |
113 | 0 | r = hashmap_ensure_put(&d->positive_by_key, &dns_answer_hash_ops_by_key, key, answer); |
114 | 0 | if (r < 0) |
115 | 0 | return r; |
116 | | |
117 | 0 | TAKE_PTR(answer); |
118 | 0 | return 0; |
119 | 0 | } |
120 | | |
121 | 0 | static int dns_trust_anchor_add_builtin_negative(DnsTrustAnchor *d) { |
122 | |
|
123 | 0 | static const char private_domains[] = |
124 | | /* RFC 6761 says that .test is a special domain for |
125 | | * testing and not to be installed in the root zone */ |
126 | 0 | "test\0" |
127 | | |
128 | | /* RFC 6761 says that these reverse IP lookup ranges |
129 | | * are for private addresses, and hence should not |
130 | | * show up in the root zone */ |
131 | 0 | "10.in-addr.arpa\0" |
132 | 0 | "16.172.in-addr.arpa\0" |
133 | 0 | "17.172.in-addr.arpa\0" |
134 | 0 | "18.172.in-addr.arpa\0" |
135 | 0 | "19.172.in-addr.arpa\0" |
136 | 0 | "20.172.in-addr.arpa\0" |
137 | 0 | "21.172.in-addr.arpa\0" |
138 | 0 | "22.172.in-addr.arpa\0" |
139 | 0 | "23.172.in-addr.arpa\0" |
140 | 0 | "24.172.in-addr.arpa\0" |
141 | 0 | "25.172.in-addr.arpa\0" |
142 | 0 | "26.172.in-addr.arpa\0" |
143 | 0 | "27.172.in-addr.arpa\0" |
144 | 0 | "28.172.in-addr.arpa\0" |
145 | 0 | "29.172.in-addr.arpa\0" |
146 | 0 | "30.172.in-addr.arpa\0" |
147 | 0 | "31.172.in-addr.arpa\0" |
148 | 0 | "168.192.in-addr.arpa\0" |
149 | | |
150 | | /* The same, but for IPv6. */ |
151 | 0 | "d.f.ip6.arpa\0" |
152 | | |
153 | | /* RFC 6762 reserves the .local domain for Multicast |
154 | | * DNS, it hence cannot appear in the root zone. (Note |
155 | | * that we by default do not route .local traffic to |
156 | | * DNS anyway, except when a configured search domain |
157 | | * suggests so.) */ |
158 | 0 | "local\0" |
159 | | |
160 | | /* These two are well known, popular private zone |
161 | | * TLDs, that are blocked from delegation, according |
162 | | * to: |
163 | | * http://icannwiki.com/Name_Collision#NGPC_Resolution |
164 | | * |
165 | | * There's also ongoing work on making this official |
166 | | * in an RRC: |
167 | | * https://www.ietf.org/archive/id/draft-chapin-additional-reserved-tlds-02.txt */ |
168 | 0 | "home\0" |
169 | 0 | "corp\0" |
170 | | |
171 | | /* The following four TLDs are suggested for private |
172 | | * zones in RFC 6762, Appendix G, and are hence very |
173 | | * unlikely to be made official TLDs any day soon */ |
174 | 0 | "lan\0" |
175 | 0 | "intranet\0" |
176 | 0 | "internal\0" |
177 | 0 | "private\0" |
178 | | |
179 | | /* Defined by RFC 8375. The most official choice. */ |
180 | 0 | "home.arpa\0" |
181 | | |
182 | | /* RFC 9462 doesn't mention DNSSEC, but this domain |
183 | | * can't really be signed and clients need to validate |
184 | | * the answer before using it anyway. */ |
185 | 0 | "resolver.arpa\0" |
186 | | |
187 | | /* RFC 8880 says because the 'ipv4only.arpa' zone has to |
188 | | * be an insecure delegation, DNSSEC cannot be used to |
189 | | * protect these answers from tampering by malicious |
190 | | * devices on the path */ |
191 | 0 | "ipv4only.arpa\0" |
192 | 0 | "170.0.0.192.in-addr.arpa\0" |
193 | 0 | "171.0.0.192.in-addr.arpa\0"; |
194 | |
|
195 | 0 | int r; |
196 | |
|
197 | 0 | assert(d); |
198 | | |
199 | | /* Only add the built-in trust anchor if there's no negative |
200 | | * trust anchor defined at all. This enables easy overriding |
201 | | * of negative trust anchors. */ |
202 | |
|
203 | 0 | if (!set_isempty(d->negative_by_name)) |
204 | 0 | return 0; |
205 | | |
206 | | /* We add a couple of domains as default negative trust |
207 | | * anchors, where it's very unlikely they will be installed in |
208 | | * the root zone. If they exist they must be private, and thus |
209 | | * unsigned. */ |
210 | | |
211 | 0 | NULSTR_FOREACH(name, private_domains) { |
212 | 0 | if (dns_trust_anchor_knows_domain_positive(d, name)) |
213 | 0 | continue; |
214 | | |
215 | 0 | r = set_put_strdup_full(&d->negative_by_name, &dns_name_hash_ops_free, name); |
216 | 0 | if (r < 0) |
217 | 0 | return r; |
218 | 0 | } |
219 | | |
220 | 0 | return 0; |
221 | 0 | } |
222 | | |
223 | 0 | static int dns_trust_anchor_load_positive(DnsTrustAnchor *d, const char *path, unsigned line, const char *s) { |
224 | 0 | _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; |
225 | 0 | _cleanup_free_ char *domain = NULL, *class = NULL, *type = NULL; |
226 | 0 | _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; |
227 | 0 | DnsAnswer *old_answer = NULL; |
228 | 0 | const char *p = s; |
229 | 0 | int r; |
230 | |
|
231 | 0 | assert(d); |
232 | 0 | assert(line); |
233 | |
|
234 | 0 | r = extract_first_word(&p, &domain, NULL, EXTRACT_UNQUOTE); |
235 | 0 | if (r < 0) |
236 | 0 | return log_warning_errno(r, "Unable to parse domain in line %s:%u: %m", path, line); |
237 | | |
238 | 0 | r = dns_name_is_valid(domain); |
239 | 0 | if (r < 0) |
240 | 0 | return log_warning_errno(r, "Failed to check validity of domain name '%s', at line %s:%u, ignoring line: %m", domain, path, line); |
241 | 0 | if (r == 0) { |
242 | 0 | log_warning("Domain name %s is invalid, at line %s:%u, ignoring line.", domain, path, line); |
243 | 0 | return -EINVAL; |
244 | 0 | } |
245 | | |
246 | 0 | r = extract_many_words(&p, NULL, 0, &class, &type); |
247 | 0 | if (r < 0) |
248 | 0 | return log_warning_errno(r, "Unable to parse class and type in line %s:%u: %m", path, line); |
249 | 0 | if (r != 2) { |
250 | 0 | log_warning("Missing class or type in line %s:%u", path, line); |
251 | 0 | return -EINVAL; |
252 | 0 | } |
253 | | |
254 | 0 | if (!strcaseeq(class, "IN")) { |
255 | 0 | log_warning("RR class %s is not supported, ignoring line %s:%u.", class, path, line); |
256 | 0 | return -EINVAL; |
257 | 0 | } |
258 | | |
259 | 0 | if (strcaseeq(type, "DS")) { |
260 | 0 | _cleanup_free_ char *key_tag = NULL, *algorithm = NULL, *digest_type = NULL; |
261 | 0 | _cleanup_free_ void *dd = NULL; |
262 | 0 | uint16_t kt; |
263 | 0 | int a, dt; |
264 | 0 | size_t l; |
265 | |
|
266 | 0 | r = extract_many_words(&p, NULL, 0, &key_tag, &algorithm, &digest_type); |
267 | 0 | if (r < 0) { |
268 | 0 | log_warning_errno(r, "Failed to parse DS parameters on line %s:%u: %m", path, line); |
269 | 0 | return -EINVAL; |
270 | 0 | } |
271 | 0 | if (r != 3) { |
272 | 0 | log_warning("Missing DS parameters on line %s:%u", path, line); |
273 | 0 | return -EINVAL; |
274 | 0 | } |
275 | | |
276 | 0 | r = safe_atou16(key_tag, &kt); |
277 | 0 | if (r < 0) |
278 | 0 | return log_warning_errno(r, "Failed to parse DS key tag %s on line %s:%u: %m", key_tag, path, line); |
279 | | |
280 | 0 | a = dnssec_algorithm_from_string(algorithm); |
281 | 0 | if (a < 0) { |
282 | 0 | log_warning("Failed to parse DS algorithm %s on line %s:%u", algorithm, path, line); |
283 | 0 | return -EINVAL; |
284 | 0 | } |
285 | | |
286 | 0 | dt = dnssec_digest_from_string(digest_type); |
287 | 0 | if (dt < 0) { |
288 | 0 | log_warning("Failed to parse DS digest type %s on line %s:%u", digest_type, path, line); |
289 | 0 | return -EINVAL; |
290 | 0 | } |
291 | | |
292 | 0 | if (isempty(p)) { |
293 | 0 | log_warning("Missing DS digest on line %s:%u", path, line); |
294 | 0 | return -EINVAL; |
295 | 0 | } |
296 | | |
297 | 0 | r = unhexmem(p, &dd, &l); |
298 | 0 | if (r < 0) { |
299 | 0 | log_warning("Failed to parse DS digest %s on line %s:%u", p, path, line); |
300 | 0 | return -EINVAL; |
301 | 0 | } |
302 | | |
303 | 0 | rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, domain); |
304 | 0 | if (!rr) |
305 | 0 | return log_oom(); |
306 | | |
307 | 0 | rr->ds.key_tag = kt; |
308 | 0 | rr->ds.algorithm = a; |
309 | 0 | rr->ds.digest_type = dt; |
310 | 0 | rr->ds.digest_size = l; |
311 | 0 | rr->ds.digest = TAKE_PTR(dd); |
312 | |
|
313 | 0 | } else if (strcaseeq(type, "DNSKEY")) { |
314 | 0 | _cleanup_free_ char *flags = NULL, *protocol = NULL, *algorithm = NULL; |
315 | 0 | _cleanup_free_ void *k = NULL; |
316 | 0 | uint16_t f; |
317 | 0 | size_t l; |
318 | 0 | int a; |
319 | |
|
320 | 0 | r = extract_many_words(&p, NULL, 0, &flags, &protocol, &algorithm); |
321 | 0 | if (r < 0) |
322 | 0 | return log_warning_errno(r, "Failed to parse DNSKEY parameters on line %s:%u: %m", path, line); |
323 | 0 | if (r != 3) { |
324 | 0 | log_warning("Missing DNSKEY parameters on line %s:%u", path, line); |
325 | 0 | return -EINVAL; |
326 | 0 | } |
327 | | |
328 | 0 | if (!streq(protocol, "3")) { |
329 | 0 | log_warning("DNSKEY Protocol is not 3 on line %s:%u", path, line); |
330 | 0 | return -EINVAL; |
331 | 0 | } |
332 | | |
333 | 0 | r = safe_atou16(flags, &f); |
334 | 0 | if (r < 0) |
335 | 0 | return log_warning_errno(r, "Failed to parse DNSKEY flags field %s on line %s:%u", flags, path, line); |
336 | 0 | if ((f & DNSKEY_FLAG_ZONE_KEY) == 0) { |
337 | 0 | log_warning("DNSKEY lacks zone key bit set on line %s:%u", path, line); |
338 | 0 | return -EINVAL; |
339 | 0 | } |
340 | 0 | if ((f & DNSKEY_FLAG_REVOKE)) { |
341 | 0 | log_warning("DNSKEY is already revoked on line %s:%u", path, line); |
342 | 0 | return -EINVAL; |
343 | 0 | } |
344 | | |
345 | 0 | a = dnssec_algorithm_from_string(algorithm); |
346 | 0 | if (a < 0) { |
347 | 0 | log_warning("Failed to parse DNSKEY algorithm %s on line %s:%u", algorithm, path, line); |
348 | 0 | return -EINVAL; |
349 | 0 | } |
350 | | |
351 | 0 | if (isempty(p)) { |
352 | 0 | log_warning("Missing DNSKEY key on line %s:%u", path, line); |
353 | 0 | return -EINVAL; |
354 | 0 | } |
355 | | |
356 | 0 | r = unbase64mem(p, &k, &l); |
357 | 0 | if (r < 0) |
358 | 0 | return log_warning_errno(r, "Failed to parse DNSKEY key data %s on line %s:%u", p, path, line); |
359 | | |
360 | 0 | rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, domain); |
361 | 0 | if (!rr) |
362 | 0 | return log_oom(); |
363 | | |
364 | 0 | rr->dnskey.flags = f; |
365 | 0 | rr->dnskey.protocol = 3; |
366 | 0 | rr->dnskey.algorithm = a; |
367 | 0 | rr->dnskey.key_size = l; |
368 | 0 | rr->dnskey.key = TAKE_PTR(k); |
369 | |
|
370 | 0 | } else { |
371 | 0 | log_warning("RR type %s is not supported, ignoring line %s:%u.", type, path, line); |
372 | 0 | return -EINVAL; |
373 | 0 | } |
374 | | |
375 | 0 | r = hashmap_ensure_allocated(&d->positive_by_key, &dns_answer_hash_ops_by_key); |
376 | 0 | if (r < 0) |
377 | 0 | return log_oom(); |
378 | | |
379 | 0 | old_answer = hashmap_get(d->positive_by_key, rr->key); |
380 | 0 | answer = dns_answer_ref(old_answer); |
381 | |
|
382 | 0 | r = dns_answer_add_extend(&answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL); |
383 | 0 | if (r < 0) |
384 | 0 | return log_error_errno(r, "Failed to add trust anchor RR: %m"); |
385 | | |
386 | 0 | r = hashmap_replace(d->positive_by_key, rr->key, answer); |
387 | 0 | if (r < 0) |
388 | 0 | return log_error_errno(r, "Failed to add answer to trust anchor: %m"); |
389 | | |
390 | 0 | old_answer = dns_answer_unref(old_answer); |
391 | 0 | TAKE_PTR(answer); |
392 | |
|
393 | 0 | return 0; |
394 | 0 | } |
395 | | |
396 | 0 | static int dns_trust_anchor_load_negative(DnsTrustAnchor *d, const char *path, unsigned line, const char *s) { |
397 | 0 | _cleanup_free_ char *domain = NULL; |
398 | 0 | const char *p = s; |
399 | 0 | int r; |
400 | |
|
401 | 0 | assert(d); |
402 | 0 | assert(line); |
403 | |
|
404 | 0 | r = extract_first_word(&p, &domain, NULL, EXTRACT_UNQUOTE); |
405 | 0 | if (r < 0) |
406 | 0 | return log_warning_errno(r, "Unable to parse line %s:%u: %m", path, line); |
407 | | |
408 | 0 | r = dns_name_is_valid(domain); |
409 | 0 | if (r < 0) |
410 | 0 | return log_warning_errno(r, "Failed to check validity of domain name '%s', at line %s:%u, ignoring line: %m", domain, path, line); |
411 | 0 | if (r == 0) { |
412 | 0 | log_warning("Domain name %s is invalid, at line %s:%u, ignoring line.", domain, path, line); |
413 | 0 | return -EINVAL; |
414 | 0 | } |
415 | | |
416 | 0 | if (!isempty(p)) { |
417 | 0 | log_warning("Trailing garbage at line %s:%u, ignoring line.", path, line); |
418 | 0 | return -EINVAL; |
419 | 0 | } |
420 | | |
421 | 0 | r = set_ensure_consume(&d->negative_by_name, &dns_name_hash_ops_free, TAKE_PTR(domain)); |
422 | 0 | if (r < 0) |
423 | 0 | return log_oom(); |
424 | | |
425 | 0 | return 0; |
426 | 0 | } |
427 | | |
428 | | static int dns_trust_anchor_load_files( |
429 | | DnsTrustAnchor *d, |
430 | | const char *suffix, |
431 | 0 | int (*loader)(DnsTrustAnchor *d, const char *path, unsigned n, const char *line)) { |
432 | |
|
433 | 0 | _cleanup_strv_free_ char **files = NULL; |
434 | 0 | int r; |
435 | |
|
436 | 0 | assert(d); |
437 | 0 | assert(suffix); |
438 | 0 | assert(loader); |
439 | |
|
440 | 0 | r = conf_files_list_nulstr(&files, suffix, NULL, 0, trust_anchor_dirs); |
441 | 0 | if (r < 0) |
442 | 0 | return log_error_errno(r, "Failed to enumerate %s trust anchor files: %m", suffix); |
443 | | |
444 | 0 | STRV_FOREACH(f, files) { |
445 | 0 | _cleanup_fclose_ FILE *g = NULL; |
446 | 0 | unsigned n = 0; |
447 | |
|
448 | 0 | g = fopen(*f, "re"); |
449 | 0 | if (!g) { |
450 | 0 | if (errno == ENOENT) |
451 | 0 | continue; |
452 | | |
453 | 0 | log_warning_errno(errno, "Failed to open '%s', ignoring: %m", *f); |
454 | 0 | continue; |
455 | 0 | } |
456 | | |
457 | 0 | for (;;) { |
458 | 0 | _cleanup_free_ char *line = NULL; |
459 | |
|
460 | 0 | r = read_stripped_line(g, LONG_LINE_MAX, &line); |
461 | 0 | if (r < 0) { |
462 | 0 | log_warning_errno(r, "Failed to read '%s', ignoring: %m", *f); |
463 | 0 | break; |
464 | 0 | } |
465 | 0 | if (r == 0) |
466 | 0 | break; |
467 | | |
468 | 0 | n++; |
469 | |
|
470 | 0 | if (isempty(line)) |
471 | 0 | continue; |
472 | | |
473 | 0 | if (*line == ';') |
474 | 0 | continue; |
475 | | |
476 | 0 | (void) loader(d, *f, n, line); |
477 | 0 | } |
478 | 0 | } |
479 | |
|
480 | 0 | return 0; |
481 | 0 | } |
482 | | |
483 | 0 | static int dns_trust_anchor_dump(DnsTrustAnchor *d) { |
484 | 0 | DnsAnswer *a; |
485 | |
|
486 | 0 | assert(d); |
487 | |
|
488 | 0 | if (hashmap_isempty(d->positive_by_key)) |
489 | 0 | log_info("No positive trust anchors defined."); |
490 | 0 | else { |
491 | 0 | log_info("Positive Trust Anchors:"); |
492 | 0 | HASHMAP_FOREACH(a, d->positive_by_key) { |
493 | 0 | DnsResourceRecord *rr; |
494 | |
|
495 | 0 | DNS_ANSWER_FOREACH(rr, a) |
496 | 0 | log_info("%s", dns_resource_record_to_string(rr)); |
497 | 0 | } |
498 | 0 | } |
499 | |
|
500 | 0 | if (set_isempty(d->negative_by_name)) |
501 | 0 | log_info("No negative trust anchors defined."); |
502 | 0 | else { |
503 | 0 | _cleanup_free_ char **l = NULL, *j = NULL; |
504 | |
|
505 | 0 | if (set_dump_sorted(d->negative_by_name, (void***) &l, /* ret_n = */ NULL) < 0) |
506 | 0 | return log_oom(); |
507 | | |
508 | 0 | j = strv_join(l, " "); |
509 | 0 | if (!j) |
510 | 0 | return log_oom(); |
511 | | |
512 | 0 | log_info("Negative trust anchors: %s", j); |
513 | 0 | } |
514 | | |
515 | 0 | return 0; |
516 | 0 | } |
517 | | |
518 | 0 | int dns_trust_anchor_load(DnsTrustAnchor *d) { |
519 | 0 | int r; |
520 | |
|
521 | 0 | assert(d); |
522 | | |
523 | | /* If loading things from disk fails, we don't consider this fatal */ |
524 | 0 | (void) dns_trust_anchor_load_files(d, ".positive", dns_trust_anchor_load_positive); |
525 | 0 | (void) dns_trust_anchor_load_files(d, ".negative", dns_trust_anchor_load_negative); |
526 | | |
527 | | /* However, if the built-in DS fails, then we have a problem. */ |
528 | 0 | r = dns_trust_anchor_add_builtin_positive(d); |
529 | 0 | if (r < 0) |
530 | 0 | return log_error_errno(r, "Failed to add built-in positive trust anchor: %m"); |
531 | | |
532 | 0 | r = dns_trust_anchor_add_builtin_negative(d); |
533 | 0 | if (r < 0) |
534 | 0 | return log_error_errno(r, "Failed to add built-in negative trust anchor: %m"); |
535 | | |
536 | 0 | dns_trust_anchor_dump(d); |
537 | |
|
538 | 0 | return 0; |
539 | 0 | } |
540 | | |
541 | 0 | void dns_trust_anchor_flush(DnsTrustAnchor *d) { |
542 | 0 | assert(d); |
543 | |
|
544 | 0 | d->positive_by_key = hashmap_free(d->positive_by_key); |
545 | 0 | d->revoked_by_rr = set_free(d->revoked_by_rr); |
546 | 0 | d->negative_by_name = set_free(d->negative_by_name); |
547 | 0 | } |
548 | | |
549 | 0 | int dns_trust_anchor_lookup_positive(DnsTrustAnchor *d, const DnsResourceKey *key, DnsAnswer **ret) { |
550 | 0 | DnsAnswer *a; |
551 | |
|
552 | 0 | assert(d); |
553 | 0 | assert(key); |
554 | 0 | assert(ret); |
555 | | |
556 | | /* We only serve DS and DNSKEY RRs. */ |
557 | 0 | if (!IN_SET(key->type, DNS_TYPE_DS, DNS_TYPE_DNSKEY)) |
558 | 0 | return 0; |
559 | | |
560 | 0 | a = hashmap_get(d->positive_by_key, key); |
561 | 0 | if (!a) |
562 | 0 | return 0; |
563 | | |
564 | 0 | *ret = dns_answer_ref(a); |
565 | 0 | return 1; |
566 | 0 | } |
567 | | |
568 | 0 | int dns_trust_anchor_lookup_negative(DnsTrustAnchor *d, const char *name) { |
569 | 0 | int r; |
570 | |
|
571 | 0 | assert(d); |
572 | 0 | assert(name); |
573 | |
|
574 | 0 | for (;;) { |
575 | | /* If the domain is listed as-is in the NTA database, then that counts */ |
576 | 0 | if (set_contains(d->negative_by_name, name)) |
577 | 0 | return true; |
578 | | |
579 | | /* If the domain isn't listed as NTA, but is listed as positive trust anchor, then that counts. See RFC |
580 | | * 7646, section 1.1 */ |
581 | 0 | if (hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DS, name))) |
582 | 0 | return false; |
583 | | |
584 | 0 | if (hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_KEY, name))) |
585 | 0 | return false; |
586 | | |
587 | | /* And now, let's look at the parent, and check that too */ |
588 | 0 | r = dns_name_parent(&name); |
589 | 0 | if (r < 0) |
590 | 0 | return r; |
591 | 0 | if (r == 0) |
592 | 0 | break; |
593 | 0 | } |
594 | | |
595 | 0 | return false; |
596 | 0 | } |
597 | | |
598 | 0 | static int dns_trust_anchor_revoked_put(DnsTrustAnchor *d, DnsResourceRecord *rr) { |
599 | 0 | int r; |
600 | |
|
601 | 0 | assert(d); |
602 | |
|
603 | 0 | r = set_ensure_put(&d->revoked_by_rr, &dns_resource_record_hash_ops, rr); |
604 | 0 | if (r < 0) |
605 | 0 | return r; |
606 | 0 | if (r > 0) |
607 | 0 | dns_resource_record_ref(rr); |
608 | |
|
609 | 0 | return r; |
610 | 0 | } |
611 | | |
612 | 0 | static int dns_trust_anchor_remove_revoked(DnsTrustAnchor *d, DnsResourceRecord *rr) { |
613 | 0 | _cleanup_(dns_answer_unrefp) DnsAnswer *new_answer = NULL; |
614 | 0 | DnsAnswer *old_answer; |
615 | 0 | DnsAnswerItem *item; |
616 | 0 | int r; |
617 | | |
618 | | /* Remember that this is a revoked trust anchor RR */ |
619 | 0 | r = dns_trust_anchor_revoked_put(d, rr); |
620 | 0 | if (r < 0) |
621 | 0 | return r; |
622 | | |
623 | | /* Remove this from the positive trust anchor */ |
624 | 0 | old_answer = hashmap_get(d->positive_by_key, rr->key); |
625 | 0 | if (!old_answer) |
626 | 0 | return 0; |
627 | | |
628 | 0 | new_answer = dns_answer_ref(old_answer); |
629 | |
|
630 | 0 | r = dns_answer_remove_by_rr(&new_answer, rr); |
631 | 0 | if (r <= 0) |
632 | 0 | return r; |
633 | | |
634 | | /* We found the key! Warn the user */ |
635 | 0 | log_struct(LOG_WARNING, |
636 | 0 | LOG_MESSAGE_ID(SD_MESSAGE_DNSSEC_TRUST_ANCHOR_REVOKED_STR), |
637 | 0 | LOG_MESSAGE("DNSSEC trust anchor %s has been revoked.\n" |
638 | 0 | "Please update the trust anchor, or upgrade your operating system.", |
639 | 0 | strna(dns_resource_record_to_string(rr))), |
640 | 0 | LOG_ITEM("TRUST_ANCHOR=%s", dns_resource_record_to_string(rr))); |
641 | |
|
642 | 0 | if (dns_answer_size(new_answer) <= 0) { |
643 | 0 | assert_se(hashmap_remove(d->positive_by_key, rr->key) == old_answer); |
644 | 0 | dns_answer_unref(old_answer); |
645 | 0 | return 1; |
646 | 0 | } |
647 | | |
648 | 0 | item = ordered_set_first(new_answer->items); |
649 | 0 | r = hashmap_replace(d->positive_by_key, item->rr->key, new_answer); |
650 | 0 | if (r < 0) |
651 | 0 | return r; |
652 | | |
653 | 0 | TAKE_PTR(new_answer); |
654 | 0 | dns_answer_unref(old_answer); |
655 | 0 | return 1; |
656 | 0 | } |
657 | | |
658 | 0 | static int dns_trust_anchor_check_revoked_one(DnsTrustAnchor *d, DnsResourceRecord *revoked_dnskey) { |
659 | 0 | DnsAnswer *a; |
660 | 0 | int r; |
661 | |
|
662 | 0 | assert(d); |
663 | 0 | assert(revoked_dnskey); |
664 | 0 | assert(revoked_dnskey->key->type == DNS_TYPE_DNSKEY); |
665 | 0 | assert(revoked_dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE); |
666 | |
|
667 | 0 | a = hashmap_get(d->positive_by_key, revoked_dnskey->key); |
668 | 0 | if (a) { |
669 | 0 | DnsResourceRecord *anchor; |
670 | | |
671 | | /* First, look for the precise DNSKEY in our trust anchor database */ |
672 | |
|
673 | 0 | DNS_ANSWER_FOREACH(anchor, a) { |
674 | |
|
675 | 0 | if (anchor->dnskey.protocol != revoked_dnskey->dnskey.protocol) |
676 | 0 | continue; |
677 | | |
678 | 0 | if (anchor->dnskey.algorithm != revoked_dnskey->dnskey.algorithm) |
679 | 0 | continue; |
680 | | |
681 | 0 | if (anchor->dnskey.key_size != revoked_dnskey->dnskey.key_size) |
682 | 0 | continue; |
683 | | |
684 | | /* Note that we allow the REVOKE bit to be |
685 | | * different! It will be set in the revoked |
686 | | * key, but unset in our version of it */ |
687 | 0 | if (((anchor->dnskey.flags ^ revoked_dnskey->dnskey.flags) | DNSKEY_FLAG_REVOKE) != DNSKEY_FLAG_REVOKE) |
688 | 0 | continue; |
689 | | |
690 | 0 | if (memcmp(anchor->dnskey.key, revoked_dnskey->dnskey.key, anchor->dnskey.key_size) != 0) |
691 | 0 | continue; |
692 | | |
693 | 0 | dns_trust_anchor_remove_revoked(d, anchor); |
694 | 0 | break; |
695 | 0 | } |
696 | 0 | } |
697 | |
|
698 | 0 | a = hashmap_get(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(revoked_dnskey->key->class, DNS_TYPE_DS, dns_resource_key_name(revoked_dnskey->key))); |
699 | 0 | if (a) { |
700 | 0 | DnsResourceRecord *anchor; |
701 | | |
702 | | /* Second, look for DS RRs matching this DNSKEY in our trust anchor database */ |
703 | |
|
704 | 0 | DNS_ANSWER_FOREACH(anchor, a) { |
705 | | |
706 | | /* We set mask_revoke to true here, since our |
707 | | * DS fingerprint will be the one of the |
708 | | * unrevoked DNSKEY, but the one we got passed |
709 | | * here has the bit set. */ |
710 | 0 | r = dnssec_verify_dnskey_by_ds(revoked_dnskey, anchor, true); |
711 | 0 | if (r < 0) |
712 | 0 | return r; |
713 | 0 | if (r == 0) |
714 | 0 | continue; |
715 | | |
716 | 0 | dns_trust_anchor_remove_revoked(d, anchor); |
717 | 0 | break; |
718 | 0 | } |
719 | 0 | } |
720 | | |
721 | 0 | return 0; |
722 | 0 | } |
723 | | |
724 | 0 | int dns_trust_anchor_check_revoked(DnsTrustAnchor *d, DnsResourceRecord *dnskey, DnsAnswer *rrs) { |
725 | 0 | DnsResourceRecord *rrsig; |
726 | 0 | int r; |
727 | |
|
728 | 0 | assert(d); |
729 | 0 | assert(dnskey); |
730 | | |
731 | | /* Looks if "dnskey" is a self-signed RR that has been revoked |
732 | | * and matches one of our trust anchor entries. If so, removes |
733 | | * it from the trust anchor and returns > 0. */ |
734 | |
|
735 | 0 | if (dnskey->key->type != DNS_TYPE_DNSKEY) |
736 | 0 | return 0; |
737 | | |
738 | | /* Is this DNSKEY revoked? */ |
739 | 0 | if ((dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE) == 0) |
740 | 0 | return 0; |
741 | | |
742 | | /* Could this be interesting to us at all? If not, |
743 | | * there's no point in looking for and verifying a |
744 | | * self-signed RRSIG. */ |
745 | 0 | if (!dns_trust_anchor_knows_domain_positive(d, dns_resource_key_name(dnskey->key))) |
746 | 0 | return 0; |
747 | | |
748 | | /* Look for a self-signed RRSIG in the other rrs belonging to this DNSKEY */ |
749 | 0 | DNS_ANSWER_FOREACH(rrsig, rrs) { |
750 | 0 | DnssecResult result; |
751 | |
|
752 | 0 | if (rrsig->key->type != DNS_TYPE_RRSIG) |
753 | 0 | continue; |
754 | | |
755 | 0 | r = dnssec_rrsig_match_dnskey(rrsig, dnskey, true); |
756 | 0 | if (r < 0) |
757 | 0 | return r; |
758 | 0 | if (r == 0) |
759 | 0 | continue; |
760 | | |
761 | 0 | r = dnssec_verify_rrset(rrs, dnskey->key, rrsig, dnskey, USEC_INFINITY, &result); |
762 | 0 | if (r < 0) |
763 | 0 | return r; |
764 | 0 | if (result != DNSSEC_VALIDATED) |
765 | 0 | continue; |
766 | | |
767 | | /* Bingo! This is a revoked self-signed DNSKEY. Let's |
768 | | * see if this precise one exists in our trust anchor |
769 | | * database, too. */ |
770 | 0 | r = dns_trust_anchor_check_revoked_one(d, dnskey); |
771 | 0 | if (r < 0) |
772 | 0 | return r; |
773 | | |
774 | 0 | return 1; |
775 | 0 | } |
776 | | |
777 | 0 | return 0; |
778 | 0 | } |
779 | | |
780 | 0 | int dns_trust_anchor_is_revoked(DnsTrustAnchor *d, DnsResourceRecord *rr) { |
781 | 0 | assert(d); |
782 | |
|
783 | 0 | if (!IN_SET(rr->key->type, DNS_TYPE_DS, DNS_TYPE_DNSKEY)) |
784 | 0 | return 0; |
785 | | |
786 | 0 | return set_contains(d->revoked_by_rr, rr); |
787 | 0 | } |