use simoom_tls::asn1;
use simoom_tls::certgen::{self, CertParams};
use simoom_tls::crypto::md::{bmp_password, Md, PKCS12_MAC};
use simoom_tls::keys::PrivateKey;
use simoom_tls::keystore::{self, pbe, pkcs12, KeyStore, KeyStoreFormat};
use simoom_tls::x509::{Certificate, RootStore};
use simoom_tls::{CertifiedKey, ClientConfig, ClientConnection, ServerConfig, ServerConnection};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join(name)
}
fn read(name: &str) -> Vec<u8> {
std::fs::read(fixture(name)).unwrap_or_else(|e| panic!("{name}: {e}"))
}
fn leaf_key() -> PrivateKey {
PrivateKey::from_pem(&read("leaf.key")).unwrap()
}
fn rsa_key() -> PrivateKey {
PrivateKey::from_pem(&read("rsa2048.pkcs8.pem")).unwrap()
}
fn subject_cn(der: &[u8]) -> String {
Certificate::from_der(der).unwrap().subject.common_name().unwrap_or("").to_string()
}
fn same_key(a: &PrivateKey, b: &PrivateKey) -> bool {
a.public_key().same_as(&b.public_key())
}
fn check_leaf_store(f: &str, ks: &KeyStore, full_chain: bool) {
let (key, chain) = ks.key_entry(Some("server")).unwrap_or_else(|| panic!("{f}: no 'server' entry: {:?}", ks.aliases()));
assert!(same_key(key, &leaf_key()), "{f}");
assert_eq!(subject_cn(&chain[0]), "leaf.camel.test", "{f}");
if full_chain {
assert_eq!(chain.len(), 3, "{f}: chain leaf, intermediate, root");
assert_eq!(subject_cn(&chain[1]), "Camel Test Intermediate", "{f}");
assert_eq!(subject_cn(&chain[2]), "Camel Test Root CA", "{f}");
assert!(ks.trusted_certs().is_empty(), "{f}");
}
}
#[test]
fn openssl_pkcs12_fixtures() {
for f in ["keystores/openssl_modern.p12", "keystores/openssl_legacy.p12", "keystores/openssl_aes128_sha1mac.p12"] {
let data = read(f);
assert_eq!(KeyStoreFormat::detect(&data), KeyStoreFormat::Pkcs12, "{f}");
let ks = KeyStore::load(&data, Some("changeit"), None).unwrap_or_else(|e| panic!("{f}: {e}"));
check_leaf_store(f, &ks, !f.contains("aes128"));
let e = KeyStore::load(&data, Some("wrong"), None).unwrap_err();
assert!(e.contains("password"), "{f}: {e}");
}
let ts = KeyStore::load(&read("keystores/openssl_trust.p12"), Some("changeit"), None).unwrap();
assert_eq!(ts.trusted_certs().len(), 1);
assert!(ts.key_entry(None).is_none());
}
#[test]
fn fresh_openssl3_pkcs12_fixtures() {
for f in ["keystores/openssl3_default.p12", "keystores/openssl3_legacy.p12", "keystores/openssl3_aes192_sha512mac.p12"] {
let data = read(f);
let ks = KeyStore::load(&data, Some("changeit"), None).unwrap_or_else(|e| panic!("{f}: {e}"));
check_leaf_store(f, &ks, true);
assert!(KeyStore::load(&data, Some("changeiT"), None).unwrap_err().contains("password"), "{f}");
assert!(KeyStore::load(&data, None, None).is_err(), "{f}");
}
let ks = KeyStore::load(&read("keystores/openssl3_plain_nomac.p12"), None, None).unwrap();
check_leaf_store("plain_nomac", &ks, false);
let ks = KeyStore::load(&read("keystores/openssl3_rsa.p12"), Some("changeit"), None).unwrap();
let (k, chain) = ks.key_entry(Some("rsa")).unwrap();
assert!(matches!(k, PrivateKey::Rsa(_)));
assert!(same_key(k, &rsa_key()));
assert_eq!(chain.len(), 1);
}
#[test]
fn keytool_stores() {
for f in ["keystores/keytool.jks", "keystores/keytool.jceks"] {
let data = read(f);
let ks = KeyStore::load(&data, Some("changeit"), Some("keypass")).unwrap_or_else(|e| panic!("{f}: {e}"));
check_leaf_store(f, &ks, true);
assert!(KeyStore::load(&data, Some("changeit"), Some("nope")).is_err());
let e = KeyStore::load(&data, Some("wrong"), Some("keypass")).unwrap_err();
assert!(e.contains("password"), "{e}");
assert!(KeyStore::load(&data, None, Some("keypass")).is_ok());
assert!(KeyStore::load(&data, None, None).unwrap_err().contains("key password"));
}
let ts = KeyStore::load(&read("keystores/keytool_trust.jks"), None, None).unwrap();
assert_eq!(subject_cn(&ts.trusted_certs()[0]), "Camel Test Root CA");
let ts = KeyStore::load(&read("keystores/keytool_trust.jks"), Some("changeit"), None).unwrap();
assert_eq!(ts.trusted_certs().len(), 1);
let ts = KeyStore::load(&read("keystores/keytool_trust.p12"), Some("changeit"), None).unwrap();
assert_eq!(subject_cn(&ts.trusted_certs()[0]), "Camel Test Root CA");
let ec = KeyStore::load(&read("keystores/keytool_ec.p12"), Some("changeit"), None).unwrap();
assert!(matches!(ec.key_entry(Some("server")).unwrap().0, PrivateKey::Ec { .. }));
let rsa = KeyStore::load(&read("keystores/keytool_rsa.jks"), Some("changeit"), None).unwrap();
assert!(matches!(rsa.key_entry(Some("rsaserver")).unwrap().0, PrivateKey::Rsa(_)));
let ed = KeyStore::load(&read("keystores/keytool_ed25519.p12"), Some("changeit"), None).unwrap();
assert!(matches!(ed.key_entry(Some("ed")).unwrap().0, PrivateKey::Ed25519 { .. }));
}
#[test]
fn encrypted_pkcs8_pem() {
let leaf = [
"leaf_pbes2_aes256.key",
"leaf_pbes2_sha1_aes128.key",
"leaf_pbes2_des3.key",
"leaf_pbe_sha1_3des.key",
"leaf_openssl3_aes256.key",
"leaf_openssl3_aes192_sha512.key",
"leaf_openssl3_pbe_sha1_2des.key",
"leaf_openssl3_pbe_sha1_rc2_40.key",
"leaf_openssl3_pbe_sha1_rc2_128.key",
];
for f in leaf.iter().map(|f| (f, leaf_key())).chain([(&"rsa_openssl3_aes128_sha384.key", rsa_key())]) {
let (name, expected) = f;
let data = read(&format!("keystores/{name}"));
assert!(PrivateKey::from_pem(&data).is_err(), "{name}: the plain parser refuses encrypted keys");
assert!(keystore::private_key_from_pem(&data, None).unwrap_err().contains("password"), "{name}");
let k = keystore::private_key_from_pem(&data, Some("secret")).unwrap_or_else(|e| panic!("{name}: {e}"));
assert!(same_key(&k, &expected), "{name}");
assert!(keystore::decrypt_private_key_pem(&data, Some("wrong")).is_err(), "{name}");
let ks = KeyStore::load(&data, None, Some("secret")).unwrap();
assert!(same_key(ks.key_entry(None).unwrap().0, &expected));
}
assert!(same_key(&keystore::private_key_from_pem(&read("leaf.key"), None).unwrap(), &leaf_key()));
assert!(keystore::private_key_from_pem(&read("ec256.sec1.pem"), None).is_ok());
assert!(keystore::private_key_from_pem(&read("rsa2048.pkcs1.pem"), None).is_ok());
assert!(keystore::private_key_from_pem(&read("leaf.pem"), None).unwrap_err().contains("no private key"));
}
fn mixed_store() -> (KeyStore, PrivateKey, PrivateKey, PrivateKey, Vec<u8>) {
let id = certgen::self_signed("localhost");
let rsa = rsa_key();
let rsa_cert = simoom_tls::pem::parse_pem(&read("rsa_selfsigned.pem")).unwrap().remove(0).der;
let ed = certgen::issue(&CertParams::server(&["ed.test"]), PrivateKey::generate_ed25519(), None);
let p384 = certgen::issue(&CertParams::server(&["p384.test"]), PrivateKey::generate_ec(simoom_tls::keys::EcCurve::P384), None);
let ks = KeyStore::from_key_entry("ec", id.key.clone(), vec![id.cert_der.clone()])
.add_key("rsa", rsa.clone(), vec![rsa_cert.clone()])
.add_key("ed", ed.key.clone(), vec![ed.cert_der.clone()])
.add_key("p384", p384.key.clone(), vec![p384.cert_der.clone()])
.add_cert("trusted", rsa_cert.clone());
(ks, id.key, rsa, ed.key, rsa_cert)
}
#[test]
fn writer_round_trips() {
let (ks, ec, rsa, ed, rsa_cert) = mixed_store();
for (name, bytes, kp) in [
("pkcs12", ks.to_pkcs12("s3cret"), "s3cret"),
("legacy pkcs12", pkcs12::write(&ks, "s3cret", pkcs12::Pkcs12Profile::Legacy), "s3cret"),
("jks", ks.to_jks("s3cret", "kp"), "kp"),
("jceks", ks.to_jceks("s3cret", "kp"), "kp"),
] {
let back = KeyStore::load(&bytes, Some("s3cret"), Some(kp)).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(back.aliases(), vec!["ec", "rsa", "ed", "p384", "trusted"], "{name}");
assert!(same_key(back.key_entry(Some("ec")).unwrap().0, &ec), "{name}");
assert!(same_key(back.key_entry(Some("rsa")).unwrap().0, &rsa), "{name}");
assert!(same_key(back.key_entry(Some("ed")).unwrap().0, &ed), "{name}");
assert!(matches!(back.key_entry(Some("p384")).unwrap().0, PrivateKey::Ec { .. }), "{name}");
assert_eq!(back.trusted_certs(), vec![rsa_cert.clone()], "{name}");
assert!(KeyStore::load(&bytes, Some("S3cret"), Some(kp)).is_err(), "{name}: wrong store password");
}
let p12 = ks.to_pkcs12("pw");
let has = |needle: &str| p12.windows(asn1::oid(needle).len()).any(|w| w == asn1::oid(needle).as_slice());
assert!(has(pbe::OID_PBES2) && has(pbe::OID_AES256_CBC) && has(pbe::OID_HMAC_SHA256) && has("2.16.840.1.101.3.4.2.1"));
assert!(!has(pbe::OID_PBE_SHA_3DES) && !has(pbe::OID_PBE_SHA_RC2_40));
let empty = KeyStore::new();
assert!(KeyStore::load(&empty.to_pkcs12(""), Some(""), None).unwrap().entries.is_empty());
assert!(KeyStore::load(&empty.to_jks("", ""), Some(""), None).unwrap().entries.is_empty());
let enc = pbe::encrypt_private_key_info(&ec.to_pkcs8(), "pw");
let pem = simoom_tls::pem::encode_pem("ENCRYPTED PRIVATE KEY", &enc);
assert!(same_key(&keystore::private_key_from_pem(pem.as_bytes(), Some("pw")).unwrap(), &ec));
assert!(KeyStore::load(&ks.to_pkcs12("pw"), Some("pw"), Some("other")).is_err());
}
#[test]
fn tampered_and_truncated_fixtures_fail_cleanly() {
for (f, sp, kp) in [
("keystores/openssl_modern.p12", "changeit", "changeit"),
("keystores/openssl3_legacy.p12", "changeit", "changeit"),
("keystores/keytool.jks", "changeit", "keypass"),
("keystores/keytool.jceks", "changeit", "keypass"),
] {
let data = read(f);
for i in (0..data.len()).step_by(97) {
let mut bad = data.clone();
bad[i] ^= 0x40;
assert!(KeyStore::load(&bad, Some(sp), Some(kp)).is_err(), "{f}: flipped byte {i} accepted");
}
for n in [0, 3, 16, 33, data.len() / 3, data.len() - 21, data.len() - 1] {
assert!(KeyStore::load(&data[..n], Some(sp), Some(kp)).is_err(), "{f}: truncated to {n} accepted");
}
let mut longer = data.clone();
longer.push(0);
assert!(KeyStore::load(&longer, Some(sp), Some(kp)).is_err(), "{f}: trailing byte accepted");
}
}
fn pfx_with_mac(auth_safe: &[u8], password: &str, iterations: u64, mac_iterations_field: u64) -> Vec<u8> {
let salt = [7u8; 8];
let key = Md::Sha256.pkcs12_kdf(&bmp_password(password), &salt, PKCS12_MAC, iterations as u32, 32);
let mac = Md::Sha256.hmac(&key, &[auth_safe]);
let data_ci = asn1::seq(&[asn1::oid("1.2.840.113549.1.7.1"), asn1::explicit(0, &asn1::octet_string(auth_safe))]);
let mac_data = asn1::seq(&[
asn1::seq(&[asn1::seq(&[asn1::oid(Md::Sha256.oid()), asn1::tlv(asn1::TAG_NULL, &[])]), asn1::octet_string(&mac)]),
asn1::octet_string(&salt),
asn1::uint(&mac_iterations_field.to_be_bytes()),
]);
asn1::seq(&[asn1::uint(&[3]), data_ci, mac_data])
}
#[test]
fn hostile_iteration_counts_and_sizes_are_bounded() {
let t = Instant::now();
let auth_safe = asn1::seq(&[]);
let e = pkcs12::parse(&pfx_with_mac(&auth_safe, "pw", 1, pbe::MAX_ITERATIONS + 1), "pw", None).unwrap_err();
assert!(e.contains("iteration"), "{e}");
let e = pkcs12::parse(&pfx_with_mac(&auth_safe, "pw", 1, u64::MAX >> 1), "pw", None).unwrap_err();
assert!(e.contains("iteration"), "{e}");
let alg = asn1::seq(&[
asn1::oid(pbe::OID_PBES2),
asn1::seq(&[
asn1::seq(&[asn1::oid(pbe::OID_PBKDF2), asn1::seq(&[asn1::octet_string(&[1; 8]), asn1::uint(&u32::MAX.to_be_bytes())])]),
asn1::seq(&[asn1::oid(pbe::OID_AES256_CBC), asn1::octet_string(&[0; 16])]),
]),
]);
let eci = asn1::seq(&[asn1::oid("1.2.840.113549.1.7.1"), alg.clone(), asn1::tlv(0x80, &[0; 32])]);
let auth_safe = asn1::seq(&[asn1::seq(&[asn1::oid("1.2.840.113549.1.7.6"), asn1::explicit(0, &asn1::seq(&[asn1::uint(&[0]), eci]))])]);
let e = pkcs12::parse(&pfx_with_mac(&auth_safe, "pw", 1, 1), "pw", None).unwrap_err();
assert!(e.contains("iteration"), "{e}");
let epki = asn1::seq(&[alg, asn1::octet_string(&[0; 32])]);
let pem = simoom_tls::pem::encode_pem("ENCRYPTED PRIVATE KEY", &epki);
assert!(keystore::private_key_from_pem(pem.as_bytes(), Some("pw")).unwrap_err().contains("iteration"));
let id = certgen::self_signed("localhost");
let bag = asn1::seq(&[
asn1::oid("1.2.840.113549.1.12.10.1.3"),
asn1::explicit(0, &asn1::seq(&[asn1::oid("1.2.840.113549.1.9.22.1"), asn1::explicit(0, &asn1::octet_string(&id.cert_der))])),
]);
let bags = asn1::seq(&vec![bag; keystore::MAX_ENTRIES + 1]);
let auth_safe = asn1::seq(&[asn1::seq(&[asn1::oid("1.2.840.113549.1.7.1"), asn1::explicit(0, &asn1::octet_string(&bags))])]);
let e = pkcs12::parse(&pfx_with_mac(&auth_safe, "pw", 1, 1), "pw", None).unwrap_err();
assert!(e.contains("entries"), "{e}");
let mut inner = asn1::seq(&[]);
for _ in 0..10 {
inner = asn1::seq(&[asn1::seq(&[asn1::oid("1.2.840.113549.1.12.10.1.6"), asn1::explicit(0, &inner)])]);
}
let auth_safe = asn1::seq(&[asn1::seq(&[asn1::oid("1.2.840.113549.1.7.1"), asn1::explicit(0, &asn1::octet_string(&inner))])]);
let e = pkcs12::parse(&pfx_with_mac(&auth_safe, "pw", 1, 1), "pw", None).unwrap_err();
assert!(e.contains("nested"), "{e}");
assert!(t.elapsed() < Duration::from_secs(20), "bounded inputs took {:?}", t.elapsed());
}
fn handshake(server_key: CertifiedKey, root_der: &[u8], host: &str) {
let server_cfg = Arc::new(ServerConfig::new(server_key));
let mut roots = RootStore::empty();
roots.add_der(root_der).unwrap();
let client_cfg = Arc::new(ClientConfig::new(roots));
let mut server = ServerConnection::new(server_cfg);
let mut client = ClientConnection::new(client_cfg, host).unwrap();
client.write_plaintext(b"ping").unwrap();
for _ in 0..20 {
if !(client.wants_write() || server.wants_write()) {
break;
}
let n = server.read_tls(client.tls_output());
client.consume_tls_output(n);
server.process_new_packets().unwrap();
let n = client.read_tls(server.tls_output());
server.consume_tls_output(n);
client.process_new_packets().unwrap();
}
let mut buf = [0u8; 8];
let n = server.read_plaintext(&mut buf);
assert_eq!(&buf[..n], b"ping");
}
#[test]
fn key_stores_drive_tls_handshakes() {
let trust = KeyStore::load(&read("keystores/keytool_trust.jks"), Some("changeit"), None).unwrap();
let root = trust.trusted_certs().remove(0);
for (f, kp) in [("keystores/openssl_modern.p12", None), ("keystores/openssl3_legacy.p12", None), ("keystores/keytool.jceks", Some("keypass"))] {
let ks = KeyStore::load(&read(f), Some("changeit"), kp).unwrap();
let (key, chain) = ks.key_entry(Some("server")).unwrap();
handshake(CertifiedKey::new(chain.to_vec(), key.clone()).unwrap(), &root, "leaf.camel.test");
}
let ks = KeyStore::load(&read("keystores/keytool_ed25519.p12"), Some("changeit"), None).unwrap();
let (key, chain) = ks.key_entry(None).unwrap();
let host = Certificate::from_der(&chain[0]).unwrap().dns_names.first().cloned().unwrap_or_else(|| "localhost".into());
handshake(CertifiedKey::new(chain.to_vec(), key.clone()).unwrap(), &chain[0], &host);
let (mixed, ..) = mixed_store();
let back = KeyStore::load(&mixed.to_pkcs12("pw"), Some("pw"), None).unwrap();
let (key, chain) = back.key_entry(Some("ec")).unwrap();
handshake(CertifiedKey::new(chain.to_vec(), key.clone()).unwrap(), &chain[0], "localhost");
}
#[test]
fn openssl_reads_our_pkcs12() {
let Ok(v) = std::process::Command::new("openssl").arg("version").output() else {
eprintln!("openssl not found: skipping");
return;
};
if !v.status.success() {
return;
}
let dir = std::env::temp_dir().join(format!("simoom-tls-ks-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let id = certgen::self_signed("localhost");
let ks = KeyStore::from_key_entry("mykey", id.key.clone(), vec![id.cert_der.clone()]);
let p = dir.join("ours.p12");
std::fs::write(&p, ks.to_pkcs12("changeit")).unwrap();
let out = std::process::Command::new("openssl")
.args(["pkcs12", "-in", p.to_str().unwrap(), "-passin", "pass:changeit", "-nodes"])
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success() && text.contains("BEGIN PRIVATE KEY") && text.contains("BEGIN CERTIFICATE"), "{text} {}", String::from_utf8_lossy(&out.stderr));
assert!(same_key(&PrivateKey::from_pem(text.as_bytes()).unwrap(), &id.key));
let k = dir.join("ours.key");
std::fs::write(&k, simoom_tls::pem::encode_pem("ENCRYPTED PRIVATE KEY", &pbe::encrypt_private_key_info(&id.key.to_pkcs8(), "pw"))).unwrap();
let out = std::process::Command::new("openssl").args(["pkey", "-in", k.to_str().unwrap(), "-passin", "pass:pw"]).output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
assert!(same_key(&PrivateKey::from_pem(&out.stdout).unwrap(), &id.key));
let _ = std::fs::remove_dir_all(&dir);
}