lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }