diff options
author | djm@openbsd.org <djm@openbsd.org> | 2019-09-03 08:34:19 +0000 |
---|---|---|
committer | Damien Miller <djm@mindrot.org> | 2019-09-03 18:40:23 +1000 |
commit | 2a9c9f7272c1e8665155118fe6536bebdafb6166 (patch) | |
tree | 177a8c032d9396249708e4a5cb65321d9250fdee /sshsig.c | |
parent | 5485f8d50a5bc46aeed829075ebf5d9c617027ea (diff) |
upstream: sshsig: lightweight signature and verification ability
for OpenSSH
This adds a simple manual signature scheme to OpenSSH.
Signatures can be made and verified using ssh-keygen -Y sign|verify
Signatures embed the key used to make them. At verification time, this
is matched via principal name against an authorized_keys-like list
of allowed signers.
Mostly by Sebastian Kinne w/ some tweaks by me
ok markus@
OpenBSD-Commit-ID: 2ab568e7114c933346616392579d72be65a4b8fb
Diffstat (limited to 'sshsig.c')
-rw-r--r-- | sshsig.c | 787 |
1 files changed, 787 insertions, 0 deletions
diff --git a/sshsig.c b/sshsig.c new file mode 100644 index 000000000..0a1e14627 --- /dev/null +++ b/sshsig.c | |||
@@ -0,0 +1,787 @@ | |||
1 | /* | ||
2 | * Copyright (c) 2019 Google LLC | ||
3 | * | ||
4 | * Permission to use, copy, modify, and distribute this software for any | ||
5 | * purpose with or without fee is hereby granted, provided that the above | ||
6 | * copyright notice and this permission notice appear in all copies. | ||
7 | * | ||
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | ||
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
15 | */ | ||
16 | |||
17 | #include <stdio.h> | ||
18 | #include <stdlib.h> | ||
19 | #include <stdarg.h> | ||
20 | #include <errno.h> | ||
21 | #include <string.h> | ||
22 | #include <unistd.h> | ||
23 | |||
24 | #include "authfd.h" | ||
25 | #include "authfile.h" | ||
26 | #include "log.h" | ||
27 | #include "misc.h" | ||
28 | #include "sshbuf.h" | ||
29 | #include "sshsig.h" | ||
30 | #include "ssherr.h" | ||
31 | #include "sshkey.h" | ||
32 | #include "match.h" | ||
33 | #include "digest.h" | ||
34 | |||
35 | #define SIG_VERSION 0x01 | ||
36 | #define MAGIC_PREAMBLE "SSHSIG" | ||
37 | #define MAGIC_PREAMBLE_LEN (sizeof(MAGIC_PREAMBLE) - 1) | ||
38 | #define BEGIN_SIGNATURE "-----BEGIN SSH SIGNATURE-----\n" | ||
39 | #define END_SIGNATURE "-----END SSH SIGNATURE-----" | ||
40 | #define RSA_SIGN_ALG "rsa-sha2-512" /* XXX maybe make configurable */ | ||
41 | #define RSA_SIGN_ALLOWED "rsa-sha2-512,rsa-sha2-256" | ||
42 | #define HASHALG_DEFAULT "sha512" /* XXX maybe make configurable */ | ||
43 | #define HASHALG_ALLOWED "sha256,sha512" | ||
44 | |||
45 | int | ||
46 | sshsig_armor(const struct sshbuf *blob, struct sshbuf **out) | ||
47 | { | ||
48 | struct sshbuf *buf = NULL; | ||
49 | int r = SSH_ERR_INTERNAL_ERROR; | ||
50 | |||
51 | *out = NULL; | ||
52 | |||
53 | if ((buf = sshbuf_new()) == NULL) { | ||
54 | error("%s: sshbuf_new failed", __func__); | ||
55 | r = SSH_ERR_ALLOC_FAIL; | ||
56 | goto out; | ||
57 | } | ||
58 | |||
59 | if ((r = sshbuf_put(buf, BEGIN_SIGNATURE, | ||
60 | sizeof(BEGIN_SIGNATURE)-1)) != 0) { | ||
61 | error("%s: sshbuf_putf failed: %s", __func__, ssh_err(r)); | ||
62 | goto out; | ||
63 | } | ||
64 | |||
65 | if ((r = sshbuf_dtob64(blob, buf, 1)) != 0) { | ||
66 | error("%s: Couldn't base64 encode signature blob: %s", | ||
67 | __func__, ssh_err(r)); | ||
68 | goto out; | ||
69 | } | ||
70 | |||
71 | if ((r = sshbuf_put(buf, END_SIGNATURE, | ||
72 | sizeof(END_SIGNATURE)-1)) != 0 || | ||
73 | (r = sshbuf_put_u8(buf, '\n')) != 0) { | ||
74 | error("%s: sshbuf_put failed: %s", __func__, ssh_err(r)); | ||
75 | goto out; | ||
76 | } | ||
77 | /* success */ | ||
78 | *out = buf; | ||
79 | buf = NULL; /* transferred */ | ||
80 | r = 0; | ||
81 | out: | ||
82 | sshbuf_free(buf); | ||
83 | return r; | ||
84 | } | ||
85 | |||
86 | int | ||
87 | sshsig_dearmor(struct sshbuf *sig, struct sshbuf **out) | ||
88 | { | ||
89 | int r; | ||
90 | size_t eoffset = 0; | ||
91 | struct sshbuf *buf = NULL; | ||
92 | struct sshbuf *sbuf = NULL; | ||
93 | char *b64 = NULL; | ||
94 | |||
95 | if ((sbuf = sshbuf_fromb(sig)) == NULL) { | ||
96 | error("%s: sshbuf_fromb failed", __func__); | ||
97 | return SSH_ERR_ALLOC_FAIL; | ||
98 | } | ||
99 | |||
100 | if ((r = sshbuf_cmp(sbuf, 0, | ||
101 | BEGIN_SIGNATURE, sizeof(BEGIN_SIGNATURE)-1)) != 0) { | ||
102 | error("Couldn't parse signature: missing header"); | ||
103 | goto done; | ||
104 | } | ||
105 | |||
106 | if ((r = sshbuf_consume(sbuf, sizeof(BEGIN_SIGNATURE)-1)) != 0) { | ||
107 | error("%s: sshbuf_consume failed: %s", __func__, ssh_err(r)); | ||
108 | goto done; | ||
109 | } | ||
110 | |||
111 | if ((r = sshbuf_find(sbuf, 0, "\n" END_SIGNATURE, | ||
112 | sizeof("\n" END_SIGNATURE)-1, &eoffset)) != 0) { | ||
113 | error("Couldn't parse signature: missing footer"); | ||
114 | goto done; | ||
115 | } | ||
116 | |||
117 | if ((r = sshbuf_consume_end(sbuf, sshbuf_len(sbuf)-eoffset)) != 0) { | ||
118 | error("%s: sshbuf_consume failed: %s", __func__, ssh_err(r)); | ||
119 | goto done; | ||
120 | } | ||
121 | |||
122 | if ((b64 = sshbuf_dup_string(sbuf)) == NULL) { | ||
123 | error("%s: sshbuf_dup_string failed", __func__); | ||
124 | r = SSH_ERR_ALLOC_FAIL; | ||
125 | goto done; | ||
126 | } | ||
127 | |||
128 | if ((buf = sshbuf_new()) == NULL) { | ||
129 | error("%s: sshbuf_new() failed", __func__); | ||
130 | r = SSH_ERR_ALLOC_FAIL; | ||
131 | goto done; | ||
132 | } | ||
133 | |||
134 | if ((r = sshbuf_b64tod(buf, b64)) != 0) { | ||
135 | error("Coundn't decode signature: %s", ssh_err(r)); | ||
136 | goto done; | ||
137 | } | ||
138 | |||
139 | /* success */ | ||
140 | *out = buf; | ||
141 | r = 0; | ||
142 | buf = NULL; /* transferred */ | ||
143 | done: | ||
144 | sshbuf_free(buf); | ||
145 | sshbuf_free(sbuf); | ||
146 | free(b64); | ||
147 | return r; | ||
148 | } | ||
149 | |||
150 | static int | ||
151 | sshsig_wrap_sign(struct sshkey *key, const char *hashalg, | ||
152 | const struct sshbuf *h_message, const char *sig_namespace, | ||
153 | struct sshbuf **out, sshsig_signer *signer, void *signer_ctx) | ||
154 | { | ||
155 | int r; | ||
156 | size_t slen = 0; | ||
157 | u_char *sig = NULL; | ||
158 | struct sshbuf *blob = NULL; | ||
159 | struct sshbuf *tosign = NULL; | ||
160 | const char *sign_alg = NULL; | ||
161 | |||
162 | if ((tosign = sshbuf_new()) == NULL || | ||
163 | (blob = sshbuf_new()) == NULL) { | ||
164 | error("%s: sshbuf_new failed", __func__); | ||
165 | r = SSH_ERR_ALLOC_FAIL; | ||
166 | goto done; | ||
167 | } | ||
168 | |||
169 | if ((r = sshbuf_put(tosign, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 || | ||
170 | (r = sshbuf_put_cstring(tosign, sig_namespace)) != 0 || | ||
171 | (r = sshbuf_put_string(tosign, NULL, 0)) != 0 || /* reserved */ | ||
172 | (r = sshbuf_put_cstring(tosign, hashalg)) != 0 || | ||
173 | (r = sshbuf_putb(tosign, h_message)) != 0) { | ||
174 | error("Couldn't construct message to sign: %s", ssh_err(r)); | ||
175 | goto done; | ||
176 | } | ||
177 | |||
178 | /* If using RSA keys then default to a good signature algorithm */ | ||
179 | if (sshkey_type_plain(key->type) == KEY_RSA) | ||
180 | sign_alg = RSA_SIGN_ALG; | ||
181 | |||
182 | if (signer != NULL) { | ||
183 | if ((r = signer(key, &sig, &slen, | ||
184 | sshbuf_ptr(tosign), sshbuf_len(tosign), | ||
185 | sign_alg, 0, signer_ctx)) != 0) { | ||
186 | error("Couldn't sign message: %s", ssh_err(r)); | ||
187 | goto done; | ||
188 | } | ||
189 | } else { | ||
190 | if ((r = sshkey_sign(key, &sig, &slen, | ||
191 | sshbuf_ptr(tosign), sshbuf_len(tosign), | ||
192 | sign_alg, 0)) != 0) { | ||
193 | error("Couldn't sign message: %s", ssh_err(r)); | ||
194 | goto done; | ||
195 | } | ||
196 | } | ||
197 | |||
198 | if ((r = sshbuf_put(blob, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 || | ||
199 | (r = sshbuf_put_u32(blob, SIG_VERSION)) != 0 || | ||
200 | (r = sshkey_puts(key, blob)) != 0 || | ||
201 | (r = sshbuf_put_cstring(blob, sig_namespace)) != 0 || | ||
202 | (r = sshbuf_put_string(blob, NULL, 0)) != 0 || /* reserved */ | ||
203 | (r = sshbuf_put_cstring(blob, hashalg)) != 0 || | ||
204 | (r = sshbuf_put_string(blob, sig, slen)) != 0) { | ||
205 | error("Couldn't populate blob: %s", ssh_err(r)); | ||
206 | goto done; | ||
207 | } | ||
208 | |||
209 | *out = blob; | ||
210 | blob = NULL; | ||
211 | r = 0; | ||
212 | done: | ||
213 | free(sig); | ||
214 | sshbuf_free(blob); | ||
215 | sshbuf_free(tosign); | ||
216 | return r; | ||
217 | } | ||
218 | |||
219 | /* Check preamble and version. */ | ||
220 | static int | ||
221 | sshsig_parse_preamble(struct sshbuf *buf) | ||
222 | { | ||
223 | int r = SSH_ERR_INTERNAL_ERROR; | ||
224 | uint32_t sversion; | ||
225 | |||
226 | if ((r = sshbuf_cmp(buf, 0, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 || | ||
227 | (r = sshbuf_consume(buf, (sizeof(MAGIC_PREAMBLE)-1))) != 0 || | ||
228 | (r = sshbuf_get_u32(buf, &sversion)) != 0) { | ||
229 | error("Couldn't verify signature: invalid format"); | ||
230 | return r; | ||
231 | } | ||
232 | |||
233 | if (sversion < SIG_VERSION) { | ||
234 | error("Signature version %lu is larger than supported " | ||
235 | "version %u", (unsigned long)sversion, SIG_VERSION); | ||
236 | return SSH_ERR_INVALID_FORMAT; | ||
237 | } | ||
238 | return 0; | ||
239 | } | ||
240 | |||
241 | static int | ||
242 | sshsig_check_hashalg(const char *hashalg) | ||
243 | { | ||
244 | if (match_pattern_list(hashalg, HASHALG_ALLOWED, 0) == 1) | ||
245 | return 0; | ||
246 | error("%s: unsupported hash algorithm \"%.100s\"", __func__, hashalg); | ||
247 | return SSH_ERR_SIGN_ALG_UNSUPPORTED; | ||
248 | } | ||
249 | |||
250 | static int | ||
251 | sshsig_peek_hashalg(struct sshbuf *signature, char **hashalgp) | ||
252 | { | ||
253 | struct sshbuf *buf = NULL; | ||
254 | char *hashalg = NULL; | ||
255 | int r = SSH_ERR_INTERNAL_ERROR; | ||
256 | |||
257 | if (hashalgp != NULL) | ||
258 | *hashalgp = NULL; | ||
259 | if ((buf = sshbuf_fromb(signature)) == NULL) | ||
260 | return SSH_ERR_ALLOC_FAIL; | ||
261 | if ((r = sshsig_parse_preamble(buf)) != 0) | ||
262 | goto done; | ||
263 | if ((r = sshbuf_get_string_direct(buf, NULL, NULL)) != 0 || | ||
264 | (r = sshbuf_get_string_direct(buf, NULL, NULL)) != 0 || | ||
265 | (r = sshbuf_get_string(buf, NULL, NULL)) != 0 || | ||
266 | (r = sshbuf_get_cstring(buf, &hashalg, NULL)) != 0 || | ||
267 | (r = sshbuf_get_string_direct(buf, NULL, NULL)) != 0) { | ||
268 | error("Couldn't parse signature blob: %s", ssh_err(r)); | ||
269 | goto done; | ||
270 | } | ||
271 | if ((r = sshsig_check_hashalg(hashalg)) != 0) | ||
272 | goto done; | ||
273 | |||
274 | /* success */ | ||
275 | r = 0; | ||
276 | *hashalgp = hashalg; | ||
277 | hashalg = NULL; | ||
278 | done: | ||
279 | free(hashalg); | ||
280 | sshbuf_free(buf); | ||
281 | return r; | ||
282 | } | ||
283 | |||
284 | static int | ||
285 | sshsig_wrap_verify(struct sshbuf *signature, const char *hashalg, | ||
286 | const struct sshbuf *h_message, const char *expect_namespace, | ||
287 | struct sshkey **sign_keyp) | ||
288 | { | ||
289 | int r = SSH_ERR_INTERNAL_ERROR; | ||
290 | struct sshbuf *buf = NULL, *toverify = NULL; | ||
291 | struct sshkey *key = NULL; | ||
292 | const u_char *sig; | ||
293 | char *got_namespace = NULL, *sigtype = NULL, *sig_hashalg = NULL; | ||
294 | size_t siglen; | ||
295 | |||
296 | if (sign_keyp != NULL) | ||
297 | *sign_keyp = NULL; | ||
298 | |||
299 | if ((toverify = sshbuf_new()) == NULL) { | ||
300 | error("%s: sshbuf_new failed", __func__); | ||
301 | r = SSH_ERR_ALLOC_FAIL; | ||
302 | goto done; | ||
303 | } | ||
304 | if ((r = sshsig_check_hashalg(hashalg)) != 0) | ||
305 | goto done; | ||
306 | |||
307 | if ((r = sshbuf_put(toverify, MAGIC_PREAMBLE, | ||
308 | MAGIC_PREAMBLE_LEN)) != 0 || | ||
309 | (r = sshbuf_put_cstring(toverify, expect_namespace)) != 0 || | ||
310 | (r = sshbuf_put_string(toverify, NULL, 0)) != 0 || /* reserved */ | ||
311 | (r = sshbuf_put_cstring(toverify, hashalg)) != 0 || | ||
312 | (r = sshbuf_putb(toverify, h_message)) != 0) { | ||
313 | error("Couldn't construct message to verify: %s", ssh_err(r)); | ||
314 | goto done; | ||
315 | } | ||
316 | |||
317 | if ((r = sshsig_parse_preamble(signature)) != 0) | ||
318 | goto done; | ||
319 | |||
320 | if ((r = sshkey_froms(signature, &key)) != 0 || | ||
321 | (r = sshbuf_get_cstring(signature, &got_namespace, NULL)) != 0 || | ||
322 | (r = sshbuf_get_string(signature, NULL, NULL)) != 0 || | ||
323 | (r = sshbuf_get_cstring(signature, &sig_hashalg, NULL)) != 0 || | ||
324 | (r = sshbuf_get_string_direct(signature, &sig, &siglen)) != 0) { | ||
325 | error("Couldn't parse signature blob: %s", ssh_err(r)); | ||
326 | goto done; | ||
327 | } | ||
328 | |||
329 | if (sshbuf_len(signature) != 0) { | ||
330 | error("Signature contains trailing data"); | ||
331 | r = SSH_ERR_INVALID_FORMAT; | ||
332 | goto done; | ||
333 | } | ||
334 | |||
335 | if (strcmp(expect_namespace, got_namespace) != 0) { | ||
336 | error("Couldn't verify signature: namespace does not match"); | ||
337 | debug("%s: expected namespace \"%s\" received \"%s\"", | ||
338 | __func__, expect_namespace, got_namespace); | ||
339 | r = SSH_ERR_SIGNATURE_INVALID; | ||
340 | goto done; | ||
341 | } | ||
342 | if (strcmp(hashalg, sig_hashalg) != 0) { | ||
343 | error("Couldn't verify signature: hash algorithm mismatch"); | ||
344 | debug("%s: expected algorithm \"%s\" received \"%s\"", | ||
345 | __func__, hashalg, sig_hashalg); | ||
346 | r = SSH_ERR_SIGNATURE_INVALID; | ||
347 | goto done; | ||
348 | } | ||
349 | /* Ensure that RSA keys use an acceptable signature algorithm */ | ||
350 | if (sshkey_type_plain(key->type) == KEY_RSA) { | ||
351 | if ((r = sshkey_get_sigtype(sig, siglen, &sigtype)) != 0) { | ||
352 | error("Couldn't verify signature: unable to get " | ||
353 | "signature type: %s", ssh_err(r)); | ||
354 | goto done; | ||
355 | } | ||
356 | if (match_pattern_list(sigtype, RSA_SIGN_ALLOWED, 0) != 1) { | ||
357 | error("Couldn't verify signature: unsupported RSA " | ||
358 | "signature algorithm %s", sigtype); | ||
359 | r = SSH_ERR_SIGN_ALG_UNSUPPORTED; | ||
360 | goto done; | ||
361 | } | ||
362 | } | ||
363 | if ((r = sshkey_verify(key, sig, siglen, sshbuf_ptr(toverify), | ||
364 | sshbuf_len(toverify), NULL, 0)) != 0) { | ||
365 | error("Signature verification failed: %s", ssh_err(r)); | ||
366 | goto done; | ||
367 | } | ||
368 | |||
369 | /* success */ | ||
370 | r = 0; | ||
371 | if (sign_keyp != NULL) { | ||
372 | *sign_keyp = key; | ||
373 | key = NULL; /* transferred */ | ||
374 | } | ||
375 | done: | ||
376 | free(got_namespace); | ||
377 | free(sigtype); | ||
378 | free(sig_hashalg); | ||
379 | sshbuf_free(buf); | ||
380 | sshbuf_free(toverify); | ||
381 | sshkey_free(key); | ||
382 | return r; | ||
383 | } | ||
384 | |||
385 | int | ||
386 | sshsig_sign_message(struct sshkey *key, const char *hashalg, | ||
387 | const struct sshbuf *message, const char *sig_namespace, | ||
388 | struct sshbuf **out, sshsig_signer *signer, void *signer_ctx) | ||
389 | { | ||
390 | u_char hash[SSH_DIGEST_MAX_LENGTH]; | ||
391 | struct sshbuf *b = NULL; | ||
392 | int alg, r = SSH_ERR_INTERNAL_ERROR; | ||
393 | |||
394 | if (out != NULL) | ||
395 | *out = NULL; | ||
396 | if (hashalg == NULL) | ||
397 | hashalg = HASHALG_DEFAULT; | ||
398 | |||
399 | if ((r = sshsig_check_hashalg(hashalg)) != 0) | ||
400 | return r; | ||
401 | if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { | ||
402 | error("%s: can't look up hash algorithm %s", | ||
403 | __func__, HASHALG_DEFAULT); | ||
404 | return SSH_ERR_INTERNAL_ERROR; | ||
405 | } | ||
406 | if ((r = ssh_digest_buffer(alg, message, hash, sizeof(hash))) != 0) { | ||
407 | error("%s: ssh_digest_buffer failed: %s", __func__, ssh_err(r)); | ||
408 | return r; | ||
409 | } | ||
410 | if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { | ||
411 | error("%s: sshbuf_from failed", __func__); | ||
412 | r = SSH_ERR_ALLOC_FAIL; | ||
413 | goto out; | ||
414 | } | ||
415 | if ((r = sshsig_wrap_sign(key, hashalg, b, sig_namespace, out, | ||
416 | signer, signer_ctx)) != 0) | ||
417 | goto out; | ||
418 | /* success */ | ||
419 | r = 0; | ||
420 | out: | ||
421 | sshbuf_free(b); | ||
422 | explicit_bzero(hash, sizeof(hash)); | ||
423 | return r; | ||
424 | } | ||
425 | |||
426 | int | ||
427 | sshsig_verify_message(struct sshbuf *signature, const struct sshbuf *message, | ||
428 | const char *expect_namespace, struct sshkey **sign_keyp) | ||
429 | { | ||
430 | u_char hash[SSH_DIGEST_MAX_LENGTH]; | ||
431 | struct sshbuf *b = NULL; | ||
432 | int alg, r = SSH_ERR_INTERNAL_ERROR; | ||
433 | char *hashalg = NULL; | ||
434 | |||
435 | if (sign_keyp != NULL) | ||
436 | *sign_keyp = NULL; | ||
437 | |||
438 | if ((r = sshsig_peek_hashalg(signature, &hashalg)) != 0) | ||
439 | return r; | ||
440 | if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { | ||
441 | error("%s: can't look up hash algorithm %s", | ||
442 | __func__, HASHALG_DEFAULT); | ||
443 | return SSH_ERR_INTERNAL_ERROR; | ||
444 | } | ||
445 | if ((r = ssh_digest_buffer(alg, message, hash, sizeof(hash))) != 0) { | ||
446 | error("%s: ssh_digest_buffer failed: %s", __func__, ssh_err(r)); | ||
447 | goto out; | ||
448 | } | ||
449 | if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { | ||
450 | error("%s: sshbuf_from failed", __func__); | ||
451 | r = SSH_ERR_ALLOC_FAIL; | ||
452 | goto out; | ||
453 | } | ||
454 | if ((r = sshsig_wrap_verify(signature, hashalg, b, expect_namespace, | ||
455 | sign_keyp)) != 0) | ||
456 | goto out; | ||
457 | /* success */ | ||
458 | r = 0; | ||
459 | out: | ||
460 | sshbuf_free(b); | ||
461 | free(hashalg); | ||
462 | explicit_bzero(hash, sizeof(hash)); | ||
463 | return r; | ||
464 | } | ||
465 | |||
466 | static int | ||
467 | hash_file(int fd, int hashalg, u_char *hash, size_t hashlen) | ||
468 | { | ||
469 | char *hex, rbuf[8192]; | ||
470 | ssize_t n, total = 0; | ||
471 | struct ssh_digest_ctx *ctx; | ||
472 | int r, oerrno; | ||
473 | |||
474 | memset(hash, 0, hashlen); | ||
475 | if ((ctx = ssh_digest_start(hashalg)) == NULL) { | ||
476 | error("%s: ssh_digest_start failed", __func__); | ||
477 | return SSH_ERR_INTERNAL_ERROR; | ||
478 | } | ||
479 | for (;;) { | ||
480 | if ((n = read(fd, rbuf, sizeof(rbuf))) == -1) { | ||
481 | if (errno == EINTR || errno == EAGAIN) | ||
482 | continue; | ||
483 | oerrno = errno; | ||
484 | error("%s: read: %s", __func__, strerror(errno)); | ||
485 | ssh_digest_free(ctx); | ||
486 | errno = oerrno; | ||
487 | return SSH_ERR_SYSTEM_ERROR; | ||
488 | } else if (n == 0) { | ||
489 | debug2("%s: hashed %zu bytes", __func__, total); | ||
490 | break; /* EOF */ | ||
491 | } | ||
492 | total += (size_t)n; | ||
493 | if ((r = ssh_digest_update(ctx, rbuf, (size_t)n)) != 0) { | ||
494 | error("%s: ssh_digest_update: %s", | ||
495 | __func__, ssh_err(r)); | ||
496 | ssh_digest_free(ctx); | ||
497 | return r; | ||
498 | } | ||
499 | } | ||
500 | if ((r = ssh_digest_final(ctx, hash, hashlen)) != 0) { | ||
501 | error("%s: ssh_digest_final: %s", __func__, ssh_err(r)); | ||
502 | ssh_digest_free(ctx); | ||
503 | } | ||
504 | if ((hex = tohex(hash, hashlen)) != NULL) { | ||
505 | debug3("%s: final hash: %s", __func__, hex); | ||
506 | freezero(hex, strlen(hex)); | ||
507 | } | ||
508 | /* success */ | ||
509 | ssh_digest_free(ctx); | ||
510 | return 0; | ||
511 | } | ||
512 | |||
513 | int | ||
514 | sshsig_sign_fd(struct sshkey *key, const char *hashalg, | ||
515 | int fd, const char *sig_namespace, struct sshbuf **out, | ||
516 | sshsig_signer *signer, void *signer_ctx) | ||
517 | { | ||
518 | u_char hash[SSH_DIGEST_MAX_LENGTH]; | ||
519 | struct sshbuf *b = NULL; | ||
520 | int alg, r = SSH_ERR_INTERNAL_ERROR; | ||
521 | |||
522 | if (out != NULL) | ||
523 | *out = NULL; | ||
524 | if (hashalg == NULL) | ||
525 | hashalg = HASHALG_DEFAULT; | ||
526 | |||
527 | if ((r = sshsig_check_hashalg(hashalg)) != 0) | ||
528 | return r; | ||
529 | if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { | ||
530 | error("%s: can't look up hash algorithm %s", | ||
531 | __func__, HASHALG_DEFAULT); | ||
532 | return SSH_ERR_INTERNAL_ERROR; | ||
533 | } | ||
534 | if ((r = hash_file(fd, alg, hash, sizeof(hash))) != 0) { | ||
535 | error("%s: hash_file failed: %s", __func__, ssh_err(r)); | ||
536 | return r; | ||
537 | } | ||
538 | if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { | ||
539 | error("%s: sshbuf_from failed", __func__); | ||
540 | r = SSH_ERR_ALLOC_FAIL; | ||
541 | goto out; | ||
542 | } | ||
543 | if ((r = sshsig_wrap_sign(key, hashalg, b, sig_namespace, out, | ||
544 | signer, signer_ctx)) != 0) | ||
545 | goto out; | ||
546 | /* success */ | ||
547 | r = 0; | ||
548 | out: | ||
549 | sshbuf_free(b); | ||
550 | explicit_bzero(hash, sizeof(hash)); | ||
551 | return r; | ||
552 | } | ||
553 | |||
554 | int | ||
555 | sshsig_verify_fd(struct sshbuf *signature, int fd, | ||
556 | const char *expect_namespace, struct sshkey **sign_keyp) | ||
557 | { | ||
558 | u_char hash[SSH_DIGEST_MAX_LENGTH]; | ||
559 | struct sshbuf *b = NULL; | ||
560 | int alg, r = SSH_ERR_INTERNAL_ERROR; | ||
561 | char *hashalg = NULL; | ||
562 | |||
563 | if (sign_keyp != NULL) | ||
564 | *sign_keyp = NULL; | ||
565 | |||
566 | if ((r = sshsig_peek_hashalg(signature, &hashalg)) != 0) | ||
567 | return r; | ||
568 | if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) { | ||
569 | error("%s: can't look up hash algorithm %s", | ||
570 | __func__, HASHALG_DEFAULT); | ||
571 | return SSH_ERR_INTERNAL_ERROR; | ||
572 | } | ||
573 | if ((r = hash_file(fd, alg, hash, sizeof(hash))) != 0) { | ||
574 | error("%s: hash_file failed: %s", __func__, ssh_err(r)); | ||
575 | return r; | ||
576 | } | ||
577 | if ((b = sshbuf_from(hash, ssh_digest_bytes(alg))) == NULL) { | ||
578 | error("%s: sshbuf_from failed", __func__); | ||
579 | r = SSH_ERR_ALLOC_FAIL; | ||
580 | goto out; | ||
581 | } | ||
582 | if ((r = sshsig_wrap_verify(signature, hashalg, b, expect_namespace, | ||
583 | sign_keyp)) != 0) | ||
584 | goto out; | ||
585 | /* success */ | ||
586 | r = 0; | ||
587 | out: | ||
588 | sshbuf_free(b); | ||
589 | free(hashalg); | ||
590 | explicit_bzero(hash, sizeof(hash)); | ||
591 | return r; | ||
592 | } | ||
593 | |||
594 | struct sigopts { | ||
595 | int ca; | ||
596 | char *namespaces; | ||
597 | }; | ||
598 | |||
599 | static struct sigopts * | ||
600 | sigopts_parse(const char *opts, const char *path, u_long linenum, | ||
601 | const char **errstrp) | ||
602 | { | ||
603 | struct sigopts *ret; | ||
604 | int r; | ||
605 | const char *errstr = NULL; | ||
606 | |||
607 | if ((ret = calloc(1, sizeof(*ret))) == NULL) | ||
608 | return NULL; | ||
609 | if (opts == NULL || *opts == '\0') | ||
610 | return ret; /* Empty options yields empty options :) */ | ||
611 | |||
612 | while (*opts && *opts != ' ' && *opts != '\t') { | ||
613 | /* flag options */ | ||
614 | if ((r = opt_flag("cert-authority", 0, &opts)) != -1) { | ||
615 | ret->ca = 1; | ||
616 | } else if (opt_match(&opts, "namespaces")) { | ||
617 | if (ret->namespaces != NULL) { | ||
618 | errstr = "multiple \"namespaces\" clauses"; | ||
619 | goto fail; | ||
620 | } | ||
621 | ret->namespaces = opt_dequote(&opts, &errstr); | ||
622 | if (ret->namespaces == NULL) | ||
623 | goto fail; | ||
624 | } | ||
625 | /* | ||
626 | * Skip the comma, and move to the next option | ||
627 | * (or break out if there are no more). | ||
628 | */ | ||
629 | if (*opts == '\0' || *opts == ' ' || *opts == '\t') | ||
630 | break; /* End of options. */ | ||
631 | /* Anything other than a comma is an unknown option */ | ||
632 | if (*opts != ',') { | ||
633 | errstr = "unknown key option"; | ||
634 | goto fail; | ||
635 | } | ||
636 | opts++; | ||
637 | if (*opts == '\0') { | ||
638 | errstr = "unexpected end-of-options"; | ||
639 | goto fail; | ||
640 | } | ||
641 | } | ||
642 | /* success */ | ||
643 | return ret; | ||
644 | fail: | ||
645 | if (errstrp != NULL) | ||
646 | *errstrp = errstr; | ||
647 | free(ret); | ||
648 | return NULL; | ||
649 | } | ||
650 | |||
651 | static void | ||
652 | sigopts_free(struct sigopts *opts) | ||
653 | { | ||
654 | if (opts == NULL) | ||
655 | return; | ||
656 | free(opts->namespaces); | ||
657 | free(opts); | ||
658 | } | ||
659 | |||
660 | static int | ||
661 | check_allowed_keys_line(const char *path, u_long linenum, char *line, | ||
662 | const struct sshkey *sign_key, const char *principal, | ||
663 | const char *sig_namespace) | ||
664 | { | ||
665 | struct sshkey *found_key = NULL; | ||
666 | char *cp, *opts = NULL, *identities = NULL; | ||
667 | int r, found = 0; | ||
668 | const char *reason = NULL; | ||
669 | struct sigopts *sigopts = NULL; | ||
670 | |||
671 | if ((found_key = sshkey_new(KEY_UNSPEC)) == NULL) { | ||
672 | error("%s: sshkey_new failed", __func__); | ||
673 | return SSH_ERR_ALLOC_FAIL; | ||
674 | } | ||
675 | |||
676 | /* format: identity[,identity...] [option[,option...]] key */ | ||
677 | cp = line; | ||
678 | cp = cp + strspn(cp, " \t"); /* skip leading whitespace */ | ||
679 | if (*cp == '#' || *cp == '\0') | ||
680 | goto done; | ||
681 | if ((identities = strdelimw(&cp)) == NULL) { | ||
682 | error("%s:%lu: invalid line", path, linenum); | ||
683 | goto done; | ||
684 | } | ||
685 | if (match_pattern_list(principal, identities, 0) != 1) { | ||
686 | /* principal didn't match */ | ||
687 | goto done; | ||
688 | } | ||
689 | debug("%s: %s:%lu: matched principal \"%s\"", | ||
690 | __func__, path, linenum, principal); | ||
691 | |||
692 | if (sshkey_read(found_key, &cp) != 0) { | ||
693 | /* no key? Check for options */ | ||
694 | opts = cp; | ||
695 | if (sshkey_advance_past_options(&cp) != 0) { | ||
696 | error("%s:%lu: invalid options", | ||
697 | path, linenum); | ||
698 | goto done; | ||
699 | } | ||
700 | *cp++ = '\0'; | ||
701 | skip_space(&cp); | ||
702 | if (sshkey_read(found_key, &cp) != 0) { | ||
703 | error("%s:%lu: invalid key", path, | ||
704 | linenum); | ||
705 | goto done; | ||
706 | } | ||
707 | } | ||
708 | debug3("%s:%lu: options %s", path, linenum, opts == NULL ? "" : opts); | ||
709 | if ((sigopts = sigopts_parse(opts, path, linenum, &reason)) == NULL) { | ||
710 | error("%s:%lu: bad options: %s", path, linenum, reason); | ||
711 | goto done; | ||
712 | } | ||
713 | |||
714 | /* Check whether options preclude the use of this key */ | ||
715 | if (sigopts->namespaces != NULL && | ||
716 | match_pattern_list(sig_namespace, sigopts->namespaces, 0) != 1) { | ||
717 | error("%s:%lu: key is not permitted for use in signature " | ||
718 | "namespace \"%s\"", path, linenum, sig_namespace); | ||
719 | goto done; | ||
720 | } | ||
721 | |||
722 | if (!sigopts->ca && sshkey_equal(found_key, sign_key)) { | ||
723 | /* Exact match of key */ | ||
724 | debug("%s:%lu: matched key and principal", path, linenum); | ||
725 | /* success */ | ||
726 | found = 1; | ||
727 | } else if (sigopts->ca && sshkey_is_cert(sign_key) && | ||
728 | sshkey_equal_public(sign_key->cert->signature_key, found_key)) { | ||
729 | /* Match of certificate's CA key */ | ||
730 | if ((r = sshkey_cert_check_authority(sign_key, 0, 1, | ||
731 | principal, &reason)) != 0) { | ||
732 | error("%s:%lu: certificate not authorized: %s", | ||
733 | path, linenum, reason); | ||
734 | goto done; | ||
735 | } | ||
736 | debug("%s:%lu: matched certificate CA key", path, linenum); | ||
737 | /* success */ | ||
738 | found = 1; | ||
739 | } else { | ||
740 | /* Principal matched but key didn't */ | ||
741 | goto done; | ||
742 | } | ||
743 | done: | ||
744 | sshkey_free(found_key); | ||
745 | sigopts_free(sigopts); | ||
746 | return found ? 0 : SSH_ERR_KEY_NOT_FOUND; | ||
747 | } | ||
748 | |||
749 | int | ||
750 | sshsig_check_allowed_keys(const char *path, const struct sshkey *sign_key, | ||
751 | const char *principal, const char *sig_namespace) | ||
752 | { | ||
753 | FILE *f = NULL; | ||
754 | char *line = NULL; | ||
755 | size_t linesize = 0; | ||
756 | u_long linenum = 0; | ||
757 | int r, oerrno; | ||
758 | |||
759 | /* Check key and principal against file */ | ||
760 | if ((f = fopen(path, "r")) == NULL) { | ||
761 | oerrno = errno; | ||
762 | error("Unable to open allowed keys file \"%s\": %s", | ||
763 | path, strerror(errno)); | ||
764 | errno = oerrno; | ||
765 | return SSH_ERR_SYSTEM_ERROR; | ||
766 | } | ||
767 | |||
768 | while (getline(&line, &linesize, f) != -1) { | ||
769 | linenum++; | ||
770 | r = check_allowed_keys_line(path, linenum, line, sign_key, | ||
771 | principal, sig_namespace); | ||
772 | if (r == SSH_ERR_KEY_NOT_FOUND) | ||
773 | continue; | ||
774 | else if (r == 0) { | ||
775 | /* success */ | ||
776 | fclose(f); | ||
777 | free(line); | ||
778 | return 0; | ||
779 | /* XXX continue and check revocation? */ | ||
780 | } else | ||
781 | break; | ||
782 | } | ||
783 | /* Either we hit an error parsing or we simply didn't find the key */ | ||
784 | fclose(f); | ||
785 | free(line); | ||
786 | return r == 0 ? SSH_ERR_KEY_NOT_FOUND : r; | ||
787 | } | ||