forked from osresearch/safeboot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtpm2-attest
executable file
·706 lines (573 loc) · 17.6 KB
/
tpm2-attest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
#!/bin/bash
# TPM2 Remote Attestation
#
# This wraps the low level `tpm2-tools` programs into a fairly turn-key
# remote attestation system to allow a client to use the TPM2 to prove to
# a server that the PCRs are in a given state, and to then receive a
# sealed secret that can only be decrypted if the system is still in
# that state.
#
# For more info: https://safeboot.dev/attestation/
#
# turn off "expressions don't expand in single quotes"
# and "can't follow non-constant sources"
# shellcheck disable=SC2016 disable=SC1090
set -e -o pipefail
export LC_ALL=C
die_msg=""
die() { echo "$die_msg""$*" >&2 ; exit 1 ; }
warn() { echo "$@" >&2 ; }
debug() { [ "$VERBOSE" == 1 ] && echo "$@" >&2 ; }
cleanup() {
if [ "$NOCLEANUP" == "1" ]; then
warn "$TMP: Not cleaning up"
else
rm -rf "$TMP"
fi
}
hex2bin() { perl -ne 'chomp; print pack("H*", $_)' "$@" ; }
bin2hex() { perl -ne 'print unpack("H*", $_)' "$@" ; }
sha256() { sha256sum - | cut -d' ' -f1 ; }
DIR="/etc/safeboot"
TMP="$(mktemp -d)"
trap cleanup EXIT
# Expected values for the EK and AK types to ensure that they
# are created inside a TPM and have the proper policies associated.
EK_TYPE='fixedtpm|fixedparent|sensitivedataorigin|adminwithpolicy|restricted|decrypt'
AK_TYPE='fixedtpm|stclear|fixedparent|sensitivedataorigin|userwithauth|restricted|sign'
usage='
# tpm2-attest subcommands
Usage: `tpm2-attest subcommand [options...]`
For more information see: <https://safeboot.dev/attestation/>
'
commands="commands"
commands() {
echo "$usage"
exit 0
}
show_help() {
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
echo "$2"
exit 0
fi
}
tpm2_flush_all()
{
# prevent "out of memory for object contexts" by
# flushing transient handles, as well as transient sessions
warn "tpm2: flushing resources"
tpm2 flushcontext \
--transient-object \
|| die "tpm2_flushcontext: unable to flush transient handles"
tpm2 flushcontext \
--loaded-session \
|| die "tpm2_flushcontext: unable to flush sessions"
}
tpm2_secret_session()
{
SESSION="${1:-$TPM/session.ctx}"
tpm2 flushcontext --loaded-session
warn "tpm2: starting secret session $SESSION"
tpm2 startauthsession >&2 \
--session "$SESSION" \
--policy-session \
|| die "tpm2_startauthsession: unable to start authenticated session"
# context is TPM_RH_ENDORSEMENT because why would you want names?
tpm2 policysecret >&2 \
--session "$SESSION" \
--object-context "0x4000000b" \
|| die "tpm2_policysecret: unable to set context TPM_RH_ENDORSEMENT"
}
unpack-quote()
{
QUOTE_TAR="$1"
if [ -z "$QUOTE_TAR" ]; then
die "unpack-quote: needs an quote.tgz"
fi
if [ ! -r "$TMP/ek.crt" ]; then
tar -zxf "$QUOTE_TAR" -C "$TMP" \
|| die "unpack-quote: $QUOTE_TAR failed"
fi
# check for the common files?
if [ ! -r "$TMP/ek.crt" ]; then
die "unpack-quote: $QUOTE_TAR is missing EK certificate"
fi
}
########################################
quote_usage='
## quote
Usage:
```
tpm2-attest quote [nonce [pcrs,...]] > quote.tgz
scp quote.tgz ...
```
After contacting the remote attestation server to receive the
nonce, the machine will generate the endorsement key,
endorsement cert, a one-time attestation key, and a signed quote
for the PCRs using that nonce.
This will result in two output files, `quote.tgz` to be sent to
the remote side, and `ak.ctx` that is to remain on this machine
for decrypting the return result from the remote attestation server.
'
usage+="$quote_usage"
commands+="|quote"
quote()
{
show_help "$1" "$quote_usage"
if [ "$#" -gt 2 ]; then
die "Unexpected arguments.$quote_usage"
fi
QUOTE_NONCE=${1:-0decafbad0}
QUOTE_PCRS=${2:-0,1,2}
tpm2_flush_all ""
echo -n "$QUOTE_NONCE" > "$TMP/nonce" \
|| die "$TMP/nonce: unable to create"
# the cert is fixed at manufacturing time
# and of course there is a random number that identifies it
# because why would you want useful names with tpm2-tools?
warn "tpm2: reading endorsement certificate"
tpm2 nvread -o "$TMP/ek.crt" 0x01c00002 \
|| die "tpm2_nvread: unable to read endorsement key cert"
openssl x509 >&2 \
-inform "DER" \
-in "$TMP/ek.crt" \
-text \
-noout \
|| die "openssl x509: unable to parse endorsement key cert"
# this key should always be the same
# and for stupid tpm2-tools reasons it has to be in the
# non-standard TPM2B_PUBLIC format rather than a useful PEM file.
warn "tpm2: reading endorsement key"
tpm2 createek >&2 \
--ek-context "$TMP/ek.ctx" \
--key-algorithm "rsa" \
--public "$TMP/ek.pub" \
|| die "tpm2_createek: unable to retrieve endorsement key"
# this public key is generated each time and includes the
# `stclear` attribute so that it can not be persistent
# and it will not be valid after a reboot.
#
# The much simpler `tpm2_createak` could have been used,
# except that it makes persistent attestation keys, which
# would allow an attacker to reboot the machine into an
# untrusted state and unseal the response from the attestation
# server.
#
# tpm2_createak >&2 \
# --ek-context "$TMP/ek.ctx" \
# --ak-context "ak.ctx" \
# --public "$TMP/ak.pem" \
# --format "pem" \
#|| die "tpm2_createak: unable to create attestation key"
tpm2_secret_session "$TMP/session.ctx"
warn "tpm2: creating ephemeral attestation key"
tpm2 create >&2 \
--parent-context "$TMP/ek.ctx" \
--parent-auth "session:$TMP/session.ctx" \
--key-algorithm "ecc:ecdsa:null" \
--attributes "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign|stclear" \
--public "$TMP/ak-pub.key" \
--private "$TMP/ak-priv.key" \
|| die "tpm2_create: unable to create an attestation key"
# have to start a new secret session to load the attestation key
tpm2_secret_session "$TMP/session.ctx"
tpm2 load >&2 \
--parent-context "$TMP/ek.ctx" \
--auth "session:$TMP/session.ctx" \
--key-context "ak.ctx" \
--public "$TMP/ak-pub.key" \
--private "$TMP/ak-priv.key" \
|| die "tpm2_load: unable to load attestation key"
# read the public component so that the name can be
# computed with sha256
tpm2 readpublic >&2 \
--object-context "ak.ctx" \
--output "$TMP/ak.pub" \
--format "tpmt" \
|| die "tpm2_readpublic: unable to display info"
tpm2 flushcontext --transient-object
# get a quote using this attestation key
tpm2_flush_all ""
warn "tpm2: generating quote"
tpm2 quote >&2 \
--key-context "ak.ctx" \
--pcr-list "sha256:$QUOTE_PCRS" \
--qualification "$QUOTE_NONCE" \
--message "$TMP/quote.out" \
--signature "$TMP/quote.sig" \
--pcr "$TMP/quote.pcr" \
|| die "tpm2_quote: unable to generate quote"
# Include the TPM event log if it exists
cp \
/sys/kernel/security/tpm0/binary_bios_measurements \
"$TMP/eventlog" \
|| die "eventlog: unable to copy"
cp \
/sys/kernel/security/ima/ascii_runtime_measurements \
"$TMP/ima" \
|| die "ima: unable to copy"
tar \
-zcf "$TMP/quote.tgz" \
-C "$TMP" \
"ak.pub" \
"ek.pub" \
"ek.crt" \
"nonce" \
"quote.out" \
"quote.pcr" \
"quote.sig" \
"eventlog" \
"ima" \
|| die "$TMP/quote.tgz: Unable to create"
# ensure that this quote validates locally before
# sending it to the attestation server.
quote-verify >&2 \
"$TMP/quote.tgz" \
"$QUOTE_NONCE" \
|| die "unable to self-verify quote"
cat "$TMP/quote.tgz" || die "quote.tgz: unable to display"
}
########################################
verify_usage='
## verify
Usage:
```
tpm2-attest verify quote.tgz [good-pcrs.txt [nonce [ca-path]]]
```
This will validate that the quote was signed with the attestation key
with the provided nonce, and verify that the endorsement key from a valid
TPM.
If the `nonce` is not specified, the one in the quote file will be used,
although this opens up the possibility of a replay attack.
If the `ca-path` is not specified, the system one will be used.
* TODO: verify event log
'
usage+="$verify_usage"
commands+="|verify"
verify()
{
show_help "$1" "$verify_usage"
if [ "$#" -lt 1 ] || [ "$#" -gt 4 ]; then
die "Wrong arguments.$verify_usage"
fi
QUOTE_TAR="$1"
GOOD_PCRS="$2"
NONCE="$3"
CA_ROOT="${4:-$PREFIX$DIR/certs}"
unpack-quote "$QUOTE_TAR" \
|| die "$QUOTE_TAR: unable to unpack"
quote-verify "$QUOTE_TAR" "$NONCE" \
|| die "$QUOTE_TAR: unable to verify quote"
eventlog-verify "$QUOTE_TAR" "$GOOD_PCRS" \
|| die "$QUOTE_TAR: unable to verify TPM event log"
ek-verify "$QUOTE_TAR" "$CA_ROOT" \
|| die "$QUOTE_TAR: unable to verify EK certificate"
warn "$QUOTE_TAR: all tests passed"
}
########################################
eventlog_verify_usage='
## eventlog-verify
Usage:
```
tpm2-attest eventlog-verify quote.tgz [good-pcrs.txt]
```
This will verify that the PCRs included in the quote match the
TPM event log, and if `good-prcs.txt` are passed in that they
match those as well.
'
usage+="$eventlog_verify_usage"
commands+="|eventlog-verify"
eventlog-verify()
{
show_help "$1" "$eventlog_verify_usage"
if [ "$#" -lt 1 ]; then
die "Wrong arguments.$eventlog_verify_usage"
fi
QUOTE_TAR="$1"
GOOD_PCRS="$2"
if [ ! -r "$TMP/quote.txt" ]; then
# make sure that the quote has been validated
quote-verify "$QUOTE_TAR"
fi
tpm2 eventlog "$TMP/eventlog" \
> "$TMP/eventlog.pcr" \
|| die "$TMP/eventlog: Unable to parse"
if [ -n "$GOOD_PCRS" ]; then
tpm2-pcr-validate "$GOOD_PCRS" "$TMP/quote.txt" "$TMP/eventlog.pcr" \
|| die "$QUOTE_TAR: golden PCR mismatch"
warn "$QUOTE_TAR: eventlog PCRs match golden values"
else
tpm2-pcr-validate "$TMP/quote.txt" "$TMP/eventlog.pcr" \
|| die "$QUOTE_TAR: eventlog PCR mismatch"
warn "$QUOTE_TAR: eventlog PCRs match quote"
fi
}
########################################
ek_verify_usage='
## ek-verify
Usage:
```
tpm2-attest ek-verify quote.tgz ca-path
```
This will validate that the endorsement key came from a valid TPM.
The TPM endorsement key is signed by the manufacturer OEM key, which is
in turn signed by a trusted root CA. Before trusting an attestation it is
necessary to validate this chain of signatures to ensure that it came
from a legitimate TPM, otherwise an attacker could send a quote that
has a fake key and decrypt the message in software.
The `ca-path` should contain a file named `roots.pem` with the trusted
root keys and have the hash symlinks created by `c_rehash`.
* TODO: check parameters of attestation key.
'
usage+="$ek_verify_usage"
commands+="|ek-verify"
ek-verify()
{
show_help "$1" "$ek_verify_usage"
if [ "$#" -ne 2 ]; then
die "Wrong arguments.$ek_verify_usage"
fi
QUOTE_TAR="$1"
CA_PATH="$2"
CA_ROOT="$CA_PATH/roots.pem"
unpack-quote "$QUOTE_TAR" \
|| die "$QUOTE_TAR: Unable to unpack"
# convert the DER into a PEM since 'openssl verify' only works with PEM
openssl x509 \
-inform DER \
-outform PEM \
-in "$TMP/ek.crt" \
-out "$TMP/ek.pem" \
|| die "$TMP/ek.crt: unable to convert to PEM"
openssl verify \
-CAfile "$CA_ROOT" \
-CApath "$CA_PATH" \
-show_chain \
-verbose \
"$TMP/ek.pem" \
|| die "$TMP/ek.pem: SSL verification failure"
warn "$QUOTE_TAR: ek.crt certificate chain valid"
# make sure the EK has the proper key attributes
tpm2 print \
--type "TPM2B_PUBLIC" \
"$TMP/ek.pub" \
> "$TMP/ek.pub.txt" \
|| die "$TMP/ek.pub: unable to parse file"
if ! grep -q "value: $EK_TYPE" "$TMP/ek.pub.txt"; then
die "$TMP/ek.pub: unexpected EK key parameters"
fi
# make sure that the keys have the same modulus
mod1="$(awk '/^rsa: / { print $2 }' "$TMP/ek.pub.txt")"
mod2="$(openssl x509 \
-in "$TMP/ek.pem" \
-noout \
-modulus \
| cut -d= -f2 \
| tr 'A-F' 'a-f')"
if [ "$mod1" != "$mod2" ]; then
warn "ek.pub: $mod1"
warn "ek.crt: $mod2"
die "ek.crt and ek.pub have different moduli"
fi
warn "$QUOTE_TAR: ek.pub matches ek.crt"
}
quote_verify_usage='
## quote-verify
Usage:
```
tpm2-attest quote-verify quote.tgz [nonce]
```
This command checks that the quote includes the given nonce and
was signed by the public attestation key (AK) in the quote file.
This also check the attributes of the AK to ensure that it has
the correct bits set (`fixedtpm`, `stclear`, etc).
NOTE: This does not verify that the AK came from a valid TPM.
See `tpm2-attest verify` for the full validation.
If the `nonce` is not specified on the command line, the one in the
quote file will be used. Note that this is a potential for a replay
attack -- the remote attestation server should keep track of which
nonce it used for this quote so that it can verify that the quote
is actually live.
'
usage+="$quote_verify_usage"
commands+="|quote-verify"
quote-verify()
{
show_help "$1" "$quote_verify_usage"
if [ "$#" -lt 1 ]; then
die "Insufficent arguments.$quote_verify_usage"
fi
QUOTE_TAR="$1"
QUOTE_NONCE="${2:-}"
unpack-quote "$QUOTE_TAR" \
|| die "$QUOTE_TAR: unable to unpack"
tpm2 print \
-t "TPMS_ATTEST" \
"$TMP/quote.out" \
|| die "tpm2_print: unable to parse quote"
if [ "$QUOTE_NONCE" = "" ]; then
# if no nonce was specified, read it from the tar file
QUOTE_NONCE="$(cat "$TMP/nonce")"
fi
# Read the attributes from the ak.pub and ensure that they
# if `stclear` is not set, then an attacker might have
# a persistent version of this key and they could reboot into
# an untrusted state.
tpm2 print \
--type "TPMT_PUBLIC" \
"$TMP/ak.pub" \
> "$TMP/ak.pub.txt" \
|| die "$TMP/ak.pub: Unable to parse file"
if ! grep -q "value: $AK_TYPE" "$TMP/ak.pub.txt"; then
cat >&2 "$TMP/ak.pub.txt"
die "$TMP/ak.pub: incorrect key attributes"
fi
# since the ak.pub is now used to verify the quote, it
# is no longer necessary to cross check that ak.pem and ak.pub
# have the same ECC parameters
tpm2 checkquote \
--qualification "$QUOTE_NONCE" \
--message "$TMP/quote.out" \
--signature "$TMP/quote.sig" \
--pcr "$TMP/quote.pcr" \
--public "$TMP/ak.pub" \
| tee "$TMP/quote.txt" \
|| die "$QUOTE_TAR: unable to verify quote with '$QUOTE_NONCE'"
warn "$QUOTE_TAR: quote signature verified"
}
########################################
seal_usage='
## seal
Usage:
```
echo secret | tpm2-attest seal quote.tgz [nonce] > cipher.bin
```
After a attested quote has been validated, an encrypted reply is sent to
the machine with a sealed secret, encrypted with that machines
endorsment key (`ek.crt`), with the name of the attestation key
used to sign the quote. The TPM will not decrypt the sealed
message unless the attestation key was one that it generated.
The `cipher.bin` file should be sent back to the device being attested;
it can then run `tpm2-attest unseal ak.ctx < cipher.bin > secret.txt`
to extract the sealed secret.
'
usage+="$seal_usage"
commands+="|seal"
seal()
{
show_help "$1" "$seal_usage"
if [ "$#" -lt 1 ]; then
die "Insufficent arguments.$seal_usage"
fi
QUOTE_TAR="$1"
QUOTE_NONCE="${2:-}"
unpack-quote "$QUOTE_TAR" \
|| die "$QUOTE_TAR: unable to unpack"
cat > "$TMP/secret" \
|| die "Unable to read secret data from stdin"
# convert the attestation key into a "name"
# so that the TPM will only decrypt if it matches an
# active attestation key in that device.
AK_NAME="000b$(sha256 < "$TMP/ak.pub")"
warn "tpm2: making credential: $AK_NAME"
tpm2 makecredential \
--tcti "none" \
--encryption-key "$TMP/ek.pub" \
--secret "$TMP/secret" \
--name "$AK_NAME" \
--credential-blob "$TMP/cipher.bin" \
|| die "tpm2_makecredential: unable to seal secret"
# remove the secret so that it doesn't live on disk for longer
rm -f "$TMP/secret"
# and output the decrypted message
cat "$TMP/cipher.bin"
}
########################################
unseal_usage='
## unseal
Usage:
```
cat cipher.bin | tpm2-attest unseal ak.ctx > secret.txt
```
When the remote attestation has been successful, the remote machine will
reply with an encrypted blob that is only unsealable by this TPM
if and only if the EK matches and the AK is one that it generated.
'
usage+="$unseal_usage"
commands+="|unseal"
unseal()
{
show_help "$1" "$unseal_usage"
if [ "$#" -ne 1 ]; then
die "Insufficent arguments.$unseal_usage"
fi
AK_CTX="$1"
# recreate the endorsement key context since it doesn't change per call
tpm2_flush_all ""
tpm2 createek \
--ek-context "$TMP/ek.ctx" \
--key-algorithm "rsa" \
|| die "tpm2_createek: unable to create EK context"
cat > "$TMP/cipher.bin" \
|| die "$TMP/cipher.bin: unable to create cipher text"
tpm2_secret_session "$TMP/session.ctx"
tpm2 activatecredential \
--credentialedkey-context "$AK_CTX" \
--credentialkey-context "$TMP/ek.ctx" \
--credentialkey-auth "session:$TMP/session.ctx" \
--credential-blob "$TMP/cipher.bin" \
--certinfo-data "$TMP/secret.txt" \
|| die "tpm2_activatecredential: unable to decrypt cipher text"
cat "$TMP/secret.txt"
rm -f "$TMP/secret.txt"
}
########################################
verify_and_seal_usage='
## verify-and-seal
Usage:
```
tpm2-attest verify-and-seal quote.tgz [nonce [pcrs]] < secret.txt > cipher.bin
```
If the `nonce` is not specified on the command line, the one in the
quote file will be used. Note that this is a potential for a replay
attack -- the remote attestation server should keep track of which
nonce it used for this quote so that it can verify that the quote
is actually live.
'
usage+="$verify_and_seal_usage"
commands+="|verify-and-seal"
verify-and-seal()
{
show_help "$1" "$verify_and_seal_usage"
if [ "$#" -lt 1 ]; then
die "Insufficent arguments.$verify_and_seal_usage"
fi
QUOTE_TAR="$1"
QUOTE_NONCE="${2:-}"
EXPECTED_PCRS="${3:-}"
if [ -n "$EXPECTED_PCRS" ]; then
die "$QUOTE_TAR: PCR verification isn't implemented yet"
fi
verify "$QUOTE_TAR" "$QUOTE_NONCE" >&2 \
|| die "$QUOTE_TAR: verification failed"
seal "$QUOTE_TAR" \
|| die "$QUOTE_TAR: sealing failed"
}
if [ $# -lt 1 ]; then
die "Usage: $0 [$commands] ...."
fi
command=$1 ; shift
#echo "$commands"
case "$command" in
-h|--help)
echo "$usage"
exit 0
;;
#$commands)
commands|quote|verify-and-seal|verify|seal|unseal|ek-verify|quote-verify|eventlog-verify)
$command "$@"
;;
*)
die "$0: subcommand $command unknown"
;;
esac