lib

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

commit 36aeec6ed6190fd16046dc10cc6926e5fbb2cc07
parent 5f094b213b982a2e1693618d202c9e2058a2c65f
Author: triesap <tyson@radroots.org>
Date:   Wed,  5 Aug 2026 17:31:32 +0000

test(secrets): freeze envelope v1 and v2 wire vectors

Diffstat:
Acrates/secrets/tests/wire_vectors.rs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 110 insertions(+), 0 deletions(-)

diff --git a/crates/secrets/tests/wire_vectors.rs b/crates/secrets/tests/wire_vectors.rs @@ -0,0 +1,110 @@ +use chacha20poly1305::aead::{Aead, KeyInit, Payload}; +use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; + +const V1_ENVELOPE_HEX: &str = "52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d"; +const V2_CONTEXT_HEX: &str = "0001726164726f6f74732e656e76656c6f70655f636f6e746578742e76310019726164726f6f74732e707269766174655f61727469666163740010707269766174655f617274696661637400203031303130313031303130313031303130313031303130313031303130313031001674726164652e707269766174655f7465726d732e7631"; +const V2_HEADER_HEX: &str = "52525331000201010100000007000c656e76656c6f70652d6b65790001726164726f6f74732e656e76656c6f70655f636f6e746578742e76310019726164726f6f74732e707269766174655f61727469666163740010707269766174655f617274696661637400203031303130313031303130313031303130313031303130313031303130313031001674726164652e707269766174655f7465726d732e7631222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028"; +const V2_CIPHERTEXT_HEX: &str = + "f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596f762615d1c221c5c10964c4f799d5d59"; +const V2_ENVELOPE_HEX: &str = "52525331000201010100000007000c656e76656c6f70652d6b65790001726164726f6f74732e656e76656c6f70655f636f6e746578742e76310019726164726f6f74732e707269766174655f61727469666163740010707269766174655f617274696661637400203031303130313031303130313031303130313031303130313031303130313031001674726164652e707269766174655f7465726d732e7631222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596f762615d1c221c5c10964c4f799d5d59"; + +fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.private_artifact").expect("purpose"), + EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") + .expect("subject"), + PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), + ) +} + +fn independent_v2_vector() -> (Vec<u8>, Vec<u8>, Vec<u8>) { + let context = context().to_canonical_bytes(); + let mut header = Vec::new(); + header.extend_from_slice(b"RRS1"); + header.extend_from_slice(&2_u16.to_be_bytes()); + header.extend_from_slice(&[1, 1, 1]); + header.extend_from_slice(&7_u32.to_be_bytes()); + header.extend_from_slice(&12_u16.to_be_bytes()); + header.extend_from_slice(b"envelope-key"); + header.extend_from_slice(&context); + header.extend_from_slice(&[0x22; 24]); + header.extend_from_slice(&32_u32.to_be_bytes()); + header.extend_from_slice(&[0x4b; 32]); + header.extend_from_slice(&40_u32.to_be_bytes()); + + let cipher = XChaCha20Poly1305::new(Key::from_slice(&[0x11; 32])); + let ciphertext = cipher + .encrypt( + XNonce::from_slice(&[0x22; 24]), + Payload { + msg: b"radroots envelope vector", + aad: &header, + }, + ) + .expect("independent encryption"); + let mut envelope = header.clone(); + envelope.extend_from_slice(&ciphertext); + (header, ciphertext, envelope) +} + +#[test] +fn v1_corpus_is_stable_and_strictly_bounded() { + let bytes = hex::decode(V1_ENVELOPE_HEX).expect("v1 vector hex"); + assert_eq!(&bytes[..4], b"RRS1"); + assert_eq!(u16::from_be_bytes([bytes[4], bytes[5]]), 1); + assert!(bytes.len() < radroots_secrets::envelope::ENVELOPE_MAX_BYTES); + let decoded = radroots_secrets::EncryptedEnvelope::decode(&bytes).expect("v1 corpus decode"); + assert_eq!(decoded.encode().expect("v1 re-encode"), bytes); +} + +#[test] +fn v2_vector_freezes_context_header_ciphertext_and_envelope() { + let (header, ciphertext, envelope) = independent_v2_vector(); + assert_eq!(hex::encode(context().to_canonical_bytes()), V2_CONTEXT_HEX); + assert_eq!(hex::encode(header), V2_HEADER_HEX); + assert_eq!(hex::encode(ciphertext), V2_CIPHERTEXT_HEX); + assert_eq!(hex::encode(envelope), V2_ENVELOPE_HEX); +} + +#[test] +fn v2_header_positions_and_length_prefixes_are_exact() { + let (header, ciphertext, envelope) = independent_v2_vector(); + let context_bytes = context().to_canonical_bytes(); + assert_eq!(&header[..4], b"RRS1"); + assert_eq!(&header[4..6], &2_u16.to_be_bytes()); + assert_eq!(&header[6..9], &[1, 1, 1]); + assert_eq!(&header[9..13], &7_u32.to_be_bytes()); + assert_eq!(&header[13..15], &12_u16.to_be_bytes()); + assert_eq!(&header[15..27], b"envelope-key"); + assert_eq!(&header[27..27 + context_bytes.len()], &context_bytes); + let nonce_offset = 27 + context_bytes.len(); + assert_eq!(&header[nonce_offset..nonce_offset + 24], &[0x22; 24]); + let wrapped_length_offset = nonce_offset + 24; + assert_eq!( + &header[wrapped_length_offset..wrapped_length_offset + 4], + &32_u32.to_be_bytes() + ); + assert_eq!( + &header[wrapped_length_offset + 4..wrapped_length_offset + 36], + &[0x4b; 32] + ); + assert_eq!(&header[header.len() - 4..], &40_u32.to_be_bytes()); + assert_eq!(ciphertext.len(), 40); + assert_eq!(envelope.len(), header.len() + ciphertext.len()); +} + +#[test] +fn vector_corruption_and_truncation_change_authenticated_material() { + let (_, _, envelope) = independent_v2_vector(); + for offset in [0, 4, 6, 8, 9, 13, 15, 27, envelope.len() - 1] { + let mut corrupted = envelope.clone(); + corrupted[offset] ^= 1; + assert_ne!(hex::encode(corrupted), V2_ENVELOPE_HEX); + } + for length in [0, 1, 4, 6, 27, envelope.len() - 1] { + assert_ne!(hex::encode(&envelope[..length]), V2_ENVELOPE_HEX); + } +}