xref: /freebsd/crypto/openssh/regress/misc/ssh-verify-attestation/ssh-verify-attestation.c (revision 8e28d84935f2f0ee081d44f9803f3052b960e50b)
1 /* $OpenBSD: ssh-verify-attestation.c,v 1.2 2024/12/06 10:37:42 djm 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 
74 extern char *__progname;
75 
76 #define ATTEST_MAGIC	"ssh-sk-attest-v01"
77 
78 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)79 prepare_fido_cred(fido_cred_t *cred, int credtype, const char *attfmt,
80     const char *rp_id, struct sshbuf *b, const struct sshbuf *challenge,
81     struct sshbuf **attestation_certp)
82 {
83 	struct sshbuf *attestation_cert = NULL, *sig = NULL, *authdata = NULL;
84 	char *magic = NULL;
85 	int r = SSH_ERR_INTERNAL_ERROR;
86 
87 	*attestation_certp = NULL;
88 
89 	/* Make sure it's the format we're expecting */
90 	if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0) {
91 		error_fr(r, "parse header");
92 		goto out;
93 	}
94 	if (strcmp(magic, ATTEST_MAGIC) != 0) {
95 		error_f("unsupported format");
96 		r = SSH_ERR_INVALID_FORMAT;
97 		goto out;
98 	}
99 	/* Parse the remaining fields */
100 	if ((r = sshbuf_froms(b, &attestation_cert)) != 0 ||
101 	    (r = sshbuf_froms(b, &sig)) != 0 ||
102 	    (r = sshbuf_froms(b, &authdata)) != 0 ||
103 	    (r = sshbuf_get_u32(b, NULL)) != 0 || /* reserved flags */
104 	    (r = sshbuf_get_string_direct(b, NULL, NULL)) != 0) { /* reserved */
105 		error_fr(r, "parse body");
106 		goto out;
107 	}
108 	debug3_f("attestation cert len=%zu, sig len=%zu, "
109 	    "authdata len=%zu challenge len=%zu", sshbuf_len(attestation_cert),
110 	    sshbuf_len(sig), sshbuf_len(authdata), sshbuf_len(challenge));
111 
112 	fido_cred_set_type(cred, credtype);
113 	fido_cred_set_fmt(cred, attfmt);
114 	fido_cred_set_clientdata(cred, sshbuf_ptr(challenge),
115 	    sshbuf_len(challenge));
116 	fido_cred_set_rp(cred, rp_id, NULL);
117 	fido_cred_set_authdata(cred, sshbuf_ptr(authdata),
118 	    sshbuf_len(authdata));
119 	/* XXX set_extensions, set_rk, set_uv */
120 	fido_cred_set_x509(cred, sshbuf_ptr(attestation_cert),
121 	    sshbuf_len(attestation_cert));
122 	fido_cred_set_sig(cred, sshbuf_ptr(sig), sshbuf_len(sig));
123 
124 	/* success */
125 	*attestation_certp = attestation_cert;
126 	attestation_cert = NULL;
127 	r = 0;
128  out:
129 	free(magic);
130 	sshbuf_free(attestation_cert);
131 	sshbuf_free(sig);
132 	sshbuf_free(authdata);
133 	return r;
134 }
135 
136 static uint8_t *
get_pubkey_from_cred_ecdsa(const fido_cred_t * cred,size_t * pubkey_len)137 get_pubkey_from_cred_ecdsa(const fido_cred_t *cred, size_t *pubkey_len)
138 {
139 	const uint8_t *ptr;
140 	uint8_t *pubkey = NULL, *ret = NULL;
141 	BIGNUM *x = NULL, *y = NULL;
142 	EC_POINT *q = NULL;
143 	EC_GROUP *g = NULL;
144 
145 	if ((x = BN_new()) == NULL ||
146 	    (y = BN_new()) == NULL ||
147 	    (g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)) == NULL ||
148 	    (q = EC_POINT_new(g)) == NULL) {
149 		error_f("libcrypto setup failed");
150 		goto out;
151 	}
152 	if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) {
153 		error_f("fido_cred_pubkey_ptr failed");
154 		goto out;
155 	}
156 	if (fido_cred_pubkey_len(cred) != 64) {
157 		error_f("bad fido_cred_pubkey_len %zu",
158 		    fido_cred_pubkey_len(cred));
159 		goto out;
160 	}
161 
162 	if (BN_bin2bn(ptr, 32, x) == NULL ||
163 	    BN_bin2bn(ptr + 32, 32, y) == NULL) {
164 		error_f("BN_bin2bn failed");
165 		goto out;
166 	}
167 	if (EC_POINT_set_affine_coordinates_GFp(g, q, x, y, NULL) != 1) {
168 		error_f("EC_POINT_set_affine_coordinates_GFp failed");
169 		goto out;
170 	}
171 	*pubkey_len = EC_POINT_point2oct(g, q,
172 	    POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL);
173 	if (*pubkey_len == 0 || *pubkey_len > 2048) {
174 		error_f("bad pubkey length %zu", *pubkey_len);
175 		goto out;
176 	}
177 	if ((pubkey = malloc(*pubkey_len)) == NULL) {
178 		error_f("malloc pubkey failed");
179 		goto out;
180 	}
181 	if (EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED,
182 	    pubkey, *pubkey_len, NULL) == 0) {
183 		error_f("EC_POINT_point2oct failed");
184 		goto out;
185 	}
186 	/* success */
187 	ret = pubkey;
188 	pubkey = NULL;
189  out:
190 	free(pubkey);
191 	EC_POINT_free(q);
192 	EC_GROUP_free(g);
193 	BN_clear_free(x);
194 	BN_clear_free(y);
195 	return ret;
196 }
197 
198 /* copied from sshsk_ecdsa_assemble() */
199 static int
cred_matches_key_ecdsa(const fido_cred_t * cred,const struct sshkey * k)200 cred_matches_key_ecdsa(const fido_cred_t *cred, const struct sshkey *k)
201 {
202 	struct sshkey *key = NULL;
203 	struct sshbuf *b = NULL;
204 	EC_KEY *ec = NULL;
205 	uint8_t *pubkey = NULL;
206 	size_t pubkey_len;
207 	int r;
208 
209 	if ((key = sshkey_new(KEY_ECDSA_SK)) == NULL) {
210 		error_f("sshkey_new failed");
211 		r = SSH_ERR_ALLOC_FAIL;
212 		goto out;
213 	}
214 	key->ecdsa_nid = NID_X9_62_prime256v1;
215 	if ((key->pkey = EVP_PKEY_new()) == NULL ||
216 	    (ec = EC_KEY_new_by_curve_name(key->ecdsa_nid)) == NULL ||
217 	    (b = sshbuf_new()) == NULL) {
218 		error_f("allocation failed");
219 		r = SSH_ERR_ALLOC_FAIL;
220 		goto out;
221 	}
222 	if ((pubkey = get_pubkey_from_cred_ecdsa(cred, &pubkey_len)) == NULL) {
223 		error_f("get_pubkey_from_cred_ecdsa failed");
224 		r = SSH_ERR_INVALID_FORMAT;
225 		goto out;
226 	}
227 	if ((r = sshbuf_put_string(b, pubkey, pubkey_len)) != 0) {
228 		error_fr(r, "sshbuf_put_string");
229 		goto out;
230 	}
231 	if ((r = sshbuf_get_eckey(b, ec)) != 0) {
232 		error_fr(r, "parse");
233 		r = SSH_ERR_INVALID_FORMAT;
234 		goto out;
235 	}
236 	if (sshkey_ec_validate_public(EC_KEY_get0_group(ec),
237 	    EC_KEY_get0_public_key(ec)) != 0) {
238 		error("Authenticator returned invalid ECDSA key");
239 		r = SSH_ERR_KEY_INVALID_EC_VALUE;
240 		goto out;
241 	}
242 	if (EVP_PKEY_set1_EC_KEY(key->pkey, ec) != 1) {
243 		/* XXX assume it is a allocation error */
244 		error_f("allocation failed");
245 		r = SSH_ERR_ALLOC_FAIL;
246 		goto out;
247 	}
248 	key->sk_application = xstrdup(k->sk_application); /* XXX */
249 	if (!sshkey_equal_public(key, k)) {
250 		error("sshkey_equal_public failed");
251 		r = SSH_ERR_INVALID_ARGUMENT;
252 		goto out;
253 	}
254 	r = 0; /* success */
255  out:
256 	EC_KEY_free(ec);
257 	free(pubkey);
258 	sshkey_free(key);
259 	sshbuf_free(b);
260 	return r;
261 }
262 
263 
264 /* copied from sshsk_ed25519_assemble() */
265 static int
cred_matches_key_ed25519(const fido_cred_t * cred,const struct sshkey * k)266 cred_matches_key_ed25519(const fido_cred_t *cred, const struct sshkey *k)
267 {
268 	struct sshkey *key = NULL;
269 	const uint8_t *ptr;
270 	int r = -1;
271 
272 	if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) {
273 		error_f("fido_cred_pubkey_ptr failed");
274 		goto out;
275 	}
276 	if (fido_cred_pubkey_len(cred) != ED25519_PK_SZ) {
277 		error_f("bad fido_cred_pubkey_len %zu",
278 		    fido_cred_pubkey_len(cred));
279 		goto out;
280 	}
281 
282 	if ((key = sshkey_new(KEY_ED25519_SK)) == NULL) {
283 		error_f("sshkey_new failed");
284 		r = SSH_ERR_ALLOC_FAIL;
285 		goto out;
286 	}
287 	if ((key->ed25519_pk = malloc(ED25519_PK_SZ)) == NULL) {
288 		error_f("malloc failed");
289 		r = SSH_ERR_ALLOC_FAIL;
290 		goto out;
291 	}
292 	memcpy(key->ed25519_pk, ptr, ED25519_PK_SZ);
293 	key->sk_application = xstrdup(k->sk_application); /* XXX */
294 	if (!sshkey_equal_public(key, k)) {
295 		error("sshkey_equal_public failed");
296 		r = SSH_ERR_INVALID_ARGUMENT;
297 		goto out;
298 	}
299 	r = 0; /* success */
300  out:
301 	sshkey_free(key);
302 	return r;
303 }
304 
305 static int
cred_matches_key(const fido_cred_t * cred,const struct sshkey * k)306 cred_matches_key(const fido_cred_t *cred, const struct sshkey *k)
307 {
308 	switch (sshkey_type_plain(k->type)) {
309 	case KEY_ECDSA_SK:
310 		switch (k->ecdsa_nid) {
311 		case NID_X9_62_prime256v1:
312 			return cred_matches_key_ecdsa(cred, k);
313 			break;
314 		default:
315 			fatal("Unsupported ECDSA key size");
316 		}
317 		break;
318 	case KEY_ED25519_SK:
319 		return cred_matches_key_ed25519(cred, k);
320 	default:
321 		error_f("key type %s not supported", sshkey_type(k));
322 		return -1;
323 	}
324 }
325 
326 int
main(int argc,char ** argv)327 main(int argc, char **argv)
328 {
329 	LogLevel log_level = SYSLOG_LEVEL_INFO;
330 	int r, ch, credtype = -1;
331 	struct sshkey *k = NULL;
332 	struct sshbuf *attestation = NULL, *challenge = NULL;
333 	struct sshbuf *attestation_cert = NULL;
334 	char *fp;
335 	const char *attfmt = "packed", *style = NULL;
336 	fido_cred_t *cred = NULL;
337 	int write_attestation_cert = 0;
338 	extern int optind;
339 	/* extern char *optarg; */
340 
341 	ERR_load_crypto_strings();
342 
343 	sanitise_stdfd();
344 	log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1);
345 
346 	while ((ch = getopt(argc, argv, "UAv")) != -1) {
347 		switch (ch) {
348 		case 'U':
349 			attfmt = "fido-u2f";
350 			break;
351 		case 'A':
352 			write_attestation_cert = 1;
353 			break;
354 		case 'v':
355 			if (log_level == SYSLOG_LEVEL_ERROR)
356 				log_level = SYSLOG_LEVEL_DEBUG1;
357 			else if (log_level < SYSLOG_LEVEL_DEBUG3)
358 				log_level++;
359 			break;
360 		default:
361 			goto usage;
362 		}
363 	}
364 	log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1);
365 	argv += optind;
366 	argc -= optind;
367 
368 	if (argc < 3) {
369  usage:
370 		fprintf(stderr, "usage: %s [-vAU] "
371 		   "pubkey challenge attestation-blob\n", __progname);
372 		exit(1);
373 	}
374 	if ((r = sshkey_load_public(argv[0], &k, NULL)) != 0)
375 		fatal_r(r, "load key %s", argv[0]);
376 	if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT,
377 	    SSH_FP_DEFAULT)) == NULL)
378 		fatal("sshkey_fingerprint failed");
379 	debug2("key %s: %s %s", argv[2], sshkey_type(k), fp);
380 	free(fp);
381 	if ((r = sshbuf_load_file(argv[1], &challenge)) != 0)
382 		fatal_r(r, "load challenge %s", argv[1]);
383 	if ((r = sshbuf_load_file(argv[2], &attestation)) != 0)
384 		fatal_r(r, "load attestation %s", argv[2]);
385 	if ((cred = fido_cred_new()) == NULL)
386 		fatal("fido_cred_new failed");
387 
388 	switch (sshkey_type_plain(k->type)) {
389 	case KEY_ECDSA_SK:
390 		switch (k->ecdsa_nid) {
391 		case NID_X9_62_prime256v1:
392 			credtype = COSE_ES256;
393 			break;
394 		default:
395 			fatal("Unsupported ECDSA key size");
396 		}
397 		break;
398 	case KEY_ED25519_SK:
399 		credtype = COSE_EDDSA;
400 		break;
401 	default:
402 		fatal("unsupported key type %s", sshkey_type(k));
403 	}
404 
405 	if ((r = prepare_fido_cred(cred, credtype, attfmt, k->sk_application,
406 	    attestation, challenge, &attestation_cert)) != 0)
407 		fatal_r(r, "prepare_fido_cred %s", argv[2]);
408 	if (fido_cred_x5c_ptr(cred) != NULL) {
409 		debug("basic attestation");
410 		if ((r = fido_cred_verify(cred)) != FIDO_OK)
411 			fatal("basic attestation failed");
412 		style = "basic";
413 	} else {
414 		debug("self attestation");
415 		if ((r = fido_cred_verify_self(cred)) != FIDO_OK)
416 			fatal("self attestation failed");
417 		style = "self";
418 	}
419 	if (cred_matches_key(cred, k) != 0)
420 		fatal("cred authdata does not match key");
421 
422 	fido_cred_free(&cred);
423 
424 	if (write_attestation_cert) {
425 		PEM_write(stdout, "CERTIFICATE", NULL,
426 		    sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert));
427 	}
428 	sshbuf_free(attestation_cert);
429 
430 	logit("%s: verified %s attestation", argv[2], style);
431 
432 	return (0);
433 }
434