//! The published test vectors the sealing builds on, read from WAI's corpus //! (`wai/live-conformance/e2ee/`): RFC 9605 Appendix C, every header, AEAD and SFrame //! vector; and `draft-ietf-moq-secure-objects-01` Appendix B, every Full Track Name, key //! derivation, nonce and AAD vector, and its encryption vectors, exact in the GCM suites //! and, in the counter-mode suites, the difference WAI confidentiality §11.2 states. #![cfg(feature = "secure-objects")] use joule_moq::secure_objects::{aad, aead, ctr, derive_key_salt, labels, nonce, serialize_full_track_name, sframe, Suite}; use joule_moq::{TrackName, TrackNamespace}; use serde_json::Value; const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../../wai/live-conformance/e2ee/"); fn load(name: &str) -> Value { serde_json::from_str(&std::fs::read_to_string(format!("{DIR}{name}")).expect("the corpus file")).expect("JSON") } fn h(v: &Value) -> Vec { let s = v.as_str().expect("a hex string"); (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect() } fn n(v: &Value) -> u64 { v.as_u64().expect("a number") } fn suite(v: &Value) -> Suite { Suite::from_id(n(v) as u16).expect("a registered suite") } #[test] fn rfc9605_appendix_c() { let c = load("rfc9605.json"); let headers = c["header"].as_array().unwrap(); assert!(headers.len() > 100); for v in headers { let (kid, ctr, hdr) = (n(&v["kid"]), n(&v["ctr"]), h(&v["header"])); assert_eq!(sframe::encode_header(kid, ctr), hdr, "{v}"); assert_eq!(sframe::decode_header(&hdr), Some((kid, ctr, hdr.len())), "{v}"); } let aeads = c["aead"].as_array().unwrap(); assert_eq!(aeads.len(), 3); for v in aeads { let s = suite(&v["cipher_suite"]); let key = h(&v["key"]); assert_eq!(&key[..16], h(&v["enc_key"]).as_slice()); assert_eq!(&key[16..], h(&v["auth_key"]).as_slice()); let nonce: [u8; 12] = h(&v["nonce"]).try_into().unwrap(); let ct = aead::seal(s, &key, &nonce, &h(&v["aad"]), &h(&v["pt"])); assert_eq!(ct, h(&v["ct"]), "{v}"); assert_eq!(aead::open(s, &key, &nonce, &h(&v["aad"]), &ct), Some(h(&v["pt"]))); let mut bad = ct.clone(); *bad.last_mut().unwrap() ^= 1; assert_eq!(aead::open(s, &key, &nonce, &h(&v["aad"]), &bad), None); } let frames = c["sframe"].as_array().unwrap(); assert_eq!(frames.len(), 5, "one SFrame vector per suite"); for v in frames { let s = suite(&v["cipher_suite"]); let (kid, ctr) = (n(&v["kid"]), n(&v["ctr"])); let base = h(&v["base_key"]); assert_eq!(s.hkdf_extract(&[], &base), h(&v["sframe_secret"])); let (key, salt) = sframe::derive_key_salt(s, kid, &base); assert_eq!((key, salt.to_vec()), (h(&v["sframe_key"]), h(&v["sframe_salt"])), "{v}"); let ct = sframe::seal(s, kid, ctr, &base, &h(&v["metadata"]), &h(&v["pt"])); assert_eq!(ct, h(&v["ct"]), "{v}"); let opened = sframe::open(s, &h(&v["metadata"]), &ct, |k| (k == kid).then(|| base.clone())); assert_eq!(opened, Some(h(&v["pt"]))); assert_eq!(sframe::open(s, b"other metadata", &ct, |_| Some(base.clone())), None); } } fn names(v: &Value) -> (TrackNamespace, TrackName) { let ns = TrackNamespace::from_strs(v["namespace_tuples"].as_array().unwrap().iter().map(|t| t.as_str().unwrap())); (ns, TrackName::from_str(v["track_name"].as_str().unwrap())) } #[test] fn secure_objects_01_appendix_b() { let c = load("secure-objects-01.json"); for v in c["full_track_name"].as_array().unwrap() { let (ns, name) = names(v); assert_eq!(serialize_full_track_name(&ns, &name), h(&v["serialized_full_track_name"])); } for v in c["key_derivation"].as_array().unwrap() { let s = suite(&v["cipher_suite"]); let (kid, base, ftn) = (n(&v["key_id"]), h(&v["track_base_key"]), h(&v["serialized_full_track_name"])); assert_eq!(s.hkdf_extract(&[], &base), h(&v["moq_secret"])); let (kl, sl) = labels(s, kid, &ftn); assert_eq!((kl, sl), (h(&v["moq_key_label"]), h(&v["moq_salt_label"]))); let ks = derive_key_salt(s, kid, &base, &ftn); assert_eq!((ks.key, ks.salt.to_vec()), (h(&v["moq_key"]), h(&v["moq_salt"])), "{v}"); } // B.3's AAD holds the Key ID property, which the vector's // `serialized_immutable_properties` leaves out, in Appendix B's form: a 16-bit type 0x0002, // a length and the value (confidentiality §11.3). for v in c["nonce_aad"].as_array().unwrap() { let (g, o, p, kid) = (n(&v["group_id"]), n(&v["object_id"]), n(&v["publisher_priority"]) as u8, n(&v["key_id"])); let c = ctr(g, o).unwrap(); assert_eq!(c.to_vec(), h(&v["ctr"])); let salt: [u8; 12] = h(&v["moq_salt"]).try_into().unwrap(); assert_eq!(nonce(&salt, &c).to_vec(), h(&v["nonce"])); let mut sip = vec![0x00, 0x02, 0x01, kid as u8]; sip.extend_from_slice(&h(&v["serialized_immutable_properties"])); assert_eq!(aad(g, o, p, &sip).unwrap(), h(&v["aad"])); } let mut gcm = 0; for v in c["encryption"].as_array().unwrap() { let s = suite(&v["cipher_suite"]); let (ns, name) = names(v); let ftn = serialize_full_track_name(&ns, &name); assert_eq!(ftn, h(&v["serialized_full_track_name"])); let ks = derive_key_salt(s, n(&v["key_id"]), &h(&v["track_base_key"]), &ftn); assert_eq!((ks.key.clone(), ks.salt.to_vec()), (h(&v["moq_key"]), h(&v["moq_salt"]))); let (g, o, p) = (n(&v["group_id"]), n(&v["object_id"]), n(&v["publisher_priority"]) as u8); let nonce_ = nonce(&ks.salt, &ctr(g, o).unwrap()); assert_eq!(nonce_.to_vec(), h(&v["nonce"])); let payload = h(&v["original_payload"]); let mut pt = vec![payload.len() as u8]; pt.extend_from_slice(&payload); pt.extend_from_slice(&h(&v["encrypted_properties_list"])); assert_eq!(pt, h(&v["plaintext"])); let aad_ = h(&v["aad"]); assert_eq!(aad_[..13], aad(g, o, p, &[]).unwrap()[..], "the AAD's head: Group ID, Object ID, priority"); let expected = h(&v["ciphertext"]); let sealed = aead::seal(s, &ks.key, &nonce_, &aad_, &pt); match s { Suite::AesGcm128Sha256 | Suite::AesGcm256Sha512 => { assert_eq!(sealed, expected, "{v}"); assert_eq!(aead::open(s, &ks.key, &nonce_, &aad_, &expected), Some(pt)); gcm += 1; } _ => { // Counter mode: the encryption is RFC 9605's, the tag is not (§11.2). let nt = s.nt(); let body = expected.len() - nt; assert_eq!(&sealed[..body], &expected[..body], "the counter-mode encryption is RFC 9605's"); assert_ne!(&sealed[body..], &expected[body..], "RFC 9605 §4.5.1's tag differs from the vector's"); let (enc_ct, auth) = (&expected[..body], &ks.key[16..]); let alt = aead::hmac_sha256(auth, &[&aad_, &nonce_, enc_ct]); assert_eq!(&alt[..nt], &expected[body..], "the vector's tag is HMAC(auth_key, aad ‖ nonce ‖ ct)"); assert_eq!(aead::ctr_tag(s, auth, &nonce_, &aad_, enc_ct), sealed[body..].to_vec()); } } } assert_eq!(gcm, 2); }