xref: /openbsd-src/regress/usr.bin/ssh/misc/ssh-verify-attestation/ssh-verify-attestation.c (revision a8b9d729612ea0b05a54e9350670f4fc06c738b1)
1 /* $OpenBSD: ssh-verify-attestation.c,v 1.1 2024/12/04 16:42:49 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) Only ECDSA keys are currently supported. Ed25519 support is not yet
42  *    implemented.
43  * 3) Probably bugs.
44  *
45  */
46 
47 #include <stdint.h>
48 #include <inttypes.h>
49 #include <stdlib.h>
50 #include <stdio.h>
51 #include <unistd.h>
52 #include <stdarg.h>
53 
54 #include "xmalloc.h"
55 #include "log.h"
56 #include "sshbuf.h"
57 #include "sshkey.h"
58 #include "authfile.h"
59 #include "ssherr.h"
60 #include "misc.h"
61 #include "digest.h"
62 
63 #include <fido.h>
64 #include <openssl/x509.h>
65 #include <openssl/x509v3.h>
66 #include <openssl/bio.h>
67 #include <openssl/err.h>
68 #include <openssl/pem.h>
69 
70 extern char *__progname;
71 
72 #define ATTEST_MAGIC	"ssh-sk-attest-v01"
73 
74 static int
75 prepare_fido_cred(fido_cred_t *cred, int credtype, const char *attfmt,
76     const char *rp_id, struct sshbuf *b, const struct sshbuf *challenge,
77     struct sshbuf **attestation_certp)
78 {
79 	struct sshbuf *attestation_cert = NULL, *sig = NULL, *authdata = NULL;
80 	char *magic = NULL;
81 	int r = SSH_ERR_INTERNAL_ERROR;
82 
83 	*attestation_certp = NULL;
84 
85 	/* Make sure it's the format we're expecting */
86 	if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0) {
87 		error_fr(r, "parse header");
88 		goto out;
89 	}
90 	if (strcmp(magic, ATTEST_MAGIC) != 0) {
91 		error_f("unsupported format");
92 		r = SSH_ERR_INVALID_FORMAT;
93 		goto out;
94 	}
95 	/* Parse the remaining fields */
96 	if ((r = sshbuf_froms(b, &attestation_cert)) != 0 ||
97 	    (r = sshbuf_froms(b, &sig)) != 0 ||
98 	    (r = sshbuf_froms(b, &authdata)) != 0 ||
99 	    (r = sshbuf_get_u32(b, NULL)) != 0 || /* reserved flags */
100 	    (r = sshbuf_get_string_direct(b, NULL, NULL)) != 0) { /* reserved */
101 		error_fr(r, "parse body");
102 		goto out;
103 	}
104 	debug3_f("blob len=%zu, attestation cert len=%zu, sig len=%zu, "
105 	    "authdata len=%zu challenge len=%zu", sshbuf_len(attestation_cert),
106 	    sshbuf_len(b), sshbuf_len(sig), sshbuf_len(authdata),
107 	    sshbuf_len(challenge));
108 
109 	fido_cred_set_type(cred, COSE_ES256);
110 	fido_cred_set_fmt(cred, attfmt);
111 	fido_cred_set_clientdata(cred, sshbuf_ptr(challenge),
112 	    sshbuf_len(challenge));
113 	fido_cred_set_rp(cred, rp_id, NULL);
114 	fido_cred_set_authdata(cred, sshbuf_ptr(authdata),
115 	    sshbuf_len(authdata));
116 	/* XXX set_extensions, set_rk, set_uv */
117 	fido_cred_set_x509(cred, sshbuf_ptr(attestation_cert),
118 	    sshbuf_len(attestation_cert));
119 	fido_cred_set_sig(cred, sshbuf_ptr(sig), sshbuf_len(sig));
120 
121 	/* success */
122 	*attestation_certp = attestation_cert;
123 	attestation_cert = NULL;
124 	r = 0;
125  out:
126 	free(magic);
127 	sshbuf_free(attestation_cert);
128 	sshbuf_free(sig);
129 	sshbuf_free(authdata);
130 	return r;
131 }
132 
133 static uint8_t *
134 get_pubkey_from_cred_ecdsa(const fido_cred_t *cred, size_t *pubkey_len)
135 {
136 	const uint8_t *ptr;
137 	uint8_t *pubkey = NULL, *ret = NULL;
138 	BIGNUM *x = NULL, *y = NULL;
139 	EC_POINT *q = NULL;
140 	EC_GROUP *g = NULL;
141 
142 	if ((x = BN_new()) == NULL ||
143 	    (y = BN_new()) == NULL ||
144 	    (g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)) == NULL ||
145 	    (q = EC_POINT_new(g)) == NULL) {
146 		error_f("libcrypto setup failed");
147 		goto out;
148 	}
149 	if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) {
150 		error_f("fido_cred_pubkey_ptr failed");
151 		goto out;
152 	}
153 	if (fido_cred_pubkey_len(cred) != 64) {
154 		error_f("bad fido_cred_pubkey_len %zu",
155 		    fido_cred_pubkey_len(cred));
156 		goto out;
157 	}
158 
159 	if (BN_bin2bn(ptr, 32, x) == NULL ||
160 	    BN_bin2bn(ptr + 32, 32, y) == NULL) {
161 		error_f("BN_bin2bn failed");
162 		goto out;
163 	}
164 	if (EC_POINT_set_affine_coordinates_GFp(g, q, x, y, NULL) != 1) {
165 		error_f("EC_POINT_set_affine_coordinates_GFp failed");
166 		goto out;
167 	}
168 	*pubkey_len = EC_POINT_point2oct(g, q,
169 	    POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL);
170 	if (*pubkey_len == 0 || *pubkey_len > 2048) {
171 		error_f("bad pubkey length %zu", *pubkey_len);
172 		goto out;
173 	}
174 	if ((pubkey = malloc(*pubkey_len)) == NULL) {
175 		error_f("malloc pubkey failed");
176 		goto out;
177 	}
178 	if (EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED,
179 	    pubkey, *pubkey_len, NULL) == 0) {
180 		error_f("EC_POINT_point2oct failed");
181 		goto out;
182 	}
183 	/* success */
184 	ret = pubkey;
185 	pubkey = NULL;
186  out:
187 	free(pubkey);
188 	EC_POINT_free(q);
189 	EC_GROUP_free(g);
190 	BN_clear_free(x);
191 	BN_clear_free(y);
192 	return ret;
193 }
194 
195 /* copied from sshsk_ecdsa_assemble() */
196 static int
197 cred_matches_key_ecdsa(const fido_cred_t *cred, const struct sshkey *k)
198 {
199 	struct sshkey *key = NULL;
200 	struct sshbuf *b = NULL;
201 	EC_KEY *ec = NULL;
202 	uint8_t *pubkey = NULL;
203 	size_t pubkey_len;
204 	int r;
205 
206 	if ((key = sshkey_new(KEY_ECDSA_SK)) == NULL) {
207 		error_f("sshkey_new failed");
208 		r = SSH_ERR_ALLOC_FAIL;
209 		goto out;
210 	}
211 	key->ecdsa_nid = NID_X9_62_prime256v1;
212 	if ((key->pkey = EVP_PKEY_new()) == NULL ||
213 	    (ec = EC_KEY_new_by_curve_name(key->ecdsa_nid)) == NULL ||
214 	    (b = sshbuf_new()) == NULL) {
215 		error_f("allocation failed");
216 		r = SSH_ERR_ALLOC_FAIL;
217 		goto out;
218 	}
219 	if ((pubkey = get_pubkey_from_cred_ecdsa(cred, &pubkey_len)) == NULL) {
220 		error_f("get_pubkey_from_cred_ecdsa failed");
221 		r = SSH_ERR_INVALID_FORMAT;
222 		goto out;
223 	}
224 	if ((r = sshbuf_put_string(b, pubkey, pubkey_len)) != 0) {
225 		error_fr(r, "sshbuf_put_string");
226 		goto out;
227 	}
228 	if ((r = sshbuf_get_eckey(b, ec)) != 0) {
229 		error_fr(r, "parse");
230 		r = SSH_ERR_INVALID_FORMAT;
231 		goto out;
232 	}
233 	if (sshkey_ec_validate_public(EC_KEY_get0_group(ec),
234 	    EC_KEY_get0_public_key(ec)) != 0) {
235 		error("Authenticator returned invalid ECDSA key");
236 		r = SSH_ERR_KEY_INVALID_EC_VALUE;
237 		goto out;
238 	}
239 	if (EVP_PKEY_set1_EC_KEY(key->pkey, ec) != 1) {
240 		/* XXX assume it is a allocation error */
241 		error_f("allocation failed");
242 		r = SSH_ERR_ALLOC_FAIL;
243 		goto out;
244 	}
245 	key->sk_application = xstrdup(k->sk_application); /* XXX */
246 	if (!sshkey_equal_public(key, k)) {
247 		error("sshkey_equal_public failed");
248 		r = SSH_ERR_INVALID_ARGUMENT;
249 		goto out;
250 	}
251 	r = 0; /* success */
252  out:
253 	EC_KEY_free(ec);
254 	free(pubkey);
255 	sshkey_free(key);
256 	sshbuf_free(b);
257 	return r;
258 }
259 
260 static int
261 cred_matches_key(const fido_cred_t *cred, const struct sshkey *k)
262 {
263 	switch (sshkey_type_plain(k->type)) {
264 	case KEY_ECDSA_SK:
265 		return cred_matches_key_ecdsa(cred, k);
266 	default:
267 		error_f("key type %s not supported", sshkey_type(k));
268 		return -1;
269 	}
270 }
271 
272 int
273 main(int argc, char **argv)
274 {
275 	LogLevel log_level = SYSLOG_LEVEL_INFO;
276 	int r, ch, credtype = -1;
277 	struct sshkey *k = NULL;
278 	struct sshbuf *attestation = NULL, *challenge = NULL;
279 	struct sshbuf *attestation_cert = NULL;
280 	char *fp;
281 	const char *attfmt = "packed";
282 	fido_cred_t *cred = NULL;
283 	int write_attestation_cert = 0;
284 	extern int optind;
285 	/* extern char *optarg; */
286 
287 	ERR_load_crypto_strings();
288 
289 	sanitise_stdfd();
290 	log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1);
291 
292 	while ((ch = getopt(argc, argv, "UAv")) != -1) {
293 		switch (ch) {
294 		case 'U':
295 			attfmt = "fido-u2f";
296 			break;
297 		case 'A':
298 			write_attestation_cert = 1;
299 			break;
300 		case 'v':
301 			if (log_level == SYSLOG_LEVEL_ERROR)
302 				log_level = SYSLOG_LEVEL_DEBUG1;
303 			else if (log_level < SYSLOG_LEVEL_DEBUG3)
304 				log_level++;
305 			break;
306 		default:
307 			goto usage;
308 		}
309 	}
310 	log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1);
311 	argv += optind;
312 	argc -= optind;
313 
314 	if (argc < 3) {
315  usage:
316 		fprintf(stderr, "usage: %s [-vAU] "
317 		   "pubkey challenge attestation-blob\n", __progname);
318 		exit(1);
319 	}
320 	if ((r = sshkey_load_public(argv[0], &k, NULL)) != 0)
321 		fatal_r(r, "load key %s", argv[0]);
322 	if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT,
323 	    SSH_FP_DEFAULT)) == NULL)
324 		fatal("sshkey_fingerprint failed");
325 	debug2("key %s: %s %s", argv[2], sshkey_type(k), fp);
326 	free(fp);
327 	if ((r = sshbuf_load_file(argv[1], &challenge)) != 0)
328 		fatal_r(r, "load challenge %s", argv[1]);
329 	if ((r = sshbuf_load_file(argv[2], &attestation)) != 0)
330 		fatal_r(r, "load attestation %s", argv[2]);
331 	if ((cred = fido_cred_new()) == NULL)
332 		fatal("fido_cred_new failed");
333 
334 	switch (sshkey_type_plain(k->type)) {
335 	case KEY_ECDSA_SK:
336 		credtype = COSE_ES256;
337 		break;
338 	default:
339 		fatal("unsupported key type %s", sshkey_type(k));
340 	}
341 
342 	if ((r = prepare_fido_cred(cred, credtype, attfmt, k->sk_application,
343 	    attestation, challenge, &attestation_cert)) != 0)
344 		fatal_r(r, "prepare_fido_cred %s", argv[2]);
345 	if (fido_cred_x5c_ptr(cred) != NULL) {
346 		debug("basic attestation");
347 		r = fido_cred_verify(cred);
348 	} else {
349 		debug("self attestation");
350 		r = fido_cred_verify_self(cred);
351 	}
352 	if (r != FIDO_OK)
353 		fatal("verification of attestation data failed");
354 	if (cred_matches_key(cred, k) != 0)
355 		fatal("cred authdata does not match key");
356 
357 	fido_cred_free(&cred);
358 
359 	if (write_attestation_cert) {
360 		PEM_write(stdout, "CERTIFICATE", NULL,
361 		    sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert));
362 	}
363 	sshbuf_free(attestation_cert);
364 
365 	logit("%s: GOOD", argv[2]);
366 
367 	return (0);
368 }
369