kv-store: SQLite (bundled) and in-memory Store holding only ciphertext envelopes, with base copies for three-way merges and a 10-version history. kv-ffi: VaultCore::open(path); persisted account record; local unlock key (enable/unlock_with_local_key/disable); async sync API (configure_server, set_device_signer, register_account, login, sync_now, logout, devices); DeviceSigner foreign trait; IntegrityAlarm/PinMismatch/... error variants. kv-sync: reqwest + rustls (TLS 1.3 only, ring) with SPKI pinning; OPAQUE client on kv-proto's OpaqueSuite; RFC 9421 signing per kv_proto::httpsig; pull/push with If-Match, field-level three-way merge, edit-beats-delete; signed manifest (canonical CBOR, Merkle root, prev chain, Ed25519) with rollback/fork/Merkle checks as hard integrity alarms. Tested against a wiremock fake server, plus an #[ignore] test against kestrove-server. kv-crypto: gen-vectors example and tests reproducing kestrove-spec/vectors. Co-Authored-By: Claude Opus 5.5 <[email protected]>
101 lines
3.9 KiB
Rust
101 lines
3.9 KiB
Rust
//! The code must reproduce the published test vectors byte for byte, and the vectors must decode.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use kv_crypto::{aead::SymKey, seal::EncKeypair, secret_key::SecretKey};
|
|
use kv_proto::Envelope;
|
|
use serde_json::Value;
|
|
use uuid::Uuid;
|
|
|
|
#[path = "../vectors/gen.rs"]
|
|
mod gen;
|
|
|
|
fn dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors")
|
|
}
|
|
|
|
fn load(name: &str) -> Value {
|
|
let text = std::fs::read_to_string(dir().join(format!("{name}.json"))).expect("vector file (run gen-vectors)");
|
|
serde_json::from_str(&text).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn code_reproduces_vector_files() {
|
|
let spec = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../kestrove-spec/vectors");
|
|
for (name, doc) in gen::generate() {
|
|
let rendered = gen::render(&doc);
|
|
let on_disk = std::fs::read_to_string(dir().join(format!("{name}.json"))).unwrap();
|
|
assert_eq!(rendered, on_disk, "{name}.json differs from what the code produces");
|
|
// When the spec repo is checked out next to this one, it must carry the same files.
|
|
if let Ok(published) = std::fs::read_to_string(spec.join(format!("{name}.json"))) {
|
|
assert_eq!(published, on_disk, "kestrove-spec/vectors/{name}.json is stale");
|
|
}
|
|
}
|
|
assert_eq!(gen::FILES.len(), gen::generate().len());
|
|
}
|
|
|
|
#[test]
|
|
fn envelopes_open() {
|
|
for v in load("envelope")["vectors"].as_array().unwrap() {
|
|
let key: [u8; 32] = gen::unhex(v["key"].as_str().unwrap()).try_into().unwrap();
|
|
let key = SymKey::from_bytes(Uuid::parse_str(v["kid"].as_str().unwrap()).unwrap(), key);
|
|
let env: Envelope = serde_json::from_value(v["envelope"].clone()).unwrap();
|
|
let pt = key.open(&env, &gen::unhex(v["aad"].as_str().unwrap())).unwrap();
|
|
assert_eq!(gen::hex(&pt), v["plaintext"].as_str().unwrap());
|
|
assert_eq!(gen::hex(&env.ct), v["ciphertext"].as_str().unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hpke_opens() {
|
|
for v in load("hpke")["vectors"].as_array().unwrap() {
|
|
let kp = EncKeypair::from_secret_bytes(
|
|
Uuid::parse_str(v["recipient_kid"].as_str().unwrap()).unwrap(),
|
|
&gen::unhex(v["recipient_sk"].as_str().unwrap()),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(gen::hex(&kp.public_bytes()), v["recipient_pk"].as_str().unwrap());
|
|
let env: Envelope = serde_json::from_value(v["envelope"].clone()).unwrap();
|
|
let pt = kp.open(&env, &gen::unhex(v["aad"].as_str().unwrap())).unwrap();
|
|
assert_eq!(gen::hex(&pt), v["plaintext"].as_str().unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn secret_keys_parse() {
|
|
let doc = load("secret_key_format");
|
|
for v in doc["valid"].as_array().unwrap() {
|
|
let k = SecretKey::parse(v["display"].as_str().unwrap()).unwrap();
|
|
assert_eq!(gen::hex(k.expose()), v["raw"].as_str().unwrap());
|
|
}
|
|
for v in doc["alternate_inputs"].as_array().unwrap() {
|
|
let k = SecretKey::parse(v["input"].as_str().unwrap()).unwrap();
|
|
assert_eq!(gen::hex(k.expose()), v["raw"].as_str().unwrap());
|
|
}
|
|
for v in doc["invalid"].as_array().unwrap() {
|
|
assert!(SecretKey::parse(v["input"].as_str().unwrap()).is_err(), "{v}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rfc6238_codes_are_the_published_ones() {
|
|
// Spot-check against the RFC 6238 Appendix B table itself, not only our own output.
|
|
let doc = load("totp");
|
|
let find = |t: u64, alg: &str| {
|
|
doc["vectors"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|v| v["time"] == t && v["uri"].as_str().unwrap().contains(&format!("algorithm={alg}&")))
|
|
.unwrap()["code"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_string()
|
|
};
|
|
assert_eq!(find(59, "SHA1"), "94287082");
|
|
assert_eq!(find(59, "SHA256"), "46119246");
|
|
assert_eq!(find(59, "SHA512"), "90693936");
|
|
assert_eq!(find(20_000_000_000, "SHA1"), "65353130");
|
|
assert_eq!(find(20_000_000_000, "SHA512"), "47863826");
|
|
}
|