Skip to main content

winterwallet_core/
lib.rs

1#![no_std]
2#![deny(missing_docs)]
3
4//! Winternitz one-time signatures with BIP-39 keypair derivation.
5//!
6//! `no_std` (no `alloc`). The `signer` and `mnemonic` paths
7//! ([`WinternitzKeypair`], [`WinternitzPrivkey`]) compile only off-Solana;
8//! verification ([`WinternitzSignature`], [`WinternitzPubkey`],
9//! [`WinternitzRoot`]) builds everywhere.
10//!
11//! ## Security
12//!
13//! Winternitz is a **one-time signature scheme**. Signing two different
14//! messages with the same privkey scalars allows an attacker to forge a third
15//! signature. Use [`WinternitzKeypair::sign_and_increment`] to enforce
16//! position advancement after every signature.
17//!
18//! Derivation uses a custom magic string `"Winternitz seed"` and is
19//! **not** BIP-32 compatible — keys derived here will not match any standard
20//! Bitcoin/Solana wallet.
21//!
22//! ## Parameters
23//!
24//! All public types take a const generic `N` (number of message scalars).
25//! `N` must be even and in `16..=32`; the constraint is enforced at compile
26//! time. `N = 32` gives 256-bit message-hash security.
27
28mod error;
29mod pubkey;
30mod signature;
31
32#[cfg(not(any(target_arch = "bpf", target_os = "solana")))]
33mod privkey;
34
35#[cfg(not(any(target_arch = "bpf", target_os = "solana")))]
36mod keypair;
37
38pub use error::WinternitzError;
39pub use pubkey::{WinternitzPubkey, WinternitzRoot};
40pub use signature::WinternitzSignature;
41
42#[cfg(not(any(target_arch = "bpf", target_os = "solana")))]
43pub use privkey::WinternitzPrivkey;
44
45#[cfg(not(any(target_arch = "bpf", target_os = "solana")))]
46pub use keypair::WinternitzKeypair;
47
48pub(crate) const fn assert_n<const N: usize>() {
49    assert!(
50        N >= 16 && N <= 32 && N.is_multiple_of(2),
51        "N must be even and in 16..=32",
52    );
53}
54
55#[inline(always)]
56pub(crate) fn chain(seed: &[u8; 32], iters: u8) -> [u8; 32] {
57    let mut current = *seed;
58    for _ in 0..iters {
59        current = solana_sha256_hasher::hash(&current).to_bytes();
60    }
61    current
62}
63
64#[inline(always)]
65pub(crate) fn hash<const N: usize>(message: &[&[u8]]) -> [u8; N] {
66    const { assert_n::<N>() };
67    let digest = solana_sha256_hasher::hashv(message).to_bytes();
68    let mut out = [0u8; N];
69    out.copy_from_slice(&digest[..N]);
70    out
71}