#[cfg(test)]
mod test {
use crate::cesr::{CryptoType, SignatureType};
use crate::definitions::{Payload, VerifiedVid};
use crate::vid::OwnedVid;
use base64ct::{Base64UrlUnpadded, Encoding};
use wasm_bindgen_test::wasm_bindgen_test;
const VECTORS: &str = include_str!("../test_vectors/rev3.json");
struct Vectors {
vids: serde_json::Map<String, serde_json::Value>,
vectors: Vec<serde_json::Value>,
}
fn load() -> Vectors {
let doc: serde_json::Value = serde_json::from_str(VECTORS).expect("vectors parse");
Vectors {
vids: doc["vids"].as_object().expect("vids").clone(),
vectors: doc["vectors"].as_array().expect("vectors").clone(),
}
}
impl Vectors {
fn vid(&self, name: &str) -> OwnedVid {
serde_json::from_str(&self.vids[name].to_string()).expect("VID parse")
}
}
fn signature_type(expect: &serde_json::Value) -> SignatureType {
match expect["signature"].as_str().unwrap_or("Ed25519") {
"Ed25519" => SignatureType::Ed25519,
"MlDsa65" => SignatureType::MlDsa65,
other => panic!("unknown signature type in vectors: {other}"),
}
}
fn crypto_type(name: &str) -> CryptoType {
match name {
"SealedBox" => CryptoType::SealedBox,
"HpkeBase" => CryptoType::HpkeBase,
"Plaintext" => CryptoType::Plaintext,
other => panic!("unknown crypto type in vectors: {other}"),
}
}
#[test]
#[wasm_bindgen_test]
fn every_vector_opens_to_what_it_says_it_does() {
let vectors = load();
for vector in &vectors.vectors {
let name = vector["name"].as_str().expect("name");
let sender = vectors.vid(vector["sender"].as_str().expect("sender"));
let receiver = vectors.vid(vector["receiver"].as_str().expect("receiver"));
let mut message =
Base64UrlUnpadded::decode_vec(vector["message"].as_str().expect("message"))
.expect("message decodes");
let expect = &vector["expect"];
let expected_crypto = crypto_type(expect["crypto"].as_str().expect("crypto"));
let layout: Vec<&str> = vector["layout"]
.as_array()
.expect("layout")
.iter()
.map(|f| f.as_str().expect("layout field"))
.collect();
assert_eq!(layout[0], "-Z##", "{name}: every payload is -Z## framed");
let declared_type = layout[1];
if expected_crypto == CryptoType::Plaintext {
let (content, message_type) = crate::crypto::verify(&sender, &mut message)
.unwrap_or_else(|e| panic!("{name}: signed-only vector does not verify: {e}"));
assert_eq!(declared_type, "XSCS", "{name}: payload type");
assert_eq!(message_type.crypto_type, CryptoType::Plaintext, "{name}");
assert_eq!(
message_type.signature_type,
signature_type(expect),
"{name}"
);
assert_eq!(
content,
expect["payload"]["content"]
.as_str()
.expect("content")
.as_bytes(),
"{name}"
);
continue;
}
let (payload, crypto, signature) =
crate::crypto::open(&receiver, &sender, &mut message)
.unwrap_or_else(|e| panic!("{name}: vector does not open: {e}"));
assert_eq!(crypto, expected_crypto, "{name}: crypto type");
assert_eq!(signature, signature_type(expect), "{name}: signature type");
match payload {
Payload::Content(content) => {
assert_eq!(declared_type, "XSCS", "{name}: payload type");
assert_eq!(
content,
expect["payload"]["content"]
.as_str()
.expect("content")
.as_bytes(),
"{name}"
);
}
Payload::RequestRelationship {
thread_id,
reply_path,
..
} => {
assert_eq!(declared_type, "XRFI", "{name}: payload type");
let e = &expect["payload"]["request_relationship"];
assert_eq!(
Base64UrlUnpadded::encode_string(&thread_id),
e["thread_id"].as_str().expect("thread_id"),
"{name}: the digest is the message's own SAID"
);
assert!(
reply_path.is_empty(),
"{name}: a direct invite has no reply path"
);
}
Payload::AcceptRelationship {
thread_id,
reply_thread_id,
..
} => {
assert_eq!(declared_type, "XRFA", "{name}: payload type");
let e = &expect["payload"]["accept_relationship"];
assert_eq!(
Base64UrlUnpadded::encode_string(&thread_id),
e["thread_id"].as_str().expect("thread_id"),
"{name}: the invite's digest is echoed verbatim"
);
assert_eq!(
Base64UrlUnpadded::encode_string(&reply_thread_id),
e["reply_thread_id"].as_str().expect("reply_thread_id"),
"{name}: the accept carries its own SAID"
);
}
Payload::CancelRelationship { thread_id } => {
assert_eq!(declared_type, "XRFD", "{name}: payload type");
assert_eq!(
Base64UrlUnpadded::encode_string(&thread_id),
expect["payload"]["cancel_relationship"]["thread_id"]
.as_str()
.expect("thread_id"),
"{name}"
);
}
Payload::ControlMessage(_) | Payload::Padding => {
panic!("{name}: no vector carries these payload types yet")
}
Payload::NestedMessage(inner) => {
assert_eq!(declared_type, "XHOP", "{name}: payload type");
let e = &expect["payload"]["nested"];
let inner_sender = vectors.vid("nested_alice");
let inner_receiver = vectors.vid("nested_bob");
assert_eq!(inner_sender.identifier(), e["inner_sender"], "{name}");
assert_eq!(inner_receiver.identifier(), e["inner_receiver"], "{name}");
let mut inner = inner.to_vec();
let (inner_payload, ..) =
crate::crypto::open(&inner_receiver, inner_sender.vid(), &mut inner)
.unwrap_or_else(|e| panic!("{name}: inner message does not open: {e}"));
let Payload::Content(content) = inner_payload else {
panic!("{name}: the inner message carries an application payload");
};
assert_eq!(
content,
e["inner_content"]
.as_str()
.expect("inner_content")
.as_bytes(),
"{name}"
);
}
Payload::RoutedMessage(hops, inner) => {
assert_eq!(declared_type, "XHOP", "{name}: payload type");
let e = &expect["payload"]["routed"];
let expected_hops: Vec<&str> = e["hops"]
.as_array()
.expect("hops")
.iter()
.map(|h| h.as_str().expect("hop"))
.collect();
let hops: Vec<String> = hops
.iter()
.map(|h| String::from_utf8(h.to_vec()).expect("hop is a VID"))
.collect();
assert_eq!(hops, expected_hops, "{name}: the hop list");
assert_eq!(
hops.last().map(String::as_str),
e["inner_receiver"].as_str(),
"{name}: the exit entry is the destination's VID"
);
let inner_sender = vectors.vid("nested_alice");
let inner_receiver = vectors.vid("nested_bob");
let mut inner = inner.to_vec();
let (inner_payload, ..) =
crate::crypto::open(&inner_receiver, inner_sender.vid(), &mut inner)
.unwrap_or_else(|e| {
panic!("{name}: endpoint-to-endpoint message does not open: {e}")
});
let Payload::Content(content) = inner_payload else {
panic!("{name}: the inner message carries an application payload");
};
assert_eq!(
content,
e["inner_content"]
.as_str()
.expect("inner_content")
.as_bytes(),
"{name}"
);
}
}
}
assert_eq!(vectors.vectors.len(), 10, "every vector was exercised");
}
#[test]
#[wasm_bindgen_test]
fn every_vector_regenerates_from_what_the_file_records() {
let vectors = load();
let mut checked = 0;
for vector in &vectors.vectors {
let name = vector["name"].as_str().expect("name");
let Some(seed) = vector["seed"].as_str() else {
continue;
};
let seed: [u8; 32] = Base64UrlUnpadded::decode_vec(seed)
.expect("seed")
.try_into()
.expect("32-byte seed");
let nonce: Option<[u8; 16]> = vector["nonce"].as_str().map(|n| {
Base64UrlUnpadded::decode_vec(n)
.expect("nonce")
.try_into()
.expect("16-byte nonce")
});
let sender = vectors.vid(vector["sender"].as_str().expect("sender"));
let receiver = vectors.vid(vector["receiver"].as_str().expect("receiver"));
let crypto = crypto_type(vector["expect"]["crypto"].as_str().expect("crypto"));
let expect = &vector["expect"]["payload"];
let payload = if let Some(content) = expect["content"].as_str() {
Payload::Content(content.as_bytes())
} else if expect["request_relationship"].is_object() {
Payload::RequestRelationship {
thread_id: Default::default(),
reply_path: vec![],
form: crate::definitions::RelationshipForm::Direct,
}
} else if let Some(cancel) = expect["cancel_relationship"].as_object() {
let thread_id: [u8; 32] =
Base64UrlUnpadded::decode_vec(cancel["thread_id"].as_str().expect("thread_id"))
.expect("digest")
.try_into()
.expect("32-byte digest");
Payload::CancelRelationship { thread_id }
} else {
continue;
};
let mut digest = Default::default();
let regenerated = crate::crypto::seal_reproducibly(
&sender,
receiver.vid(),
payload,
Some(&mut digest),
crypto,
seed,
nonce,
)
.unwrap_or_else(|e| panic!("{name}: could not regenerate: {e}"));
assert_eq!(
Base64UrlUnpadded::encode_string(®enerated),
vector["message"].as_str().expect("message"),
"{name}: regenerating from the recorded seed must give the same bytes"
);
checked += 1;
}
assert!(
checked >= 4,
"several vectors were regenerated, got {checked}"
);
}
#[test]
#[wasm_bindgen_test]
fn a_vector_whose_bytes_changed_does_not_open() {
let vectors = load();
let vector = &vectors.vectors[0];
let sender = vectors.vid(vector["sender"].as_str().unwrap());
let receiver = vectors.vid(vector["receiver"].as_str().unwrap());
let pristine = Base64UrlUnpadded::decode_vec(vector["message"].as_str().unwrap()).unwrap();
for position in [8, pristine.len() / 2, pristine.len() - 1] {
let mut tampered = pristine.clone();
tampered[position] ^= 0x01;
assert!(
crate::crypto::open(&receiver, &sender, &mut tampered).is_err(),
"a vector altered at byte {position} must not open"
);
}
}
#[test]
#[wasm_bindgen_test]
fn the_segmenter_knows_every_code_in_every_vector() {
use crate::cesr::{SegmentKind, segments};
let vectors = load();
for vector in &vectors.vectors {
let name = vector["name"].as_str().unwrap();
let text = vector["message"].as_str().unwrap();
let message = Base64UrlUnpadded::decode_vec(text).unwrap();
let segments = segments(&message);
let unparsed: Vec<_> = segments
.iter()
.filter(|s| s.kind == SegmentKind::Unparsed)
.collect();
assert!(
unparsed.is_empty(),
"{name}: the segmenter does not know {unparsed:#?}"
);
let covered: String = segments.iter().map(|s| s.text.as_str()).collect();
assert_eq!(covered, text, "{name}: the segments must cover the message");
}
}
}