use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::uxmss;
use super::Keys;
use super::{PublicKey, StatelessSignature, HASH_LEN};
use crate::sphincs_plus_c;
use crate::sphincs_plus_c::{ForsEntry, ForsSignature, LayerSignature as HypertreeLayerSignature};
use crate::wots_c::Signature as WotsCSignature;
pub(crate) const FIXTURE_PATH_ENV: &str = "SHRINCS_TEST_KEY_FIXTURE_PATH";
pub(crate) const KEY_MODE_ENV: &str = "SHRINCS_TEST_KEY_MODE";
pub(crate) const DEFAULT_FIXTURE_DIR: &str = "tests/test_fixtures";
pub(crate) const KEY_FIXTURE_BASENAME: &str = "account_keys";
pub(crate) const STATEFUL_SIGNER_FIXTURE_PATH_ENV: &str =
"SHRINCS_TEST_STATEFUL_SIGNER_FIXTURE_PATH";
pub(crate) const STATEFUL_SIGNER_FIXTURE_BASENAME: &str = "stateful_signer_keys";
fn profile_fixture_path(base_name: &str) -> PathBuf {
PathBuf::from(DEFAULT_FIXTURE_DIR)
.join(format!("{base_name}.{}.json", crate::shrincs::PROFILE_NAME))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TestKeyMode {
Fixture,
Fresh,
}
impl TestKeyMode {
pub(crate) fn from_env() -> Self {
match env::var(KEY_MODE_ENV) {
Ok(value) if value.eq_ignore_ascii_case("fresh") => Self::Fresh,
Ok(value) if value.eq_ignore_ascii_case("fixture") => Self::Fixture,
Ok(value) if value.is_empty() => Self::Fixture,
Ok(_) | Err(_) => Self::Fixture,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SigningKeyDto {
pub(crate) stateful_sk_seed: [u8; HASH_LEN],
pub(crate) stateful_prf_seed: [u8; HASH_LEN],
pub(crate) stateful_pk_seed: [u8; HASH_LEN],
pub(crate) stateful_root: [u8; HASH_LEN],
pub(crate) max_stateful_signatures: u32,
pub(crate) next_stateful_leaf_index: u32,
pub(crate) stateless_sk_seed: [u8; HASH_LEN],
pub(crate) stateless_prf_seed: [u8; HASH_LEN],
pub(crate) pk_seed: [u8; HASH_LEN],
pub(crate) hypertree_root: [u8; HASH_LEN],
}
impl From<&Keys> for SigningKeyDto {
fn from(value: &Keys) -> Self {
Self {
stateful_sk_seed: *value.stateful().secret().as_sk_seed().as_bytes(),
stateful_prf_seed: *value.stateful().secret().as_prf_seed().as_bytes(),
stateful_pk_seed: *value.stateful().public_key().pk_seed.as_bytes(),
stateful_root: *value.stateful().public_key().root.as_bytes(),
max_stateful_signatures: value.stateful().public_key().max_signatures,
next_stateful_leaf_index: value.stateful().next_leaf_index(),
stateless_sk_seed: *value.stateless().secret().as_sk_seed().as_bytes(),
stateless_prf_seed: *value.stateless().secret().as_prf_seed().as_bytes(),
pk_seed: *value.stateless().public_key.pk_seed.as_bytes(),
hypertree_root: *value.stateless().public_key.root.as_bytes(),
}
}
}
impl From<SigningKeyDto> for Keys {
fn from(value: SigningKeyDto) -> Self {
let stateful = uxmss::Key::new(
uxmss::PrivateKey::new(
uxmss::SkSeed::new(value.stateful_sk_seed),
uxmss::PrfSeed::new(value.stateful_prf_seed),
),
uxmss::StructuredPublicKey {
pk_seed: uxmss::PkSeed::new(value.stateful_pk_seed),
root: uxmss::Root::new(value.stateful_root),
max_signatures: value.max_stateful_signatures,
},
value.next_stateful_leaf_index,
);
let stateless = sphincs_plus_c::Key::new(
sphincs_plus_c::PrivateKey::new(
sphincs_plus_c::SkSeed::new(value.stateless_sk_seed),
sphincs_plus_c::PrfSeed::new(value.stateless_prf_seed),
),
sphincs_plus_c::PublicKey {
pk_seed: sphincs_plus_c::PkSeed::new(value.pk_seed),
root: sphincs_plus_c::Root::new(value.hypertree_root),
},
);
Keys::new(stateless, stateful)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct PublicKeyDto {
pub(crate) stateful_public_key: Vec<u8>,
pub(crate) public_key_commitment: Vec<u8>,
pub(crate) pk_seed: Vec<u8>,
pub(crate) hypertree_root: Vec<u8>,
}
impl From<&PublicKey> for PublicKeyDto {
fn from(value: &PublicKey) -> Self {
Self {
stateful_public_key: value.stateful_public_key.clone(),
public_key_commitment: value.public_key_commitment.clone(),
pk_seed: value.pk_seed.clone(),
hypertree_root: value.hypertree_root.clone(),
}
}
}
impl From<PublicKeyDto> for PublicKey {
fn from(value: PublicKeyDto) -> Self {
Self {
stateful_public_key: value.stateful_public_key,
public_key_commitment: value.public_key_commitment,
pk_seed: value.pk_seed,
hypertree_root: value.hypertree_root,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct KeyFixtureEntry {
pub(crate) seed_label: String,
pub(crate) signing_key: SigningKeyDto,
pub(crate) public_key: PublicKeyDto,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct KeyFixtureFile {
pub(crate) profile_name: String,
pub(crate) entries: Vec<KeyFixtureEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ForsEntryDto {
pub(crate) secret_leaf: Vec<u8>,
pub(crate) auth_path: Vec<Vec<u8>>,
}
impl From<&ForsEntry> for ForsEntryDto {
fn from(value: &ForsEntry) -> Self {
Self {
secret_leaf: value.secret_leaf.to_vec(),
auth_path: nodes_to_vecs(&value.auth_path),
}
}
}
impl From<ForsEntryDto> for ForsEntry {
fn from(value: ForsEntryDto) -> Self {
Self {
secret_leaf: fixture_word(value.secret_leaf),
auth_path: vecs_to_nodes(value.auth_path),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ForsSignatureDto {
pub(crate) randomizer: Vec<u8>,
pub(crate) counter: u32,
pub(crate) entries: Vec<ForsEntryDto>,
}
impl From<&ForsSignature> for ForsSignatureDto {
fn from(value: &ForsSignature) -> Self {
Self {
randomizer: value.randomizer.to_vec(),
counter: value.counter,
entries: value.entries.iter().map(ForsEntryDto::from).collect(),
}
}
}
impl From<ForsSignatureDto> for ForsSignature {
fn from(value: ForsSignatureDto) -> Self {
Self {
randomizer: fixture_word(value.randomizer),
counter: value.counter,
entries: value.entries.into_iter().map(ForsEntry::from).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct WotsCSignatureDto {
pub(crate) randomizer: Vec<u8>,
pub(crate) counter: u32,
pub(crate) chains: Vec<Vec<u8>>,
}
impl From<&WotsCSignature> for WotsCSignatureDto {
fn from(value: &WotsCSignature) -> Self {
Self {
randomizer: value.randomizer.to_vec(),
counter: value.counter,
chains: nodes_to_vecs(&value.chains),
}
}
}
impl From<WotsCSignatureDto> for WotsCSignature {
fn from(value: WotsCSignatureDto) -> Self {
Self {
randomizer: fixture_word(value.randomizer),
counter: value.counter,
chains: vecs_to_nodes(value.chains),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct HypertreeLayerSignatureDto {
pub(crate) wots_c_pk_hash: Vec<u8>,
pub(crate) wots_c_signature: WotsCSignatureDto,
pub(crate) auth_path: Vec<Vec<u8>>,
}
impl From<&HypertreeLayerSignature> for HypertreeLayerSignatureDto {
fn from(value: &HypertreeLayerSignature) -> Self {
Self {
wots_c_pk_hash: value.wots_c_pk_hash.to_vec(),
wots_c_signature: WotsCSignatureDto::from(&value.wots_c_signature),
auth_path: nodes_to_vecs(&value.auth_path),
}
}
}
impl From<HypertreeLayerSignatureDto> for HypertreeLayerSignature {
fn from(value: HypertreeLayerSignatureDto) -> Self {
Self {
wots_c_pk_hash: fixture_word(value.wots_c_pk_hash),
wots_c_signature: value.wots_c_signature.into(),
auth_path: vecs_to_nodes(value.auth_path),
}
}
}
fn fixture_word(bytes: Vec<u8>) -> [u8; HASH_LEN] {
bytes
.try_into()
.expect("fixture hash field must be exactly HASH_LEN bytes")
}
fn vecs_to_nodes(list: Vec<Vec<u8>>) -> Vec<[u8; HASH_LEN]> {
list.into_iter().map(fixture_word).collect()
}
fn nodes_to_vecs(nodes: &[[u8; HASH_LEN]]) -> Vec<Vec<u8>> {
nodes.iter().map(|node| node.to_vec()).collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct StatelessSignatureDto {
pub(crate) fors: ForsSignatureDto,
pub(crate) hypertree: Vec<HypertreeLayerSignatureDto>,
}
impl From<&StatelessSignature> for StatelessSignatureDto {
fn from(value: &StatelessSignature) -> Self {
Self {
fors: ForsSignatureDto::from(&value.fors),
hypertree: value
.hypertree
.iter()
.map(HypertreeLayerSignatureDto::from)
.collect(),
}
}
}
impl From<StatelessSignatureDto> for StatelessSignature {
fn from(value: StatelessSignatureDto) -> Self {
Self {
fors: value.fors.into(),
hypertree: value
.hypertree
.into_iter()
.map(HypertreeLayerSignature::from)
.collect(),
}
}
}
pub(crate) fn fixture_path() -> PathBuf {
env::var_os(FIXTURE_PATH_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| profile_fixture_path(KEY_FIXTURE_BASENAME))
}
pub(crate) fn stateful_signer_fixture_path() -> PathBuf {
env::var_os(STATEFUL_SIGNER_FIXTURE_PATH_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| profile_fixture_path(STATEFUL_SIGNER_FIXTURE_BASENAME))
}
pub(crate) fn load_fixture_file(path: &Path) -> KeyFixtureFile {
let json = fs::read_to_string(path)
.unwrap_or_else(|error| panic!("failed to read fixture file {}: {error}", path.display()));
serde_json::from_str(&json)
.unwrap_or_else(|error| panic!("failed to parse fixture file {}: {error}", path.display()))
}
pub(crate) fn write_fixture_file(path: &Path, fixture_file: &KeyFixtureFile) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap_or_else(|error| {
panic!(
"failed to create fixture directory {}: {error}",
parent.display()
)
});
}
let json = serde_json::to_string(fixture_file).expect("fixture file must serialize");
fs::write(path, json)
.unwrap_or_else(|error| panic!("failed to write fixture file {}: {error}", path.display()));
}
pub(crate) fn fixture_entry_opt<'a>(
fixture_file: &'a KeyFixtureFile,
seed_label: &str,
) -> Option<&'a KeyFixtureEntry> {
fixture_file
.entries
.iter()
.find(|entry| entry.seed_label == seed_label)
}
pub(crate) fn fixture_pair(entry: &KeyFixtureEntry) -> (Keys, PublicKey) {
(
entry.signing_key.clone().into(),
entry.public_key.clone().into(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shrincs::{ShrincsSigner, PROFILE_NAME};
#[cfg(any(shrincs_profile_128s_q18, shrincs_profile_128s_q20))]
fn full_key_fixture_specs() -> Vec<(&'static str, u32)> {
vec![
("deterministic keygen seed", 4),
("shrincs solidity vector stateful seed", 4),
]
}
#[cfg(not(any(shrincs_profile_128s_q18, shrincs_profile_128s_q20)))]
fn full_key_fixture_specs() -> Vec<(&'static str, u32)> {
vec![
("stateless negative seed", 2),
("stateless malformed seed", 2),
("stateless empty message seed", 2),
("stateless signer seed", 2),
("deterministic keygen seed", 4),
("public key structure seed", 8),
("initial stateful leaf seed", 8),
("stateful signer seed", 4),
("action signer seed", 4),
("explicit leaf helper seed", 4),
("stateful exhaustion seed", 1),
("stateful negative seed", 4),
("action negative seed", 4),
("public key negative seed", 4),
("shrincs solidity vector stateful seed", 4),
]
}
use crate::test_support::stateful_only_key;
fn stateful_signer_fixture_specs() -> Vec<(&'static str, u32)> {
vec![
("stateful signer seed", 4),
("action signer seed", 4),
("explicit leaf helper seed", 4),
("stateful exhaustion seed", 1),
("stateful negative seed", 4),
("action negative seed", 4),
("public key negative seed", 4),
]
}
#[test]
#[ignore = "writes checked-in test fixtures on demand"]
fn write_account_key_fixture_file() {
let entries = full_key_fixture_specs()
.into_iter()
.map(|(seed_label, max_stateful_signatures)| {
let (signing_key, public_key) = ShrincsSigner::keygen(
seed_label.as_bytes(),
max_stateful_signatures,
)
.unwrap_or_else(|| {
panic!(
"fixture keygen failed for seed label {seed_label:?} with max_stateful_signatures={max_stateful_signatures}"
)
});
KeyFixtureEntry {
seed_label: seed_label.to_string(),
signing_key: SigningKeyDto::from(&signing_key),
public_key: PublicKeyDto::from(&public_key),
}
})
.collect::<Vec<_>>();
let fixture_file = KeyFixtureFile {
profile_name: PROFILE_NAME.to_string(),
entries,
};
write_fixture_file(&fixture_path(), &fixture_file);
}
#[test]
#[ignore = "writes checked-in stateful signer fixtures on demand"]
fn write_stateful_signer_fixture_file() {
let entries = stateful_signer_fixture_specs()
.into_iter()
.map(|(seed_label, max_stateful_signatures)| {
let (signing_key, public_key) =
stateful_only_key(seed_label.as_bytes(), max_stateful_signatures);
KeyFixtureEntry {
seed_label: seed_label.to_string(),
signing_key: SigningKeyDto::from(&signing_key),
public_key: PublicKeyDto::from(&public_key),
}
})
.collect();
let fixture_file = KeyFixtureFile {
profile_name: PROFILE_NAME.to_string(),
entries,
};
write_fixture_file(&stateful_signer_fixture_path(), &fixture_file);
}
}