Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/shrincs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub mod verifier;
#[cfg(test)]
mod vector_conformance;

pub use signer::{ShrincsSigner, ShrincsSignerResult, ShrincsSigningKey};
pub use signer::{ShrincsSigner, ShrincsSignerError, ShrincsSignerResult, ShrincsSigningKey};
pub use verifier::{
ActionContext, ForsEntry, ForsSignature, HypertreeLayerSignature, PublicKey, RotationContext,
RotationTarget, ShrincsVerifier, StatefulPublicKey, StatefulRotationTarget, StatefulSignature,
Expand Down
6 changes: 3 additions & 3 deletions src/shrincs/shrincs_signer_fors_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

use zeroize::Zeroizing;

use super::shrincs_signer_types::{ShrincsSignerResult, ShrincsSigningKey};
use super::shrincs_signer_types::{ShrincsSignerError, ShrincsSignerResult, ShrincsSigningKey};
use super::shrincs_signer_utils::{
fors_address_word, hash_node, hash_packed, pack, read_bits32, read_bits64,
FORS_C_MAX_GRIND_COUNTER,
Expand Down Expand Up @@ -109,7 +109,7 @@ pub(crate) fn sign_fors_c(
// The verifier aggregates the reconstructed per-tree roots the same way.
// The public seed is included so roots from a different FORS key cannot
// be transplanted into this key.
return Some(SignedForsC {
return Ok(SignedForsC {
root: hash_node(&[b"fors-pk", &signing_key.pk_seed, &roots]),
signature: ForsSignature {
randomizer: randomizer.to_vec(),
Expand All @@ -121,7 +121,7 @@ pub(crate) fn sign_fors_c(
});
}

None
Err(ShrincsSignerError::GrindBudgetExhausted)
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
10 changes: 5 additions & 5 deletions src/shrincs/shrincs_signer_hypertree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

use zeroize::Zeroizing;

use super::shrincs_signer_types::{ShrincsSignerResult, ShrincsSigningKey};
use super::shrincs_signer_types::{ShrincsSignerError, ShrincsSignerResult, ShrincsSigningKey};
use super::shrincs_signer_utils::{
address_word32, base_w_digit, derive32, hash_node, hash_packed, hypertree_address_word,
wots_digest_bytes, WOTS_C_MAX_GRIND_COUNTER,
Expand Down Expand Up @@ -84,7 +84,7 @@ pub(crate) fn sign_hypertree(
// Mirror the verifier's guard so a retuned profile fails closed instead of
// panicking on the shift below.
if subtree_height == 0 || subtree_height >= u32::BITS {
return None;
return Err(ShrincsSignerError::InvalidConfiguration);
}
let leaf_mask = (1u64 << subtree_height) - 1;
// `stateless_sk_seed` is the shared SK.seed-style master for FORS-C and
Expand Down Expand Up @@ -166,7 +166,7 @@ pub(crate) fn sign_hypertree(
leaf = (tree & leaf_mask) as u32;
tree >>= subtree_height;
}
Some(layers)
Ok(layers)
}

pub(crate) fn hypertree_public_root(
Expand Down Expand Up @@ -337,14 +337,14 @@ fn sign_stateless_wots_c(
.to_vec()
})
.collect();
return Some(WotsCSignature {
return Ok(WotsCSignature {
randomizer: randomizer.to_vec(),
counter,
chains,
});
}

None
Err(ShrincsSignerError::GrindBudgetExhausted)
}

fn stateless_wots_c_secret(sk_seed: &[u8; HASH_LEN], chain: u32) -> [u8; HASH_LEN] {
Expand Down
18 changes: 9 additions & 9 deletions src/shrincs/shrincs_signer_stateful.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

use zeroize::Zeroizing;

use super::shrincs_signer_types::{ShrincsSignerResult, ShrincsSigningKey};
use super::shrincs_signer_types::{ShrincsSignerError, ShrincsSignerResult, ShrincsSigningKey};
use super::shrincs_signer_utils::{
address_word32, base_w16_digit, hash_node, hash_packed, WOTS_C_MAX_GRIND_COUNTER,
};
Expand All @@ -36,10 +36,10 @@ pub(crate) fn sign_stateful_raw(
// signer must advance one leaf at a time and must never reuse a prior leaf.
let leaf_index = signing_key.next_stateful_leaf_index;
if leaf_index == 0 {
return None;
return Err(ShrincsSignerError::StatefulLeavesExhausted);
}
if leaf_index > signing_key.max_stateful_signatures {
return None;
return Err(ShrincsSignerError::StatefulLeavesExhausted);
}

// sign_stateful_raw_at_leaf already computes the identical auth_path (same
Expand All @@ -48,7 +48,7 @@ pub(crate) fn sign_stateful_raw(
// the dominant signing cost.
let signature = sign_stateful_raw_at_leaf(signing_key, leaf_index, message)?;
signing_key.next_stateful_leaf_index = leaf_index.saturating_add(1);
Some(signature)
Ok(signature)
}

pub(crate) fn sign_stateful_raw_at_leaf(
Expand All @@ -60,10 +60,10 @@ pub(crate) fn sign_stateful_raw_at_leaf(
// Production signing should use `sign_stateful_raw`, which advances the
// monotonic `next_stateful_leaf_index` and avoids accidental leaf reuse.
if leaf_index == 0 {
return None;
return Err(ShrincsSignerError::StatefulLeavesExhausted);
}
if leaf_index > signing_key.max_stateful_signatures {
return None;
return Err(ShrincsSignerError::StatefulLeavesExhausted);
}
let mut signature = sign_stateful_wots_c(
&signing_key.stateful_sk_seed,
Expand All @@ -78,7 +78,7 @@ pub(crate) fn sign_stateful_raw_at_leaf(
leaf_index,
signing_key.max_stateful_signatures,
);
Some(signature)
Ok(signature)
}

pub(crate) fn stateful_subtree_root(
Expand Down Expand Up @@ -155,15 +155,15 @@ fn sign_stateful_wots_c(
})
.collect();

return Some(StatefulSignature {
return Ok(StatefulSignature {
randomizer,
counter,
chains,
auth_path: Vec::new(),
});
}

None
Err(ShrincsSignerError::GrindBudgetExhausted)
}

fn stateful_chain_secret(
Expand Down
16 changes: 13 additions & 3 deletions src/shrincs/shrincs_signer_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,19 @@ use core::fmt;
use super::verifier::HASH_LEN;
use zeroize::{Zeroize, ZeroizeOnDrop};

/// Signer operations return `None` when stateful leaves are exhausted or
/// WOTS-C/FORS-C grinding fails within the configured counter budget.
pub type ShrincsSignerResult<T> = Option<T>;
/// Typed errors returned by SHRINCS signer operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ShrincsSignerError {
#[error("stateful leaves out of range or budget exhausted")]
StatefulLeavesExhausted,
#[error("grind counter budget exhausted for the supplied key and message")]
GrindBudgetExhausted,
#[error("invalid parameter or key configuration")]
InvalidConfiguration,
}

/// Signer operations return `Result<T, ShrincsSignerError>`.
pub type ShrincsSignerResult<T> = Result<T, ShrincsSignerError>;

/// Secret material for both the stateful fast path and stateless recovery path.
///
Expand Down
3 changes: 3 additions & 0 deletions src/shrincs/shrincs_signer_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub(crate) use super::super::shrincs_common::{
use super::verifier::{PublicKey, HASH_LEN, PROFILE_NAME, STATEFUL_PUBLIC_KEY_BYTES};

pub(crate) const WOTS_C_MAX_GRIND_COUNTER: u32 = 1 << 24;
#[cfg(any(feature = "profile-128s-q18", feature = "profile-128s-q20"))]
pub(crate) const FORS_C_MAX_GRIND_COUNTER: u32 = 1 << 30;
#[cfg(not(any(feature = "profile-128s-q18", feature = "profile-128s-q20")))]
pub(crate) const FORS_C_MAX_GRIND_COUNTER: u32 = 1 << 24;

pub(crate) fn public_key_from_components(
Expand Down
43 changes: 22 additions & 21 deletions src/shrincs/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ mod shrincs_signer_types;
#[path = "shrincs_signer_utils.rs"]
mod shrincs_signer_utils;

pub use self::shrincs_signer_types::{ShrincsSignerResult, ShrincsSigningKey};
pub use self::shrincs_signer_types::{ShrincsSignerError, ShrincsSignerResult, ShrincsSigningKey};

use self::shrincs_signer_fors_c::sign_fors_c;
use self::shrincs_signer_hypertree::{hypertree_public_root, sign_hypertree};
Expand Down Expand Up @@ -66,10 +66,10 @@ impl ShrincsSigner {
max_stateful_signatures: u32,
) -> ShrincsSignerResult<(ShrincsSigningKey, PublicKey)> {
if max_stateful_signatures == 0 {
return None;
return Err(ShrincsSignerError::InvalidConfiguration);
}
if max_stateful_signatures > MAX_STATEFUL_SIGNATURES_LIMIT {
return None;
return Err(ShrincsSignerError::InvalidConfiguration);
}

let stateful_sk_seed = derive32(b"shrincs-stateful-sk-seed", seed_material, &[]);
Expand Down Expand Up @@ -104,7 +104,7 @@ impl ShrincsSigner {
hypertree_root,
);

Some((signing_key, public_key))
Ok((signing_key, public_key))
}

/// Reconstruct a signing key from previously exported fields (the inverse
Expand All @@ -119,11 +119,11 @@ impl ShrincsSigner {
) -> ShrincsSignerResult<(ShrincsSigningKey, PublicKey)> {
let max = candidate.max_stateful_signatures;
if max == 0 || max > MAX_STATEFUL_SIGNATURES_LIMIT {
return None;
return Err(ShrincsSignerError::InvalidConfiguration);
}
let next = candidate.next_stateful_leaf_index;
if next < INITIAL_STATEFUL_LEAF_INDEX || next > max.saturating_add(1) {
return None;
return Err(ShrincsSignerError::InvalidConfiguration);
}
// Recompute, never trust: the roots are consensus-critical inputs to
// every signature this key will produce. The stateful root always
Expand All @@ -137,14 +137,14 @@ impl ShrincsSigner {
let hypertree_root =
hypertree_public_root(&candidate.stateless_sk_seed, &candidate.pk_seed);
if stateful_root != candidate.stateful_root || hypertree_root != candidate.hypertree_root {
return None;
return Err(ShrincsSignerError::InvalidConfiguration);
}
let public_key = public_key_from_components(
encode_stateful_public_key(candidate.stateful_pk_seed, stateful_root, max),
candidate.pk_seed,
hypertree_root,
);
Some((candidate, public_key))
Ok((candidate, public_key))
}

/// Sign the verifier's canonical stateful action hash and advance the leaf counter.
Expand All @@ -153,7 +153,8 @@ impl ShrincsSigner {
public_key: &PublicKey,
context: &ActionContext,
) -> ShrincsSignerResult<StatefulSignature> {
let expected = word32(&public_key.public_key_commitment)?;
let expected = word32(&public_key.public_key_commitment)
.ok_or(ShrincsSignerError::InvalidConfiguration)?;
let verifier = ShrincsVerifier::new();
let message = verifier.stateful_action_message_hash(expected, context);
sign_stateful_raw_inner(signing_key, &message)
Expand Down Expand Up @@ -194,7 +195,7 @@ impl ShrincsSigner {
signed_fors.tree_index,
signed_fors.leaf_index,
)?;
Some(StatelessSignature {
Ok(StatelessSignature {
fors: signed_fors.signature,
hypertree,
})
Expand Down Expand Up @@ -481,8 +482,8 @@ mod tests {

#[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());
assert!(ShrincsSigner::keygen(b"seed", 0).is_err());
assert!(ShrincsSigner::keygen(b"seed", MAX_STATEFUL_SIGNATURES_LIMIT + 1).is_err());
}

#[cfg_attr(
Expand Down Expand Up @@ -510,7 +511,7 @@ mod tests {

// The stateful signer is one-time per leaf. With a budget of one, the
// next signing attempt must fail instead of reusing the previous leaf.
assert!(ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).is_none());
assert!(ShrincsSigner::sign_stateful_raw(&mut signing_key, &message).is_err());
}

#[cfg_attr(
Expand Down Expand Up @@ -679,43 +680,43 @@ mod tests {
let (mut key, _) = ShrincsSigner::keygen(b"import counter seed", 4).unwrap();
key.next_stateful_leaf_index = 5; // max + 1: exhausted, still valid
let (imported, _) = ShrincsSigner::import_signing_key(key).unwrap();
assert!(ShrincsSigner::sign_stateful_raw(&mut { imported }, b"no leaves left").is_none());
assert!(ShrincsSigner::sign_stateful_raw(&mut { imported }, b"no leaves left").is_err());
}

#[test]
fn import_rejects_out_of_range_counters_and_budgets() {
let (mut key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
key.next_stateful_leaf_index = 0;
assert!(ShrincsSigner::import_signing_key(key).is_none());
assert!(ShrincsSigner::import_signing_key(key).is_err());

let (mut key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
key.next_stateful_leaf_index = 6; // max + 2
assert!(ShrincsSigner::import_signing_key(key).is_none());
assert!(ShrincsSigner::import_signing_key(key).is_err());

let (mut key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
key.max_stateful_signatures = 0;
assert!(ShrincsSigner::import_signing_key(key).is_none());
assert!(ShrincsSigner::import_signing_key(key).is_err());

let (mut key, _) = ShrincsSigner::keygen(b"import bounds seed", 4).unwrap();
key.max_stateful_signatures = 4097; // > MAX_STATEFUL_SIGNATURES_LIMIT
assert!(ShrincsSigner::import_signing_key(key).is_none());
assert!(ShrincsSigner::import_signing_key(key).is_err());
}

#[test]
fn import_rejects_tampered_roots() {
let (mut key, _) = ShrincsSigner::keygen(b"import tamper seed", 4).unwrap();
key.stateful_root[0] ^= 0x01;
assert!(ShrincsSigner::import_signing_key(key).is_none());
assert!(ShrincsSigner::import_signing_key(key).is_err());

let (mut key, _) = ShrincsSigner::keygen(b"import tamper seed", 4).unwrap();
key.hypertree_root[0] ^= 0x01;
assert!(ShrincsSigner::import_signing_key(key).is_none());
assert!(ShrincsSigner::import_signing_key(key).is_err());

// Field splice: seeds from one key, roots from another.
let (key_a, _) = ShrincsSigner::keygen(b"import splice seed A", 4).unwrap();
let (mut key_b, _) = ShrincsSigner::keygen(b"import splice seed B", 4).unwrap();
key_b.stateful_root = key_a.stateful_root;
assert!(ShrincsSigner::import_signing_key(key_b).is_none());
assert!(ShrincsSigner::import_signing_key(key_b).is_err());
}

#[test]
Expand Down
Loading