use hex_literal::hex;
use wolfhsm::crypto::rsa::RsaRawOp;
use wolfhsm::nvm::NvmId;
use wolfhsm::{Client, Transport, Error};
fn connect_or_skip() -> Option<Client> {
let server = std::env::var("WOLFHSM_SERVER").ok()?;
let transport = if server.starts_with('/') {
Transport::Uds { path: server }
} else {
let (ip, port_str) = server.rsplit_once(':')?;
let port: u16 = port_str.parse().ok()?;
Transport::Tcp {
ip: ip.to_string(),
port,
}
};
Client::connect(transport, 1).ok()
}
macro_rules! require_client {
($name:ident) => {
let mut $name = match connect_or_skip() {
Some(c) => c,
None => return,
};
};
}
#[test]
fn connect_echo() {
require_client!(client);
let msg = b"hello wolfhsm";
let mut buf = [0u8; 32];
let n = client.echo(msg, &mut buf).expect("echo");
assert_eq!(&buf[..n], msg);
}
#[test]
fn server_info() {
require_client!(client);
let info = client.info().expect("info");
assert!(
info.version > 0 || info.build > 0,
"server returned all-zero version/build: {info:?}",
);
}
#[test]
fn rng_nonzero() {
require_client!(client);
let bytes = client.rng_generate(32).expect("rng generate");
assert_eq!(bytes.len(), 32);
assert_ne!(bytes, vec![0u8; 32], "RNG returned 32 zero bytes");
}
#[test]
fn with_key_lifecycle() {
require_client!(client);
client
.with_aes_key(&[0u8; 32], |_, _| Ok(()) as Result<(), Error>)
.expect("with_aes_key lifecycle");
}
const NVM_ID_RW: NvmId = NvmId::new(0x4242);
const NVM_ID_LIST: NvmId = NvmId::new(0x4244);
#[test]
fn nvm_available_returns_space() {
require_client!(client);
let avail = client.nvm_available().expect("nvm_available");
assert!(
avail.avail_size > 0 || avail.reclaim_size > 0,
"NVM reports no space at all: {avail:?}",
);
}
#[test]
fn nvm_overwrite_read_delete() {
require_client!(client);
let data = b"integration test payload";
let _ = client.nvm_delete(NVM_ID_RW);
client
.nvm_overwrite(NVM_ID_RW, 0, 0, b"test", data)
.expect("nvm_overwrite");
let read = client.nvm_read(NVM_ID_RW, 0).expect("nvm_read");
assert_eq!(read, data);
client.nvm_delete(NVM_ID_RW).expect("nvm_delete");
}
#[test]
fn nvm_list_contains_written_object() {
require_client!(client);
let _ = client.nvm_delete(NVM_ID_LIST);
client
.nvm_overwrite(NVM_ID_LIST, 0, 0, b"list-test", b"payload")
.expect("nvm_overwrite");
let ids = client.nvm_list().expect("nvm_list");
assert!(
ids.contains(&NVM_ID_LIST),
"written NVM ID not found in list: {ids:?}",
);
client.nvm_delete(NVM_ID_LIST).expect("nvm_delete cleanup");
}
const COUNTER_ID: NvmId = NvmId::new(0x4243);
#[test]
fn counter_lifecycle() {
require_client!(client);
let _ = client.counter_destroy(COUNTER_ID);
let v0 = client.counter_init(COUNTER_ID, 5).expect("counter_init");
assert_eq!(v0, 5);
let v1 = client
.counter_increment(COUNTER_ID)
.expect("counter_increment");
assert_eq!(v1, 6);
let v2 = client.counter_read(COUNTER_ID).expect("counter_read");
assert_eq!(v2, 6);
let v3 = client.counter_reset(COUNTER_ID).expect("counter_reset");
assert_eq!(v3, 0);
client.counter_destroy(COUNTER_ID).expect("counter_destroy");
}
#[test]
fn sha256_nist_abc() {
require_client!(client);
let expected = hex!("ba7816bf8f01cfea414140de5dae2ec73b00361bbef0469121b9e42a45b6b0d5");
let got = client.sha256(b"abc").expect("sha256");
assert_eq!(got, expected);
}
#[test]
fn cmac_nist_empty_message() {
require_client!(client);
let key_bytes = hex!("2b7e151628aed2a6abf7158809cf4f3c");
let expected = hex!("bb1d6929e95937287fa37d129b756746");
let tag = client
.with_cmac_key(&key_bytes, |key, client| key.compute(client, b""))
.expect("cmac compute");
assert_eq!(tag, expected);
}
#[test]
fn aes_gcm_nist_empty_plaintext() {
require_client!(client);
let key_bytes = [0u8; 32];
let iv = [0u8; 12];
let expected_tag = hex!("530f8afbc74536b9a963b4f1c4cb738b");
let (ct, tag) = client
.with_aes_key(&key_bytes, |key, client| key.gcm_encrypt(client, &iv, &[], &[]))
.expect("aes gcm_encrypt");
assert!(ct.is_empty(), "ciphertext of empty plaintext must be empty");
assert_eq!(tag, expected_tag);
}
#[test]
fn ecc_p256_sign_verify_cross() {
require_client!(client);
use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p256::ecdsa::{Signature, VerifyingKey};
use p256::pkcs8::DecodePublicKey;
use sha2::{Digest, Sha256};
let msg = b"cross-validation: wolfhsm signs, p256 verifies";
let digest: [u8; 32] = Sha256::digest(msg).into();
let (sig_der, pub_der) = client
.with_ecc_p256_key(|key, client| {
let sig_der = key.sign_digest(client, &digest)?;
let pub_der = key.public_key_der(client)?;
Ok((sig_der, pub_der))
})
.expect("ecc sign+export");
let vk =
VerifyingKey::from_public_key_der(&pub_der).expect("p256: parse SubjectPublicKeyInfo DER");
let sig = Signature::from_der(&sig_der).expect("p256: parse DER-encoded ECDSA signature");
vk.verify_prehash(&digest, &sig)
.expect("p256: verify_prehash failed — wolfhsm/p256 cross-validation error");
}
#[test]
fn ecc_p256_ecdh_cross() {
require_client!(client);
use p256::pkcs8::{DecodePublicKey, EncodePublicKey};
use p256::{ecdh::EphemeralSecret, PublicKey};
use rand::rngs::OsRng;
let local_secret = EphemeralSecret::random(&mut OsRng);
let local_pub_key = local_secret.public_key();
let local_pub_der = local_pub_key
.to_public_key_der()
.expect("p256: encode SubjectPublicKeyInfo DER");
let (hsm_shared, hsm_pub_der) = client
.with_ecc_p256_key(|key, client| {
let hsm_shared = key.ecdh(client, local_pub_der.as_bytes())?;
let hsm_pub_der = key.public_key_der(client)?;
Ok((hsm_shared, hsm_pub_der))
})
.expect("ecc ecdh+export");
let hsm_public = PublicKey::from_public_key_der(&hsm_pub_der)
.expect("p256: parse HSM SubjectPublicKeyInfo DER");
let local_shared = local_secret.diffie_hellman(&hsm_public);
assert_eq!(
hsm_shared.as_slice(),
local_shared.raw_secret_bytes().as_slice(),
"ECDH shared secrets do not match",
);
}
#[test]
fn ed25519_sign_verify_cross() {
require_client!(client);
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let msg = b"cross-validation: wolfhsm signs, ed25519-dalek verifies";
let (sig_bytes, pub_bytes) = client
.with_ed25519_key(|key, client| {
let sig_bytes = key.sign(client, msg)?;
let pub_bytes = key.public_key(client)?;
Ok((sig_bytes, pub_bytes))
})
.expect("ed25519 sign+export");
let vk = VerifyingKey::from_bytes(&pub_bytes).expect("ed25519-dalek: parse public key bytes");
let sig = Signature::from_bytes(&sig_bytes);
vk.verify(msg, &sig)
.expect("ed25519-dalek: verify failed — wolfhsm/ed25519-dalek cross-validation error");
}
#[test]
fn curve25519_x25519_ecdh_cross() {
require_client!(client);
use rand::rngs::OsRng;
use x25519_dalek::{PublicKey, StaticSecret};
let local_secret = StaticSecret::random_from_rng(OsRng);
let local_public = PublicKey::from(&local_secret);
let (hsm_shared, hsm_pub_bytes) = client
.with_curve25519_key(|key, client| {
let hsm_shared = key.diffie_hellman(client, local_public.as_bytes())?;
let hsm_pub_bytes = key.public_key(client)?;
Ok((hsm_shared, hsm_pub_bytes))
})
.expect("curve25519 dh+export");
let hsm_public = PublicKey::from(hsm_pub_bytes);
let local_shared = local_secret.diffie_hellman(&hsm_public);
assert_eq!(
hsm_shared,
*local_shared.as_bytes(),
"X25519 shared secrets do not match",
);
}
#[test]
fn rsa_round_trip() {
require_client!(client);
use rsa::{pkcs8::DecodePublicKey, traits::PublicKeyParts, BigUint, RsaPublicKey};
let (msg, pub_der, ciphertext, plaintext) = client
.with_rsa_key(1024, 65537, |key, client| {
let key_size = key.key_size_bytes() as usize;
let mut msg = vec![0u8; key_size];
msg[key_size - 1] = 0x42;
let pub_der = key.public_key_der(client)?;
let ciphertext = key.raw_op(client, RsaRawOp::PublicEncrypt, &msg)?;
let plaintext = key.raw_op(client, RsaRawOp::PrivateDecrypt, &ciphertext)?;
Ok((msg, pub_der, ciphertext, plaintext))
})
.expect("rsa operations");
let key_size = msg.len();
let pub_key = RsaPublicKey::from_public_key_der(&pub_der).expect("parse public key DER");
let m_big = BigUint::from_bytes_be(&msg);
let c_expected = m_big.modpow(pub_key.e(), pub_key.n());
let mut c_expected_bytes = c_expected.to_bytes_be();
while c_expected_bytes.len() < key_size {
c_expected_bytes.insert(0, 0);
}
assert_eq!(
ciphertext, c_expected_bytes,
"HSM PublicEncrypt mismatch vs pure-Rust m^e mod n"
);
assert_eq!(plaintext, msg, "RSA round-trip plaintext mismatch");
}
#[cfg(feature = "mldsa")]
#[test]
fn mldsa_round_trip() {
require_client!(client);
let msg = b"ML-DSA level-44 round-trip test";
client
.with_mldsa_key(44, |key, client| {
let sig = key.sign(client, msg)?;
key.verify(client, msg, &sig)
})
.expect("mldsa round-trip");
}
#[test]
fn cryptocb_register_lifecycle() {
let mut client1 = match connect_or_skip() {
Some(c) => c,
None => return,
};
let mut client2 = match connect_or_skip() {
Some(c) => c,
None => return,
};
let guard1 = client1.register_cryptocb().expect("first registration");
match client2.register_cryptocb() {
Err(Error::AlreadyRegistered) => {}
Err(e) => panic!("expected AlreadyRegistered, got Err: {e}"),
Ok(_) => panic!("expected Err(AlreadyRegistered), got Ok"),
}
drop(guard1);
let guard2 = client2
.register_cryptocb()
.expect("re-registration after guard drop");
drop(guard2);
}