context_contract.rs (4404B)
1 use radroots_secrets::Error; 2 use radroots_secrets::context::{ 3 ENVELOPE_CONTEXT_DOMAIN, ENVELOPE_CONTEXT_VERSION, ENVELOPE_PURPOSE_MAX_BYTES, 4 ENVELOPE_SUBJECT_TYPE_MAX_BYTES, ENVELOPE_SUBJECT_VALUE_MAX_BYTES, EnvelopeContext, 5 EnvelopePurpose, EnvelopeSubject, PAYLOAD_SCHEMA_MAX_BYTES, PayloadSchemaId, 6 }; 7 use radroots_secrets::error::{ContextField, ContextValueError}; 8 9 fn context() -> EnvelopeContext { 10 EnvelopeContext::new( 11 EnvelopePurpose::parse("radroots.private_artifact").expect("purpose"), 12 EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") 13 .expect("subject"), 14 PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), 15 ) 16 } 17 18 #[test] 19 fn context_encoding_is_canonical_and_domain_separated() { 20 let encoded = context().to_canonical_bytes(); 21 assert_eq!(&encoded[..2], &ENVELOPE_CONTEXT_VERSION.to_be_bytes()); 22 assert_eq!( 23 &encoded[2..2 + ENVELOPE_CONTEXT_DOMAIN.len()], 24 ENVELOPE_CONTEXT_DOMAIN 25 ); 26 assert_eq!(encoded, context().to_canonical_bytes()); 27 } 28 29 #[test] 30 fn context_parts_accept_exact_boundaries() { 31 let purpose = format!("a.{}", "b".repeat(ENVELOPE_PURPOSE_MAX_BYTES - 2)); 32 let subject_type = format!("a{}", "b".repeat(ENVELOPE_SUBJECT_TYPE_MAX_BYTES - 1)); 33 let subject_value = "a".repeat(ENVELOPE_SUBJECT_VALUE_MAX_BYTES); 34 let schema = format!("a.{}", "b".repeat(PAYLOAD_SCHEMA_MAX_BYTES - 2)); 35 assert!(EnvelopePurpose::parse(purpose).is_ok()); 36 assert!(EnvelopeSubject::parse(subject_type, subject_value).is_ok()); 37 assert!(PayloadSchemaId::parse(schema).is_ok()); 38 } 39 40 #[test] 41 fn invalid_context_is_rejected_without_echoing_values() { 42 let cases = [ 43 ( 44 EnvelopePurpose::parse("").err(), 45 ContextField::Purpose, 46 ContextValueError::Empty, 47 ), 48 ( 49 EnvelopePurpose::parse("Not.namespaced").err(), 50 ContextField::Purpose, 51 ContextValueError::NonCanonical, 52 ), 53 ( 54 EnvelopePurpose::parse("not_namespaced").err(), 55 ContextField::Purpose, 56 ContextValueError::NonCanonical, 57 ), 58 ( 59 EnvelopePurpose::parse("a".repeat(ENVELOPE_PURPOSE_MAX_BYTES + 1)).err(), 60 ContextField::Purpose, 61 ContextValueError::TooLong { 62 actual_bytes: ENVELOPE_PURPOSE_MAX_BYTES + 1, 63 max_bytes: ENVELOPE_PURPOSE_MAX_BYTES, 64 }, 65 ), 66 ( 67 EnvelopePurpose::parse("radroots.café").err(), 68 ContextField::Purpose, 69 ContextValueError::NonCanonical, 70 ), 71 ( 72 EnvelopePurpose::parse(" radroots.private_artifact").err(), 73 ContextField::Purpose, 74 ContextValueError::NonCanonical, 75 ), 76 ( 77 EnvelopeSubject::parse("private artifact", "subject").err(), 78 ContextField::SubjectType, 79 ContextValueError::NonCanonical, 80 ), 81 ( 82 EnvelopeSubject::parse("private_artifact", "SUBJECT-SECRET").err(), 83 ContextField::SubjectValue, 84 ContextValueError::NonCanonical, 85 ), 86 ( 87 PayloadSchemaId::parse("schema\n.v1").err(), 88 ContextField::PayloadSchema, 89 ContextValueError::NonCanonical, 90 ), 91 ]; 92 for (error, field, reason) in cases { 93 let error = error.expect("invalid context"); 94 assert_eq!(error, Error::InvalidContextValue { field, reason }); 95 assert!(!error.to_string().contains("SUBJECT-SECRET")); 96 } 97 } 98 99 #[test] 100 fn diagnostics_redact_semantic_values() { 101 let context = context(); 102 let debug = format!("{context:?}"); 103 assert!(debug.contains("private_artifact")); 104 assert!(!debug.contains("01010101010101010101010101010101")); 105 assert!(!debug.contains("trade.private_terms.v1")); 106 } 107 108 #[cfg(feature = "serde")] 109 #[test] 110 fn serde_revalidates_context_parts() { 111 let encoded = serde_json::to_vec(&context()).expect("serialize"); 112 let decoded: EnvelopeContext = serde_json::from_slice(&encoded).expect("deserialize"); 113 assert_eq!(decoded, context()); 114 let invalid = br#"{"purpose":"radroots.private_artifact","subject_type":"private_artifact","subject":"INVALID","payload_schema":"trade.private_terms.v1"}"#; 115 assert!(serde_json::from_slice::<EnvelopeContext>(invalid).is_err()); 116 }