#![cfg(feature = "cli")]
use std::io::{Read, Write};
use std::process::{Command, Stdio};
fn run(args: &[&str], stdin: &[u8]) -> (String, bool) {
let mut child = Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn purecrypto");
child
.stdin
.take()
.unwrap()
.write_all(stdin)
.expect("write stdin");
let out = child.wait_with_output().expect("wait");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
out.status.success(),
)
}
fn run_capture(args: &[&str], stdin: &[u8]) -> (String, String, bool) {
let mut child = Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn purecrypto");
child
.stdin
.take()
.unwrap()
.write_all(stdin)
.expect("write stdin");
let out = child.wait_with_output().expect("wait");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.success(),
)
}
#[test]
fn hash_sha256_stdin() {
let (out, ok) = run(&["hash", "sha256"], b"abc");
assert!(ok);
assert_eq!(
out.trim(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn hash_unknown_algorithm_fails() {
let (_, ok) = run(&["hash", "nope"], b"x");
assert!(!ok);
}
#[test]
fn rand_emits_hex() {
let (out, ok) = run(&["rand", "16"], b"");
assert!(ok);
assert_eq!(out.trim().len(), 32); assert!(out.trim().bytes().all(|b| b.is_ascii_hexdigit()));
}
#[test]
fn ca_workflow_genpkey_req_sign() {
let dir = std::env::temp_dir().join(format!("pc_cli_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("ca_key.pem")
],
b""
)
.1
);
assert!(
run(
&[
"x509",
"-new",
"--ca",
"-key",
&p("ca_key.pem"),
"-subj",
"/CN=Test CA",
"-out",
&p("ca.pem")
],
b"",
)
.1
);
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("leaf_key.pem")
],
b""
)
.1
);
assert!(
run(
&[
"req",
"-key",
&p("leaf_key.pem"),
"-subj",
"/CN=leaf.test",
"-addext",
"subjectAltName=DNS:leaf.test",
"-out",
&p("leaf.csr"),
],
b"",
)
.1
);
let (vout, ok) = run(&["req", "-in", &p("leaf.csr"), "-verify"], b"");
assert!(ok && vout.contains("verify OK"));
assert!(
run(
&[
"x509",
"-req",
"-in",
&p("leaf.csr"),
"-CA",
&p("ca.pem"),
"-CAkey",
&p("ca_key.pem"),
"-out",
&p("leaf.pem")
],
b"",
)
.1
);
let (text, ok) = run(&["x509", "-in", &p("leaf.pem"), "-text"], b"");
assert!(ok, "x509 -text failed: {text}");
assert!(text.contains("CN=leaf.test"), "{text}");
assert!(
text.contains("Issuer:") && text.contains("CN=Test CA"),
"{text}"
);
assert!(text.contains("leaf.test"), "{text}");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn req_warns_on_world_readable_key() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("pc_cli_warn_perm_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("loose.key"),
],
b"",
)
.1,
"genpkey failed"
);
let mut perms = std::fs::metadata(p("loose.key")).unwrap().permissions();
perms.set_mode(0o644);
std::fs::set_permissions(p("loose.key"), perms).unwrap();
let (_, err, ok) = run_capture(
&[
"req",
"-key",
&p("loose.key"),
"-subj",
"/CN=test",
"-out",
&p("test.csr"),
],
b"",
);
assert!(ok, "req should still succeed despite the warning");
assert!(
err.contains("group/other-readable"),
"missing permission warning in stderr: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn pkey_writes_private_key_with_0600_mode() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("pc_cli_pkey_mode_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("priv.key"),
],
b"",
)
.1,
"genpkey failed"
);
assert!(
run(
&["pkey", "-in", &p("priv.key"), "-out", &p("priv2.key")],
b"",
)
.1,
"pkey re-emit failed"
);
let mode = std::fs::metadata(p("priv2.key"))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o600, "expected 0o600, got {mode:o}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn req_rejects_subject_with_newline() {
let dir = std::env::temp_dir().join(format!("pc_cli_subj_nl_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("k.pem"),
],
b"",
)
.1,
"genpkey failed"
);
let (_, ok) = run(
&[
"req",
"-key",
&p("k.pem"),
"-subj",
"/CN=evil\nbob",
"-out",
&p("evil.csr"),
],
b"",
);
assert!(!ok, "req should fail on subject with control char");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn ca_subcommand_full_flow() {
use purecrypto::x509::{Certificate, CertificateRevocationList};
let dir = std::env::temp_dir().join(format!("pc_cli_ca_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"ca",
"init",
"-dir",
dir.to_str().unwrap(),
"-cn",
"Test CLI CA"
],
b""
)
.1,
"ca init failed"
);
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("leaf.key"),
],
b""
)
.1,
"genpkey failed"
);
let (pubkey_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
assert!(ok, "pkey -pubout failed");
std::fs::write(dir.join("leaf.pub"), pubkey_pem).unwrap();
assert!(
run(
&[
"ca",
"issue",
"-dir",
dir.to_str().unwrap(),
"-pubkey",
&p("leaf.pub"),
"-cn",
"host.example",
"-sans",
"host.example",
"-out",
&p("leaf.crt"),
],
b""
)
.1,
"ca issue failed"
);
let (text, ok) = run(&["x509", "-in", &p("leaf.crt"), "-text"], b"");
assert!(ok, "x509 inspect failed: {text}");
assert!(text.contains("CN=host.example"), "subject missing: {text}");
assert!(text.contains("CN=Test CLI CA"), "issuer missing: {text}");
let root_pem = std::fs::read_to_string(dir.join("root.crt")).unwrap();
let leaf_pem = std::fs::read_to_string(dir.join("leaf.crt")).unwrap();
let root = Certificate::from_pem(&root_pem).unwrap();
let leaf = Certificate::from_pem(&leaf_pem).unwrap();
let root_key = root.subject_public_key().unwrap();
leaf.verify_signature_with(&root_key)
.expect("leaf should verify under the CA key");
assert!(
run(
&[
"ca",
"revoke",
"-dir",
dir.to_str().unwrap(),
"-serial",
"2",
"-reason",
"key-compromise",
],
b""
)
.1,
"ca revoke failed"
);
assert!(
run(
&[
"ca",
"crl",
"-dir",
dir.to_str().unwrap(),
"-out",
&p("crl.pem"),
],
b""
)
.1,
"ca crl failed"
);
let crl_pem = std::fs::read_to_string(dir.join("crl.pem")).unwrap();
let crl = CertificateRevocationList::from_pem(&crl_pem).unwrap();
assert!(
crl.is_revoked(&[2]).unwrap(),
"CRL should list serial 2 as revoked"
);
crl.verify_signature_with(&root_key)
.expect("CRL signature should verify under the CA key");
crl.check_signature_algid_consistent()
.expect("CRL inner/outer algid should agree");
let (show, ok) = run(&["ca", "show", "-dir", dir.to_str().unwrap()], b"");
assert!(ok, "ca show failed");
assert!(show.contains("CN=Test CLI CA"), "show output: {show}");
assert!(show.contains("Revoked: 1"), "show output: {show}");
assert!(show.contains("CRL: present"), "show output: {show}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn s_client_loopback() {
use purecrypto::rsa::{BoxedRsaPrivateKey, RsaPrivateKey};
use purecrypto::tls::{Config, Connection, HandshakeStatus, SigningKey};
use purecrypto::x509::{Certificate, DistinguishedName, Time, Validity};
use std::net::TcpListener;
const KEY: &str = include_str!("../testdata/rsa2048_test_a.pem");
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().unwrap().port();
let server = std::thread::spawn(move || {
let (mut sock, _) = listener.accept().expect("accept");
let signing = RsaPrivateKey::<32>::from_pkcs1_pem(KEY).unwrap();
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed(
&signing,
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
)
.unwrap();
let key = BoxedRsaPrivateKey::from_pkcs1_pem(KEY).unwrap();
let cfg = Config::builder()
.tls_only()
.identity(vec![cert.to_der().to_vec()], SigningKey::Rsa(key))
.build();
let mut conn = Connection::server(&cfg).expect("server config");
let mut read_buf = [0u8; 8192];
loop {
let out = conn.pop().unwrap_or_default();
if !out.is_empty() {
sock.write_all(&out).unwrap();
}
match conn.handshake().unwrap() {
HandshakeStatus::Complete => break,
HandshakeStatus::WantWrite => continue,
HandshakeStatus::WantRead => {
let n = sock.read(&mut read_buf).expect("read");
if n == 0 {
panic!("peer closed during handshake");
}
conn.feed(&read_buf[..n]).expect("feed");
}
}
}
let mut got = conn.recv().unwrap_or_default();
while got.is_empty() {
let n = sock.read(&mut read_buf).unwrap();
if n == 0 {
break;
}
conn.feed(&read_buf[..n]).unwrap();
got = conn.recv().unwrap_or_default();
}
conn.send(b"PONG").unwrap();
let _ = conn.close();
let out = conn.pop().unwrap_or_default();
sock.write_all(&out).unwrap();
sock.flush().unwrap();
let _ = sock.shutdown(std::net::Shutdown::Write);
});
let (out, err, ok) = run_capture(
&[
"s_client",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-quiet",
],
b"PING",
);
server.join().unwrap();
assert!(ok, "s_client exited with failure");
assert!(
out.contains("PONG"),
"expected PONG in stdout, got: {out:?}"
);
assert!(
err.contains("certificate NOT verified"),
"expected -insecure warning on stderr, got: {err:?}"
);
}
#[test]
fn s_client_s_server_roundtrip_alpn_keylog() {
use purecrypto::ec::Ed25519PrivateKey;
use purecrypto::rng::OsRng;
use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind probe");
let port = listener.local_addr().unwrap().port();
drop(listener);
let dir = std::env::temp_dir().join(format!("pc_s_server_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("server.pem");
let key_path = dir.join("server.key");
let log_path = dir.join("keylog.txt");
let key = Ed25519PrivateKey::generate(&mut OsRng);
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed_general(
&CertSigner::Ed25519(&key),
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
&["127.0.0.1"],
)
.unwrap();
std::fs::write(&cert_path, cert.to_pem()).unwrap();
std::fs::write(&key_path, key.to_pkcs8_pem()).unwrap();
let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args([
"s_server",
"-cert",
cert_path.to_str().unwrap(),
"-key",
key_path.to_str().unwrap(),
"-accept",
&port.to_string(),
"-alpn",
"h2,http/1.1",
"-www",
"-quiet",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn s_server");
std::thread::sleep(std::time::Duration::from_millis(200));
let (out, ok) = run(
&[
"s_client",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-alpn",
"http/1.1",
"-keylogfile",
log_path.to_str().unwrap(),
"-quiet",
],
b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n",
);
let _ = server_proc.wait_with_output();
assert!(ok, "s_client failed");
assert!(
out.contains("hello from purecrypto s_server"),
"expected -www body in client stdout, got: {out:?}"
);
let log = std::fs::read_to_string(&log_path).expect("read keylog");
for label in [
"CLIENT_HANDSHAKE_TRAFFIC_SECRET",
"SERVER_HANDSHAKE_TRAFFIC_SECRET",
"CLIENT_TRAFFIC_SECRET_0",
"SERVER_TRAFFIC_SECRET_0",
"EXPORTER_SECRET",
] {
assert!(log.contains(label), "missing {label} in keylog:\n{log}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "requires network access"]
fn s_client_live_cloudflare() {
let (_out, ok) = run(
&["s_client", "-connect", "cloudflare.com:443"],
b"GET / HTTP/1.1\r\nHost: cloudflare.com\r\nConnection: close\r\n\r\n",
);
assert!(ok);
}
#[test]
fn s_client_s_server_tls12_roundtrip() {
use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
use purecrypto::rng::OsRng;
use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind probe");
let port = listener.local_addr().unwrap().port();
drop(listener);
let dir = std::env::temp_dir().join(format!("pc_s_server_tls12_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("server.pem");
let key_path = dir.join("server.key");
let mut rng = OsRng;
let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed_general(
&CertSigner::Ecdsa(&key),
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
&["127.0.0.1"],
)
.unwrap();
std::fs::write(&cert_path, cert.to_pem()).unwrap();
std::fs::write(&key_path, key.to_sec1_pem()).unwrap();
let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args([
"s_server",
"-tls1_2",
"-cert",
cert_path.to_str().unwrap(),
"-key",
key_path.to_str().unwrap(),
"-accept",
&port.to_string(),
"-www",
"-quiet",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn s_server");
std::thread::sleep(std::time::Duration::from_millis(200));
let (out, ok) = run(
&[
"s_client",
"-tls1_2",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-quiet",
],
b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n",
);
let _ = server_proc.wait_with_output();
assert!(ok, "s_client -tls1_2 failed");
assert!(
out.contains("hello from purecrypto s_server"),
"expected -www body in client stdout, got: {out:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "requires network access"]
fn s_client_live_cloudflare_tls12() {
let (_out, ok) = run(
&["s_client", "-tls1_2", "-connect", "cloudflare.com:443"],
b"GET / HTTP/1.1\r\nHost: cloudflare.com\r\nConnection: close\r\n\r\n",
);
assert!(ok);
}
#[test]
fn s_dtls_client_s_dtls_server_roundtrip() {
use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
use purecrypto::rng::OsRng;
use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};
let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
let port = probe.local_addr().unwrap().port();
drop(probe);
let dir = std::env::temp_dir().join(format!("pc_s_dtls_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("server.pem");
let key_path = dir.join("server.key");
let mut rng = OsRng;
let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed_general(
&CertSigner::Ecdsa(&key),
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
&["127.0.0.1"],
)
.unwrap();
std::fs::write(&cert_path, cert.to_pem()).unwrap();
std::fs::write(&key_path, key.to_sec1_pem()).unwrap();
let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args([
"s_dtls_server",
"-cert",
cert_path.to_str().unwrap(),
"-key",
key_path.to_str().unwrap(),
"-accept",
&format!("127.0.0.1:{port}"),
"-no_cookie",
"-quiet",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn s_dtls_server");
std::thread::sleep(std::time::Duration::from_millis(200));
let (out, _ok) = run(
&[
"s_dtls_client",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-quiet",
],
b"hello\n",
);
let _ = server_proc.wait_with_output();
assert!(
out.contains("hello"),
"expected 'hello' in client stdout, got: {out:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn s_client_s_server_dtls12_roundtrip() {
use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
use purecrypto::rng::OsRng;
use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};
let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
let port = probe.local_addr().unwrap().port();
drop(probe);
let dir = std::env::temp_dir().join(format!("pc_dtls12_unified_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("server.pem");
let key_path = dir.join("server.key");
let mut rng = OsRng;
let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed_general(
&CertSigner::Ecdsa(&key),
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
&["127.0.0.1"],
)
.unwrap();
std::fs::write(&cert_path, cert.to_pem()).unwrap();
std::fs::write(&key_path, key.to_sec1_pem()).unwrap();
let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args([
"s_server",
"-dtls1_2",
"-cert",
cert_path.to_str().unwrap(),
"-key",
key_path.to_str().unwrap(),
"-accept",
&format!("127.0.0.1:{port}"),
"-no_cookie",
"-quiet",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn s_server -dtls1_2");
std::thread::sleep(std::time::Duration::from_millis(200));
let (out, _ok) = run(
&[
"s_client",
"-dtls1_2",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-quiet",
],
b"hello\n",
);
let _ = server_proc.wait_with_output();
assert!(
out.contains("hello"),
"expected 'hello' in client stdout, got: {out:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn s_client_s_server_dtls13_roundtrip() {
use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
use purecrypto::rng::OsRng;
use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};
let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
let port = probe.local_addr().unwrap().port();
drop(probe);
let dir = std::env::temp_dir().join(format!("pc_dtls13_unified_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("server.pem");
let key_path = dir.join("server.key");
let mut rng = OsRng;
let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed_general(
&CertSigner::Ecdsa(&key),
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
&["127.0.0.1"],
)
.unwrap();
std::fs::write(&cert_path, cert.to_pem()).unwrap();
std::fs::write(&key_path, key.to_sec1_pem()).unwrap();
let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args([
"s_server",
"-dtls1_3",
"-cert",
cert_path.to_str().unwrap(),
"-key",
key_path.to_str().unwrap(),
"-accept",
&format!("127.0.0.1:{port}"),
"-no_cookie",
"-quiet",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn s_server -dtls1_3");
std::thread::sleep(std::time::Duration::from_millis(200));
let (out, _ok) = run(
&[
"s_client",
"-dtls1_3",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-quiet",
],
b"hello\n",
);
let _ = server_proc.wait_with_output();
assert!(
out.contains("hello"),
"expected 'hello' in client stdout, got: {out:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn genpkey_ec_then_inspect() {
let (key_pem, ok) = run(&["genpkey", "-algorithm", "EC", "-curve", "P-256"], b"");
assert!(ok);
assert!(key_pem.contains("BEGIN EC PRIVATE KEY"));
let (pub_pem, ok) = run(&["pkey", "-pubout"], key_pem.as_bytes());
assert!(ok);
assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
let (text, ok) = run(&["pkey", "-text"], key_pem.as_bytes());
assert!(ok);
assert!(text.contains("P-256"));
}
#[test]
fn genpkey_ed25519_then_inspect() {
let (key_pem, ok) = run(&["genpkey", "-algorithm", "ED25519"], b"");
assert!(ok);
assert!(key_pem.contains("BEGIN PRIVATE KEY"));
let (pub_pem, ok) = run(&["pkey", "-pubout"], key_pem.as_bytes());
assert!(ok);
assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
let (text, ok) = run(&["pkey", "-text"], key_pem.as_bytes());
assert!(ok);
assert!(text.contains("Ed25519"));
}
#[test]
fn genpkey_ml_dsa_65_roundtrip() {
let (pem, ok) = run(&["genpkey", "-algorithm", "ML-DSA-65"], b"");
assert!(ok);
assert!(pem.contains("BEGIN PRIVATE KEY"));
let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
assert!(ok);
assert!(text.contains("ML-DSA-65"));
let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
assert!(ok);
assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
}
#[test]
fn genpkey_ml_kem_768_roundtrip() {
let (pem, ok) = run(&["genpkey", "-algorithm", "ML-KEM-768"], b"");
assert!(ok);
let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
assert!(ok);
assert!(text.contains("ML-KEM-768"));
}
#[test]
fn genpkey_ml_kem_512_roundtrip() {
let (pem, ok) = run(&["genpkey", "-algorithm", "ML-KEM-512"], b"");
assert!(ok);
let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
assert!(ok && text.contains("ML-KEM-512"));
let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
assert!(ok && pub_pem.contains("BEGIN PUBLIC KEY"));
}
#[test]
fn genpkey_ml_kem_1024_roundtrip() {
let (pem, ok) = run(&["genpkey", "-algorithm", "ML-KEM-1024"], b"");
assert!(ok);
let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
assert!(ok && text.contains("ML-KEM-1024"));
let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
assert!(ok && pub_pem.contains("BEGIN PUBLIC KEY"));
}
#[test]
fn genpkey_slh_dsa_sha2_128f_roundtrip() {
let (pem, ok) = run(&["genpkey", "-algorithm", "SLH-DSA-SHA2-128f"], b"");
assert!(ok);
let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
assert!(ok);
assert!(text.contains("SLH-DSA"));
let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
assert!(ok);
assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
}
#[test]
fn ca_template_tls_server() {
let dir = std::env::temp_dir().join(format!("pc_tmpl_srv_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"ca",
"init",
"-dir",
dir.to_str().unwrap(),
"-cn",
"Root CA"
],
b"",
)
.1,
"ca init failed"
);
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("leaf.key")
],
b""
)
.1
);
let (pubkey_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("leaf.pub"), pubkey_pem).unwrap();
assert!(
run(
&[
"ca",
"issue",
"-dir",
dir.to_str().unwrap(),
"-template",
"tls-server",
"-pubkey",
&p("leaf.pub"),
"-cn",
"host.example",
"-sans",
"host.example,*.host.example",
"-out",
&p("leaf.crt"),
],
b""
)
.1,
"ca issue with template failed"
);
let (text, ok) = run(&["x509", "-in", &p("leaf.crt"), "-text", "-ext"], b"");
assert!(ok, "x509 -text -ext failed: {text}");
assert!(text.contains("CA: false"), "basicConstraints: {text}");
assert!(
text.contains("digitalSignature") && text.contains("keyEncipherment"),
"keyUsage: {text}"
);
assert!(text.contains("serverAuth"), "EKU: {text}");
assert!(
text.contains("DNS:host.example") && text.contains("DNS:*.host.example"),
"SAN: {text}"
);
assert!(text.contains("subjectKeyIdentifier"), "SKI: {text}");
assert!(text.contains("authorityKeyIdentifier"), "AKI: {text}");
let root_pem = std::fs::read_to_string(dir.join("root.crt")).unwrap();
let leaf_pem = std::fs::read_to_string(dir.join("leaf.crt")).unwrap();
let root = purecrypto::x509::Certificate::from_pem(&root_pem).unwrap();
let leaf = purecrypto::x509::Certificate::from_pem(&leaf_pem).unwrap();
let root_key = root.subject_public_key().unwrap();
leaf.verify_signature_with(&root_key).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn ca_template_intermediate_chain() {
let dir = std::env::temp_dir().join(format!("pc_tmpl_chain_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"ca",
"init",
"-dir",
dir.to_str().unwrap(),
"-cn",
"Chain Root"
],
b"",
)
.1
);
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("int.key")
],
b""
)
.1
);
let (int_pub, ok) = run(&["pkey", "-in", &p("int.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("int.pub"), int_pub).unwrap();
assert!(
run(
&[
"ca",
"issue",
"-dir",
dir.to_str().unwrap(),
"-template",
"ca-intermediate",
"-pubkey",
&p("int.pub"),
"-cn",
"Chain Intermediate",
"-out",
&p("int.crt"),
],
b""
)
.1,
"intermediate issue failed"
);
let root_pem = std::fs::read_to_string(dir.join("root.crt")).unwrap();
let int_pem = std::fs::read_to_string(dir.join("int.crt")).unwrap();
let root = purecrypto::x509::Certificate::from_pem(&root_pem).unwrap();
let int_cert = purecrypto::x509::Certificate::from_pem(&int_pem).unwrap();
int_cert
.verify_signature_with(&root.subject_public_key().unwrap())
.expect("intermediate must verify under root");
let bc = int_cert.basic_constraints().unwrap().unwrap();
assert_eq!(bc, (true, Some(0)), "ca-intermediate path_len");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn req_template_csr() {
let dir = std::env::temp_dir().join(format!("pc_tmpl_req_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("leaf.key")
],
b""
)
.1
);
assert!(
run(
&[
"req",
"-key",
&p("leaf.key"),
"-subj",
"/CN=tmpl.example",
"-template",
"tls-server",
"-san",
"tmpl.example,alt.example",
"-out",
&p("leaf.csr"),
],
b"",
)
.1,
"req -template failed"
);
let (vout, ok) = run(&["req", "-in", &p("leaf.csr"), "-verify"], b"");
assert!(ok && vout.contains("verify OK"));
let csr_pem = std::fs::read_to_string(dir.join("leaf.csr")).unwrap();
let csr = purecrypto::x509::CertificationRequest::from_pem(&csr_pem).unwrap();
let exts = csr.extension_requests().unwrap();
assert!(
exts.iter()
.any(|e| e.oid == purecrypto::x509::oid::EXT_KEY_USAGE),
"CSR should request EKU"
);
assert!(
exts.iter()
.any(|e| e.oid == purecrypto::x509::oid::KEY_USAGE),
"CSR should request keyUsage"
);
let sans = csr.subject_alt_names().unwrap();
assert!(sans.contains(&"tmpl.example".to_string()));
assert!(sans.contains(&"alt.example".to_string()));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn template_user_file_overrides_critical() {
let dir = std::env::temp_dir().join(format!("pc_tmpl_user_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let p = |name: &str| dir.join(name).to_str().unwrap().to_string();
assert!(
run(
&[
"ca",
"init",
"-dir",
dir.to_str().unwrap(),
"-cn",
"UF Root"
],
b"",
)
.1
);
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("leaf.key")
],
b""
)
.1
);
let (pubkey_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("leaf.pub"), pubkey_pem).unwrap();
let tmpl = r#"name = "custom"
[basic_constraints]
ca = false
[key_usage]
critical = false
digital_signature = true
[subject_key_identifier]
include = true
[authority_key_identifier]
include = true
"#;
let tmpl_path = dir.join("custom.toml");
std::fs::write(&tmpl_path, tmpl).unwrap();
assert!(
run(
&[
"ca",
"issue",
"-dir",
dir.to_str().unwrap(),
"-template-file",
tmpl_path.to_str().unwrap(),
"-pubkey",
&p("leaf.pub"),
"-cn",
"user.example",
"-out",
&p("leaf.crt"),
],
b"",
)
.1,
"user-file override failed"
);
let leaf_pem = std::fs::read_to_string(dir.join("leaf.crt")).unwrap();
let leaf = purecrypto::x509::Certificate::from_pem(&leaf_pem).unwrap();
let exts = leaf.extensions().unwrap();
let ku = exts
.iter()
.find(|e| e.oid == purecrypto::x509::oid::KEY_USAGE)
.expect("keyUsage emitted");
assert!(!ku.critical, "user override should flip critical bit");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn ca_list_templates_lists_builtins() {
let (out, ok) = run(&["ca", "list-templates"], b"");
assert!(ok, "ca list-templates failed");
for name in [
"tls-server",
"tls-client",
"mtls-client",
"ca-root",
"ca-intermediate",
"code-signing",
"email-protection",
"time-stamping",
] {
assert!(out.contains(name), "missing {name} in {out}");
}
}
#[test]
fn mac_hmac_sha256_known_answer() {
let (out, ok) = run(
&[
"mac",
"-alg",
"hmac-sha256",
"-key",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
],
b"Hi There",
);
assert!(ok);
assert_eq!(
out.trim(),
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
);
}
#[test]
fn kdf_hkdf_known_answer() {
let (out, ok) = run(
&[
"kdf",
"hkdf",
"-hash",
"sha256",
"-ikm",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"-salt",
"000102030405060708090a0b0c",
"-info",
"f0f1f2f3f4f5f6f7f8f9",
"-len",
"42",
],
b"",
);
assert!(ok);
assert_eq!(
out.trim(),
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
);
}
#[test]
fn kdf_pbkdf2_known_answer() {
let (out, ok) = run(
&[
"kdf",
"pbkdf2",
"-hash",
"sha256",
"-password",
"passwd",
"-salt",
"73616c74",
"-iter",
"1",
"-len",
"64",
],
b"",
);
assert!(ok);
assert_eq!(
out.trim(),
"55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc\
49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783"
);
}
#[test]
fn enc_aes_gcm_roundtrip() {
let dir = std::env::temp_dir().join(format!("pc_enc_gcm_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
std::fs::write(dir.join("pt.bin"), b"hello purecrypto").unwrap();
let key = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
let nonce = "010203040506070809101112";
assert!(
run(
&[
"enc",
"-alg",
"AES-256-GCM",
"-key",
key,
"-nonce",
nonce,
"-in",
&p("pt.bin"),
"-out",
&p("ct.bin"),
],
b"",
)
.1
);
assert!(
run(
&[
"enc",
"-alg",
"AES-256-GCM",
"-d",
"-key",
key,
"-nonce",
nonce,
"-in",
&p("ct.bin"),
"-out",
&p("rt.bin"),
],
b"",
)
.1
);
let rt = std::fs::read(dir.join("rt.bin")).unwrap();
assert_eq!(rt, b"hello purecrypto");
let mut ct = std::fs::read(dir.join("ct.bin")).unwrap();
*ct.last_mut().unwrap() ^= 1;
std::fs::write(dir.join("ct_bad.bin"), &ct).unwrap();
let (_o, ok) = run(
&[
"enc",
"-alg",
"AES-256-GCM",
"-d",
"-key",
key,
"-nonce",
nonce,
"-in",
&p("ct_bad.bin"),
"-out",
&p("rt_bad.bin"),
],
b"",
);
assert!(!ok, "tampered AES-GCM ciphertext must be rejected");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn enc_chacha20_poly1305_roundtrip() {
let dir = std::env::temp_dir().join(format!("pc_enc_cc20_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
std::fs::write(dir.join("pt.bin"), b"chacha20 + poly1305").unwrap();
let key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
let nonce = "010203040506070809101112";
assert!(
run(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-key",
key,
"-nonce",
nonce,
"-aad",
"deadbeef",
"-in",
&p("pt.bin"),
"-out",
&p("ct.bin"),
],
b"",
)
.1
);
assert!(
run(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-d",
"-key",
key,
"-nonce",
nonce,
"-aad",
"deadbeef",
"-in",
&p("ct.bin"),
"-out",
&p("rt.bin"),
],
b"",
)
.1
);
let rt = std::fs::read(dir.join("rt.bin")).unwrap();
assert_eq!(rt, b"chacha20 + poly1305");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn enc_keyfile_and_aadfile_roundtrip() {
let dir = std::env::temp_dir().join(format!("pc_enc_keyfile_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
std::fs::write(dir.join("pt.bin"), b"keyfile + aadfile").unwrap();
let key_bytes: Vec<u8> = (0..32u8).collect();
let aad_bytes: Vec<u8> = vec![0xde, 0xad, 0xbe, 0xef];
std::fs::write(dir.join("key.bin"), &key_bytes).unwrap();
std::fs::write(dir.join("aad.bin"), &aad_bytes).unwrap();
let nonce = "010203040506070809101112";
assert!(
run(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-keyfile",
&p("key.bin"),
"-nonce",
nonce,
"-aadfile",
&p("aad.bin"),
"-in",
&p("pt.bin"),
"-out",
&p("ct.bin"),
],
b"",
)
.1
);
assert!(
run(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-d",
"-keyfile",
&p("key.bin"),
"-nonce",
nonce,
"-aadfile",
&p("aad.bin"),
"-in",
&p("ct.bin"),
"-out",
&p("rt.bin"),
],
b"",
)
.1
);
let rt = std::fs::read(dir.join("rt.bin")).unwrap();
assert_eq!(rt, b"keyfile + aadfile");
let key_hex: String = key_bytes.iter().map(|b| format!("{b:02x}")).collect();
let aad_hex: String = aad_bytes.iter().map(|b| format!("{b:02x}")).collect();
let (_o, _e, ok) = run_capture(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-d",
"-key",
&key_hex,
"-nonce",
nonce,
"-aad",
&aad_hex,
"-in",
&p("ct.bin"),
"-out",
&p("rt2.bin"),
],
b"",
);
assert!(ok);
assert_eq!(
std::fs::read(dir.join("rt2.bin")).unwrap(),
b"keyfile + aadfile"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn enc_key_and_aad_argv_warn() {
let dir = std::env::temp_dir().join(format!("pc_enc_warn_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
std::fs::write(dir.join("pt.bin"), b"argv warning").unwrap();
let key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
let nonce = "010203040506070809101112";
let (_out, err, ok) = run_capture(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-key",
key,
"-nonce",
nonce,
"-aad",
"deadbeef",
"-in",
&p("pt.bin"),
"-out",
&p("ct.bin"),
],
b"",
);
assert!(ok);
assert!(
err.contains("-key HEX exposes"),
"expected -key argv warning, got stderr: {err}"
);
assert!(
err.contains("-aad HEX exposes"),
"expected -aad argv warning, got stderr: {err}"
);
let key_bytes: Vec<u8> = (0..32u8).collect();
std::fs::write(dir.join("key.bin"), &key_bytes).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(dir.join("key.bin"))
.unwrap()
.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(dir.join("key.bin"), perms).unwrap();
}
let (_out, err2, ok2) = run_capture(
&[
"enc",
"-alg",
"CHACHA20-POLY1305",
"-keyfile",
&p("key.bin"),
"-nonce",
nonce,
"-in",
&p("pt.bin"),
"-out",
&p("ct2.bin"),
],
b"",
);
assert!(ok2);
assert!(
!err2.contains("exposes"),
"expected no argv warning when using -keyfile, got stderr: {err2}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn kem_mlkem768_round_trip() {
let dir = std::env::temp_dir().join(format!("pc_kem_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
assert!(
run(
&[
"kem",
"keygen",
"-alg",
"ML-KEM-768",
"-out-secret",
&p("sk.pem"),
"-out-public",
&p("pk.pem"),
],
b""
)
.1
);
assert!(
run(
&[
"kem",
"encaps",
"-peer",
&p("pk.pem"),
"-out-ct",
&p("ct.bin"),
"-out-ss",
&p("ss1.bin"),
],
b""
)
.1
);
assert!(
run(
&[
"kem",
"decaps",
"-key",
&p("sk.pem"),
"-ct",
&p("ct.bin"),
"-out-ss",
&p("ss2.bin"),
],
b""
)
.1
);
let s1 = std::fs::read(dir.join("ss1.bin")).unwrap();
let s2 = std::fs::read(dir.join("ss2.bin")).unwrap();
assert_eq!(s1, s2, "ML-KEM shared secrets must match");
assert_eq!(s1.len(), 32);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn kex_x25519_round_trip() {
let dir = std::env::temp_dir().join(format!("pc_kex_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
use purecrypto::ec::x25519::X25519PrivateKey;
let a_priv = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
let b_priv = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
let priv_bytes = |s: &str| {
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap();
}
out
};
let a_pub = X25519PrivateKey::from_bytes(priv_bytes(a_priv)).public_key();
let b_pub = X25519PrivateKey::from_bytes(priv_bytes(b_priv)).public_key();
let a_pub_hex = a_pub.iter().fold(String::new(), |mut s, b| {
s.push_str(&format!("{b:02x}"));
s
});
let b_pub_hex = b_pub.iter().fold(String::new(), |mut s, b| {
s.push_str(&format!("{b:02x}"));
s
});
use std::io::Write;
std::fs::File::create(dir.join("a_priv.hex"))
.unwrap()
.write_all(a_priv.as_bytes())
.unwrap();
std::fs::File::create(dir.join("b_priv.hex"))
.unwrap()
.write_all(b_priv.as_bytes())
.unwrap();
std::fs::File::create(dir.join("a_pub.hex"))
.unwrap()
.write_all(a_pub_hex.as_bytes())
.unwrap();
std::fs::File::create(dir.join("b_pub.hex"))
.unwrap()
.write_all(b_pub_hex.as_bytes())
.unwrap();
assert!(
run(
&[
"kex",
"-alg",
"X25519",
"-key",
&p("a_priv.hex"),
"-peer",
&p("b_pub.hex"),
"-out",
&p("ss_a.bin")
],
b""
)
.1
);
assert!(
run(
&[
"kex",
"-alg",
"X25519",
"-key",
&p("b_priv.hex"),
"-peer",
&p("a_pub.hex"),
"-out",
&p("ss_b.bin")
],
b""
)
.1
);
let s1 = std::fs::read(dir.join("ss_a.bin")).unwrap();
let s2 = std::fs::read(dir.join("ss_b.bin")).unwrap();
assert_eq!(s1, s2, "X25519 shared secrets must match");
assert_eq!(s1.len(), 32);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn kex_ecdh_p256_round_trip() {
let dir = std::env::temp_dir().join(format!("pc_ecdh_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
for name in &["a", "b"] {
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p(&format!("{name}.key")),
],
b""
)
.1
);
let (pub_pem, ok) = run(&["pkey", "-in", &p(&format!("{name}.key")), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join(format!("{name}.pub")), pub_pem).unwrap();
}
assert!(
run(
&[
"kex",
"-alg",
"ECDH-P256",
"-key",
&p("a.key"),
"-peer",
&p("b.pub"),
"-out",
&p("ss_a.bin"),
],
b""
)
.1
);
assert!(
run(
&[
"kex",
"-alg",
"ECDH-P256",
"-key",
&p("b.key"),
"-peer",
&p("a.pub"),
"-out",
&p("ss_b.bin"),
],
b""
)
.1
);
let s1 = std::fs::read(dir.join("ss_a.bin")).unwrap();
let s2 = std::fs::read(dir.join("ss_b.bin")).unwrap();
assert_eq!(s1, s2);
assert_eq!(s1.len(), 32);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pkeyutl_rsa_oaep_round_trip() {
let dir = std::env::temp_dir().join(format!("pc_oaep_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"RSA",
"-bits",
"2048",
"-out",
&p("rsa.key")
],
b"",
)
.1
);
let (pub_pem, ok) = run(&["pkey", "-in", &p("rsa.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("rsa.pub"), pub_pem).unwrap();
std::fs::write(dir.join("msg.bin"), b"oaep round trip").unwrap();
assert!(
run(
&[
"pkeyutl",
"encrypt",
"-inkey",
&p("rsa.pub"),
"-pubin",
"-pkeyopt",
"rsa_padding_mode:oaep",
"-pkeyopt",
"rsa_oaep_md:sha256",
"-in",
&p("msg.bin"),
"-out",
&p("ct.bin"),
],
b"",
)
.1
);
assert!(
run(
&[
"pkeyutl",
"decrypt",
"-inkey",
&p("rsa.key"),
"-pkeyopt",
"rsa_padding_mode:oaep",
"-pkeyopt",
"rsa_oaep_md:sha256",
"-in",
&p("ct.bin"),
"-out",
&p("rt.bin"),
],
b"",
)
.1
);
let rt = std::fs::read(dir.join("rt.bin")).unwrap();
assert_eq!(rt, b"oaep round trip");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pkeyutl_rsa_pss_sign_verify() {
let dir = std::env::temp_dir().join(format!("pc_pss_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"RSA",
"-bits",
"2048",
"-out",
&p("rsa.key")
],
b"",
)
.1
);
let (pub_pem, ok) = run(&["pkey", "-in", &p("rsa.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("rsa.pub"), pub_pem).unwrap();
std::fs::write(dir.join("msg.bin"), b"pss message").unwrap();
assert!(
run(
&[
"pkeyutl",
"sign",
"-inkey",
&p("rsa.key"),
"-pkeyopt",
"rsa_padding_mode:pss",
"-pkeyopt",
"digest:sha256",
"-in",
&p("msg.bin"),
"-out",
&p("sig.bin"),
],
b"",
)
.1
);
let (vout, ok) = run(
&[
"pkeyutl",
"verify",
"-inkey",
&p("rsa.pub"),
"-pkeyopt",
"rsa_padding_mode:pss",
"-pkeyopt",
"digest:sha256",
"-sigfile",
&p("sig.bin"),
"-in",
&p("msg.bin"),
],
b"",
);
assert!(ok, "{vout}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pkeyutl_ed25519_sign_verify() {
let dir = std::env::temp_dir().join(format!("pc_ed_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
assert!(
run(
&["genpkey", "-algorithm", "ED25519", "-out", &p("ed.key")],
b"",
)
.1
);
let (pub_pem, ok) = run(&["pkey", "-in", &p("ed.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("ed.pub"), pub_pem).unwrap();
std::fs::write(dir.join("msg.bin"), b"ed25519 message").unwrap();
assert!(
run(
&[
"pkeyutl",
"sign",
"-inkey",
&p("ed.key"),
"-in",
&p("msg.bin"),
"-out",
&p("sig.bin"),
],
b"",
)
.1
);
let (_vout, ok) = run(
&[
"pkeyutl",
"verify",
"-inkey",
&p("ed.pub"),
"-sigfile",
&p("sig.bin"),
"-in",
&p("msg.bin"),
],
b"",
);
assert!(ok);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pkeyutl_mldsa65_sign_verify() {
let dir = std::env::temp_dir().join(format!("pc_mldsa_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
assert!(
run(
&[
"genpkey",
"-algorithm",
"ML-DSA-65",
"-out",
&p("mldsa.key")
],
b"",
)
.1
);
let (pub_pem, ok) = run(&["pkey", "-in", &p("mldsa.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("mldsa.pub"), pub_pem).unwrap();
std::fs::write(dir.join("msg.bin"), b"hello ml-dsa").unwrap();
assert!(
run(
&[
"pkeyutl",
"sign",
"-inkey",
&p("mldsa.key"),
"-in",
&p("msg.bin"),
"-out",
&p("sig.bin"),
],
b"",
)
.1
);
let (_vout, ok) = run(
&[
"pkeyutl",
"verify",
"-inkey",
&p("mldsa.pub"),
"-sigfile",
&p("sig.bin"),
"-in",
&p("msg.bin"),
],
b"",
);
assert!(ok);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn crl_inspect_verify_serial() {
let dir = std::env::temp_dir().join(format!("pc_crl_cli_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
assert!(
run(
&[
"ca",
"init",
"-dir",
dir.to_str().unwrap(),
"-cn",
"Test CRL CA"
],
b""
)
.1
);
assert!(
run(
&[
"genpkey",
"-algorithm",
"EC",
"-curve",
"P-256",
"-out",
&p("leaf.key")
],
b"",
)
.1
);
let (pub_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
assert!(ok);
std::fs::write(dir.join("leaf.pub"), pub_pem).unwrap();
assert!(
run(
&[
"ca",
"issue",
"-dir",
dir.to_str().unwrap(),
"-pubkey",
&p("leaf.pub"),
"-cn",
"leaf.test",
"-out",
&p("leaf.crt"),
],
b"",
)
.1
);
assert!(
run(
&[
"ca",
"revoke",
"-dir",
dir.to_str().unwrap(),
"-serial",
"2",
"-reason",
"key-compromise",
],
b"",
)
.1
);
assert!(
run(
&[
"ca",
"crl",
"-dir",
dir.to_str().unwrap(),
"-out",
&p("ca.crl")
],
b"",
)
.1
);
let (text, ok) = run(&["crl", "-in", &p("ca.crl"), "-text"], b"");
assert!(ok);
assert!(text.contains("Test CRL CA"), "{text}");
assert!(text.contains("Revoked entries"), "{text}");
let (vout, ok) = run(
&[
"crl",
"-in",
&p("ca.crl"),
"-CAfile",
&p("root.crt"),
"-verify",
],
b"",
);
assert!(ok, "{vout}");
assert!(vout.contains("verify OK"), "{vout}");
let (_o, ok) = run(
&["crl", "-in", &p("ca.crl"), "-serial", "2", "-is-revoked"],
b"",
);
assert!(ok);
let (_o, ok) = run(
&["crl", "-in", &p("ca.crl"), "-serial", "999", "-is-revoked"],
b"",
);
assert!(!ok);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn q_client_q_server_roundtrip() {
use purecrypto::ec::Ed25519PrivateKey;
use purecrypto::rng::OsRng;
use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};
let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
let port = probe.local_addr().unwrap().port();
drop(probe);
let dir = std::env::temp_dir().join(format!("pc_quic_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
let mut rng = OsRng;
let key = Ed25519PrivateKey::generate(&mut rng);
let validity = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed_general(
&CertSigner::Ed25519(&key),
&DistinguishedName::common_name("127.0.0.1"),
&validity,
1,
false,
&["127.0.0.1"],
)
.unwrap();
std::fs::write(&cert_path, cert.to_pem()).unwrap();
std::fs::write(&key_path, key.to_pkcs8_pem()).unwrap();
let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.args([
"q_server",
"-accept",
&format!("127.0.0.1:{port}"),
"-cert",
cert_path.to_str().unwrap(),
"-key",
key_path.to_str().unwrap(),
"-alpn",
"h3",
"-www",
"-quiet",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn q_server");
std::thread::sleep(std::time::Duration::from_millis(300));
let (out, _ok) = run(
&[
"q_client",
"-connect",
&format!("127.0.0.1:{port}"),
"-insecure",
"-alpn",
"h3",
"-quiet",
],
b"",
);
let _ = server_proc.wait_with_output();
assert!(
out.contains("hello from purecrypto q_server"),
"expected -www body in q_client stdout, got: {out:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cli_subcommand_table_help() {
for sub in [
"hash",
"mac",
"kdf",
"enc",
"kem",
"kex",
"pkeyutl",
"crl",
"rand",
"genpkey",
"pkey",
"req",
"x509",
"ca",
"s_client",
"s_server",
"s_dtls_client",
"s_dtls_server",
"q_client",
"q_server",
] {
let mut child = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
.arg(sub)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
drop(child.stdin.take()); let out = child.wait_with_output().unwrap();
let combined = String::from_utf8_lossy(&out.stdout).into_owned()
+ &String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() || combined.contains(sub) || !combined.is_empty(),
"subcommand `{sub}` produced no output / bad status: {combined}"
);
}
}