use alloc::vec::Vec;
use super::action_context::ActionContext;
use super::key::{encode_stateful_public_key, Commitment, PublicKey};
use super::signature::Signature;
use crate::hash::{derive32, word32};
use crate::shrincs::uxmss;
use crate::sphincs_plus_c::Signature as StatelessSignature;
use crate::sphincs_plus_c::{self};
use crate::HASH_LEN;
use super::dispatch::stateful_action_message_hash;
use super::key::Keys;
pub type ShrincsSignerResult<T> = Option<T>;
pub(crate) fn public_key_from_components(
stateful_public_key: Vec<u8>,
pk_seed: [u8; HASH_LEN],
hypertree_root: [u8; HASH_LEN],
) -> PublicKey {
let commitment = Commitment::of(&stateful_public_key, &pk_seed, &hypertree_root);
PublicKey {
stateful_public_key,
public_key_commitment: commitment.as_bytes().to_vec(),
pk_seed: pk_seed.to_vec(),
hypertree_root: hypertree_root.to_vec(),
}
}
fn public_key_of(keys: &Keys) -> PublicKey {
public_key_from_components(
encode_stateful_public_key(
*keys.stateful().public_key().pk_seed.as_bytes(),
*keys.stateful().public_key().root.as_bytes(),
keys.stateful().public_key().max_signatures,
),
*keys.stateless().public_key.pk_seed.as_bytes(),
*keys.stateless().public_key.root.as_bytes(),
)
}
pub fn sign(keys: &mut Keys, hash: &[u8; HASH_LEN]) -> Option<Vec<u8>> {
let public_key = public_key_of(keys);
let signature = ShrincsSigner::sign_stateful_raw(keys, hash)?;
Some(super::signature::encode_stateful_envelope(
&public_key,
&signature,
))
}
#[cfg(test)]
use crate::shrincs::ShrincsVerifier;
#[derive(Debug, Clone, Copy)]
pub struct ShrincsSigner;
pub(crate) use super::uxmss::{INITIAL_STATEFUL_LEAF_INDEX, MAX_STATEFUL_SIGNATURES_LIMIT};
use crate::trace_macros::stateless_trace_enabled;
impl ShrincsSigner {
pub fn keygen(
seed_material: &[u8],
max_stateful_signatures: u32,
) -> ShrincsSignerResult<(Keys, PublicKey)> {
if max_stateful_signatures == 0 {
return None;
}
if max_stateful_signatures > MAX_STATEFUL_SIGNATURES_LIMIT {
return None;
}
let stateless = sphincs_plus_c::keygen_from_master_seed(seed_material);
Some(Self::build_keys(
seed_material,
max_stateful_signatures,
stateless,
))
}
pub(crate) fn build_keys(
seed: &[u8],
max: u32,
stateless: sphincs_plus_c::Key,
) -> (Keys, PublicKey) {
let stateful_sk_seed = derive32(b"shrincs-stateful-sk-seed", seed, &[]);
let stateful_prf_seed = derive32(b"shrincs-stateful-prf-seed", seed, &[]);
let stateful_pk_seed = derive32(b"shrincs-stateful-pk-seed", seed, &[]);
let stateful_root = uxmss::stateful_subtree_root(
&stateful_sk_seed,
&stateful_pk_seed,
INITIAL_STATEFUL_LEAF_INDEX,
max,
);
let stateful = uxmss::Key::new(
uxmss::PrivateKey::new(
uxmss::SkSeed::new(stateful_sk_seed),
uxmss::PrfSeed::new(stateful_prf_seed),
),
uxmss::StructuredPublicKey {
pk_seed: uxmss::PkSeed::new(stateful_pk_seed),
root: uxmss::Root::new(stateful_root),
max_signatures: max,
},
INITIAL_STATEFUL_LEAF_INDEX,
);
let hypertree_root = *stateless.public_key.root.as_bytes();
let stateless_pk_seed = *stateless.public_key.pk_seed.as_bytes();
let signing_key = Keys::new(stateless, stateful);
let public_key = public_key_from_components(
encode_stateful_public_key(stateful_pk_seed, stateful_root, max),
stateless_pk_seed,
hypertree_root,
);
(signing_key, public_key)
}
pub fn import_signing_key(candidate: Keys) -> ShrincsSignerResult<(Keys, PublicKey)> {
let validated = Keys::import(&candidate.to_bytes())?;
let public_key = public_key_from_components(
encode_stateful_public_key(
*validated.stateful().public_key().pk_seed.as_bytes(),
*validated.stateful().public_key().root.as_bytes(),
validated.stateful().public_key().max_signatures,
),
*validated.stateless().public_key.pk_seed.as_bytes(),
*validated.stateless().public_key.root.as_bytes(),
);
Some((validated, public_key))
}
pub fn sign_stateful_action(
signing_key: &mut Keys,
public_key: &PublicKey,
context: &ActionContext,
) -> ShrincsSignerResult<Signature> {
let expected = word32(&public_key.public_key_commitment)?;
let message = stateful_action_message_hash(expected, context);
uxmss::sign_stateful_raw(signing_key.stateful_mut(), &message)
}
pub fn sign_stateful_raw(
signing_key: &mut Keys,
message: &[u8],
) -> ShrincsSignerResult<Signature> {
uxmss::sign_stateful_raw(signing_key.stateful_mut(), message)
}
#[cfg(test)]
pub(crate) fn sign_stateful_raw_at_leaf(
signing_key: &Keys,
leaf_index: u32,
message: &[u8],
) -> ShrincsSignerResult<Signature> {
uxmss::sign_stateful_raw_at_leaf(signing_key.stateful(), leaf_index, message)
}
pub fn sign_stateless_raw(
signing_key: &Keys,
message: &[u8],
) -> ShrincsSignerResult<StatelessSignature> {
if stateless_trace_enabled() {
hashsigs_println!(
"stateless trace: signer start message_len={}",
message.len()
);
}
let sig = sphincs_plus_c::sign(signing_key.stateless(), message)?;
if stateless_trace_enabled() {
hashsigs_println!("stateless trace: signer done");
}
Some(sig)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::hash_packed;
use crate::shrincs::test_fixtures::{
fixture_entry_opt, fixture_pair, fixture_path, load_fixture_file,
stateful_signer_fixture_path, TestKeyMode,
};
use crate::HASH_LEN;
#[cfg(not(target_arch = "wasm32"))]
use proptest::prelude::*;
use crate::test_support::stateful_only_key;
fn action_context() -> ActionContext {
ActionContext {
domain_separator: [7u8; HASH_LEN],
nonce: [1u8; HASH_LEN],
key_version: [2u8; HASH_LEN],
action_type: [3u8; HASH_LEN],
payload_hash: [4u8; HASH_LEN],
}
}
fn expected_key(public_key: &PublicKey) -> [u8; HASH_LEN] {
word32(&public_key.public_key_commitment).unwrap()
}
fn fixture_or_fresh_full_key(
seed_label: &'static str,
max_stateful_signatures: u32,
) -> (Keys, PublicKey) {
match TestKeyMode::from_env() {
TestKeyMode::Fresh => {
ShrincsSigner::keygen(seed_label.as_bytes(), max_stateful_signatures)
.unwrap_or_else(|| panic!("fresh keygen failed for seed label {seed_label:?}"))
}
TestKeyMode::Fixture => {
let path = fixture_path();
if path.is_file() {
let fixture_file = load_fixture_file(&path);
assert_eq!(
fixture_file.profile_name,
crate::profiles::PROFILE_NAME,
"fixture profile mismatch",
);
if let Some(entry) = fixture_entry_opt(&fixture_file, seed_label) {
return fixture_pair(entry);
}
}
ShrincsSigner::keygen(seed_label.as_bytes(), max_stateful_signatures)
.unwrap_or_else(|| panic!("fresh keygen failed for seed label {seed_label:?}"))
}
}
}
fn fixture_or_stateful_only_key(
seed_label: &'static str,
max_stateful_signatures: u32,
) -> (Keys, PublicKey) {
match TestKeyMode::from_env() {
TestKeyMode::Fresh => stateful_only_key(seed_label.as_bytes(), max_stateful_signatures),
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::profiles::PROFILE_NAME,
"fixture profile mismatch",
);
if let Some(entry) = fixture_entry_opt(&fixture_file, seed_label) {
return fixture_pair(entry);
}
}
stateful_only_key(seed_label.as_bytes(), max_stateful_signatures)
}
}
}
#[cfg(not(any(feature = "profile-128s-q18", feature = "profile-128s-q20")))]
#[test]
fn signer_constants_match_verifier_constants() {
use crate::profiles::{
HYPERTREE_HEIGHT, NUM_HYPERTREE_LAYERS, NUM_WOTS_CHAINS, WOTS_CHAIN_LEN,
};
assert_eq!(HASH_LEN, 32);
assert_eq!(HYPERTREE_HEIGHT, 64);
assert_eq!(NUM_HYPERTREE_LAYERS, 8);
assert_eq!(HYPERTREE_HEIGHT / NUM_HYPERTREE_LAYERS, 8);
assert_eq!(NUM_WOTS_CHAINS, 64);
assert_eq!(WOTS_CHAIN_LEN, 16);
}
#[cfg(any(feature = "profile-128s-q18", feature = "profile-128s-q20"))]
#[test]
fn stateful_round_trip_verifies_under_128s_truncation() {
use crate::profiles::HASH_TRUNC_LEN;
let seed = b"128s stateful truncation seed";
let max = 4u32;
let stateful_sk_seed = derive32(b"shrincs-stateful-sk-seed", seed, &[]);
let stateful_prf_seed = derive32(b"shrincs-stateful-prf-seed", seed, &[]);
let stateful_pk_seed = derive32(b"shrincs-stateful-pk-seed", seed, &[]);
let stateful_root = uxmss::stateful_subtree_root(
&stateful_sk_seed,
&stateful_pk_seed,
INITIAL_STATEFUL_LEAF_INDEX,
max,
);
let pk_seed = derive32(b"shrincs-pk-seed", seed, &[]);
let hypertree_root = derive32(b"placeholder-hypertree-root", seed, &[]);
let stateful = uxmss::Key::new(
uxmss::PrivateKey::new(
uxmss::SkSeed::new(stateful_sk_seed),
uxmss::PrfSeed::new(stateful_prf_seed),
),
uxmss::StructuredPublicKey {
pk_seed: uxmss::PkSeed::new(stateful_pk_seed),
root: uxmss::Root::new(stateful_root),
max_signatures: max,
},
INITIAL_STATEFUL_LEAF_INDEX,
);
let stateless = sphincs_plus_c::Key::new(
sphincs_plus_c::PrivateKey::new(
sphincs_plus_c::SkSeed::new(derive32(b"shrincs-stateless-sk-seed", seed, &[])),
sphincs_plus_c::PrfSeed::new(derive32(b"shrincs-stateless-prf-seed", seed, &[])),
),
sphincs_plus_c::PublicKey {
pk_seed: sphincs_plus_c::PkSeed::new(pk_seed),
root: sphincs_plus_c::Root::new(hypertree_root),
},
);
let signing_key = Keys::new(stateless, stateful);
let public_key = public_key_from_components(
encode_stateful_public_key(stateful_pk_seed, stateful_root, max),
pk_seed,
hypertree_root,
);
let expected = word32(&public_key.public_key_commitment).unwrap();
let message = hash_packed(&[b"128s stateful message"]);
let signature =
ShrincsSigner::sign_stateful_raw_at_leaf(&signing_key, 2, &message).unwrap();
assert_eq!(signature.auth_path.len(), 2);
assert!(ShrincsVerifier::new().verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&signature,
));
assert_eq!(
&signature.auth_path[1][HASH_TRUNC_LEN..],
&[0u8; HASH_LEN - HASH_TRUNC_LEN]
);
assert_ne!(
&signature.auth_path[1][..HASH_TRUNC_LEN],
&[0u8; HASH_TRUNC_LEN]
);
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s full keygen remains manual; stateful signer behavior is covered by stateful fixtures"
)]
#[test]
fn keygen_is_deterministic_for_same_seed_material() {
let (signing_key_a, public_key_a) =
fixture_or_fresh_full_key("deterministic keygen seed", 4);
let (signing_key_b, public_key_b) =
fixture_or_fresh_full_key("deterministic keygen seed", 4);
assert_eq!(signing_key_a, signing_key_b);
assert_eq!(public_key_a, public_key_b);
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s full keygen remains manual; stateful signer behavior is covered by stateful fixtures"
)]
#[test]
fn keygen_public_key_uses_single_stateless_seed_and_root() {
let (_, public_key) = fixture_or_fresh_full_key("deterministic keygen seed", 4);
assert_eq!(
public_key.stateful_public_key.len(),
uxmss::STATEFUL_PUBLIC_KEY_BYTES
);
assert_eq!(public_key.public_key_commitment.len(), HASH_LEN);
assert_eq!(public_key.pk_seed.len(), HASH_LEN);
assert_eq!(public_key.hypertree_root.len(), HASH_LEN);
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s full keygen remains manual; stateful signer behavior is covered by stateful fixtures"
)]
#[test]
fn keygen_starts_stateful_signer_at_leaf_one() {
let (signing_key, _) = fixture_or_fresh_full_key("deterministic keygen seed", 4);
assert_eq!(
signing_key.stateful().next_leaf_index(),
INITIAL_STATEFUL_LEAF_INDEX
);
}
#[test]
fn generated_stateful_signature_verifies() {
let (mut signing_key, public_key) = fixture_or_stateful_only_key("stateful signer seed", 4);
let expected = expected_key(&public_key);
let message = hash_packed(&[b"stateful test message"]);
let signature = ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).unwrap();
assert!(ShrincsVerifier::new().verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&signature,
));
}
#[test]
fn generated_stateful_action_signature_verifies() {
let (mut signing_key, public_key) = fixture_or_stateful_only_key("action signer seed", 4);
let context = action_context();
let expected = expected_key(&public_key);
let signature =
ShrincsSigner::sign_stateful_action(&mut signing_key, &public_key, &context).unwrap();
assert!(ShrincsVerifier::new().verify_stateful(
expected,
&public_key,
&context,
&signature,
));
}
#[test]
fn explicit_leaf_test_helper_verifies_for_requested_leaf() {
let (signing_key, public_key) =
fixture_or_stateful_only_key("explicit leaf helper seed", 4);
let expected = expected_key(&public_key);
let message = hash_packed(&[b"explicit leaf test message"]);
let signature =
ShrincsSigner::sign_stateful_raw_at_leaf(&signing_key, 2, &message).unwrap();
assert_eq!(signature.auth_path.len(), 2);
assert!(ShrincsVerifier::new().verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&signature,
));
}
#[cfg(not(any(feature = "profile-128s-q18", feature = "profile-128s-q20")))]
#[test]
fn stateless_sign_via_sphincs_plus_c_verifies_hybrid_and_independent() {
use crate::sphincs_plus_c::{self};
let (signing_key, public_key) =
fixture_or_fresh_full_key("sphincs-plus-c hybrid cross-check", 4);
let message = hash_packed(&[b"sphincs-plus-c-hybrid-cross"]);
let spk = signing_key.stateless().clone();
let sig = sphincs_plus_c::sign(&spk, &message).expect("independent sign");
let pk = spk.public_key;
assert!(sphincs_plus_c::verify(&pk, &message, &sig));
let expected = expected_key(&public_key);
assert!(ShrincsVerifier::new().verify_stateless_unsafe_raw(
expected,
&public_key,
&message,
&sig,
));
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s stateless keygen/signing is compute-infeasible in-process"
)]
#[test]
fn generated_stateless_raw_signature_verifies() {
let (signing_key, public_key) = ShrincsSigner::keygen(b"stateless signer seed", 2).unwrap();
let message = hash_packed(&[b"stateless test"]);
let signature = ShrincsSigner::sign_stateless_raw(&signing_key, &message).unwrap();
let expected = expected_key(&public_key);
assert!(ShrincsVerifier::new().verify_stateless_unsafe_raw(
expected,
&public_key,
&message,
&signature,
));
}
#[test]
fn keygen_rejects_empty_or_excessive_stateful_budget() {
assert!(ShrincsSigner::keygen(b"seed", 0).is_none());
assert!(ShrincsSigner::keygen(b"seed", MAX_STATEFUL_SIGNATURES_LIMIT + 1).is_none());
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s full keygen remains manual; covered by stateful fixtures"
)]
#[test]
fn sign_round_trips_and_advances_the_key() {
use crate::verifier::{VerifierInterface, VerifyOutcome};
let (mut keys, public_key) =
ShrincsSigner::keygen(b"signer iface shrincs seed", 4).expect("keygen");
let hash = hash_packed(&[b"signer-interface-round-trip"]);
let key = public_key.public_key_commitment.clone();
assert_eq!(
keys.stateful().next_leaf_index(),
INITIAL_STATEFUL_LEAF_INDEX
);
let sig1 = sign(&mut keys, &hash).expect("first sign");
assert_eq!(
ShrincsVerifier::new().verify(&key, &hash, &sig1),
VerifyOutcome::Valid
);
assert_eq!(
keys.stateful().next_leaf_index(),
INITIAL_STATEFUL_LEAF_INDEX + 1
);
let sig2 = sign(&mut keys, &hash).expect("second sign");
assert_ne!(sig1, sig2, "distinct leaves yield distinct signatures");
assert_eq!(
ShrincsVerifier::new().verify(&key, &hash, &sig2),
VerifyOutcome::Valid
);
}
#[test]
fn stateful_signing_advances_leaf_and_rejects_exhaustion() {
let (mut signing_key, public_key) =
fixture_or_stateful_only_key("stateful exhaustion seed", 1);
let expected = expected_key(&public_key);
let message = hash_packed(&[b"first and only stateful signature"]);
let signature = ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).unwrap();
assert_eq!(
signing_key.stateful().next_leaf_index(),
INITIAL_STATEFUL_LEAF_INDEX + 1
);
assert!(ShrincsVerifier::new().verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&signature,
));
assert!(ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).is_none());
}
#[test]
fn stateful_signature_rejects_wrong_message_and_tampered_chain() {
let (mut signing_key, public_key) =
fixture_or_stateful_only_key("stateful negative seed", 4);
let expected = expected_key(&public_key);
let message = hash_packed(&[b"stateful valid message"]);
let wrong_message = hash_packed(&[b"stateful wrong message"]);
let signature = ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).unwrap();
let verifier = ShrincsVerifier::new();
assert!(!verifier.verify_stateful_unsafe_raw(
expected,
&public_key,
&wrong_message,
&signature,
));
let mut tampered = signature.clone();
tampered.chains[0][0] ^= 1;
assert!(!verifier.verify_stateful_unsafe_raw(expected, &public_key, &message, &tampered,));
}
#[test]
fn stateful_action_rejects_tampered_context() {
let (mut signing_key, public_key) = fixture_or_stateful_only_key("action negative seed", 4);
let expected = expected_key(&public_key);
let context = action_context();
let signature =
ShrincsSigner::sign_stateful_action(&mut signing_key, &public_key, &context).unwrap();
let mut tampered_context = context;
tampered_context.nonce[31] ^= 1;
assert!(!ShrincsVerifier::new().verify_stateful(
expected,
&public_key,
&tampered_context,
&signature,
));
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s stateless keygen/signing is compute-infeasible in-process"
)]
#[test]
fn stateless_signature_rejects_wrong_message_and_tampered_hypertree_path() {
let (signing_key, public_key) = fixture_or_fresh_full_key("stateless negative seed", 2);
let message = hash_packed(&[b"stateless valid message"]);
let wrong_message = hash_packed(&[b"stateless wrong message"]);
let signature = ShrincsSigner::sign_stateless_raw(&signing_key, &message).unwrap();
let expected = expected_key(&public_key);
let verifier = ShrincsVerifier::new();
assert!(!verifier.verify_stateless_unsafe_raw(
expected,
&public_key,
&wrong_message,
&signature,
));
let mut tampered = signature.clone();
tampered.hypertree[0].auth_path[0][0] ^= 1;
assert!(!verifier.verify_stateless_unsafe_raw(expected, &public_key, &message, &tampered,));
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s stateless keygen/signing is compute-infeasible in-process"
)]
#[test]
fn stateless_signature_rejects_malformed_lengths() {
let (signing_key, public_key) = fixture_or_fresh_full_key("stateless malformed seed", 2);
let message = hash_packed(&[b"stateless malformed message"]);
let signature = ShrincsSigner::sign_stateless_raw(&signing_key, &message).unwrap();
let expected = expected_key(&public_key);
let verifier = ShrincsVerifier::new();
let mut missing_layer = signature.clone();
missing_layer.hypertree.pop();
assert!(!verifier.verify_stateless_unsafe_raw(
expected,
&public_key,
&message,
&missing_layer,
));
let mut missing_chain = signature;
missing_chain.hypertree[0].wots_c_signature.chains.pop();
assert!(!verifier.verify_stateless_unsafe_raw(
expected,
&public_key,
&message,
&missing_chain,
));
}
#[test]
fn public_key_commitment_rejects_tampered_component() {
let (mut signing_key, mut public_key) =
fixture_or_stateful_only_key("public key negative seed", 4);
let expected = expected_key(&public_key);
let message = hash_packed(&[b"public key commitment message"]);
let signature = ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).unwrap();
public_key.stateful_public_key[0] ^= 1;
assert!(!ShrincsVerifier::new().verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&signature,
));
}
#[test]
fn import_round_trips_a_keygen_key() {
let (key, pk) = ShrincsSigner::keygen(b"import round trip seed", 4).unwrap();
let (imported_key, imported_pk) = ShrincsSigner::import_signing_key(key).unwrap();
let (key_again, _) = ShrincsSigner::keygen(b"import round trip seed", 4).unwrap();
assert_eq!(imported_key, key_again);
assert_eq!(imported_pk, pk);
}
fn with_next_leaf(key: &Keys, next_leaf_index: u32) -> Keys {
Keys::new(
key.stateless().clone(),
uxmss::Key::new(
key.stateful().secret().clone(),
*key.stateful().public_key(),
next_leaf_index,
),
)
}
#[test]
fn import_accepts_advanced_and_exhausted_counters() {
let (key, _) = ShrincsSigner::keygen(b"import counter seed", 4).unwrap();
let key = with_next_leaf(&key, 3);
let (imported, _) = ShrincsSigner::import_signing_key(key).unwrap();
assert_eq!(imported.stateful().next_leaf_index(), 3);
let (key, _) = ShrincsSigner::keygen(b"import counter seed", 4).unwrap();
let key = with_next_leaf(&key, 5); let (imported, _) = ShrincsSigner::import_signing_key(key).unwrap();
assert!(ShrincsSigner::sign_stateful_raw(&mut { imported }, b"no leaves left").is_none());
}
#[test]
fn import_rejects_out_of_range_counters_and_budgets() {
let (key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
let key = with_next_leaf(&key, 0);
assert!(ShrincsSigner::import_signing_key(key).is_none());
let (key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
let key = with_next_leaf(&key, 6); assert!(ShrincsSigner::import_signing_key(key).is_none());
let (key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
let mut public_key = *key.stateful().public_key();
public_key.max_signatures = 0;
let key = Keys::new(
key.stateless().clone(),
uxmss::Key::new(
key.stateful().secret().clone(),
public_key,
key.stateful().next_leaf_index(),
),
);
assert!(ShrincsSigner::import_signing_key(key).is_none());
let (key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
let mut public_key = *key.stateful().public_key();
public_key.max_signatures = 4097; let key = Keys::new(
key.stateless().clone(),
uxmss::Key::new(
key.stateful().secret().clone(),
public_key,
key.stateful().next_leaf_index(),
),
);
assert!(ShrincsSigner::import_signing_key(key).is_none());
}
#[test]
fn import_rejects_tampered_roots() {
let (key, _) = ShrincsSigner::keygen(b"import tamper seed", 4).unwrap();
let mut stateful_root = *key.stateful().public_key().root.as_bytes();
stateful_root[0] ^= 0x01;
let mut public_key = *key.stateful().public_key();
public_key.root = uxmss::Root::new(stateful_root);
let key = Keys::new(
key.stateless().clone(),
uxmss::Key::new(
key.stateful().secret().clone(),
public_key,
key.stateful().next_leaf_index(),
),
);
assert!(ShrincsSigner::import_signing_key(key).is_none());
let (key, _) = ShrincsSigner::keygen(b"import tamper seed", 4).unwrap();
let mut hypertree_root = *key.stateless().public_key.root.as_bytes();
hypertree_root[0] ^= 0x01;
let mut stateless = key.stateless().clone();
stateless.public_key.root = sphincs_plus_c::Root::new(hypertree_root);
let key = Keys::new(stateless, key.stateful().clone());
assert!(ShrincsSigner::import_signing_key(key).is_none());
let (key_a, _) = ShrincsSigner::keygen(b"import splice seed A", 4).unwrap();
let (key_b, _) = ShrincsSigner::keygen(b"import splice seed B", 4).unwrap();
let mut public_key = *key_b.stateful().public_key();
public_key.root = key_a.stateful().public_key().root;
let key_b = Keys::new(
key_b.stateless().clone(),
uxmss::Key::new(
key_b.stateful().secret().clone(),
public_key,
key_b.stateful().next_leaf_index(),
),
);
assert!(ShrincsSigner::import_signing_key(key_b).is_none());
}
#[test]
fn imported_key_signs_and_verifies() {
let (key, _) = ShrincsSigner::keygen(b"import sign seed", 4).unwrap();
let key = with_next_leaf(&key, 2);
let (mut imported, pk) = ShrincsSigner::import_signing_key(key).unwrap();
let message = b"signed after import".to_vec();
let signature = ShrincsSigner::sign_stateful_raw(&mut imported, &message).unwrap();
assert_eq!(signature.auth_path.len(), 2);
let expected = word32(&pk.public_key_commitment).unwrap();
assert!(
ShrincsVerifier::new().verify_stateful_unsafe_raw(expected, &pk, &message, &signature)
);
}
#[test]
fn stateful_boundary_leaves_and_empty_message_round_trip() {
let budget = 4u32;
let (signing_key, public_key) = stateful_only_key(b"stateful boundary seed", budget);
let expected = expected_key(&public_key);
let verifier = ShrincsVerifier::new();
let message = hash_packed(&[b"stateful boundary message"]);
let leaf_one = ShrincsSigner::sign_stateful_raw_at_leaf(&signing_key, 1, &message).unwrap();
assert_eq!(leaf_one.auth_path.len(), 1);
assert!(verifier.verify_stateful_unsafe_raw(expected, &public_key, &message, &leaf_one));
let leaf_budget =
ShrincsSigner::sign_stateful_raw_at_leaf(&signing_key, budget, &message).unwrap();
assert_eq!(leaf_budget.auth_path.len(), budget as usize);
assert!(verifier.verify_stateful_unsafe_raw(expected, &public_key, &message, &leaf_budget));
let empty = ShrincsSigner::sign_stateful_raw_at_leaf(&signing_key, 1, &[]).unwrap();
assert!(verifier.verify_stateful_unsafe_raw(expected, &public_key, &[], &empty));
assert!(!verifier.verify_stateful_unsafe_raw(expected, &public_key, &[0u8], &empty));
}
#[cfg_attr(
any(feature = "profile-128s-q18", feature = "profile-128s-q20"),
ignore = "128s stateless keygen/signing is compute-infeasible in-process"
)]
#[test]
fn stateless_empty_message_round_trip_and_fors_boundary() {
use crate::profiles::NUM_FORS_TREES;
let (signing_key, public_key) =
fixture_or_fresh_full_key("stateless empty message seed", 2);
let expected = expected_key(&public_key);
let verifier = ShrincsVerifier::new();
let signature = ShrincsSigner::sign_stateless_raw(&signing_key, &[]).unwrap();
assert_eq!(signature.fors.entries.len(), NUM_FORS_TREES as usize - 1);
assert!(verifier.verify_stateless_unsafe_raw(expected, &public_key, &[], &signature));
assert!(!verifier.verify_stateless_unsafe_raw(expected, &public_key, &[0u8], &signature));
}
#[cfg(not(target_arch = "wasm32"))]
proptest! {
#![proptest_config(ProptestConfig::with_cases(24))]
#[test]
fn stateful_sign_verify_round_trip_and_single_byte_tamper_rejects(
message in proptest::collection::vec(any::<u8>(), 0..48usize),
leaf in 1u32..=4,
tamper_chain in 0usize..crate::wots_c::NUM_CHAINS,
tamper_byte in 0usize..HASH_LEN,
) {
let (signing_key, public_key) = stateful_only_key(b"proptest stateful seed", 4);
let expected = word32(&public_key.public_key_commitment).unwrap();
let verifier = ShrincsVerifier::new();
let signature =
ShrincsSigner::sign_stateful_raw_at_leaf(&signing_key, leaf, &message).unwrap();
prop_assert!(verifier.verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&signature
));
let mut tampered = signature;
tampered.chains[tamper_chain][tamper_byte] ^= 1;
prop_assert!(!verifier.verify_stateful_unsafe_raw(
expected,
&public_key,
&message,
&tampered
));
}
}
}