22///
33/// `validate_chain` walks from a leaf capability proof back to the root, verifying:
44/// 1. Every node's ed25519 signature is cryptographically valid.
5- /// 2. Every intermediate node's signature is by the key it claims as issuer.
6- /// 3. Child rights ⊆ parent rights (attenuation invariant, Bug 5 fix).
5+ /// 2. Every node's epoch >= min_epoch (AT-3.1 fix: intermediate delegation nodes
6+ /// issued in a compromised epoch cannot serve as valid chain links).
7+ /// 3. Every intermediate node's issuer_pubkey binds to the parent's subject_id
8+ /// via SHA-256 (AT-5.1 fix: closes delegation impersonation gap).
9+ /// 4. Child rights ⊆ parent rights (attenuation invariant).
710///
811/// The root is identified by `IssuerRef::Root` — its signature is verified
912/// against the root key passed into verify(). No other node is implicitly trusted.
13+ ///
14+ /// Identity model: subject_id = SHA-256(pubkey). This is the invariant that
15+ /// AT-5.1 relies on. All capability issuers must have their pubkey enrolled
16+ /// as SHA-256(pubkey) = subject_id in the granting proof.
17+ #![ forbid( unsafe_code) ]
18+
1019use ed25519_dalek:: { Signature , VerifyingKey , Verifier } ;
20+ use sha2:: { Digest , Sha256 } ;
1121use crate :: tcb:: types:: { CapabilityProof , IssuerRef } ;
1222
1323const MAX_CHAIN_DEPTH : usize = 16 ;
@@ -17,11 +27,15 @@ const MAX_CHAIN_DEPTH: usize = 16;
1727/// `all_proofs` is the full bundle from `CanonicalAction` — intermediate nodes
1828/// must be present here. Proofs absent from the bundle cannot be trusted.
1929///
30+ /// `min_epoch` is the caller's minimum required epoch. Every node in the chain
31+ /// must have `epoch >= min_epoch` (AT-3.1 fix).
32+ ///
2033/// Returns `Ok(())` if the chain is valid; `Err(reason)` otherwise.
2134pub fn validate_chain (
2235 leaf : & CapabilityProof ,
2336 all_proofs : & [ CapabilityProof ] ,
2437 root_key : & VerifyingKey ,
38+ min_epoch : u64 ,
2539) -> Result < ( ) , & ' static str > {
2640 let mut current = leaf;
2741 let mut depth = 0usize ;
@@ -32,50 +46,50 @@ pub fn validate_chain(
3246 }
3347 depth += 1 ;
3448
35- let msg = current. signing_message ( ) ;
49+ // AT-3.1 fix: epoch check on every chain node, not just the leaf.
50+ // A delegator whose key was compromised in epoch N cannot serve as a
51+ // valid chain link even if the leaf was reissued in a later epoch.
52+ if current. epoch < min_epoch {
53+ return Err ( "delegation chain node epoch predates minimum required epoch" ) ;
54+ }
3655
56+ let msg = current. signing_message ( ) ;
3757 let sig = Signature :: from_bytes ( & current. signature )
3858 . map_err ( |_| "malformed signature encoding" ) ?;
3959
4060 match & current. issuer {
4161 IssuerRef :: Root => {
42- // This node claims to be root-signed — verify against root_key.
4362 root_key
4463 . verify ( & msg, & sig)
4564 . map_err ( |_| "root signature verification failed" ) ?;
46- // Reached root without violation — chain is valid.
4765 return Ok ( ( ) ) ;
4866 }
4967
5068 IssuerRef :: Delegated { parent_hash } => {
51- // Verify this node's signature with the issuer's own key.
52- // We cannot trust self-reported issuer_pubkey blindly —
53- // it will be validated when we traverse up to the parent.
5469 let issuer_key = VerifyingKey :: from_bytes ( & current. issuer_pubkey )
5570 . map_err ( |_| "malformed issuer pubkey in proof" ) ?;
5671 issuer_key
5772 . verify ( & msg, & sig)
5873 . map_err ( |_| "intermediate signature verification failed" ) ?;
5974
60- // Locate parent in the bundle — reject if absent.
6175 let parent = all_proofs
6276 . iter ( )
6377 . find ( |p| p. proof_hash == * parent_hash)
6478 . ok_or ( "parent proof not found in bundle" ) ?;
6579
66- // Verify that `issuer_pubkey` in this node matches what the parent
67- // was issued to. This closes the impersonation gap:
68- // a proof claiming a different issuer's key would fail here.
69- if current. issuer_pubkey != parent. issuer_pubkey
70- && matches ! ( parent. issuer, IssuerRef :: Root )
71- {
72- // At root level, the "issuer pubkey" of children must come from
73- // what root actually granted — checked via signature above.
74- // No additional check needed here; root_key verify in next iteration.
80+ // AT-5.1 fix: the principal this node claims as issuer must be
81+ // the same principal the parent capability was issued to.
82+ // Identity model: subject_id = SHA-256(pubkey).
83+ // Without this, an attacker who knows the parent proof can sign a
84+ // child with their own key and set parent_hash to the real parent —
85+ // the chain would traverse correctly despite no key from the parent's
86+ // subject ever being used.
87+ let claimed_issuer_id: [ u8 ; 32 ] = Sha256 :: digest ( & current. issuer_pubkey ) . into ( ) ;
88+ if claimed_issuer_id != parent. subject_id {
89+ return Err ( "issuer pubkey does not correspond to parent subject identity" ) ;
7590 }
7691
77- // Bug 5: Attenuation enforcement.
78- // A delegator cannot grant rights they don't possess.
92+ // Attenuation: a delegator cannot grant rights they don't possess.
7993 if ( current. rights & !parent. rights ) != 0 {
8094 return Err ( "attenuation violation: child rights exceed parent" ) ;
8195 }
@@ -94,6 +108,10 @@ mod tests {
94108 use rand_core:: OsRng ;
95109 use sha2:: { Digest , Sha256 } ;
96110
111+ fn subject_id_of ( sk : & SigningKey ) -> [ u8 ; 32 ] {
112+ Sha256 :: digest ( sk. verifying_key ( ) . to_bytes ( ) ) . into ( )
113+ }
114+
97115 fn make_root_proof (
98116 root_sk : & SigningKey ,
99117 subject_id : [ u8 ; 32 ] ,
@@ -116,60 +134,114 @@ mod tests {
116134 } ;
117135 let msg = p. signing_message ( ) ;
118136 p. signature = root_sk. sign ( & msg) . to_bytes ( ) ;
119- let hash: [ u8 ; 32 ] = Sha256 :: digest ( p. to_canonical_bytes ( ) ) . into ( ) ;
120- p. proof_hash = hash;
137+ p. proof_hash = Sha256 :: digest ( p. to_canonical_bytes ( ) ) . into ( ) ;
138+ p
139+ }
140+
141+ fn make_delegated_proof (
142+ delegator_sk : & SigningKey ,
143+ parent : & CapabilityProof ,
144+ subject_id : [ u8 ; 32 ] ,
145+ resource_hash : [ u8 ; 32 ] ,
146+ rights : u64 ,
147+ expiry : u64 ,
148+ epoch : u64 ,
149+ ) -> CapabilityProof {
150+ let issuer_pubkey = delegator_sk. verifying_key ( ) . to_bytes ( ) ;
151+ let mut p = CapabilityProof {
152+ proof_hash : [ 0u8 ; 32 ] ,
153+ subject_id,
154+ resource_hash,
155+ rights,
156+ expiry,
157+ epoch,
158+ issuer : IssuerRef :: Delegated { parent_hash : parent. proof_hash } ,
159+ signature : [ 0u8 ; 64 ] ,
160+ issuer_pubkey,
161+ } ;
162+ let msg = p. signing_message ( ) ;
163+ p. signature = delegator_sk. sign ( & msg) . to_bytes ( ) ;
164+ p. proof_hash = Sha256 :: digest ( p. to_canonical_bytes ( ) ) . into ( ) ;
121165 p
122166 }
123167
168+ const RESOURCE : [ u8 ; 32 ] = [ 0x02 ; 32 ] ;
169+ const MIN_EPOCH : u64 = 1 ;
170+
124171 #[ test]
125172 fn valid_root_proof_passes ( ) {
126173 let root_sk = SigningKey :: generate ( & mut OsRng ) ;
127174 let root_vk = root_sk. verifying_key ( ) ;
128- let proof = make_root_proof (
129- & root_sk,
130- [ 1u8 ; 32 ] ,
131- [ 2u8 ; 32 ] ,
132- RIGHT_READ ,
133- u64:: MAX ,
134- 1 ,
135- ) ;
136- assert ! ( validate_chain( & proof, & [ proof. clone( ) ] , & root_vk) . is_ok( ) ) ;
175+ let proof = make_root_proof ( & root_sk, [ 1u8 ; 32 ] , RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
176+ assert ! ( validate_chain( & proof, & [ proof. clone( ) ] , & root_vk, MIN_EPOCH ) . is_ok( ) ) ;
137177 }
138178
139179 #[ test]
140180 fn wrong_root_key_fails ( ) {
141181 let root_sk = SigningKey :: generate ( & mut OsRng ) ;
142182 let other_sk = SigningKey :: generate ( & mut OsRng ) ;
143183 let other_vk = other_sk. verifying_key ( ) ;
144- let proof = make_root_proof ( & root_sk, [ 1u8 ; 32 ] , [ 2u8 ; 32 ] , RIGHT_READ , u64:: MAX , 1 ) ;
145- assert ! ( validate_chain( & proof, & [ proof. clone( ) ] , & other_vk) . is_err( ) ) ;
184+ let proof = make_root_proof ( & root_sk, [ 1u8 ; 32 ] , RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
185+ assert ! ( validate_chain( & proof, & [ proof. clone( ) ] , & other_vk, MIN_EPOCH ) . is_err( ) ) ;
146186 }
147187
148188 #[ test]
149189 fn attenuation_violation_rejected ( ) {
150190 let root_sk = SigningKey :: generate ( & mut OsRng ) ;
151191 let root_vk = root_sk. verifying_key ( ) ;
152- // Parent has READ only; child claims READ|WRITE — must be rejected.
153- let parent = make_root_proof ( & root_sk, [ 1u8 ; 32 ] , [ 2u8 ; 32 ] , RIGHT_READ , u64:: MAX , 1 ) ;
154192 let child_sk = SigningKey :: generate ( & mut OsRng ) ;
155- let parent_hash = parent. proof_hash ;
156- let issuer_pubkey = child_sk. verifying_key ( ) . to_bytes ( ) ;
157- let mut child = CapabilityProof {
158- proof_hash : [ 0u8 ; 32 ] ,
159- subject_id : [ 3u8 ; 32 ] ,
160- resource_hash : [ 2u8 ; 32 ] ,
161- rights : RIGHT_READ | RIGHT_WRITE , // violation
162- expiry : u64:: MAX ,
163- epoch : 1 ,
164- issuer : IssuerRef :: Delegated { parent_hash } ,
165- signature : [ 0u8 ; 64 ] ,
166- issuer_pubkey,
167- } ;
168- let msg = child. signing_message ( ) ;
169- child. signature = child_sk. sign ( & msg) . to_bytes ( ) ;
170- child. proof_hash = Sha256 :: digest ( child. to_canonical_bytes ( ) ) . into ( ) ;
171- let result = validate_chain ( & child, & [ parent. clone ( ) , child. clone ( ) ] , & root_vk) ;
193+ // Parent grants READ to child_sk's identity.
194+ let parent = make_root_proof ( & root_sk, subject_id_of ( & child_sk) , RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
195+ // Child claims READ|WRITE — attenuation violation.
196+ let child = make_delegated_proof ( & child_sk, & parent, [ 3u8 ; 32 ] , RESOURCE , RIGHT_READ | RIGHT_WRITE , u64:: MAX , 1 ) ;
197+ let result = validate_chain ( & child, & [ parent. clone ( ) , child. clone ( ) ] , & root_vk, MIN_EPOCH ) ;
172198 assert ! ( result. is_err( ) ) ;
173199 assert ! ( result. unwrap_err( ) . contains( "attenuation" ) ) ;
174200 }
201+
202+ // AT-5.1: attacker with a different key cannot forge delegation from [0xAA;32]
203+ #[ test]
204+ fn at5_delegation_impersonation_rejected ( ) {
205+ let root_sk = SigningKey :: generate ( & mut OsRng ) ;
206+ let root_vk = root_sk. verifying_key ( ) ;
207+ let attacker_sk = SigningKey :: generate ( & mut OsRng ) ;
208+ // Parent issued to [0xAA;32] — NOT to attacker_sk's identity.
209+ let parent = make_root_proof ( & root_sk, [ 0xAA ; 32 ] , RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
210+ // Attacker signs a child claiming delegation from that parent, using their own key.
211+ let fake_child = make_delegated_proof ( & attacker_sk, & parent, [ 0x01 ; 32 ] , RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
212+ let result = validate_chain ( & fake_child, & [ parent. clone ( ) , fake_child. clone ( ) ] , & root_vk, MIN_EPOCH ) ;
213+ assert ! ( result. is_err( ) ) ;
214+ assert ! ( result. unwrap_err( ) . contains( "issuer pubkey" ) ) ;
215+ }
216+
217+ // AT-3.1: intermediate node with stale epoch fails even if leaf epoch is fresh
218+ #[ test]
219+ fn at3_1_intermediate_stale_epoch_rejected ( ) {
220+ let root_sk = SigningKey :: generate ( & mut OsRng ) ;
221+ let root_vk = root_sk. verifying_key ( ) ;
222+ let delegator_sk = SigningKey :: generate ( & mut OsRng ) ;
223+ let next_sk = SigningKey :: generate ( & mut OsRng ) ;
224+ // Parent issued at epoch 0 (stale).
225+ let parent = make_root_proof ( & root_sk, subject_id_of ( & delegator_sk) , RESOURCE , RIGHT_READ , u64:: MAX , 0 ) ;
226+ // Child at fresh epoch — but its parent node is stale.
227+ let child = make_delegated_proof ( & delegator_sk, & parent, subject_id_of ( & next_sk) , RESOURCE , RIGHT_READ , u64:: MAX , 5 ) ;
228+ let result = validate_chain ( & child, & [ parent. clone ( ) , child. clone ( ) ] , & root_vk, 5 ) ;
229+ assert ! ( result. is_err( ) ) ;
230+ assert ! ( result. unwrap_err( ) . contains( "epoch" ) ) ;
231+ }
232+
233+ // Valid two-level chain with proper identity binding
234+ #[ test]
235+ fn valid_two_level_chain ( ) {
236+ let root_sk = SigningKey :: generate ( & mut OsRng ) ;
237+ let root_vk = root_sk. verifying_key ( ) ;
238+ let delegator_sk = SigningKey :: generate ( & mut OsRng ) ;
239+ let actor = [ 0x01 ; 32 ] ;
240+ // Parent granted to delegator_sk's identity.
241+ let parent = make_root_proof ( & root_sk, subject_id_of ( & delegator_sk) , RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
242+ // delegator_sk issues to actor.
243+ let child = make_delegated_proof ( & delegator_sk, & parent, actor, RESOURCE , RIGHT_READ , u64:: MAX , 1 ) ;
244+ let result = validate_chain ( & child, & [ parent. clone ( ) , child. clone ( ) ] , & root_vk, MIN_EPOCH ) ;
245+ assert ! ( result. is_ok( ) ) ;
246+ }
175247}
0 commit comments