From d99d2e38e554db0cd0156c6587d9cc38ae8f3b4c Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Mon, 4 Mar 2019 12:48:34 -0700 Subject: [PATCH 01/17] WIP: support timeserver key rotation through root metadata handling fast-forward attacks that hijack the timeserver key to push current time to some value in the future, expiring all metadata and preventing update. Signed-off-by: Sebastien Awwad --- uptane/clients/primary.py | 54 +++++++++++++++++++++++++++- uptane/clients/secondary.py | 71 ++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index d502985..186f817 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -341,7 +341,59 @@ def refresh_toplevel_metadata_from_repositories(self): Uptane Implementation Specification, section 8.3.2 (Full Verification of Metadata). """ - self.updater.refresh() + + # In order to provide Timeserver fast-forward attack protection, we do more + # than simply calling updater.refresh(). Instead, we: + # 1. Make note of the Timeserver key listed in the root metadata currently + # trusted by this client. + # 2. Attempt updater.refresh() + # 3. If refresh() failed (preferably only do this if it failed due to + # expired metadata), check to see if the Timeserver key listed in the + # root metadata NOW currently trusted is the same as before. If it is + # not, reset the clock and try to refresh() one more time. + # 4. Else if refresh() succeeded, check to see if the Timeserver key + # listed in the root metadata NOW currently trusted is the same as + # before. If it is not, reset the clock. Don't bother calling + # refresh() again, though. + + + # Make note of the currently-trusted Timeserver key. + current_trusted_timeserver_key = \ + self.updater.metadata['current']['root']['roles']['timeserver'] + + try: + self.updater.refresh() + + except (tuf.NoWorkingMirrorError, tuf.ExpiredMetadataError): + # TODO: <~> In the except line above, see if it's sufficient to only + # catch ExpiredMetadataError here. (When do we get + # ExpiredMetadataError instead of NoWorkingMirrorError? + # Do we need to comb through the component errors in the + # NoWorkingMirrorErrors looking for ExpiredMetadataError?) + + new_trusted_timeserver_key = \ + self.updater.metadata['current']['root']['roles']['timeserver'] + + if current_trusted_timeserver_key != new_trusted_timeserver_key: + self.reset_clock() + self.updater.refresh() + + else: + new_trusted_timeserver_key = \ + self.updater.metadata['current']['root']['roles']['timeserver'] + + if current_trusted_timeserver_key != new_trusted_timeserver_key: + self.reset_clock() + + + + + + def reset_clock(self): + '''Reset the clock to epoch and discard old timeserver attestations.''' + tuf.conf.CLOCK_OVERRIDE = 0 + self.all_valid_timeserver_times = [time.gmtime(0)] + self.all_valid_timeserver_attestations = [] diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index c5accb0..c6af8d8 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -470,6 +470,75 @@ def update_time(self, timeserver_attestation): + def refresh_toplevel_metadata_from_repositories(self): + """ + Refreshes client's metadata for the top-level roles: + root, targets, snapshot, and timestamp + + See tuf.client.updater.Updater.refresh() for details, or the + Uptane Implementation Specification, section 8.3.2 (Full Verification of + Metadata). + + # TODO: Handle the duplicated code! This is the same in primary.py. + """ + + # In order to provide Timeserver fast-forward attack protection, we do more + # than simply calling updater.refresh(). Instead, we: + # 1. Make note of the Timeserver key listed in the root metadata currently + # trusted by this client. + # 2. Attempt updater.refresh() + # 3. If refresh() failed (preferably only do this if it failed due to + # expired metadata), check to see if the Timeserver key listed in the + # root metadata NOW currently trusted is the same as before. If it is + # not, reset the clock and try to refresh() one more time. + # 4. Else if refresh() succeeded, check to see if the Timeserver key + # listed in the root metadata NOW currently trusted is the same as + # before. If it is not, reset the clock. Don't bother calling + # refresh() again, though. + + + # Make note of the currently-trusted Timeserver key. + current_trusted_timeserver_key = \ + self.updater.metadata['current']['root']['roles']['timeserver'] + + try: + self.updater.refresh() + + except (tuf.NoWorkingMirrorError, tuf.ExpiredMetadataError): + # TODO: <~> In the except line above, see if it's sufficient to only + # catch ExpiredMetadataError here. (When do we get + # ExpiredMetadataError instead of NoWorkingMirrorError? + # Do we need to comb through the component errors in the + # NoWorkingMirrorErrors looking for ExpiredMetadataError?) + + new_trusted_timeserver_key = \ + self.updater.metadata['current']['root']['roles']['timeserver'] + + if current_trusted_timeserver_key != new_trusted_timeserver_key: + self.reset_clock() + self.updater.refresh() + + else: + new_trusted_timeserver_key = \ + self.updater.metadata['current']['root']['roles']['timeserver'] + + if current_trusted_timeserver_key != new_trusted_timeserver_key: + self.reset_clock() + + + + + + def reset_clock(self): + '''Reset the clock to epoch and discard old timeserver attestations.''' + tuf.conf.CLOCK_OVERRIDE = 0 + self.all_valid_timeserver_times = [time.gmtime(0)] + self.all_valid_timeserver_attestations = [] + + + + + def fully_validate_metadata(self): """ Treats the unvalidated metadata obtained from the Primary (which the @@ -504,7 +573,7 @@ def fully_validate_metadata(self): """ # Refresh the top-level metadata first (all repositories). - self.updater.refresh() + self.refresh_toplevel_metadata_from_repositories() validated_targets_for_this_ecu = [] From 3bd9c6b71267c10e4e9a0818fe8789d36d995c62 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 7 Mar 2019 13:21:44 -0500 Subject: [PATCH 02/17] PR revision: retrieve timeserver key metadata correctly in clients secondary.py and primary.py. The mechanism for retrieving metadata from the TAP4-conforming multi-repository-updater is the Updater.get_metadata() call. Note that until the test metadata includes timeserver keys, the tests will still fail. Signed-off-by: Sebastien Awwad --- uptane/clients/primary.py | 12 ++++++------ uptane/clients/secondary.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index 186f817..e2bfed5 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -358,8 +358,8 @@ def refresh_toplevel_metadata_from_repositories(self): # Make note of the currently-trusted Timeserver key. - current_trusted_timeserver_key = \ - self.updater.metadata['current']['root']['roles']['timeserver'] + current_trusted_timeserver_key = self.updater.get_metadata( + self.director_repo_name, 'current')['root']['roles']['timeserver'] try: self.updater.refresh() @@ -371,16 +371,16 @@ def refresh_toplevel_metadata_from_repositories(self): # Do we need to comb through the component errors in the # NoWorkingMirrorErrors looking for ExpiredMetadataError?) - new_trusted_timeserver_key = \ - self.updater.metadata['current']['root']['roles']['timeserver'] + new_trusted_timeserver_key = self.updater.get_metadata( + self.director_repo_name, 'current')['root']['roles']['timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() self.updater.refresh() else: - new_trusted_timeserver_key = \ - self.updater.metadata['current']['root']['roles']['timeserver'] + new_trusted_timeserver_key = self.updater.get_metadata( + self.director_repo_name, 'current')['root']['roles']['timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index c6af8d8..fbd769a 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -498,8 +498,8 @@ def refresh_toplevel_metadata_from_repositories(self): # Make note of the currently-trusted Timeserver key. - current_trusted_timeserver_key = \ - self.updater.metadata['current']['root']['roles']['timeserver'] + current_trusted_timeserver_key = self.updater.get_metadata( + self.director_repo_name, 'current')['root']['roles']['timeserver'] try: self.updater.refresh() @@ -511,16 +511,16 @@ def refresh_toplevel_metadata_from_repositories(self): # Do we need to comb through the component errors in the # NoWorkingMirrorErrors looking for ExpiredMetadataError?) - new_trusted_timeserver_key = \ - self.updater.metadata['current']['root']['roles']['timeserver'] + new_trusted_timeserver_key = self.updater.get_metadata( + self.director_repo_name, 'current')['root']['roles']['timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() self.updater.refresh() else: - new_trusted_timeserver_key = \ - self.updater.metadata['current']['root']['roles']['timeserver'] + new_trusted_timeserver_key = self.updater.get_metadata( + self.director_repo_name, 'current')['root']['roles']['timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() From 2472f32a4a9f5ce760721a48a2d7ea02b9450fa5 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 19 Mar 2019 11:28:58 -0400 Subject: [PATCH 03/17] PR revision: casing on Timeserver "role" t->T to match uptuf (to match uptuf's old expectations regarding role capitalization). Signed-off-by: Sebastien Awwad --- uptane/clients/primary.py | 6 +++--- uptane/clients/secondary.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index e2bfed5..cf71ca5 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -359,7 +359,7 @@ def refresh_toplevel_metadata_from_repositories(self): # Make note of the currently-trusted Timeserver key. current_trusted_timeserver_key = self.updater.get_metadata( - self.director_repo_name, 'current')['root']['roles']['timeserver'] + self.director_repo_name, 'current')['root']['roles']['Timeserver'] try: self.updater.refresh() @@ -372,7 +372,7 @@ def refresh_toplevel_metadata_from_repositories(self): # NoWorkingMirrorErrors looking for ExpiredMetadataError?) new_trusted_timeserver_key = self.updater.get_metadata( - self.director_repo_name, 'current')['root']['roles']['timeserver'] + self.director_repo_name, 'current')['root']['roles']['Timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() @@ -380,7 +380,7 @@ def refresh_toplevel_metadata_from_repositories(self): else: new_trusted_timeserver_key = self.updater.get_metadata( - self.director_repo_name, 'current')['root']['roles']['timeserver'] + self.director_repo_name, 'current')['root']['roles']['Timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index fbd769a..3db1aaf 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -499,7 +499,7 @@ def refresh_toplevel_metadata_from_repositories(self): # Make note of the currently-trusted Timeserver key. current_trusted_timeserver_key = self.updater.get_metadata( - self.director_repo_name, 'current')['root']['roles']['timeserver'] + self.director_repo_name, 'current')['root']['roles']['Timeserver'] try: self.updater.refresh() @@ -512,7 +512,7 @@ def refresh_toplevel_metadata_from_repositories(self): # NoWorkingMirrorErrors looking for ExpiredMetadataError?) new_trusted_timeserver_key = self.updater.get_metadata( - self.director_repo_name, 'current')['root']['roles']['timeserver'] + self.director_repo_name, 'current')['root']['roles']['Timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() @@ -520,7 +520,7 @@ def refresh_toplevel_metadata_from_repositories(self): else: new_trusted_timeserver_key = self.updater.get_metadata( - self.director_repo_name, 'current')['root']['roles']['timeserver'] + self.director_repo_name, 'current')['root']['roles']['Timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: self.reset_clock() From f052b147318fe3edf688c1d9873a94429baa6895 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 19 Mar 2019 12:17:22 -0400 Subject: [PATCH 04/17] Update sample/test metadata to include Timeserver keys in root Signed-off-by: Sebastien Awwad --- .../initial_w_no_update/director_targets.der | Bin 136 -> 136 bytes .../initial_w_no_update/director_targets.json | 4 ++-- .../full_metadata_archive.zip | Bin 13900 -> 11436 bytes .../director/metadata/root.der | Bin 631 -> 631 bytes .../director/metadata/root.json | 20 ++++++++++++++++-- .../director/metadata/snapshot.der | 9 ++++---- .../director/metadata/snapshot.json | 8 +++---- .../director/metadata/targets.der | Bin 136 -> 136 bytes .../director/metadata/targets.json | 4 ++-- .../director/metadata/timestamp.der | Bin 196 -> 196 bytes .../director/metadata/timestamp.json | 6 +++--- .../imagerepo/metadata/root.der | Bin 631 -> 631 bytes .../imagerepo/metadata/root.json | 10 +++++++-- .../imagerepo/metadata/snapshot.der | Bin 217 -> 217 bytes .../imagerepo/metadata/snapshot.json | 8 +++---- .../imagerepo/metadata/targets.der | Bin 1105 -> 1105 bytes .../imagerepo/metadata/targets.json | 4 ++-- .../imagerepo/metadata/timestamp.der | Bin 196 -> 196 bytes .../imagerepo/metadata/timestamp.json | 6 +++--- .../update_to_one_ecu/director_targets.der | Bin 292 -> 292 bytes .../update_to_one_ecu/director_targets.json | 4 ++-- .../full_metadata_archive.zip | Bin 14244 -> 12188 bytes .../director/metadata/root.der | Bin 631 -> 631 bytes .../director/metadata/root.json | 20 ++++++++++++++++-- .../director/metadata/snapshot.der | 7 +++--- .../director/metadata/snapshot.json | 8 +++---- .../director/metadata/targets.der | Bin 292 -> 292 bytes .../director/metadata/targets.json | 4 ++-- .../director/metadata/timestamp.der | Bin 196 -> 196 bytes .../director/metadata/timestamp.json | 6 +++--- .../imagerepo/metadata/root.der | Bin 631 -> 631 bytes .../imagerepo/metadata/root.json | 10 +++++++-- .../imagerepo/metadata/snapshot.der | Bin 217 -> 217 bytes .../imagerepo/metadata/snapshot.json | 8 +++---- .../imagerepo/metadata/targets.der | Bin 1105 -> 1105 bytes .../imagerepo/metadata/targets.json | 4 ++-- .../imagerepo/metadata/timestamp.der | Bin 196 -> 196 bytes .../imagerepo/metadata/timestamp.json | 6 +++--- 38 files changed, 100 insertions(+), 56 deletions(-) diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.der b/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.der index d836c76b810055b6692d375312a06177282c9e4b..5c4965d46523b5ef5912e385ffdb940528348839 100644 GIT binary patch delta 87 zcmV-d0I2_n0f+$(FoA`j6@UQ&fdqg4u8ENjIzTN%#Os6UY(R?yFf}5XgkruPpgZmT tJR}T%#*0wNPAY9^EnFQ^1&rdu-Y}QGyi0SrOViFEGT7ZHf1Zp1H4FJzB)I?p delta 87 zcmV-d0I2_n0f+$(FoA`j6@UQ&fdqRep$m}?Izafgy{5BMr8+Z4yg*%i*``2PUnDB6 teD{-py)BHh$^Mgf2RDKs)r?)d;${BmV#Z diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json b/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json index 8614ec3..252e865 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json @@ -3,7 +3,7 @@ { "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", "method": "ed25519", - "sig": "263a6d873455bac478366d92edc04cac55cc0e545b5f4249cf696254263d58de6a225446ce96725bccb83ffdaab548fcd89cb790020039ef336fb422364c1800" + "sig": "ee8c2399a95889c3db401200f5607d85b510427ee0e3905fd9ee91c0159952f6786eb02ff6b46fb33373043ab2f0e5637277ea0406b229010b2d4ecb45da0508" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "targets": {}, "version": 1 } diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive.zip b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive.zip index f2fb767f2e095df0985ee8510d918fbc2adbb24b..4b1e12ad957f6b8be98764ac31ffda8d92c30576 100644 GIT binary patch literal 11436 zcmd^lbyQVd_wJz)K{}))B?P3qOS(HHr8^{~yAdgo?ha`wr9nWtTk;UnE%%()EAaW@ z`@3V@asRsOj6Kdi{F`xjThOK8W1OEQq9)NCib)^M_WEIt( z%l)+*+TCu3rglaK4%T)z{ow(zFYRR>2DPw@g8ID%T?F^`r_-eqR@7Al4K=b1Wk6^l zLV=Fwhu=`hZelxywA>Pwd#JgG&O6UFD2k6$%j0BwjZD`PE5WOf6MXrjBBZM5dxt*D zA;pHDIMr80EQ6|Hh`$SrepdifOTAY{c1AYVcO)+SLt?`F2mC)28LTtVLvI>*D*mEc zxQT3*1<>EnH~;{6DsH;n=yN-s;gJtNipZ-GpSF&np=Yn541PfC@MCZx`cG-P&_%9Z zCKD4ws$hv;OU5-Do(R-@BqgbDij#~^lxD&ct6Ls~0pbvdMsKz@$x9GpQ}rCAZ=$<; zC3TBO{FwULRPu}lO%R^=d#12UU(Rj~s^CwQ56Gci%HR8#2ndeFXuVdlRbxU;?i1%vu|z&P<5sd}R!k>zhgCcMl)iw4v4fEgvB^)m1-dRq)aSMw7%>(>v6 z`NCxOEt@(FUqYGDZf?*WbK}}Bki2|O*?G{hX5L{ zlH)>BVT}DKT*ED4G)zF6l|iD9GB1d{wVG*{(HgxW)4tvXTNp(IX11+2fc`Vk7u%r> z&ar3p%A>=DZ}-WQ=J`O>uz6)@s1X|p-%CD=ZZbd^bdf4*Sq5wye~YD5P#TYy# ziLOedwU%YowXcbdgM^bBbAGZ)OELqYs>+f@w&G?6WFGd<6D?Dp8BAdaV5Kw@WyJv6 z@-{jIWf{#N%xVwY6(Gbu2rN1{Q{q#8uwwR4&9SN9q^N~W+9sy6dVJ`B7j^>pkUdb=h0H1h~%W+Dc*&xFq+Fy5l-hoITm(Wj;s>2T#Sk=FBTCnQ*{ zA;W1C@y_qAF|CUqpTMmFD<^lYT-<8-tX7Y%RL@n0XFIc}6wEm4xh!Qq?yOD^MowfPXOs*a#7pfV{HR{xLNuVg~OjXi(Z zuN!%#Hx>ruiWGy1z4hwMCEwR@U7RlW2?Z&hat4w0S@EKMCJIra)%sSzkMtfd^1?LP zAlL}YFohW=1fhq*Ei&q2W}l~U;WE#!ImGh?WJrp~JJejwIr1>Gy2R+;16YyTm&KA; z&D)KNhgu9M&G3;?2yN{wXZOm;!5+-b-VhKOJYMj>sT%at2NKfR7Xl)Sf}R`!0s@+7I6uo42polh zt0je4mggc2e4Afq7cQ{{grAFDP}uzhMlhv+1x<9ueR;scqP zBD(pz?oO50A;|GyG=9nszY=(b^JfXLLYoNB&h`^7WryJn8gp_E9Bd*GX3boL`9kw? z;o(yR8r^zvD{*`Yr+0rv)}=lAc{%DZzHz=tw9Q)HR#E|D+Nq5 z+EING)AWdbyDkVqYgPi$XwoYzQ;MM(X=Kzgo^OeqLg_*pE_!5+%iO*R2n3jN~#N@M_I{=uOo_yx|dmTc}o zIZe{)j1uYE-QI(Cl{Q(5#;?OJlP$?tpYV5+IiTNC{E*e*h1z~IadEiPG$I6SePuI# z;Bt0qz?-TQ`&pV(HmUP!URE|FT6Gxp?V*X3GA{H1bU8Lf0a2p9e*%(*-ypkUcpjn_ z!dl1EOt=LWxyZ^`?pj?ZP8!7%k+6;Qt16)TjL9YSjOuL0xG(Ji*SC{T=9-#c*nFpV zjv;TYmk4CNEybjWL7)i&U*I_QzpeLa@8bSJSw>yO^Y}^ z;+rPqlQ#=WsW4QSFS}%Tir+k8Y}6hX><$n+AT2#Gr%Y$+!cP=jcw}FZhQv<{#|tNs z^%?g$q$5_IA(q~(_fuoW_Ok^tZ>n~3poE}sm=E2tIM;-hVNZvAAI<@Ca8^|WW z*ZofHU}|Y(@1SRC17<9G@6T!SUJy{X)*+NB;DvaX{r+Jwz~o&P!#@r4^nqeEn`;f* z`miS2nkLw-UBxlBh3AJ{WadXz|+}C&;?yp<#_X73CO9i}y;OUv#3z z5KVsl$Fkn|E!JlXS?)w9gsjpUOvo>+ zL$}5qI&@l4+9DxPiVM+hXY~h*)y^#j=!2|^8pL8m_dCap0>HMV9NlL%1E#SHjDUV= zVg)pj9sBPi8I&dxA+t`ND4#@p_2(Naob9{hNjI0juFvb)^j_MH9i5nK&)A;2;snq= z^1V=>Ume0!RSJ9iK)qs}i4`q^wvdrXt~+t$U66jTz5$K&w8>+B$a50oo#B42QOEkN z77cpxsKB}LUCoa7)Oe>KsQ0?QDvx5zuof_PL+eavW}-m2AGOnvml(8+L`h0}4T*mK z!fTEOV}(X#D5Zl+TOF70Dj}U9C_nn8rA^N*h+wTgaa!+mOnN(fq;GS~@1m(m8)&@e z!aHg-I_&7YvpElRf8LjleEEIx!RcfmX&Y%*{DqusF|yoR%2B;k{Lw+=ik*il9NPxt z_J@nQ0qKz7W0fx^K^!@&Rl=FDYOX%`B&H_fZpM-2WkE-!E#K%(A7=&%wv0JQj@oZa zBk!p4G#4qZJqjyl4}|h-$sw zsw9k&oU{Z3ot$DjH*nF}xqGq*y{{`vMC6NSr{rq-4h;Yc`RE9m-N>sN4aMh0x&RAn zi5sK9_owstwKea2AtExNQOd{r zwCVJHLn?<33OW6Hj*k-8a6y{vA3xZb-8Rx;DWM4eX zaD6USlwD)|DsK0aW9Oi-@M+DaBwDL1I+c8&X4=si33}r7(JMllX+hTJc|rUJY}UcX z%PeO7`oU)*(Fm$#7)Q$&g%YDS`Z3zkgFUS@Pd*N<qJnr+S?vqwxvVu9|momVv?cd`rn=<;xBYR}m$ zJ(HX5^SPSfYsM008)|MEuI99LfihU8k?k(ne!`A-ZDtbw-YHvz16XXMRKcODjpMcj zxQeNVWhMWF!QeXEucXp5ZNC-ye%8hdeP7+#O0csNxwhRf$v4Cnc9I%rAz84psW&C_5r6g5hF7ks0f!Y_`z%>} ztvkD=>SpHx3Agjly1;n4xpN2LW?@x|Y59zrKb6Ex8BEaQDDN@}N%DZAIt1l`N%?_d z6=)}KMyE~%vu?8t^1#SoLb9gmg~~G#>`-Pr#JVER;^xV~?5e=kF^tGVcM2f+KB4n` z2-5O~_PpiBQl7-z#Wyatc(apG-iSKkbz|=@C2db5l%dYvWxOZIKzK^An)h}X;qyqv zfLTT%&-pyZpda)MBb8=oLXceB=(jKVy z{!3NCfDRE$sw+bIu2}|)dhe)E_JiLWNRmOKhtM(uSSiP5mdc#!wH@fOp@!p9YGAo+MS%nk=*x@ z{$xGK>cYPrBDM(8Ns4*U;k2+?s~K0~nKAC7Ue^p~do*#49z@}G;mlu>QsoKBxO}Fw zYAtvL**W_2CmVjKfbmQ!IH6SCd@k1HGHy}d&}zQW+ZSJY8&&=S81)nP54q8Ts@i{u7Ehr}5j?6zxWu6V!&xlMp9*@>TD< zXL1@U;<+8bm1=#rf%M%Z&*2}p7M)cazH`; zscr|YIVcnZCzM40gI0cLlz7A_e!CtBZ5((kNX+57q#WcS8L+sK2L_rc;_sBdJ0mlF zbn8k%5mD4Wy5hm%N|nV{_n398esOU2@+(Ss`TWt@On|-!XInp#{yJh?wj!-}h?!(8 z1EnY=tOXCsaZg)>LWop!xkUf*?CObP{6_s_vsRqhLf)~`jNQIim3%qbZ-rz;&v|>< z-$(8C$9H^YDwItZ&G=p(S{apS?(C(7r^-(fH{}geu2jGwO6=&Z0IbV{-tYR*AoyTe zsOP5B1CrWz{q{oAv(Ag#B0xMu@&expg64jvxWEWEKJ)u1v z7U4IBd6^}8LnF&ifQ~NLmQ6Z3mN*Rm1fMr$Mfq;|i)JHLl=B_hY2+-1;v9q?YwjMnNG_}BJ*f(vk*)t~ z?bDR3ZEA^h?e-d<@cPa=Kn5~jaw}VzDbP9~5B)0}yT^Qgr(>X;!E(q&trr5nbI-#= za!R8gzy0ef@01p?9>}$ocV1OQWu~N9is%1O(i|=yjfxJhRLTFw*`e^*lFVWwEsLs{ zx!ia#!vL4ijeFbW^62X1$equchG3~X*S7};kS353=s)NH=*sflN90t)gi^cW#Z=S4 zPsPH6YOS@LX&gZX;^3l6Ky?gx%O_%N;Zp+mqlCU~@e?jed5P4scD*O6dHtc8g1ucp znX|x@zLuucvqo+A8@KD~uMMDMVRh#pp8oRj(m7iF` zO+HhVQJ04Q`mn%J@R03C0TzdtHGAd<@x9U2sBBMSbWuT+C4qUOz4aa=Ly5@2ih(q9 z^4F>?o^z%IdX|{P`Ww}K6MQkY3^julhNw!wyP?E0BcIeXwOy(K=EB*$4 zj%aE>VH3Y|uXt&1p_YH%;@J;Ao!i_Jwo=p*j^31$0fo$6>c1F2j;!a7mTRon{^ zyM%VpY4N!lZ0e5~bm=H-^yboV8nR++{>y+|2FW0;_^qXvDWppg^CMwN$OK|bkUPuQFWyr<1$s0@-l z2Uao|LEaDp?T7RP;w5VOu> zY&pkkN9goJmtCpHd9p`44t&asQyIzqR8IS$gLhvS2U?EoZdbYnl-2no)s7U8nmI%e?pg{Gny-w5l$QKf=74zMi&C!FCZ}W%abD?I=eti89!%MNQ6t+<} zJMm|{Ewo}xq|bN>;I&4}eAS5|-)(+xwCr#CD239GH*Rj5*=cEG*B><2w)?0z^!0!Y zU=lKbVX=5y=1Xx@K1!5)o3y+zJ`mUsqx_;qw{(a0*y=-v-DHCU)>#Qh8%FSB!}Up` zA095d2`g4b)x=p&WklvNSlXdmQGA|e1|RU>zVuPzk=fHC6nf1-YEz#&m43J$od&_s zqdMyAHZI}Dp(5JuoOYyzcG=rA)5q6K)!IgOY*DSO4gYbi#E1I*+v@kQ03ZG>TX=;4 zXTFc@l4Qfv8}E>NpIiBAs+3H4vdDj#|ISQNUcTG&1fwL$m3o-qL!)tW48i127p?b_ z6k7Gju@Y4k$fp(AGJ(PS!P|!Lu{Jwue)W?Rf)O6?y;0;dWS7+9FB*7}!p~uD^M^bT zeP0E&BvybJp$D|@_m@TOH!FbIJ65GT0)qju@c=V#fW`@pg5gC}RBa0k9g&|!Cqx@H zt2fGZ|1;d$GI-;x$FYUeD-C@|XJ6Wfr*k89r#_un?{9BRwZmc4=?L#FwGV&1JakC6 z3mIzY{(&~uAUYXNeqtx2Q5EcibiH_4pij zzq567-y4lH?kCfqa!Yjx%dY&krrYBWy5|~PJvt5-2QQ{;_LqUVuhL}wHyfnf6W6p0 z#%r}RZLep}XHV8HP27&o!`a2STlo|!;qK$&FiS3UmiQ=m2(v^v=lW0`8Ke*A!hssCDmK#?o6o~hu z0$;9i1tb7nn%pmU9<1tfAj~iFwzgd(qOWdGlhwQBK7xsFa>(R_6abr>9uc89PE zZ@3GR8f7+(;ySk;{Zn&?R1S0Xmu3;G56Upq0uX`i>$3EHx-Y4(5fz`&pCMo=(Bhb= z>=JR$&Pb%#4FmKVkiH${)Ccnd;0*W99#TD;d%H9DtzFnua4b zB4MY|Wjrx=3MM=YcorI*WeAO%rZ z$_f9JjlK^;ZrTbyroKFXVG=cF@Jo?Gss@vjuNhfSWpFNuGPIwPp$-s35I|q%oD=7x zmOz-$(axZr^z1lH=kX?$V$EQe4s%()4e7;}r%(PhF2Sh=uG3Q=iWFGKg2$p_JoSDYW0SPGDDqMLRqId6}Kb#}M}cIk0VJfL`wp7bEa zPoLE<&97>D$O0n;n&tCin7;-|$JWG;;BdF11-c1nRQPIAAa+}1S{N%eHhb%JUe4Y_ zscS-+X#*eB4k25j+B^~+g7>!YZ!AAr7PB6>rCAT5ZiHfuMv zWrZCJ190jPX?<$Z^Y||5T`B;NqF!!iw~#M?E=nz=`Y9g)@x6MxF<$2d+Sj<+pv3IH zta2REi2w`I^Wz>Ny}jmEKfX1#VC1t^S{>d9E^N7<`fcMJ z{{->>C#BOMW6q~VZ}*C!?ckoWnltaG8J@Svix9L$((ptvc!MLL>YWuQwo;2II_qw-A0PxOLcEoF96Lw8q&Sv!(P> zLTYj5k^8t|yPV4C6EY*H;4eWk-A-}GnjZG~Od{x0j0IwLvxv8Z502Nd&+0s1g5q^> z>6G~Xc>Pub|I|*Sdcn0*k&^>VpD}3xi44L60`)3NgpdsVnq)Zr8WXv%&8o*zSbP1Q zZAWz{^PeY-JU9-{FO;#}n01jm5KY3a@~!Rh+B)cpZEPnMSL6I+;!qkhWyHz#NTVdk z$@`H$ps1SQDrS`sK$cxqt|dDp)48RwzVKW5+_~$l_8A>wJSS?-#&(>yPfh8ES8~I zAIq*5nxJ^5MVOH}i|e)|-i74Ab0D)_z8~CqW@&Fj?*eQGJ-MKE8W~3%qgktAS;C0J7YB0RAG(eP^3~VS$AS z1!4VF9d|3!x9S9oP;qzIO_}=k6##(y&Ia8+_6x}IZXSD6q`e*gtFHU!S)p4M?B6^7 zcairl&V5HpZgJ3I?vvvm9I)H}0=nIE`g1q^7Dx|tTIIiA=x!LJfx^&# z!TM9M`!-5%!Ja(45A1h13rceD%5~p{f7WnkRpL=w-SX@Z=igmML^@m_! zh5fnTbqmJ*=-X<5fH!JnuWk z9ryd=F2*zV!x(G*)?DlP&AH~BYsyPOK%zkaK+VM;Q|9*fM}q;q=;+7@3d<>~GRpt4 z8_KWU3}4t888}$m{p=47z%_a-^%;AnRRq-UCFsGww?BgpgNUMzB50_QWhgT&fEXS! z(RaOZ^bL>oM<@;_l2`s^Utlt=N%V;k;0=2iw69me+$_;h(A8ff@|&v;NFLHT2}?BV z*+=Zf^=YQV|5;qzd#ATFa?mr>bI|)mZZ^Ze%T4j`1OL|+fcmQ(FD&&;jO>hTtbY-l z^tTNsxPQR^n!tGX&wtOBbNsgDl>cGm|Fb20(R?fgn)_$FRfRuL8zL*GbqEpwfFuF{ zZdKuDx1W4IP?g)P z?<+3S>nq#siy0|N7KC}Tzh4yC8(ObZeKIzBb2D+4ntZv9Wq_5&$-`Qpppp|PRfj@O z-g$a;@vut%&1bJGY}TYvw}Qe%*H9^Lksu^uAwOtj58e;MTT$)yhhjlUtNagF2vhv* zbg5=wu`Tf9Ac+Bk)tMLacu3-hZ^)fDV_IU+S`fNFfMwHmBSCbh8@`uiv7y9nWc5qM z=zb5OHv~mP04&$Cp9z)Eeb( z4bJH=54|S~D{Alclzc*-^{a_Yw@Y_*gU9 zr|jUJcXTHw z7*mX;ldUGQY(>vXbSa9=gb);+)JO)oa9+hsSSc?18IC&GH9VR&&P zKVg+ItsICw!Axh;HR-dd_F^Gz{z)li)}hG2pI`u{7qL19OT= zH#V6?ck*2_iJW6SOKUT>p4B)*V2%^yjuTThH4UCN^{xNl^9{?(8_T;2D-tFtC^;#R zjfE-7KkzZEYDk@GB{sEB=kk z^BZImkuQPX)q_fQOk-{XUIX)wGcuk#WNnLKf|2om)B7@@1u3%a>F`Z#oBtAUcdTN8 z4w;|k5%lsm(|3<)zdcRO06~_mpuUh;0Kn}r{L@ta?0;u76PXdZnb3qgvW3YLBEsnK zyl}IM)4brAY#um8j5@Co=a_B7UgZ(q6x+63_ab{ApSFs?&z-4Fa@i;MoR?y^6a=>G zg)#RhS;m@h>>%zfgfV+UZ}&*J#Y5B7+j%1yvC00lNqNdCavjI-aZs)FU?RfiG%hNr~ADaXX*!9Dd*%@U)-2nptoWTJAoO>5-XKn4kU}$9LAEhC# z4u%(a-cSRFUcy21)F7!;zpfwK0M2>c!b;VEqx$VEe0=z{5yV_bW%anhITNjKNC=x$ zivqH!+FN04Jqd+z{AMI?#CU(Fn#<@Dk7a&7j}FAX7lUSL51%zs2($RGS#fz`1U_L$ zSa>GD#`Rca4*I&3zhtwix@vGQzfsn7QihME8a-q!v_WvGOvu!+tYQ$CRD40YVs8~^qT+GRm`%(fC*on%y}v*bC+#e zm`l-zP~$?=W_Fs7(hj)vksoUp#k!G4T@DZnwDt*6_Nm;bco~RA1}fw%tutWu8QZO3 z=FD;W_MN}=OMYJB-z=!YvdtFrSL}`I;KU4eY;_yPQ7l%Z4kD#!)%BvXbqQ;rs~7f&Z(vpbHHp7V|Nuq4sqx|X~kazF|)U}>Qf#H!DEJR zJ$laOF56jeF}FMh%}wqnnvkDPe4^is`iba@mq8s z<96*0ne6^zQPz;?Vy_7&iAa6%wbp8;BD5}#Y|uo9_{`7AA3`@9A>DTjzeVU_VLY-_ z1Q^LPG!e(8%MZ$SR03Vk!`853#G=9;wLyT9VFJT;Q-|t`1<1mKnW-WD-)Z7M8cu9k z4fj^Zj()J)-TQ^f^YI|~ao;glMV1)fpq9q!FXF0xg@uJA_zLke!5EFk*01wAi=;wy3D2e7!+P4u#*sbp9$QR5az0zaM5g zL&acVeqG@f19|&fd-A>JlMJU7M%@01mimQHcGW6xV@g=@=d7Udm`!LRh$J1fYmJVR zo6Id`o5&}^iRl7-J1wWol!pi~Q5{({$jV;_=MD*0;Rq%Vmv!YfAqT@Ck3hz2>`Tsb z;o2f;RG!6tudMLw`A0^ zPP1lVDDh%sCQ&^H=1@`{SGmE*q~qk?P!{ZVn`ummLL9~yH})tNIR?56^qAd~lEJcU zdZ&Kqu6ms$3~G21(K+PFrO9xr@Gi&KFx}{s4aRex+alSb2=5hbin#QOyNvyN3)iSy z^vklg#z~o=w&}KLqC#w-cqqW$Xn8?hOf~ z%!cy;1Ity-QSkx;%SK^B@`7{FvokSru)npl-e6!kPrRUim|f7nKH%WazF=T^luYEX zU|^6$=F#(SySQE*rn$qC^qxo?JoK{GQ)=4l%ZjMxaf1UYFGCO$Z_xw}Z)gg!>qQq0)7UkJ5Oc?Y1>59B;dOP& z&}s_4SvS=gBjW z$sqjqO}RB0T2UibxXkc=M;@?(+XYb~(J@yz2SuU^@+dowo)$eV^f+i9=cY5G#N1{g z2p!2cyawCrtGz>+CBshE?T2C5Im~B;j|ng3OEFC6(Xt@2%pkj|!ZoGWX1AA-;X(v3 zArY0NEb__o;#hg<2R&x5Jcd}(IfZw?-X;d}jaKNBh-sk+msD*(L(w0jVaVNrj|MBU zJ&R|$q-tWy&CMF7hTv8v+*Ot2&ay)$q@{IG@xkB?d`MfP2m{cxgW7sJ&a7uO9G^A| zX1|bZ4Pj<3qB2O*=eDC<;7|*kj zQwaI`wa2zM*CQ|7T$RJpUuJMbUINoHxQHwZq}!efh2-|lhx8QZ62vU0$_cuo>cVro zoT@=2P<!jdFEBth;3w4# z;7|QdVuay{vGcgKx%FiOPB;5FMWG6V+ci%#K1NOdMYS*WZ3Q)o`8=#oTLrY-<_ZK< zb;xMS+|J`rv(*W6G0?#|*{AV!lfpv!L)}P;mPgDaI6QWS{TO&01$aL%OhNyhYQ!p2 z?F1mI)q|e<_3y_LZkhHZN=mjz07>MW&!63#B>a*!GmVs1-fwFHl}-p{?jbVqN_rK( zs)~e`^0h~bO8E5-O2L@m(>iONl=?E`g<|KX^Cq4*S+f54=${2%kvgO!`ei(514A)^ zg`<2bRccu|IzR(wM>oG`F*%WPPCT*T_$Us3INkG0odl%F;Qkn(Sh_SL(PyrxRNMEFee6A0UCZnjBt>2dEUNT$h7$*_1r`sQ%N!a zTO3beU9rJd!INnPT4c2X2C~ULhmJv(W2Ass>Gfs}Bkmz;>Fd*K-Al3h5SL4f#thDa z0dinl%#lWiw@TLA!`c;ZUWZD`8mOCAQMt&B4zg479@Ws|x9pA^SAQw-on>=tcxTrO zBE{`Eijgm7?SyV zIe;8XG&lSgwLZ-S%3K`PpfjF~S?N#3%wraGeOr8_$C#P0WOS6F(blct(Vi;ZLn~&M zrtGQQk*g!jhAfBq@|6^q`m8ZL=Yzjfiv{wqNFPKi8juwux?ekfZonD}4X1LbsA3|PoCt!Jas-)gwC7mW42x*jK78C=(X$!4vfDa6FQGr= za(ec;bm6}F z>Zj4o63=QAVnilF)Y)B8+r^*JUl=zW>aKYMDSxRheE zGSg6E88UVzZHZghE@0Uv+^MNmpFeJHiEHl(_S29K~jVoP1mKL?sSnF%fysKoY!u3!s-pS>^NDJ z_HeS&hE_m=^S3Xg2Fk8cEH&;r$xdm#=X|WlAH#w-t#!m@`k5g_3dkb4NMz>lY}JHP zaAQbY*@={2&4cp>&V$oubLF$Xr!nnC7D^8xh!bYs@Iob)r7nD1 zdjCxUF{mf=l`0f2aCDLcsK1lz->bJ*QozC6?{jLp96m3%KtB6|B6!b!UvFI+=l(PxsH2Q z{lfvhjr$crasMS@q{3D|c-e=+fK^|~!Z0;)wCssp!MIInLX0(|Q3um;OTPuR9A9H? z8b|Vs9k4FkpatKV9UHCyo(e^@bZ4}J)HSq8?kDT5ndgy_D@Q#x8>HT(M1i!hN*GUH z+Imsoox$KaZF-s6Lieq`jao6zFz1rkC&@k6#LZvAYf$HmeTz(qs&6Pn>?Ds2@Cv}I zFS=1ZP}?vakW`RU?{;b-L&A-6jhVo6@zpuB&7q2RkXFo>w!E2Z?eQc-;ej=QIAvd% z3@VDRJC)cFfpE)7$xdCpMn6s`crtX>Tbb&4x)XA(OLca!uxlR^jt@hn5Gs{-CKng_ zh|s|WXumaH7iS)ne$z?Ue#*@b*D!e@QW3(sHMdh-$qScn)aK)6r!pKSkCeC1Ag#v&TGfmq;2!{w~4KJ|e$1y4P z2&M(#*jM3gttv%gGOtd>pvdXy8yahQTgtEy;7?jGo-wqzI9uBn;4MridI(Cb(EBM2 zO|g^7!LFEOTFLU$jpr$emU=!xtsMW&k4MLyiF^+t$8FgE2er71`{M^ACI#RFjvkA~ zv5MC`!PtV7cOeqRsm01yGCg@SGvL>2J-OaqozYCZ zc%yBmmBuo$3 z?JqQM&mC?#Ns)JbJvURa?R0H7>uN3oqdxeoihXqVnXIz=$p>6P`xcay{GC0mmWlaZSft(P{aj|Ew;@wTrSZacV>5@onY%JNBGc(~UWF{p= zFpA)Uk3GKGM2oJSoVaLnr-Ms zd#0>IvQxskl{}Ahb^qqIQAakRZ|G)n$ryP1$5n=|^nt_$_vz5h&@$$%McOGZX2!=! zA(N2Ih9jblUo!9;G^L358Egu+ngzht^~CYrr9Rj7o%5zRb-dDlF)}L9HC=eXepwlJ zR@X@1$N8AQ)-k*_x95B);P_KL{#h$eapn1Rz;uaJzRLl9%QCUP;?6jJFHL#L!!v5p zk*25UlCr}JQ45l`e3FShdeG!jZJKzTYP%MDp5z} z7a?ocgA+0!%TZO2Z^6u_V(vno*$ES9+V1@hKei+uK1tNmxSMz`fNBI*n%-`FqbBJ( zg{?+{#H5yMAWlQHv@DkOM)0*NuW9+mo8&loVBJMMJycK*RlJZqiuLFWmU5zzNrH7$ zO8-Nm&Np)lNZB&u1{7ziO<^-9dRl@DK1~e3!vP_}=T0Z3*8sicxwD0I#G&TXNS;$u zwQsVoM;_i#60>R%8h(@A#^kZ$nCjS>B{WLed3oU*lj`}%Q{mGR)6qur)m44xHf3hV zUuos<&hbZ3*BJe1DDmvu^&c;{MBEx0Kk8+C7yNA2+8yTgo_BoPMzk;SctonsJtdpKQ^Wsp*1gjl*#3dgLdt|D^(pk@IDGad+39}4zYiJ9S&RW^X$hgNDsXP$o(TV8{*;#lcK#EKh6?Bq#oUm%3k zZ94{7{rolDJ5I1|CrEZef=F`3-N|1>V3-6lSRI1QvF^pY=2mw8RL4B4&k9w2V5J7b zvjKX~t=!}kj14|-UfDPt(;}CeMv6H2+v$63UIaqmuJAG+6VBKkpJKLN^hbr?cym$k zh6ZcKF+6>JTuOtXGb9jzdGzW%!uyJphnXia(eP2(O)JhK2{FzlUTqYZ{JU(EfL zsB7nShh-Sqn+NnbD6Ybe9-}$$;m5j{Ov=}1TL`vCdCs$Ue2OiY`iYvjB7El9D*P8X z+oT|f`_gT=x@{mW+%pWfRA(a}P?D z(>xh%Q&9#>i=yVMlrjZMA7ZBR2)J6e@CLQ;=T9tN6t@H8s+s&$=JmXofEu-xZS(8y z#$!lZ53oASb!usJB)~ONmu8N9UG1uu$)5KIH#~jpHd*>9$hZx8zS)sL^HoVI=qgBGETcs8el4SMmxBs}DSHy{I~yT8Ka?CppQolkswGCDRtCbNE zg|5{=4#ErGb6Ab(na>Jg9iJ4@+tC>+v*K99E}1<};9?l&@wufI_6mpHIwCCZMpzA< z<-CF7>q_kM&}6ddMxiUd<6bNB$~5quhbrmk)btyzE-ZFCeF-fk`TWhW2SK*50Rct^ zD_wcb=Utw^+yUNR@yN+dh}VGZ!5dH=$sbO%A2#9EbjPbSl`AvD?_a<=m}5}dqNJjb zk>QwTfe{;0auBIh!T5b~-m>uy5FKwV+_R4`h&1WHa9ds5IhYt=JXpHqJU(+cSo4L$ zWF&ccytbp}=Gu(RUG`?w-Bqi9Sw)87iCs%z3kJvhLT zq1X{$TSRVe%h{G~EAX=Ha3W&+cy{@T`{^gI14_dc4>xY-v#qHU)&j)H#g+Du$coGi zZ$4$S6PBG#8fU4t{&n@}%k9ad_RJH~gQ-2v!+2VU&7}cnetk?ix&4D!>)_E2h0mxZ z?s&-)Fs2BiGtBPL(Z>6$d4P#vm71jiva-J1#%HH$W%YF~!-t!XlkDGU??%U@;pu!; z=Vo3FKel864^woZHjb44a>mZ?7#EEuQ>2G6NV|Ke&6|v=0@m^k8?=Jcn`?-9`}HvD^}@O&Sa{bi%^f48Y7;N zP{;CoXy`l!44IBt>a|7766bUGrM~=Rt@}=m(otRS5qE8G8k; zcx{rD)h29|A8RHm_zsU}cW4CMfTsXbnl9y1>5(2%$k#kIuWaYbN78CiLvKH}D@J0d z1OtuH&`)5{YUv>i1QbdXy0eZ+R4nk^4w6#ycXczXQ-dIs81R(4Yzfd`)0z^B*Bxh) za+rpH<&DXj{3ge=)a-sTdh?0_wl$EnWvY~kIcb!N2WBkMC6u|=KH79)!WfLND;1Ci zl!!UQP1}-u9fJp7qzuz$HD4f6_Kt?vvUMVu zAIgi3ua%AK%fAT%*Alm7yTVN}1Xq#`NUrTI@G;(w@j){Ve+bW~!lF9y933?c_W2H$ zxbOCH))WkY;WhCBgjGA^tVpr>CS#@*+N8)(vj%JbG6m8AMMNH!?wqOgolktQ$-e0719KVd*^gE5IwIKksk-!73#+HN z8-WT88D(w@Z#)ert{xP4au$9jPZICSHif1!t9%Y+Qu)rHPz`4tw#t0LPh2Ach52}x zrIYI7k+gYvLATa95qzxz&&(TJk>RrY$y8$0`LvYo38Ho}-+>h{s#>41@!*aL}F?u(v+Y zod=H}+{*SNz5NGY^^f`O?=Jj*li!{Xj@?*46V@KmbSr{T^6Xm1D6cadg=8VMo%7f; zlXLv(ty_FKwI(7TQIE&_6{3K9G(}X6fyFCTxhTN z9^H9?w-(<(?JJos$Xmb7ZxjE)3;cV6J8CEbj~JBTzD5snqyYEr`s_SRqkx8OQ9vWX z&!!yA67qFLSoxgJc(9L$ul)w0pW$REd1j@ec{caLmC4wze>U?g$B?+it_&<@mRGM( zcuhhQxb8qHJy39lCLt}kS5UZImTl8dt0gqzpfKI}on!O90J^~E&FIKe4e?>VX(pRx z$4_BTguG&^$-oc1s@9_bAJJ0*BA>MJ9|{)Go$zp~rAe`U9Q;4TK^DYHxv z2a+N(+7|=+r>+2x4wEZ}9Mt%4R2jC})a&~J{!3wQQ*GbdtuW#s_58WOJ7MnK zs{4wQza!mBOZu zrlkj^B!5nO?_S(j&38Ay*1hBJ5#;^~xjUd|AbO*7vnw$A`dv49o@AsN_QZW5(+&!cQrRVOsCcB%M1awvUUznQTug&fxP`n@D z9#OvQ*Y8(RcSsW0_loi-1^fNV;tt3RbO7D6rk@uWziQZhDc(CIKTyu>9;AO$vEMVJ zcVH=?VEf;|{>~U^z{Q=@J*BxH?BCSv gcL((jO`P^WpnVta!Nc1j#GpSpkd@x1yZ!Hf03{=l5&!@I diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.der index 1e9e82aa3a929e48a6cde8d1878bf9cd199869ee..f5c5af2f5d214b02c12c186c6d2194fed932f69b 100644 GIT binary patch delta 91 zcmV-h0Hpu-1os3HFoFVepn?JcfB^u31b_aniLntT0zeiR@Q0eA1i`Hi4Xc7uMj@g3 x(a5~xEZW4@CgnD3K)r=qx>lRfqr>Ioi%A+S&Wk}MZvj#)ClUYv diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json index 790a3a4..c0b2f6c 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "e9855e5171934d56a78033cead3dc217d6df3730f9c668742346a4e66f0d1141fe7283a21964e0c35163e76b6103e36a04d44f1b0799fe34af45c65f32f38b09" + "sig": "a976e1b7b5ff03505a709ad3f0af13ad1ab0683752e9dced52735d2fb0d3086292c78b00472c1b59e4e3b5a90d42ad53be419de069aa8c6b176d202f96621c06" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "keys": { "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6": { "keyid_hash_algorithms": [ @@ -24,6 +24,16 @@ "public": "99ef8790687ca252c4677a80a34e401efb7e17ccdf9b0fcb5f1bc3260c432cb9" } }, + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "5d7750b208dfc7ade8f6106b9c3fa25162d5a184f302161e429f19a79e66a908" + } + }, "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc": { "keyid_hash_algorithms": [ "sha256", @@ -56,6 +66,12 @@ } }, "roles": { + "Timeserver": { + "keyids": [ + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" + ], + "threshold": 1 + }, "root": { "keyids": [ "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2" diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der index ff0488e..58bff46 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der @@ -1,4 +1,5 @@ -0֠f{' XV0 targets.der=root.derw'0% - k8} -"Lc~rI9A9i0g <=3_6T`GࡍY^SkVx -@*Ed5[{%UYQ)'gH\ǃL8 s|jz&j \ No newline at end of file +0֠fXV0 targets.der=root.derw'0% + өq#Q;rJޕRﻁi0g <=3_6T`GࡍY^SkVx +@(ƄR+C ++Octp +jyD?A{_cNzRÛ0\ \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json index 7623663..6bdb60f 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json @@ -3,18 +3,18 @@ { "keyid": "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97", "method": "ed25519", - "sig": "7606de7a3cb4d899755d8fe085f8834a356ca81b65814e89aa5062faa78360ff12b9977825fa9e14baf3daf63c4da2c354ec0fe233a98d3aac63d5758caab60e" + "sig": "ac9265b8022216662d9041696260caa82a1b4a200b0ecd6f98a31a4a81bc883162462e9ea32ea30cd1ae332aff7f239414ca55923bb9a83a88402b028acf5700" } ], "signed": { "_type": "Snapshot", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "meta": { "root.json": { "hashes": { - "sha256": "2a6db46564a0fbc905bf7a36eddc172f1ce3a52871f18f2594e5160b2321e62d" + "sha256": "7098bc863d8f04e1305ed28aa08b35d05be0f507e6b3d52a606cc6bf6e871006" }, - "length": 2120, + "length": 2259, "version": 1 }, "targets.json": { diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.der index d836c76b810055b6692d375312a06177282c9e4b..5c4965d46523b5ef5912e385ffdb940528348839 100644 GIT binary patch delta 87 zcmV-d0I2_n0f+$(FoA`j6@UQ&fdqg4u8ENjIzTN%#Os6UY(R?yFf}5XgkruPpgZmT tJR}T%#*0wNPAY9^EnFQ^1&rdu-Y}QGyi0SrOViFEGT7ZHf1Zp1H4FJzB)I?p delta 87 zcmV-d0I2_n0f+$(FoA`j6@UQ&fdqRep$m}?Izafgy{5BMr8+Z4yg*%i*``2PUnDB6 teD{-py)BHh$^Mgf2RDKs)r?)d;${BmV#Z diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json index 8614ec3..252e865 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", "method": "ed25519", - "sig": "263a6d873455bac478366d92edc04cac55cc0e545b5f4249cf696254263d58de6a225446ce96725bccb83ffdaab548fcd89cb790020039ef336fb422364c1800" + "sig": "ee8c2399a95889c3db401200f5607d85b510427ee0e3905fd9ee91c0159952f6786eb02ff6b46fb33373043ab2f0e5637277ea0406b229010b2d4ecb45da0508" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "targets": {}, "version": 1 } diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.der index e1e1df6c43c727752ee1ca2462ec51d3c3fb707c..17233db0f0e38b031edf6372a4c4a6e578b5ccb4 100644 GIT binary patch delta 123 zcmV->0EGX<0mK0gFoD6KQGfvhfdqg4u8ENjC?Fj8jDLayHfr9(jO?7}M#G>&$z8QM z(g~FlsuelmtKyMXEj{&LpT?*yn dDbEd(d-|z@F6TUp()g;cz)C@{8Ue*}ZU!deJdywa delta 123 zcmV->0EGX<0mK0gFoD6KQGfvhfdqRep$m}?C?HmdONk1H&O_R~_6d>U)LhCqQ9wwy z{{US8<9R=-eS(ozEoK=5SzoTD6G!ai1c&JM_TG(a`Hw9##IC=m8 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json index b08013e..2625519 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc", "method": "ed25519", - "sig": "3500dd4af7002c2a7d36c1a4b546b87979805a5c31f6038b39ca19d5d61868b5d3cc8c96c93eb44c3ed862859f45739e926626b73b7beb2d37ceeb83b32c8b0c" + "sig": "dbe5e8c1075e68b888eaa0d85cba9f97610980cf634110e0dcc558eb325f28de5129fb92d807a59a18f94637250a22c4ff76e065f9863daaad818683e5122e0d" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "114817f21566942dbcdafe97e3d1aca1660db86d8920a872c65b76d9890409de" + "sha256": "d3bc22745f0ad4c6f12df987346a3f5a29f024d899c16d23f25adb401d618cfa" }, "length": 594, "version": 1 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.der index 6fea55fcce628c6a57f51fe4856f29e0aae645a0..5c932fb18debbe442545aea5f71502aac26e0eee 100644 GIT binary patch delta 91 zcmV-h0Hpu-1os3HFoFVepn?JcfB^u31b_anf3XoJ0zhM7htlajzZKQ%PzIY0EGY90oef#FoD*fW`F?#fdqg4u78mZJs>Cs%ugHtr12Rs3?VL1^s zafdq)i;?G_!Ay~BE9T+^a_*x6R$s0Dqeh9&YXk^Ny8wI|67cIN1OI delta 123 zcmV->0EGY90oef#FoD*fW`F?#fdqRep#PB$Js|sue45Pf*1?3KN9uWO3@Y2QfJ`%P z{4x;*Sa!$EO;eF;EBS&3o0 dpcI`c&@&(dyj!(w6d33d#TARTWfDh}(qkjVd_Yhk#b)4c{OstN;K2 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json index 9275521..ee1c00e 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596", "method": "ed25519", - "sig": "aa5c4bd41dbb8bcf5fdc6fd44ec6b42998488fb1849cc9634e58e514dc5f00f7a59e75216db325f07bfdaba72558a9a4dcfe694f76c8e9af2980477a2893b208" + "sig": "6c300b24ffb94f9d1d3465e3e9a877a359c5e5316e3edfd86154eeee08503e111fa23c8b7ca8af2af74747e701272999e61b4930b98a15a41e4a6d20e781490f" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "targets": { "/BCU1.0.txt": { "hashes": { diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.der index 78e17e843069c81f71a370b9e5d0cea60d5a4fbd..93282aba6be7ee35159657aec9c72044e82002e6 100644 GIT binary patch delta 123 zcmV->0EGX<0mK0gFoD6KQGfvhfdqg4u78mZC?JStIwq`=@vQ`M2PQUaH1eQi`ovKh zm+eV1+Szr;7W|P`ELhSUvt*MLr4d*()lk8MrN=)J04t&>S#ZXq& dA0%Z1iNc6Z$#$0ZTYht?Ger~HGO9>&-3);DIp_cY delta 123 zcmV->0EGX<0mK0gFoD6KQGfvhfdqRep#PB$C?ML2ljP&}Lte{Ve5nIef5F}suC-*& z1TrIW*?#M4Pj!)2ECpjQ(>1?vN*X$8BPNnigg6w)>kX@lIpzQW diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json index 1a34ad6..eeeaea0 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a", "method": "ed25519", - "sig": "8aa14b371d10f81a5f4232967d682ef254f5a92221b5cd224811e053d5a8bd99cc1298984c284580684a958eec2f40a6eaf0d8756bd950e5b3714736befda805" + "sig": "eb505991b63a7dddd153edd4e1a1ee00d46caefbfaf39ec85cf0b96c681f8f33b25666bb101b7f0e7a614dbd8c9eaa97986f5cb5e2873cc1213cafa2d1f59e0c" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "d37a3f9b41bd7ad4c87bc978f0341cbeb081bdee0891f087f33d34963c565e16" + "sha256": "d88bd443c813a54a029527f70e8dc8c7266c20a02c1a07d2d3227c51c7f5c669" }, "length": 594, "version": 1 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.der index 09869c3ee50cdeb5a880d5f1f87a3d71cc4ba34c..5d7ac88775e85579e8136ce1d7c9b1972f768b14 100644 GIT binary patch delta 89 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 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json index 790a3a4..c0b2f6c 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "e9855e5171934d56a78033cead3dc217d6df3730f9c668742346a4e66f0d1141fe7283a21964e0c35163e76b6103e36a04d44f1b0799fe34af45c65f32f38b09" + "sig": "a976e1b7b5ff03505a709ad3f0af13ad1ab0683752e9dced52735d2fb0d3086292c78b00472c1b59e4e3b5a90d42ad53be419de069aa8c6b176d202f96621c06" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "keys": { "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6": { "keyid_hash_algorithms": [ @@ -24,6 +24,16 @@ "public": "99ef8790687ca252c4677a80a34e401efb7e17ccdf9b0fcb5f1bc3260c432cb9" } }, + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "5d7750b208dfc7ade8f6106b9c3fa25162d5a184f302161e429f19a79e66a908" + } + }, "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc": { "keyid_hash_algorithms": [ "sha256", @@ -56,6 +66,12 @@ } }, "roles": { + "Timeserver": { + "keyids": [ + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" + ], + "threshold": 1 + }, "root": { "keyids": [ "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2" diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der index c5a2945..958d6ea 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der @@ -1,4 +1,3 @@ -0֠f{' XV0 targets.der=root.derw'0% - k8} -"Lc~rI9A9i0g <=3_6T`GࡍY^SkVx -@dK-D5rmՆԋ ղfAtͫE@6y% -!^ɂ \ No newline at end of file +0֠fXV0 targets.der=root.derw'0% + өq#Q;rJޕRﻁi0g <=3_6T`GࡍY^SkVx +@M&~qm%L|*E?Кd,bg*KE/W鍲o*hJ Ǻ \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json index 49b5157..f073cdf 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json @@ -3,18 +3,18 @@ { "keyid": "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97", "method": "ed25519", - "sig": "701d882716b54c0953d6811e0d4e5cfe788d75f4d0c425af9b2ec9bd8e3abec3f2ac707575680601a1b983b11aa290ec7add7070734a6a05b2a1117a9a31320c" + "sig": "3432f7fdfa51c050929c1eefc9812ed7dc18b5dcc1fd318c48b0a8cc4654b74905cbbbee947ea6bd8e7aabd06df9cdabc4791a2e0201e69a5f8cb7a072676b0d" } ], "signed": { "_type": "Snapshot", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "meta": { "root.json": { "hashes": { - "sha256": "2a6db46564a0fbc905bf7a36eddc172f1ce3a52871f18f2594e5160b2321e62d" + "sha256": "7098bc863d8f04e1305ed28aa08b35d05be0f507e6b3d52a606cc6bf6e871006" }, - "length": 2120, + "length": 2259, "version": 1 }, "targets.json": { diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.der index 09869c3ee50cdeb5a880d5f1f87a3d71cc4ba34c..5d7ac88775e85579e8136ce1d7c9b1972f768b14 100644 GIT binary patch delta 89 zcmV-f0H*(>0;B>EFoFRfpnSb0U;uE+g;L0;B>EFoFRfpn0EGX<0mK0gFoD6KQGfvhfdqg4u8ENjC?H0xhx`7E2T%30DB$mAt!U%@+!0Tr z>aNhqoxTF*!W)rRE?x3GAMSKIsylU|(qz3S`q5nrJpvk13lANbka0EGX<0mK0gFoD6KQGfvhfdqRep$m}?C?GKUojuOVkgI*cB2T1TG*^ViBi*YF zmB3dB@(+Is)sm4`E0EGY90oef#FoD*fW`F?#fdqg4u78mZJs>Cs%ugHtr12Rs3?VL1^s zafdq)i;?G_!Ay~BE9T+^a_*x6R$s0Dqeh9&YXk^Ny8wI|67cIN1OI delta 123 zcmV->0EGY90oef#FoD*fW`F?#fdqRep#PB$Js|sue45Pf*1?3KN9uWO3@Y2QfJ`%P z{4x;*Sa!$EO;eF;EBS&3o0 dpcI`c&@&(dyj!(w6d33d#TARTWfDh}(qkjVd_Yhk#b)4c{OstN;K2 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json index 9275521..ee1c00e 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596", "method": "ed25519", - "sig": "aa5c4bd41dbb8bcf5fdc6fd44ec6b42998488fb1849cc9634e58e514dc5f00f7a59e75216db325f07bfdaba72558a9a4dcfe694f76c8e9af2980477a2893b208" + "sig": "6c300b24ffb94f9d1d3465e3e9a877a359c5e5316e3edfd86154eeee08503e111fa23c8b7ca8af2af74747e701272999e61b4930b98a15a41e4a6d20e781490f" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "targets": { "/BCU1.0.txt": { "hashes": { diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.der index 78e17e843069c81f71a370b9e5d0cea60d5a4fbd..93282aba6be7ee35159657aec9c72044e82002e6 100644 GIT binary patch delta 123 zcmV->0EGX<0mK0gFoD6KQGfvhfdqg4u78mZC?JStIwq`=@vQ`M2PQUaH1eQi`ovKh zm+eV1+Szr;7W|P`ELhSUvt*MLr4d*()lk8MrN=)J04t&>S#ZXq& dA0%Z1iNc6Z$#$0ZTYht?Ger~HGO9>&-3);DIp_cY delta 123 zcmV->0EGX<0mK0gFoD6KQGfvhfdqRep#PB$C?ML2ljP&}Lte{Ve5nIef5F}suC-*& z1TrIW*?#M4Pj!)2ECpjQ(>1?vN*X$8BPNnigg6w)>kX@lIpzQW diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json index 1a34ad6..eeeaea0 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a", "method": "ed25519", - "sig": "8aa14b371d10f81a5f4232967d682ef254f5a92221b5cd224811e053d5a8bd99cc1298984c284580684a958eec2f40a6eaf0d8756bd950e5b3714736befda805" + "sig": "eb505991b63a7dddd153edd4e1a1ee00d46caefbfaf39ec85cf0b96c681f8f33b25666bb101b7f0e7a614dbd8c9eaa97986f5cb5e2873cc1213cafa2d1f59e0c" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "d37a3f9b41bd7ad4c87bc978f0341cbeb081bdee0891f087f33d34963c565e16" + "sha256": "d88bd443c813a54a029527f70e8dc8c7266c20a02c1a07d2d3227c51c7f5c669" }, "length": 594, "version": 1 From 4595e09a1561bdd816af9811ddabec9453ad2d04 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 19 Mar 2019 16:49:13 -0400 Subject: [PATCH 05/17] Update more test metadata to include Timeserver keys in root Signed-off-by: Sebastien Awwad --- tests/test_data/director_metadata/root.json | 10 ++++++++-- tests/test_data/image_repo_metadata/root.json | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_data/director_metadata/root.json b/tests/test_data/director_metadata/root.json index 790a3a4..09f90f6 100644 --- a/tests/test_data/director_metadata/root.json +++ b/tests/test_data/director_metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "e9855e5171934d56a78033cead3dc217d6df3730f9c668742346a4e66f0d1141fe7283a21964e0c35163e76b6103e36a04d44f1b0799fe34af45c65f32f38b09" + "sig": "a976e1b7b5ff03505a709ad3f0af13ad1ab0683752e9dced52735d2fb0d3086292c78b00472c1b59e4e3b5a90d42ad53be419de069aa8c6b176d202f96621c06" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:19Z", "keys": { "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6": { "keyid_hash_algorithms": [ @@ -56,6 +56,12 @@ } }, "roles": { + "Timeserver": { + "keyids": [ + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" + ], + "threshold": 1 + }, "root": { "keyids": [ "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2" diff --git a/tests/test_data/image_repo_metadata/root.json b/tests/test_data/image_repo_metadata/root.json index cd2cd95..27b7138 100644 --- a/tests/test_data/image_repo_metadata/root.json +++ b/tests/test_data/image_repo_metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", "method": "ed25519", - "sig": "bdbbbfa6b75e98bbc5f04549f1b181e39cc7c8ad8bd86a41c061825e01ed5d2cf1a294cb45f7441cb15fe0302264a98c7813af699c12ba25ccee08878436c20b" + "sig": "b8dcfed4e1ab955968b3d94eb173a1f6440dc2bd8fe5dcde10fd1c10aa1feeb44c306e3f2375ecd859267307959ffdb549108e8598e39d150f8e1ffecb15860e" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "keys": { "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a": { "keyid_hash_algorithms": [ @@ -56,6 +56,12 @@ } }, "roles": { + "Timeserver": { + "keyids": [ + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" + ], + "threshold": 1 + }, "root": { "keyids": [ "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290" From 99b9be94aef2255d2a842ec7e38dbcccd4f22c83 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 19 Mar 2019 16:49:48 -0400 Subject: [PATCH 06/17] PR revision: bugfix refresh failure case if no timeserver rotation The prior modifications in this PR caused a failure case: an was missing. If an error occurred obtaining verified metadata, and there was no timeserver key rotation, we weren't raising the error. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 3db1aaf..a112b93 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -515,9 +515,25 @@ def refresh_toplevel_metadata_from_repositories(self): self.director_repo_name, 'current')['root']['roles']['Timeserver'] if current_trusted_timeserver_key != new_trusted_timeserver_key: + # TODO: Consider another, more invasive way to accomplish this (within + # root chain verification, after switch to theupdateframework/tuf) + # because there's a corner case here that isn't addressed: + # Suppose in root version X you change the Timeserver key after a + # fast-forward attack, then later in root version Y, change it + # back because you decide the key was not exposed or something.... + # If a client goes from root version X-1 to root version Y within + # this update cycle (it would root chain within the refresh + # call), then we won't notice here that the key ever changed, + # and we won't resolve the fast-forward attack. Detection should + # occur at a lower level, in every root chain link step. + # This will do for now, but fix the corner case by moving this + # check. self.reset_clock() self.updater.refresh() + else: + raise + else: new_trusted_timeserver_key = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] From 93695ff18c303adecdf59177794b87b4b516e998 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 19 Mar 2019 17:28:12 -0400 Subject: [PATCH 07/17] Add sample/test data with the timeserver key rotated in JSON only. samples/timeserver_key_rotated has metadata that is updated from samples/initial_w_no_update, with later version of Root, Snapshot, and Timestamp, that allow a full verification Primary or Secondary to verify a new Timeserver key. For use with upcoming testing. Signed-off-by: Sebastien Awwad --- .../director_targets.json | 19 ++++ .../full_metadata_archive.zip | Bin 0 -> 10022 bytes .../director/metadata/root.json | 92 ++++++++++++++++++ .../director/metadata/snapshot.json | 26 +++++ .../director/metadata/targets.json | 19 ++++ .../director/metadata/timestamp.json | 23 +++++ .../imagerepo/metadata/root.json | 92 ++++++++++++++++++ .../imagerepo/metadata/snapshot.json | 26 +++++ .../imagerepo/metadata/targets.json | 69 +++++++++++++ .../imagerepo/metadata/timestamp.json | 23 +++++ 10 files changed, 389 insertions(+) create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/director_targets.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive.zip create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/root.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/snapshot.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/targets.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/timestamp.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/snapshot.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/targets.json create mode 100644 samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/timestamp.json diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/director_targets.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/director_targets.json new file mode 100644 index 0000000..252e865 --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/director_targets.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "ee8c2399a95889c3db401200f5607d85b510427ee0e3905fd9ee91c0159952f6786eb02ff6b46fb33373043ab2f0e5637277ea0406b229010b2d4ecb45da0508" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2038-01-18T03:14:19Z", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive.zip b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..e631b5c72fcdc02ab36857faf48a75625fca611e GIT binary patch literal 10022 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 literal 0 HcmV?d00001 diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/root.json new file mode 100644 index 0000000..1acc7af --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/root.json @@ -0,0 +1,92 @@ +{ + "signatures": [ + { + "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", + "method": "ed25519", + "sig": "b7812100db40cf8348cb7643786f5b4a77173f8586cca219baef5e6f4af4d459e3eb01b33636ecee51ec02c142d52be409b3ae1c372145a1341750f8ed4d730e" + } + ], + "signed": { + "_type": "Root", + "compression_algorithms": [ + "gz" + ], + "consistent_snapshot": false, + "expires": "2038-01-18T03:14:19Z", + "keys": { + "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "99ef8790687ca252c4677a80a34e401efb7e17ccdf9b0fcb5f1bc3260c432cb9" + } + }, + "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "d1ab5126fd6f0e30944910e81c0448044dfe9d5a39f478212b2afa913bb7ca7c" + } + }, + "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "228342cc8b78a65b8840ef5691a693d8c368e053a7e8e8f85faf7c83eff1e1d2" + } + }, + "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "f3b4c231520580eca92e17ae1581a708f606f72d43cc200af493afeec22a5e79" + } + } + }, + "roles": { + "Timeserver": { + "keyids": [ + "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc" + ], + "threshold": 1 + }, + "root": { + "keyids": [ + "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc" + ], + "threshold": 1 + } + }, + "version": 2 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/snapshot.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/snapshot.json new file mode 100644 index 0000000..58de307 --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/snapshot.json @@ -0,0 +1,26 @@ +{ + "signatures": [ + { + "keyid": "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97", + "method": "ed25519", + "sig": "48264bae99b07b6c2cc3a9112264cd0f2d2e4cee94e1ab7a3160ca579c0aab13452b8a70b43b3b5a746d1d34723695aaace087e99c84d5ad35d2fd1d658e1902" + } + ], + "signed": { + "_type": "Snapshot", + "expires": "2038-01-18T03:14:19Z", + "meta": { + "root.json": { + "hashes": { + "sha256": "072863a1e81e4dc4b8d19b23cfecf742ee15a7d5afc06a03dd45f63fec81849c" + }, + "length": 2259, + "version": 2 + }, + "targets.json": { + "version": 1 + } + }, + "version": 2 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/targets.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/targets.json new file mode 100644 index 0000000..252e865 --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/targets.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "ee8c2399a95889c3db401200f5607d85b510427ee0e3905fd9ee91c0159952f6786eb02ff6b46fb33373043ab2f0e5637277ea0406b229010b2d4ecb45da0508" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2038-01-18T03:14:19Z", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/timestamp.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/timestamp.json new file mode 100644 index 0000000..8dbe9fe --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/director/metadata/timestamp.json @@ -0,0 +1,23 @@ +{ + "signatures": [ + { + "keyid": "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc", + "method": "ed25519", + "sig": "1c82207c253296522647abaddc3b0555945ef91833ac0e66fd79227d4c36465f3e616e00906ef9b4516bbff32bbb43d7b7d9dc0e2ff55d70401ce6480af0c700" + } + ], + "signed": { + "_type": "Timestamp", + "expires": "2038-01-18T03:14:19Z", + "meta": { + "snapshot.json": { + "hashes": { + "sha256": "b1a604f49312e9fc498c28cda12b794f8874c9069174916e0bad7ea360d2bbec" + }, + "length": 594, + "version": 2 + } + }, + "version": 2 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json new file mode 100644 index 0000000..27b7138 --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json @@ -0,0 +1,92 @@ +{ + "signatures": [ + { + "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", + "method": "ed25519", + "sig": "b8dcfed4e1ab955968b3d94eb173a1f6440dc2bd8fe5dcde10fd1c10aa1feeb44c306e3f2375ecd859267307959ffdb549108e8598e39d150f8e1ffecb15860e" + } + ], + "signed": { + "_type": "Root", + "compression_algorithms": [ + "gz" + ], + "consistent_snapshot": false, + "expires": "2038-01-18T03:14:07Z", + "keys": { + "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "97c1112bbd9047b1fdb50dd638bfed6d0639e0dff2c1443f5593fea40e30f654" + } + }, + "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "f4ac8d95cfdf65a4ccaee072ba5a48e8ad6a0c30be6ffd525aec6bc078211033" + } + }, + "aaf05f8d054f8068bf6cb46beed7c824e2560802df462fc8681677586582ca99": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "497f62d80e5b892718da8788bb549bcf8369a1460ec23d6d67d0ca099a8e8f83" + } + }, + "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "729d9cb5f74688ef8e9a22fae1516f33ff98c7910b64bf3b66e6cfc51559840e" + } + } + }, + "roles": { + "Timeserver": { + "keyids": [ + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" + ], + "threshold": 1 + }, + "root": { + "keyids": [ + "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "aaf05f8d054f8068bf6cb46beed7c824e2560802df462fc8681677586582ca99" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a" + ], + "threshold": 1 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/snapshot.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/snapshot.json new file mode 100644 index 0000000..fb412ae --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/snapshot.json @@ -0,0 +1,26 @@ +{ + "signatures": [ + { + "keyid": "aaf05f8d054f8068bf6cb46beed7c824e2560802df462fc8681677586582ca99", + "method": "ed25519", + "sig": "3fd1bdc6b6f70fca7063a39a063da63f13bd84388e374c566732faad4988cf1028cd8c02668acf08a22bc6c955a6e1467df9bea46328873e0b4cd8529746c108" + } + ], + "signed": { + "_type": "Snapshot", + "expires": "2038-01-18T03:14:07Z", + "meta": { + "root.json": { + "hashes": { + "sha256": "33f29c8ffce9b7ea3095a15000b9265d69e2498a9beb29f5cecc0fb3b7494a08" + }, + "length": 2259, + "version": 1 + }, + "targets.json": { + "version": 1 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/targets.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/targets.json new file mode 100644 index 0000000..ee1c00e --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/targets.json @@ -0,0 +1,69 @@ +{ + "signatures": [ + { + "keyid": "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596", + "method": "ed25519", + "sig": "6c300b24ffb94f9d1d3465e3e9a877a359c5e5316e3edfd86154eeee08503e111fa23c8b7ca8af2af74747e701272999e61b4930b98a15a41e4a6d20e781490f" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2038-01-18T03:14:07Z", + "targets": { + "/BCU1.0.txt": { + "hashes": { + "sha256": "fb0aa5699a4e7b68009fed6b094ecb00c3ad5670921be1b902b72a23cd4675b1", + "sha512": "0b0bb00bccf7bdad519d0a0af2794c945bd51ebdbc79f9616f0e3903b32f4ce2d5b250ab1bc2d34194bacf720b4f0aed361ef8d59ac72b1bc19e3a223a5e87cd" + }, + "length": 15 + }, + "/BCU1.1.txt": { + "hashes": { + "sha256": "1eb6fa5c6bb606c5326d6ef0ff05f5fcefde4e50c7daea530978090778b38bf4", + "sha512": "9727058c2ba828fdd2fc5ae02f52c10e47404283f92df3539989e2ada3cf7e85a9772faed1bd0bad3fc2bd8f6e5d15b976b8e832dd46874be72b994bc57a62a0" + }, + "length": 18 + }, + "/BCU1.2.txt": { + "hashes": { + "sha256": "42914dc1509923fc83b6945cbaaec193a22077ae3bb799e84b900570715fcb5a", + "sha512": "f213f63b79b05e3ea2045ffe198ab75a993ca5b2709a2e1eac5f18a1a6be1b5eb6af9964b78d388b404414e296b046228f9b68eb75db684eed75b509518a77a5" + }, + "length": 18 + }, + "/INFO1.0.txt": { + "hashes": { + "sha256": "e116d4ef5a2f2dbba9a61970a25cab3e6695418e3dbfa71071e4d07aebb1f083", + "sha512": "7cfa230b2ad2290d38d0da9f0320c1de0dcc9acc40a74154d8f6461c9acb63e7a41a34034b6d84fed6220e1a42afbaa0846efcfc85e1d83c5174f1f8d88d2694" + }, + "length": 18 + }, + "/TCU1.0.txt": { + "hashes": { + "sha256": "c0f997636d40ef418697e85add2e3e6f994592de0c4d90ffe0f86e177281b0dc", + "sha512": "87e3d4f40b43f457e507c81e0caa306893ecf4eb65c28a8ef4a5e5e66323c460c500a7cb9489221eb8bcd2eb5b7e848dcf8c631518289fa07e629c4ffcf8e686" + }, + "length": 16 + }, + "/TCU1.1.txt": { + "hashes": { + "sha256": "56d7cd56a85e34e40d005e1f79c0e95d6937d5528ac0b301dbe68d57e03a5c21", + "sha512": "94d7419b8606103f363aa17feb875575a978df8e88038ea284ff88d90e534eaa7218040384b19992cc7866f5eca803e1654c9ccdf3b250d6198b3c4731216db4" + }, + "length": 17 + }, + "/TCU1.2.txt": { + "hashes": { + "sha256": "fbc8fa01df33f30833428be0fb20edfaafa444ed23d020e3cbc9c60cff167288", + "sha512": "c7c3924d33804eca1d691c5fe61bb965646bf9b6225ce1f040dcde81ce0fc26f4be323f4995e0dee35a847fdbb6efe5a8c8e248e07d1728af0b4cc6fb3179d38" + }, + "length": 17 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/timestamp.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/timestamp.json new file mode 100644 index 0000000..eeeaea0 --- /dev/null +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/timestamp.json @@ -0,0 +1,23 @@ +{ + "signatures": [ + { + "keyid": "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a", + "method": "ed25519", + "sig": "eb505991b63a7dddd153edd4e1a1ee00d46caefbfaf39ec85cf0b96c681f8f33b25666bb101b7f0e7a614dbd8c9eaa97986f5cb5e2873cc1213cafa2d1f59e0c" + } + ], + "signed": { + "_type": "Timestamp", + "expires": "2038-01-18T03:14:07Z", + "meta": { + "snapshot.json": { + "hashes": { + "sha256": "d88bd443c813a54a029527f70e8dc8c7266c20a02c1a07d2d3227c51c7f5c669" + }, + "length": 594, + "version": 1 + } + }, + "version": 1 + } +} \ No newline at end of file From 77dcc9da9d66a29114be0f5f75bfbda9eaec60a2 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 19 Mar 2019 19:15:43 -0400 Subject: [PATCH 08/17] PR revision: correct test/sample root metadata changes - Do not add timeserver keys to the image repo root metadata test/sample data. - Add the public key value where appropriate, not just the keyid. - Re-sign. Signed-off-by: Sebastien Awwad --- .../director/metadata/root.json | 2 +- .../imagerepo/metadata/root.json | 10 ++-------- .../imagerepo/metadata/root.json | 10 ++-------- .../director/metadata/root.json | 2 +- .../imagerepo/metadata/root.json | 10 ++-------- tests/test_data/director_metadata/root.json | 12 +++++++++++- tests/test_data/image_repo_metadata/root.json | 10 ++-------- 7 files changed, 21 insertions(+), 35 deletions(-) diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json index c0b2f6c..3f76868 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "a976e1b7b5ff03505a709ad3f0af13ad1ab0683752e9dced52735d2fb0d3086292c78b00472c1b59e4e3b5a90d42ad53be419de069aa8c6b176d202f96621c06" + "sig": "2ab82986eb585c970cd972cf647a7d98f0c8d44744f091d19821edf2dd746c3b56d0dc777b364d1b383503ff44bdbe33ce1443576117c816bb1c17b6f2240e00" } ], "signed": { diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json index 27b7138..4fe725a 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json @@ -1,9 +1,9 @@ { "signatures": [ { - "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", + "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "b8dcfed4e1ab955968b3d94eb173a1f6440dc2bd8fe5dcde10fd1c10aa1feeb44c306e3f2375ecd859267307959ffdb549108e8598e39d150f8e1ffecb15860e" + "sig": "e47bc67e9d4433ad91f7e3b22b09752c735ba7f84a7812f9a6be086cdad70cbd8b93d68cea3ed94beb514aa099740046c352486dade1c60f68a3a24ce427e900" } ], "signed": { @@ -56,12 +56,6 @@ } }, "roles": { - "Timeserver": { - "keyids": [ - "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" - ], - "threshold": 1 - }, "root": { "keyids": [ "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290" diff --git a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json index 27b7138..4fe725a 100644 --- a/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json +++ b/samples/metadata_samples_long_expiry/timeserver_key_rotated/full_metadata_archive/imagerepo/metadata/root.json @@ -1,9 +1,9 @@ { "signatures": [ { - "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", + "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "b8dcfed4e1ab955968b3d94eb173a1f6440dc2bd8fe5dcde10fd1c10aa1feeb44c306e3f2375ecd859267307959ffdb549108e8598e39d150f8e1ffecb15860e" + "sig": "e47bc67e9d4433ad91f7e3b22b09752c735ba7f84a7812f9a6be086cdad70cbd8b93d68cea3ed94beb514aa099740046c352486dade1c60f68a3a24ce427e900" } ], "signed": { @@ -56,12 +56,6 @@ } }, "roles": { - "Timeserver": { - "keyids": [ - "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" - ], - "threshold": 1 - }, "root": { "keyids": [ "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290" diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json index c0b2f6c..3f76868 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "a976e1b7b5ff03505a709ad3f0af13ad1ab0683752e9dced52735d2fb0d3086292c78b00472c1b59e4e3b5a90d42ad53be419de069aa8c6b176d202f96621c06" + "sig": "2ab82986eb585c970cd972cf647a7d98f0c8d44744f091d19821edf2dd746c3b56d0dc777b364d1b383503ff44bdbe33ce1443576117c816bb1c17b6f2240e00" } ], "signed": { diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json index 27b7138..4fe725a 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json @@ -1,9 +1,9 @@ { "signatures": [ { - "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", + "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "b8dcfed4e1ab955968b3d94eb173a1f6440dc2bd8fe5dcde10fd1c10aa1feeb44c306e3f2375ecd859267307959ffdb549108e8598e39d150f8e1ffecb15860e" + "sig": "e47bc67e9d4433ad91f7e3b22b09752c735ba7f84a7812f9a6be086cdad70cbd8b93d68cea3ed94beb514aa099740046c352486dade1c60f68a3a24ce427e900" } ], "signed": { @@ -56,12 +56,6 @@ } }, "roles": { - "Timeserver": { - "keyids": [ - "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" - ], - "threshold": 1 - }, "root": { "keyids": [ "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290" diff --git a/tests/test_data/director_metadata/root.json b/tests/test_data/director_metadata/root.json index 09f90f6..3f76868 100644 --- a/tests/test_data/director_metadata/root.json +++ b/tests/test_data/director_metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "a976e1b7b5ff03505a709ad3f0af13ad1ab0683752e9dced52735d2fb0d3086292c78b00472c1b59e4e3b5a90d42ad53be419de069aa8c6b176d202f96621c06" + "sig": "2ab82986eb585c970cd972cf647a7d98f0c8d44744f091d19821edf2dd746c3b56d0dc777b364d1b383503ff44bdbe33ce1443576117c816bb1c17b6f2240e00" } ], "signed": { @@ -24,6 +24,16 @@ "public": "99ef8790687ca252c4677a80a34e401efb7e17ccdf9b0fcb5f1bc3260c432cb9" } }, + "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "5d7750b208dfc7ade8f6106b9c3fa25162d5a184f302161e429f19a79e66a908" + } + }, "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc": { "keyid_hash_algorithms": [ "sha256", diff --git a/tests/test_data/image_repo_metadata/root.json b/tests/test_data/image_repo_metadata/root.json index 27b7138..4fe725a 100644 --- a/tests/test_data/image_repo_metadata/root.json +++ b/tests/test_data/image_repo_metadata/root.json @@ -1,9 +1,9 @@ { "signatures": [ { - "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", + "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "b8dcfed4e1ab955968b3d94eb173a1f6440dc2bd8fe5dcde10fd1c10aa1feeb44c306e3f2375ecd859267307959ffdb549108e8598e39d150f8e1ffecb15860e" + "sig": "e47bc67e9d4433ad91f7e3b22b09752c735ba7f84a7812f9a6be086cdad70cbd8b93d68cea3ed94beb514aa099740046c352486dade1c60f68a3a24ce427e900" } ], "signed": { @@ -56,12 +56,6 @@ } }, "roles": { - "Timeserver": { - "keyids": [ - "79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e" - ], - "threshold": 1 - }, "root": { "keyids": [ "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290" From 9bb5d5a3b194e96e5a83ad1d5e9255a33207903d Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 20 Mar 2019 14:44:36 -0400 Subject: [PATCH 09/17] PR revision: Add timeserver key rotation to the update clients - Correct the code handling Timeserver key rotation to recognize that keyids+threshold is what is listed in the 'role' metadata in Root, and to use that to obtain the full public key value from tuf.keydb. - Also performs the actual update of the client's noted Timeserver key. In a future commit, this should probably be modified a bit such that the clients just use the value from metadata via get_metadata() calls instead of caching the value (since in this implementation, the Timeserver key information will be in every Root version). - Make notes where more duplicate code has been added to primary.py and secondary.py. - Slightly reorganize code in refresh_toplevel_metadata_from_repositories. Signed-off-by: Sebastien Awwad --- uptane/clients/primary.py | 118 ++++++++++++++++++++++++++++++------ uptane/clients/secondary.py | 102 +++++++++++++++++++++++++------ 2 files changed, 180 insertions(+), 40 deletions(-) diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index cf71ca5..1de7f3a 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -340,6 +340,10 @@ def refresh_toplevel_metadata_from_repositories(self): See tuf.client.updater.Updater.refresh() for details, or the Uptane Implementation Specification, section 8.3.2 (Full Verification of Metadata). + + # TODO: This function is duplicated in primary.py and secondary.py. It must + # be moved to a general client.py as part of a fix to issue #14 + # (github.com/uptane/uptane/issues/14). """ # In order to provide Timeserver fast-forward attack protection, we do more @@ -357,8 +361,8 @@ def refresh_toplevel_metadata_from_repositories(self): # refresh() again, though. - # Make note of the currently-trusted Timeserver key. - current_trusted_timeserver_key = self.updater.get_metadata( + # Make note of the currently-trusted Timeserver key(s) and threshold. + prior_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] try: @@ -366,31 +370,106 @@ def refresh_toplevel_metadata_from_repositories(self): except (tuf.NoWorkingMirrorError, tuf.ExpiredMetadataError): # TODO: <~> In the except line above, see if it's sufficient to only - # catch ExpiredMetadataError here. (When do we get + # catch NoWorkingMirrorError here. (Do we ever get # ExpiredMetadataError instead of NoWorkingMirrorError? - # Do we need to comb through the component errors in the - # NoWorkingMirrorErrors looking for ExpiredMetadataError?) - - new_trusted_timeserver_key = self.updater.get_metadata( + # Should we comb through the component errors in the + # NoWorkingMirrorErrors looking for ExpiredMetadataError? + # If so, write a function that returns True/False given the + # NoWorkingMirrorError, based on whether or not the failure was + # caused by ExpiredMetadataErrors. Consider generalizing to + # return an error class if the NoWorkingMirrorError is caused + # solely by one error class, and something else if the causes + # are various. + + new_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] - if current_trusted_timeserver_key != new_trusted_timeserver_key: - self.reset_clock() + if prior_timeserver_auth_info != new_timeserver_auth_info: + # TODO: Consider another, more invasive way to accomplish this (within + # root chain verification, after switch to theupdateframework/tuf) + # because there's a corner case here that isn't addressed: + # Suppose in root version X you change the Timeserver key after a + # fast-forward attack, then later in root version Y, change it + # back because you decide the key was not exposed or something.... + # If a client goes from root version X-1 to root version Y within + # this update cycle (it would root chain within the refresh + # call), then we won't notice here that the key ever changed, + # and we won't resolve the fast-forward attack. Detection should + # occur at a lower level, in every root chain link step. + # This will do for now, but fix the corner case by moving this + # check. + self.update_timeserver_key_and_reset_clock(prior_timeserver_auth_info) + # Since we failed to update and the Timeserver key changed, we try to + # refresh again, since we may have failed because of a fast-forward + # attack. + # Note that the only difference between this except clause and the + # try-except-else's else clause below is that we refresh again here. self.updater.refresh() + else: + raise + else: - new_trusted_timeserver_key = self.updater.get_metadata( + new_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] - if current_trusted_timeserver_key != new_trusted_timeserver_key: - self.reset_clock() - - - - - - def reset_clock(self): - '''Reset the clock to epoch and discard old timeserver attestations.''' + if prior_timeserver_auth_info != new_timeserver_auth_info: + self.update_timeserver_key_and_reset_clock(new_timeserver_auth_info) + + + + + + def update_timeserver_key_and_reset_clock(self, new_auth_info): + ''' + Update the expected timeserver key, reset the clock to epoch, and discard + old timeserver attestations. + This function assumes that the timeserver key has changed. (i.e. Do not + call it if the key has not changed.)) + + The argument new_auth_info is in the keyids+threshold format expected in + the Root metadata, e.g.: + {'keyids': ['1234...'], 'threshold': 1} + This implementation supports only one Timeserver key. + + # TODO: This function is duplicated in primary.py and secondary.py. It must + # be moved to a general client.py as part of a fix to issue #14 + # (github.com/uptane/uptane/issues/14). + ''' + + # TODO: Separate and migrate away from ROLE_SCHEMA. ROLE_SCHEMA is poorly + # named and used for too many distinct purposes. + tuf.formats.ROLE_SCHEMA.check_match(new_auth_info) + + if len(new_auth_info['keyids']) != 1 or new_auth_info['threshold'] != 1: + raise uptane.Error( + 'This implementation supports only a single key and threshold of ' + '1 for the Timeserver. The given authentication information drawn ' + 'from verified Root metadata does not match these constraints, ' + 'listing ' + str(len(new_auth_info['keyids'])) + ' keys and having ' + 'a threshold of ' + str(new_auth_info['threshold']) + '.') + + new_keyid = new_auth_info['keyids'][0] + + # We retrieve the key from tuf.keydb, using the keyid provided (obtained, + # by the caller of this function, from the 'roles' section of the currently + # trusted Director Root metadata. + # We could instead fetch the key information directly from the 'keys' + # section of the currently trusted Director Root metadata, looking it up + # using the keyid from the 'roles' section like this: + # self.updater.get_metadata(self.director_repo_name, 'current')['root']['keys'][new_trusted_timeserver_keyid] + # BUT we will instead use tuf.keydb.get_key(). keydb is fed the key + # information when the metadata is verified. The difference is only that + # certain implementations of general-purpose key rotation (TUF's TAP 8) + # might result in these not matching, and the more trustworthy source being + # tuf.keydb. (At the time of this writing, TAP 8 is not implemented here.) + # however, we do not fetch it directly, but request it from keydb, where + # that information ends up when the metadata is updated. There is a + # possible edge case if general-purpose key rotation is implemented.... + self.timeserver_public_key = tuf.keydb.get_key( + new_keyid, repository_name=self.director_repo_name) + + # Reset the clock to epoch and discard previously-trusted time attestations. tuf.conf.CLOCK_OVERRIDE = 0 self.all_valid_timeserver_times = [time.gmtime(0)] self.all_valid_timeserver_attestations = [] @@ -398,7 +477,6 @@ def reset_clock(self): - def get_target_list_from_director(self): """ This method extracts the Director's instructions from the targets role in diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index a112b93..a91e948 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -30,6 +30,7 @@ import zipfile # to expand the metadata archive retrieved from the Primary import hashlib import iso8601 +import time import tuf.formats import tuf.keys @@ -479,7 +480,9 @@ def refresh_toplevel_metadata_from_repositories(self): Uptane Implementation Specification, section 8.3.2 (Full Verification of Metadata). - # TODO: Handle the duplicated code! This is the same in primary.py. + # TODO: This function is duplicated in primary.py and secondary.py. It must + # be moved to a general client.py as part of a fix to issue #14 + # (github.com/uptane/uptane/issues/14). """ # In order to provide Timeserver fast-forward attack protection, we do more @@ -497,8 +500,8 @@ def refresh_toplevel_metadata_from_repositories(self): # refresh() again, though. - # Make note of the currently-trusted Timeserver key. - current_trusted_timeserver_key = self.updater.get_metadata( + # Make note of the currently-trusted Timeserver key(s) and threshold. + prior_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] try: @@ -506,15 +509,21 @@ def refresh_toplevel_metadata_from_repositories(self): except (tuf.NoWorkingMirrorError, tuf.ExpiredMetadataError): # TODO: <~> In the except line above, see if it's sufficient to only - # catch ExpiredMetadataError here. (When do we get + # catch NoWorkingMirrorError here. (Do we ever get # ExpiredMetadataError instead of NoWorkingMirrorError? - # Do we need to comb through the component errors in the - # NoWorkingMirrorErrors looking for ExpiredMetadataError?) - - new_trusted_timeserver_key = self.updater.get_metadata( + # Should we comb through the component errors in the + # NoWorkingMirrorErrors looking for ExpiredMetadataError? + # If so, write a function that returns True/False given the + # NoWorkingMirrorError, based on whether or not the failure was + # caused by ExpiredMetadataErrors. Consider generalizing to + # return an error class if the NoWorkingMirrorError is caused + # solely by one error class, and something else if the causes + # are various. + + new_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] - if current_trusted_timeserver_key != new_trusted_timeserver_key: + if prior_timeserver_auth_info != new_timeserver_auth_info: # TODO: Consider another, more invasive way to accomplish this (within # root chain verification, after switch to theupdateframework/tuf) # because there's a corner case here that isn't addressed: @@ -528,25 +537,78 @@ def refresh_toplevel_metadata_from_repositories(self): # occur at a lower level, in every root chain link step. # This will do for now, but fix the corner case by moving this # check. - self.reset_clock() + self.update_timeserver_key_and_reset_clock(prior_timeserver_auth_info) + # Since we failed to update and the Timeserver key changed, we try to + # refresh again, since we may have failed because of a fast-forward + # attack. + # Note that the only difference between this except clause and the + # try-except-else's else clause below is that we refresh again here. self.updater.refresh() else: raise else: - new_trusted_timeserver_key = self.updater.get_metadata( + new_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] - if current_trusted_timeserver_key != new_trusted_timeserver_key: - self.reset_clock() - - - - - - def reset_clock(self): - '''Reset the clock to epoch and discard old timeserver attestations.''' + if prior_timeserver_auth_info != new_timeserver_auth_info: + self.update_timeserver_key_and_reset_clock(new_timeserver_auth_info) + + + + + + def update_timeserver_key_and_reset_clock(self, new_auth_info): + ''' + Update the expected timeserver key, reset the clock to epoch, and discard + old timeserver attestations. + This function assumes that the timeserver key has changed. (i.e. Do not + call it if the key has not changed.)) + + The argument new_auth_info is in the keyids+threshold format expected in + the Root metadata, e.g.: + {'keyids': ['1234...'], 'threshold': 1} + This implementation supports only one Timeserver key. + + # TODO: This function is duplicated in primary.py and secondary.py. It must + # be moved to a general client.py as part of a fix to issue #14 + # (github.com/uptane/uptane/issues/14). + ''' + + # TODO: Separate and migrate away from ROLE_SCHEMA. ROLE_SCHEMA is poorly + # named and used for too many distinct purposes. + tuf.formats.ROLE_SCHEMA.check_match(new_auth_info) + + if len(new_auth_info['keyids']) != 1 or new_auth_info['threshold'] != 1: + raise uptane.Error( + 'This implementation supports only a single key and threshold of ' + '1 for the Timeserver. The given authentication information drawn ' + 'from verified Root metadata does not match these constraints, ' + 'listing ' + str(len(new_auth_info['keyids'])) + ' keys and having ' + 'a threshold of ' + str(new_auth_info['threshold']) + '.') + + new_keyid = new_auth_info['keyids'][0] + + # We retrieve the key from tuf.keydb, using the keyid provided (obtained, + # by the caller of this function, from the 'roles' section of the currently + # trusted Director Root metadata. + # We could instead fetch the key information directly from the 'keys' + # section of the currently trusted Director Root metadata, looking it up + # using the keyid from the 'roles' section like this: + # self.updater.get_metadata(self.director_repo_name, 'current')['root']['keys'][new_trusted_timeserver_keyid] + # BUT we will instead use tuf.keydb.get_key(). keydb is fed the key + # information when the metadata is verified. The difference is only that + # certain implementations of general-purpose key rotation (TUF's TAP 8) + # might result in these not matching, and the more trustworthy source being + # tuf.keydb. (At the time of this writing, TAP 8 is not implemented here.) + # however, we do not fetch it directly, but request it from keydb, where + # that information ends up when the metadata is updated. There is a + # possible edge case if general-purpose key rotation is implemented.... + self.timeserver_public_key = tuf.keydb.get_key( + new_keyid, repository_name=self.director_repo_name) + + # Reset the clock to epoch and discard previously-trusted time attestations. tuf.conf.CLOCK_OVERRIDE = 0 self.all_valid_timeserver_times = [time.gmtime(0)] self.all_valid_timeserver_attestations = [] From a1ccae282ebd881f8cf9b7fb7a11e19174ca7d39 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 20 Mar 2019 14:49:39 -0400 Subject: [PATCH 10/17] Add test of timeserver key rotation to test_secondary.py The test feeds in two sets of metadata, the second of which has a different Timeserver key listed. This checks to make sure that the key change is taken into account by the clients, but it does not yet test the fast-forward attack. Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index a90ee1b..c3dd012 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -751,6 +751,69 @@ def test_50_validate_image(self): + def test_90_timeserver_key_rotation(self): + # This test works only in JSON mode. ASN.1/DER metadata has not been + # extended here to enable Timeserver key rotation. + + # Note that this test, like the rest in this test module, builds on prior + # tests, and should not be run on its own. + + # Use the first test Secondary instance, which has by now verified metadata. + # Try updating to a later version of metadata that indicates a different + # Timeserver key in the Director's Root metadata. + + if tuf.conf.METADATA_FORMAT == 'der': + print('Skipping Test 90 in DER mode.') + return + + instance = secondary_instances[0] + + archive_with_rotated_key = os.path.join( + SAMPLES_DIR, 'metadata_samples_long_expiry', 'timeserver_key_rotated', + 'full_metadata_archive.zip') + + initially_trusted_timeserver_keyid = \ + instance.timeserver_public_key['keyid'] + + # Make sure that the currently trusted Root metadata in the Secondary's + # updater for the Director repository lists the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + self.assertEqual( + initially_trusted_timeserver_keyid, + instance.updater.repositories['director'].metadata['current']['root'] + ['roles']['Timeserver']['keyids'][0]) + + self.assertEqual( + '79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e', + initially_trusted_timeserver_keyid) + + # Update to the new metadata with the new Timeserver key. + instance.process_metadata(archive_with_rotated_key) + + now_trusted_timeserver_keyid = \ + instance.timeserver_public_key['keyid'] + + # Make sure that the currently trusted Root metadata in the Secondary's + # updater for the Director repository lists the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + self.assertEqual( + now_trusted_timeserver_keyid, + instance.updater.repositories['director'].metadata['current']['root'] + ['roles']['Timeserver']['keyids'][0]) + + self.assertEqual( + 'da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc', + now_trusted_timeserver_keyid) + + # For good measure, to avoid stupid mistakes in future test code changes. + # Make sure the key changed. + self.assertNotEqual( + initially_trusted_timeserver_keyid, now_trusted_timeserver_keyid) + + + + + # Run unit tests. if __name__ == '__main__': unittest.main() From d83ebec9fd64a1fc92c2a22a39f2b4c751668422 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Mon, 25 Mar 2019 15:22:59 -0400 Subject: [PATCH 11/17] PR revision: correct the epoch-start time format in the clients This is the value used in all_valid_timeserver_times() after a Timeserver key rotation, and is also the value used to set tuf.conf.CLOCK_OVERRIDE. They were previously set to time.gmtime(0), which is the wrong type. They are now correctly set to an iso8601 value. Signed-off-by: Sebastien Awwad --- uptane/clients/primary.py | 3 ++- uptane/clients/secondary.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index 1de7f3a..c59a4a3 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -471,7 +471,8 @@ def update_timeserver_key_and_reset_clock(self, new_auth_info): # Reset the clock to epoch and discard previously-trusted time attestations. tuf.conf.CLOCK_OVERRIDE = 0 - self.all_valid_timeserver_times = [time.gmtime(0)] + self.all_valid_timeserver_times = [tuf.formats.unix_timestamp_to_datetime( + 0).isoformat() + 'Z'] self.all_valid_timeserver_attestations = [] diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index a91e948..b2859d1 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -610,7 +610,8 @@ def update_timeserver_key_and_reset_clock(self, new_auth_info): # Reset the clock to epoch and discard previously-trusted time attestations. tuf.conf.CLOCK_OVERRIDE = 0 - self.all_valid_timeserver_times = [time.gmtime(0)] + self.all_valid_timeserver_times = [tuf.formats.unix_timestamp_to_datetime( + 0).isoformat() + 'Z'] self.all_valid_timeserver_attestations = [] From 603284a62c5d093b1cdadd83fc799ffca7113eb2 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Mon, 25 Mar 2019 15:30:36 -0400 Subject: [PATCH 12/17] Test Timeserver fast forward attack and resolution test_secondary.test_95_timeserver_fastforward_attack We add an additional test client (bringing us to 4 test Secondary clients). Resolution uses rotation of the Timeserver key. See: - https://github.com/uptane/uptane/issues/173 - https://github.com/uptane/uptane-standard/pull/41 This commit also improves the prior test added in this PR, test_90_timeserver_key_rotation, adding a few checks and improving readability and comments. This requires a bit more editing. Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 328 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 300 insertions(+), 28 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index c3dd012..69401d6 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -19,6 +19,7 @@ import shutil import hashlib import iso8601 +import time from six.moves.urllib.error import URLError @@ -31,6 +32,7 @@ import uptane.clients.secondary as secondary import uptane.common # verify sigs, create client dir structure, convert key import uptane.encoding.asn1_codec as asn1_codec +import uptane.services.timeserver as timeserver from uptane.encoding.asn1_codec import DATATYPE_TIME_ATTESTATION from uptane.encoding.asn1_codec import DATATYPE_ECU_MANIFEST @@ -55,17 +57,18 @@ TEMP_CLIENT_DIRS = [ os.path.join(TEST_DATA_DIR, 'temp_test_secondary0'), os.path.join(TEST_DATA_DIR, 'temp_test_secondary1'), - os.path.join(TEST_DATA_DIR, 'temp_test_secondary2')] + os.path.join(TEST_DATA_DIR, 'temp_test_secondary2'), + os.path.join(TEST_DATA_DIR, 'temp_test_secondary3')] # I'll initialize these in the __init__ test, and use this for the simple # non-damaging tests so as to avoid creating objects all over again. -secondary_instances = [None, None, None] +secondary_instances = [None, None, None, None] # Changing these values would require producing new signed test data from the # Timeserver (in the case of nonce) or a Secondary (in the case of the others). nonce = 5 -vins = ['democar', 'democar', '000'] -ecu_serials = ['TCUdemocar', '00000', '00000'] +vins = ['democar', 'democar', '000', 'democar'] +ecu_serials = ['TCUdemocar', '00000', '00000', 'TCUdemocar'] # Set starting firmware fileinfo (that this ECU had coming from the factory) # It will serve as the initial firmware state for the Secondary clients. @@ -349,7 +352,7 @@ def test_01_init(self): - # Try initializing three Secondaries, expecting the three calls to work. + # Try initializing four Secondaries, expecting the four calls to work. # Save the instances for future tests as class variables to save time and # code. @@ -360,13 +363,13 @@ def test_01_init(self): # it work for these tests. - # Initialize three clients and perform checks on each of them. + # Initialize four clients and perform checks on each of them. for i in range(0, len(TEMP_CLIENT_DIRS)): client_dir = TEMP_CLIENT_DIRS[i] ecu_serial = ecu_serials[i] vin = vins[i] - # Try initializing each of three secondaries, expecting these calls to + # Try initializing each of four secondaries, expecting these calls to # work. Save the instances for future tests as elements in a module list # variable(secondary_instances) to save time and code. tuf.conf.repository_directory = client_dir @@ -445,7 +448,7 @@ def test_10_nonce_rotation(self): - change_nonce() - set_nonce_as_sent() """ - # We'll just test one of the three client instances, since it shouldn't + # We'll just test one of the four client instances, since it shouldn't # make a difference. instance = secondary_instances[0] @@ -468,7 +471,7 @@ def test_20_update_time(self): Tests uptane.clients.secondary.Secondary::update_time() """ - # We'll just test one of the three client instances, since it shouldn't + # We'll just test one of the four client instances, since it shouldn't # make a difference. instance = secondary_instances[0] @@ -571,7 +574,7 @@ def test_25_generate_signed_ecu_manifest(self): Tests uptane.clients.secondary.Secondary::generate_signed_ecu_manifest() """ - # We'll just test one of the three client instances, since it shouldn't + # We'll just test one of the four client instances, since it shouldn't # make a difference. ecu_manifest = secondary_instances[0].generate_signed_ecu_manifest() @@ -610,10 +613,11 @@ def test_40_process_metadata(self): """ Tests uptane.clients.secondary.Secondary::process_metadata() - Tests three clients: + Tests four clients: - secondary_instances[0]: an update is provided in Director metadata - secondary_instances[1]: no update is provided in Director metadata - secondary_instances[2]: no Director metadata can be retrieved + - secondary_instances[3]: an update is provided in Director metadata (same as 0) """ # --- Test this test module's setup (defensive) @@ -678,7 +682,7 @@ def test_40_process_metadata(self): '/metadata/' + role + '.' + tuf.conf.METADATA_FORMAT)) - # Verify the results of the test, which are different for the three clients. + # Verify the results of the test, which are different for the four clients. # First: Check the top-level metadata files in the client directories. @@ -707,10 +711,13 @@ def test_40_process_metadata(self): # Second: Check targets each Secondary client has been instructed to # install (and has in turn validated). - # Client 0 should have validated expected_updated_fileinfo. + # Clients 0 and 3 should have validated expected_updated_fileinfo. self.assertEqual( expected_updated_fileinfo, secondary_instances[0].validated_targets_for_this_ecu[0]) + self.assertEqual( + expected_updated_fileinfo, + secondary_instances[3].validated_targets_for_this_ecu[0]) # Clients 1 and 2 should have no validated targets. self.assertFalse(secondary_instances[1].validated_targets_for_this_ecu) @@ -752,22 +759,34 @@ def test_50_validate_image(self): def test_90_timeserver_key_rotation(self): - # This test works only in JSON mode. ASN.1/DER metadata has not been - # extended here to enable Timeserver key rotation. + ''' + This test works only in JSON mode. ASN.1/DER metadata has not been + extended here to enable Timeserver key rotation. - # Note that this test, like the rest in this test module, builds on prior - # tests, and should not be run on its own. + Note that this test, like the rest in this test module, builds on prior + tests, and should not be run on its own. - # Use the first test Secondary instance, which has by now verified metadata. - # Try updating to a later version of metadata that indicates a different - # Timeserver key in the Director's Root metadata. + Use the first test Secondary instance, which has by now verified metadata. + Try updating to a later version of metadata that indicates a different + Timeserver key in the Director's Root metadata. + ''' if tuf.conf.METADATA_FORMAT == 'der': - print('Skipping Test 90 in DER mode.') + print('Skipping the JSON-only test_90_timeserver_key_rotation.') return instance = secondary_instances[0] + # Since we're switching clients, we should switch the TUF override time to + # what this client thinks it should be. (We're cheating by juggling + # multiple clients in one process for testing purposes, and the TUF clock + # override is a module-level variable in tuf.conf, so it's common to all + # clients in a process.) Use the last trusted time for the instance. + tuf.conf.CLOCK_OVERRIDE = int(tuf.formats.datetime_to_unix_timestamp( + iso8601.parse_date(instance.all_valid_timeserver_times[-1]))) + + + # PREPARE an update with the timeserver key rotated. archive_with_rotated_key = os.path.join( SAMPLES_DIR, 'metadata_samples_long_expiry', 'timeserver_key_rotated', 'full_metadata_archive.zip') @@ -775,32 +794,280 @@ def test_90_timeserver_key_rotation(self): initially_trusted_timeserver_keyid = \ instance.timeserver_public_key['keyid'] - # Make sure that the currently trusted Root metadata in the Secondary's - # updater for the Director repository lists the same Timeserver keyid as the - # Secondary has noted is the currently trusted Timeserver key's keyid. + + # CHECK INITIAL state before the test: + + # 1. There should be trusted timeserver attestations. + self.assertEqual(3, len(instance.all_valid_timeserver_times)) + + # 2. The current trusted time should be '2016-11-02T21:06:05Z' from the + # attestation in the sample metadata from prior tests. + # self.assertIsNotNone(tuf.conf.CLOCK_OVERRIDE) + self.assertEqual( + int(tuf.formats.datetime_to_unix_timestamp(iso8601.parse_date( + '2016-11-02T21:06:05Z'))), tuf.conf.CLOCK_OVERRIDE) + + # 3. The currently trusted Root metadata in the Secondary's updater for the + # Director repository should list the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + # This should be the 79c79... key we know was in the original root + # metadata. self.assertEqual( initially_trusted_timeserver_keyid, instance.updater.repositories['director'].metadata['current']['root'] ['roles']['Timeserver']['keyids'][0]) - self.assertEqual( '79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e', initially_trusted_timeserver_keyid) + # Update to the new metadata with the new Timeserver key. instance.process_metadata(archive_with_rotated_key) now_trusted_timeserver_keyid = \ instance.timeserver_public_key['keyid'] - # Make sure that the currently trusted Root metadata in the Secondary's - # updater for the Director repository lists the same Timeserver keyid as the - # Secondary has noted is the currently trusted Timeserver key's keyid. + + # CHECK RESULTING state: + + # 1. There should be zero trusted timeserver attestations immediately after + # rotation. + self.assertEqual(0, len(instance.all_valid_timeserver_attestations)) + + # 2. The trusted time and the TUF clock override should be 0, epoch start. + self.assertEqual( + ['1970-01-01T00:00:00Z'], instance.all_valid_timeserver_times) + self.assertEqual(0, tuf.conf.CLOCK_OVERRIDE) + + # 3. The currently trusted Root metadata in the Secondary's updater for the + # Director repository should list the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + # This should be the da9c6... key we know is in the sample rotated + # metadata. self.assertEqual( now_trusted_timeserver_keyid, instance.updater.repositories['director'].metadata['current']['root'] ['roles']['Timeserver']['keyids'][0]) + self.assertEqual( + 'da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc', + now_trusted_timeserver_keyid) + + # For good measure, to avoid stupid mistakes in future test code changes. + # Make sure the key changed. + self.assertNotEqual( + initially_trusted_timeserver_keyid, now_trusted_timeserver_keyid) + + + + + + def test_95_timeserver_fastforward_attack(self): + ''' + This test works only in JSON mode. ASN.1/DER metadata has not been + extended here to enable Timeserver key rotation. + + This is similar to test 90, except that we'll conduct a Timeserver + fast-forward attack before rotating the Timeserver key, make sure that the + attack impairs updating, check that the failed update during the attack had + no unexpected consequences, and after performing the rotation and updating + the Secondary, confirm that the attack is resolved. + + We'll use the fourth test Secondary instance (secondary_instances[3]), + which was more or less identical to instance [0] until test 90 above. + ''' + + if tuf.conf.METADATA_FORMAT == 'der': + print('Skipping the JSON-only test_95_timeserver_fastforward_attack') + return + + instance = secondary_instances[3] + + # Since we're switching clients, we should switch the TUF override time to + # what this client thinks it should be. (We're cheating by juggling + # multiple clients in one process for testing purposes, and the TUF clock + # override is a module-level variable in tuf.conf, so it's common to all + # clients in a process.) Use the last trusted time for the instance. + tuf.conf.CLOCK_OVERRIDE = int(tuf.formats.datetime_to_unix_timestamp( + iso8601.parse_date(instance.all_valid_timeserver_times[-1]))) + + + + # CHECK INITIAL state before the attack: + + # 1. There should be 2 trusted timeserver attestations initially. + self.assertEqual(2, len(instance.all_valid_timeserver_times)) + + # 2. We would check the client's override time, but that's stored in a + # module-level variable for the whole process, and we're cheating and + # running four clients in one process, so we just reset the clock + # override a few lines ago. It'll be overwritten when the next + # timeserver attestation is verified. + # self.assertEqual( + # int(tuf.formats.datetime_to_unix_timestamp(iso8601.parse_date( + # '2016-11-02T21:06:05Z'))), tuf.conf.CLOCK_OVERRIDE) + + # 3. The currently trusted Root metadata in the Secondary's updater for the + # Director repository should list the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + # This should be the 79c79... key we know was in the original root + # metadata. + initially_trusted_timeserver_keyid = \ + instance.timeserver_public_key['keyid'] + self.assertEqual( + initially_trusted_timeserver_keyid, + instance.updater.repositories['director'].metadata['current']['root'] + ['roles']['Timeserver']['keyids'][0]) + self.assertEqual( + '79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e', + initially_trusted_timeserver_keyid) + + + + # BUILD the ATTACK. + + # Simulate a request from the Secondary to the Timeserver containing the + # next nonce. (This populates instance.last_nonce_sent.) + instance.set_nonce_as_sent() + + # Construct a Timeserver Attestation that sets the clock to the future, + # near the end of the current UNIX epoch. Include the nonce this + # Secondary thinks it sent the Timeserver. + # (This has no signatures list yet.) + fastforward_bare = { + 'time': '2038-01-19T03:14:07Z', # a.k.a. 2147483647 in UNIX time + 'nonces': [instance.last_nonce_sent] + } + + uptane.formats.TIMESERVER_ATTESTATION_SCHEMA.check_match( + fastforward_bare) + + # Construct and check the signable version of the time attestation. + fastforward_signable = tuf.formats.make_signable(fastforward_bare) + uptane.formats.SIGNABLE_TIMESERVER_ATTESTATION_SCHEMA.check_match( + fastforward_signable) + + # Sign the fast-forward time attestation. + uptane.common.sign_signable( + fastforward_signable, + [self.key_timeserver_pri], + asn1_codec.DATATYPE_TIME_ATTESTATION, + metadata_format='json') + + + + # PERFORM the ATTACK. + + # Provide the Secondary with the fast-forward attestation. + # If the time_attestation is not deemed valid, an exception will be raised. + instance.update_time(fastforward_signable) + + + # Check results of the attack. + + # 1. There should be 3 trusted timeserver attestations (including the + # fast-forwarding attestation). + self.assertEqual(3, len(instance.all_valid_timeserver_times)) + + # 2. The current trusted time should be end-of-epoch from the + # fast-forwarding attestation. + self.assertEqual(2147483647, tuf.conf.CLOCK_OVERRIDE) # about epoch max + + + # CHECK ATTACK state: an update without the key rotated should now fail. + # (Fast-forward attack succeeds until Timeserver key is rotated.) + + archive_with_old_timeserver_key = os.path.join( + SAMPLES_DIR, 'metadata_samples_long_expiry', 'update_to_one_ecu', + 'full_metadata_archive.zip') + + + # TRY UPDATING during attack. This should fail with a NoWorkingMirrorError. + # The individual errors for each mirror should be ExpiredMetadataErrors. + # Check to make sure all the individual errors spooled in the + # NoWorkingMirrorError are each an ExpiredMetadataError. + try: + instance.process_metadata(archive_with_old_timeserver_key) + except tuf.NoWorkingMirrorError as e: + for mirror in e.mirror_errors: + self.assertIsInstance(e.mirror_errors[mirror], tuf.ExpiredMetadataError) + else: + self.Fail( + 'Expected update to fail during attack and provide a ' + 'NoWorkingMirrorError, which we would then check to confirm ' + 'consists of one ExpiredMetadataError for each mirror.') + + + + # TODO: Consider generating more metadata to conduct more tests..... Ugh. + + + + # CHECK that the failed update had no strange side-effects by + # REPEATING all the initial-metadata-state and attacked-time-state checks + # above to make sure that the update that should have failed had no strange + # effects. + + # 1. There should be 3 trusted timeserver attestations (including the + # fast-forwarding attestation). + self.assertEqual(3, len(instance.all_valid_timeserver_times)) + + # 2. The current trusted time should be end-of-epoch from the + # fast-forwarding attestation. + self.assertEqual(2147483647, tuf.conf.CLOCK_OVERRIDE) # about epoch max + + # 3. The currently trusted Root metadata in the Secondary's updater for the + # Director repository should list the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + # This should be the 79c79... key we know was in the original root + # metadata. + initially_trusted_timeserver_keyid = \ + instance.timeserver_public_key['keyid'] + self.assertEqual( + initially_trusted_timeserver_keyid, + instance.updater.repositories['director'].metadata['current']['root'] + ['roles']['Timeserver']['keyids'][0]) + self.assertEqual( + '79c796d7e87389d1ebad04edce49faef611d139ee41ea9fb1931732afbfaac2e', + initially_trusted_timeserver_keyid) + + + + # RESOLVE the ATTACK by providing the Secondary with metadata in which the + # timeserver key was rotated, which should allow the Secondary to update + # again. + # Provide a new root file with the timeserver key rotated. Update and + # confirm that the update succeeds. + archive_with_rotated_key = os.path.join( + SAMPLES_DIR, 'metadata_samples_long_expiry', 'timeserver_key_rotated', + 'full_metadata_archive.zip') + + # Update to the new metadata with the new Timeserver key. + instance.process_metadata(archive_with_rotated_key) + + now_trusted_timeserver_keyid = \ + instance.timeserver_public_key['keyid'] + + # CHECK RESULTING state: + + # 1. There should be zero trusted timeserver attestations immediately after + # rotation. + self.assertEqual(0, len(instance.all_valid_timeserver_attestations)) + + # 2. The trusted time and the TUF clock override should be 0, epoch start. + self.assertEqual( + ['1970-01-01T00:00:00Z'], instance.all_valid_timeserver_times) + self.assertEqual(0, tuf.conf.CLOCK_OVERRIDE) + + # 3. The currently trusted Root metadata in the Secondary's updater for the + # Director repository should list the same Timeserver keyid as the + # Secondary has noted is the currently trusted Timeserver key's keyid. + # This should be the da9c6... key we know is in the sample rotated + # metadata. + self.assertEqual( + now_trusted_timeserver_keyid, + instance.updater.repositories['director'].metadata['current']['root'] + ['roles']['Timeserver']['keyids'][0]) self.assertEqual( 'da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc', now_trusted_timeserver_keyid) @@ -812,6 +1079,11 @@ def test_90_timeserver_key_rotation(self): + # TODO: Try a normal update to provide an additional test that the attack + # has been fully resolved? + + + # Run unit tests. From b8bdf4f02756677dfb46ad54459ccea4c2be96f3 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 26 Mar 2019 17:28:50 -0400 Subject: [PATCH 13/17] PR revision: fix bug in clock reset instruction Now that a local uptuf branch has had some issues fixed, I got through to this line and saw an obvious issue. :) Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index b2859d1..a60cef0 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -537,7 +537,7 @@ def refresh_toplevel_metadata_from_repositories(self): # occur at a lower level, in every root chain link step. # This will do for now, but fix the corner case by moving this # check. - self.update_timeserver_key_and_reset_clock(prior_timeserver_auth_info) + self.update_timeserver_key_and_reset_clock(new_timeserver_auth_info) # Since we failed to update and the Timeserver key changed, we try to # refresh again, since we may have failed because of a fast-forward # attack. From a036ab2bb7bf39d0147345bd0dcb3b84017e5bd1 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 26 Mar 2019 17:36:56 -0400 Subject: [PATCH 14/17] DEBUG: use uptuf branch with fixed expiration checks THIS COMMIT SHOULD NOT BE MERGED AND IS HERE FOR TESTING PURPOSES. The uptuf branch pointed to here is expected to be merged into develop, at which point this commit will cease to make sense. Signed-off-by: Sebastien Awwad --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 8281097..4d353ec 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,6 +4,6 @@ cryptography==2.3 pynacl==1.2.1 pyasn1==0.4.4 pycrypto==2.6.1 ---editable git://github.com/awwad/tuf.git@develop#egg=tuf +--editable git://github.com/awwad/tuf.git@remove_outdated_expiration_checks#egg=tuf --editable . tox==3.1.2 From 02e3a7f746102ebe8ccb14e471de5e1ff7f6da8a Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 26 Mar 2019 17:40:07 -0400 Subject: [PATCH 15/17] PR revision: DOC: minor test clarification comment in test_secondary Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 69401d6..70b0674 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -1042,12 +1042,16 @@ def test_95_timeserver_fastforward_attack(self): 'full_metadata_archive.zip') # Update to the new metadata with the new Timeserver key. + # This will involve an initial failure due to an apparently-expired set of + # metadata, but then when the newly trusted Root metadata is inspected, it + # will be found to have contained an updated Timeserver key, which will + # lead to tuf.conf.CLOCK_OVERRIDE being reset to 0, resolving the attack. + # An update attempt will then be repeated and succeed. instance.process_metadata(archive_with_rotated_key) now_trusted_timeserver_keyid = \ instance.timeserver_public_key['keyid'] - # CHECK RESULTING state: # 1. There should be zero trusted timeserver attestations immediately after From 8b726810a46f47d2ae5a84adc75659b49e3e0404 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 26 Mar 2019 17:41:54 -0400 Subject: [PATCH 16/17] PR revision: adjust timeserver ff atk test to anticipate PR 179 Adjust the timeserver fast-forward attack test in this PR to anticipate the merging of PR 179. PR 179 enforces that the Director repository is updated before other repositories. This is required by the Uptane Standard, and is also useful in handling the fast-forward attack. Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 70b0674..5a83788 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -981,20 +981,28 @@ def test_95_timeserver_fastforward_attack(self): 'full_metadata_archive.zip') - # TRY UPDATING during attack. This should fail with a NoWorkingMirrorError. - # The individual errors for each mirror should be ExpiredMetadataErrors. - # Check to make sure all the individual errors spooled in the - # NoWorkingMirrorError are each an ExpiredMetadataError. - try: + # TRY UPDATING during attack. This should fail with an + # ExpiredMetadataError, which will occur when we update the Root metadata + # but find it to be expired and so refuse to continue and update the other + # metadata. + with self.assertRaises(tuf.ExpiredMetadataError): instance.process_metadata(archive_with_old_timeserver_key) - except tuf.NoWorkingMirrorError as e: - for mirror in e.mirror_errors: - self.assertIsInstance(e.mirror_errors[mirror], tuf.ExpiredMetadataError) - else: - self.Fail( - 'Expected update to fail during attack and provide a ' - 'NoWorkingMirrorError, which we would then check to confirm ' - 'consists of one ExpiredMetadataError for each mirror.') + + # OLD, NOW WRONG: + # TRY UPDATING during attack. This should fail with a NoWorkingMirrorError + # # The individual errors for each mirror should be ExpiredMetadataErrors. + # # Check to make sure all the individual errors spooled in the + # # NoWorkingMirrorError are each an ExpiredMetadataError. + # try: + # instance.process_metadata(archive_with_old_timeserver_key) + # except tuf.NoWorkingMirrorError as e: + # for mirror in e.mirror_errors: + # self.assertIsInstance(e.mirror_errors[mirror], tuf.ExpiredMetadataError) + # else: + # self.Fail( + # 'Expected update to fail during attack and provide a ' + # 'NoWorkingMirrorError, which we would then check to confirm ' + # 'consists of one ExpiredMetadataError for each mirror.') @@ -1082,8 +1090,7 @@ def test_95_timeserver_fastforward_attack(self): initially_trusted_timeserver_keyid, now_trusted_timeserver_keyid) - - # TODO: Try a normal update to provide an additional test that the attack + # TODO: Try another update to provide an additional test that the attack # has been fully resolved? From 59b4ca2900d1c21ede512ec985906afcfca11c14 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 26 Mar 2019 18:02:01 -0400 Subject: [PATCH 17/17] Guarantee clients update the Director repo before Image repo This is important for conformance to the Uptane Standard and to be able to resolve Timeserver fast-forward attacks. Signed-off-by: Sebastien Awwad --- uptane/clients/primary.py | 20 +++++++++++++++++++- uptane/clients/secondary.py | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index c59a4a3..d8aefd2 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -344,6 +344,13 @@ def refresh_toplevel_metadata_from_repositories(self): # TODO: This function is duplicated in primary.py and secondary.py. It must # be moved to a general client.py as part of a fix to issue #14 # (github.com/uptane/uptane/issues/14). + + This can raise TUF update exceptions like + - tuf.ExpiredMetadataError: + if after attempts to update the Root metadata succeeded or failed, + whatever currently trusted Root metadata we ended up with was expired. + - tuf.NoWorkingMirrorError: + if we could not obtain and verify all necessary metadata """ # In order to provide Timeserver fast-forward attack protection, we do more @@ -365,8 +372,10 @@ def refresh_toplevel_metadata_from_repositories(self): prior_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] + # Refresh the Director first. If the Director refresh fails, we check to + # see if the Timeserver key has been rotated. try: - self.updater.refresh() + self.updater.refresh(repo_name=self.director_repo_name) except (tuf.NoWorkingMirrorError, tuf.ExpiredMetadataError): # TODO: <~> In the except line above, see if it's sufficient to only @@ -417,6 +426,15 @@ def refresh_toplevel_metadata_from_repositories(self): self.update_timeserver_key_and_reset_clock(new_timeserver_auth_info) + # Now that we've dealt with the Director repository, deal with any and all + # other repositories, presumably Image Repositories. + for repository_name in self.updater.repositories: + if repository_name == self.director_repo_name: + continue + + self.updater.refresh(repo_name=repository_name) + + diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index a60cef0..1ff7072 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -483,6 +483,13 @@ def refresh_toplevel_metadata_from_repositories(self): # TODO: This function is duplicated in primary.py and secondary.py. It must # be moved to a general client.py as part of a fix to issue #14 # (github.com/uptane/uptane/issues/14). + + This can raise TUF update exceptions like + - tuf.ExpiredMetadataError: + if after attempts to update the Root metadata succeeded or failed, + whatever currently trusted Root metadata we ended up with was expired. + - tuf.NoWorkingMirrorError: + if we could not obtain and verify all necessary metadata """ # In order to provide Timeserver fast-forward attack protection, we do more @@ -504,8 +511,10 @@ def refresh_toplevel_metadata_from_repositories(self): prior_timeserver_auth_info = self.updater.get_metadata( self.director_repo_name, 'current')['root']['roles']['Timeserver'] + # Refresh the Director first. If the Director refresh fails, we check to + # see if the Timeserver key has been rotated. try: - self.updater.refresh() + self.updater.refresh(repo_name=self.director_repo_name) except (tuf.NoWorkingMirrorError, tuf.ExpiredMetadataError): # TODO: <~> In the except line above, see if it's sufficient to only @@ -556,6 +565,15 @@ def refresh_toplevel_metadata_from_repositories(self): self.update_timeserver_key_and_reset_clock(new_timeserver_auth_info) + # Now that we've dealt with the Director repository, deal with any and all + # other repositories, presumably Image Repositories. + for repository_name in self.updater.repositories: + if repository_name == self.director_repo_name: + continue + + self.updater.refresh(repo_name=repository_name) + +