Line | Count | Source |
1 | | /* $OpenBSD: dns.c,v 1.48 2026/03/03 09:57:25 dtucker Exp $ */ |
2 | | |
3 | | /* |
4 | | * Copyright (c) 2003 Wesley Griffin. All rights reserved. |
5 | | * Copyright (c) 2003 Jakob Schlyter. All rights reserved. |
6 | | * |
7 | | * Redistribution and use in source and binary forms, with or without |
8 | | * modification, are permitted provided that the following conditions |
9 | | * are met: |
10 | | * 1. Redistributions of source code must retain the above copyright |
11 | | * notice, this list of conditions and the following disclaimer. |
12 | | * 2. Redistributions in binary form must reproduce the above copyright |
13 | | * notice, this list of conditions and the following disclaimer in the |
14 | | * documentation and/or other materials provided with the distribution. |
15 | | * |
16 | | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
17 | | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
18 | | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
19 | | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
20 | | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
21 | | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
22 | | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
23 | | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
24 | | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
25 | | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
26 | | */ |
27 | | |
28 | | #include "includes.h" |
29 | | |
30 | | #include <sys/types.h> |
31 | | #include <sys/socket.h> |
32 | | |
33 | | #include <netdb.h> |
34 | | #include <stdarg.h> |
35 | | #include <stdio.h> |
36 | | #include <string.h> |
37 | | #include <stdlib.h> |
38 | | |
39 | | #include "xmalloc.h" |
40 | | #include "sshkey.h" |
41 | | #include "dns.h" |
42 | | #include "log.h" |
43 | | #include "digest.h" |
44 | | |
45 | | static const char * const errset_text[] = { |
46 | | "success", /* 0 ERRSET_SUCCESS */ |
47 | | "out of memory", /* 1 ERRSET_NOMEMORY */ |
48 | | "general failure", /* 2 ERRSET_FAIL */ |
49 | | "invalid parameter", /* 3 ERRSET_INVAL */ |
50 | | "name does not exist", /* 4 ERRSET_NONAME */ |
51 | | "data does not exist", /* 5 ERRSET_NODATA */ |
52 | | }; |
53 | | |
54 | | static const char * |
55 | | dns_result_totext(unsigned int res) |
56 | 0 | { |
57 | 0 | switch (res) { |
58 | 0 | case ERRSET_SUCCESS: |
59 | 0 | return errset_text[ERRSET_SUCCESS]; |
60 | 0 | case ERRSET_NOMEMORY: |
61 | 0 | return errset_text[ERRSET_NOMEMORY]; |
62 | 0 | case ERRSET_FAIL: |
63 | 0 | return errset_text[ERRSET_FAIL]; |
64 | 0 | case ERRSET_INVAL: |
65 | 0 | return errset_text[ERRSET_INVAL]; |
66 | 0 | case ERRSET_NONAME: |
67 | 0 | return errset_text[ERRSET_NONAME]; |
68 | 0 | case ERRSET_NODATA: |
69 | 0 | return errset_text[ERRSET_NODATA]; |
70 | 0 | default: |
71 | 0 | return "unknown error"; |
72 | 0 | } |
73 | 0 | } |
74 | | |
75 | | /* |
76 | | * Read SSHFP parameters from key buffer. |
77 | | * Caller must free digest which is allocated by sshkey_fingerprint_raw(). |
78 | | */ |
79 | | static int |
80 | | dns_read_key(uint8_t *algorithm, uint8_t *digest_type, |
81 | | u_char **digest, size_t *digest_len, struct sshkey *key) |
82 | 0 | { |
83 | 0 | int r, success = 0; |
84 | 0 | int fp_alg = -1; |
85 | |
|
86 | 0 | switch (key->type) { |
87 | 0 | case KEY_RSA: |
88 | 0 | *algorithm = SSHFP_KEY_RSA; |
89 | 0 | break; |
90 | 0 | case KEY_ECDSA: |
91 | 0 | *algorithm = SSHFP_KEY_ECDSA; |
92 | 0 | break; |
93 | 0 | case KEY_ED25519: |
94 | 0 | *algorithm = SSHFP_KEY_ED25519; |
95 | 0 | break; |
96 | 0 | default: |
97 | 0 | *algorithm = SSHFP_KEY_RESERVED; /* 0 */ |
98 | 0 | } |
99 | | |
100 | 0 | switch (*digest_type) { |
101 | 0 | case SSHFP_HASH_SHA1: |
102 | 0 | fp_alg = SSH_DIGEST_SHA1; |
103 | 0 | break; |
104 | 0 | case SSHFP_HASH_SHA256: |
105 | 0 | fp_alg = SSH_DIGEST_SHA256; |
106 | 0 | break; |
107 | 0 | default: |
108 | 0 | *digest_type = SSHFP_HASH_RESERVED; /* 0 */ |
109 | 0 | } |
110 | | |
111 | 0 | if (*algorithm && *digest_type) { |
112 | 0 | if ((r = sshkey_fingerprint_raw(key, fp_alg, digest, |
113 | 0 | digest_len)) != 0) |
114 | 0 | fatal_fr(r, "sshkey_fingerprint_raw"); |
115 | 0 | success = 1; |
116 | 0 | } else { |
117 | 0 | *digest = NULL; |
118 | 0 | *digest_len = 0; |
119 | 0 | } |
120 | | |
121 | 0 | return success; |
122 | 0 | } |
123 | | |
124 | | /* |
125 | | * Read SSHFP parameters from rdata buffer. |
126 | | */ |
127 | | static int |
128 | | dns_read_rdata(uint8_t *algorithm, uint8_t *digest_type, |
129 | | u_char **digest, size_t *digest_len, u_char *rdata, int rdata_len) |
130 | 0 | { |
131 | 0 | int success = 0; |
132 | |
|
133 | 0 | *algorithm = SSHFP_KEY_RESERVED; |
134 | 0 | *digest_type = SSHFP_HASH_RESERVED; |
135 | |
|
136 | 0 | if (rdata_len >= 2) { |
137 | 0 | *algorithm = rdata[0]; |
138 | 0 | *digest_type = rdata[1]; |
139 | 0 | *digest_len = rdata_len - 2; |
140 | |
|
141 | 0 | if (*digest_len > 0) { |
142 | 0 | *digest = xmalloc(*digest_len); |
143 | 0 | memcpy(*digest, rdata + 2, *digest_len); |
144 | 0 | } else { |
145 | 0 | *digest = (u_char *)xstrdup(""); |
146 | 0 | } |
147 | |
|
148 | 0 | success = 1; |
149 | 0 | } |
150 | |
|
151 | 0 | return success; |
152 | 0 | } |
153 | | |
154 | | /* |
155 | | * Check if hostname is numerical. |
156 | | * Returns -1 if hostname is numeric, 0 otherwise |
157 | | */ |
158 | | static int |
159 | | is_numeric_hostname(const char *hostname) |
160 | 0 | { |
161 | 0 | struct addrinfo hints, *ai; |
162 | | |
163 | | /* |
164 | | * We shouldn't ever get a null host but if we do then log an error |
165 | | * and return -1 which stops DNS key fingerprint processing. |
166 | | */ |
167 | 0 | if (hostname == NULL) { |
168 | 0 | error("is_numeric_hostname called with NULL hostname"); |
169 | 0 | return -1; |
170 | 0 | } |
171 | | |
172 | 0 | memset(&hints, 0, sizeof(hints)); |
173 | 0 | hints.ai_socktype = SOCK_DGRAM; |
174 | 0 | hints.ai_flags = AI_NUMERICHOST; |
175 | |
|
176 | 0 | if (getaddrinfo(hostname, NULL, &hints, &ai) == 0) { |
177 | 0 | freeaddrinfo(ai); |
178 | 0 | return -1; |
179 | 0 | } |
180 | | |
181 | 0 | return 0; |
182 | 0 | } |
183 | | |
184 | | /* |
185 | | * Verify the given hostname, address and host key using DNS. |
186 | | * Returns 0 if lookup succeeds, -1 otherwise |
187 | | */ |
188 | | int |
189 | | verify_host_key_dns(const char *hostname, struct sockaddr *address, |
190 | | struct sshkey *hostkey, int *flags) |
191 | 0 | { |
192 | 0 | u_int counter; |
193 | 0 | int result; |
194 | 0 | struct rrsetinfo *fingerprints = NULL; |
195 | |
|
196 | 0 | uint8_t hostkey_algorithm; |
197 | 0 | u_char *hostkey_digest; |
198 | 0 | size_t hostkey_digest_len; |
199 | |
|
200 | 0 | uint8_t dnskey_algorithm; |
201 | 0 | uint8_t dnskey_digest_type; |
202 | 0 | u_char *dnskey_digest; |
203 | 0 | size_t dnskey_digest_len; |
204 | |
|
205 | 0 | *flags = 0; |
206 | |
|
207 | 0 | debug3("verify_host_key_dns"); |
208 | 0 | if (hostkey == NULL) |
209 | 0 | fatal("No key to look up!"); |
210 | | |
211 | 0 | if (is_numeric_hostname(hostname)) { |
212 | 0 | debug("skipped DNS lookup for numerical hostname"); |
213 | 0 | return -1; |
214 | 0 | } |
215 | | |
216 | 0 | result = getrrsetbyname(hostname, DNS_RDATACLASS_IN, |
217 | 0 | DNS_RDATATYPE_SSHFP, 0, &fingerprints); |
218 | 0 | if (result) { |
219 | 0 | verbose("DNS lookup error: %s", dns_result_totext(result)); |
220 | 0 | return -1; |
221 | 0 | } |
222 | | |
223 | 0 | if (fingerprints->rri_flags & RRSET_VALIDATED) { |
224 | 0 | *flags |= DNS_VERIFY_SECURE; |
225 | 0 | debug("found %d secure fingerprints in DNS", |
226 | 0 | fingerprints->rri_nrdatas); |
227 | 0 | } else { |
228 | 0 | debug("found %d insecure fingerprints in DNS", |
229 | 0 | fingerprints->rri_nrdatas); |
230 | 0 | } |
231 | |
|
232 | 0 | if (fingerprints->rri_nrdatas) |
233 | 0 | *flags |= DNS_VERIFY_FOUND; |
234 | |
|
235 | 0 | for (counter = 0; counter < fingerprints->rri_nrdatas; counter++) { |
236 | | /* |
237 | | * Extract the key from the answer. Ignore any badly |
238 | | * formatted fingerprints. |
239 | | */ |
240 | 0 | if (!dns_read_rdata(&dnskey_algorithm, &dnskey_digest_type, |
241 | 0 | &dnskey_digest, &dnskey_digest_len, |
242 | 0 | fingerprints->rri_rdatas[counter].rdi_data, |
243 | 0 | fingerprints->rri_rdatas[counter].rdi_length)) { |
244 | 0 | verbose("Error parsing fingerprint from DNS."); |
245 | 0 | continue; |
246 | 0 | } |
247 | 0 | debug3_f("checking SSHFP type %d fptype %d", dnskey_algorithm, |
248 | 0 | dnskey_digest_type); |
249 | | |
250 | | /* Calculate host key fingerprint. */ |
251 | 0 | if (!dns_read_key(&hostkey_algorithm, &dnskey_digest_type, |
252 | 0 | &hostkey_digest, &hostkey_digest_len, hostkey)) { |
253 | 0 | error("Error calculating key fingerprint."); |
254 | 0 | free(dnskey_digest); |
255 | 0 | freerrset(fingerprints); |
256 | 0 | return -1; |
257 | 0 | } |
258 | | |
259 | | /* Check if the current key is the same as the given key */ |
260 | 0 | if (hostkey_algorithm == dnskey_algorithm && |
261 | 0 | hostkey_digest_len == dnskey_digest_len) { |
262 | 0 | if (timingsafe_bcmp(hostkey_digest, dnskey_digest, |
263 | 0 | hostkey_digest_len) == 0) { |
264 | 0 | debug_f("matched SSHFP type %d fptype %d", |
265 | 0 | dnskey_algorithm, dnskey_digest_type); |
266 | 0 | *flags |= DNS_VERIFY_MATCH; |
267 | 0 | } else { |
268 | 0 | debug_f("failed SSHFP type %d fptype %d", |
269 | 0 | dnskey_algorithm, dnskey_digest_type); |
270 | 0 | *flags |= DNS_VERIFY_FAILED; |
271 | 0 | } |
272 | 0 | } |
273 | 0 | free(dnskey_digest); |
274 | 0 | free(hostkey_digest); /* from sshkey_fingerprint_raw() */ |
275 | 0 | } |
276 | | |
277 | 0 | freerrset(fingerprints); |
278 | | |
279 | | /* If any fingerprint failed to validate, return failure. */ |
280 | 0 | if (*flags & DNS_VERIFY_FAILED) |
281 | 0 | *flags &= ~DNS_VERIFY_MATCH; |
282 | |
|
283 | 0 | if (*flags & DNS_VERIFY_FOUND) |
284 | 0 | if (*flags & DNS_VERIFY_MATCH) |
285 | 0 | debug("matching host key fingerprint found in DNS"); |
286 | 0 | else |
287 | 0 | debug("mismatching host key fingerprint found in DNS"); |
288 | 0 | else |
289 | 0 | debug("no host key fingerprint found in DNS"); |
290 | |
|
291 | 0 | return 0; |
292 | 0 | } |
293 | | |
294 | | /* |
295 | | * Export the fingerprint of a key as a DNS resource record |
296 | | */ |
297 | | int |
298 | | export_dns_rr(const char *hostname, struct sshkey *key, FILE *f, int generic, |
299 | | int alg) |
300 | 0 | { |
301 | 0 | uint8_t rdata_pubkey_algorithm = 0; |
302 | 0 | uint8_t rdata_digest_type = SSHFP_HASH_RESERVED; |
303 | 0 | uint8_t dtype; |
304 | 0 | u_char *rdata_digest; |
305 | 0 | size_t i, rdata_digest_len; |
306 | 0 | int success = 0; |
307 | |
|
308 | 0 | for (dtype = SSHFP_HASH_SHA1; dtype < SSHFP_HASH_MAX; dtype++) { |
309 | 0 | if (alg != -1 && dtype != alg) |
310 | 0 | continue; |
311 | 0 | rdata_digest_type = dtype; |
312 | 0 | if (dns_read_key(&rdata_pubkey_algorithm, &rdata_digest_type, |
313 | 0 | &rdata_digest, &rdata_digest_len, key)) { |
314 | 0 | if (generic) { |
315 | 0 | fprintf(f, "%s IN TYPE%d \\# %zu %02x %02x ", |
316 | 0 | hostname, DNS_RDATATYPE_SSHFP, |
317 | 0 | 2 + rdata_digest_len, |
318 | 0 | rdata_pubkey_algorithm, rdata_digest_type); |
319 | 0 | } else { |
320 | 0 | fprintf(f, "%s IN SSHFP %d %d ", hostname, |
321 | 0 | rdata_pubkey_algorithm, rdata_digest_type); |
322 | 0 | } |
323 | 0 | for (i = 0; i < rdata_digest_len; i++) |
324 | 0 | fprintf(f, "%02x", rdata_digest[i]); |
325 | 0 | fprintf(f, "\n"); |
326 | 0 | free(rdata_digest); /* from sshkey_fingerprint_raw() */ |
327 | 0 | success = 1; |
328 | 0 | } |
329 | 0 | } |
330 | | |
331 | | /* No SSHFP record was generated at all */ |
332 | 0 | if (success == 0) { |
333 | 0 | error_f("unsupported algorithm and/or digest_type"); |
334 | 0 | } |
335 | |
|
336 | 0 | return success; |
337 | 0 | } |