envelope_contract.rs (13815B)
1 use futures_executor::block_on; 2 use radroots_secrets::context::{ 3 EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, 4 }; 5 use radroots_secrets::envelope::{ 6 Cipher, ENVELOPE_VERSION, KeySource, LegacyV1ResealAuthority, Nonce, SealMaterial, SealRequest, 7 }; 8 use radroots_secrets::error::Operation; 9 use radroots_secrets::id::{BackendKind, KeyVersion}; 10 use radroots_secrets::wrapping::{ 11 BoxFuture, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, 12 }; 13 use radroots_secrets::{EncryptedEnvelope, Error, KeyWrapping, SecretId, SecretRef}; 14 15 struct VectorWrapping; 16 17 impl KeyWrapping for VectorWrapping { 18 fn wrap<'a>(&'a self, request: WrapRequest<'a>) -> BoxFuture<'a, Result<WrappedSecret, Error>> { 19 Box::pin(async move { 20 if request.reference().id().as_str() != "envelope-key" { 21 return Err(Error::BackendFailure { 22 backend: BackendKind::Memory, 23 operation: Operation::Wrap, 24 }); 25 } 26 let wrapped = request 27 .plaintext() 28 .expose_secret(|bytes| bytes.iter().map(|byte| byte ^ 0x5A).collect::<Vec<_>>()); 29 WrappedSecret::from_bytes(wrapped) 30 }) 31 } 32 33 fn unwrap<'a>( 34 &'a self, 35 request: UnwrapRequest<'a>, 36 ) -> BoxFuture<'a, Result<SecretMaterial, Error>> { 37 Box::pin(async move { 38 if request.reference().id().as_str() != "envelope-key" { 39 return Err(Error::BackendFailure { 40 backend: BackendKind::Memory, 41 operation: Operation::Unwrap, 42 }); 43 } 44 let plaintext = request 45 .wrapped() 46 .as_bytes() 47 .iter() 48 .map(|byte| byte ^ 0x5A) 49 .collect::<Vec<_>>(); 50 SecretMaterial::from_slice(plaintext.as_slice()) 51 }) 52 } 53 54 fn unwrap_legacy_v1<'a>( 55 &'a self, 56 request: LegacyV1UnwrapRequest<'a>, 57 ) -> BoxFuture<'a, Result<SecretMaterial, Error>> { 58 Box::pin(async move { 59 if request.reference().id().as_str() != "envelope-key" { 60 return Err(Error::BackendFailure { 61 backend: BackendKind::Memory, 62 operation: Operation::Unwrap, 63 }); 64 } 65 let plaintext = request 66 .wrapped() 67 .as_bytes() 68 .iter() 69 .map(|byte| byte ^ 0x5A) 70 .collect::<Vec<_>>(); 71 SecretMaterial::from_slice(plaintext.as_slice()) 72 }) 73 } 74 } 75 76 fn reference() -> SecretRef { 77 SecretRef::new( 78 SecretId::parse("envelope-key").expect("valid id"), 79 BackendKind::Memory, 80 KeyVersion::new(7).expect("valid version"), 81 ) 82 } 83 84 fn context() -> EnvelopeContext { 85 EnvelopeContext::new( 86 EnvelopePurpose::parse("radroots.private_artifact").expect("purpose"), 87 EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") 88 .expect("subject"), 89 PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), 90 ) 91 } 92 93 fn seal(plaintext: &[u8]) -> EncryptedEnvelope { 94 let plaintext = SecretMaterial::from_slice(plaintext).expect("plaintext"); 95 let data_key = SecretMaterial::from_slice(&[0x11; 32]).expect("data key"); 96 block_on(EncryptedEnvelope::seal( 97 &VectorWrapping, 98 SealRequest::new( 99 reference(), 100 context(), 101 &plaintext, 102 SealMaterial::new(data_key, Nonce::new([0x22; 24])), 103 ), 104 )) 105 .expect("seal") 106 } 107 108 #[test] 109 fn deterministic_envelope_vector_round_trips() { 110 let envelope = seal(b"radroots envelope vector"); 111 assert_eq!(envelope.version(), ENVELOPE_VERSION); 112 assert_eq!(envelope.cipher(), Cipher::XChaCha20Poly1305); 113 assert_eq!(envelope.key_source(), KeySource::ProviderWrapped); 114 assert_eq!(envelope.reference().id().as_str(), "envelope-key"); 115 116 let encoded = envelope.encode().expect("encode"); 117 assert_eq!( 118 hex::encode(&encoded), 119 "52525331000201010100000007000c656e76656c6f70652d6b65790001726164726f6f74732e656e76656c6f70655f636f6e746578742e76310019726164726f6f74732e707269766174655f61727469666163740010707269766174655f617274696661637400203031303130313031303130313031303130313031303130313031303130313031001674726164652e707269766174655f7465726d732e7631222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596f762615d1c221c5c10964c4f799d5d59" 120 ); 121 let decoded = EncryptedEnvelope::decode(&encoded).expect("decode"); 122 let opened = block_on(decoded.open(&VectorWrapping, &context())).expect("open"); 123 opened.expose_secret(|bytes| assert_eq!(bytes, b"radroots envelope vector")); 124 } 125 126 #[test] 127 fn tamper_nonce_and_ciphertext_are_rejected() { 128 let encoded = seal(b"authenticated plaintext").encode().expect("encode"); 129 130 let mut nonce_tamper = encoded.clone(); 131 let nonce_offset = 132 4 + 2 + 1 + 1 + 1 + 4 + 2 + "envelope-key".len() + context().to_canonical_bytes().len(); 133 nonce_tamper[nonce_offset] ^= 0x01; 134 let envelope = EncryptedEnvelope::decode(&nonce_tamper).expect("decode nonce tamper"); 135 assert!(matches!( 136 block_on(envelope.open(&VectorWrapping, &context())), 137 Err(Error::DecryptFailed) 138 )); 139 140 let mut ciphertext_tamper = encoded; 141 let last = ciphertext_tamper.last_mut().expect("ciphertext byte"); 142 *last ^= 0x01; 143 let envelope = EncryptedEnvelope::decode(&ciphertext_tamper).expect("decode ciphertext tamper"); 144 assert!(matches!( 145 block_on(envelope.open(&VectorWrapping, &context())), 146 Err(Error::DecryptFailed) 147 )); 148 } 149 150 #[test] 151 fn context_mismatch_and_authenticated_context_tamper_fail_closed() { 152 let encoded = seal(b"context-bound plaintext").encode().expect("encode"); 153 let different = EnvelopeContext::new( 154 EnvelopePurpose::parse("radroots.different_artifact").expect("purpose"), 155 EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") 156 .expect("subject"), 157 PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), 158 ); 159 let envelope = EncryptedEnvelope::decode(&encoded).expect("decode"); 160 assert_eq!( 161 block_on(envelope.open(&VectorWrapping, &different)).err(), 162 Some(Error::ContextMismatch) 163 ); 164 165 let purpose = b"radroots.private_artifact"; 166 let purpose_offset = encoded 167 .windows(purpose.len()) 168 .position(|window| window == purpose) 169 .expect("purpose offset"); 170 let mut tampered = encoded; 171 tampered[purpose_offset] = b's'; 172 let envelope = EncryptedEnvelope::decode(&tampered).expect("valid altered context"); 173 let altered = envelope.context().expect("v2 context").clone(); 174 assert_eq!( 175 block_on(envelope.open(&VectorWrapping, &altered)).err(), 176 Some(Error::DecryptFailed) 177 ); 178 } 179 180 #[test] 181 fn normal_open_denies_the_frozen_v1_corpus() { 182 let legacy = hex::decode("52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d").expect("legacy hex"); 183 let envelope = EncryptedEnvelope::decode(&legacy).expect("legacy decode"); 184 assert_eq!(envelope.version(), 1); 185 assert!(envelope.context().is_none()); 186 assert_eq!( 187 block_on(envelope.open(&VectorWrapping, &context())).err(), 188 Some(Error::LegacyEnvelopeDenied) 189 ); 190 } 191 192 #[test] 193 fn explicit_legacy_open_and_fresh_reseal_preserve_plaintext() { 194 let legacy = hex::decode("52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d").expect("legacy hex"); 195 let envelope = EncryptedEnvelope::decode(&legacy).expect("legacy decode"); 196 let authority = LegacyV1ResealAuthority::new(); 197 let expected_reference = reference(); 198 let opened = block_on(envelope.open_legacy_v1( 199 &VectorWrapping, 200 &authority, 201 &expected_reference, 202 &context(), 203 )) 204 .expect("legacy open"); 205 opened.expose_secret(|bytes| assert_eq!(bytes, b"radroots envelope vector")); 206 207 let result = block_on(envelope.reseal_legacy_v1( 208 &VectorWrapping, 209 &authority, 210 &expected_reference, 211 reference(), 212 context(), 213 &|bytes: &[u8]| bytes == b"radroots envelope vector", 214 SealMaterial::new( 215 SecretMaterial::from_slice(&[0x33; 32]).expect("fresh key"), 216 Nonce::new([0x44; 24]), 217 ), 218 )) 219 .expect("legacy reseal"); 220 assert_eq!(result.envelope().version(), ENVELOPE_VERSION); 221 assert_ne!(result.plaintext_commitment(), &[0; 32]); 222 let resealed = result.into_envelope(); 223 let opened = block_on(resealed.open(&VectorWrapping, &context())).expect("open reseal"); 224 opened.expose_secret(|bytes| assert_eq!(bytes, b"radroots envelope vector")); 225 } 226 227 #[test] 228 fn legacy_reseal_rejects_wrong_authority_inputs_validation_and_entropy_reuse() { 229 let legacy = hex::decode("52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d").expect("legacy hex"); 230 let envelope = EncryptedEnvelope::decode(&legacy).expect("legacy decode"); 231 let authority = LegacyV1ResealAuthority::new(); 232 let wrong_reference = SecretRef::new( 233 SecretId::parse("wrong-key").expect("id"), 234 BackendKind::Memory, 235 KeyVersion::new(7).expect("version"), 236 ); 237 assert_eq!( 238 block_on(envelope.open_legacy_v1( 239 &VectorWrapping, 240 &authority, 241 &wrong_reference, 242 &context(), 243 )) 244 .err(), 245 Some(Error::ProviderReferenceMismatch) 246 ); 247 248 let expected_reference = reference(); 249 let invalid = block_on(envelope.reseal_legacy_v1( 250 &VectorWrapping, 251 &authority, 252 &expected_reference, 253 reference(), 254 context(), 255 &|_: &[u8]| false, 256 SealMaterial::new( 257 SecretMaterial::from_slice(&[0x33; 32]).expect("fresh key"), 258 Nonce::new([0x44; 24]), 259 ), 260 )); 261 assert!(matches!(invalid, Err(Error::LegacyPayloadValidationFailed))); 262 263 let reused_nonce = block_on(envelope.reseal_legacy_v1( 264 &VectorWrapping, 265 &authority, 266 &expected_reference, 267 reference(), 268 context(), 269 &|_: &[u8]| true, 270 SealMaterial::new( 271 SecretMaterial::from_slice(&[0x33; 32]).expect("fresh key"), 272 Nonce::new([0x22; 24]), 273 ), 274 )); 275 assert!(matches!(reused_nonce, Err(Error::LegacyEntropyReuse))); 276 277 let reused_key = block_on(envelope.reseal_legacy_v1( 278 &VectorWrapping, 279 &authority, 280 &expected_reference, 281 reference(), 282 context(), 283 &|_: &[u8]| true, 284 SealMaterial::new( 285 SecretMaterial::from_slice(&[0x11; 32]).expect("legacy key"), 286 Nonce::new([0x44; 24]), 287 ), 288 )); 289 assert!(matches!(reused_key, Err(Error::LegacyEntropyReuse))); 290 } 291 292 #[test] 293 fn wrong_key_slot_and_invalid_version_fail_closed() { 294 let encoded = seal(b"slot-bound plaintext").encode().expect("encode"); 295 let id_offset = 4 + 2 + 1 + 1 + 1 + 4 + 2; 296 let mut wrong_slot = encoded.clone(); 297 wrong_slot[id_offset] = b'x'; 298 let envelope = EncryptedEnvelope::decode(&wrong_slot).expect("decode slot tamper"); 299 assert!(matches!( 300 block_on(envelope.open(&VectorWrapping, &context())), 301 Err(Error::BackendFailure { 302 backend: BackendKind::Memory, 303 operation: Operation::Unwrap, 304 }) 305 )); 306 307 let mut bad_version = encoded; 308 bad_version[5] = 3; 309 assert!(matches!( 310 EncryptedEnvelope::decode(&bad_version), 311 Err(Error::UnsupportedEnvelopeVersion { version: 3 }) 312 )); 313 } 314 315 #[test] 316 fn malformed_lengths_and_wrong_data_key_lengths_are_rejected() { 317 assert!(matches!( 318 EncryptedEnvelope::decode(b"short"), 319 Err(Error::EnvelopeMalformed) 320 )); 321 322 let plaintext = SecretMaterial::from_slice(b"payload").expect("plaintext"); 323 let short_key = SecretMaterial::from_slice(&[0x11; 31]).expect("short key"); 324 assert!(matches!( 325 block_on(EncryptedEnvelope::seal( 326 &VectorWrapping, 327 SealRequest::new( 328 reference(), 329 context(), 330 &plaintext, 331 SealMaterial::new(short_key, Nonce::new([0x22; 24])), 332 ), 333 )), 334 Err(Error::InvalidDataKeyLength { actual_bytes: 31 }) 335 )); 336 } 337 338 #[cfg(feature = "serde")] 339 #[test] 340 fn serde_uses_the_validated_binary_envelope() { 341 let envelope = seal(b"serde envelope"); 342 let json = serde_json::to_vec(&envelope).expect("serialize envelope"); 343 let decoded: EncryptedEnvelope = serde_json::from_slice(&json).expect("deserialize envelope"); 344 let opened = block_on(decoded.open(&VectorWrapping, &context())).expect("open"); 345 opened.expose_secret(|bytes| assert_eq!(bytes, b"serde envelope")); 346 } 347 348 #[test] 349 fn envelope_diagnostics_are_redacted() { 350 let diagnostic = format!("{:?}", seal(b"must-not-appear")); 351 assert!(diagnostic.contains("<redacted>")); 352 assert!(!diagnostic.contains("must-not-appear")); 353 assert!(!diagnostic.contains("envelope-key")); 354 }