#![cfg_attr(not(feature = "wasm-bindings"), allow(dead_code, unused_imports))]
use crate::shrincs::{
encode_stateful_envelope, Keys, PublicKey, ShrincsSigner, ShrincsVerifier, HASH_LEN,
STATEFUL_PUBLIC_KEY_BYTES,
};
#[cfg(any(test, feature = "wasm-bindings"))]
use crate::verifier::VerifierInterface as _;
use crate::ErrorCode;
#[cfg(any(test, feature = "wasm-bindings"))]
use zeroize::Zeroize;
#[cfg(feature = "wasm-bindings")]
use wasm_bindgen::prelude::*;
#[cfg(any(test, feature = "wasm-bindings"))]
const MAX_STATEFUL_SIGNATURES_LIMIT: usize = 4096;
#[cfg(any(test, feature = "wasm-bindings"))]
#[derive(Debug)]
struct WasmErr {
code: &'static str,
message: String,
}
#[cfg(any(test, feature = "wasm-bindings"))]
#[cfg_attr(not(test), allow(dead_code))]
const TS_ERROR_CODES: &str = r#"
export type ShrincsErrorCode =
| "ERR_BAD_LENGTH" | "ERR_STATEFUL_LEAVES_EXHAUSTED"
| "ERR_SIGNING_FAILED" | "ERR_KEYGEN_FAILED" | "ERR_INVALID_INPUT"
| "ERR_IMPORT_INVALID"
| "ERR_ENVELOPE_MALFORMED";
"#;
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(typescript_custom_section)]
const _TS_ERROR_CODES_TYPESCRIPT_SECTION: &str = TS_ERROR_CODES;
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn bytes_word32(input: &[u8]) -> Result<[u8; HASH_LEN], WasmErr> {
bytes_fixed::<HASH_LEN>(input)
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn bytes_fixed<const N: usize>(input: &[u8]) -> Result<[u8; N], WasmErr> {
if input.len() != N {
return Err(WasmErr {
code: ErrorCode::BadLength.as_str(),
message: format!(
"expected {N} bytes for fixed-width field, got {}",
input.len()
),
});
}
let mut out = [0u8; N];
out.copy_from_slice(input);
Ok(out)
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn message_hash(message: &[u8]) -> Result<[u8; HASH_LEN], WasmErr> {
bytes_word32(message).map_err(|_| WasmErr {
code: ErrorCode::BadLength.as_str(),
message: format!("message must be exactly 32 bytes, got {}", message.len()),
})
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn deserialize_sphincs_plus_c_signing_key(
bytes: &[u8],
) -> Result<crate::sphincs_plus_c::Key, WasmErr> {
crate::sphincs_plus_c::Key::from_bytes(bytes).ok_or_else(|| WasmErr {
code: ErrorCode::BadLength.as_str(),
message: format!("SPHINCS+C secretKey must be 128 bytes, got {}", bytes.len()),
})
}
#[cfg(feature = "wasm-bindings")]
#[derive(Debug)]
#[wasm_bindgen]
pub struct WasmSphincsPlusCKeys {
signing_key: crate::sphincs_plus_c::Key,
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
impl WasmSphincsPlusCKeys {
#[wasm_bindgen(getter, js_name = secretKey)]
pub fn secret_key(&self) -> alloc::vec::Vec<u8> {
self.signing_key.to_bytes().to_vec()
}
#[wasm_bindgen(getter, js_name = publicKey)]
pub fn public_key(&self) -> alloc::vec::Vec<u8> {
let mut out = alloc::vec::Vec::with_capacity(64);
out.extend_from_slice(self.signing_key.public_key.pk_seed.as_bytes());
out.extend_from_slice(self.signing_key.public_key.root.as_bytes());
out
}
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = sphincsPlusCKeygen)]
pub fn sphincs_plus_c_keygen(seed: &[u8]) -> Result<WasmSphincsPlusCKeys, JsValue> {
let mut seed = bytes_fixed::<32>(seed).map_err(js_error)?;
let signing_key = crate::sphincs_plus_c::keygen_from_master_seed(&seed);
seed.zeroize();
Ok(WasmSphincsPlusCKeys { signing_key })
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = sphincsPlusCSign)]
pub fn sphincs_plus_c_sign(
message: &[u8],
secret_key: &[u8],
) -> Result<alloc::vec::Vec<u8>, JsValue> {
let full_key = deserialize_sphincs_plus_c_signing_key(secret_key).map_err(js_error)?;
let hash = message_hash(message).map_err(js_error)?;
let signature = crate::sphincs_plus_c::sign(&full_key, &hash).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::SigningFailed.as_str(),
message: "stateless signing failed for the supplied key/message".into(),
})
})?;
Ok(signature.to_bytes())
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = sphincsPlusCVerify)]
pub fn sphincs_plus_c_verify(signature: &[u8], message: &[u8], public_key: &[u8]) -> bool {
let Ok(hash) = message_hash(message) else {
return false;
};
crate::sphincs_plus_c::verifier::SphincsPlusCVerifier::new()
.verify(public_key, &hash, signature)
== crate::verifier::VerifyOutcome::Valid
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn serialize_shrincs_signing_key(key: &Keys) -> alloc::vec::Vec<u8> {
key.to_bytes().to_vec()
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn deserialize_shrincs_signing_key(bytes: &[u8]) -> Result<Keys, WasmErr> {
Keys::from_bytes(bytes).ok_or_else(|| WasmErr {
code: ErrorCode::BadLength.as_str(),
message: format!("shrincs secretKey must be 264 bytes, got {}", bytes.len()),
})
}
#[cfg(any(test, feature = "wasm-bindings"))]
fn encode_public_key_flat(public_key: &PublicKey) -> alloc::vec::Vec<u8> {
let mut out = alloc::vec::Vec::with_capacity(STATEFUL_PUBLIC_KEY_BYTES + HASH_LEN * 3);
out.extend_from_slice(&public_key.stateful_public_key);
out.extend_from_slice(&public_key.public_key_commitment);
out.extend_from_slice(&public_key.pk_seed);
out.extend_from_slice(&public_key.hypertree_root);
out
}
#[cfg(feature = "wasm-bindings")]
#[derive(Debug)]
#[wasm_bindgen]
pub struct WasmShrincsKeys {
signing_key: Keys,
public_key: PublicKey,
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
impl WasmShrincsKeys {
#[wasm_bindgen(getter, js_name = secretKey)]
pub fn secret_key(&self) -> alloc::vec::Vec<u8> {
serialize_shrincs_signing_key(&self.signing_key)
}
#[wasm_bindgen(getter, js_name = publicKey)]
pub fn public_key(&self) -> alloc::vec::Vec<u8> {
encode_public_key_flat(&self.public_key)
}
#[wasm_bindgen(getter, js_name = publicKeyCommitment)]
pub fn public_key_commitment(&self) -> alloc::vec::Vec<u8> {
self.public_key.public_key_commitment.clone()
}
#[wasm_bindgen(getter, js_name = statelessPublicKey)]
pub fn stateless_public_key(&self) -> alloc::vec::Vec<u8> {
let mut out = alloc::vec::Vec::with_capacity(64);
out.extend_from_slice(&self.public_key.pk_seed);
out.extend_from_slice(&self.public_key.hypertree_root);
out
}
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsKeygen)]
pub fn shrincs_keygen(seed: &[u8], max_signatures: u32) -> Result<WasmShrincsKeys, JsValue> {
let mut seed = bytes_fixed::<32>(seed).map_err(js_error)?;
if max_signatures == 0 || max_signatures > MAX_STATEFUL_SIGNATURES_LIMIT as u32 {
return Err(js_error(WasmErr {
code: ErrorCode::InvalidInput.as_str(),
message: format!("maxSignatures must be in 1..={MAX_STATEFUL_SIGNATURES_LIMIT}"),
}));
}
let result = ShrincsSigner::keygen(&seed, max_signatures);
seed.zeroize();
let (signing_key, public_key) = result.ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::KeygenFailed.as_str(),
message: "key generation failed for the supplied inputs".into(),
})
})?;
Ok(WasmShrincsKeys {
signing_key,
public_key,
})
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsImportSigningKey)]
pub fn shrincs_import_signing_key(secret_key: &[u8]) -> Result<WasmShrincsKeys, JsValue> {
let candidate = deserialize_shrincs_signing_key(secret_key).map_err(js_error)?;
let (signing_key, public_key) =
ShrincsSigner::import_signing_key(candidate).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::ImportInvalid.as_str(),
message: "secretKey failed validation: counter out of range or roots do not \
match the seeds"
.into(),
})
})?;
Ok(WasmShrincsKeys {
signing_key,
public_key,
})
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsSign)]
pub fn shrincs_sign(message: &[u8], secret_key: &mut [u8]) -> Result<alloc::vec::Vec<u8>, JsValue> {
let candidate = deserialize_shrincs_signing_key(secret_key).map_err(js_error)?;
let (mut signing_key, public_key) =
ShrincsSigner::import_signing_key(candidate).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::ImportInvalid.as_str(),
message: "secretKey failed validation: counter out of range or roots do not \
match the seeds"
.into(),
})
})?;
if signing_key.stateful().next_leaf_index() > signing_key.stateful().public_key().max_signatures
{
return Err(js_error(WasmErr {
code: ErrorCode::StatefulLeavesExhausted.as_str(),
message: "no unused stateful leaf available for this key".into(),
}));
}
let hash = message_hash(message).map_err(js_error)?;
let signature = ShrincsSigner::sign_stateful_raw(&mut signing_key, &hash).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::SigningFailed.as_str(),
message: "stateful signing failed for the supplied key/message".into(),
})
})?;
secret_key.copy_from_slice(&serialize_shrincs_signing_key(&signing_key));
Ok(encode_stateful_envelope(&public_key, &signature))
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsSignStateless)]
pub fn shrincs_sign_stateless(
message: &[u8],
secret_key: &[u8],
) -> Result<alloc::vec::Vec<u8>, JsValue> {
let candidate = deserialize_shrincs_signing_key(secret_key).map_err(js_error)?;
let (signing_key, _public_key) =
ShrincsSigner::import_signing_key(candidate).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::ImportInvalid.as_str(),
message: "secretKey failed validation: counter out of range or roots do not \
match the seeds"
.into(),
})
})?;
let hash = message_hash(message).map_err(js_error)?;
let signature = ShrincsSigner::sign_stateless_raw(&signing_key, &hash).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::SigningFailed.as_str(),
message: "stateless signing failed for the supplied key/message".into(),
})
})?;
Ok(signature.to_bytes())
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsVerify)]
pub fn shrincs_verify(signature: &[u8], message: &[u8], public_key_commitment: &[u8]) -> bool {
let Ok(hash) = message_hash(message) else {
return false;
};
ShrincsVerifier::new().verify(public_key_commitment, &hash, signature)
== crate::verifier::VerifyOutcome::Valid
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsVerifyStateless)]
pub fn shrincs_verify_stateless(
signature: &[u8],
message: &[u8],
stateless_public_key: &[u8],
) -> bool {
sphincs_plus_c_verify(signature, message, stateless_public_key)
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsReset)]
pub fn shrincs_reset(secret_key: &mut [u8], new_seed: &[u8]) -> Result<(), JsValue> {
let new_seed = bytes_word32(new_seed).map_err(|_| {
js_error(WasmErr {
code: ErrorCode::BadLength.as_str(),
message: format!("newSeed must be exactly 32 bytes, got {}", new_seed.len()),
})
})?;
let candidate = deserialize_shrincs_signing_key(secret_key).map_err(js_error)?;
let (mut keys, _public_key) =
ShrincsSigner::import_signing_key(candidate).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::ImportInvalid.as_str(),
message: "secretKey failed validation: counter out of range or roots do not \
match the seeds"
.into(),
})
})?;
keys.reset(&new_seed);
secret_key.copy_from_slice(&serialize_shrincs_signing_key(&keys));
Ok(())
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsComputePublicKeyCommitment)]
pub fn shrincs_compute_public_key_commitment(
secret_key: &[u8],
) -> Result<alloc::vec::Vec<u8>, JsValue> {
let candidate = deserialize_shrincs_signing_key(secret_key).map_err(js_error)?;
let (keys, _public_key) = ShrincsSigner::import_signing_key(candidate).ok_or_else(|| {
js_error(WasmErr {
code: ErrorCode::ImportInvalid.as_str(),
message: "secretKey failed validation: counter out of range or roots do not \
match the seeds"
.into(),
})
})?;
Ok(keys.recompute_commitment().as_bytes().to_vec())
}
#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen(js_name = shrincsRecoverPublicKeyCommitment)]
pub fn shrincs_recover_public_key_commitment(
signature: &[u8],
) -> Result<alloc::vec::Vec<u8>, JsValue> {
Keys::recover_commitment(signature)
.map(|commitment| commitment.as_bytes().to_vec())
.ok_or_else(malformed_envelope)
}
#[cfg(feature = "wasm-bindings")]
fn malformed_envelope() -> JsValue {
js_error(WasmErr {
code: ErrorCode::EnvelopeMalformed.as_str(),
message: "signature envelope could not be decoded".into(),
})
}
#[cfg(feature = "wasm-bindings")]
fn js_error(err: WasmErr) -> JsValue {
let e = js_sys::Error::new(&err.message);
if js_sys::Reflect::set(&e, &JsValue::from_str("code"), &JsValue::from_str(err.code)).is_err() {
return js_sys::Error::new(&format!("[{}] {}", err.code, err.message)).into();
}
e.into()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
use crate::shrincs::test_fixtures::{
fixture_entry_opt, fixture_pair, load_fixture_file, stateful_signer_fixture_path,
TestKeyMode,
};
use crate::shrincs::{Keys, PublicKey as SignerPublicKey, ShrincsSigner};
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
use wasm_bindgen_test::wasm_bindgen_test;
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
fn expect_err<T>(result: Result<T, JsValue>) -> JsValue {
match result {
Ok(_) => panic!("expected an Err"),
Err(err) => err,
}
}
fn signing_key_and_public_key() -> (Keys, SignerPublicKey) {
ShrincsSigner::keygen(b"wasm verifier test seed", 4).unwrap()
}
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
use crate::test_support::stateful_only_key;
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
fn stateful_signing_key_and_public_key() -> (Keys, SignerPublicKey) {
match TestKeyMode::from_env() {
TestKeyMode::Fresh => stateful_only_key(b"wasm verifier test seed", 4),
TestKeyMode::Fixture => {
let path = stateful_signer_fixture_path();
if path.is_file() {
let fixture_file = load_fixture_file(&path);
assert_eq!(
fixture_file.profile_name,
crate::shrincs::PROFILE_NAME,
"stateful signer fixture profile mismatch",
);
if let Some(entry) = fixture_entry_opt(&fixture_file, "stateful signer seed") {
return fixture_pair(entry);
}
}
stateful_only_key(b"wasm verifier test seed", 4)
}
}
}
#[test]
fn bytes_fixed_rejects_wrong_length_without_echoing_the_value() {
let err = bytes_fixed::<32>(&[0x42u8; 31]).unwrap_err();
assert_eq!(err.code, ErrorCode::BadLength.as_str());
assert!(!err.message.contains("42"));
assert!(err.message.contains("31"));
assert!(bytes_fixed::<32>(&[0x42u8; 32]).is_ok());
assert!(bytes_word32(&[0u8; 32]).is_ok());
assert!(bytes_word32(&[0u8; 33]).is_err());
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn sphincs_plus_c_noble_keygen_sign_verify_round_trips_and_rejects_tamper() {
let seed = [0x11u8; 32];
let keys = sphincs_plus_c_keygen(&seed).unwrap();
let secret_key = keys.secret_key();
let public_key = keys.public_key();
assert_eq!(secret_key.len(), 128);
assert_eq!(public_key.len(), 64);
let message = [0x01u8; 32].to_vec();
let signature = sphincs_plus_c_sign(&message, &secret_key).unwrap();
assert!(sphincs_plus_c_verify(&signature, &message, &public_key));
assert!(!sphincs_plus_c_verify(
&signature,
&[0xEEu8; 32],
&public_key
));
let mut tampered = signature.clone();
tampered[0] ^= 1;
assert!(!sphincs_plus_c_verify(&tampered, &message, &public_key));
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn sphincs_plus_c_noble_keygen_is_deterministic_for_the_same_seed() {
let seed = [0x22u8; 32];
let a = sphincs_plus_c_keygen(&seed).unwrap();
let b = sphincs_plus_c_keygen(&seed).unwrap();
assert_eq!(a.secret_key(), b.secret_key());
assert_eq!(a.public_key(), b.public_key());
}
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
#[wasm_bindgen_test]
fn sphincs_plus_c_noble_keygen_rejects_wrong_length_seed() {
let err = expect_err(sphincs_plus_c_keygen(&[0u8; 31]));
assert_eq!(
js_sys::Reflect::get(&err, &JsValue::from_str("code"))
.unwrap()
.as_string()
.unwrap(),
ErrorCode::BadLength.as_str(),
);
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_keygen_sign_verify_round_trips_and_rejects_tamper() {
let seed = [0x33u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let mut secret_key = keys.secret_key();
let public_key = keys.public_key();
let public_key_commitment = keys.public_key_commitment();
assert_eq!(secret_key.len(), 264);
assert_eq!(public_key.len(), 164);
let message = [0x03u8; 32].to_vec();
let signature = shrincs_sign(&message, &mut secret_key).unwrap();
assert!(shrincs_verify(&signature, &message, &public_key_commitment));
assert!(!shrincs_verify(
&signature,
&[0xEEu8; 32],
&public_key_commitment
));
let mut tampered = signature.clone();
tampered[0] ^= 1;
assert!(!shrincs_verify(&tampered, &message, &public_key_commitment));
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_verify_rejects_wrong_public_key_commitment() {
let seed = [0x55u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let mut secret_key = keys.secret_key();
let real_commitment = keys.public_key_commitment();
let message = [0x05u8; 32].to_vec();
let signature = shrincs_sign(&message, &mut secret_key).unwrap();
assert!(shrincs_verify(&signature, &message, &real_commitment));
let mut wrong_commitment = real_commitment.clone();
wrong_commitment[0] ^= 1;
assert!(!shrincs_verify(&signature, &message, &wrong_commitment));
assert!(!shrincs_verify(&signature, &message, &[0xFFu8; 32]));
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_sign_advances_secret_key_in_place_across_two_signatures() {
let seed = [0x44u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let mut secret_key = keys.secret_key();
let public_key_commitment = keys.public_key_commitment();
let before = secret_key.clone();
let message = [0x04u8; 32].to_vec();
let first = shrincs_sign(&message, &mut secret_key).unwrap();
assert_ne!(
secret_key, before,
"secretKey must mutate in place after sign"
);
assert!(shrincs_verify(&first, &message, &public_key_commitment));
let after_first = secret_key.clone();
let second = shrincs_sign(&message, &mut secret_key).unwrap();
assert_ne!(
secret_key, after_first,
"secretKey must advance again on the next sign"
);
assert_ne!(first, second, "two leaves must yield distinct signatures");
assert!(shrincs_verify(&second, &message, &public_key_commitment));
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_reset_changes_commitment_keeps_stateless_and_still_signs() {
let seed = [0x88u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let mut secret_key = keys.secret_key();
let stateless_public_key = keys.stateless_public_key();
let original_commitment = keys.public_key_commitment();
shrincs_reset(&mut secret_key, &[0x99u8; 32]).unwrap();
let reset_commitment = shrincs_compute_public_key_commitment(&secret_key).unwrap();
assert_ne!(reset_commitment, original_commitment);
let reimported = shrincs_import_signing_key(&secret_key).unwrap();
assert_eq!(reimported.public_key_commitment(), reset_commitment);
assert_eq!(
reimported.stateless_public_key(),
stateless_public_key,
"reset must not touch the stateless half"
);
let message = [0x09u8; 32].to_vec();
let signature = shrincs_sign(&message, &mut secret_key).unwrap();
assert!(shrincs_verify(&signature, &message, &reset_commitment));
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_compute_public_key_commitment_matches_keygen() {
let seed = [0x99u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let secret_key = keys.secret_key();
let commitment = shrincs_compute_public_key_commitment(&secret_key).unwrap();
assert_eq!(commitment, keys.public_key_commitment());
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_recover_public_key_commitment_matches_signer() {
let seed = [0xaau8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let mut secret_key = keys.secret_key();
let expected_commitment = keys.public_key_commitment();
let message = [0x0au8; 32].to_vec();
let signature = shrincs_sign(&message, &mut secret_key).unwrap();
let recovered = shrincs_recover_public_key_commitment(&signature).unwrap();
assert_eq!(recovered, expected_commitment);
}
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
#[wasm_bindgen_test]
fn shrincs_noble_recover_public_key_commitment_rejects_garbage_envelope() {
let err = expect_err(shrincs_recover_public_key_commitment(&[0u8; 4]));
assert_eq!(
js_sys::Reflect::get(&err, &JsValue::from_str("code"))
.unwrap()
.as_string()
.unwrap(),
ErrorCode::EnvelopeMalformed.as_str(),
);
}
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
#[wasm_bindgen_test]
fn shrincs_noble_sign_throws_when_stateful_leaves_are_exhausted() {
let seed = [0x55u8; 32];
let keys = shrincs_keygen(&seed, 1).unwrap();
let mut secret_key = keys.secret_key();
let message = [0x06u8; 32].to_vec();
shrincs_sign(&message, &mut secret_key).unwrap(); let err = expect_err(shrincs_sign(&message, &mut secret_key));
assert_eq!(
js_sys::Reflect::get(&err, &JsValue::from_str("code"))
.unwrap()
.as_string()
.unwrap(),
ErrorCode::StatefulLeavesExhausted.as_str(),
);
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_sign_stateless_never_mutates_and_verifies() {
let seed = [0x66u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let secret_key = keys.secret_key();
let stateless_public_key = keys.stateless_public_key();
let before = secret_key.clone();
let message = [0x05u8; 32].to_vec();
let signature = shrincs_sign_stateless(&message, &secret_key).unwrap();
assert_eq!(
secret_key, before,
"stateless sign must not mutate secretKey"
);
assert!(shrincs_verify_stateless(
&signature,
&message,
&stateless_public_key
));
assert!(!shrincs_verify_stateless(
&signature,
&[0xEEu8; 32],
&stateless_public_key,
));
assert!(sphincs_plus_c_verify(
&signature,
&message,
&stateless_public_key
));
}
#[cfg(feature = "wasm-bindings")]
#[test]
fn shrincs_noble_import_signing_key_round_trips() {
let seed = [0x77u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let secret_key = keys.secret_key();
let imported = shrincs_import_signing_key(&secret_key).unwrap();
assert_eq!(imported.secret_key(), secret_key);
assert_eq!(
imported.public_key_commitment(),
keys.public_key_commitment()
);
}
#[cfg(all(feature = "wasm-bindings", target_arch = "wasm32"))]
#[wasm_bindgen_test]
fn shrincs_noble_import_signing_key_rejects_tampered_roots_and_bad_length() {
let seed = [0x77u8; 32];
let keys = shrincs_keygen(&seed, 4).unwrap();
let secret_key = keys.secret_key();
let mut tampered = secret_key.clone();
tampered[0] ^= 1; let err = expect_err(shrincs_import_signing_key(&tampered));
assert_eq!(
js_sys::Reflect::get(&err, &JsValue::from_str("code"))
.unwrap()
.as_string()
.unwrap(),
ErrorCode::ImportInvalid.as_str(),
);
let err = expect_err(shrincs_import_signing_key(&secret_key[..263]));
assert_eq!(
js_sys::Reflect::get(&err, &JsValue::from_str("code"))
.unwrap()
.as_string()
.unwrap(),
ErrorCode::BadLength.as_str(),
);
}
#[test]
fn shrincs_signing_key_flat_serialization_round_trips() {
let (key, _) = signing_key_and_public_key();
let bytes = serialize_shrincs_signing_key(&key);
assert_eq!(bytes.len(), 264);
let parsed = deserialize_shrincs_signing_key(&bytes).unwrap();
assert_eq!(parsed, key);
let err = deserialize_shrincs_signing_key(&bytes[..263]).unwrap_err();
assert_eq!(err.code, ErrorCode::BadLength.as_str());
}
#[test]
fn sphincs_plus_c_signing_key_flat_serialization_round_trips() {
let (key, _) = signing_key_and_public_key();
let spk = key.stateless().clone();
let bytes = spk.to_bytes();
assert_eq!(bytes.len(), 128);
let parsed = deserialize_sphincs_plus_c_signing_key(&bytes).unwrap();
assert_eq!(parsed, spk);
let err = deserialize_sphincs_plus_c_signing_key(&bytes[..127]).unwrap_err();
assert_eq!(err.code, ErrorCode::BadLength.as_str());
}
#[test]
fn typescript_union_lists_every_error_code() {
for code in crate::ErrorCode::ALL {
let quoted = alloc::format!("\"{}\"", code.as_str());
assert!(
TS_ERROR_CODES.contains("ed),
"the ShrincsErrorCode TypeScript union is missing {quoted}"
);
}
}
#[test]
fn every_typescript_union_member_maps_to_an_error_code() {
let known: alloc::vec::Vec<_> = crate::ErrorCode::ALL
.iter()
.map(|code| code.as_str())
.collect();
let members: alloc::vec::Vec<&str> = TS_ERROR_CODES.split('"').skip(1).step_by(2).collect();
assert!(
!members.is_empty(),
"parsed no members out of the union; the literal's shape changed"
);
for member in &members {
assert!(
member.starts_with("ERR_"),
"parsed {member:?} out of the union, which is not an error code. \
TS_ERROR_CODES most likely gained a quoted string outside the \
union members, which this parse cannot distinguish from one."
);
}
for member in &members {
assert!(
known.contains(member),
"the TypeScript union declares {member}, which no ErrorCode variant produces"
);
}
assert_eq!(
members.len(),
known.len(),
"union has {} members, ErrorCode has {} variants",
members.len(),
known.len()
);
}
}