1 /* $OpenBSD: ssh-verify-attestation.c,v 1.3 2025/05/12 05:42:02 tb Exp $ */
2 /*
3 * Copyright (c) 2022-2024 Damien Miller
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18 /*
19 * This is a small program to verify FIDO attestation objects that
20 * ssh-keygen(1) can record when enrolling a FIDO key. It requires that
21 * the attestation object and challenge used when creating the key be
22 * recorded.
23 *
24 * Example usage:
25 *
26 * $ # Generate a random challenge.
27 * $ dd if=/dev/urandom of=key_ecdsa_sk.challenge bs=32 count=1
28 * $ # Generate a key, record the attestation blob.
29 * $ ssh-keygen -f key_ecdsa_sk -t ecdsa-sk \
30 * -Ochallenge=key_ecdsa_sk.challenge \
31 * -Owrite-attestation=key_ecdsa_sk.attest -N ''
32 * $ # Validate the challenge (-A = print attestation CA cert)
33 * $ ./obj/ssh-verify-attestation -A key_ecdsa_sk key_ecdsa_sk.challenge \
34 * key_ecdsa_sk.attest
35 *
36 * Limitations/TODO:
37 *
38 * 1) It doesn't automatically detect the attestation statement format. It
39 * assumes the "packed" format used by FIDO2 keys. If that doesn't work,
40 * then try using the -U option to select the "fido-u2f" format.
41 * 2) It makes assumptions about RK, UV, etc status of the key/cred.
42 * 3) Probably bugs.
43 *
44 * Thanks to Markus Friedl and Pedro Martelletto for help getting this
45 * working.
46 */
47
48 #include "includes.h"
49
50 #include <stdint.h>
51 #include <inttypes.h>
52 #include <stdlib.h>
53 #include <stdio.h>
54 #include <unistd.h>
55 #include <stdarg.h>
56
57 #include "xmalloc.h"
58 #include "log.h"
59 #include "sshbuf.h"
60 #include "sshkey.h"
61 #include "authfile.h"
62 #include "ssherr.h"
63 #include "misc.h"
64 #include "digest.h"
65 #include "crypto_api.h"
66
67 #include <fido.h>
68 #include <openssl/x509.h>
69 #include <openssl/x509v3.h>
70 #include <openssl/bio.h>
71 #include <openssl/err.h>
72 #include <openssl/pem.h>
73 #include "openbsd-compat/openssl-compat.h"
74
75 extern char *__progname;
76
77 #define ATTEST_MAGIC "ssh-sk-attest-v01"
78
79 static int
prepare_fido_cred(fido_cred_t * cred,int credtype,const char * attfmt,const char * rp_id,struct sshbuf * b,const struct sshbuf * challenge,struct sshbuf ** attestation_certp)80 prepare_fido_cred(fido_cred_t *cred, int credtype, const char *attfmt,
81 const char *rp_id, struct sshbuf *b, const struct sshbuf *challenge,
82 struct sshbuf **attestation_certp)
83 {
84 struct sshbuf *attestation_cert = NULL, *sig = NULL, *authdata = NULL;
85 char *magic = NULL;
86 int r = SSH_ERR_INTERNAL_ERROR;
87
88 *attestation_certp = NULL;
89
90 /* Make sure it's the format we're expecting */
91 if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0) {
92 error_fr(r, "parse header");
93 goto out;
94 }
95 if (strcmp(magic, ATTEST_MAGIC) != 0) {
96 error_f("unsupported format");
97 r = SSH_ERR_INVALID_FORMAT;
98 goto out;
99 }
100 /* Parse the remaining fields */
101 if ((r = sshbuf_froms(b, &attestation_cert)) != 0 ||
102 (r = sshbuf_froms(b, &sig)) != 0 ||
103 (r = sshbuf_froms(b, &authdata)) != 0 ||
104 (r = sshbuf_get_u32(b, NULL)) != 0 || /* reserved flags */
105 (r = sshbuf_get_string_direct(b, NULL, NULL)) != 0) { /* reserved */
106 error_fr(r, "parse body");
107 goto out;
108 }
109 debug3_f("attestation cert len=%zu, sig len=%zu, "
110 "authdata len=%zu challenge len=%zu", sshbuf_len(attestation_cert),
111 sshbuf_len(sig), sshbuf_len(authdata), sshbuf_len(challenge));
112
113 fido_cred_set_type(cred, credtype);
114 fido_cred_set_fmt(cred, attfmt);
115 fido_cred_set_clientdata(cred, sshbuf_ptr(challenge),
116 sshbuf_len(challenge));
117 fido_cred_set_rp(cred, rp_id, NULL);
118 fido_cred_set_authdata(cred, sshbuf_ptr(authdata),
119 sshbuf_len(authdata));
120 /* XXX set_extensions, set_rk, set_uv */
121 fido_cred_set_x509(cred, sshbuf_ptr(attestation_cert),
122 sshbuf_len(attestation_cert));
123 fido_cred_set_sig(cred, sshbuf_ptr(sig), sshbuf_len(sig));
124
125 /* success */
126 *attestation_certp = attestation_cert;
127 attestation_cert = NULL;
128 r = 0;
129 out:
130 free(magic);
131 sshbuf_free(attestation_cert);
132 sshbuf_free(sig);
133 sshbuf_free(authdata);
134 return r;
135 }
136
137 static uint8_t *
get_pubkey_from_cred_ecdsa(const fido_cred_t * cred,size_t * pubkey_len)138 get_pubkey_from_cred_ecdsa(const fido_cred_t *cred, size_t *pubkey_len)
139 {
140 const uint8_t *ptr;
141 uint8_t *pubkey = NULL, *ret = NULL;
142 BIGNUM *x = NULL, *y = NULL;
143 EC_POINT *q = NULL;
144 EC_GROUP *g = NULL;
145
146 if ((x = BN_new()) == NULL ||
147 (y = BN_new()) == NULL ||
148 (g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)) == NULL ||
149 (q = EC_POINT_new(g)) == NULL) {
150 error_f("libcrypto setup failed");
151 goto out;
152 }
153 if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) {
154 error_f("fido_cred_pubkey_ptr failed");
155 goto out;
156 }
157 if (fido_cred_pubkey_len(cred) != 64) {
158 error_f("bad fido_cred_pubkey_len %zu",
159 fido_cred_pubkey_len(cred));
160 goto out;
161 }
162
163 if (BN_bin2bn(ptr, 32, x) == NULL ||
164 BN_bin2bn(ptr + 32, 32, y) == NULL) {
165 error_f("BN_bin2bn failed");
166 goto out;
167 }
168 if (EC_POINT_set_affine_coordinates(g, q, x, y, NULL) != 1) {
169 error_f("EC_POINT_set_affine_coordinates failed");
170 goto out;
171 }
172 *pubkey_len = EC_POINT_point2oct(g, q,
173 POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL);
174 if (*pubkey_len == 0 || *pubkey_len > 2048) {
175 error_f("bad pubkey length %zu", *pubkey_len);
176 goto out;
177 }
178 if ((pubkey = malloc(*pubkey_len)) == NULL) {
179 error_f("malloc pubkey failed");
180 goto out;
181 }
182 if (EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED,
183 pubkey, *pubkey_len, NULL) == 0) {
184 error_f("EC_POINT_point2oct failed");
185 goto out;
186 }
187 /* success */
188 ret = pubkey;
189 pubkey = NULL;
190 out:
191 free(pubkey);
192 EC_POINT_free(q);
193 EC_GROUP_free(g);
194 BN_clear_free(x);
195 BN_clear_free(y);
196 return ret;
197 }
198
199 /* copied from sshsk_ecdsa_assemble() */
200 static int
cred_matches_key_ecdsa(const fido_cred_t * cred,const struct sshkey * k)201 cred_matches_key_ecdsa(const fido_cred_t *cred, const struct sshkey *k)
202 {
203 struct sshkey *key = NULL;
204 struct sshbuf *b = NULL;
205 EC_KEY *ec = NULL;
206 uint8_t *pubkey = NULL;
207 size_t pubkey_len;
208 int r;
209
210 if ((key = sshkey_new(KEY_ECDSA_SK)) == NULL) {
211 error_f("sshkey_new failed");
212 r = SSH_ERR_ALLOC_FAIL;
213 goto out;
214 }
215 key->ecdsa_nid = NID_X9_62_prime256v1;
216 if ((key->pkey = EVP_PKEY_new()) == NULL ||
217 (ec = EC_KEY_new_by_curve_name(key->ecdsa_nid)) == NULL ||
218 (b = sshbuf_new()) == NULL) {
219 error_f("allocation failed");
220 r = SSH_ERR_ALLOC_FAIL;
221 goto out;
222 }
223 if ((pubkey = get_pubkey_from_cred_ecdsa(cred, &pubkey_len)) == NULL) {
224 error_f("get_pubkey_from_cred_ecdsa failed");
225 r = SSH_ERR_INVALID_FORMAT;
226 goto out;
227 }
228 if ((r = sshbuf_put_string(b, pubkey, pubkey_len)) != 0) {
229 error_fr(r, "sshbuf_put_string");
230 goto out;
231 }
232 if ((r = sshbuf_get_eckey(b, ec)) != 0) {
233 error_fr(r, "parse");
234 r = SSH_ERR_INVALID_FORMAT;
235 goto out;
236 }
237 if (sshkey_ec_validate_public(EC_KEY_get0_group(ec),
238 EC_KEY_get0_public_key(ec)) != 0) {
239 error("Authenticator returned invalid ECDSA key");
240 r = SSH_ERR_KEY_INVALID_EC_VALUE;
241 goto out;
242 }
243 if (EVP_PKEY_set1_EC_KEY(key->pkey, ec) != 1) {
244 /* XXX assume it is a allocation error */
245 error_f("allocation failed");
246 r = SSH_ERR_ALLOC_FAIL;
247 goto out;
248 }
249 key->sk_application = xstrdup(k->sk_application); /* XXX */
250 if (!sshkey_equal_public(key, k)) {
251 error("sshkey_equal_public failed");
252 r = SSH_ERR_INVALID_ARGUMENT;
253 goto out;
254 }
255 r = 0; /* success */
256 out:
257 EC_KEY_free(ec);
258 free(pubkey);
259 sshkey_free(key);
260 sshbuf_free(b);
261 return r;
262 }
263
264
265 /* copied from sshsk_ed25519_assemble() */
266 static int
cred_matches_key_ed25519(const fido_cred_t * cred,const struct sshkey * k)267 cred_matches_key_ed25519(const fido_cred_t *cred, const struct sshkey *k)
268 {
269 struct sshkey *key = NULL;
270 const uint8_t *ptr;
271 int r = -1;
272
273 if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) {
274 error_f("fido_cred_pubkey_ptr failed");
275 goto out;
276 }
277 if (fido_cred_pubkey_len(cred) != ED25519_PK_SZ) {
278 error_f("bad fido_cred_pubkey_len %zu",
279 fido_cred_pubkey_len(cred));
280 goto out;
281 }
282
283 if ((key = sshkey_new(KEY_ED25519_SK)) == NULL) {
284 error_f("sshkey_new failed");
285 r = SSH_ERR_ALLOC_FAIL;
286 goto out;
287 }
288 if ((key->ed25519_pk = malloc(ED25519_PK_SZ)) == NULL) {
289 error_f("malloc failed");
290 r = SSH_ERR_ALLOC_FAIL;
291 goto out;
292 }
293 memcpy(key->ed25519_pk, ptr, ED25519_PK_SZ);
294 key->sk_application = xstrdup(k->sk_application); /* XXX */
295 if (!sshkey_equal_public(key, k)) {
296 error("sshkey_equal_public failed");
297 r = SSH_ERR_INVALID_ARGUMENT;
298 goto out;
299 }
300 r = 0; /* success */
301 out:
302 sshkey_free(key);
303 return r;
304 }
305
306 static int
cred_matches_key(const fido_cred_t * cred,const struct sshkey * k)307 cred_matches_key(const fido_cred_t *cred, const struct sshkey *k)
308 {
309 switch (sshkey_type_plain(k->type)) {
310 case KEY_ECDSA_SK:
311 switch (k->ecdsa_nid) {
312 case NID_X9_62_prime256v1:
313 return cred_matches_key_ecdsa(cred, k);
314 break;
315 default:
316 fatal("Unsupported ECDSA key size");
317 }
318 break;
319 case KEY_ED25519_SK:
320 return cred_matches_key_ed25519(cred, k);
321 default:
322 error_f("key type %s not supported", sshkey_type(k));
323 return -1;
324 }
325 }
326
327 int
main(int argc,char ** argv)328 main(int argc, char **argv)
329 {
330 LogLevel log_level = SYSLOG_LEVEL_INFO;
331 int r, ch, credtype = -1;
332 struct sshkey *k = NULL;
333 struct sshbuf *attestation = NULL, *challenge = NULL;
334 struct sshbuf *attestation_cert = NULL;
335 char *fp;
336 const char *attfmt = "packed", *style = NULL;
337 fido_cred_t *cred = NULL;
338 int write_attestation_cert = 0;
339 extern int optind;
340 /* extern char *optarg; */
341
342 ERR_load_crypto_strings();
343
344 sanitise_stdfd();
345 log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1);
346
347 while ((ch = getopt(argc, argv, "UAv")) != -1) {
348 switch (ch) {
349 case 'U':
350 attfmt = "fido-u2f";
351 break;
352 case 'A':
353 write_attestation_cert = 1;
354 break;
355 case 'v':
356 if (log_level == SYSLOG_LEVEL_ERROR)
357 log_level = SYSLOG_LEVEL_DEBUG1;
358 else if (log_level < SYSLOG_LEVEL_DEBUG3)
359 log_level++;
360 break;
361 default:
362 goto usage;
363 }
364 }
365 log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1);
366 argv += optind;
367 argc -= optind;
368
369 if (argc < 3) {
370 usage:
371 fprintf(stderr, "usage: %s [-vAU] "
372 "pubkey challenge attestation-blob\n", __progname);
373 exit(1);
374 }
375 if ((r = sshkey_load_public(argv[0], &k, NULL)) != 0)
376 fatal_r(r, "load key %s", argv[0]);
377 if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT,
378 SSH_FP_DEFAULT)) == NULL)
379 fatal("sshkey_fingerprint failed");
380 debug2("key %s: %s %s", argv[2], sshkey_type(k), fp);
381 free(fp);
382 if ((r = sshbuf_load_file(argv[1], &challenge)) != 0)
383 fatal_r(r, "load challenge %s", argv[1]);
384 if ((r = sshbuf_load_file(argv[2], &attestation)) != 0)
385 fatal_r(r, "load attestation %s", argv[2]);
386 if ((cred = fido_cred_new()) == NULL)
387 fatal("fido_cred_new failed");
388
389 switch (sshkey_type_plain(k->type)) {
390 case KEY_ECDSA_SK:
391 switch (k->ecdsa_nid) {
392 case NID_X9_62_prime256v1:
393 credtype = COSE_ES256;
394 break;
395 default:
396 fatal("Unsupported ECDSA key size");
397 }
398 break;
399 case KEY_ED25519_SK:
400 credtype = COSE_EDDSA;
401 break;
402 default:
403 fatal("unsupported key type %s", sshkey_type(k));
404 }
405
406 if ((r = prepare_fido_cred(cred, credtype, attfmt, k->sk_application,
407 attestation, challenge, &attestation_cert)) != 0)
408 fatal_r(r, "prepare_fido_cred %s", argv[2]);
409 if (fido_cred_x5c_ptr(cred) != NULL) {
410 debug("basic attestation");
411 if ((r = fido_cred_verify(cred)) != FIDO_OK)
412 fatal("basic attestation failed");
413 style = "basic";
414 } else {
415 debug("self attestation");
416 if ((r = fido_cred_verify_self(cred)) != FIDO_OK)
417 fatal("self attestation failed");
418 style = "self";
419 }
420 if (cred_matches_key(cred, k) != 0)
421 fatal("cred authdata does not match key");
422
423 fido_cred_free(&cred);
424
425 if (write_attestation_cert) {
426 PEM_write(stdout, "CERTIFICATE", NULL,
427 sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert));
428 }
429 sshbuf_free(attestation_cert);
430
431 logit("%s: verified %s attestation", argv[2], style);
432
433 return (0);
434 }
435