lib

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

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 }