147 lines
7.2 KiB
Rust
147 lines
7.2 KiB
Rust
//! 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<u8> {
|
|
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);
|
|
}
|