Skip to main content

rsa/pss/
generic_signing_key.rs

1//! Heapless RSASSA-PSS signing key. Generic over `D` (digest),
2//! `T` (integer), and `M` (Montgomery parameters), mirrors
3//! [`super::GenericVerifyingKey`] on the verify side and the
4//! [`crate::pkcs1v15::GenericSigningKey`] shape for the sign side.
5//!
6//! PSS sign needs an RNG for the salt (unlike PKCS#1 v1.5 sign which
7//! is deterministic) — `try_sign_with_rng_into` mirrors the
8//! `RandomizedEncryptor::encrypt_with_rng_into` shape on the encrypt
9//! side: caller passes `&mut R: TryCryptoRng + ?Sized`.
10
11use crate::{
12    algorithms::pss::{sign_into, sign_with_rng_into},
13    errors::{Error, Result},
14    key::GenericRsaPrivateKey,
15    traits::{
16        modular::{ModulusParams, Pow, PowBoundedExp},
17        PublicKeyParts, UnsignedModularInt,
18    },
19};
20use core::fmt;
21use core::marker::PhantomData;
22use digest::{Digest, FixedOutputReset};
23use rand_core::TryCryptoRng;
24use zeroize::Zeroize;
25
26/// Signing key for RSASSA-PSS — heapless variant.
27///
28/// Generic over the digest `D`, integer type `T`, and Montgomery parameters
29/// `M`. Holds a [`GenericRsaPrivateKey<T, M>`] plus the salt length to use
30/// when signing (caller-chosen at construction; defaults to
31/// `<D as Digest>::output_size()` via [`new`](Self::new)).
32///
33/// PSS has no DigestInfo prefix — `D` is purely a phantom marker for the
34/// digest algorithm used by both encode and verify.
35pub struct GenericSigningKey<D, T, M>
36where
37    D: Digest,
38    T: UnsignedModularInt + Zeroize,
39    M: ModulusParams<Modulus = T>,
40{
41    pub(super) inner: GenericRsaPrivateKey<T, M>,
42    pub(super) salt_len: usize,
43    pub(super) phantom: PhantomData<D>,
44}
45
46// Manual `Debug` — `#[derive]` would synthesize unwanted bounds on
47// `D`/`T`/`M`; the inner private key already redacts `d`.
48impl<D, T, M> fmt::Debug for GenericSigningKey<D, T, M>
49where
50    D: Digest,
51    T: UnsignedModularInt + Zeroize,
52    M: ModulusParams<Modulus = T>,
53    GenericRsaPrivateKey<T, M>: fmt::Debug,
54{
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.debug_struct("GenericSigningKey")
57            .field("inner", &self.inner)
58            .field("salt_len", &self.salt_len)
59            .finish()
60    }
61}
62
63// Manual Clone impls — split by `alloc` cfg to avoid imposing
64// the `M::MontgomeryForm: Clone` bound on heapless backends where it
65// isn't needed (only `GenericRsaPrivateKey`'s `precomputed` field
66// requires it, and that field is `cfg(alloc)`).
67#[cfg(feature = "alloc")]
68impl<D, T, M> Clone for GenericSigningKey<D, T, M>
69where
70    D: Digest,
71    T: UnsignedModularInt + Zeroize + Clone,
72    M: ModulusParams<Modulus = T> + Clone,
73    M::MontgomeryForm: Clone,
74{
75    fn clone(&self) -> Self {
76        Self {
77            inner: self.inner.clone(),
78            salt_len: self.salt_len,
79            phantom: PhantomData,
80        }
81    }
82}
83
84#[cfg(not(feature = "alloc"))]
85impl<D, T, M> Clone for GenericSigningKey<D, T, M>
86where
87    D: Digest,
88    T: UnsignedModularInt + Zeroize + Clone,
89    M: ModulusParams<Modulus = T> + Clone,
90{
91    fn clone(&self) -> Self {
92        Self {
93            inner: self.inner.clone(),
94            salt_len: self.salt_len,
95            phantom: PhantomData,
96        }
97    }
98}
99
100impl<D, T, M> GenericSigningKey<D, T, M>
101where
102    D: Digest,
103    T: UnsignedModularInt + Zeroize,
104    M: ModulusParams<Modulus = T>,
105{
106    /// Construct with salt length = `D::output_size()` (standard choice
107    /// per RFC 8017 § 8.1).
108    pub fn new(key: GenericRsaPrivateKey<T, M>) -> Self {
109        Self {
110            inner: key,
111            salt_len: <D as Digest>::output_size(),
112            phantom: PhantomData,
113        }
114    }
115
116    /// Construct with a custom salt length.
117    pub fn new_with_salt_len(key: GenericRsaPrivateKey<T, M>, salt_len: usize) -> Self {
118        Self {
119            inner: key,
120            salt_len,
121            phantom: PhantomData,
122        }
123    }
124
125    /// Salt length this key will use when signing.
126    pub fn salt_len(&self) -> usize {
127        self.salt_len
128    }
129}
130
131// Borrow the inner private-key components — `AsRef` impl mirrors
132// the verifier's `AsRef<GenericRsaPublicKey<T, M>>`.
133impl<D, T, M> AsRef<GenericRsaPrivateKey<T, M>> for GenericSigningKey<D, T, M>
134where
135    D: Digest,
136    T: UnsignedModularInt + Zeroize,
137    M: ModulusParams<Modulus = T>,
138{
139    fn as_ref(&self) -> &GenericRsaPrivateKey<T, M> {
140        &self.inner
141    }
142}
143
144// RNG-taking sign methods use base blinding in addition to the salt
145// sampling. The extra bounds are satisfied by both backends we
146// support.
147impl<D, T, M> GenericSigningKey<D, T, M>
148where
149    D: Digest + FixedOutputReset,
150    T: UnsignedModularInt + Zeroize + crate::traits::modular::TryRandomMod,
151    T::Bytes: Zeroize,
152    M: ModulusParams<Modulus = T> + crate::traits::modular::CtModulusParams,
153    M::MontgomeryForm: Pow<M>
154        + PowBoundedExp<M>
155        + crate::traits::modular::InvertCt<M>
156        + crate::traits::modular::MulCt<M>,
157{
158    /// Sign `msg` (hashed internally with `D`) into `sig_storage`, drawing
159    /// the PSS salt AND the RSA base-blinding factor from `rng`. Uses
160    /// `em_storage` and `salt_storage` as scratch. All three buffers
161    /// must be at least the relevant sizes (`em_storage`:
162    /// `em_bits.div_ceil(8)`; `sig_storage`: `n.size()`;
163    /// `salt_storage`: `self.salt_len()`).
164    ///
165    /// Mirrors [`crate::traits::RandomizedEncryptor::encrypt_with_rng_into`]:
166    /// caller owns storage, returned slice borrows from `sig_storage`.
167    pub fn try_sign_with_rng_into<'sig, R: TryCryptoRng + ?Sized>(
168        &self,
169        rng: &mut R,
170        msg: &[u8],
171        em_storage: &mut [u8],
172        sig_storage: &'sig mut [u8],
173        salt_storage: &mut [u8],
174    ) -> Result<&'sig [u8]> {
175        let digest = D::digest(msg);
176        self.try_sign_prehash_with_rng_into(
177            rng,
178            digest.as_ref(),
179            em_storage,
180            sig_storage,
181            salt_storage,
182        )
183    }
184
185    /// Sign a precomputed `prehash` (`D::output_size()` bytes) into
186    /// `sig_storage`, drawing the PSS salt AND the RSA base-blinding
187    /// factor from `rng`.
188    ///
189    /// Returns [`Error::InputNotHashed`] if `prehash.len()` doesn't match
190    /// `D::output_size()`. Returns [`Error::OutputBufferTooSmall`] if
191    /// `salt_storage.len() < self.salt_len()`.
192    pub fn try_sign_prehash_with_rng_into<'sig, R: TryCryptoRng + ?Sized>(
193        &self,
194        rng: &mut R,
195        prehash: &[u8],
196        em_storage: &mut [u8],
197        sig_storage: &'sig mut [u8],
198        salt_storage: &mut [u8],
199    ) -> Result<&'sig [u8]> {
200        if prehash.len() != <D as Digest>::output_size() {
201            return Err(Error::InputNotHashed);
202        }
203        let salt = salt_storage
204            .get_mut(..self.salt_len)
205            .ok_or(Error::OutputBufferTooSmall)?;
206        rng.try_fill_bytes(salt).map_err(|_| Error::Rng)?;
207        let mut hash = D::new();
208        sign_with_rng_into(
209            rng,
210            self.inner.n_params(),
211            crate::traits::keys::PrivateKeyParts::d(&self.inner),
212            self.inner.e(),
213            prehash,
214            salt,
215            self.inner.size(),
216            &mut hash,
217            em_storage,
218            sig_storage,
219        )
220    }
221}
222
223// Deterministic (caller-supplied salt, no RNG) variant kept in a
224// separate impl block so it doesn't require the blinding bounds —
225// callers who explicitly want a deterministic sign path (typically
226// tests) don't need the extra trait impls on `T` / `M`.
227impl<D, T, M> GenericSigningKey<D, T, M>
228where
229    D: Digest + FixedOutputReset,
230    T: UnsignedModularInt + Zeroize,
231    T::Bytes: Zeroize,
232    M: ModulusParams<Modulus = T> + crate::traits::modular::CtModulusParams,
233    M::MontgomeryForm: Pow<M> + PowBoundedExp<M>,
234{
235    /// Sign with caller-supplied salt — useful for determinism in tests.
236    ///
237    /// Returns [`Error::InputNotHashed`] if `prehash.len()` doesn't match
238    /// `D::output_size()`. Returns [`Error::InvalidArguments`] if
239    /// `salt.len() != self.salt_len()` — a mismatch silently produces a
240    /// signature that no verifier configured with the key's advertised
241    /// `salt_len` will accept, so we reject it up front.
242    pub fn try_sign_prehash_with_salt_into<'sig>(
243        &self,
244        prehash: &[u8],
245        salt: &[u8],
246        em_storage: &mut [u8],
247        sig_storage: &'sig mut [u8],
248    ) -> Result<&'sig [u8]> {
249        if prehash.len() != <D as Digest>::output_size() {
250            return Err(Error::InputNotHashed);
251        }
252        if salt.len() != self.salt_len {
253            return Err(Error::InvalidArguments);
254        }
255        let mut hash = D::new();
256        sign_into(
257            self.inner.n_params(),
258            crate::traits::keys::PrivateKeyParts::d(&self.inner),
259            self.inner.e(),
260            prehash,
261            salt,
262            self.inner.size(),
263            &mut hash,
264            em_storage,
265            sig_storage,
266        )
267    }
268}
269
270// Allow callers to wrap a `GenericSigningKey` in `zeroize::Zeroizing<_>`
271// or invoke `.zeroize()` directly. The salt_len is public; only the
272// inner `GenericRsaPrivateKey` carries secret bytes.
273impl<D, T, M> Zeroize for GenericSigningKey<D, T, M>
274where
275    D: Digest,
276    T: UnsignedModularInt + Zeroize,
277    M: ModulusParams<Modulus = T>,
278{
279    fn zeroize(&mut self) {
280        self.inner.zeroize();
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    // End-to-end round-trip test for `try_sign_with_rng_into` on the boxed alias
287    // (`pss::SigningKey<D>`). Exercises the full stack: wrapper (which
288    // internally samples salt AND blinding `r`) →
289    // `algorithms::pss::sign_with_rng_into` →
290    // `rsa_private_op_and_check_blinded` → `TryRandomMod` →
291    // `rsa_private_op_blinded` (InvertCt/MulCt). The heapless
292    // exact-width equivalent lives in `modmath_support.rs`
293    // (`pss_signing_key_try_sign_prehash_with_rng_into_round_trip_2048_sha1`).
294    #[test]
295    #[cfg(feature = "encoding")]
296    fn signing_key_try_sign_with_rng_into_round_trip() {
297        use crate::pss::{Signature, SigningKey, VerifyingKey};
298        use crate::RsaPrivateKey;
299        use pkcs1::DecodeRsaPrivateKey;
300        use rand::rngs::ChaCha8Rng;
301        use rand_core::SeedableRng;
302        use sha2::Sha256;
303        use signature::Verifier;
304
305        const PRIV_KEY_PKCS1_PEM: &str =
306            include_str!("../../tests/examples/pkcs1/rsa2048-priv.pem");
307
308        let priv_key = RsaPrivateKey::from_pkcs1_pem(PRIV_KEY_PKCS1_PEM).unwrap();
309        let signing_key = SigningKey::<Sha256>::new(priv_key.clone());
310        let verifying_key = VerifyingKey::<Sha256>::new(priv_key.to_public_key());
311
312        let msg: &[u8] = b"blinded pss sign test message";
313        let mut rng = ChaCha8Rng::from_seed([42; 32]);
314
315        // 2048-bit key → 256-byte signature and EM;
316        // salt_len defaults to `Sha256::output_size()` = 32.
317        let mut em_storage = [0u8; 256];
318        let mut sig_storage = [0u8; 256];
319        let mut salt_storage = [0u8; 32];
320        let sig_slice = signing_key
321            .try_sign_with_rng_into(
322                &mut rng,
323                msg,
324                &mut em_storage,
325                &mut sig_storage,
326                &mut salt_storage,
327            )
328            .unwrap();
329        assert_eq!(sig_slice.len(), 256);
330
331        let sig = Signature::try_from(sig_slice).unwrap();
332        verifying_key.verify(msg, &sig).unwrap();
333    }
334}