Skip to main content

winterwallet_core/
signature.rs

1use core::mem::MaybeUninit;
2
3use crate::{WinternitzError, WinternitzPubkey, WinternitzRoot};
4
5/// Winternitz one-time signature: `N` message scalars and 2 checksum scalars,
6/// each 32 bytes. Total size is `(N + 2) * 32` bytes.
7///
8/// Verify with [`Self::verify`] against a [`WinternitzRoot`] and the original
9/// message. Verification reconstructs the corresponding pubkey by completing
10/// each chain, merklizes, and compares to the supplied root.
11#[repr(C)]
12pub struct WinternitzSignature<const N: usize> {
13    scalars: [[u8; 32]; N],
14    checksum: [[u8; 32]; 2],
15}
16
17impl<const N: usize> WinternitzSignature<N> {
18    /// Create a signature from `N * [u8;32]` scalars + 2 checksum scalars.
19    pub fn new(scalars: [[u8; 32]; N], checksum: [[u8; 32]; 2]) -> Self {
20        const { crate::assert_n::<N>() };
21        Self { scalars, checksum }
22    }
23
24    /// Return the signature's `(N + 2) * 32` raw bytes (message scalars then
25    /// checksum scalars), with no copy.
26    pub fn as_bytes(&self) -> &[u8] {
27        // SAFETY: #[repr(C)], all fields are [u8; _] with align 1, no padding.
28        unsafe {
29            core::slice::from_raw_parts(
30                self as *const Self as *const u8,
31                core::mem::size_of::<Self>(),
32            )
33        }
34    }
35
36    /// Verify this signature against a message and a stored root. Returns
37    /// `true` if the signature is valid. The message is supplied as a slice
38    /// of byte slices (matching `solana_sha256_hasher::hashv`), so callers
39    /// can mix domain-separation tags or context bytes with the payload
40    /// without an intermediate allocation.
41    pub fn verify(&self, message: &[&[u8]], root: &WinternitzRoot) -> bool {
42        self.recover_pubkey(message).merklize() == *root
43    }
44
45    /// Verify against a pre-hashed message digest. See [`Self::hash`] for the
46    /// expected digest construction.
47    #[inline(always)]
48    pub fn verify_prehashed(&self, hash: &[u8; N], root: &WinternitzRoot) -> bool {
49        self.recover_pubkey_prehashed(hash).merklize() == *root
50    }
51
52    /// Recover the [`WinternitzPubkey`] implied by this signature over the
53    /// given message. No verification is performed — pair with
54    /// [`WinternitzPubkey::merklize`] (or rely on the `Into<WinternitzRoot>`
55    /// impl) and compare against a trusted root to verify.
56    ///
57    /// The message is supplied as a slice of byte slices so callers can
58    /// include domain-separation tags or context bytes alongside the payload
59    /// (matching `solana_sha256_hasher::hashv`).
60    #[inline(always)]
61    pub fn recover_pubkey(&self, message: &[&[u8]]) -> WinternitzPubkey<N> {
62        const { crate::assert_n::<N>() };
63        let h = crate::hash::<N>(message);
64        self.recover_pubkey_prehashed(&h)
65    }
66
67    /// Recover the [`WinternitzPubkey`] from a pre-hashed message digest. See
68    /// [`Self::hash`] for the expected digest construction.
69    #[inline(always)]
70    pub fn recover_pubkey_prehashed(&self, hash: &[u8; N]) -> WinternitzPubkey<N> {
71        const { crate::assert_n::<N>() };
72        let mut pk_scalars: [MaybeUninit<[u8; 32]>; N] = [const { MaybeUninit::uninit() }; N];
73        let mut checksum_sum: u16 = 0;
74        for i in 0..N {
75            let b = hash[i];
76            pk_scalars[i].write(crate::chain(&self.scalars[i], 255 - b));
77            checksum_sum += 255u16 - b as u16;
78        }
79        let pk_checksum = [
80            crate::chain(&self.checksum[0], 255 - (checksum_sum >> 8) as u8),
81            crate::chain(&self.checksum[1], 255 - checksum_sum as u8),
82        ];
83        // SAFETY: the loop above wrote every slot of `pk_scalars`, and
84        // `[MaybeUninit<T>; N]` has the same layout as `[T; N]`.
85        let pk_scalars: [[u8; 32]; N] =
86            unsafe { core::ptr::read(pk_scalars.as_ptr() as *const [[u8; 32]; N]) };
87        WinternitzPubkey::new(pk_scalars, pk_checksum)
88    }
89
90    /// Hash a message into the `N`-byte Winternitz digest used by verification.
91    /// Equivalent to truncating SHA-256 of the concatenated `message` slices
92    /// to `N` bytes.
93    #[inline(always)]
94    pub fn hash(message: &[&[u8]]) -> [u8; N] {
95        crate::hash::<N>(message)
96    }
97}
98
99impl<const N: usize> core::fmt::Display for WinternitzSignature<N> {
100    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101        write!(f, "0x")?;
102        for s in self.scalars.iter().chain(self.checksum.iter()) {
103            for b in s {
104                write!(f, "{:02x}", b)?;
105            }
106        }
107        Ok(())
108    }
109}
110
111impl<const N: usize> core::fmt::Debug for WinternitzSignature<N> {
112    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113        writeln!(f, "WinternitzSignature {{")?;
114        for (i, s) in self.scalars.iter().enumerate() {
115            write!(f, "  scalars[{}]  = 0x", i)?;
116            for b in s {
117                write!(f, "{:02x}", b)?;
118            }
119            writeln!(f)?;
120        }
121        for (i, s) in self.checksum.iter().enumerate() {
122            write!(f, "  checksum[{}] = 0x", i)?;
123            for b in s {
124                write!(f, "{:02x}", b)?;
125            }
126            writeln!(f)?;
127        }
128        write!(f, "}}")
129    }
130}
131
132impl<'a, const N: usize> TryFrom<&'a [u8]> for &'a WinternitzSignature<N> {
133    type Error = WinternitzError;
134
135    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
136        const { crate::assert_n::<N>() };
137        if value.len() != (N + 2) * 32 {
138            return Err(WinternitzError::InvalidLength);
139        }
140        // SAFETY: length verified; alignment is 1; every bit pattern is valid.
141        Ok(unsafe { &*value.as_ptr().cast::<WinternitzSignature<N>>() })
142    }
143}