#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use std::collections::BTreeMap;
use gunnar_crypt::keyring::Keyring;
use gunnar_crypt::members::{Level, Members};
use gunnar_crypt::userkey::{self, recovery_code, security_key, UserKey};
use gunnar_crypt::wrap;
#[test]
fn a_new_repository_and_a_new_key_are_quantum_safe_by_default() {
assert_eq!(Level::NEW, Level::QuantumSafe);
assert_eq!(
Level::default(),
Level::NEW,
"the API's default is the new default"
);
assert_eq!(
Level::UNSTATED,
Level::Classic,
"a file without a level line reads classic"
);
let fresh = Members::new();
assert_eq!(
fresh.level(),
Level::QuantumSafe,
"a new members file is quantum-safe"
);
assert!(
fresh.to_text().lines().any(|l| l == "level quantum-safe"),
"{}",
fresh.to_text()
);
assert_eq!(
Members::at(Level::Classic).level(),
Level::Classic,
"classic is a visible choice"
);
assert!(
!Members::at(Level::Classic).to_text().contains("level"),
"a classic file keeps its old shape"
);
let key = UserKey::generate(Level::NEW).unwrap();
assert!(
key.public().starts_with("age1pq1"),
"a new key is quantum-safe: {}",
key.public()
);
assert_eq!("classic".parse::<Level>().unwrap(), Level::Classic);
assert_eq!("quantum-safe".parse::<Level>().unwrap(), Level::QuantumSafe);
let bad = "post-quantum".parse::<Level>().unwrap_err();
assert!(
bad.contains("classic") && bad.contains("quantum-safe"),
"{bad}"
);
}
#[test]
fn a_members_file_without_a_level_line_stays_classic() {
let classic = UserKey::generate(Level::Classic).unwrap();
let text = format!("# gunnar-crypt members v1\nalice {}\n", classic.public());
let m = Members::parse(&text).unwrap();
assert_eq!(m.level(), Level::Classic);
assert_eq!(
Members::parse(&m.to_text()).unwrap(),
m,
"it round-trips classic"
);
}
#[test]
fn a_classic_repository_becomes_quantum_safe_only_with_a_pq_key_for_every_member() {
let (a, b) = (
UserKey::generate(Level::Classic).unwrap(),
UserKey::generate(Level::Classic).unwrap(),
);
let mut m = Members::at(Level::Classic);
m.add("alice", &a.public()).unwrap();
m.add("bob", &b.public()).unwrap();
let (pa, pb) = (
UserKey::generate(Level::QuantumSafe).unwrap(),
UserKey::generate(Level::QuantumSafe).unwrap(),
);
let mut only_alice = BTreeMap::new();
only_alice.insert("alice".to_string(), pa.public());
let refused = m.to_quantum_safe(&only_alice).unwrap_err();
assert_eq!(refused.missing, vec!["bob".to_string()]);
let words = refused.to_string();
assert!(
words.contains("bob") && words.contains("age1pq1"),
"{words}"
);
let mut classic_for_bob = only_alice.clone();
classic_for_bob.insert("bob".to_string(), b.public());
let refused = m.to_quantum_safe(&classic_for_bob).unwrap_err();
assert_eq!(
refused.missing,
vec!["bob".to_string()],
"a classic key is no quantum-safe key"
);
let mut both = only_alice;
both.insert("bob".to_string(), pb.public());
let up = m.to_quantum_safe(&both).unwrap();
assert_eq!(up.level(), Level::QuantumSafe);
assert_eq!(
up.names(),
vec!["alice", "bob"],
"the same members, in the same order"
);
assert!(up.entries().iter().all(|e| e.key.starts_with("age1pq1")));
assert_eq!(
m.level(),
Level::Classic,
"the classic file itself is untouched"
);
let mut ring = Keyring::generate();
let before = ring.clone();
ring.rotate();
let keys = wrap::wrap(&ring, &up).unwrap();
let opened = wrap::unwrap_checked(keys.as_bytes(), &up.to_text(), &[pb.identity()]).unwrap();
assert_eq!(opened, ring);
before.check_continuity(&opened).unwrap();
assert!(
wrap::unwrap_checked(keys.as_bytes(), &up.to_text(), &[b.identity()]).is_err(),
"bob's classic key opens nothing"
);
let already = up.to_quantum_safe(&BTreeMap::new()).unwrap();
assert_eq!(already, up, "a quantum-safe file keeps its own keys");
}
fn prf(fill: u8) -> [u8; 32] {
[fill; 32]
}
#[test]
fn a_quantum_safe_key_opens_with_its_security_key_key_file_and_recovery_code() {
let user = UserKey::generate(Level::NEW).unwrap();
let mut members = Members::new();
members.add("alice", &user.public()).unwrap();
let ring = Keyring::generate();
let keys = wrap::wrap(&ring, &members).unwrap();
let cred = b"credential-pq";
let sk = user
.wrap_to_security_key(&security_key::recipient(&prf(9), cred))
.unwrap();
let k = userkey::open(
sk.as_bytes(),
&security_key::identity(&prf(9), cred),
&user.public(),
)
.unwrap();
assert_eq!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[k.identity()]).unwrap(),
ring,
"security key"
);
let file = gunnar_crypt::pq::Identity::generate();
let w = user.wrap_to_pq(&file.to_public().to_string()).unwrap();
let k = userkey::open(w.as_bytes(), &file, &user.public()).unwrap();
assert_eq!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[k.identity()]).unwrap(),
ring,
"age1pq1 key file"
);
let code = recovery_code::generate();
let w = user.wrap_to_recovery_code(&code).unwrap();
let k = userkey::open_with_recovery_code(w.as_bytes(), &code, &user.public()).unwrap();
assert_eq!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[k.identity()]).unwrap(),
ring,
"recovery code"
);
assert!(userkey::wrap_is_quantum_safe("age-pq", ""));
assert!(userkey::wrap_is_quantum_safe("recovery-code", ""));
assert!(!userkey::wrap_is_quantum_safe("ssh", ""));
assert!(!userkey::wrap_is_quantum_safe("security-key", &sk));
let v2 = user.wrap_to_security_key_v2(&prf(9), cred).unwrap();
assert!(userkey::wrap_is_quantum_safe("security-key", &v2));
assert!(
!userkey::wrap_is_quantum_safe("something-else", &v2),
"an unknown kind is never called quantum-safe"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn an_ssh_wrap_of_a_quantum_safe_key_is_classical_and_says_so() {
const TEST_KEY: &str = include_str!("vectors/test_ed25519");
const TEST_KEY_PUB: &str = include_str!("vectors/test_ed25519.pub");
let user = UserKey::generate(Level::NEW).unwrap();
let w = user.wrap_to_ssh(TEST_KEY_PUB).unwrap();
let stanzas = gunnar_crypt::policy::age_stanzas(w.as_bytes()).unwrap();
assert!(
stanzas.iter().any(|s| s.tag == "ssh-ed25519"),
"{stanzas:?}"
);
let ssh = wrap::ssh_identity(TEST_KEY.as_bytes(), None, wrap::NoPassphrase).unwrap();
let k = userkey::open(w.as_bytes(), ssh.as_ref(), &user.public()).unwrap();
assert_eq!(
k.level(),
Level::QuantumSafe,
"the key inside is the quantum-safe one"
);
let words = userkey::SSH_WRAP_IS_CLASSICAL;
for needed in [
"not quantum-safe",
"X25519",
"RSA",
"gunnar crypt key add-pq",
"recovery code",
"gunnar crypt key remove",
] {
assert!(words.contains(needed), "{needed:?} missing from: {words}");
}
}