From 68d0ad69519774eacc28195d1c3b35af814d0139 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Fri, 30 Jul 2021 20:31:03 -0500 Subject: [PATCH 01/18] test-enroll.sh: Quiet shellcheck integration check --- tests/test-enroll.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index 88b2ddcb..95a7fe62 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -12,7 +12,7 @@ fi TOP=${TOP%/*} -# shellcheck source=functions.sh +# shellcheck disable=SC1091 . "$TOP/functions.sh" #PATH=$TOP/sbin:$TOP/swtpm/src/swtpm:$PATH From e3782c86afc91db1a6f37f41133a02541672e16d Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Mon, 26 Jul 2021 18:44:50 -0500 Subject: [PATCH 02/18] Rename "EK method" to "WK method" Good suggestion by Erik Larsson. --- docs/attest-enroll.md | 4 ++-- sbin/attest-enroll | 10 +++++----- sbin/tpm2-recv | 2 +- sbin/tpm2-send | 16 ++++++++-------- tests/test-enroll.sh | 2 +- tpm2-tools | 2 +- tpm2-tss | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/attest-enroll.md b/docs/attest-enroll.md index 177152ac..df899a4b 100644 --- a/docs/attest-enroll.md +++ b/docs/attest-enroll.md @@ -93,14 +93,14 @@ Decryption is implemented by [`sbin/tpm2-recv`](/sbin/tpm2-recv). Two methods are possible for encryption to a target TPM's `EKpub`: - - the "EK" method (our name for it) + - the "WK" method (our name for it) - the "TK" method (our name for it) Both methods support setting a policy on the ciphertext such that any application using the target's TPM to decrypt it must first execute and satisfy that policy. -The "EK" method uses `TPM2_MakeCredential()` via tpm2-tools' `tpm2 +The "WK" method uses `TPM2_MakeCredential()` via tpm2-tools' `tpm2 makecredential` command, using the `none` TCTI (i.e., implemented in software). The target's `EKpub` is used as the `handle` input parameter to `TPM2_MakeCredential()`. A well-known key (`WK`), and the desired policy diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 813fdfaa..3ed1a0c0 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -36,7 +36,7 @@ DBDIR="$BASEDIR/build/attest" POLICY= ESCROW_POLICY= ESCROW_PUBS_DIR= -TRANSPORT_METHOD=EK +TRANSPORT_METHOD=WK DEFAULT_EK_POLICY= declare -a GENPROGS GENPROGS=(genhostname genrootfskey) @@ -223,7 +223,7 @@ $(configs) names of hard-coded policies, or names of executables (default: POLICIES[rootkey]=pcr11). - TRANSPORT_METHOD should be EK or TK (default: EK). + TRANSPORT_METHOD should be WK or TK (default: WK). NOTE: Until https://github.com/tpm2-software/tpm2-tools/issues/2761 is closed, {$PROG} may require a TPM (simulated will suffice) for @@ -337,14 +337,14 @@ if [[ -n ${DBDIR:-} && -f ${DBDIR:-}/attest-enroll.conf ]]; then configured=true fi fi -[[ ${TRANSPORT_METHOD:-} = @(TK|EK) ]] \ +[[ ${TRANSPORT_METHOD:-} = @(TK|WK) ]] \ || die "TRANSPORT_METHOD must be either 'TK' or 'EK'" [[ -z $ESCROW_PUBS_DIR || -d $ESCROW_PUBS_DIR ]] \ || die "ESCROW_PUBS_DIR -- must be a directory or not given" -# XXX This policy is for the EK method. +# XXX This policy is for the WK method. # -# FIXME We could make policies for EK/TK have the same digest by using +# FIXME We could make policies for WK/TK have the same digest by using # TPM2_PolicyOR: # # tpm2 policy... -L ... diff --git a/sbin/tpm2-recv b/sbin/tpm2-recv index 20e43ebe..ff1e4c93 100755 --- a/sbin/tpm2-recv +++ b/sbin/tpm2-recv @@ -19,7 +19,7 @@ Usage: $PROG CIPHERTEXT OUT [POLICY-CMD [ARGS] [;] ...] If {CIPHERTEXT}.tk.pem, {CIPHERTEXT}.tk.dpriv, {CIPHERTEXT}.tk.pub, and {CIPHERTEXT}.tk.seed exist, then the "TK" method of encryption is - assumed. Otherwise the "EK" method of encryption is assumed. + assumed. Otherwise the "WK" method of encryption is assumed. See {tpm2-send} for details of the two encryption-to-TPM methods supported. diff --git a/sbin/tpm2-send b/sbin/tpm2-send index 29a52b90..dbd5ca81 100755 --- a/sbin/tpm2-send +++ b/sbin/tpm2-send @@ -39,7 +39,7 @@ Usage: $PROG EK-PUB SECRET OUT # Null policy Options: -h This help message. - -M EK|TK Method to use for encryption to TPM (default: EK). + -M WK|TK Method to use for encryption to TPM (default: WK). -P POLICY Use the named policy or policyDigest. -f Overwrite {OUT}. -x Trace this script. @@ -65,14 +65,14 @@ Usage: $PROG EK-PUB SECRET OUT # Null policy The two methods of encryption to a TPM are: - - EK Uses {TPM2_MakeCredential()} to encrypt an AES key to + - WK Uses {TPM2_MakeCredential()} to encrypt an AES key to the target's EKpub. The target uses {TPM2_ActivateCredential()} to decrypt the AES key. - A well-known key is used as the activation object, and - the given policy is associated with it. + A well-known key ("WK") is used as the activation object, + and the given policy is associated with it. This method produces a single file named {OUT}. - TK Uses {TPM2_Duplicate()} to encrypt an RSA private key to @@ -100,7 +100,7 @@ EOF . "$BASEDIR/../functions.sh" force=false -method=EK +method=WK policy= policyDigest= while getopts +:hfxM:P: opt; do @@ -121,9 +121,9 @@ function err { } case "$method" in -EK) command_code=TPM2_CC_ActivateCredential;; +WK) command_code=TPM2_CC_ActivateCredential;; TK) command_code=TPM2_CC_RSA_Decrypt;; -*) err "METHOD must be \"EK\" or \"TK\"";; +*) err "METHOD must be \"WK\" or \"TK\"";; esac if [[ -n $policy ]] && (($# > 3)); then echo "Error: -P and policy commands are mutually exclusive" 1>&2 @@ -229,7 +229,7 @@ function wkname { } case "$method" in -EK) info "Computing WKname" +WK) info "Computing WKname" wkname=$(wkname "$@") \ || die "unable to compute the MakeCredential activation object's cryptographic name" info "Encrypting to EKpub using TPM2_MakeCredential" diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index 95a7fe62..21c2402d 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -49,7 +49,7 @@ cat > "${d}/attest-enroll.conf" < Date: Wed, 28 Jul 2021 13:19:09 -0500 Subject: [PATCH 03/18] attest-enroll: Fix POLICIES not set error --- sbin/attest-enroll | 1 + 1 file changed, 1 insertion(+) diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 3ed1a0c0..3174d471 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -41,6 +41,7 @@ DEFAULT_EK_POLICY= declare -a GENPROGS GENPROGS=(genhostname genrootfskey) declare -A POLICIES +POLICIES=() # For the configure function (see below) declare -A vars From c77049a111ae355e6d705037241c156757c4a4be Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Thu, 29 Jul 2021 14:33:31 -0500 Subject: [PATCH 04/18] Makefile: shellcheck tests/test-enroll.sh too --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 6c16338e..886af57d 100644 --- a/Makefile +++ b/Makefile @@ -307,6 +307,7 @@ shellcheck: sbin/tpm2-recv \ sbin/tpm2-policy \ initramfs/*/* \ + tests/test-enroll.sh \ ; do \ shellcheck $$file functions.sh ; \ done From 20bbd6636123b62bc43c611c8a2e0a827d1296ea Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Mon, 26 Jul 2021 23:14:57 -0500 Subject: [PATCH 05/18] Add protocol documentation for security review --- docs/images/attestation.png | Bin 0 -> 54007 bytes docs/images/attestation.puml | 28 + docs/images/enrollment.png | Bin 0 -> 26865 bytes docs/images/enrollment.puml | 21 + docs/protocol.md | 1115 ++++++++++++++++++++++++++++++++++ 5 files changed, 1164 insertions(+) create mode 100644 docs/images/attestation.png create mode 100644 docs/images/attestation.puml create mode 100644 docs/images/enrollment.png create mode 100644 docs/images/enrollment.puml create mode 100644 docs/protocol.md diff --git a/docs/images/attestation.png b/docs/images/attestation.png new file mode 100644 index 0000000000000000000000000000000000000000..e72ac9e08ec47a14a30837d16879f30927951e76 GIT binary patch literal 54007 zcmdSBWmr{h*EPIBl#&t&Nf8N2m2Q=k6zPysIu+?uQgMSyiL|tIgVHJ8EvZPCbiZ?} z*LB_TeBbdL$NTH$&n7l&t@Au*j4|e%`^7y)X*?VX90US^cUMN@J_3PWhd`Y1z(R-L zkVXf-hnH(lB{iQuw6$}wGB$aNkT$k4eq#94_|at}m&;~PpV|p>a@tuL+B|izw&Hkb zYfW@rfC{d_<*KUr^snC|5Y90ks$)+^uM>Lg%AXk0u8idot{=i^Lfjh(9Sbm)YGZMQmLP6uSnCm2Y1~EFN9Lmi&PSi{Ui|!Fr5YB5?=D8kklr4GfBxecoZF3fWr;gKo{Sh}e^48D za@*=J85&)N$9Kg(Q_%=%KKaWmw1ROx1PD3Ier{i%*f-p<+LV%CeiT z%`5pOD?}MTead*DnEln5td*2xy~V+bw!#DCi#tgoT{ASIJ~Adh$W(^W=vegw)|c%V zbz5aKm^*(Gl}ImKD(+`S_0wNq2j{-{$MRMVkKctdQNkWOe4%D29X6D!liaK}OfLZ@l?2 zd_j=QREb*jejh#$yU6~Ws5YhtQPk0pk#cJlmm^sg*P=k*^tF=}WNH-q=MElgVJ-$_ zwsXe#m1Kk%hNt}QJpt3^ z=XqBJR?`;VF{*sTpqm|Ku3}y>6P}B)CU#j1LxvgTGBY0fO~!TMa#t$drux0 z8RwB*vGqfx<$>g;@{ese8NN2Tlo(x;dy%-$S&sFzL_$8*F4^R+>Gn?$$_18;P6_(n zAe*(FyW(0iADU&;MG$-_xK92f48~h}+(>-$me>*HQ=1U;BxEya!o?)KCxLdCyHA>yT{c?c zEB4lCg$r$wame69TmX>hb8qY+Ci(?)^Lx5Eo>Z|mq9@~#TF#`a-#d+L($Hh>;70% zQ*&~B*!O|K(sspZ)NS)ehkLk@_%tCxfo!6AO61O+JM+C?lcYl; z^*sfcm;y|(zk(GwZq4^(YUFb0l)TbbjJoNUdx2v~$Z>gncQ@_La>EPa+1Xho(fwql zZ+0ox@;-enp-f8gq6+jF>_2||C@ALE?*7s`7$@lP5~t`(0O7W_oD|teVK?lxvRE!A zjk7l?pY^CJ9Lmw&MphQvi4NUy4u#dq3x?cVxq6jV2aBwNK3t}q$&!oYli{S6Y(hj< z?073hnV&uhs++g;XpOwxIow@KyZG$q(8^fp>73Z9fb;t0S-l9WbTPtP#D1vLhr{;s z^UKHOPE-mA5j)xQ!zF8Q#KYrGzvnYQZ>C4DJ7}Je(pVorP$)Ty%|7wCnR>KI-+kk( zPBJ<>BLhReB=5V8Vz-^;M9BkHix%xfN{hNbE8yID<1v);fY0V@S-ArJaZXXzB0PBw z>|YD1xz&!jaJZFn4Mm$GhR)%d*XiMpa}m_XCYJUqm;^NPL8G>p`M3QK&Y*ECgQaVf zJFbkH#(V5E5Gxf~47_}hLF@9JsN`c&(Nj18u(#!|8}1a<$T>=!$nAuL%j!w9b8~6d z^7K?X$pjrmJMv97^9eAFp6Ww{X2n8aPf4J@}3plNQ z^}!^Fh>X-wSFhUt5&bm9^JIT+WVE3{LR+`emB)kBYrPiJZ>cP=4E%V^<}*7Za33+S(urfA%-39tYlite{0B4N!yF{7%R0kwH&E*7%d)kTk5xjAC*2I{D`(4{t!2# zHD5X~lpa3>u6%fMxE2`^F*rDAGgg8uQBYKjSZVzjXEyXvAI(}$3$IzdMbUTKe=%vv zEyqTQ^Wm*qx6%}2pV-?kuuCuWrz#-86k+9l3o56Tcf@{LURSjR&8exF|F%K}pPw$^p0@4pZg4aAoP5j7&xr)I#8zpD=U6;6F0mRy@0NB)JmfFhq z67R4GID;wFC>}58ZgMPZF6A`m?{8KdreJn&`hvj|hEHJ&3(NZk3`rwd;5uI4nr=IZ zbk5~$uSU|`>LR|Qv~f%IIn(<3bs`!)TAsK#DR<%1*h;ru&+P#XNl8iY*39&D!b6-X zNnyIJiv$u3&gcXL#M9H;%HI~u8$J!)6gyh4_d3~(M@2!6)0xH~BYm`rKqlX~nP=%3 zN>kH@SA;7>Erdmon%m7IsH~8x5IsMTCLH{hKR*|t*YH0)!lP5Vhm9{uwiZ=rN+GZH z_TK;hdS}IHn#)#iE~LTnPYz)XBqSs+JkG9Pi6eA>9;A+rf&b| z^ZUS2`3w`gii(Qfsh$3{GPk;hZGzkoiltsGT7i|(G23c(QBhaOpK5A{d;Tw8I4$T; zx~(qMmJEnhoWK@``q6l)YIq4eY)W1oe|QGnCg|09kG*NM@^afkHQ}&FH;kvn^94{3 zaz#Ez@AxMDLzc5L_wLNx2PQf0&K~{z3`m2J zzBDl$0)pvG_1w7Fx4rsfQ;7kqZflhS2gi8oaR<4t! z>Q57%ht~P|)F(%CEkilfQ`S$OkbZTu?c%le9Lb3_|5_HnsrR*ZvDjA}jFgzU?&|gR zou5Br4_tTd`PPrwrb$cs9UdI``xZ@4CiVHWGO9%5lAV}O|2Xh*_2@6-K&EFNI_<9J z>w6uGX6`R$7CTK`k`DiuC;GgU!=dk`$#*Vv1#CYQd_0Doj@6hCRGW@%Dj}_y- zO3g~sl%I|}aqX$YHp$Al)%@)EWesV`L_9pFAJ`1L6FiBTf_rnc3x9AP-QV8(CO(wo zF>WWeJiW~+;vFZrg^q6X`RRhahltyJU*PxeZaZsUk$eg5RdSKR6$*M2Ztes7n_~;& z@AAq|P|g7dgrwvW7at3zIi7lcmQ$tEzDLh5h4!rTG3M7 z+x=a+XGH>Yb2$f8qDm8;Rc&*;P-l`}|9z&BnpOSCWwC#9Yax`7xX*C1%1S*KAM;?V ztyLq}^Dg@}xw4*jtoNd_3+(KVQC3=O?pqa=w|Km(A1}P0e>YT0r73Nq^{=}f%Hhch zb;cg5PTn7r(ofQFqz#l^$5_1HBMP?VF&Fzd%gTjcK8BwKynw`{ zE*~GA!h+f9TP?0bIk6eH#P%aEMc8tnf{t#i)I4p0+MmniemZDtX!z`TI5sUEoRynB z?sc#g;&-zDKUW>14LtauHT5?Ku9 z#*-2E)pq6jRpI@PAYiYq%lVi>PtC~4o!#8sw`foI#EwWSDF=P)+oM#pOSaPDM`hdi zhD2fQR!a0S$Lk0GQ;?UTM+pB{fPO87a3)m35@|&FM@@abgfm2l%Sgof<^O#!b-wc} zbFE|0&_L!|N;+qb;f=ZRA87~=awWkG_U{Bn?CcB)?5_>_&s6Y{duI5^W$S2sJ0Zz! zSB@ZI?mz!cjba*`fYWmUZA+NxS9U7X2*rZz)TsP0MoRbdQT&={*TayIx$45z;&wH! znAqch^R9Hd$6s1^22oq%^E?0P@E{@oO;GYQnfne-RJx&Z+U|s#t>0mHPXaFIKBo*3pCPZjo zVz)}}S+h<;YIRiJj_psBw&c2bGlE^G#BJB+G4-v@)L?4DKkrN`_B!Z`KiMg*_ShLQ zoe}i@p+GHk+0K>XnVTBo>m8445)^2ceGJ=J6qV>}crRVLl#!8PWUZ11uwrk|nU*HM)GuEn4<4V^iDi4vAe0wzMU$b@iN=RBdI;aJonk@}x)6>&?9ga=@#54Z6 zxv6shzUO|M{>cnkbpjp%wcyo~Jn}UkZAV8(ZSD8>_79eF5BK*gH<~C%WmoVCXhr3_ z%qA*a-fuhR=ar(n8d_RdG-xw;ULGMbo_l}s;zi+u`6R~B47E)4I1x7#iGJ7|JiG02xO2Q-PvFv~5Y4NMXX^!2eeU~r z-3sT8IzQYl(Zj3*C(Py$S^`|$w)S>5OKz`|qwenRy1G00vBth>m&KJ#2R^Eez6WA6 zJUlEJMB1aovguk?50~vB{Or1IQPt{6x}$DTukKDfQc_^A(1Ipn*&3r5E2yTdoNqs; z*t+Sfz&t0Tq*M(KHe$5-_mkejuh5W;kb1p2*VPxGs8?+>E_h(jI>gAx=m~_zU%Wq@ zRb#%dP&0D_2x|9$y3Qu}o`nkO3QLs?ZF9WW$$nrU7RC4U8qwH8vpkb9!uA+}Li@SS z+S=N0%^|s2S&z1F+_=G{7-L{;(iXuE3^4Hyh!}gAtN!sJGW2NFOMOaWv)ea;C9I8q zb}}$9D76`P?lc_6^RPWjDk{w`TP+n#WqvaXyq(La0ju@9riiUonVQd$Zr_V{JeH*T zDJkQ=oi{~{)p^~|R$Kss8soPcw!nXtJBpuN6`>_b{;(*Bj6)X-4{vsxo1>|@*=wVb ztf=f^Gd_z7a+ykNkj#7d$+W= zQwclU-MxF)#bv+j@i#s94M}w+ACTk1ON?b@TYt{BWxNg#PdPsDbu2Ti_n)7iuc)Yq z&YrzOPd}8a@3p-+@cjAn8xNcEWVQIt>)+_Cn@b-kH0}EO_3Pr|Vp}AqQmmjuoiBFQ z#7FKsZaE9f7s<#r)wJD1+WBg zmSMj|CS-I@cdJih$Mvp1o$oH4^wb%P6LMZRP*v^M(Dz{Ix7^>~?|Ge+ov55=+!m3= zwshahYPly#T1!AoOpJp=(X8g`)vHuOj<3QO#2tY|Y6@Jwd<&qA%q3FNrcbxUGUUZY zbtHwhK>v|{&ZoXKoc|3>7c`&j>@WxM)0gI2dsN0OEG+Ylrp9f3SkthYr}sdTmfdfrPiXhIIbf!lb2jSbSSc>ynu7 za_A?(g{;>fe0H3Te_JdSLQ`qfh@)MX@tohiXmClrPfs&)a&jsvbfX!t&OXbE>+D<}DQp~-8dU(NmsHwm4QH*8 za{iA!H-$Z~WEo5)Lf4Crin>HiecZt}`SsI;rlFysnc4i&{^q{EMYpuq39C2c%dU4f zk_rk6l9R8pvm77JPw|!E7e!;l;M)HC77s+mw$mb@6)>UY94-}56OF? zu1?Bnf9={ewyr7Zuf>*R9(5+WO4t2!>+=lj@o9u*u3_PQc=P5>*8M@e#8UJTdv0M4 z@%kO$r*QVn#1sNgl11HKTeD_zvao#c=)IdL3R$;1XuVsUAYZMHF-Z#Px|Cf?aW(gr zPfjJ-J$;-vU{k+b{(WyspR&1cNGFOI>x@mzbSH{0pGw_pQ_t4+INly|C-J~hl9xY^ zgR=-~t)pW}S64uS@?!a19Imaz*^^l@Emxf=G!5CYRV#yhw6i$Cdi^>o)0LN(|Gmbr zVkVtZo0ZXGO9+D3kV*7EBMzS5QdeT`xpcz_V>?u_!yE0)o+X17m`Z$}n8OYCvyV76 zLj6$*<(8|%xysSeQK7X8d$__34%0gkw;d45*ao%Pu3dW(^2Q)Sqe>UTLe1INQktfx zC&!>0!U_mA5YFTPBc{-0=skb-Xuf)9G*@_QsVRI(s!AQ9B?l9y!7Y;oW5 zU_Z02{t~+nkCi{7g8_{mzN zM~OE6?b`yA4vLWv{{)5--OjA%n}$qd{#0XSKQ%d~Cb;iB=5J&yjE_;$FeGq(K@^PakyUEXK20dErT5BMnvS~&U3L&s}q&cQg8P5_Po8l8yXs( zKSv*Ix4W=@E@{7DyZL_ml66aB#t2}*hYufu%9Sb~bzeuPZP8kab`_G9%yFVeZ=8!5 zUVPH}Q(sB-dfW(M>iQ&ZOLj|lll2d=~Brqz|AA4s3E$iL8_xjIG zYiG|3zcgCs$|55*4)vz&jAUPKSgx2@lw=|AylTTWdZya^jpG-Bt8T#s!Pr3su7=9L zRz)_tu2!Jo3^5U+W_D&KC6~#0W4EmP??kH-Z?q&_l`08DdCF7-E2Hye!x9@&baUxz zjUTKo*h#(P)Zvd9aj6^#Qk^39?KOQ|Z%aSCp21XNdG(O@&JLAd+P7ql-a1?QJ-NrQ z%qrO(oIaO@k-|ApZ@-q?o5l&4r-hh89Hj9&dOULF7hwIiRD5Pyk3*3e(?iZ8zS-KX-bI-3MR$a<`HDEF-M)gE6E z#hj6j2hNN%PV-qzQxv}Pk%Q@PS}ja`c)Szb;~s2;B<)a3oj(|KbTCxy3Ax)>c4ccrwfkh((lEbP z_it~FaZWUCj|&-rJSkB)05p9 zi`;wq`Y2RWDIG#{WGiZ&g#UMB`Np}0-nd^@cDNLKjWg?W_UYltZuV(oE^pM^H{#C3 zrpZK*o&sOKv>3`6=jsxsKiWX~YvW5k zo__M^HFWJ#C_~JPr|iWVXEbmKt~`;PkuxuhX}~y zx`6hc-C^0g<{Qd?vS&35fe}STmVto*YG2=)kZ!;ciY+f$ z|4d{kIb+t!_6l_-=%}SNhs2WVRg9L}dTq@st4GDhcS%1sT#Jrw zdN}`Kmix)kK7cWEAlbB}zYi>6tTzyfLo15|nOSdfO!@-7y%E-K+7~IVKYR9!uUc zawrEBhfxX<>$rb?TmypN&kE?34=6z)@@;77KJ!Gi*XboL6IFHfh_JBal$4DF^S;y# zH+sOnkXfFp(*oS@BPuYcMYoXR2@6`Dtd8HQ(Lm#7{g)*GN>Yg7?|F2yv8jpU!Kbh^ zTp$-OUc9({`!*B=+B!RbY-{A|d90St)ln)xJAF>i#Z?IZQ=#SIaSWt^_`(G<0-&xP zEiH!Ihz{iMv+~wq%bqFWzEkEr?Sz8}HrBa{?k(teYih4n#{>Nl87CaT|K^u=8|Zd; ze=Pj@k2op*Z#u7Ey?WK(ubRK3jxUfCD>U-i$=2PyB31}3WT5B@QH}i+saz(%C)VTT z#VGHb(EErPfkv(dgz*q?U~M;J3n0ZI7t(CqAvdp7Q*05 z&f_ny@yK$4!ktbNcqJ$`!AWut22=1@fVsU##(a7EW^a2)e|K}!?HiJ>D0~zTL>VEJiP%3k(TVfwR`}ynA3(1{ca3Jj=d~)gE|Fa`-X~WI>2Kg2fxSWj_d~oewpq7TneC(~g(aL>2Ps6_N{a!OiC4t< zk9Nu;^6Z&jcI_!WlWZ@c6mcO{B~1~ks>@%?Lpx*huxF_j78XER;MBoz z%BQ=4hR9-|ma#IaZv-g34&3;?I^Ra^i<{?9);^MQ>a{OrypD|>hjLd!LIPm3qa$~9 z&gdHzB_)G)&!0qr;CE*g3@!9TqGzkFi=VCIBcl7p&^F<@pI0b4h{}= zVk;32pY!tUzJGbsU&(7d`ilI<3?RBJPat4$@l>*pfVqW@->gU&Oj9jskG4V@4Dj{M zyE#i-Xwos0SyU8_>g4#;+UhTYNtXBPxs9*;4!8IRX#l5x7$Q;v#oWb33xzS-jau`A zt%d$&-6T43|8-ZE^R%KBMMYs3{J@vC)+XZz+Dy);B~dEg;}FMuP~|Q_0*IlAPqEjZ zlGiFQIJmQ|?eoZ|8w@k{&#!|XpkM9j0sSS#;RQCk%OhNTe35#Xt(mD1Eez{0)M+nW zx+_!;s?P1(weM~`>=z=^^458rza%lz`3HKp}~Q3R&HH5ee#?@QB~<1AuFwjt7^1V zSa^8;P2}W}c%D_$dRZBNJ$5Tz&ubLvKiS|tPQE$IU`1j*zqY0n)xb{qRj`;`oZZ92 zW71>yL1J&-nD$nIgrsEtO=CJ-G7e#ngQAjW3_|9wyDU=f^Z5hh0Ovj&qz;)E*afuSKh!{b6A^rpmU8X zl9Ptt&Y(NtHmFd`cAlP|S)!brwtMT-baZqZ8yjOK*2EDTHkJ}) zU1n>A)9;$`Ck|W6&!sIA>hTn) zm04L?(ljm->B*vbY|m`VEQdG_(mPh;FNa~wms-5uRnbg-<_P}^DR;}_6_vo-Ap%O? z<*8bqu-a||h+fyMjLW{X{LqXnwtbxos?P_S&UmqSM^GDYcO54N&=_Uu%@EhmsBC&z<#=%SwCym>3K8QLP7#I);6+&>XOIK}(YfKnz@dkTLx4MRfKFZ^3sEaZz`kMB!N^1Y%F3)&2N+W`v!oo9qm;UCS(JP>^8}IM;eG-&6e#3) zAJ}<=T;6d%6M`()zv-D{%UcE!*Db8`=PNclg%;>}knZ4#5S9Qo9C>+eqV?M4tSk-X zrlqAx!qbEcFkVmk*uYI;Dr!|=!36y;woIOju6klomEohGqSUd zw_|d1bJr*NU%q?^#W|Z!)FYq-g4a@IXn07dJ^-mt*x;w9PlA0mH#cu=Y_!^VhDkHk zp;-$z)+#i?qvC(=)it=oW|&y{LwpEdWT?C#!MDV;2M0_t$cqbgfb8p_V%@f5j8NOac7=U0$< z$xH=WLwG`rRC01c+vR=%sji~A!*A%NxAeKPw2NDZhBVynjS4d}%k(8j0VwyzmC4=X zt#52&8%f|``}urYcWiW_y5puB*7gUhL5KQaY@s&_9vs}E?DuQ!BJ6XZbOpJ+_oN^x zJ)N1CSN*T6Tt?cR8F_t;4QIPfcZ~aq&WSaKhu#oJD?Xi0SjVSMtlvx3b!0PqgX3ezb^IbVSuMVyAT@(2k9!3aHk1`ppDwH+Ihl7 z$$vKR@2f)otN_eCYLfNrUg-Pxt+o=@sN9CNa8xx4Pq+MD0P+Jz$W=KJ;{DWKTE82X}sK~}n`1C;eW z+q>_e0OPV-9sluU$7o|L+JaLpko76bK7Ny%X|)PlberQyJgQkjxN_36gw8%ld~U~L z^(U*F6-YXQRm$D8pwHE$L*|z+pjn}WUzywP`Eo%=#_f&bEu!^hgUaSdBemOZuM@QZV9ns_O%|L6f6pl617W7? zg?Yp3_-C=tBm?)*l{GbG1SBN=YAa_E2H351gZq=x8Qwcfm}n?=iUt=(Or4+fh%2p9 zdwcTNZ`0C<$~4Z_JX_aiLyN(yk5G$=lLBD`F?IDHJ^oK|@-KP*|NlpR5o8TY08OmA z2t@03ZJsd++ygO13j2xDQ4!8ez&QSp-d_*)i{YC4vhMF`IbhEQrM{5jvF^`6G6Fm=;<&82qAfLuLQ0_Y zM@2;$BT*gAL8>i-kbAeHOyjPxus|>8HFkE7-EpV2@;RE4mp7K<7*0UYQH*qdUPnHNmyT<#<;(8bn_q-<`5HJ}91 z91?QJkcP%aY7y74a1;>20lo-rg@&_`!=3B>@v_?Y%<|sX)zv`^d@rB+508IE>AHfZn@!023We{xtVaot|j;X8QKo%uiL z0$Gan^D$Yx+>wVNvzb zqpL!R1>Zi!qH^9=Vc;g(6zO8Oy#>YN={mp}_c~hRzl?U9J)LR5u6o&h1$jR!22OMM zVaUozVcMrp+GGA)%QT5}$a7pQkeE{;`DoLJxj&TsO6tUn<4Z-up7wKz{SC7dUeTuNlNU3 zYj{)?N;(6$@$=^s*|y&`%Cxxc2tAbMQ7_80o$&(#UuYFwBw%{d4lyu+Mbp_QnfV-V?FItMcmzNY}QCEG^72y80jlvB8pcsnW2vS(+_ldvu!`TL2`wTlJE3pO4nRRF`|6mkj6TXtto^a+%$9U0<5g z3Ft(=Zq}UuZTW-ttzNp=I5OuBM=D(Gq0fA2aS_f~+?+5IBiC>p{~UsI>HsQ_W-^2RQe?-KL}8|8 zmSuk^$tcB$O-s$QGm~J&pVv+{QWD~$d948$8fBV2h7Ot1BAbR)mZ^DSRE3c9!6%6% z0~eQa$G`#Jq((r7{^1M6h#GCNU?@0MLnVV;n_$MB3PTV&! z`Q<$K-6;*rbQbME=+O7vpY4nl+N8WzV@Y1^hDgY;ykp(~7Z-PA z)NQpK)ye?)3p^O)Ht2n@l9hewXg^{@N#PaSlPHe4gZ(bf3d+_?xz&os6w$!pJyu3} z9{c$F=jB}&CA~nAeQiIXHbn6HlkYMk>q(&L4JP_PYOvDN%3-dSxUH zqiS{L8_?^8nQPv?P5PcWMiQ~s@7s&@1jt4Jjq5xa(v0~4+DG<3a#rolU(U5S67Q2uCxGkPjBC;@H}>%=A+j&F);zGu`dd}4zlqJkl>*f ztFVBTJ0y187(}MBE6>=Md^dE@)TS_+(owHu03^(Jw61(VjDke1f# z_`pH0FJXzDTUk*o2$fmc+2rZp&w1s$LqpF(=qJrF-EsqdR2lnt!fowEaU2dR=bo%8 z>$f3l?)E1RwfAAI*zVS5m`i}Dhf?clL|oUa_qW_pv4eP0}{vX3gP@F*JHmn7gePOmOK)F_ROhR%&FMryYz5&r9f#tm8fXh1oXQ21l;%R z9zFVA;j-zBoGm)c_wCl3r_^u5#oKXElKZUWkTLGH z=FH^Qi)ASP7EigtbF#_<=##&_qV}BRC@ukE&8LU|fM>4{jCPL$meDeBMz>aWIYrh<)PUgw?qETIF8riwet_CGP_z@u<`Ms0j#iO z!tS{_#n>OUU0w)-q9pPSUJAO_?##Du+mljXe)N+%a(v~f`yyDAhthn8JO${Ml9zcN z_giA&pl-4_zDD>5CrMKDBv6tW*I&(@EVk5;k-<6~7s~`Q_ylo?%cMPlJLOlGg#zj> zrkT!K>gL0#wOQg7tp!19eGeJVl=h8mP} z>6lTyHHuzt)9#2eKf$ejbKm%Q{Ra=I^|yMaw<|gl#RH&lA?CEIt8U9*`xJyRBO{}G z_wGR$Dkv;0KD=R7ZQ4ac!K0-m=cZf2+|AeLxb`;*11$6%f{NA|IM2~o?)g+A)4dOd z0p^g(tch7rnuq7{0nZoU_AUxk+Z4~A|AI{1=(```Sq|=}E7C(6MU9DAGa8Z@h81hw zzkeU37tN)zVrdYjWKOuA9 z9LVxdHozosZiLOi54^%PD?tqeL?o17P zf1>GKidr1tNC1Pkpg=Tog#j~6vpt$`4e%b!PzX7#QVBjy|5r7Ai%N#R=E5&DHW~QG zmfHFP{hKavLeqj~)Xx{7Tx_Umk1FKomR}$z|I+%qkB*PsacR&Rn$9YaiT?u<2G7L7 z!(-> z&B2vnfp+pg#UH&2O_3+8J4${GauLw$=%eS0>QVu^3Ua}3eW$+?)p{Yn^K^?d;Y1S# z@?dl`Ij6+D&!i!c2oU|FZBm-iw2gR!-o9jF5#!b{7)?{36h`rkkLh;5k8M#dQO&#o zQmENwQ)3FVprD|A7WOJ^yiEv<)rYI`f`peYm4o#G{OnEOF=>AnO-cTD#~!vZ_Md`Y z#@nW!s4lIF4#47i&eLbLzWF!B{ZaCzWlrd_!H->%<)-VG4@1Ca5h`LFGPt!Bk$p9U4=Nk(3l} zAY(PPs;Q|Rxe*!8&jxK>I|2bZD+X$nY0b&5CoZuRSkX%@aB8;I#}VNfVg;)$alzBL z>GSe1dj#2)Rma)v<6LeHu(9j6nKcy;q%gc0J5IQtn?o_3SV?#ThG_F$3n!(%LMtSY z%HKsX`pm9>>9DKte9MA{XLZz_46s0DY%*W`QKSD&CQt(%a_QUt=B-*uyHA)Ar!)n z&3!co0{z#4a2~4}S3? zu{{ITXsS2pGIC;kOhjs|)VzN7VsJ5x<$|7lp8$pp&6x&KzR$@sZw{>gZYbl1{_K~y z45xY!V-~{el>Z9QQ3~`i?5Iw+K!rNUVP;{*vy4M7g}z(QH>*r7uu+wez3gwd{shR< zG$Mt@|4C!V)uQP66<8F#mZpA3c<7fit9^bJgch()w;xfaknbA?CP2uE_?5m&@qfcu z0%(tT_3sG{)n9P#w-AN!*5NMa{_u~!UWSekCI``I?K_>h?`-u`5T_?A-JCuxz!4M8Jw*lA1S49$E?HBG_&muDv=xH@eef z(DBIL&>l<{3ZHiv5YB9W1Tpqyd3rPUXo2?~vDUoE*B!;|MUfqT9S+OGkD-2QbyEpZ zTJu}lTC}``;C~^w1xXFAJv#Ccz9|^V_T4Ec&npLEV*lU(M+dF!?XOj8HvU$UVCQ|y z3p?PQ;gN}^?e~``udt4d5n#RT@bUB?lk@Z)b-( z;<@e5tmZa4OkGy2=Kg)$|DyZiAcSI0x7v1(S0607IVtwjH~*-#tdAs4_^~B-2G9>| z4L$usfUb~UNrx^yMh#e84ZG`ZH|DVwr%z^g`e_VvstvojE(NVd2(}zGSOLn=DoCmz z`{vejVejlskyY145(h({<}XBbw%TY0>K6yisoj_hbN(eh)yHPJp4sb!Nx0k~jZOjT z87eStGkNcMwE8URJvbd|QUeCih23_t%Fw)r1a_J}uN|JeBh32jQNvUjf0U;(TCxff z?-Iq1wXbW;{oJ#FpY&`XmwQ=`%yHp;OvLz*m-qTf;7c6$3|zh7S;FW$G)Ku@DwLz= zNBLHj>MDsAAInyf{+U(b*73s~%-XD8J0{&VtM(*JKl3O%e3zb&57k9PTSxdl4Tglq z%wz|>jxXQC{G!czD)^}!W?P==u)%n*Ay&uh+0(`g=Ds4Z8W?JEzL`w41AR#dLu6xs#Lkw?dyMWp+QrvW!}Sa1z6V>->UtS|?nisheFqPm zcvqYpojLsshPf|l_GB&KI>xz-KRh_epfmy!2?2~Qmp`Xg`_5w|H4fBg5g{$JRL;l}1>v`8XLCnIY& z1lZ7yoh$Rq1P7wj-hy24kwmE@(_l$y%;wHAsou`h7&r)#m=k=Jcp8xY)BF>~&#@E= z5mCIe=Db&=k@bXMgQhLiWpwG!sfa}`_MJg+O!Z%X`sKBOBa;k$#f!8`{6E`epCKv& zihVFn{eKk_dtd)o$BDkazL@=wc2KP_F)>4E#i|Ml?pG44??Joac$LQ?Wa~W1s)%nq z{+R4+Y@oa+w>+}wOYI0GqUX@7Fe?*C(@$Y%(<(>?fgNOSrwNxi2#tjW1waqaprg+R zo;`auD{9%?X0oah>ZnE5K(f@K!N!r9sx7ZHD}^B-)np63sj11|*B9#EDJ3Nl1C|>P z7Y^wYBGxK46;)Nka&v8cj$AIqiE2HhP3lYbqSj>K{2ceoZUm0oGf;Nrom6M{U!0x+ z4*{QQ!O6~^Qu`2uLl{&t-3HMgKEQwj+VVL$Y2kw_f;g+vC$Nu?s!tEA^YZdKqWKDC z*$v;lp)LY~0-fsv0|TJk!v}aQ`eGA1A4XG7%2u1|0q{-n?4^TRk#?~~ctnJ{u5LRi zt@9Kb0DNv3yZSxqqDHaZ{OVHuZVS$<52Yx~%K)P^))Ib0dTz+IsBhBT%ORx2R3OoT zA~gJA`S%J(%zNYUJ&>#!DBpbF*A=6(wt|k3<7YV!v%PgTK1#J=76ESO><4^eX1XMddbidcJNQn(o(x_&B`0!j5rc{l>de@@Js*ZPb)KkU3 z1T(w6{|8l%TuGYu@79sSSJ;MwG@b=^0)>kF0#dOLhF(SKvjD00|9V8pF0ESe%gDR{ z(^xsg`XiLk{a8Ic46mNsWY4U~tmpYao8tva%EM-{6Xo=|N76*31b^$oj`v{9Ch=LE zl?JM*d53%R=g)bce`Zm9v&nJYSmBdLycjZWaV~dAiG4%j9VSV#+kP#pB{S}7Ti8L(%mu1=K<%uQ`6IGQ3m#$zsqVo}YVHGL zShS@lPZZO>BR8^nQ`n=RsUVJ3F+c}4pqYJ+9C3>da@thQGmvr*#whr!$_#3sfB*i7 zVh$kG4s=9CL^Lgu2*D?3P~piy`OZ7-hZMd(4seZvkZo5 z*{7Kc=8T;W9wCiIhT`!PQ-UBc2MBdhA-LH=Id_{4*ZA<^gBg`Jpo7dXJd*A0ZD>@I z2L%R(K>o~jXsr33OS2jObPP(rNzfl{wgbNi%{_gp$7z+rFjlxsSoZ;( z`fFJ}lsdrgq0g+dqr*77F8@(wIJKPSKYiE|^nxk$qb3!2tmHe=a{Mp<*tu4h-;f~+ zVXw-S-hy=ElkkL1dVK6Gf>)f}mBH|}%_l%V=eZ~&Z^Iwcs&NO>Frnera6Y_(5oss% zD^1uRe7@)53ROx%e?4q--=tsZ?0w&)->=~4{ckC)>#pHEb-X|IHXArC^a(a<&%NFj z_&wTT#QrYnSGH?flf4FN!JAmirs5dAT>dy1LDD4M|86#TiSTY<=-77 zM(oU}^VzDzLL{V;x04s*Eq{M~EcA#8S`GA2pOX}}5<0XtR>~{YUbN{FJ8k-ca3cx*CKcJ^M`J1g7o+z)-K@Avcl^?LP(dfd-F-uL^Qb6wZD zPD@Zw(0I7B;JI_pJ{vMDIMr1hX2t`715EH`Wqi0OMiEk3PPP>g7$ZQyiP*q}jj_**dMZK}sYNTT zU*k@Ll0VT`pF)Oiz0||MEh`k;^8Im#F)zV)`T2(q9ZF70TAn>3SNf|TkUz5D0^M#M z9r@!5-mGJMW|#{J+)D0k>aiGyLR8r!?_ z7k$Pu?P;QUYKXt1hkAQq@3}#7c5ns2g7@MJ{zY(L4ES0S`A~2Uv^^!*{+Pve<}u^S z61CfAm<;fph{|Q1u_QzY54n z*aKP(fib#KJY<$KD=PdQ%CyM^k#tu~03t8#puUA!gjB?C%ZUL}b+>4qttVUzUg$2j z%gAx6V}9i^OF`aVTVnO5ZIi5CCUx675>(4HU&O9*{oL2X)qEspHqO`x38AWqU1OG3 z`t?mr{vb8s);uh^x3YgY^gV4wUV}0j?Lc5`^yBW{y*m~iWKwQ?e7q|6!_aBq;y%wm zC1W>e0_AAiiV0mg)hL9$aMt^xu#TZ?6MB8Ew>U$BgPWs4_U7uR%%;&kyaM&uF&rEO z@=oLGP08sy145y0A=KNI!`YaGv3_!%<=5@Ica#NCbKkEw!)E{-sByp4<=R2g;AP52 z6YBPvc|9D6tp!oJCZ~0CrN&&NJS*=-s5U^;1`i|YD=4B(9nV`@D$2{haC0LeAz9Qr zsL^ci{EpBsftQCa?*FF^Mr# zbZ3fu!l7BgGk!!K1h{R-b|06ku&bYEoB-8($*<0F@K?cSpDA-?JJxO{3MY?wj z<28?9$^sq+T)Tc2tuhx7clH+$MI_$XyHwQR6BV?Xx~wd1JLecVJeD3RK}T*-^Z*M` z^qI5!$wA2PT?J0KqqI1>PMGEH^FV+#mRJ=t=~f?1N8aKy_y@}FnT)hMcAU|;w# z9G`5l$M@7sgGezfEbKH;d@Ek0%zAzhgdj{K%)W)i0>EdNFJE523;AOxsI?#~6`?_W z1gXVok87y(Q#4r*o};6jKqK+~%E;)b;j}&o@hk?*PSV{4cuS0$aYf1_AK|??V<|_b@=g@nwiuYn2SjL2^9g|Uot#Rf zKl}|ZDt)ki1cZG5t9h!jyO`RBTMm<^i@k{CWXiLumpZiq$phPm+kjlRB48dkVacls zA3Su3!_Gqr6lh8Uen|cj{MGx27SmqnvZqSg&s&?q*q=F#jf@<|!ZPnK`p{8bT|Hg; zdIF(c4HKoiB6{)xHL)fv)5B!QP8<17CN0X3{k{Zvy zR!Y~p{^-#n^h^Avd9~?oD=CrXd}~KRcMDR3y}IB3&xT?kj_jjQ2n{oBQ@N9HHbo$ z`Tmb$5oy8?V#z`)1(|B)@AoLQj{?>VpdxMiGis!W)jn|XIRuB^2Cs^y1)uBLx-0ET zA3x#(de6W>q?o4$Ef-2kN>)}mc?V69MdOXbas;zwEo`wcGBVCIMLu_0`~l*Cq13`T z24-f`DV{9lXWMvzNVuk9q@X|(E!g*ewv&yDkBI@3G|_~^!FwtA+T3Gq%#jR;w7HF1 z6Ofkjgb-M`P!PCEFm0c>oL+i>4H_;1JNhBgrBKwLC>y2!;K6HhwtqiJU_gLPHMLD^ zN5>K(*G8m?cNDd?hrYa}1wIA9Owm_l4}T1cw2SU?piDXJ7IlrlatE#Zl}N)p&po_E z+@Lb(YaG1~CV(HloP}qn?A6BSAh8T!ti83POgnk{oBiUbJ|44F?p(li$0g4M(!JzX zcs|XOWf_GzK#R@H%&e?dVCevcUQ-%1NPCI7iC73UHqb(?-w+}$Au+t7_p|Iv96bRp zE;zUi*Xz=veI(;<#=Ab>s32XzD`VKNIJv?}2YQY)$+7#f^n%y{VDX}e0jmymkVC}k zZA&5T@V>m4r(3W$0-ca{Axvb~!P`4FHui~T>;q{wpWaWj`a+n9Br-u@jN!tQsIu2Y zCr{FUh$t7Md2*9xz;63@bNXi!C?pips7sZv^R06e+ZJ~Tuw)w6+}F|ZrV(lg#!-DJ zc;y38@t*J#9ww4+t<4=qSwx3~gn(|r;~4vh?8Q&h(7&pXs%AIY99vLO0D2+8%&Ft8 z?;5u-@LloHX;+-;2QBYGJZ2A`V2(0E*=!&CyD$$}U}x?7jSHQ~mi~(N0IG~>Le>^O zXHW+BHa-^;FP%N%I1~m@O||M16uk)x6a3YIWLIoLQ11F2PMQ9Q-N;)pTQ)0-Z-XQB z-w?r#^PzLoxJuY?LOD?00?s&Kg4d43 z9|(1KMgg2)c4K+$0Z$*HF?CXEWoWW@arvNF=HgP!I{k4UUBcubsDCh`|1S*wjq6w< z#wf^3+@*bK>%mij)ww=O8k+esYR=2HeJ0Q0q1!eda4DXlDPB7bg7fr;-#@D=a?)Ct zyt6jzH$irfeoK@VGnTgopn|E1iG;?K1ei_hk1X3Hx=+8Q`LV;C1}5xNp*m}yMhBQE z>P`Qg?IAsVI(2)%o70!Nc8Q}etPtAn90mojQ2t42Y2iGUdXVNlfBqb@KWKgWI<-EL z1Kn9*$b)tCDDa4{eI_zfvO7;ZZG=U>)jlMkAl32nS3eFkvIxK*Uf!T7;PUDi9tcE+ zA_8v!)B&sZ6f`j8GJG?ipPP%Q-Qhu9xnc%FaL4fShLsiTIhv2pkig{J$g>I0fTb4F zyFVmsi*Or?*M_}5hWTiKhzBs8Qu}{;VpI($t7V@4dtfvePk*!GYJJtDGacGJ!Kg#} z1n1k?4{qVW=|LqL#$$Pro?d*F{jXE_1*fq5-MyD_m8ST&0xe5KG}q7HiHdk_0;7DW zId5j#-xMoJy8S9BGpy+@G_QUQ{t&eQclpAy) z!H-NsTkdKR?-z>>cYo#SKvOX>F%cVEJwNx_8H6P-Jv_9$;Mp#JPq?NY?(2J!iV7q$ zdKyx8uXcK{m8D!mvw8?yUGL~Rczbz$w{(OS9anRa3TKeiT?VcHWOzyFCRXyV^3!q; zn@eC(!^GgLuT|L(&kd9cfqO*T(l_JIbZvm2V|=RKi{2C!O+q)n!wV1T+JJL<;p)n$ zSrWnoVrN_`?)1DoB?oiYk96e?73Jk?os6__WtR5#8;~qFaz-6>Oy1)O_Pi-vDW?D9 zEW8WQ$5AF@2`nGe(lqldw4px^Bn}|nv3&GMNr0x9)2z1>k~i?-g%!TkmHa@}@T;3t zgP42A5=g75jiB2SGP6WSAcPb(>@F}ccxHY8?SAIb%rFBa<_q1&#Qdk}3c2G@YrZ^5 z2i#R>r_$8X0S4HbSy|sw*i5%G@$sqdKA!0nQi6s6frYR`C<{W|r7$Sh;fJH}Lk!_R zKfKwPGREIfbC3jCjUEnHWs3)7 z14GU-kU}CUfa;-r6dc*%G-Yvwk2{BcTKK$Iqx)IQgTY5Q8Segpvd$TpM(;)2bGYU^ zXK&tjl2%gU25p}2*H);@2x&#Z=IAZg$z#8KfZ`xF0aL`R;um_2EoKjH_eqU=1lT>P zF@tsj^*l7$d1{KEvZgF%v7MGd%j$s}8p8+n{Cb&t8vOU&i zt0^NxNK9N+1G-4QdSYQONQ=5DFogsQz6uJ}(Go`pZQoEJN?Ta4Y;w4R>bgmEQ}Q)< z6lGnNm0RA;vY}g$C$uh{=tN8q@0<4*p>a`TdgzNI6t?8zda0e z(3>mfu@Rr!ej+qp0T~9*c%AZC!)`CtoE(7VUOXrYY}&eOWCu{DFUUlf=lUEVY=8~Whd?I?4ZL`-!LvO&lcQNb?{w|mSeM>5Jg>6J?n-@96@nvVr0Y%=@``3 zrqNho5eD}`V8K|gtFND?lH=Lv8GI!D1xq#Fy{APdmr3O% z&KeR+e|Cq=+TPwCD8KL{>9OW#uxXd!?A8R?g;97<4qYdUbZ0qO_A`?Fpuq$sb~(0y zDL?`kEUKudYTOU|sK|b$FsFx@33d%-<+7qIIPg!94z!t^c<)U0P zg~_zx+EJ(lW4LJ+4rEVl3Qt^TKS0o0_;zxDwi9)1n+{g3h|Ql{`K3s>$-d4+baEeHG5<401Z9ZgBtL#8HDC zf1MZHkiotysI(;swrr&t$D&TA!8{<%;ho~?u*unI1qgykDg9c*iUXXn!D#UW_x9)k z6v2W7+lYE-QlsoBUvgyRSw==kH6=6#3ss&>)zsF4)fpJ9o?~7x$DDYY-_NM;m`f|W zT;F7-s9%BtXS1vDnh6@O--Ak1m_Xq1&X!$?2k$p9^JPANJ_aCPz%yX8@>RgW!>ey- zFndn;@-YJyKbsqXYQP%Mj&E>bo=wMw!?)`jwTDor2mSO6(8+9?E?}0Vn*ZoR_^2@$ zQ+bjItPMUl@qc#c7~{@@yAZVCMwOK8K#x;HUEOf3m5q@t#vsA)bW1sXta&bJ{Ux3d5a0`b=H1KchJDa&S)<5>sg zd;#AB{Sa)EAjCsj-XBQBKcj=)9X2o$=*A@^x?tcxE)~N4_O2nAM?sTu)u9**&jz7z zV$yXc%sm`mrTlUkmaxm03-xabVj)9a>;evnAKFUaPYjeggWeK4K7l$~Tl?-KwD^Vg zql;;{Sa=omPC*&M3)K!t^Bd8HN8+HP86?J_agR1b91TF@q`%%;uUX>wf%IetujLr7 z)d=Gq>RIsaT0cjq`J!MQG+^6;a+g-50CO!64NJ6?f_eZl#ef9Fh?Yf5c4V*J&xaczI$_0^b zI$&|}AX~0#4&}4q#HCHZqHcLs-*4Sxdg{c2&N(la8(33S$~)lkFC!N>&sEkbI;_++r`0PBFoae#IiKP}Jp1UO5bfqg&~(8+K=do}NZ z1=wtB^O-8;p(6*ZL7;TI5GFiS1^r(1zT;%~;5=pX;gr#-g0D}zz zzAvC?F<`36cU)ftU~S!bG3JCW#FU~yhF1~s7Tw=08wG42tp|Z2!hV8G{DD`D{9Nho zju13Wy#E62&)(r-WAv}Ug=P2%t_|F>wh7EBVk4&r*~_90!slEyZf6W@9C#gJdBuzq z(k4gs)&-?4JS5UM9m*epot35HMZEvjv0n?0WF8 z0Fjx;Zs-k_7v6IY1DmHWfF%G~zO#YT^tB)Qns_w~-GUUU z$qSEAjaNM_XI;)ExJIV!RvW~Ma{01aN_GSasgHCs7OfL9A(b z?(4Y}+cFhwhh8MFo_le`vEAaMY4z`e^g0zfjKX6+ksm{z^tmXPuZ~M3f46F3df#{{A|*B zM$E$jBtSaxYp$5O?q8l1Ca_^riz_B>x%V}a-PUvO*y=@Cv}pjzHeUCkrKt&#YN`Z` zC;vYEy{Cd6n7d*JHAIjHHr-dPiUfxlnv@ygSA2#kw%~%pxgBQj$&t1rS z;CN47z6}~B1&syo{kVYS&YPhlijc8n-$^xcYZWo=Opk<~`l6x`p>BZAkj7Ae%s_4k zH4-RM3*yqXFU~hZM+dk9-^~Rp*>C{|PUbh5I_=JragbEQ%2zHqo14?2=`6s{?+7R$ zV@vuw^P(R|Mc7&evaS*Ux(4Ol$$e>j-j@F+ifkH3L*0v5@ukB`Y%f zb26F)el29T_YeY^uVhO!kF$N;L3nu??-WOOQg=s{GZ!r!6sQF6;9&hE2B2JQGMPmt zv|;vzc|d`NzRWf~zm6gA2`9Cr;$-jIt+S8kdX;%ef*{!-vUF+rr)CIG4jKaVU~-ZN z($BMJwZU+P(U-TBndX~N2XwIA(XP1N-*<1kqqFl%^ZN+pM3CFFdc44m!$f;i#t4F%yUcY*!o7L(L-irhGZk!?JtQ+ZzAM3&PX9@9ZYK9zob+Tm~ zG1F*-FE(lh`zL5Z;^pM@7ek%P7$^BUs*+IhT6$K!8$UcJP|y_sC_u)&s$g;vh757-FV)(Z)R z-Eco}82ihWStykIx3{)Rm-2yv-9ItuyfPqCDT@K6TQA15DcZtIDc^qh@O3(|yKpM6 zj;7p`_%d(;pY+Ayt#*!YGs-d++Pe$APIUQG5HSom6geuIo10;>(&3{=!Lb)msf;?9 zKm>UXhI@?w;M}agNLjX%#n;C0l*a8)UXG|E7uIOF<@!?2mSpGVa?;aJa+R*XmRA=C zqF@6k9snAbLKLEpK}Z)O04cMV7jV1dpoFSily^|14p4?#S$6* zM|cshL*?(}88)v8T}$(h<9u5}w}XCy-EwPt`)MI3M<@nL*Q%IDs#3?ig%^b37Z9jm z_Fa{S+AXh|>d&76cNf#pD*r1B2-=#D_O_@#yLJ|~K<WTu z+RS$Qy?Iks!g8Xecq549xWD_rDYY>Pj8f@+KCk#nm7RuQNG7+^ID1*M6|w*I6BeVu(g!P*0bojxto8{vA_9fzy&}FQg%Jm!pZ$(%Eej9 zQOd|)qk0h%9m{3jKa%AJXGbpP#vFh-rKC?Kc*iOAC{BCFDKvkq{O2Hj)JIWTk`!Zn zVEnAcAo#xJKI{DF%AMP7=$FL-x2|-Qe{vL*49yrq6Aoj}ZVXekEDT=X>1C$Qo9*mX z2)2x+m4a;b+jQT8=G%2C96p%%MQu2$Af>}1dX9wwuAyF(%FNULSbEf#f%h{ z9fYKLECbMH1jat&PjCdtfz~{HRB!Q^2kZ9kr*N7qA{qmc?e|<2CvjrJGljyM1u9G1Zl}8X&WzLf&$|y1i`} zRp0b5_qo}$gU(xSPP9~}08a_)Sl-?W&bG@A!lbGZ?5%pSi=@q&m+pF-?*UW|ec~w*m^bks2&-{c+cmi#vN?sn5-2oUd_4CUCC%?9j$KWP8ZuuWX8Hk0HAtK?ogS)Q%lYaOJw2f_i-eBNruA{#x6iF= zYifwFX#Mo?kn}P#84$X=57QfcxeNVG$=(J8ph0n%Ced3nw>nF*a1Lu4(TQT+%n9Xi zg~jTg>Pfzz2ny7l)7poC@NsY^+f#a=eFnhBYDp-|*nw4g-K1@}UR4J+zF_J^4|6|@ z?c3(2?m?%)9MHy$cMOgrAQ3S#s{zcUrl!B!=7{{6cp%quxeZikCokTH z{7-BB&y6F7_Q8i{aTc|XTr;=LoLt;%HDY8Ld*=#iCT4?k(VMUchW?&Ft^%3+QNtJ|sq+Yt-*;L1;gldgVfbDzW=Q+>Vdw!pK)&=nnM~+tV=x!UcV8c*Hq<@&<9tH9-;`dS zrg3f0+2MtW=8tFpED-I(TE}04SJULz;Ml}YWa~H=H2U2d4A9CJSXETiB|K<95_Lc0 zK9QiEg3Dzf$#}8g|H+@`TH<|%xRZy)rBg1q)mxOd`+vFvP^Mg#8zw^!Tl9WNyk9vi z^Cvd;0G$T?M&L2a!~`ta7q7k|J73ahWx+2b7cO1YGOXWwR1otRO-)}iRN^h_SaT&X zU+jiF6d);h5;ty?11_Shr=@j9^~GdUVhGhB`I4k{l+uavY(o;<@6WejB^1f>=+%+6?cebD zB09ArkT2U(2Stv*aCcwY36>T@14$Vd7Y7)o_i2Id+|BVUP+HIQd4T4x9)Z<*)I;eK zI%vS;ThbTopGrOf{GxF?=Q>uhDe-EcuWuG~?tlI)I@JJkAC<-sz{Msz5ukn~L*1om zg?kU-raNfa?64$Dl2vR0`CI@X`d;R05?OA0Mbtr5838c~i8V~3w0Q$)Z?yZ&Z#3U} z*U|dNaMj+f)>?aSw|;Rpwc%vz3iO%*kCUm~T@JGx4UM5!6S|EuRB15hrKF<53qmQN z$Ux95Xkcq;$>z6boKC@QF$h_w@(xIjq0bT-#z`&ICf0o_T17D_c*yB814HFN@H;*E zeSo|yzU~*^;T&;@)OlMaNdEet06vn+pq%hvO9kkD0eFcHgd;#u2iWHJH_w0P4E|qW z|Gy6g_2@O#{{i4brMX8eAcb)esL-sC-4@A%9LOU7-hKaH0^#qnIqbg=b^tXP0fgs; z^XHWq*VGRBKxK0@=rSss`1w&h1|}vy%LC97iYcSZaDcCx06ANDo1>)#JVMQ##Q?BA z0S*kJ)=#kafNgU026_tgQnIoqC5>}?ITIO7SkH}tcN#zRB7!o0d=!jOY?j9ElpK+u zgMpSgGnq^m)vsb`pmn!HsHU~=G`rjnhbzOkM(Ch0Du;)f&=TcLF954PQ5WFAXdgip zd3lky*Fi6HIj!gsc%X4Hv1F;^k(^qLwHC#^3t+t&|-i03F^Ew52vjfg}`?L|>-nmKM+<)Gx{l zj6!UHp`#)a((7=-KvleKhR_mr^}(?Vd%;H3Au?eUYE z5XeDMk;`do4Ul%{467q50$g0^f`Xuf-Y}D5!Kv?U4P~<{Aux_O$B%zsKlS7M`SV3! z)5^XHzKk+KkL2jwWz-3szQ!z`X~ty0i^UU<_7eIey)_@_t>@d?!*oLHUzZw%8cT1) zXV21bK&VNqre@K)26y!P_wO){hkj@DaQ&h==>&cF?x;qaEYw>cR&Tuo2E@(XomqWj zJ>c@GXpey{w(J&|yXqguL?z@D!Ypz7_Q!+-EkpZ_`L<+IOG7OWV&~+{WCt22R$jXj2CkBvJSqHg-sxu{ESha_?!9GL6X_^{ zD8RtE9TEk95@-?`Q9Yq47sKsBti}6yqKuTB=<=Bc-g|b!xsgDsxi0b@xHkSHKJXOeGImlZxD=0_k|Ma=9(WAJkzUI34;;~i$2)<~38BT1Ya z+a2LL&J2K8ZjYI2o<-#Q`BQ8Gb(Qb)T8+G@zrB%RQh4_S;>^)4vmf52 zeA>we$P^Ok#vRv&+?Nd+O4gx=3pZA1V}dppaqI+gW_FJks3K2diOSFa;D7Pr#iK`# zfW4yySg|rX!?GQA_8OEcE=gLv_rf%h^U}lfn)0HW-*|4<-lHjqV3Es&geLQL_iHe0aY94}0`9Qd(o!ibMZFMwh*)PmhQwqc)%A zS!;-}^Pc%)2H1PI2e^0uL--|MYo7!!X8pBJw1tI*LDmWWkwsj==*4wwx?IE zk7pC^VV1c%|7qF7I39sum*;`aH|jNADOU)%?9pD{Ou1q zdXmc0#<70{n%+-`fDyYaC*3=crgD(6&6Ib?mU0{`ZC)XEiB<5(6vd`U^k z=rZ6^tea!R8C!-s7kZcTL*Bl9IMqte*~>YckIO454_&wbP9J{6tR(%{t%Iu45r()a0E* zsFWs{Bf8QPmr;Yu$Pv z7i}SPAWX`*>q&3vAL+WRRg7K(LmH3RI2e)KsyU1UW!;DaKI+;9D2O1-c2uCc8l1(= z$&Y4-3)+<%wL4&_;Fm@&peFGKb}CSw_|`~zvEP42IhG98w_Nb&H-L#bcv98HR|!c; zQCfc;9o&;a6XNL4D^fA8_fUa@pG)A=(hru&r(tQa49uqfjS>}>OOA!n?vj)Mz`UE~ zq6#vnwH-}#>4O%s-c{{@_M+J(o;PD=60kz&iU`G=L zL9*F=t}Sp-&PCA)1ZP{JI1klj_TWkiN=ghBsq-O;mcvrP6zg&$v}G_`Jws|^+&SBh zX3f*61r)G9Q`x{FA~fO5?<`mSJ4}~RifbK8bQ_PucLLcdzy!;cQqzk$a-%>9vh#CE zby5rTo{KP&ma5x5d5k%l;$(00q7VnDAq+&OCf9cdrC;-C{FPY;B~0C_50Re)w==oH z@p!5|eg(sOkaF&8Yt$NEkeHg*JuFZWZd*g4qy)upUYEPK#%dz=Z$g^Clj47e3S2mE z*vovY;Lo*rC}u{{+4rUzzv&^q0uCge_@O!|6wRPk{P4xp?!vy=g@bdH@PR&0yRhf- z2n;VA4^*J;e`lF=JNUG*prYthiGdITcK<-egBk%4e-_PeZ~9UaJDJWv=V|GD$tLLW z3Sf-{1r-2|oA1gN(#JPG<5^V1p3Yaw8zL9B1!L`_Btd1e^>m#3Zm;)u7#%ubJ=6IE znFA6yImCd09At4(TDLX=$NqQzX}b;%Z~)h>gQNs9AUoL8mj^0(`hq1t?~L_a}$2JejsB`BesX_V?pys_460hH)^Z2hv7Lry5O|)&)1@QEJ&- z`;0fy&L30`06(re5isp5BfE0AEWNLtlZC7RZbf52y5kgd(9!OK7r@JPx9^dXJ%pF_ zlr-+cXn=vu{-2i~0(s%7xoVpy0bz@!Bw6hihE413z?G*N7y;lN6o35T8x4SHLu~cY z8vs`l30ZILYVh8PgIiOZf3Q%s#fNAIJnY=v-CH!2;*6S1?Xd;zmoG_5GJ50nY#W;s zxLv`UF|*B0VOj^KGJtkt^y2#1Qu6$~WecH#wX)8$0Xw_469(v>mLennQC)FfNzZd;M z#rGAdAhLD5_|-080;9eKUxEJ~Rh&lN?c~S?wo7CH6hMksrE}-b9ax{B1C4X;;y%?p z1-^15D~KOXZlGc9TswQTDF__6e*(JzxVq9SSk8foy`C3iSjsyL3dr^!PlhP06c9Q48nAtdd@lu7C^)+b1lK4Yh2x+rS)>P2~KR zxd7vU1^?q8lHMSgd06T;iU$t#l}l%wc!q`;_8^n*g*7jFe*!$p{pCr$blZ~n%or)S zuvJ#xdajn-*%##WejzA`V(N>XM+C*=!lWe6+@)U2Mwt;A7`_7rHN7yHdf1w~jihMr z;`^3FtI0rOrJ4|j$W_9_i?5@Q!}HpC#X-ReDW#BFB(Q*f-|zg0i+oK;LbP4)S2OG6<##o{h#581P%}LgaQ~N~*$;qT>Cs6lm}=tD!^&vR?ZivXEfn4h zuKIJ(PM=(z4A|o<)j2qcg$sH|tw}c@v8i!86$B?Cm}G|<5&S!A>Qt8cmpho9PW1{7 z%EfNR%jTsTc5fVBm?@V$vO}8XpFj7r7}|C5Z~Uge_YpBy>rYz&cyP(!H2KDIc8^rh zOZ%X{46CmY{V<>cYEE0*HIRdi98tlj`x<9BvLyG(@JMwQ8c`+Hk~iR$^g1MY zPfMw0QNR34*w}p}z?Bf1E5O&<&oXBZmph*N;WTC=Vi$&|AO>Y3Owzn}uNmCa;I;np z2m!E*zMC5v1#z0H=<4d;ym|P-8i9cFD^VfEA=NCtcbkBiRi#lO(jsDFE2zMYYa4w$ zh3sc<89amgvpEiJgWY^n54iq;E@|53h5acJnlBV_k`opE{FMYy&Mp0TCeLq;co``? zSKneS$^Ib9H( zXV(DCLmDOwPHgLx5eg0tx^PtO-L0TKHqO9J`mg*UFZS1?^^IH5nQBcWWV-Zex9poI zOZn8otvMhvrEcBI1ADN>#Ds(s&Q3KJuO6vl7Mzrgi25(}#V5LLm_MdbY@ZXz20ocT zs?({dqSbyYTnyX>?*|13|HDr1ItKlbsIUtg$O`V#kaCXPP>~aaMjY6`7oREYE&c}e ze1$R~)HD$F2m+Y^o{mE8eC?t$dHJ>N(?f_{Wv0i#bo=6WlyCC$?{KwGdrqOKs&-#d z6B7P_k&mH5&SA{mrC>0u(9WeQJ*>IAL8|%qf8b$NDKFPl1j|N-0@bLg+~Mnb<4}W_ z`ZGYn3axMR5O#I5fK$gOAYcam*7OJLa^DyqWX1wy>m#^f0NxUS;~BrKyVaaF>4ggY^b{L({y6@^@}FD7b7eD1?n>{qt>m8`7*z zNJ6xYu$&2?G$S-XgC?wH8rKE$6XE_0=(_E&zO|J%KPO}*Eqfy z5m1MYXC<*l%imX8`V;i)U;1C z{6cXI*6;lx#+1r}Qi9t)Caxc#oNYsx+8LNeKnobtSAF^WqRF4rIjIV|yUwQIaiu1` z{L*#o)_T#wB*KhO@_QR&=1PI*uM3f&W)kZQo-HIirSHMhyN|!HisdLL*F(+l?bR$< zJ5Vt?hW0h+%?B>o%!ciWXLHH*FJHsa2peY7IMC+NImod0`gGZ=SIGkxa4k7Mi9Bgz z6Z9xRr-2Qc_pTIPPzsDamYgq`PK7NV8t9gRdWL%k@JcmQ-j!|g!8zTG_g<5yP*m_D z3}+Av!f+mYmL;r0X{IKB@@*FJm&V3%D4&#QDhM+;#_>XsJ-GJ;Eap3mJAWT`F#dRflw6ft_qdJ2&vUWff;{*_|8E6?6s^;S5 z`TZ~$kj{1nDjJ$n=(Wrc%%#;)HD$m}LBMk4JA)k)19GSdDb=tc{*|=Up-?wV2u1U( zZYD`s5_8u|*D%7e%g8e9S!YHC!{wVtYVAJ|`=Tv3>ZnjNY)VKS~y zDGF6AGJaHE{)zk-k&u*N&%W7CCM~OoSA7QsO<)iZpli94Al6z~uxM*y+Uo^qoZUZ4 zUNm`bDy5}0Qx8j2OibkhI;_qz*DNIKvL{v*45op3bFawR#^B|=M^0RLq^Rn46_py; z3#V?0aaU&Nj2_2%cufv#FV%8S{jkSv_I|^q-l_}$LbUUHCPG31s8Y%>#M{vR+R|F2 z=eZGBpC%^S!MNASE)|s+9kdHNFfs7?v}aX}Jfu$Rmp*z!ql24p2QJiGgy|tL84(aR zFb{tW#-5NJA?WJ`Fe0D;28N*51R;5#Jc*2qgn=k9Ix4BJE%v6GS_n68CzK#iHA8Ja$RD6}9Xg(&kbwj%>$W%PeVl3n z&wSe-=V%n(Ge?QdLMid>o5;-~TdPoJqdqxd9orJfZ2T%SZkxyWDgu$gWb|ZuujC6GFr*qa#L`9F% za3UL5PGQ0O2yZbedIxCE2f4s0CD6#+g%wRoVxt0GN6=&nSrOo)uW5vwU|tvKyN@b- zKqz7m+o&hh$Uemk+%}rJ3d$s8_{fs-jUcP7h`*p7j{-|U6qfuug1Sgv-@i2V>xHPQb&oV?ZbP$D#t`zb&AuTJc#ZOU)>yqdKrgKNK1>?SMZP1K{i4!1AhWDJ5X^{ zV2K%8(SQ3B3OYZ_{vZ0axMD9_(?#S=4@@a22Tf^h);}F$jOM*WDA3-*^R_0z?NAQ4 z#YGXD-iKl`GT*?}@|<3hcWa&UC`Sj|N1yCeBSf;&WQBtW)~Jw(>z>Mma=E#&1|y0Q zsy2aES3n0Il3fNU+!uA&YsI3X4p$v!Oi@;sKqfQ@ItIq3Fv%PlI72u4e#0SG86%U| zR0|+-Bu!cbXy79Wijxp!fmJnK?JObrx<;CKoU9e6WA#@=uhgZ8MH z2~aY)t-NiQulHQP_aP~X;aE{66_RmPL{6K!@j=pxiia04Q|OK5GJBCul_&V~fQdnL zYHGv6$?*EqFk3WsQaeaU7-3T34fbc&Y-o1nJ&OQQ@HH4P9AB7k)GrdtzQjMNXf`7q zQD>KY$EpH0>>TH38!$7M#9$C$6$L4 zZziSFa8_4c-R)JUClrCe4-}|Q#;#XDG$rISU*X{3iJR6U{0gU{CuCBCcqEt6v zO;+90RFs z=&h}H5D0z97hDk1Jl&`eb$u64Q0WzkHWK*Q$s@FB8wY*c6VbaXHK}|36r1T zA*s-LxXl-ElWeN3j_zBH#TGbcWEYHLH`y)f%daq+JMyE`^J zI?NtOSiM-NeOO9ycV)$NpupL*O#OYnz~q6OM1yE3g;(QMWg5U6^O9R9bp63|9U(?R zOI%terRXi^x$UXimo79WYo7%?&X*C$SgviRTyj6?0gZs3XMy({B`$*=JA_OcG_$V2 zZmS({bAD26GI6S~l=8q}a+4`f6++r);i7tP4lJ{4AHM%nk@0||U&*Y?VfYgBW-b9Z zHJ5>AH1!*}{DnIkanKcbO=Q6O+B4`5_eC*JV60S$Ky6S*P7 z2%2AX5On3XC`6C$3P18A=Wc!jBm}2jkORP@LJ?qE0_8CuHTalAUPdX zsWPiij>>ykGMOSmHZ(AC6iOrV+kpmA!P>Gu+9}@WF?a7ti!j;H_z6+1;gfy1@wHA} zQ@7w-F=y{e=!CC*_@ZD&@X*>S9j7a9SMgR35OdIF4*bYhuGgSNLk+~3Lto(*M7P_H z!(0^^DZBblEH?O@H6S|~wLOop5@|{YZI6^=gL?kyOR3>{i{+KX>4Ny;oV>~(x5^Qj z|L^~8uD8(jvV03$66TWG9WTC0S7OZla5@-+-&&6Ej4uooD{u5Wd_R6{E`w>lRo8)y zn^+ukf3az=RpgEv#QRqBKC1XC?|?3())*YFn$*^QIyx$(p9PZuh)$n|BE>gIe&((l z*YoeczsSS^-;0n{=-e>n?uzCB&y$Vag++03?T*wZIiM!E=$4sdu(3H%jZaC^ku%*9 z9aX}keZ#pVYDm|{IO(g|0mCRKo7M#L7p{-znn#U=wzOfq;m;obhmMb|=i#r2MoFG+ zrH^y3nA+p{;vZ>6SDUSgtLb`zpWeY2B|_92o)=sc;Ue(l8z1(?xqr{9_K_;t-GPxJ zn?JVZorNAj)m1d&hWAqjpIOL2`my2UM0fYDtSsBgH?iI4&IIP%U421DnX1=9CeDUE ziyYX#vOV!l9A;F?D6F12!kb$~6&})E$jwuDj#(-|YkiS$IO;*In-BR?m55g>zH+)~2c2Sjvh&y;9Xn z&4L^03RU?YPe3c?G$m1UzA8Fxe9Xx!>SW0|$*rYuq1mpb;6?$i_sxoFI|DqHM}JPW zF>&WDNjY!yd6C9vT|ISZdyH$Bel9>NJ2XI}fQz?4@>$Xgb~~sAI`%M{HUB(;WgcGWr-U)7&@Ylrg#KM`#%%-|uKy z=e#9$Q)|!^{FyIY5Xxmr44+T{V_L6^??8z&7`%K3I(Nd2)yZUJ**F+yl3AW@GzdO< zHejdEUq9E$>fijHHl-o1L!;2VDI)F0;hwK^?IoLi3#GeHW@06!!i07!N4^ehb~PSd zAf6n!6Ff&iKoC`&UN^Ao1gv^7K24bI>P+^-?b&sjFui67)d0mESTNTqM=p~PZzt$C zn!^GVF1j{Q5{X41SkoMTSD-&xQPCezYW&vVZnHP0<6qv2TcfWvZtre%R4{IT6BGL0 zChHe}kWNalp}BQ?X-C8AU6nZRZU2d0Wqeu#&Pxftvr~?Kd~*{81~Q*B?rTzq%AV|F z0BjF{>3)a;(8va@veVPkRSr16_n?MEmS>aSXeT~lZGw}DY?Tfkp47LBxf(yAIA?m( z!VQ>?l*r6@*+IH4J9Nfuc6YlpJTCs{G5onY2AEoi(qz4oy2;iU%rkC zzD6F)Yx7yvfnz+6bfdDprkUnN>w@*SE+22!PRhO3)>*ns=nuk7_jQ$|#U$Ua{blL( z9aI-3d!3z!Gaqyf7u5RWjcXE;oB7^3b>6h!(OGwd3pw{yvDYwDcQyVsz|I}5tsXz$ zK?p)E9CIa%7%FGE;kUavK=AVAWDTGCHtcY%yQZBMn!*)O$@uiyXlf=p3p~_0W;Mcp z({?T&j4D4jr%q4Dm%?({a=z94oX>Qez?tmE+v*p`rHWSC^Xlqs4aGK?>O$5lv@3?a zX~J)xBpx(QtVzDBXKVY$R*#DRcF?6SUm8kAcZ+ao`g<0t#7kB8SY(lC>Mph{l|8RfUjDOcr7qG?!h(qML_yvMQpo%O^x#+|cD7{z7J7 zOuV#>_Nn{Ho%oV{rCUeG%{M= zScx`S+SXlsz4jmf*O98JFC=Q$r??@vJ>pwEvHd%gFWe?OYn?3ERVCpobvhbOVQ=kd zlZhsXCX&!q$i!1pgaz?hX~B9scdq0*jv$p=wDEUFhcrlig-X`*ACLhYBQ zr;L=SC~H+i*op6df08wj9VX!L#woZtm4oBd&jx*tHWn(;qC0mW5#2fk=?V)jZKXI> ztV#&3SZG|e{lm4HwKe9WA#yJWV`HPi1$gOO%vfV79Y4Rv>1F!RfB^8XP%Krczi60l zB8FVoohsX%Y8s)sCFD|=?z&ajNoEBZk-??J{mZ2o8m^fHJus9f`!N|`>hW^D$bF~5 zrko2l;kiQdc=pls>s)ee>D-+DXl_FWy}-2YxY2fXW1}y%9>_uc&1I2=P^5!1NQ|UG zEU%EA_hgp6RYRzas7%mKU;0o z!Lp0QgX>AA5`_wF(i~gpI10DXp4O3VmK(duM^R=`m^3d$8=6k{Zpltt+T57)W_a+E6oXM@u`(L+8?VA@DFK(Bkv#mUIL6;fxY{oThG0Z6HEY5P7;{t$>U0x`ymkb&vy^J zIC^g1c0+FV{&qPIuV*#pmyor2=2h9qAm%#Y^#yB>K&lYTx4>Ht&K|zc|9nc%%}}@D z|BI+ZKejpfA8-kt2<}&LC2V?Te`Ja>!9BuhSYE3=)z5MZsS`@0kq6+?1sV+~Y(RWh zOjWWRAFp4lobJfecm8p56P~QPI#d4*q>l0}nS`$&0}?7`ycX1S_vkVXs?^o)F!R*T zf1AC|h119IxB?n^JkUTX?WgcAiAhOFg>FT#9FdQ&QaAUL_Rmj;@Y&ogdVw^<5=)}e zAbawTpp%>;n;=9+_U#^L@|OxZtG_N#ATV(;QuT#d;Kqji50Xa?P^@Ucg;QNuKPaGf zZ$MgIUHwSJH8j@V8Mm~yp5Hs;lW-mjC?s)#P3X^>rwj@Njf}%cXhNktH>;(G;JO_C z05y2=f0k1q6WYHfl-#l@*7@IeMEV=lVqmsXR>&~xQ8m1}`IOK`PFfnw(wP_^dk0U| zaZSg~I=JjapMd@ISOHUfc8AZG)u<;{H&T;Q5Pb+99vFrvg?C__ z6GNrpv@rMy05L&u76D%ZP(rd64kUm&3dkBEn0*Awg3&7UBk{EFgQ8GR+_*G#2OtcD zABT`DYkQvbk=ygloKcXm@T=zm=&RLHP(sLy&8 z#3`-vOO6|rKu_Av7ymR^O+oiZv5($LW&;~Y3Vz#kxC5Y$ga?}T9=xJRN(&;c@B@No z{|SsEVNOiO?FtE?s*xeA{FX4k1x6Ab-q%b2-JIi*N22%-E-W*`x+j&+aMXR zf0Qua1pZROjIZn|VPu4;PoM4w+SqU-pt<#Jy2z7_<)OyR|Euh)!=n1SaA)X7R7wN{ z1d&ovLRv&bK~U)ik&XeRB?crE#X<&YL_tcWyFpQq5b2PT5J~C2>wy07_kG`eo_psH zeVCauXP+Hwuf5j$zAM_8v%bDQWxLpWvnY2&oWg@Z+Iyow>vOlD-U;=<#Ke&Q)LhB$ zb74w*&fo1=l@VL7pNoMd2?&semcfyD^&0q;-B-paG2 z6*CAv>Si2^(c*lPV_p3#Rn>SIhH%fa+?aKk(Ge(N$@8N93cx%_e#S`N*5CtDFNjCw zflT4m7^**t5R@>vF0dRjgAq8G-INZ4# zoCY3I7Y^uK+;84r)jT%Y-vV4}+`DaISuxrB53xQ$mG%YR3q^KWAhUfT^*lm582Jo@d+65BZVUzZOnRZl4{J@9lSI))^OGE zK+Uh#m&=n~^Ksi7$C^&numE~H&Hdp@LG&8a|u%`;4GlE2DAtSuGq$Tl{t3;<<7o~ zrsR7g-D495x+JPMGc3Py8OW=178m2pe$O7{AWnb{hS1q}+#@wt`@zMl&ZzyxtH5jo z9kr6CW>^*wT=st!5f=8|qimNaG%-=*%8}f2AfHcT6zci90Ky4UCr!YOg^0Qd&_rx+ zLbSRhBj*W%)7E)*3qYN4-kINK(*~A@E}UJ)K*3OZ5B6j8#A&}89LbxSK7W>I^CO!@ zz>eWE&aJ8V-$?<}$k-ldTSmrgW?{j7+eh|uxY^V+5PV|DipS*mz9xPrT zH^6tI@L4e-4zkbiY`R|M?qKYb* zav!QwSvonLksQ%&GjT~HTGt56XP`W^^?sR@A7XXQ6jB15&mlwq3z+^1khS>M5j2!M+k@4Oe$ z$AXqukQeWf1OY^d$WueeEFWmm>VSQ0wpV9hdrHJQMtTioO<+a85`;wb4bGIt5>(Uc zT(>gCzsx|Td7||oGI_Cmhn+Kbxap~a$&&Q9lHF?0rjpjiWHGvksPbz=_J*3_L$?OYlyD zwj!SfW$U|}fp9YfffyZ~Zb;W9x^mPA{<$^qR*;S#dkw0YycnkMrb#$i1l}wF1we@jxSWvusuE1} zR{PUY>I~Pkv^t>%T+cuSJR}`j{mxJrXx1s10xlgmbceq9?cbng#NoD6Xz1xL@?PJl z*(MHBBx3LIImrG1b@g2S$Uy`=HNY26A@}eem|6Kd)b!t5_zJF7&ceHQRLKgZauDyTXfA5Ahp!j&^jwRa; zKUh*kPL-(S@g6FL-mMq+T-7U}jC?XwF9xy}4rJ{en%itb)QXqX$k&!(W^upg+!Ks}J9h57iaHAOLe%IVEMQB1#I^T{Un<`4ztegMS>ium zL=56yF1G6KPA+!$S6eIwjv^`7R?FNj<>EkO%qpf>BOA-o92aFbzl4hztS+B*N3Xp7 zTfk)g3BUTzBkLWaEJ;X^-B>*yPSMoo0>;*Y*}RxvdJJTfwzav$mZvYyJc`mkwDvN4 zyOZ|>zWp5#gT5~vIQ4X}u!m~T>zVUra9EV}8e#3Qneyu2&3Hawz^nLcN!3Id*A%)+ zcMA#ocr{Bh-R@nt3Mw}E8Tk;trhev1Q!pgyu0WD*DBz@@&*oLQC1rm>5ZMtV`Pq|n z3~c$1f92rCaE@L^`VEGZ+3wToTKup6a>s$L#AjwRc}GIdv~Yd3&Sed-hJBMeBZ+{@ zQIUbooYdlnd5}t3)cQivyVq|w1s|-`K4R%_XcBMt-t8mzRyL@`l4^C0>rUZ3cW{#=)zpn@GB3S9FkP3~wbzxN|?co3P}@ zk7q87@(TUDotu#_z(oS=x}yMAWnM;+VD{1)xCG+N_PpaKu!>S8(UcsMbf{TL7Pyhr zC1lLjw$y#lD?uHA#C;MM#|qUEp;oTj+>N}P!b11|l0`U3%(o-F0JPZ(_5nqAz(c)) zvnSY1VAiZJ&zMKtSpB^}DRZ#1bNgbBS7@H?+z+FwfJ7Dy$T_$#D<~@NZAhIhK&AmV zm_^(r{q0-3KZ2GA(Ypt)S#8{7v*S;8Rix%>cU7bk@%(Ru|4o#oN3ZNB)@6rYy$7be z#@bj8z?NP4gowyba4;DT%9uJq07xrx2n%Xe^EV<-`uXk0m3giL3_4y73;F@9P*f^0 z1rLD<-$!}gyTeZd?lr+XEwT63#x)&fOSwS8w><8=VYsz9cnfp}jaHwPxzBT*NFwE! z97Uo6GvWKs)bF+9WHnp|fIGW&TRG5{hC@T5QrZ>Ief#zuX*?82p6cnzg!Zng+?~O* zIt+8!ahrVn{8Ml^A1bDS+8A|k)&#f`Xe|NTK@#?F+==(ZAV#JZ*o0jFZ+XNrl8SKA zonFQ=LKkn1`D~f*>X(3v55#?AMX+o4gM*=J0Mx!DqB>jvGTz7!?h2vghwML0%CWc6 z^AmP=gZoA=MODCDzmjxPHY zX;|p}6sOkd`L$V7zHQ#2Gj(wgB8e!nbO+x%daUyHP`G6M>l<$Kz9Q#^OoUVmhlx+T zDD3HnWhFn_s9ZBqLyMig4U*aU$0V^Bh+u^FGRT*JvaU}#?Z{QsHEr!nr{ug!D;14V z14ro2r_V@O==Ku}J6-be*?2c+2M&HL6GTMqLz_(QK6F)OVDsKY?8S~jh>uy_aOLm| z>i+ZIt}}-!*yjxijlMmvYX?X1XM^0aLwk08$N21dA%6fC@Qggjz##f~nuHa+3#h9+cvDYml#kV&JadvDPGrvOnfXa_E{lCiYO(at{g1zWsmz4Bcn(Qk19hO?G+|~0n zT@CDPH}Kh9oSmJ8%6dq2tZeQB)4_(TB29G?P|^Z0bsLVS4fl!LJW+cPA%X!KVmBx- zS?!8g2~xV++IBfYQLTn;%#3IJOYUD1QfOZS$tOqSpC%t=+fRu0Vn5eTdlHM5^ST=x zglW=U+;?@ZZM|UV;{4~fDd8VjNXCG^5YmDt$$n*`PDXDdqVYbZ%; zDten1vhz~jrY3LvD6qKN z`DNBvb#w(idp3=xzH=LBqfoI**}VJw@ndJXPr14jZ~}!o^i0A6LIT2UQWURT>C4B& zsV6<^17K)6n*X!LDlox8`9wpTM96zMLP7j6nCLzGl*F%UG6%9 z@Mj>{1w<8)>Ip5+WZjQmnmc_{UlpM#rfbUxUKCl(`bktG^M<+NMT?bM#ZNT$L~`P> zY#O?pmD264bxl2ofZR*<_7E6%=eMy(p8dizuT&g=qO8RXI6xf`mD4oz3RABmy2Jsp z5KX{X?tyd1AT_JE!KV9)OUPppM^L%>87Y2AN$G)C=+sCg=}*%RN;h8}aqM}%FmAsN ze3ao;MHMrS=`sX?;QDn;Ul0wo5E9i3L5l$5vhT{)fNY`)C_BLVT{}akkwxnOXxqu{ z6~NDnnS7vl>uVoc-3{XL=YB53bG?BFzvt(NC-xn7Ofw;x(9qD+u#oN$Q+LOOJa%qq zNPsX+YoIOJlwUgZd4bsxp5$D7Q3;>z!9`RBeQQf-*IhVNLm?9p;5mO_!t|DT6|1|$ zO4!9xIsrQrWR|}HY&0P=<0-%M%@eVnzdqG7y}(;fFvxpGW_;U{zkIsrCvY^FpHe^7 zf_h(2Py#4qAFbGvK*{_)y`hq_GFMOz0N~TEw+o5f2R5;xOP8r~_>rjMQfQOQbZ-jO z{gA^Vos`&?j5SwB%@QuYxoXDo;5(F7ssq5jb%+t5Tt27yTKJms@=3tS6J>}Efyt+m z0T?R?5oh;%;-M1AZ3s|iH{gT215C$XEI|kN=UUU3R*R((B+iqAE|LUrSDYUFEp zOfT5rI1j2OL)JoEz3+Te5s0+?K40qo=`>gx~}eeeiF@2<9zNX&zPc6kFF{&+$t}gzQUN#c@{AnKe*#W|`DK%q`>3$auJaiRn~h z+uaXmjjIUK_zqwH_}=NJWo-&&t_t|W3JT99+v6Br#pf>3kZI8dux{80)BTW;+#Zs@ZrW9Xhujn|cI2iewLxi022HCaV85q? zXs|Fr-7=ws=aK(<%+88cS0CeYn(7#dg?ZOW4Q=70B425em|ugoyO{gZJS3|yWvj4Q zU-L0VWx#TC=Bejz3<>V}PU40O)t7WzOe&4FK2H@$Wb+Pf@X`YxjnX;sq>F?+{9aXn zc6MP;(WI7ubzZ>9v4Q}vzO3qe>l$sO5OWZ#cPCGM;CC~zD(KI^#OdCA4C{JtQ-cEp zuX#1!=5+WUR>g%h$hvvyTvK*A);mzhYqq^L3Ldph^3thW!-knR-UCKrQ z+ZeaoOy>!R%Cq-^_G9;^tO?_37w%tw)v{A52IxqV5;nz{9?3`}S5lGG27%Mw4 z`aCL1++)Ea@A#idBy6N@H)n4vm)v!>D?p!wxJf_r+#MpQ#+R0qGzzT5M1$VWyVqXI zcd>qG9<}SuGAT^&s`BRGY9{f@DgFLbaXbg2aJ{e-bQvJSdF<$A zaTbe>MET2?FME1QtWqzXi;m*pFM>r4(U|rm`?{=5QYLaf`G4MBS!?y{YW(ZYfjaOuerI~rzYOLpERd7Q}Xis7J; zcX80+u$n?qHT7@0NrDy3T`=NcKXT-fxZQ7~Myep7Y%PbZr*BN0c(Wd;0dlJ!mGY<~ z=ixmx6B-+loJdd0^yNEoI~Nm*-1fj-!{;gaZQba@vG)%*rNY-Ibd8;C{XYwQoG5)c za@$6VKU79SFd@vN*cq{2I&t4oC_9TwOl&V#DLr2k%8zT}IfSd;zdT`Ubn@*th&XEy z+sLU|_|mL}$@Gr2COq|EVt9}S206Zs=4}Y*TY&nIgu@Vl^?4zX<4TMKS{4Z4e5kE; zooM|5LiRx5$~h#utvU>u%22K=2{>>mAUtcfSaq7fMytV}G1*k~^pzkR4#34OfPh*` zE#wfP=HOQ;HRDK|aE_aVcBco}B|Q`bM@NtD!w41@PHK&{`fmm%JcE`{eA|b5eyKp% zFW$K$w$~jDIUGI`WkLQE2n`0hy0<}89~fK`YvV69@W+NOkL6r9)BTj9?GTD3T=1Qw$%w69 zhF>nmQcOrFEXgFyQeo+Za0Hj`DcLTw3Vjc!!RJw-GQC$EZ-HUb+kLLxp&?*@BrtKC z5mmv_+H#=#?$zOWCs6iO(*lj5^VE_z)u%yx2n1~*A5C?pCC^qj?OWMh9YTS#LTQ*^ z6m(e>t+K5VNtQ?X@&mk%>OHdgIznO^g?9b%CBVc^8P7rFQ-f(OQDfZwjQ(cA2oK%8 z=>!fE6trR#wLd~Dc=|LPscqGh5)(&8M>W*dh0dITT(KX&bR%BwO~ykYP;C8TxT<*{U@HT+-|Hrs$0(Y>_Q>3`6pJ7b0WIguoVf zmhf*tn+-^7Nqxz)?wE!e*jzz_dg9uZTkCf@6>%9JU5o`a&A}9{%Ki~aT`3l45f&8` z=8S&&Gz$a=CL*mXY>N)Rh-z(PTb1+Q>&>E1ZQjO7-!xzocC+KyaV6r*XnAb>-94e~ z%1y2notS1}U$>m=n0$c@n}C2ZVLVu$88k3uf!R0?-tu(8@I&)h_rQKi)Lo(s8r1W9 zr-a#$_+gITfb=|w^uP-90_x*rc`^Y?jz-gp0F@&anL`6^4P?ur6<3PD1$+o>3t zNEYy;Vk*~9^C5RcGioZB!*=nC>e7Y>z&K0 zZ@Rr5oYb6m6yXXHAfbq_dy~I`A5DNd{QKUBAI$ukVtZ*s?lX2-pp01=4;83-LdF;A zo_PND@B=+TN>+ow5ZUb~5TceNf%d?Q9Yz!RpJu2qWXCvB-6X-@jt}4I?eNp6l|O$Y zOV9Jjc~mccseeGHeCu{Ub*<|&dCu`gdyS$;YoR_P#KhP_xoaLv7D*I^SRUVUx1$H> zzO>z^B}$XG;dm%waFH3W+?1I9;F^L8%~j6Xa1FKt0u&k{b@G!wo4Lc{WE;Voz|NCo1 zFb*OAZU!X`(V-W277n7-@tB+Sxbzs_9fDbYCgE%gIRTy?+XV8y#X}_>sRx=@++e-e zmgS!hTKe%rv+4QVc#3LV2L9>O7<+?$G`@#!tyuMnA~p6v!~*a&y~V8C4|EvD9b)R) zs^dLy@!`h%r74Vd@$+XlBOMuot6yA>$0v(uZS#r@Jsm*Ml4)Y&_-kajFI-)1;kJ1; zj$&B-KrGZWoj0hq8V}&(Z(x;J8NC!bZC)%WSTxg_`T1L{osZfNiAS^_3_VcM$%z;1 ziWb|;9!UtA&3s!{j@z0CiG{&H%6I)PJt6G|5zne9-@C`}+!G=t$;2!SWte`}kM=(6 zQ|oCO4KJwE8qN3_Kcsy)z-0V>u$tAt+a0fGak}_cb~bSrQ?L8jo7L6%=&{VIohQs) z7&mE`T&8en*Jaxf_Vbhn+?KeLMOX?{A z?;>&8G?bKa`aWJ0V{z{?t(GQ_kGJuL@Wkx3#;)K+ZQNE)i#CQZbU*Jw+RZYRIH$Gz zq$E*>UQy4L3P`hN3%{xbna{OqYVk*hyM8sF?;5V{|HM2^fy`Z5q*A81>s$NwIP1$) z=K~j{oyPHb9j^xNv*$aOWO|V zOJ(u2mpfO9@!xCJ4U-;Cb+5_v{3=V1tSq8u&q}HfJdS*)aE`mL#1HaG}r8)NxtV{6HVPe`JaTU*FGX8g@r|;pQ@C^qWJ)B`4ETxH7Oca5PDmxB(TlJ33#S8C!&gype zh+0P%Ya#2oRWP%*c&2fxjkkfPIJ~;%9;6eMUQ|V&4YkZATeb&_a7D4_Znn81rZ7NkaBWD>;rAW=MbA_^`oubJ2Jw(`kjwg)3?%@n6s*mE3+!n zD;pyxFhj3u$aM3!E_!;trsXwL*O(Ud_UsvLC}$FUCEXE7m<9#K)@G|k3b%h{?9Sm& zSaj+9uhGMzv$aJ zTG{JYi{kdPCtpfo0%__!Y5pD;*PUs?*f%LqCi+k+l#&Uz`sJcxjBPk3HyG7YTFK0AgUhcD9@q#k9wy z^gCPMgv!?9`Xj|RopWa20t?G^CFk2p=pA*W9K%a4giA?FBl{8TU7#tsvt|9)^^+io z8kDYTY|Pdm-9epfI1%$fh6BW825O11FIEI338D81Voa-yQ>`NNXex~|plp$uGKnV$>o{zZi}Ded&U-+*?^A GzyAZH@;xd5 literal 0 HcmV?d00001 diff --git a/docs/images/attestation.puml b/docs/images/attestation.puml new file mode 100644 index 00000000..38c66374 --- /dev/null +++ b/docs/images/attestation.puml @@ -0,0 +1,28 @@ +@startuml +participant TPM as T +participant Client as C +participant Server as S +title Safeboot attestation protocol +activate C +C --> T: TPM2_Quote(AK, set-of-all-PCRs,\n\t\t timestamp = gettimeofday()) +activate T +T --> C: quote = \n Signed_AK({hash-of-PCRs,\n\t\t misc, timestamp}) +deactivate T +C -> S: HTTP POST w/ tarball as req-body:\n {EKpub, [EKcert], AKpub,\n PCRs, eventlog, timestamp,\n quote} +deactivate C +||| +activate S +S -> C: check that timestamp is recent;\ndata = Lookup(EKpub);\n\nif EKcert\n\tValidate(EKcert);\nelse if EKcert_required\n\tfail();\n\nvalidate(PCRs, eventlog);\nvalidate(quote);\n\nsession_key = genkey();\nstuff = Encrypt(session_key,\n\t\t\t tarball(data.secrets))\n\n/* Software, not TPM: */\n(credentialBlob, secret) = \n TPM2_MakeCredential(EKpub, AKpub,\n\t\t\t\t session_key);\nPOST response body:\n tarball(credentialBlob, secret, stuff) +note over S +TPM2_MakeCredential() is +a software operation +end note +deactivate S +activate C +C --> T: TPM2_ActivateCredential(AKhandle,\n\t\t\t\t\tEKhandle,\n\t\t\t\t\tcredentialBlob,\n\t\t\t\t\tsecret); +activate T +T --> C: certInfo +deactivate T +C -> C: session_key = certInfo;\nsecrets = Decrypt(session_key, stuff);\nvalidate_signatures(secrets); +deactivate C +@enduml diff --git a/docs/images/enrollment.png b/docs/images/enrollment.png new file mode 100644 index 0000000000000000000000000000000000000000..e226b39171a163d885205f98c968aa63ceda194a GIT binary patch literal 26865 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 QLI9zrq^ S: GET / +deactivate C +activate S +S -> C: +deactivate S +activate C +C -> S: POST /v1/add\n\treq-body: {EKpub, hostname} (as HTML form) +||| +deactivate C +activate S +S -> C: e = Lookup(EKpub)\ne2 = Lookup(hostname)\nif (e || e2) && e != e2\n\treturn 409; /* conflict */\n\n/* create and/or update enrolled entry */\nif !e\n\te = create(EKpub, hostname);\nif !e\n\treturn 503;\n\n/* create enrolled assets / add missing assets */\nif !add_missing_assets(e)\n\treturn 503;\nreturn 200; +deactivate S +@enduml diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 00000000..e62e580b --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,1115 @@ +# Safeboot.dev Enrollment, Attestation, and Proof-of-Possession Protocols + +This document describes the Safeboot.dev enrollment and attestation protocols. + +These protocols are based on the Trusted Computing Group's (TCG) Trusted +Platform Module (TPM), using a discrete TPM or a firmware TPM to secure +enrollment and delivery of secrets to enrolled devices. The use of a TPM helps +provide decent assurance of device state at certain times, provides us with a +way to bootstrap trust. + +The Safeboot.dev enrollment protocol creates long-term state for an enrolled +device, including secrets/credentials needed by the device. The Safeboot.dev +attestation protocol conveys enrolled state to devices that demonstrate being +in good state. + +> NOTE: The protocol is described as it will soon be. Specifically, the use of +> digital signatures for authentication of long-term enrolled assets is not yet +> integrated. See [Pull Request #140](https://github.com/osresearch/safeboot/pull/140). + +## Goals + + - protocol specification sufficient for + - security review + +## Background + +The security of the Safeboot.dev protocols depends critically on the use of +TPMs. Reviewers must be familiar with some of the relevant TPM concepts listed +below. + +Some useful links: + + - [Introduction to TPMs](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro) + - [Device Enrollment](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Enrollment) + - [What Attestation Is](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Attestation) + +### Critical Background + +It is essential that readers understand: + + - [`TPM2_MakeCredential()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_MakeCredential.md) + - [`TPM2_ActivateCredential()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_ActivateCredential.md) + - [Cryptographic object naming](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro#cryptographic-object-naming) + +Readers should also have a passing understanding of how authorization works in +a TPM 2.0 as well, especially when a TPM requires a caller to execute some +authorization policy, and which policy. + +Readers must be familiar with the `TPM2_MakeCredential()` / +`TPM2_ActivateCredential()` constructs. We describe these somewhat here. + +`TPM2_MakeCredential()` is an operation that amounts to encryption of a small +secret to a public key, but with a binding to the cryptographic name of an +"activation object". `TPM2_ActivateCredential()` decrypts such ciphertexts +provided that: + +1. the caller has access to the private key to whose public key + `TPM2_MakeCredential()` encrypted the payload, +2. and that the caller has access to the "activation object" named by the + caller of `TPM2_MakeCredential()`. + +As the cryptographic name of an object binds to it any authorization policies +associated with use of that object, the caller of `TPM2_ActivateCredential()` +must meet that policy. + +This means that the caller of `TPM2_MakeCredential()` can require specific +authorization and other attributes -via the activation object's name- that the +caller of `TPM2_ActivateCredential()` must satisfy. + +For example, a device that will call `TPM2_ActivateCredential()` can supply the +activation object's public key to the peer that will call +`TPM2_MakeCredential()`, then that peer can combine the activation object's +public key with the attributes expected of the activation object to cause the +protocol to succeed IFF the possessor of the activation object created it with +those same attributes. + +> NOTE: The activation object's private key is not itself used for any +> cryptographic operations in `TPM2_ActivateCredential()`. Only the activation +> object's cryptographic name and its attributes are used. + +> NOTE: The `TPM2_MakeCredential()` function can be implemented entirely in +> software, as it requires no privileged access to any objects stored in any +> TPMs. + +> NOTE: It is essential to the security of the Safeboot.dev protocols that +> enrolled devices' TPMs be legitimate TPMs or virtual TPMs run by trusted +> agents. This is due to the protocol depending on the TPM to enforce +> authorization policies for certain functions that an untrusted implementation +> could forgo. + +### Other TPM Background + +Some less critical TPM background: + + - [Hash extension](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro#hash-extension) + - [Platform Configuration Registers (PCRs)](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro#platform-configuration-registers-pcrs) + - [Root of Trust Measurement (RTM)](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro#root-of-trust-measurements-rtm) + - [Authorization](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro#authentication-and-authorization) + - [Key hierarchies](https://github.com/tpm2dev/tpm.dev.tutorials/tree/master/Intro#key-hierarchies) + +## Terminology + +We will use a lot of terminology from the TCG universe. + +In particular we will speak of: + + - credential + + Depending on the context, "credential" will refer either to: + + a) a secret or private key and possibly some metadata which can be used to + access some remote resources -- for example, a PKIX certificate and private + key --, + + b) a small secret key -typically an AES key- encrypted with + [`TPM2_MakeCredential()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_MakeCredential.md). + + - credential activation + + The successful use of + [`TPM2_ActivateCredential()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_ActivateCredential.md) + to recover a small secret. + + - activation object + + A TPM entity whose cryptographic name is used in making a "credential" with + [`TPM2_MakeCredential()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_MakeCredential.md), + and a handle and authorization session to which will be provided by the + caller of + [`TPM2_ActivateCredential()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_ActivateCredential.md) + to recover the credential. + +## Use Cases: Secure Boot, Device Credential Provisioning + +The Safeboot.dev enrollment and attestation protocols support two use cases: + + - secure boot + - device credential provisioning + +These two uses differ only in whether the enrolled device is expected to +perform UEFI secure boot. + +In all cases Safeboot.dev enrollment is about creating sensitive +files/blobs/assets that are stored encrypted to the device's TPM and to escrow +agents. + +In all cases Safeboot.dev attestation is about delivering enrolled assets to +devices in trusted state. + +### Use Cases: Secure Boot + +This use-case involves the device encrypting local storage with a secret +long-term key obtained at boot time via the attestation protocol. + +### Use Cases: Device Credential Provisioning + +This use-case involves delivering to the device credentials such as: + + - private keys and PKIX certificates for their public keys (for, e.g., TLS and/or IPsec) + - Kerberos keys ("keytabs") + - OpenSSH host keys and certificates + - service account tokens of various kinds + +### Use Cases: Secure Boot and Device Credential Provisioning + +Naturally, both of these use cases can be combined. In fact, they are the same +use case, differing only in the nature of the material delivered to the client. + +### Use Cases Currently Out of Scope + +Other attestation protocols are meant to be used not just at boot time but very +often, and not so much for delivering device credentials to the client device +as for ascertaining the continued trusted state of the client device. Devices +that fail to attest successfully and often enough might, e.g., be locked out of +the network. + +Such protocols may depend on having some dynamic state on the server side. For +example, keeping track of the last time that a client attested, its TPM's +`resetCount` (to make sure it never goes backward, and to detect reboots), etc. + +Nothing about Safeboot.dev's protocols precludes the use of other attestation +protocols for purposes other than the use cases listed above. Nothing about +Safeboot.dev's current protocols precludes the addition to Safeboot.dev of +functionality similar to those other projects' attestation protocols'. + +# Threat Model + +The primary threats that the Safeboot.dev enrollment and attestation protocols +seek to protect against are: + + - theft of devices and/or their local storage + - passive attacks on the attestation protocol + - active attacks on the attestation protocol + - any attacks on the enrollment protocol + +## Assumptions + +We assume that: + + - enrollment and attestation servers are physically secure + - access to enrollment and attestation servers is secured + - the credentials held by enrollment and attestation servers are secure + - the enrollment database and any associated servers are secure + - read access to the enrollment database can be secured + - enrollment database protocols are secure + +## Threat Models Out of Scope + +The following threats are out of scope for this document: + + - any attacks on the attestation server (other than via attacks on the + attestation protocol) + - any attacks on the enrollment database + - any attacks on the enrollment server (other than via attacks on the + enrollment protocol) + - post-attestation attacks on devices + +# Architecture + +The Safeboot.dev architecture consists of two separate protocols: one for +enrollment, and one for attestation. + +The Safeboot.dev attestation protocol operates using state created at +enrollment time. + +Enrollment is the act of creating state binding a device's TPM and a name for +that device, as well as creating any secrets and/or metadata that that device +may repeatedly need in the environment it will be used in. + +The separation of enrollment and attestation is motivated by: + + - privilege separation considerations + + We'd like to isolate any issuer credentials to as few systems as possible, + while allowing the attestation service to be widely replicated. + + Because enrollment is a low-frequency event, while attestation a + high-frequency event, we can have fewer enrollment servers and more + attestation servers. Then we can isolate issuer credentials by placing them + only on enrollment servers. + + - database replication and write concurrency considerations + + Having state created and manipulated only at enrollment servers allows us to + replicate the enrollment database to attestation servers as a read-only + database. + + Together with the low frequency of enrollment events this frees us from + having to address concurrent database updates at this time, at the cost of + having primary/secondary enrollment server roles. + +> Any future evolution of Safeboot.dev towards more dynamic attestation state +> may well use separate databases for enrolled assets and attestation state. +> Our interest in privilege separation does not preclude such an evolution, as +> the separation between one database type and the other would tend to fulfill +> that interest. + +# Enrollment Protocol + +The enrollment protocol cosists of an HTTP API called over HTTPS: + + - `/v1/add` -- `POST` here to enroll, as described above + - `/v1/find` -- `GET` here to query the enrolled device database by `hostname` + - `/v1/query` -- `GET` here to query the enrolled device database by `EKhash` + - `/v1/delete` -- `POST` here to delete an enrolled device's database entry + +All of these end-points are 1 round trip, naturally (except where HTTP +authentication methods used require more round trips). + +The `/v1/add` and `/v1/delete` end-points expect an HTML form to be posted. + +![Image of Safeboot enrollment protocol sequence diagram](images/enrollment.png) + +The `/v1/find` end-point expects a single query parameter to be given: +`hostname`, with a hostname prefix. + +The `/v1/query` end-point expects a single query parameter to be given: +`ekpubhash`, with a hash of `EKpub` prefix. + +The `/v1/add` end-point takes two inputs from the client, delivered as an HTML +form over an HTTPS POST: + + - `hostname` -- the desired device name + - `ekpub` -- the device's TPM's endorsement public key (`EKpub`), either in + `TPM2B_PUBLIC` or `PEM` formats (either as a public key or as a + certificate) + +An `EK` certificate is preferred, as that can be validated by checking that its +issuer chains to a trusted TPM vendor root certification authority (CA). In +environments such as the Google compute cloud's Shielded VMs, there may not be +an `EK` certificate available, but instead an API may be available to validate +an `EKpub`. + +User authentication and authorization MAY be required if only certain users +should be allowed to enroll devices. The choice of HTTP authentication method +is not specified here (options include Negotiate, Bearer, OIDC, SCRAM, etc.). + +The enrollment server ensures that the creation of the binding of device name +and `EKpub` is made atomically. + +> NOTE: In a putative future where multiple enrollment servers can concurrently +> create these bindings, we may dispense with atomic bindings; instead a +> conflict resolution mechanism MAY be used to resolve conflicts. + +The enrollment server will also provision the device with any number of secrets +and metadata of various kinds that will be transported to the device during +attestation. These are stored encrypted at rest (more on this below). + +No request or response headers are used. No universal resource identifier +(URI) query-parts are used. The URI local-part need only denote that it is the +enrollment end-point. We are using the following URI local-parts: + +## Types of Secrets and Metadata Provisioned + +Various types of long-term secrets and metadata can be provisioned to an +enrolled device: + + - configuration + - early boot scripts + - symmetric keys (or passphrase) for local storage encryption + - private keys and PKIX certificates for them (client, server) for TLS, IPsec, etc. + - Kerberos keys ("keytab") + - service account tokens + - IPsec keys for manually keyed SAs + - etc. + +> IMPLEMENTATION NOTE: These are configurable as `genprog`s for the +> `sbin/attest-enroll` program. See its usage message. + +## Data-at-Rest Encryption + +All these secrets created by the `/v1/add` end-point are encrypted to the +device's TPM's `EKpub` and separately also encrypted to the public keys of +configured escrow agents for, e.g., break-glass recovery. + +For every secret asset the server generates a random AES-256 key. The +plaintext of the secret to be encrypted is then encrypted using the AES-256 key +in an authenticated encryption cipher mode. See +[Appendix-A](#Appendix-A-Symmetric-AEAD-Cipher-Mode-Confounded-AES-256-CBC-HMAC-SHA-256). + +The AES-256 key is then encrypted to the enrolled device's `EKpub` and to any +configured escrow agents' public keys. + +## Encryption to Escrow Agents + +Encryption to escrow agents is done using raw RSA public keys. + +## Encryption to Device `EKpub` + +All these secrets are encrypted to the device's TPM's `EKpub`, each with an +optional, configurable TPM authorization policy. Two mechanisms can be used +for encryption to a device's TPM: the "WK" and "TK" mechanisms. + +A TPM authorization policy is a TPM 2.0 enhanced authorization (EA) policy, and +will be enforced by the device's TPM when called to decrypt one of these +secrets. + +The default policy for the `rootfs` key (a symmetric key for local storage +encryption) is that the platform configuration register (PCR) #11 must have the +initial value (all zeros), with the expecation that the attestation client will +immediately extend PCR #11 (with no particular value -- just some value) so +that the TPM will not again decrypt the same ciphertext unless the device +reboots. + +Policies are configurable for each secret type. + +> NOTE: We could use well-known PCR#11 extension values for the purpose of +> creating specific time windows during the boot process during which different +> secrets could be decrypted. + +> NOTE: Both, the WK and TK methods offer equivalent functionality. We support +> both mainly for historical reasons. The WK method is simpler, but the TK +> method was implemented first. + +### Encryption to TPM `EKpub`: WK Method + +> NOTE: Readers are expected to understand the `TPM2_MakeCredential()` and +> `TPM2_ActivateCredential()` functions. See the [Critical Background +> section](#Critical-Background). + +1. A well-known public key (`WK`) is loaded into a software TPM using + `TPM2_LoadExternal()` with the desired policy's `policyDigest`. + +2. `TPM2_MakeCredential()` is called with these input parameters: + - the `WKpub` (the loaded WK) as the `objectName` input parameter, + - the device's `EKpub` as the `handle` input parameter, + - and the AES-256 symmetric key as the `credential` input parameter (the + plaintext). + +The outputs of `TPM2_MakeCredential()` (`credentialBlob` and `secret`) make up +the ciphertext of the AES-256 key encrypted to the TPM's `EKpub`. + +The details of what `TPM2_MakeCredential()` does are described in the [TCG TPM +2.0 Library part 1: Architecture, section 24 (Credential +Protection)](https://trustedcomputinggroup.org/wp-content/uploads/TCG_TPM2_r1p59_Part1_Architecture_pub.pdf). + +Decryption is done by calling `TPM2_ActivateCredential()` on the TPM that has +the `EK` corresponding to the `EKpub`. Critically, the TPM will refuse to +"activate" the credential (i.e., decrypt the ciphertext) unless the caller has +satisfied the WK's `authPolicy` (if set). + +To decrypt, access to the TPM identified by the `EKpub` is needed. The process +is as follows: + + - call `TPM2_LoadExternal()` the well-known key, with the desired + `authPolicy`, if any + - call `TPM2_StartAuthSession()` to create a policy session for the `EK` + - call `TPM2_PolicySecret()` to obtain access to the `EK` + - call `TPM2_StartAuthSession()` to create a policy session for the `WK` (if + the `WK` had a `policyDigest` set) + - call the policy commands on the `WK` session handle to satisfy its policy + (if one was set) + - call `TPM2_ActivateCredential()` with the loaded `WK` as the + `activateHandle` and its corresponding policy session, the `EK` as the + `keyHandle` and its corresponding policy session, and the ciphertext + (`credentialBlob` and `secret`) as input parameters + +The `WK`'s authorization policy, if set, is enforced by +`TPM2_ActivateCredential()`. + +Then, once the AES-256 key is decrypted, the confounded AES-256-CBC-HMAC-SHA256 +ciphertext is decrypted as described above. + +### Encryption to TPM `EKpub`: TK Method + +1. create an RSA key-pair in software +2. encrypt the AES-256 key to the RSA public key using OEAP with any software +3. use a software TPM to encrypt the RSA private key from (1) to the `EKpub` of + the target TPM using `TPM2_Duplicate()`, setting the desired policy's + `policyDigest` as the intended `authPolicy` of the RSA key as it will be + when loaded by the target TPM +4. the ciphertext then consists of a) the ciphertext from encryption to the RSA + public key, b) the outputs of `TPM2_Duplicate()` + +To decrypt, access to the TPM identified by the `EKpub` is needed. The process +is as follows: + + - call `TPM2_StartAuthSession()` to create a policy session for the `EK` + - call `TPM2_PolicySecret()` to obtain access to the `EK` + - call `TPM2_Import()` and `TPM2_Load()` to import and load the output of + `TPM2_Duplicate()` + - call `TPM2_StartAuthSession()` to create a policy session for the `TK` (if + the `TK` had a `policyDigest` set) + - call the policy commands on the `WK` session handle to satisfy its policy + (if one was set) + - call `TPM2_RSA_Decrypt()` to decrypt the AES-256 key with the imported `TK` + +The `TK`'s authorization policy, if set, is enforced by `TPM2_RSA_Decrypt()`. + +Then, once the AES-256 key is decrypted, the confounded AES-256-CBC-HMAC-SHA256 +ciphertext is decrypted as described above. + +## Break-Glass Recovery + +Break-glass recovery consists of: + +1. replacing a device's TPM or the device itself (including its TPM), +2. decrypting the secret AES-256 keys stored in the enrollment DB using an + escrow agent, +3. encrypting those to the new TPM's `EKpub`, +4. and replacing the corresponding ciphertexts in the enrolled device's entry + in the enrollment DB. + +Any break-glass recovery operations must be performed only by authorized users. + +# Attestation Protocol + +The Safeboot.dev attestation protocol is a single round trip protocol that +allows a device to obtain its enrolled assets from the attestation server in +exchange for successfully attesting to the device's state. + +![Image of Safeboot attestation protocol sequence diagram](images/attestation.png) + +State that can be attested: + + - recency -- via a timestamp + - that the caller has access to the `EK` + - the values of PCRs, which reflect the firmware ROMs and operating system + loaded + - the TPM's `resetCount` (count of reboots) + - anything that can be required by a TPM policy + +To attest its state, a client device first generates an "attestation key" +(`AK`) -- an asymmetric signing keypair. This object must have the `stClear` +attribute set, which means that the TPM will refuse to reload or re-create this +`AK` if the TPM is reset (which happens when the host device reboots). It must +also have the `fixedTPM`, `fixedParent`, and `sign` attributes set. Then the +client creates a "quote" of all the PCRs, signed with the `AK`. See +[`TPM2_Quote()`](https://github.com/tpm2dev/tpm.dev.tutorials/blob/master/TPM-Commands/TPM2_Quote.md). + +The attestation protocol consists of an HTTP POST (HTTPS not required) with: + + - `/v1/attest` as the end-point + - no particular request headers + - no URI query-parameters + - no HTTP authentication needed + - the request body consisting of an uncompressed `tar` file containing the + following items: + + - `ek.crt` -- the `EKcert`, that is, the PKIX certificate for the TPM's + endorsment key (EK) as provisioned by the TPM's vendor (this is optional, + present only if the TPM has an `EKcert`) + + - `ek.pub` -- the `EKpub` in `TPM2B_PUBLIC` format + + - `ak.pub` -- the `TPM2B_PUBLIC` representation of the `AK` + + - `ak.ctx` -- the `AK` object, saved to help make it easier for the client + to keep state + + - `quote.out`, `quote.sig`, and `quote.pcr` -- the outputs of `TPM2_Quote()` + - using the `AK` + + - `nonce` -- not actually a nonce but a timestamp as seconds since the Unix + epoch + + - `eventlog` -- if possible, this is the TPM PCR eventlog kept by the UEFI + BIOS + + - `ima` -- if possible, this is the Linux IMA log + +The attestation server then: + + - looks up the device's enrollment DB entry by the given `EKpub` + - examines the `ak.pub` to ensure that it has the desired attributes + (specifically: + - `sign` + - `fixedTPM` + - `fixedParent` + - `stClear` + and recomputes the `AK`'s cryptographic name for later use as the activation + object name request parameter of `TPM2_MakeCredential()` + - verifies that the eventlog matches the PCRs + - verifies that the digests that appear in the eventlog are acceptable, or + that the PCRs match "golden PCRs" + - examines the `nonce` to verify that it is a recent timestamp + +If all the validation steps succeed, then the attestation server: + + - generates an ephemeral AES-256 session key, + - constructs a tarball of the device's long-term enrolled assets from the + device's enrollment database entry, + - encrypts that tarball in the session key, + - encrypts the session key to the device's TPM's `EKpub` using + `TPM2_MakeCredential()` with the `AKpub`'s cryptographic name as the + `objectName` and the `EKpub` as the `handle` + +In the successful case, then, the response body is a tarball consisting of: + + - `credential.bin` -- a file containing the `credentialBlob` and `secret` + output parameters of the `TPM2_MakeCredential()` call + - `cipher.bin` -- the ciphertext of a tarball of the device's enrollment DB + entry, encrypted with the AES-256 session key using confounded + AES-256-CBC-HMAC-SHA-256 as described above. + - `ak.ctx` (as provided by the client, sent back) + +The client can decrypt and recover the AES-256 session key IFF it has a TPM +with the corresponding `EK` and `AK` loaded. + +Having recovered the AES-256 session key, the client can decrypt the tarball of +the client's long-term secrets and metadata, where the secrets are encrypted to +the client's TPM using the WK or TK methods. The client can then decrypt the +secrets whose policies it can satisfy. + +The client is expected to immediately extend PCR #11 so that long-term secrets +whose policies expect PCR #11 to be in its initial state (all zeros) cannot +again be decrypted with the client's TPM without first rebooting. + +> Note that we use the server uses `TPM2_MakeCredential()` to construct the +> response, much like the "WK method" of encrypting secrets, with these +> differences: +> +> - the client's ephemeral `AKpub` is used to construct the `objectName` input +> parameter, +> +> (This means that if the client reboots it will not be able to decrypt this +> response with `TPM2_ActivateCredential()` because the `AK` had `stClear` +> set, which means it cannot be recovered if the TPM is reset.) +> +> - the `objectName` does not involve a `policyDigest` +> +> - the ciphertext is not a long-term stable ciphertext but one made with an +> ephemeral AES-256 session key. + +# Authentication of Enrolled Assets + +All enrolled assets are signed by a private key on the enrollment server at the +time that the assets are created. + +Attestation clients validate these signatures after successful attestation and +conveyance of enrolled assets to the attestation client. + +Signatures can be made with a bare key, or they can be made with a certified +key. In the former case the attestation client must know the public key to +validate the signatures with. In the latter case the attestation client must +know a PKIX trust anchor for validating the enrollment server's certificate and +certificate chain. + +# Proof-of-Possession Protocol + +TBD (not yet designed or implemented). + +Attestation clients cannot recover their secrets unless they are in the +attested state, or unless they ran untrusted code and locally saved their +enrolled assets from a previous attestation. The attestation server currently +receives no confirmation of that state after the fact, but knows that the +client can recover its secrets IFF its attestation is correct because the +client's TPM will enforce the binding between the client's `EK` and `AK`, and +any policies needed to decrypt the client's enrolled assets. + +> We are considering chaining instances of the attestation protocol where each +> instance proves activation of the preceding instance's credential. Thus the +> proof-of-posession (PoP) protocol would be the same as the attestation +> protocol. + +> We might also use attestation chaining in this way to implement continuous +> (frequent) attestation. We can then keep some mutable per-device state, +> mainly the `resetCount`, time of last good attestation, and a sequence number +> of the last good attestation. + +## Use Cases for PoP Protocols + + - logging and alerting + + - unlocking attested device access to a wider network + + - locking out of the network devices that fail to attest frequently + +# Enrollment Database + +> NOTE: Nothing here formally specifies a schema for this database. This +> content is supplied only to help reviewers. + +The enrollment server creates state that is shared with attestation servers. +Attestation servers need only read access to that state. We shall call that +state a "database". Many options exist for representing the enrolled device +database: + + - relational (e.g., any SQL server) + - any NoSQL + - a filesystem + - a Git repository (basically a filesystem) + +Each enrolled asset, with all its encryptions and signatures, can be a `BLOB` +value in a SQL table's column, or a file in a filesystem, or base64-encoded as +a field in a JSON/YAML/etc file, or any similar concept. + +These blobs must be named, since the tarball sent to the client requires names +for them. + +> NOTE: Currently the `sbin/attest-enroll` program uses the filesystem to +> access the enrollment DB. +> +> Configurable hooks allow a site to convert the filesystem representation to +> other representations. One upcoming use will be to use a `CHECKOUT` hook to +> fetch a client's current entry from the DB and a `COMMIT` hook to commit a +> client's new current entry in the DB, using a Git repository to encode the +> client's entry as left on the filesystem by `sbin/attest-enroll`. + +## Enrollment Database Contents + +Every enrolled device is identified by the SHA-256 digest of its `EKpub` (in +`TPM2B_PUBLIC` format). This is also the cryptographic name of the device's +TPM's `EKpub`. + +> NOTE: In our current implementation this digest is part of the path to the +> enrolled device's filesystem-based database entry: +> +> `$DBDIR/${ekhash:0:2}/${ekhash}/` + +Every enrolled device's enrolled state consists for the following named blobs. +The blobs' names denote the expected type of their contents. + +Blobs: + + - `manifest` + + (metadata) A textual (ASCII), newline-separated list of enrolled assets' + blob names. + + - `manifest.sig` + + (metadata) A digital signature of the manifest. + + - `ek.pub` + + The enrolled device's `EKpub`, in `TPM2B_PUBLIC` format. + + - `hostname` + + (metadata) The enrolled device's fully-qualified hostname. + + - For each non-secret metadata: + + - `${name}` + + A blob containing some metadata of type identified by its name. + + - `${name}.sig` + + This is a digital signature the contents of the `${name}` blob. + + - For each type of secret: + + - `${secret_name}.enc` + + This is the secret itself, symmetrically encrypted in confounded + AES-256-CBC-HMAC-SHA-256, with a unique symmetric key (see item below). + + - `${secret_name}.enc.sig` + + This is a signature of the ciphertext of the symmetrically encrypted + secret. + + - `${secret_name}.symkeyenc` + + This is the AES-256 key used to encrypt the the previous item, itself + encrypted to the device's TPM's `EKpub`. (In this case using the "WK" + method.) + + - `${secret_name}.policy` + + Identifies (as a `policyDigest` value, hex-encoded in ASCII) or defines a + policy used to encrypt the previous item (`${secret_name}.symkeyenc`). + + > NOTE: A policy definition language will be documented in an appendix + > later. + + - `escrow-${escrow_agent_names[0]}.symkeyenc` + - `escrow-${escrow_agent_names[1]}.symkeyenc` + - .. + - `escrow-${escrow_agent_names[$n]}.symkeyenc` + + These are the `${secret_name}.symkey`, each encrypted to the + corresponding escrow agents, if any such are defined. + +The names and types of secrets need not be specified here, but currently we +have support for the following: + + - `rootfs.key` (a symmetric key(s) for local storage encryption) + - `cert-priv.pem` (a private key to a public key digital signature + cryptosystem) + - `keytab` (a file containing one or more Kerberos "key table" entries with + keys for the device's `host` service principals) + +The names and types of non-secret data blobs need not be specified here, but +currently we have support for the following: + +Metadata types: + + - `anchor.pem` (a trust anchor for the enrollment server's signing key) + - `signer.pem` (the enrollment server's public signing key) + - `chain.pem` (the enrollment server's signing key's PKIX certificate chain) + - `hostname` (see above) + - `cert.pem` (a certificate for `cert-key.pem` naming `hostname`) + +Metadata files are also signed, thus if there is a `something` there will be a +`something.sig`. Except that `anchor.pem`, `signer.pem`, and `chain.pem` are +not signed, as there is no point to signing them. + +> NOTE: The `anchor.pem`, `signer.pem`, and `chain.pem` are sent back to the +> attestation client, if present in the client's enrolled device entry, but the +> attestation client is expected to know the anchor.pem` or `signer.pem` a +> priori. Including these allows for key rotation and trust-on-first-use +> (TOFU) semantics on the attestation client side. The Safeboot.dev +> attestation client does _not_ implement TOFU semantics. + +# Site-local Customization + +Things that may vary locally: + + - enrollment service base URI + + Naturally, different users of Safeboot.dev may have different enrollment + service URIs, which may even vary by datacenter, by rack, by client OS, etc. + + - attestation service URIs + + Ditto. + + - enrolled assets + + - enrolled device database and schema + + - `EKpub` validation + + - TPM vendor root CAs configured for `EKcert` validation + +# Implementation Considerations + +The entire client side of the Safeboot.dev attestation protocol is implemented +in Bash using native command-line tools to interact with the TPM and to perform +software cryptographic operations, such as tpm2-tools and OpenSSL. + +The reason for the client side being implemented mostly in Bash is that we +intend to use PXE booting, and we need the Linux initramfs image to be small. +Using Bash and standard command-line tools (typically coded in C) allows the +Linux initramfs image that must contain them to be small. In particular, using +Bash consumes much less space than any scripting language such as Python. + +Most of the server side of enrollment and attestation is also implemented in +Bash, with some parts in Python. + +> An alternative would be to code the entire stack in Rust. + +# Security Considerations + +As with all TPM-based attestation protocols, the security of the protocols +depends critically on the device's TPM being a legitimate, trusted TPM. A TPM +can be implemented in software, but then it can only be trusted if it is +implemented by a trusted implementor, _and_ run in a trusted hypervisor, _and_ +used by a guest of the hypervisor. Otherwise we expect the use of discrete, +hardware TPMs, or perhaps firmware TPMs in some cases. + +The attestation server response is not authenticated. This means that any +on-path attacker or any attacker that can redirect the client's communications +with the server, can impersonate an attestation server and feed the client +arbitrary secrets and metadata, but only if the attacker knows the client's +`EKpub`. Since the client always tells the server it's `EKpub`, any attacker +can impersonate the attestation server. + +Because all the enrolled assets are signed, all the enrolled assets are sent to +the client, and a manifest of them is signed and sent to the client, no +attacker can impersonate the attestation server without having access to the +client's enrolled assets. + +An attacker that can impersonate the attestation server can furnish the +enrolled assets to a client that is in an untrusted state. + +Therefore we consider the enrolled asset database to be read-sensitive. Only +enrollment servers and attestation servers should be able to read it. + +Separation of enrollment and attestation server roles is not required, but +enables privilege separation such that attestation servers need only read from +the database, while enrollment servers need only write (for `/v1/add` and +`/v1/delete`) and also read (for `/v1/query` and `/v1/find`). + +An attacker that can write to the enrollment database can also substitute its +own assets, but only if it can sign them as a legitimate enrollment server +would. + +Strict authorization of access to the enrollment server's signing credential is +REQUIRED. + +The enrollment server can implement TOFU enrollment or authenticated and +authorized enrollment. In the case of TOFU enrollment, binding of device +`EKpub` and device name must be atomic. In the case of authenticated and +authorized enrollment, the enrollment server MUST authenticate the user +enrolling a device, and it MUST check if the user is authorized to do so (and +possibly it must check if the user is authorized to create devices with names +like the proposed name). + +We use a single round trip attestation protocol because, if the enrolled device +`EKpub` is really for a TPM (and this MUST have been validated), then the +semantics of `TPM2_ActivateCredential()` and the `AKpub` attribute validation +done by the attestation server, together serve to provide us with all the +guarantees we need that the PCR quote was legitimate. + +A proof-of-possession protocol is strictly optional, but it can help provide +alerting. + +## Analysis + +An attacker may not impersonate an attestation server without having read +access to the database of enrolled assets. If we add use of +`TPM2_PolicySigned()` then an attacker may not impersonate an attestation +server without having read access to the database of enrolled assets _and_ +having access to the attestation server's signing credential. + +Digital signatures on the manifest of enrolled assets prevent attackers able to +impersonate attestation servers from being able to add or remove enrolled +assets. + +Digital signatures on enrolled assets prevent attackers able to +impersonate attestation servers from being able to modify enrolled assets. + +Use of HTTPS (TLS) prevents impersonation of enrollment servers. + +Attestation of trusted state (PCRs) coupled with tight read access controls on +the enrollment database prevent attackers who gain control of an attestation +client from recovering the client's enrolled long-term secrets' plaintext: the +attacker would have to compromise the client in such a way that the quoted PCRs +do not reveal the fact of the client's compromise to the attestation service. + +However, it is essential that the attestation client have a locally configured +trust anchor for validating the digital signatures on its enrolled assets. + +Replays of attestation client requests will be rejected if the `nonce` +(really, timestamp) is too old. Otherwise they will be accepted, but attacker +gets nothing from the response unless they have access to the client's TPM's +`EKpub` _and_ the `AK` that was used by the client. + +Replays of previous attestation service responses will not be accepted by the +client since they will be bound to attestation keys no longer available on the +client's TPM (because each `AK` used in attestation has the `stClear` +attribute, so it will not be usable across reboots). + +If an attestation client performs attestation multiple times between reboots, +then earlier responses can be replayed if the client depends on the server +returning the client's `ak.ctx` file to it. However, since the contents +returned to the client are static, there is no value to this replay attack. + +Alterations of attestation service responses by MITMs will be detected due to +the use of authenticated symmetric encryption (via confounded +AES-256-CBC-HMAC-SHA-256). + +Attackers who do not have access to an attestation client's TPM's `EK` cannot +decrypt the attestation response. + +Impersonation of attestation services by attackers who can read the attestation +database will be detected IFF the attacker removes, replaces, or adds enrolled +assets and the attacker has not compromised the enrollment server's digital +signing key or its PKI. + +A client that saves its enrolled assets in local storage can skip attestation +going forward. As the intent is that clients attest at boot time, this is a +problem. One can deal with this problem by ensuring that clients run only +trusted code that wouldn't do that. + +> NOTE: It may be desirable to develop an attestation protocol for frequent +> attestation. Such a protocol wouldn't deliver enrolled assets to the client, +> ensuring only that the client continues to be in a trusted state. Such a +> protocol is out of scope for this document at this time. + +## Possible Improvements + + - @osresearch proposes that we can use a policy to make sure that enrolled + assets delivered to an attestation client cannot be decrypted unless the + client attested to trusted state. + + We could do this using `TPM2_PolicySigned()` with a public key whose private + key the attestation server possesses. + + The attestation client would then not be able to decrypt any of its enrolled + encrypted assets without first getting a signature from the attestation + server, which signature the attestation server would not provide unless it + were happy with the client's attested state. The signature would be part of + the response payload wrapper in a `TPM2_MakeCredential()` bound to the + client's `AK` that has `stClear`. + + A key benefit of this approach is that the enrolled assets database would no + longer be read-sensitive. + + - We can get a stronger guarantee that the client's attested state is not + spoofed by attaching a policy to the client's `AK` that binds the attested + state, though this is racy (since the attested state can change during the + attestation process, though in early boot it wouldn't). A client could + avoid the race by starting and satisfying a corresponding policy session + before engaging in the attestation protocol. + + With the current protocol the binding of attested state to the client's + ability to "activate" the attestation server's response is only this: that + the client must have access to the `AK` it used and that that `AK` must have + `fixedTPM | fixedParent | stClear | sign` as its attributes. + + Associating a policy with the `AK` that uses `TPM2_PolicyPCR()` and + `TPM2_PolicyCounterTimer()` to bind all the attested state would leave just + one item unbound: the `nonce` (really, timestamp). We could use the `nonce` + as a password to satisfy an `authValue`, using `TPM2_PolicySecret()`. Thus + we could get a much stronger binding of the attested state to the client's + ability to activat the attestation response. + + That said, the current, weaker, binding that we have in the protocol seems + sufficient for our current purposes. + + - Consider having attestation update some state on the server side for + detection of `resetCount` going backwards (replay detection), and replay + detection more generally when we start performing proof-of-possession and + frequent re-attestation. + + Because we prize availability, we may use an eventually-consistent method of + sharing mutable attestation state on the server side. + +## Securing Communications with TPMs + +Depending on the threat model it is essential to use encryption sessions to +encrypt sensitive command/response parameters, and to authenticate all commands +and responses. Authentication of communications with a TPM depends on the +application knowing the TPM's `EKpub`. + +Sadly, it is not commonly the case that a computer's BIOS knows the computer's +TPM's `EKpub` from factory. As a result, it is possible for invasive, physical +MITM attacks on TPMs. Once a device's TPM's `EKpub` is enrolled, any MITM has +to be in the middle every time the attestation client runs. Therefore the MITM +has to have been in the middle from the moment the device is enrolled. + +## Configuring Trust Anchors + +Attestation clients configured to use secure boot can find a locally configured +trust anchor on local storage after successful decryption with a `rootfs` key. +The `rootfs` key can be decrypted without authenticating its signature because +if it can decrypt the local filesystem then the `rootfs` key must not have been +altered by any attacker. + +Attestation clients that are not configured to use secure boot can find a +locally configured trust anchor on local storage, or in a TPM "non-volatile (NV) +index" (in the case of a TPM NV index, probably only a hash of the trust anchor +would be stored in the NV index). + +In any case, authentication key rotation would be difficult. Indirection via +intermediate keys (PKI-style) would help. + +## `EKpub` Validation + +We rely utterly on TPMs enforcing extended policies. This means that we must +know that some `EKpub` is indeed a TPM's `EKpub`. + +### External `EKpub` Validation (Google Compute Environment) + +In the Google Compute Environment the Google Shielded VM product allows us to +lookup a device by name and obtain its `EKpub` in `PEM` format. + +If authorized users of the enrollment service can be trusted to fetch the +`EKpub` from the Google Shielded VM API, then the enrollment server need not +validate the `EKpub` at all -- the attestation server can just trust the given +`EKpub`. + +### `EKpub` Validation using `EK` Certificates + +When enrolling bare-metal hardware, as opposed to Google Shielded VMs, we must +either extract the to-be-enrolled device's TPM's `EKpub` manually, and once +more trust and allow only authorized users of the enrollment service to enroll +those, or we must extract the to-be-enrolled device's TPM's `EKcert` and enroll +that so that the enrollment server may validate the client's `EKcert` is issued +by a trusted TPM vendor. + +> XXX We have yet to implement this. + +## Alternatives to `EKpub`s + +Any primary key object with the `fixedTPM`, `fixedParent`, and `decrypt` +attributes set is suitable as an substitute for the `EKpub` provided that the +process of calling `TPM2_CreatePrimary()` and reading its public key is secured +by using the `EKpub`, and that the legitimacy of the TPM is established. +Wherever we refer to an `EKpub` in this document, one may substitute such an +alternative key. + +# Appendix A: Symmetric AEAD Cipher Mode: Confounded AES-256-CBC-HMAC-SHA-256 + +For bulk encryption we use AES-256 with an authenticated encryption with +additional data (AEAD) cipher mode. + +Given our implementation constraints we ended up using a cipher mode based on +the well-understood Kerberos cryptosystem specified in RFCs +[3962](https://datatracker.ietf.org/doc/html/rfc3962) and +[8009](https://datatracker.ietf.org/doc/html/rfc8009). Kerberos uses AES with +the CipherText Stealing (CTS) cipher mode, confounded, and with an HMAC with a +SHA family digest. CTS is a variation of Cipher Block Chaining mode (CBC). + +The differences between our confounded AES-256-CBC-HMAC-SHA-256 and the Kerberos +cipher modes are: + + - we use CBC instead of CTS + - we use SHA-256 with AES-256 + - we don't truncate the HMAC + +> NOTE: Well, we could use SHA-384 with AES-256 for the HMAC, but then again, +> we're not truncating the HMAC. + +> NOTE: "Confounding" consists of prepending to the plaintext a cipherblock's +> worth (16 bytes) of randomly generated bits. This causes the ciphertext +> resulting from the encryption of the "confounder" to function as the actual, +> non-zero IV for the plaintext. Confounded CBC is indistinguishable from CBC +> with explicit IV, except that it costs one more cipher block operation, so it +> is slightly slower. + +> NOTE: CTS is a variation of CBC that does not require padding. It does not +> work for plaintexts shorter than a cipher block (16 bytes), but since +> confounding means prefixing a cipher block's worth of nonce to the plaintext, +> confounded CTS always expands the plaintext by just one cipher block's worth, +> and does not require padding. Therefore a plaintext that is 30 bytes will +> yield a ciphertext that is 46 bytes (+ 32 more bytes for the HMAC), a 31 byte +> plaintext will yield a 47 byte ciphertext, etc. Replacing CTS with CBC does +> not enable any further cryptanalysis since, after all, CTS applied to +> plaintexts of length divisible by the cipher's block size is equivalent to +> CBC. + +> NOTE: When we switch to using OpenSSL 3.0 we will be able to use CTS instead +> of CBC. + +> NOTE: The primary reason for using this construction is that it is easily +> implemented in Bash with OpenSSL 1.x tooling, and OpenSSL 1.x tooling does +> not provide authenticated encryption constructions in its command-line tools +> that are suitable for encrypting data at rest. + +To encrypt a secret the enrollment server: + +1. creates a random AES-256 key +2. uses confounded AES-256-CBC-HMAC-SHA-256: + a. uses AES-256 in cipher block chaining (CBC) mode with + - all-zero IV + - confounding (a cipherblock's worth of entropy prepended to the plaintext) + - padding + b. appends an HMAC-SHA-256 digest of the resulting ciphertext + +The padding is per-OpenSSL (if the plaintext is a whole multiple of 16 bytes +then 16 bytes of zeros are added, else as many bytes are appended to bring the +plaintext size to a whole multiple of 16 bytes, with the last byte set to the +count of padding bytes). + +The resulting ciphertexts are stored as-is in the enrollment DB. + +The per-secret AES-256 keys are encrypted to the device's TPM's EKpub, and to +the escrow agents. + +Decryption of confounded AES-256-CBC-HMAC-SHA-256 ciphertexts is as follows: + + - compute the HMAC-SHA-256 MAC of the ciphertext (excluding the MAC in the + ciphertext) + - constant-time compare the computed MAC to the MAC in the ciphertext + - if these do not match, fail + - decrypt the ciphertext (excluding the MAC) with AES-256 in CBC mode + - discard the first block of the resulting plaintext (the confounder) + - examine the last byte of the plaintext and drop the indicated amount of + padding From 0e88fc19e330f20d98f9b89a644e77ff7fd1fe6c Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Wed, 28 Jul 2021 20:40:55 -0500 Subject: [PATCH 06/18] Sign enrolled assets / verify their signatures Thiis is needed for non-secure boot use cases where we're using attestation to bootstrap trust. --- functions.sh | 42 +++++++++- initramfs/bootscript | 5 +- sbin/attest-enroll | 190 +++++++++++++++++++++++++++++++++++-------- sbin/tpm2-attest | 80 +++++++++++++++++- tests/test-enroll.sh | 49 ++++++++++- 5 files changed, 325 insertions(+), 41 deletions(-) diff --git a/functions.sh b/functions.sh index c57ea475..dd8f04e6 100755 --- a/functions.sh +++ b/functions.sh @@ -6,7 +6,15 @@ export LC_ALL=C die_msg="" -die() { echo "${PROG:+${PROG}: }$die_msg""$*" >&2 ; exit 1 ; } +die() { + local e=1; + if [[ ${1:-} = +([0-9]) ]]; then + e=$1 + shift + fi + echo "${PROG:+${PROG}: }$die_msg""$*" >&2 + exit "$e" +} warn() { echo "$@" >&2 ; } error() { echo "$@" >&2 ; return 1 ; } info() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } @@ -580,3 +588,35 @@ aW2TLJkwxecRh2eTwPtSx2U32M2/yHeuWRV/0juiIozefPsTAlHAi3E= -----END PRIVATE KEY----- EOF } + +# verify_sig PUBKEY_FILE BODYHASH_FILE SIG_FILE +verify_sig() { + local pubkey="$1" + local body="$2" + local sig="$3" + + (($# >= 3 && $# <= 4)) + shift $# + + # Verify the signature using a raw public key, or a certificate + # shellcheck disable=2094 + openssl pkeyutl -verify \ + -pubin \ + -inkey "$pubkey" \ + -in <(sha256 < "$body" | hex2bin) \ + -sigfile "$sig" \ + || + openssl pkeyutl -verify \ + -certin \ + -inkey "$pubkey" \ + -in <(sha256 < "$body" | hex2bin) \ + -sigfile "$sig" \ + || + openssl dgst -verify "$pubkey" \ + -keyform pem \ + -sha256 \ + -signature "$sig" \ + "$body" \ + || + die "could not verify signature on $body with $pubkey" +} diff --git a/initramfs/bootscript b/initramfs/bootscript index 2d4ca1d3..2053fa2e 100755 --- a/initramfs/bootscript +++ b/initramfs/bootscript @@ -34,10 +34,13 @@ TMPDIR=$(mktemp -d) tar -xvf - -C "$TMPDIR" \ || die "attestation response is not a valid tar file?" +[[ -f /etc/safeboot/anchor.pem || -f /etc/safeboot/enroll-signer.pem ]] \ +&& /safeboot/sbin/tpm2-attest verify-unsealed "$TMPDIR" + STARTUP="$TMPDIR/startup.sh" KEY="$TMPDIR/rootfs.key" -if [[ -x $STARTUP ]]; then +if [[ -x $STARTUP && -f ${STARTUP}.sig ]]; then # measure the startup script before doing anything else warn "$STARTUP: measuring" tpm2 pcrevent 14 "$STARTUP" diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 3174d471..770bc7e5 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -39,7 +39,7 @@ ESCROW_PUBS_DIR= TRANSPORT_METHOD=WK DEFAULT_EK_POLICY= declare -a GENPROGS -GENPROGS=(genhostname genrootfskey) +GENPROGS=(genhostname genmetadata genrootfskey) declare -A POLICIES POLICIES=() @@ -55,6 +55,17 @@ vars[CHECKOUT]=scalar vars[COMMIT]=scalar vars[GENPROGS]=array vars[POLICIES]=assoc +vars[SIGNING_KEY_PRIV]=scalar # This should be a blah.priv for a TPM entity, + # in which case there should also be a + # blah.pub, or it should be a PEM file with a + # private key +vars[SIGNING_KEY_POLICY]=scalar +vars[SIGNING_KEY_PUB]=scalar # This should be either just the public key + # or a certificate for it and a chain. MUST be + # PEM. +vars[SIGNING_KEY_ANCHOR]=scalar # MUST be PEM. +vars[TPM_VENDORS]=scalar +vars[VALIDATE]=scalar configs() { local var type @@ -74,15 +85,9 @@ configs() { die() { echo >&2 "Error: $PROG" "$@" ; exit 1 ; } warn() { echo >&2 "$@" ; } -if [[ -n ${TPM2TOOLS_TCTI:-} ]]; then - # This is used via declare -n: - # shellcheck disable=SC2034 - known_policy_pcr11=$("$BASEDIR/tpm2-policy" -A tpm2 policypcr --pcr-list=sha256:11) -else - # This is used via declare -n: - # shellcheck disable=SC2034 - known_policy_pcr11=7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 -fi +# This is used via declare -n: +# shellcheck disable=SC2034 +known_policy_pcr11=7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 genhostname() { if [[ -s $outdir/hostname ]]; then @@ -102,6 +107,25 @@ genhostname() { echo "public hostname" } +genmetadata() +{ + if [[ -s $outdir/meta-data ]]; then + echo "skip" + return 0 + fi + echo '{"instance-id": "iid-local01"}' > "${1}/meta-data" + cat > "${1}/user-data" < "${1}/manifest" + if [[ -s ${outdir}/manifest ]] \ + && cmp "${outdir}/manifest" "${1}/manifest" >/dev/null; then + echo "skip" + else + echo "public manifest" + fi +} + gentest0() { if [[ -s ${outdir}/test0.enc ]]; then echo "skip" @@ -177,6 +211,15 @@ Usage: $PROG [OPTIONS] HOSTNAME [DIR] < EKPUB credentials and/or metadata will be added. This makes enrollment idempotent. + EKPUB must be one of + + - endorsement key certificate (EKcert) in PEM or DER form + - endorsement key public key in {TPM2B_PUBLIC} form + - endorsement key public key in PEM form + + If an EKcert is given, it will be validated. Otherwise the user is + expected to have validated that the {EKPUB} is for a legitimate dTPM. + If {-a} is given, the {HOSTNAME} will be added as a secondary hostname for {EKPUB} if already enrolled. @@ -300,6 +343,7 @@ C) # Read given configuration CONF='';; I) EKPUB=$OPTARG;; V) if ! $configured && [[ -f $SAFEBOOT_ENROLL_CONF ]]; then + # shellcheck disable=SC1090 . "$SAFEBOOT_ENROLL_CONF" configured=true fi @@ -490,6 +534,47 @@ encrypt() { shift } +# Sign an enrolled host asset +sign() { + [[ -z ${SIGNING_KEY_PRIV:-} ]] \ + && die "SIGNING_KEY_PRIV not configured!" + [[ -z ${SIGNING_KEY_PUB:-} ]] \ + && die "SIGNING_KEY_PUB not configured!" + + # Sign using a TPM key if we can load it as a TPM key + if [[ $SIGNING_KEY_PRIV = *.priv ]] \ + && tpm2 flushcontext --transient-object 2>/dev/null \ + && tpm2 createprimary --hierarchy o \ + --key-context "${tmp}/primary.ctx" \ + && tpm2 load --private "$SIGNING_KEY_PRIV" \ + --public "${SIGNING_KEY_PRIV%.priv}.pub" \ + --parent-context "${tmp}/primary.ctx" \ + --key-context "${tmp}/signing-key.ctx"; then + tpm2 flushcontext --transient-object + tpm2 flushcontext --loaded-session + if [[ -n ${SIGNING_KEY_POLICY:-} ]]; then + # Execute the policy + "${SIGNING_KEY_POLICY}" -e "${tmp}/s.ctx" + fi + tpm2 sign --key-context "${tmp}/signing-key.ctx" \ + ${SIGNING_KEY_POLICY:+--auth} \ + ${SIGNING_KEY_POLICY:+session:"${tmp}"/s.ctx} \ + --scheme rsassa \ + --hash-algorithm sha256 \ + --format plain \ + --signature "${1}.sig" \ + < "$1" + return 0 + fi + + # Sign using OpenSSL + openssl pkeyutl -sign \ + -keyform PEM \ + -inkey "$SIGNING_KEY_PRIV" \ + -in <(sha256 < "$1" | hex2bin) \ + -out "${1}.sig" +} + cat "$EKPUB" > "$tmp/ekpub" \ || die "$0: unable to read EKpub from stdin" exec "$tmp/ek.pub" + if [[ -d $TPM_VENDORS ]]; then + CAarg=CApath + elif [[ -f $TPM_VENDORS ]]; then + CAarg=CAfile + else + die "TPM_VENDORS is set to a non-existent file/directory" + fi + openssl verify -$CAarg "$TPM_VENDORS" \ + -partial_chain \ + "$tmp/ekpub" \ + || die "EKcert is not trusted" + pem2tpm2bpublic "$tmp/ekpub" \ + "$tmp/ek.pub" \ + "${DEFAULT_EK_POLICY:-}" + elif grep -q PUBLIC "$tmp/ekpub"; then + # Plain public key + pem2tpm2bpublic "$tmp/ekpub" \ + "$tmp/ek.pub" \ + "${DEFAULT_EK_POLICY:-}" + else + die "Cannot understand given EKpub/EKcert" + fi;; application/octet-stream) + EKCERT= cp "$tmp/ekpub" "$tmp/ek.pub";; *) die "Given EKpub is not in a supported format: $(file -b --mime-type "$tmp/ekpub")";; @@ -553,7 +670,7 @@ if [[ -d $outdir ]] && $replace; then mv "$outdir" "${outdir}-" || die "could not rename previous enrollment" did_something=true elif [[ -d $outdir && -f ${outdir}/hostname && - $(cat ${outdir}/hostname) != "$hostname" ]]; then + $(cat "${outdir}/hostname") != "$hostname" ]]; then $add || die "already enrolled: $ekhash" @@ -568,11 +685,16 @@ else fi mkdir -p "$outdir" || die "unable to create output directory $outdir" -cp "$tmp/ek.pub" "$outdir/ek.pub" \ +cp "$EKPUB" "$outdir/ek.pub" \ || die "unable to copy EK public key to output directory $outdir" +if [[ -n $EKCERT ]]; then + cp "$EKCERT" "$outdir/ek.pub" \ + || die "unable to copy EK public key to output directory $outdir" +fi + info "Generating secrets and metadata" -for genprog in "${GENPROGS[@]}"; do +for genprog in "${GENPROGS[@]}" genmanifest; do info "Running GENPROG $genprog" # We want to split the output of genprog on spaces: @@ -596,6 +718,8 @@ for genprog in "${GENPROGS[@]}"; do # Encrypt file, escrow, and place into output dir. info "Encrypting secret file from $genprog: $1" encrypt "$genprog" "$1" + info "Signing ciphertext $1" + sign "${outdir}/${1}.enc" did_something=true shift fi @@ -609,30 +733,32 @@ for genprog in "${GENPROGS[@]}"; do info "Installing public file $1" fi cp -f "${tmp}/$1" "${outdir}/${1}" + info "Signing metadata file $1" + sign "${outdir}/$1" did_something=true shift done - done -# -# Build the cloud-init data for this host -# -if [[ ! -s $outdir/user-data ]]; then - cat > "$outdir/user-data" </dev/null; then + for sig in "{outdir}"/*.sig; do + sign "${sig%.sig}" + done did_something=true fi -if [[ ! $outdir/meta-data ]]; then - echo '{"instance-id": "iid-local01"}' > "$outdir/meta-data" +# Install the signing key public part (preferably it should be a certificate +# and chain) and anchor, if we have one +if [[ ! -f ${outdir}/${SIGNING_KEY_PUB##*/} ]] || + ! cmp "$SIGNING_KEY_PUB" "${outdir}/signer.pem" >/dev/null; then + cp -f "$SIGNING_KEY_PUB" "${outdir}/signer.pem" + did_something=true +fi +if [[ -n ${SIGNING_KEY_ANCHOR:-} ]] \ + && ! cmp "$SIGNING_KEY_ANCHOR" "${outdir}/anchor.pem" >/dev/null 2>&1; then + cp -f "$SIGNING_KEY_ANCHOR" "${outdir}/anchor.pem" did_something=true fi @@ -649,5 +775,5 @@ if $did_something; then info "$hostname: enrolled $ekhash" success=true else - die "Already enrolled and nothing to add to enrollment" + die 2 "Already enrolled and nothing to add to enrollment" fi diff --git a/sbin/tpm2-attest b/sbin/tpm2-attest index 7371adc8..3be8bc0a 100755 --- a/sbin/tpm2-attest +++ b/sbin/tpm2-attest @@ -16,10 +16,16 @@ [[ $_ != "$0" ]] || set -e -o pipefail export LC_ALL=C +# https://bosker.wordpress.com/2012/02/12/bash-scripters-beware-of-the-cdpath/ +unset CDPATH + +# Find the directory that contains functions.sh +TOP="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && cd .. && pwd )" + : "${PREFIX:=}" : "${DIR:=/etc/safeboot}" -. "$PREFIX$DIR/functions.sh" +. "$TOP/functions.sh" if [ -r "$PREFIX$DIR/safeboot.conf" ]; then . $PREFIX$DIR/safeboot.conf @@ -31,7 +37,15 @@ if [ -r "$PREFIX$DIR/local.conf" ]; then fi # Apply $PREFIX to files and use default value -CERT=$PREFIX${CERT:-$DIR/cert.pem} +[[ -n ${CERT:-} && ${CERT} != /* ]] \ +&& CERT=$PREFIX$DIR/$CERT +[[ -n ${ENROLL_SIGN_ANCHOR:-} && ${ENROLL_SIGN_ANCHOR} != /* ]] \ +&& ENROLL_SIGN_ANCHOR=$PREFIX$DIR/$ENROLL_SIGN_ANCHOR +[[ -z ${ENROLL_SIGN_ANCHOR:-} && -f $PREFIX$DIR/anchor.pem ]] \ +&& ENROLL_SIGN_ANCHOR=$PREFIX$DIR/anchor.pem +[[ -z ${ENROLL_SIGN_ANCHOR:-} && -f $PREFIX$DIR/enroll-signer.pem ]] \ +&& ENROLL_SIGN_ANCHOR=$PREFIX$DIR/enroll-signer.pem +: "${CERT:=$PREFIX$DIR/cert.pem}" : "${QUOTE_MAX_AGE:=30}" # RSA EK NVRAM handle @@ -320,6 +334,7 @@ attest() "$SERVER" \ || die "attestation failed" + # shellcheck disable=SC2119 unseal < "$TMP/cipher.tar" \ || die "unsealing failed" } @@ -751,6 +766,7 @@ if and only if the EK matches and the AK is one that it generated. usage+="$unseal_usage" commands+="|unseal" +# shellcheck disable=SC2120 unseal() { show_help "$1" "$unseal_usage" @@ -785,6 +801,64 @@ unseal() rm -f "$TMP/secret.key" } +######################################## + +verify_unsealed_usage=' +## verify_unsealed +Usage: +``` +tpm2-attest verify_unsealed DIR +``` + +Assets returned by successful remote attestation should be signed. This +command validates the signatures on those assets. +' +usage+="$verify_unsealed_usage" +commands+="|verify-unsealed" + +verify-unsealed() +{ + show_help "$1" "$verify_unsealed_usage" + (($# == 1)) || die "No arguments expected.$verify_unsealed_usage" + + # We must either know a priori an anchor for the signer's certificate, + # or we must know the signature key. + [[ -n ${ENROLL_SIGN_ANCHOR:-} ]] \ + || die "neither enrollment server public key certificate nor anchor configured" + + cd "$1" || die "Not a directory: $1" + + # Validate the signer's certificate + # shellcheck disable=SC2094 + if [[ -n ${ENROLL_SIGN_ANCHOR:-} ]] \ + && ! cmp "$ENROLL_SIGN_ANCHOR" signer.pem; then + openssl verify -CAfile "${ENROLL_SIGN_ANCHOR}" \ + -show_chain \ + -untrusted signer.pem \ + < signer.pem \ + || die 5 "Could not validate enrolled asset signing key certificate" + fi + + # Verify all the signatures + for i in *.sig; do + verify_sig signer.pem "${i%.sig}" "$i" + done + + # Make sure we also got a signed manifest (whose signature we've + # already validated) + [[ -s manifest ]] \ + || die 2 "missing manifest" + [[ -s manifest.sig ]] \ + || die 3 "missing manifest signature" + + # Verify the manifest + shopt -s extglob + if ! cmp <(sort < manifest) <(sha256sum !(manifest).sig | sort); then + diff -U15 <(sort < manifest) <(sha256sum !(manifest).sig) + die 4 "Manifest does not match signed artifacts" + fi + +} ######################################## @@ -1073,7 +1147,7 @@ else exit 0 ;; #$commands) - commands|quote|attest|verify-and-seal|verify|seal|unseal|ek-verify|quote-verify|eventlog-verify|ek-sign|ek-crt|eventlog) + commands|quote|attest|verify-and-seal|verify|seal|unseal|verify-unsealed|ek-verify|quote-verify|eventlog-verify|ek-sign|ek-crt|eventlog) $command "$@" ;; *) diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index 21c2402d..49321395 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -12,7 +12,7 @@ fi TOP=${TOP%/*} -# shellcheck disable=SC1091 +# shellcheck disable=SC1091 source=functions.sh . "$TOP/functions.sh" #PATH=$TOP/sbin:$TOP/swtpm/src/swtpm:$PATH @@ -54,8 +54,16 @@ GENPROGS+=(gentest0) ESCROW_PUBS_DIR=${d}/escrowpubs POLICIES[rootfskey]=pcr11 POLICIES[test0]=pcr11 +SIGNING_KEY_PUB=${d}/sign.pem EOF +if [[ -n ${TEST_ENROLL_USE_OPENSSL:-} ]]; then + echo "SIGNING_KEY_PRIV=${d}/sign-priv.pem" +else + echo "SIGNING_KEY_PRIV=${d}/sign.priv" +fi >> "${d}/attest-enroll.conf" + + policy_pcr11_unext=(tpm2 policypcr '--pcr-list=sha256:11') declare -A TCTIs @@ -124,12 +132,14 @@ make_client() { echo "Enrolling $1" ( (($# == 1)) || unset TPM2TOOLS_TCTI + TPM2TOOLS_TCTI="${TCTIs[_self_]}" \ attest-enroll -C "${d}/attest-enroll.conf" "$1" < "${d}/${1}/ek.pub" ) echo "Checking that PEM also works" - if attest-enroll -C "${d}/attest-enroll.conf" "$1" < "${d}/${1}/ek.pem"; then - die "Using PEM we got a different TPM2B_PUBLIC!" + if TPM2TOOLS_TCTI="${TCTIs[_self_]}" \ + attest-enroll -C "${d}/attest-enroll.conf" "$1" < "${d}/${1}/ek.pem"; then + warn "Using PEM we got a different TPM2B_PUBLIC!" fi ekpub=$(cat "${d}/db/hostname2ekpub/$1") @@ -173,8 +183,38 @@ echo "Starting an SWTPM for things that should be software-only (but aren't yet) start_swtpm _self_ export TPM2TOOLS_TCTI="${TCTIs[_self_]}" +if [[ -n ${TEST_ENROLL_USE_OPENSSL:-} ]]; then + echo "Generating a key for signing enrolled assets" + openssl genrsa -out "${d}/sign-priv.pem" \ + || die "unable to create asset signing private key" + openssl rsa \ + -pubout \ + -in "${d}/sign-priv.pem" \ + -out "${d}/sign.pem" +else + tpm2 createprimary --hierarchy o \ + --key-context "${d}/primary.ctx" + tpm2 create --parent-context "${d}/primary.ctx" \ + --key-context "${d}/sign.ctx" \ + --private "${d}/sign.priv" \ + --public "${d}/sign.pub" \ + --attributes 'sensitivedataorigin|userwithauth|sign' + tpm2 flushcontext --transient-object + tpm2 load --private "${d}/sign.priv" \ + --public "${d}/sign.pub" \ + --parent-context "${d}/primary.ctx" \ + --key-context "${d}/signing-key.ctx" + tpm2 flushcontext --transient-object + tpm2 print --type TPM2B_PUBLIC \ + --format pem "${d}/sign.pub" \ + > "${d}/sign.pem" +fi + +mkdir -p /etc/safeboot +cp "${d}/sign.pem" /etc/safeboot/enroll-signer.pem + make_escrow BreakGlass -make_client foo no-tpm +make_client foo make_client bar make_client baz for i in foo bar baz; do @@ -254,4 +294,5 @@ for i in foo bar baz; do # Note that only attest-verify needs access to the enrolled clients # attestation database (SAFEBOOT_DB_DIR). done + success=true From 4e53188a2f8164f32bfed23ae151c327ef1244ea Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Wed, 1 Sep 2021 22:52:47 -0500 Subject: [PATCH 07/18] Fix sbin/genkeytab --- sbin/genkeytab | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sbin/genkeytab b/sbin/genkeytab index 16b3890c..a9f3a0f2 100755 --- a/sbin/genkeytab +++ b/sbin/genkeytab @@ -109,7 +109,7 @@ HKD) --request POST \ --data-binary @/dev/null \ -D headers \ - "${GENKEYTAB_HKD_URI}?spn=host/$hn&create=true" + "${GENKEYTAB_HKD_URI}?spn=host/$hostname&create=true" csrf_token=$(sed -e 's/\r//' headers | grep ^X-CSRF-Token:) rm -f headers junk curl \ @@ -119,7 +119,7 @@ HKD) --data-binary @/dev/null \ -D headers \ -H "$csrf_token" \ - "${GENKEYTAB_HKD_URI}?spn=host/$hn&create=true" + "${GENKEYTAB_HKD_URI}?spn=host/$hostname&create=true" grep '^HTTP/1.1 200' headers >/dev/null \ || die "Could not create host principal" rm headers @@ -148,7 +148,7 @@ HKD-IMPERSONATE) --request POST \ --data-binary @/dev/null \ -D headers \ - "${GENKEYTAB_HKD_URI}?spn=host/$hn&create=true" + "${GENKEYTAB_HKD_URI}?spn=host/$hostname&create=true" csrf_token=$(sed -e 's/\r//' headers | grep ^X-CSRF-Token:) rm -f headers junk curl \ @@ -157,7 +157,7 @@ HKD-IMPERSONATE) --data-binary @/dev/null \ -D headers \ -H "$csrf_token" \ - "${GENKEYTAB_HKD_URI}?spn=host/$hn&create=true" + "${GENKEYTAB_HKD_URI}?spn=host/$hostname&create=true" grep '^HTTP/1.1 200' headers >/dev/null \ || die "Could not create host principal" rm headers From 0a241f09f3505d7be4a2017d0258eb31b1893407 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Wed, 1 Sep 2021 22:52:25 -0500 Subject: [PATCH 08/18] Add sbin/getkeytab utility --- sbin/getkeytab | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100755 sbin/getkeytab diff --git a/sbin/getkeytab b/sbin/getkeytab new file mode 100755 index 00000000..63748d90 --- /dev/null +++ b/sbin/getkeytab @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Get a keytab using a certificate + +set -euo pipefail +shopt -s extglob +umask 077 + +PROG=${0##*/} +BASEDIR=$(dirname "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )") + +declare -A GETCERT_DOMAIN_REALM +KEYTAB=FILE:/etc/krb5.keytab +GETKEYTAB_HKD_URI= +GETKEYTAB_REALM= +CERT_KEY= +CERT= + +curl_opts=( + --silent + --globoff + --user : + --negotiate +) + +: "${PREFIX:=}" +: "${DIR:=/etc/safeboot}" +SAFEBOOT_CONF=${PREFIX}${DIR}/safeboot.conf +# shellcheck disable=SC1090 +[[ -n $SAFEBOOT_CONF && -f $SAFEBOOT_CONF ]] \ +&& . "${SAFEBOOT_CONF}" + +# shellcheck disable=SC1090 +. "$BASEDIR/../functions.sh" + +: "${CERT_KEY:=${PREFIX}${DIR}/cert-key.pem}" +: "${CERT:=${PREFIX}${DIR}/cert.pem}" + +[[ -f $CERT_KEY && -f $CERT ]] \ +|| die "Could not get PKINIT certificate for impersonation" + +hostname=$(hostname) + +if [[ -z $GETKEYTAB_REALM ]]; then + domain=${hostname} + while [[ $domain = *.*.* ]]; do + domain=${domain#*.} + if [[ -n ${GETCERT_DOMAIN_REALM[$domain]:-} ]]; then + GETKEYTAB_REALM=${GETCERT_DOMAIN_REALM[$domain]} + break + fi + if ((${#GETCERT_DOMAIN_REALM[@]} > 0)); then + die "Could not determine domain name for $hostname" + fi + if (($(dig -t srv "_kerberos._udp.$domain" +short|wc -l) > 0)); then + GETKEYTAB_REALM=${domain^^?} + break + fi + done + [[ -n $GETKEYTAB_REALM ]] \ + || die "Could not determine realm name for $hostname" +fi + +check_keytab() { + [[ -n ${1:-} && -s $1 ]] \ + && ktutil -k "$1" list >/dev/null \ + && kinit --anonymous "$GETCERT_DOMAIN_REALM" \ + gss-token "host@$hostname" \ + | KRB5_KTNAME="$1" gss-token -r +} + +if check_keytab "$KEYTAB"; then + warn "Already have a valid keytab" + exit 0 +fi + +d= +trap 'cd /; rm -rf "$d";' EXIT +d=$(mktemp -d) +cd "$d" + +# Get a TGT using PKINIT +kinit \ + ${GETKEYTAB_KINIT_ARGS[0]:+"${GETKEYTAB_KINIT_ARGS[@]}"} \ + --cache cc \ + --pk-user "FILE:${CERT},${CERT_KEY}" \ + "host/$hostname@$GETKEYTAB_REALM" \ +|| die "Could not get TGT for host/$hostname@$GETKEYTAB_REALM with PKINIT with FILE:${CERT},${CERT_KEY}" + +# Get CSRF token +export KRB5CCNAME="${d}/cc" +curl \ + "${curl_opts[@]}" \ + --output junk \ + --request POST \ + --data-binary @/dev/null \ + -D headers \ + "${GETKEYTAB_HKD_URI}?spn=host/$hostname&create=true" +csrf_token=$(sed -e 's/\r//' headers | grep ^X-CSRF-Token:) +rm -f headers junk +curl \ + "${curl_opts[@]}" \ + --output keytab \ + --request POST \ + --data-binary @/dev/null \ + -D headers \ + -H "$csrf_token" \ + "${GETKEYTAB_HKD_URI}?spn=host/$hostname&create=true" +grep '^HTTP/1.1 200' headers >/dev/null \ +|| die "Could not create service principal host/$hostname@$GETKEYTAB_REALM" +rm headers +kdestroy -c "FILE:${d}/cc" +check_keytab "FILE:$PWD/keytab" \ +|| die "Keytab fetched did not work" +[[ -f ${KEYTAB#FILE:} ]] \ +&& mv -f "${KEYTAB#FILE:}" "${KEYTAB#FILE:}-" +cp keytab "${KEYTAB#FILE:}" From 728ed5b02988b3fb8c3b2eb3a83065cf96cc0d05 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Tue, 28 Sep 2021 17:30:04 -0500 Subject: [PATCH 09/18] attest-enroll: Start a temp SW TPM for PEM case We should be able to consume PEM and not have to convert it to TPM2B_PUBLIC. However, the tpm2-tools {tpm2 makecredential} and {tpm2 duplicate} commands can't handle that, and we use those in sbin/tpm2-send. For now we start an SW TPM per-invocation, and tear it down when exiting. --- sbin/attest-enroll | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 770bc7e5..5f8182db 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -433,6 +433,7 @@ done tmp= success=false +swtpm_pid= cleanup() { if ((VERBOSE > 1)); then ( @@ -441,6 +442,14 @@ cleanup() { ) | sort fi + ( + [[ -n $swtpm_pid ]] \ + && kill -0 "$swtpm_pid" \ + && kill "$swtpm_pid" + ) 2>/devnull \ + || true + unset TPM2TOOLS_TCTI + if $debug; then echo "LEAVING TEMP DIR ALONE: $tmp" 1>&2 exit 0; @@ -453,6 +462,28 @@ cleanup() { tmp="$(mktemp -d)" trap cleanup EXIT +# We might need a TPM for what should be software-only things... +start_swtpm() { + local -i tries port + ((port= 10240 + ( ($$ % (32768 - 10240) ) ) )) + mkdir "${tmp}/swtpm" + + for ((tries=0; tries < 3; tries++, port+=2)); do + swtpm socket --tpm2 \ + --daemon \ + --tpmstate dir="${tmp}/swtpm" \ + --pid file="${tmp}/.pid" \ + --server type="tcp,bindaddr=127.0.0.1,port=$port" \ + --ctrl type="tcp,bindaddr=127.0.0.1,port=$((port+1))" \ + --flags startup-clear \ + && { swtpm_pid=$(cat "${tmp}/.pid"); break; } + done + + [[ -n $swtpm_pid ]] \ + || die "Could not start a software TPM" + export TPM2TOOLS_TCTI="swtpm:host=127.0.0.1,port=$port" +} + info() { ((VERBOSE == 0)) || echo info: "$@" 1>&2 } @@ -575,6 +606,8 @@ sign() { -out "${1}.sig" } +start_swtpm + cat "$EKPUB" > "$tmp/ekpub" \ || die "$0: unable to read EKpub from stdin" exec Date: Tue, 28 Sep 2021 17:33:19 -0500 Subject: [PATCH 10/18] test-enroll.sh: Fix race condition --- tests/test-enroll.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index 49321395..4f8f54f3 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -92,12 +92,14 @@ start_swtpm() { # We try our best. mkdir "${d}/tpm$port" swtpm socket \ + --daemon \ --tpm2 \ --tpmstate dir="${d}/tpm$port" \ + --pid file="${d}/tpm${port}/.pid" \ --server type="tcp,bindaddr=0.0.0.0,port=$port" \ --ctrl type="tcp,bindaddr=0.0.0.0,port=$cport" \ - --flags startup-clear & - swtpmpids+=($!) + --flags startup-clear + swtpmpids+=("$(cat "${d}/tpm${port}/.pid")") TCTIs[$1]="swtpm:host=localhost,port=$port" } From 96eba44a6542e5091c24c41e9563624c459360ab Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Wed, 29 Sep 2021 14:51:11 -0500 Subject: [PATCH 11/18] fixup! attest-enroll: Start a temp SW TPM for PEM case --- sbin/attest-enroll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 5f8182db..736d430e 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -446,7 +446,7 @@ cleanup() { [[ -n $swtpm_pid ]] \ && kill -0 "$swtpm_pid" \ && kill "$swtpm_pid" - ) 2>/devnull \ + ) 2>/dev/null \ || true unset TPM2TOOLS_TCTI From 5adf56e316ca1adc8e75081a800533e36c1e4cf0 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Thu, 30 Sep 2021 15:49:56 -0500 Subject: [PATCH 12/18] Rationalize BASEDIR/TOP/PREFIX/DIR --- functions.sh | 25 +++++++++++++++++++++ sbin/attest-enroll | 28 ++++++++++++++---------- sbin/gencert | 29 ++++++++++++++----------- sbin/genkeytab | 28 ++++++++++++++---------- sbin/getkeytab | 43 +++++++++++++++++++++++++----------- sbin/safeboot | 54 +++++++++++++++++++++++++++++----------------- sbin/tpm2-attest | 35 +++++++++++++++++++++--------- sbin/tpm2-policy | 22 +++++++++++-------- sbin/tpm2-recv | 19 +++++++++------- sbin/tpm2-send | 19 +++++++++------- 10 files changed, 200 insertions(+), 102 deletions(-) diff --git a/functions.sh b/functions.sh index dd8f04e6..00d55abd 100755 --- a/functions.sh +++ b/functions.sh @@ -20,6 +20,31 @@ error() { echo "$@" >&2 ; return 1 ; } info() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } debug() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } +safeboot_dir() { + [[ -n $1 ]] \ + || die "Internal error in caller of safeboot_dir" + case "$1" in + bin) echo "$TOP/bin";; + lib) echo "$TOP/lib";; + etc) if [[ $TOP = /usr ]]; then + echo "/etc/safeboot" + elif [[ -d $TOP/etc/safeboot ]]; then + echo "$TOP/etc/safeboot" + elif [[ -d $TOP/etc && -f $TOP/etc/safeboot.conf ]]; then + echo "$TOP/etc" + else + die "Cannot find 'etc' directory for Safeboot" + fi + *) die "Internal error in caller of safeboot_dir" + esac +} +safeboot_file() { + [[ -n $1 && -n $2 ]] \ + || die "Internal error in caller of safeboot_file" + safeboot_dir "$1" > /dev/null + local dir="$(safeboot_dir "$1")" + echo "${dir}/$2" +} ######################################## # diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 736d430e..5e33cace 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -13,26 +13,35 @@ set -euo pipefail shopt -s extglob PROG=${0##*/} -if [[ $0 = /* ]]; then - BASEDIR=${0%/*} -elif [[ $0 = */* ]]; then - BASEDIR=$PWD/${0%/*} +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" else - BASEDIR=$PWD + echo "Unable to find Safeboot function library" 1>&2 + exit 1 fi # Make sure to export SAFEBOOT_ENROLL_CONF for external genprogs # # If one or more -C options given, use the first one for this (see getopts # loop below). -export SAFEBOOT_ENROLL_CONF=/etc/safeboot-enroll.conf -CONF=$SAFEBOOT_ENROLL_CONF +cf=$(safeboot_file etc safeboot.conf) +if [[ -n $cf && -f $cf ]]; then + export SAFEBOOT_ENROLL_CONF=/etc/safeboot/enroll.conf + CONF=$SAFEBOOT_ENROLL_CONF +fi configured=false EKPUB=/dev/stdin # Configuration variables -DBDIR="$BASEDIR/build/attest" +DBDIR="$TOP/build/attest" POLICY= ESCROW_POLICY= ESCROW_PUBS_DIR= @@ -79,9 +88,6 @@ configs() { done } -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" - die() { echo >&2 "Error: $PROG" "$@" ; exit 1 ; } warn() { echo >&2 "$@" ; } diff --git a/sbin/gencert b/sbin/gencert index e79981fc..d6aa9cfb 100755 --- a/sbin/gencert +++ b/sbin/gencert @@ -6,12 +6,18 @@ set -euo pipefail shopt -s extglob PROG=${0##*/} -if [[ $0 = /* ]]; then - BASEDIR=${0%/*} -elif [[ $0 = */* ]]; then - BASEDIR=$PWD/${0%/*} +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" else - BASEDIR=$PWD + echo "Unable to find Safeboot function library" 1>&2 + exit 1 fi GENCERT_CRED=PEM-FILE:/etc/safeboot/gencert-ca.pem @@ -22,13 +28,12 @@ GENCERT_INCLUDE_SAN_DNSNAME=false GENCERT_EKUS=() declare -A GENCERT_DOMAIN_REALM -: "${SAFEBOOT_ENROLL_CONF:=/etc/safeboot-enroll.conf}" -# shellcheck disable=SC1090 -[[ -n $SAFEBOOT_ENROLL_CONF && -f $SAFEBOOT_ENROLL_CONF ]] \ -&& . "${SAFEBOOT_ENROLL_CONF}" - -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" +cf=$(safeboot_file etc enroll.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" + export SAFEBOOT_ENROLL_CONF="$cf" +fi die() { echo "skip: $*"; echo >&2 "Error: $PROG" "$@" ; exit 1 ; } warn() { echo >&2 "$@" ; } diff --git a/sbin/genkeytab b/sbin/genkeytab index a9f3a0f2..18aae9a5 100755 --- a/sbin/genkeytab +++ b/sbin/genkeytab @@ -6,12 +6,18 @@ set -euo pipefail shopt -s extglob PROG=${0##*/} -if [[ $0 = /* ]]; then - BASEDIR=${0%/*} -elif [[ $0 = */* ]]; then - BASEDIR=$PWD/${0%/*} +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" else - BASEDIR=$PWD + echo "Unable to find Safeboot function library" 1>&2 + exit 1 fi # Keytab gen methods: @@ -36,13 +42,13 @@ curl_opts=( --negotiate ) -: "${SAFEBOOT_ENROLL_CONF:=/etc/safeboot-enroll.conf}" -# shellcheck disable=SC1090 -[[ -n $SAFEBOOT_ENROLL_CONF && -f $SAFEBOOT_ENROLL_CONF ]] \ -&& . "${SAFEBOOT_ENROLL_CONF}" -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" +cf=$(safeboot_file etc enroll.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" + export SAFEBOOT_ENROLL_CONF="$cf" +fi die() { echo >&2 "Error: $PROG" "$@" ; exit 1 ; } warn() { echo >&2 "$@" ; } diff --git a/sbin/getkeytab b/sbin/getkeytab index 63748d90..6b55e957 100755 --- a/sbin/getkeytab +++ b/sbin/getkeytab @@ -7,7 +7,20 @@ shopt -s extglob umask 077 PROG=${0##*/} -BASEDIR=$(dirname "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )") +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" +else + echo "Unable to find Safeboot function library" 1>&2 + exit 1 +fi + declare -A GETCERT_DOMAIN_REALM KEYTAB=FILE:/etc/krb5.keytab @@ -23,19 +36,23 @@ curl_opts=( --negotiate ) -: "${PREFIX:=}" -: "${DIR:=/etc/safeboot}" -SAFEBOOT_CONF=${PREFIX}${DIR}/safeboot.conf -# shellcheck disable=SC1090 -[[ -n $SAFEBOOT_CONF && -f $SAFEBOOT_CONF ]] \ -&& . "${SAFEBOOT_CONF}" - -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" - -: "${CERT_KEY:=${PREFIX}${DIR}/cert-key.pem}" -: "${CERT:=${PREFIX}${DIR}/cert.pem}" +cf=$(safeboot_file etc safeboot.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" + export SAFEBOOT_CONF="$cf" +else + warn "${cf:-/etc/safeboot/safeboot.conf} not present; was it installed?" +fi +cf=$(safeboot_file etc local.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" +fi +DIR=$(safeboot_dir etc) +: "${CERT_KEY:=${DIR}/cert-key.pem}" +: "${CERT:=${DIR}/cert.pem}" [[ -f $CERT_KEY && -f $CERT ]] \ || die "Could not get PKINIT certificate for impersonation" diff --git a/sbin/safeboot b/sbin/safeboot index e104e30b..ddf9032a 100755 --- a/sbin/safeboot +++ b/sbin/safeboot @@ -24,35 +24,49 @@ set -e -o pipefail export LC_ALL=C -: "${PREFIX:=}" -: "${DIR:=/etc/safeboot}" - -# shellcheck source=functions.sh -. "$PREFIX$DIR/functions.sh" +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck source=functions.sh + . "$TOP/functions.sh" +else + echo "Unable to find Safeboot function library" 1>&2 + exit 1 +fi -if [ -r "$PREFIX$DIR/safeboot.conf" ]; then - # shellcheck source=safeboot.conf - . $PREFIX$DIR/safeboot.conf +: "${PREFIX:=}" +cf=$(safeboot_file etc safeboot.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" + export SAFEBOOT_CONF="$cf" else - warn "$PREFIX$DIR/safeboot.conf not present; was it installed?" + warn "${cf:-/etc/safeboot/safeboot.conf} not present; was it installed?" fi -if [ -r "$PREFIX$DIR/local.conf" ]; then - # shellcheck source=local.conf - . $PREFIX$DIR/local.conf +cf=$(safeboot_file etc local.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" fi +: "${DIR:="$(safeboot_dir etc)"}" + +setup -# Apply $PREFIX to files and use default value -CERT=$PREFIX${CERT:-$DIR/cert.pem} -KERNEL=$PREFIX${KERNEL:-/boot/vmlinuz} -INITRD=$PREFIX${INITRD:-/boot/initrd.img} -EFIDIR=$PREFIX${EFIDIR:-/boot/efi/EFI} +: "${KERNEL:=/boot/vmlinuz}" +: "${INITRD:=/boot/initrd.img}" +: "${EFIDIR:=/boot/efi/EFI}" +: "${CERT:=$DIR/cert.pem}" if [ "$KEY" == "pkcs11:" ]; then # KEY is a hardware token, use the yubikey engine KEY_ENGINE="-e pkcs11" else # KEY is a normal file, don't use an openssl engine - KEY=$PREFIX${KEY:-$DIR/cert.priv} + KEY=${KEY:-$DIR/cert.priv} KEY_ENGINE="" fi @@ -89,7 +103,7 @@ rootdev-check() fi if [ "$TEST_ROOTDEV" = 1 ]; then - warn "$PREFXI$DIR/local.conf: setting \$ROOTDEV=$ROOTDEV" + warn "$PREFIX$DIR/local.conf: setting \$ROOTDEV=$ROOTDEV" echo "ROOTDEV=\"$ROOTDEV\"" >> $PREFIX$DIR/local.conf \ || die "$PREFIX$DIR/local.conf: Unable to set \$ROOTDEV" TEST_ROOTDEV=0 @@ -114,7 +128,7 @@ rootdev-check() fi if [ "$TEST_HASHDEV" = 1 ]; then - warn "$PREFXI$DIR/local.conf: setting \$HASHDEV=$HASHDEV" + warn "$PREFIX$DIR/local.conf: setting \$HASHDEV=$HASHDEV" echo "HASHDEV=\"$HASHDEV\"" >> $PREFIX$DIR/local.conf \ || die "$PREFIX$DIR/local.conf: Unable to set \$HASHDEV" TEST_HASHDEV=0 diff --git a/sbin/tpm2-attest b/sbin/tpm2-attest index 3be8bc0a..c9a0d19d 100755 --- a/sbin/tpm2-attest +++ b/sbin/tpm2-attest @@ -20,21 +20,36 @@ export LC_ALL=C unset CDPATH # Find the directory that contains functions.sh -TOP="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && cd .. && pwd )" -: "${PREFIX:=}" -: "${DIR:=/etc/safeboot}" +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") -. "$TOP/functions.sh" - -if [ -r "$PREFIX$DIR/safeboot.conf" ]; then - . $PREFIX$DIR/safeboot.conf +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck source=functions.sh + . "$TOP/functions.sh" else - warn "$PREFIX$DIR/safeboot.conf not present?" + echo "Unable to find Safeboot function library" 1>&2 + exit 1 +fi + +cf=$(safeboot_file etc safeboot.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" + export SAFEBOOT_CONF="$cf" fi -if [ -r "$PREFIX$DIR/local.conf" ]; then - . $PREFIX$DIR/local.conf +cf=$(safeboot_file etc local.conf) +if [[ -n $cf && -f $cf ]]; then + # shellcheck disable=SC1090 + . "$cf" fi +: "${PREFIX:=}" +: "${DIR:=/etc/safeboot}" + +setup # Apply $PREFIX to files and use default value [[ -n ${CERT:-} && ${CERT} != /* ]] \ diff --git a/sbin/tpm2-policy b/sbin/tpm2-policy index 0c85c2f9..8e859db5 100755 --- a/sbin/tpm2-policy +++ b/sbin/tpm2-policy @@ -1,13 +1,20 @@ #!/bin/bash PROG=${0##*/} - -if [[ $0 = /* ]]; then - BASEDIR=${0%/*} -elif [[ $0 = */* ]]; then - BASEDIR=$PWD/${0%/*} +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" + functions_sh=$TOP/lib/safeboot/functions.sh +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" + functions_sh=$TOP/functions.sh else - BASEDIR=$PWD + echo "Unable to find Safeboot function library" 1>&2 + exit 1 fi set -euo pipefail -o noclobber @@ -40,9 +47,6 @@ EOF exit "${1:-1}" } -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" - out= force=false activate=false diff --git a/sbin/tpm2-recv b/sbin/tpm2-recv index ff1e4c93..d841a803 100755 --- a/sbin/tpm2-recv +++ b/sbin/tpm2-recv @@ -1,12 +1,18 @@ #!/bin/bash PROG=${0##*/} -if [[ $0 = /* ]]; then - BASEDIR=${0%/*} -elif [[ $0 = */* ]]; then - BASEDIR=$PWD/${0%/*} +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" else - BASEDIR=$PWD + echo "Unable to find Safeboot function library" 1>&2 + exit 1 fi set -euo pipefail @@ -46,9 +52,6 @@ EOF exit 1 } -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" - force=false while getopts +:hfx opt; do case "$opt" in diff --git a/sbin/tpm2-send b/sbin/tpm2-send index dbd5ca81..eefa97ad 100755 --- a/sbin/tpm2-send +++ b/sbin/tpm2-send @@ -1,12 +1,18 @@ #!/bin/bash PROG=${0##*/} -if [[ $0 = /* ]]; then - BASEDIR=${0%/*} -elif [[ $0 = */* ]]; then - BASEDIR=$PWD/${0%/*} +BINDIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +TOP=$(dirname "$BINDIR") + +if [[ -s $TOP/lib/safeboot/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/lib/safeboot/functions.sh" +elif [[ -s $TOP/functions.sh ]]; then + # shellcheck disable=SC1090 source=functions.sh + . "$TOP/functions.sh" else - BASEDIR=$PWD + echo "Unable to find Safeboot function library" 1>&2 + exit 1 fi set -euo pipefail @@ -96,9 +102,6 @@ EOF exit "${1:-1}" } -# shellcheck disable=SC1090 -. "$BASEDIR/../functions.sh" - force=false method=WK policy= From 1f3539ea1c11765cd3a2ab5059e8d0a7189aec8e Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Fri, 1 Oct 2021 11:22:18 -0500 Subject: [PATCH 13/18] fixup! Rationalize BASEDIR/TOP/PREFIX/DIR --- functions.sh | 6 ++++-- sbin/attest-enroll | 5 ++++- tests/test-enroll.sh | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/functions.sh b/functions.sh index 00d55abd..e7b0f5b2 100755 --- a/functions.sh +++ b/functions.sh @@ -32,10 +32,12 @@ safeboot_dir() { echo "$TOP/etc/safeboot" elif [[ -d $TOP/etc && -f $TOP/etc/safeboot.conf ]]; then echo "$TOP/etc" + elif [[ -d /etc/safeboot ]]; then + echo "$TOP/etc" else die "Cannot find 'etc' directory for Safeboot" - fi - *) die "Internal error in caller of safeboot_dir" + fi;; + *) die "Internal error in caller of safeboot_dir";; esac } safeboot_file() { diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 5e33cace..625ebcb0 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -482,7 +482,10 @@ start_swtpm() { --server type="tcp,bindaddr=127.0.0.1,port=$port" \ --ctrl type="tcp,bindaddr=127.0.0.1,port=$((port+1))" \ --flags startup-clear \ - && { swtpm_pid=$(cat "${tmp}/.pid"); break; } + || continue + sleep 1 + swtpm_pid=$(cat "${tmp}/.pid") + break done [[ -n $swtpm_pid ]] \ diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index 4f8f54f3..e8f81e58 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -99,6 +99,7 @@ start_swtpm() { --server type="tcp,bindaddr=0.0.0.0,port=$port" \ --ctrl type="tcp,bindaddr=0.0.0.0,port=$cport" \ --flags startup-clear + sleep 1 swtpmpids+=("$(cat "${d}/tpm${port}/.pid")") TCTIs[$1]="swtpm:host=localhost,port=$port" } From 346a6b23d8df602cc103b276a43bd394fbc077f3 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Fri, 1 Oct 2021 16:35:11 -0500 Subject: [PATCH 14/18] fixup! Rationalize BASEDIR/TOP/PREFIX/DIR --- functions.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions.sh b/functions.sh index e7b0f5b2..af347d44 100755 --- a/functions.sh +++ b/functions.sh @@ -32,6 +32,8 @@ safeboot_dir() { echo "$TOP/etc/safeboot" elif [[ -d $TOP/etc && -f $TOP/etc/safeboot.conf ]]; then echo "$TOP/etc" + elif [[ -d $TOP && -f $TOP/safeboot.conf ]]; then + echo "$TOP" elif [[ -d /etc/safeboot ]]; then echo "$TOP/etc" else From 62509a5e9381000486d5759224ca4fb31bac0922 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Fri, 1 Oct 2021 16:31:10 -0500 Subject: [PATCH 15/18] functions.sh: different programs have different cleanup needs --- functions.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/functions.sh b/functions.sh index af347d44..75b90901 100755 --- a/functions.sh +++ b/functions.sh @@ -60,7 +60,6 @@ safeboot_file() { # ######################################## -TMP= TMP_MOUNT=n cleanup() { if [[ $TMP_MOUNT = "y" ]]; then @@ -70,8 +69,11 @@ cleanup() { [[ -n $TMP ]] && rm -rf "$TMP" } -trap cleanup EXIT -TMP=$(mktemp -d) +setup() { + TMP= + trap cleanup EXIT + TMP=$(mktemp -d) +} mount_tmp() { mount -t tmpfs none "$TMP" || die "Unable to mount temp directory" From f2f017b8890b345a4b1d62955d728dccacb73ab4 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Fri, 1 Oct 2021 15:18:07 -0500 Subject: [PATCH 16/18] test-enroll: No need to use lsof --- tests/test-enroll.sh | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index e8f81e58..b4f25844 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -70,18 +70,8 @@ declare -A TCTIs start_port=9880 start_swtpm() { local tries=0 - - while ((tries < 3)) && lsof -i ":${start_port}" >/dev/null; do - ((++tries)) - ((++start_port)) - done local port=$start_port ((++start_port)) - - while ((tries < 3)) && lsof -i ":${start_port}" >/dev/null; do - ((++tries)) - ((++start_port)) - done local cport=$((start_port)) ((++start_port)) @@ -98,7 +88,8 @@ start_swtpm() { --pid file="${d}/tpm${port}/.pid" \ --server type="tcp,bindaddr=0.0.0.0,port=$port" \ --ctrl type="tcp,bindaddr=0.0.0.0,port=$cport" \ - --flags startup-clear + --flags startup-clear \ + || continue sleep 1 swtpmpids+=("$(cat "${d}/tpm${port}/.pid")") TCTIs[$1]="swtpm:host=localhost,port=$port" From 7d202f668efe96e4cc2cc42c92a367fc1a5495ef Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Tue, 14 Sep 2021 20:26:48 -0500 Subject: [PATCH 17/18] WIP: Policy system, take 2 WIP WIP WIP -- not tested Let's try a different approach to expressing complex policies: - Policies as restricted bash scripts that can do very little besides evaluate TPM policies, and which are invoked via... - ...a driver script, `sbin/tpm2-policy`, that sets up the environment for running the policy, and takes optional additional artifacts to make available to the policy. - Constant artifacts would be things like: - signer keys for `policysigned` and `policyauthorize` - signer key names for `policyticket` and such - anything else you can imagine Non-constant artifacts would be things like: - saved object context files for signer keys for `policysigned` and `policyauthorize` - tickets from `verifysignature`, `policysecret`, and `policysigned` - timeout files - anything else you can imagine all of which can be written only in `.` / `$PWD`. - The policy scripts would run in the following environment: - a temp dir as the current directory, with `$TMPDIR` set to `$PWD` and in which all the necessary artifacts, including the policy script itself, shall have been placed - various TPM2_POLICY... env vars, mainly TPM2_POLICY_SESSION - `$PATH` set to have just two paths: an `rbin` (see below), and the `$PWD`/`$TMPDIR` itself into which the policy script will have been copied (this will allow the policy script to implement different sub-policies selected by its arguments by executing itself, possibly through the `rbin/policyor` wrapper). - The "rbin" directory in the PATH for the execution of policy scripts, with: - wrappers for all the tpm2_policy... commands - the wrapper for tpm2_policyor is a bit special, naturally - wrappers for tpm2_loadexternal and tpm2_verifysignature, and possibly others - links to or wrappers of a handful of useful system commands like `sha256sum`, `xxd`, `dc`, `bc`, `jq`, etc. - a wrapper around `xxd` and `cat` for creating files from stdin so that artifacts can be embedded as here documents in the script This way policies get all the expressive power of bash, and access to all the functionality of tpm2-tools' policy commands. TBD: - Test, debug, test, ... - Add rbin wrappers for importing duplicated keys (so they can provide authValues discretely!). - Maybe allow execution of `sbin/tpm2-recv` by linking it from the rbin? - Add sample policies that are interesting, like using a combination of `policysigned` and `policyauthorize` and `curl` (outside the policy script) to execute an external script that varies at runtime, or policies that use different passwords for N different users, or which use `policysigned` to let some other entity authenticate N different users (using `policyRef` to name them, say), policies requiring golden PCRs OR external policy, etc. Showcase `policyor`! It'd be very nice to be able to have a policy like this: (golden PCRs && user authValue) || (admin authValue OTP && NV revocation of OTP index) || (superadmin authValue) || ($policy_containing_policy_signed && policyauthorize_of_it) Perhaps with multiple superadmin authValues via `policyor`. Such a policy would allow a laptop to boot normally with a user password, and after emergency updates boot with an admin OTP, or after any mishaps via a superadmin password, or with a separate policy that blesses the current PCRs as golden. Such a policy could be used for sealing an NV index, or it could be set on local storage keys encrypted to the TPM's EKpub via `sbin/tpm2-send`, allowing for unattended server booting post-attestation, or attended server booting post-mishap (if the encrypted assets get stored "in the clear"). --- Makefile | 3 +- functions.sh | 98 +++++++++++++++++++++++ rbin/flushcontext | 80 +++++++++++++++++++ rbin/import | 105 +++++++++++++++++++++++++ rbin/load | 93 ++++++++++++++++++++++ rbin/loadexternal | 113 ++++++++++++++++++++++++++ rbin/policyauthorize | 95 ++++++++++++++++++++++ rbin/policyauthorizenv | 93 ++++++++++++++++++++++ rbin/policyauthvalue | 70 +++++++++++++++++ rbin/policycommandcode | 70 +++++++++++++++++ rbin/policycountertimer | 99 +++++++++++++++++++++++ rbin/policycphash | 76 ++++++++++++++++++ rbin/policyduplicationselect | 81 +++++++++++++++++++ rbin/policylocality | 70 +++++++++++++++++ rbin/policynamehash | 75 ++++++++++++++++++ rbin/policynv | 92 ++++++++++++++++++++++ rbin/policynvwritten | 70 +++++++++++++++++ rbin/policyor | 136 ++++++++++++++++++++++++++++++++ rbin/policypassword | 70 +++++++++++++++++ rbin/policypcr | 79 +++++++++++++++++++ rbin/policysecret | 104 ++++++++++++++++++++++++ rbin/policysigned | 111 ++++++++++++++++++++++++++ rbin/policytemplate | 78 ++++++++++++++++++ rbin/policyticket | 96 +++++++++++++++++++++++ rbin/startauthsession | 79 +++++++++++++++++++ rbin/tpm2 | 85 ++++++++++++++++++++ rbin/verifysignature | 92 ++++++++++++++++++++++ rbin/writeartifact | 71 +++++++++++++++++ sbin/safeboot-tpm-unseal | 2 + sbin/tpm2-policy | 148 ++++++++++++++++++++++++++--------- sbin/tpm2-send | 9 +++ tests/test-policy | 65 +++++++++++++++ 32 files changed, 2569 insertions(+), 39 deletions(-) create mode 100755 rbin/flushcontext create mode 100644 rbin/import create mode 100644 rbin/load create mode 100755 rbin/loadexternal create mode 100755 rbin/policyauthorize create mode 100755 rbin/policyauthorizenv create mode 100755 rbin/policyauthvalue create mode 100755 rbin/policycommandcode create mode 100755 rbin/policycountertimer create mode 100755 rbin/policycphash create mode 100755 rbin/policyduplicationselect create mode 100755 rbin/policylocality create mode 100755 rbin/policynamehash create mode 100755 rbin/policynv create mode 100755 rbin/policynvwritten create mode 100755 rbin/policyor create mode 100755 rbin/policypassword create mode 100755 rbin/policypcr create mode 100755 rbin/policysecret create mode 100755 rbin/policysigned create mode 100755 rbin/policytemplate create mode 100755 rbin/policyticket create mode 100755 rbin/startauthsession create mode 100755 rbin/tpm2 create mode 100755 rbin/verifysignature create mode 100755 rbin/writeartifact create mode 100755 tests/test-policy diff --git a/Makefile b/Makefile index 886af57d..42e4b15b 100644 --- a/Makefile +++ b/Makefile @@ -307,9 +307,10 @@ shellcheck: sbin/tpm2-recv \ sbin/tpm2-policy \ initramfs/*/* \ + rbin/* \ tests/test-enroll.sh \ ; do \ - shellcheck $$file functions.sh ; \ + shellcheck -x $$file functions.sh ; \ done # Fetch several of the TPM certs and make them usable diff --git a/functions.sh b/functions.sh index 75b90901..cd5f6415 100755 --- a/functions.sh +++ b/functions.sh @@ -651,3 +651,101 @@ verify_sig() { || die "could not verify signature on $body with $pubkey" } + +# getopts_long lname optstring name [args...] +# +# lname is the name of an associative array whose indices are long option +# names and whose values are either the empty string (no option argument), +# or ':' (the option requires an argument). +# +# optstring is an optstring value for getopts +# +# optname is the name of a variable in which to put the matched option +# name / letter. +# +# args... is the arguments to parse. +# +# As with getopts, $OPTIND is set to the next argument to check at the +# next invocation. Unset OPTIND or set it to 1 to reset options +# processing. +# +# As with getopts, "--" is a special argument that ends options +# processing. +# +# Example: +# +# declare -A long=([foo]=: [id]=: [silent]='') +# foo=none +# id=$USER +# silent=false +# while getopts_long long f:i:sx opt "$@"; do +# case "$opt" in +# foo|f) foo=$OPTARG;; +# id|i) id=$OPTARG;; +# silent|s) silent=true; [[ -n $OPTARG ]] && echo "Look ma: optional option arguments! --silent=$OPTARG";; +# x) set -vx;; +# *) echo "Usage: $0 [--foo FOO | -f FOO] [--id USER | -i USER] [--silent | -s] [-x] ARGS" 1>&2; exit 1;; +# esac +# done +# +# shift $((OPTIND-1)) +# echo "foo=$foo id=$id silent=$silent; args: $#: $*" +function getopts_long { + if (($# < 3)); then + printf 'bash: illegal use of getopts_long\n' + printf 'Usage: getopts_long lname optstring name [ARGS]\n' + printf '\t{lname} is the name of an associative array variable\n' + printf '\twhose keys are long option names and values are\n' + printf '\tthe empty string (no argument) or ":" (argument\n' + printf '\trequired).\n\n' + printf '\t{optstring} and {name} are as for the {getopts}\n' + printf '\tbash builtin.\n' + return 1 + fi 1>&2 + [[ ${1:-} != lopts ]] && local -n lopts="$1" + local optstr="$2" + [[ ${3:-} != opt ]] && local -n opt="$3" + local optvar="$3" + shift 3 + + # shellcheck disable=SC2034 + OPTOPT= + OPTARG= + : "${OPTIND:=1}" + # shellcheck disable=SC2124 + opt=${@:$OPTIND:1} + if [[ $opt = -- ]]; then + opt='?' + return 1 + fi + if [[ $opt = --* ]]; then + # shellcheck disable=SC2034 + OPTOPT='-' + local optval=false + opt=${opt#--} + if [[ $opt = *=* ]]; then + OPTARG=${opt#*=} + opt=${opt%%=*} + optval=true + fi + ((++OPTIND)) + if [[ ${lopts[$opt]+yes} != yes ]]; then + ((OPTERR)) && printf 'bash: illegal long option %s\n' "$opt" 1>&2 + opt='?' + return 0 + fi + if [[ ${lopts[$opt]:-} = : ]]; then + if ! $optval; then + # shellcheck disable=SC2124 + OPTARG=${@:$OPTIND:1} + ((++OPTIND)) + fi + fi + return 0 + fi + if getopts "$optstr" "$optvar" "$@"; then + return 0 + else + return $? + fi +} diff --git a/rbin/flushcontext b/rbin/flushcontext new file mode 100755 index 00000000..3bb4d331 --- /dev/null +++ b/rbin/flushcontext @@ -0,0 +1,80 @@ +#!/bin/bash + +PROG=${0##*/} +# Restore the environment so that we have a sane PATH and can find, e.g., +# tpm2-tools. +# shellcheck disable=SC1090 disable=SC1091 +. ./env + +set -euo pipefail +shopt -s extglob + +TMPDIR=$PWD +[[ -n ${TPM2_POLICY_SESSION:-} ]] \ +|| die "TPM2_POLICY_SESSION is not set" +[[ ${TPM2_POLICY_SESSION} != */* || + ${TPM2_POLICY_SESSION} = ${PWD}/* ]] \ +|| die "TPM2_POLICY_SESSION does not exist" +[[ -s ${TPM2_POLICY_SESSION} ]] \ +|| die "TPM2_POLICY_SESSION does not exist" + +# shellcheck disable=SC2209 +function usage { + ((${1:-1} > 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) || usage + +unset TPM2_POLICY_ALTERNATIVES + +declare -a policies=() +declare -a sessions=() +cleanup() { rm -rf "${policies[@]}" "${sessions[@]}"; } +trap cleanup EXIT + +let i=0 +declare -a cmd +while (($#)); do + # Extract POLICY_i from the argv + cmd=() + while (($#)) && [[ $1 != ";" ]]; do + cmd+=("$1") + shift + done + (($#)) && [[ $1 != ";" ]] && shift + + policy="digest-${TPM2_POLICY_SESSION}-${i}" + policies+=("$TPM2_POLICY") + if $trial || ((i != this_alt)); then + # Either we're not executing the policy, or not this arm of it. + sessions+=("${TPM2_POLICY_SESSION}-${i}") + tpm2 flushcontext --loaded-session + tpm2 startauthsession --session "${TPM2_POLICY_SESSION}-${i}" + TPM2_POLICY_SESSION=${TPM2_POLICY_SESSION}-${i} \ + TPM2_POLICY="$policy" \ + "${cmd[@]}" + tpm2 flushcontext --loaded-session + else + # shellcheck disable=SC2124 + TPM2_POLICY_ALTERNATIVES=${alts[@]:1:${#alts[@]}} \ + TPM2_POLICY="$policy" \ + "${cmd[@]}" + fi + ((++i)) +done + +tpm2_policyor --session "${TPM2_POLICY_SESSION}" \ + --policy "$TPM2_POLICY" \ + sha256:"$(join ',' "${policies[@]}")" diff --git a/rbin/policypassword b/rbin/policypassword new file mode 100755 index 00000000..094f3a51 --- /dev/null +++ b/rbin/policypassword @@ -0,0 +1,70 @@ +#!/bin/bash + +PROG=${0##*/} +# shellcheck disable=SC1090 disable=SC1091 +. ./env + +set -euo pipefail +shopt -s extglob + +TMPDIR=$PWD +[[ -n ${TPM2_POLICY_SESSION:-} ]] \ +|| die "TPM2_POLICY_SESSION is not set" +[[ ${TPM2_POLICY_SESSION} != */* || + ${TPM2_POLICY_SESSION} = ${PWD}/* ]] \ +|| die "TPM2_POLICY_SESSION does not exist" +[[ -s ${TPM2_POLICY_SESSION} ]] \ +|| die "TPM2_POLICY_SESSION does not exist" + +# shellcheck disable=SC2209 +function usage { + ((${1:-1} > 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager < "$1" +else + cat > "$1" +fi diff --git a/sbin/safeboot-tpm-unseal b/sbin/safeboot-tpm-unseal index aad13dfc..3a4ba378 100755 --- a/sbin/safeboot-tpm-unseal +++ b/sbin/safeboot-tpm-unseal @@ -32,6 +32,8 @@ for script in \ fi done +setup + # Override die to extend the boot mode PCR to indicate the failure die() { echo >&2 "$@" diff --git a/sbin/tpm2-policy b/sbin/tpm2-policy index 8e859db5..922ccf3b 100755 --- a/sbin/tpm2-policy +++ b/sbin/tpm2-policy @@ -23,58 +23,130 @@ shopt -s extglob function usage { ((${1:-1} > 0)) && exec 1>&2 cat < wrapper for {tpm2_verifysignature} + - {loadexternal} -> wrapper for {tpm2_loadexternal} + - {policy*} -> wrappers for {tpm2_policy*} + - sha256sum + - stat + - xxd + + The wrappers will not allow access to files outside {TMPDIR}. + + TMPDIR will be set to a directory that the policy script can use for its + needs. In particular, any key contexts loaded from policy artifacts, and any + tickets made by any of the wrapped tpm2 commands, must be placed in TMPDIR. + + The current directory when running the policy script will be the {TMPDIR}. EOF exit "${1:-1}" } -out= -force=false -activate=false -rsa_decrypt=false -command_code= -while getopts +:ADhefo:x opt; do -case "$opt" in -A) activate=true;; -D) rsa_decrypt=true;; -h) usage 0;; -f) force=true;; -o) out=$OPTARG;; -x) set -vx;; -*) usage;; -esac -done -shift $((OPTIND - 1)) - -! $activate || ! $rsa_decrypt || die "-A and -D are mutually exclusive" -$activate && command_code=TPM2_CC_ActivateCredential -$rsa_decrypt && command_code=TPM2_CC_RSA_Decrypt +# shellcheck disable=SC2034 +declare -A lopts=( + [help]='' + [trace]='' + [file]=: + [path]=: + [session]=: + [policy]=: + [existing]='' + [trial]='' +) # Make a temp dir and remove it when we exit: d= -trap 'rm -rf "$d"' EXIT +trap 'cd /; rm -rf "$d"' EXIT d=$(mktemp -d) +export TMPDIR="$d" + +existing=false +alts= +trial=false +policy= +session= +while getopts_long lopts +:L:NP:S:Thx opt "$@"; do +# shellcheck disable=SC2154 +case "$opt" in +h|help) usage 0;; +x|trace) set -vx;; +F|file) cp -f "$OPTARG" "$TMPDIR";; +L|policy) policy=$OPTARG;; +E|existing) existing=true;; +P|path) alts=$OPTARG; trial=false;; +S|session) session=$OPTARG;; +T|trial) trial=true;; +*) usage;; +esac +done +shift $((OPTIND - 1)) +(($#)) || usage + +cd "$TMPDIR" + +# Save the current environment so it can be restored easily by the rbin +# wrappers. Make sure the env is not writable so that the restricted bash +# script cannot write it. +unset TPM2_POLICY_SESSION TPM2_POLICY_ALTERNATIVES TPM2_POLICY +export PREFIX DIR +(umask 0222; export -p > "${TMPDIR}/env") + +[[ -z ${session:-} ]] && session=${TMPDIR}/session +if $existing; then + [[ -z ${session:-} || ! -s ${session:-} ]] \ + && die "Session file exists" +elif ! $existing; then + if $trial; then + tpm2 startauthsession --session "$session" + else + tpm2 startauthsession --session "$session" \ + --policy-session + fi +fi -policyDigest=$(make_policyDigest $command_code "$@") -[[ -z $out ]] || ! $force >| "$out" -[[ -z $out ]] || $force > "$out" -echo "$policyDigest" +: "${policy:="$(mktemp)"}" +TPM2_POLICY_SESSION=$session +TPM2_POLICY_ALTERNATIVES=$alts +TPM2_POLICY=${policy} +export TPM2_POLICY TPM2_POLICY_SESSION TPM2_POLICY_ALTERNATIVES + +script=$1 +shift + +# Run restricted scripts with FD 4 set to /dev/null so they can redirect output +# to /dev/null using 1>&4 and 2>&4. +BASH_ENV="${BASEDIR}/functions.sh" \ +PATH="$BASEDIR/rbin" \ +/bin/rbash "${script}" "$@" 4<>/dev/null +bin2hex < "$policy" diff --git a/sbin/tpm2-send b/sbin/tpm2-send index eefa97ad..d2a4aeb5 100755 --- a/sbin/tpm2-send +++ b/sbin/tpm2-send @@ -18,6 +18,15 @@ fi set -euo pipefail shopt -s extglob +# shellcheck disable=SC2034 +declare -A lopts=( + [help]=h + [method]=M: + [policy]=P: + [force]=f: + [trace]=x +) + # shellcheck disable=SC2209 function usage { ((${1:-1} > 0)) && exec 1>&2 diff --git a/tests/test-policy b/tests/test-policy new file mode 100755 index 00000000..20cedc5f --- /dev/null +++ b/tests/test-policy @@ -0,0 +1,65 @@ +#!/bin/bash + +PROG=${0##*/} + +set -euo pipefail +shopt -s extglob + +die() { echo "Error: $*" 1>&2; exit 1; } + +[[ ${TMPDIR:-} ]] \ +|| die "TMPDIR is not set" +[[ ${TMPDIR:-} = "$PWD" ]] \ +|| die "TMPDIR is not equal to \$PWD" +[[ -n ${TPM2_POLICY_SESSION:-} ]] \ +|| die "TPM2_POLICY_SESSION is not set" +[[ ${TPM2_POLICY_SESSION} != */* || + ${TPM2_POLICY_SESSION} = ${PWD}/* ]] \ +|| die "TPM2_POLICY_SESSION does not exist" +[[ -s ${TPM2_POLICY_SESSION} ]] \ +|| die "TPM2_POLICY_SESSION does not exist" + +IFS=: read -a path <<<"$PATH" +((${#path[@]} < 1 || ${#path[@]} > 2)) \ +&& die "PATH not set correctly (wrong number of paths)" +((${#path[@]} == 1)) && [[ ${path[0]} != */rbin ]] \ +&& die "PATH not set correctly (rbin missing)" +((${#path[@]} == 2)) \ +&& [[ ${path[0]} != */rbin && ${path[1]} != */rbin ]] \ +&& die "PATH not set correctly (rbin missing)" +((${#path[@]} == 2)) \ +&& [[ ${path[0]} != "$PWD" && ${path[1]} != "$PWD" ]] \ +&& die "PATH not set correctly (something other than \$PWD and rbin is present)" + +# shellcheck disable=SC2209 +function usage { + ((${1:-1} > 0)) && exec 1>&2 + local -a usage + readarray usage < Date: Sun, 26 Sep 2021 19:09:38 -0500 Subject: [PATCH 18/18] WIP: Policy system, take 3: JSON This is a sketch of how to represent and implement policies expressed in JSON using "take 2" as a model. Policies are represented as JSON objects that have: - zero, one, or more named policy parameters, which have type information - zero, one, or more bindings for a policy's parameters -- these can be default values, or values for parameters of other policies referred to by this one - an actual policy AST A policy can refer to other policies. This is especially necessary for TPM2_PolicyAuthorize() and TPM2_PolicyAuthorizeNV(), where the referred-to policy may not be known until run-time, so we really have to be able to separate the referrent and the referred-to policies. This may also be useful for TPM2_PolicyOr() even though its alternatives are static -- it may help organize policies, and to DRY. We treat TPM2_PolicyOr() as AST interior nodes. Interior nodes have to be singular TPM2_PolicyOr() commands. Leaf nodes are sequences of commands the first of which is allowed to be a hole, like TPM2_PolicyAuthorize() or TPM2_PolicyAuthorizeNV(). See ./policy.jq! --- policy.jq | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 policy.jq diff --git a/policy.jq b/policy.jq new file mode 100644 index 00000000..7a5eeb2f --- /dev/null +++ b/policy.jq @@ -0,0 +1,440 @@ +# This is a jq program to handle TPM 2.0 EA policies. +# +# Currently a WORK IN PROGRESS. +# +# Status: +# +# - policy merging is working +# +# - policy traversal to generate execution traces is working +# +# - TBD: implement all the commands +# +# - TBD: how to represent ancillary non-policy commands needed to execute a +# policy, such as importing and/or loading external keys referenced by the +# policy commands (e.g., signer keys for TPM2_PolicyAuthorize(), objects +# referenced by TPM2_PolicySecret(), etc.) +# +# One option is to refer to them explicitly among policy commands. This may +# not be a great option as it complicates validation of a policy's form. +# +# - TBD: how to represent parameter bindings values -- probably as textual +# inputs to tpm2-tools commands rather than as base64 encodings of TPM2 +# values +# +# Goals: +# +# - Implement a jq program with various subcommands that ultimately allows for +# the representation of complex parametrized TPM 2.0 EA policies and the +# execution of those policies in trial and/or authorization sessions. +# +# The jq program itself would execute no TPM commands -- instead a trace of +# commands to execute would be output and executed by a bash script (or +# whatever). +# +# Desired sub-commands: +# +# list-policy-references -- List URIs of policies that need to be +# fetched by the caller. +# +# Useful for fetching policies as needed at +# run-time, then merging into a single working +# policy. +# +# For example, a policy referred to by +# TPM2_PolicyAuthorize() might have to be +# downloaded at run-time because... that's that +# command's point, that the policy is to be +# determined at run-time. +# +# merge-policies -- merge N given policies into one +# +# trace-policy-trial -- given a policy, emit a trace of commands to execute +# in order to evaluate the policy in trial sessions +# +# trace-policy-exec -- given a policy and a path through the PolicyOr AST, +# emit a trace of commands to execute in order to +# evaluate the policy (including evaluation in trial +# sessions of PolicyOr alternatives not-taken) +# +# +# Some useful observations about TPM 2.0 EA policies +# +# - there are "holes", namely: TPM2_PolicyOr(), TPM2_PolicyAuthorizeNV(), and +# TPM2_PolicyAuthorize() +# +# - any sequence of TPM2_Policy*() commands is a conjunction, except for +# TPM2_PolicyOr() which... is special +# +# - TPM2_PolicyOr() defines an alternation of policies (up to 8) +# +# - the way holes work is that one must first execute the referred-to policy's +# policy commands, then the hole command itself as it will replace rather +# than extend the session's policyDigest, then any subsequent policy +# commands +# +# - if we view the policies referred to by holes as separate from the policy +# containing the hole, we can treat the former are children of the latter to +# form an AST +# +# - we can construct an AST where the nodes are holes and the policy commands +# that follow them: +# +# Interior nodes start with a hole and contain no other holes: +# +# PolicyOr, followed by non-hole commands +# / \ +# / \ +# / \ +# / \ +# / \ +# +# ... ... +# +# with each sub-policy being a sequence of policy commands the first one of +# which may be a hole. +# +# Leaf sub-policies have no holes, else they must have one or more child +# nodes. +# +# - only PolicyOr has multiple alternatives -- the other holes have just one +# +# - to trace the execution of a policy one must traverse the AST in +# port-order, executing leaves first so that their parent nodes can know the +# policyDigest of each child +# +# Here a policy is a JSON object with the following keys: +# +# params, bindings, policies, policyDef +# +# Params define parameters that may be referenced by commands in the policy. +# +# Bindings are values for those parameters. A policy may be fragmentary and +# include only parameters, and it may include default bindings for some or all +# of those parameters. +# +# Bindings of parameters are essentially command parameters for the commands +# that make up the policy, or for ancillary non-policy commands that import/ +# load/create/loadexternal objects needed by the policy. +# +# A policy may refer to other policies by name. The `policies` key's value +# should be an object whose keys are policy names and whose values are objects +# with a uri key and an optional digest key. +# +# A policyDef is a recursive data structure, and if its value is a string then +# it names a policy to substitute in. A policyDef may be either an object with +# another policyDef key, or an array of policy commands. +# +# A policy command is an object with a command key and various not-yet- +# developed keys for command input arguments. + + +# Some useful utilities +def debug($x): ($x|debug|empty),.; +def cond(c; t): if c then t else . end; +def cond(c; t; f): if c then t else f end; +def cond(c0; t0; c1; t1; f): cond(c0; t0; cond(c1; t1; f)); +def cond(c0; t0; c1; t1; c2; t2; f): cond(c0; t0; c1; t1; cond(c2; t2; f)); +def typecase(T; t; f): cond(type==T; t; f); +def typecase(T; t): typecase(T; t; empty); +def typecase(T0; t0; T1; t1; f): typecase(T0; t0; typecase(T1; t1; f)); +def typecase(T0; t0; T1; t1): typecase(T0; t0; T1; t1; empty); +def typecase(T0; t0; T1; t1; T2; t2; f): typecase(T0; t0; T1; t1; typecase(T2; t2; f)); +def typecase(T0; t0; T1; t1; T2; t2): typecase(T0; t0; T1; t1; T2; t2; empty); +def check(c; e): cond(c; .; e|error); + +# Check that the input array (or string) is a prefix of a given one ($of) +def isPrefix($of): + (length) as $inlen + | ($inlen <= ($of | length)) and (.==$of[0:$inlen]); + +# Convert an array of objects into an object where the keys in the object at +# the values of the `.[$i]|k` for each $i'th element of the array +def a2o(k): + typecase("array"; reduce .[] as $v ({}; .[$v|k] = $v); + "object"; .; + "null"; {}; + "Expected array or object; got \(type)"|error); + +# Merge a set of (e.g., "params"). Duplicates not allowed. +def merge_unique(a; k; e): + a2o(k) + | reduce (a|a2o(k)) as $o (.; + reduce ($o|keys_unsorted[]) as $k (.; + cond(has($k) and ($o|has($k)); $k|e|error) + | .[$k] //= $o[$k] + ) + ); +def merge_unique(a; k): merge_unique(a; k; "Key \(.) is not unique"); + +# Merge a set of (e.g., "bindings"). Duplicates are allowed -- first value +# wins. +def merge(a; k): + a2o(k) + | reduce (a|a2o(k)) as $o (.; + reduce ($o|keys_unsorted[]) as $k (.; .[$k] //= $o[$k]) + ); + +# Some policy checking functions. We want to check that AST nodes are +# sequences where at most the first command may be a "hole" command. + +# A policy hole is a TPM 2.0 policy command that replaces rather than extends +# the session's policyDigest +def holes: "PolicyOr", "PolicyAuthorize", "PolicyAuthorizeNV"; + +# Check if a command is a hole +def isHole: . as $command | any(holes; .==$command); + +# Check that a policy AST node contains no holes after the first command +def checkPolicyHole: + typecase("array"; + cond(any(.[1:][]; isHole); "Holes must come first"|error); + .); + +# This function merges the given policies, using the policy name given as input +# (`.`) as the name of the main policy. +def mergePolicies(policies): + # Internal utility to resolve references to policies (O(N)) + def fix_refs: + reduce path(..[]?|.policyDef?|select(type=="string")) as $path (.; + (getpath($path)) as $reference + | .policyDefs[$reference] as $target + # debug("Resolving reference to \($reference) at \($path) to \($target)") + | cond($target == null; "Missing policy \($reference)"|error) + | setpath($path; $target.policyDef) + ) + ; + + # Save the name of the main policy + . as $main_policy + + # Skeleton of merged policy + | {params:{},bindings:{},policyDefs:{}} + + # First the policies' params (these must be unique) + | reduce policies as $p (.; + (.params |= merge_unique($p.params; .name)) + ) + + # First the policies' bindings (need not be unique; first setting wins; + # this allows for policy fragments to provide default bindings that can be + # overridden by the policies that include them) + | reduce policies as $p (.; + (.bindings |= merge($p.bindings; .name)) + ) + + # TODO: Check that there are no unbound parameters. + # Check that there are no unreferenced parameters. + # Implement a way to reference parameter bindings from policy + # commands. + + # Index policies by name as prep for the policy reference resolution step + | reduce policies as $p (.; + (.policyDefs |= + merge_unique([{name:$p.name,policyDef:$p.policyDef}]; + .name; + "Policy name \(.) is not unique")) + ) + + # Resolve policy references + | fix_refs + + # The main policy, with policy references resolved, _is_ the merged policy + | .policyDef = .policyDefs[$main_policy] + + # Delete the temporary index of policies + # (XXX should just have used a local jq $binding for the index) + | del(.policyDefs) + ; + +# Post-order traversal of policies for generating execution traces. +# +# The callback `trace` does the tracing of commands. +# +# Ultimately, when executing a policy to get access to some TPM object(s) and +# TPM command(s), the user/caller must specify a path through alternations (if +# there are any) to execute. The `trace` callback gets the metadata needed to +# do exactly this. +# +# `trace` gets an object as input with a `path` key and a `policy` key +# containing a command to execute (XXX rename to `command` then). +# +# The path is the path of TPM2_PolicyOr() alternations taken through the +# policy's AST to get to the TPM command it's given. This means the `trace` +# callback can emit a trace of commands where each is associated with a policy +# or trial session according to whether we're executing the whole policy in a +# trial session or according to the desired path through the PolicyOr AST. +# +# I.e., the caller must provide a `trace` expression that can discriminate +# based on the path taken to get to each traced command, and this can be used +# to add TPM2_StartAuthSession() commands -or select a policy session instead +# of a trial session- as needed. +# +# See examples below. +def postTraversePolicyDef(trace): + def policyAuthorizeNV: [.,{command:"policyAuthorizeNV"}]; + def policyAuthorize: [.,{command:"policyAuthorize"}]; + def policyOr: [.,{command:"PolicyOr"}]; + def traverse($path): + # XXX This is because sometimes we end up having policyDef as an + # object, and sometimes as an array. + # FIXME Make it consistent. + def getPolicyDef: + typecase("object"; + cond(has("command"); .; + has("policyDef"); .policyDef; + "Object doesn't resemble a policyDef"); + "array"; cond(length>0; .; + "Zero-length policyDef!"|error); + "Expected policy as array or object"|error); + + getPolicyDef # See above + | checkPolicyHole # Check that at most the first command is a "hole" + | typecase("object"; .; + "array"; .[]; + "Not a policy fragment \(.)"|error) + | check((.command|type)=="string"; "Not a policy fragment \(.)") + | if .command == "PolicyOr" + then + [ + range(length) as $i + | .policyDef[$i] + | [traverse($path + [$i])] + ] + | policyOr + elif .command == "PolicyAuthorize" + then + [ + .policyDef + | traverse($path) + ] + | policyAuthorize + elif .command == "PolicyAuthorizeNV" + then + [ + .policyDef + | traverse($path) + ] + | policyAuthorizeNV + # XXX "And" is a crutch; remove. + elif .command == "And" + then [.policyDef|traverse($path)] + else + # XXX Implement all the comamnds here. Check their arguments and + # use bindings to supply values for any parameter references. + . + end + | {path:$path,policy:.} + | trace + ; + + traverse([]) +; + +# Output some test toy policies +def testPolicies: + { name:"first", + bindings:[ + { name:"attest_signer", + type:"PK", + encoding:"PEM", + value:"foo" + }, + { name:"policy_authority_signer", + type:"PK", + encoding:"PEM", + value:"bar" + } + ], + params:[], + policyDef:[{command:"PolicySigned",x:2}]}, + { name:"second", + bindings:[], + params:[ + { name:"attest_signer", + type:"PK", + encoding:"PEM" + } + ], + policyDef:[{command:"PolicySecret",x:1},{command:"PolicyPCR",y:2}]}, + { name:"third", + bindings:[], + params:[ + { name:"policy_authority_signer", + type:"PK", + encoding:"PEM" + } + ], + policyDef:[{command:"PolicySecret",x:2},{command:"PolicyPCR",y:3}]}, + { name:"main", + bindings:[], + params:[], + policyDef:[ + { command:"PolicyOr", + policyDef:[ + { command:"And", + policyDef:"first" }, + { command:"PolicyOr", + policyDef:[ + { command:"And", + policyDef:"second" + }, + { command:"And", + policyDef:"third" + } + ] + } + ] + } + ] + }; + + + # Merge the test policies + # XXX This is just a demo. A main program is needed that implements the + # sub-commands mentioned above. + # TODO: Implement a main program that implements the desired sub-commands for + # listing missing policies to fetch, for merging policies, and for + # tracing execution of policies. + # TODO: Implement a shell script around this program that actually executes + # policy command traces produced by this program. + "main" +| mergePolicies(testPolicies) +| ( + # Show the merged policy + ( + debug("Merged policy") + | . + ), + + # Show post-order traversal trace of the merged policy + ( + debug("Post-traversal of policy") + | .policyDef + | postTraversePolicyDef(.) + ), + + # Show post-order traversal of the merged policy using only one path + # through the PolicyOr tree + ( + debug("Post-traversal of policy using path [0]") + | .policyDef + | postTraversePolicyDef(cond(.path|debug|isPrefix([0]); .; debug("Pruning path \(.path) as it's not a prefix of [0]")|empty)) + ), + + # Show post-order traversal of the merged policy using only another path + # through the PolicyOr tree + ( + debug("Post-traversal of policy using path [1,0]") + | .policyDef + | postTraversePolicyDef(cond(.path|isPrefix([1,0]); .; debug("Pruning path \(.path) as it's not a prefix of [1,0]")|empty)) + ), + + # Show post-order traversal of the merged policy using only yet another + # path through the PolicyOr tree + ( + debug("Post-traversal of policy using path [1,1]") + | .policyDef + | postTraversePolicyDef(cond(.path|isPrefix([1,1]); .; debug("Pruning path \(.path) as it's not a prefix of [1,1]")|empty)) + ) + )