From 9a71ae17d1c8ee2b267a0b78c192fbf585a3e652 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Mon, 2 Nov 2026 10:55:09 -0700 Subject: [PATCH] Blind signing paths by default; deprecate `pss::BlindedSigningKey` Similar change to #739, but for signing instead of decryption, utilizing `getrandom` when available to supply randomness for blinding automatically when the crate feature is enabled (which it is by default). This also changes `pss::BlindedSigningKey` into a deprecated type alias for `pss::SigningKey`. Contrary to a code comment claiming that it's an implementation of `draft-irtf-cfrg-rsa-blind-signatures` (now RFC9474) it doesn't actually implement this protocol but merely blinds the signing paths, now making it redundant. A proper implementation of RFC9474 blinded signatures would be interesting to add, but the API would need to receive an already encoded and blinded representative from the client, and we don't support that. --- src/key.rs | 19 +- src/lib.rs | 1 + src/pkcs1v15.rs | 21 +++ src/pkcs1v15/signing_key.rs | 14 +- src/pss.rs | 127 +++----------- src/pss/blinded_signing_key.rs | 312 --------------------------------- src/pss/signing_key.rs | 6 +- tests/signing_blinding.rs | 164 +++++++++++++++++ 8 files changed, 243 insertions(+), 421 deletions(-) delete mode 100644 src/pss/blinded_signing_key.rs create mode 100644 tests/signing_blinding.rs diff --git a/src/key.rs b/src/key.rs index de565495..9f0e6881 100644 --- a/src/key.rs +++ b/src/key.rs @@ -21,6 +21,7 @@ use crate::algorithms::rsa::{ recover_primes, to_monty, }; +#[cfg(not(feature = "getrandom"))] use crate::dummy_rng::DummyRng; use crate::errors::{Error, Result}; use crate::traits::keys::{CrtValue, PrivateKeyParts, PublicKeyParts}; @@ -733,9 +734,20 @@ impl RsaPrivateKey { } /// Sign the given digest. + /// + /// With the `getrandom` feature (enabled by default), supplies the operating system RNG for + /// RSA blinding and any randomness required by the signature scheme. Without this feature, + /// use [`Self::sign_with_rng`] to supply an RNG explicitly. #[allow(clippy::missing_errors_doc, reason = "TODO")] pub fn sign(&self, padding: S, digest_in: &[u8]) -> Result> { - padding.sign(Option::<&mut DummyRng>::None, self, digest_in) + #[cfg(feature = "getrandom")] + { + padding.sign(Some(&mut crypto_common::getrandom::SysRng), self, digest_in) + } + #[cfg(not(feature = "getrandom"))] + { + padding.sign(Option::<&mut DummyRng>::None, self, digest_in) + } } /// Sign the given digest using the provided `rng`, which is used in the @@ -745,9 +757,8 @@ impl RsaPrivateKey { /// to mask the private key operation with random blinding, which helps /// mitigate sidechannel attacks. /// - [`Pss`][`crate::Pss`] always requires randomness. Use - /// [`Pss::new`][`crate::Pss::new`] for a standard RSASSA-PSS signature, or - /// [`Pss::new_blinded`][`crate::Pss::new_blinded`] for RSA-BSSA blind - /// signatures. + /// [`Pss::new`][`crate::Pss::new`] for a standard RSASSA-PSS signature with + /// RSA base blinding enabled by default. #[allow(clippy::missing_errors_doc, reason = "TODO")] pub fn sign_with_rng( &self, diff --git a/src/lib.rs b/src/lib.rs index e7211bd0..53ad8af9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,6 +235,7 @@ pub mod pkcs1v15; pub mod pss; pub mod traits; +#[cfg(not(feature = "getrandom"))] mod dummy_rng; mod encoding; mod key; diff --git a/src/pkcs1v15.rs b/src/pkcs1v15.rs index 1e14b992..8acfdb44 100644 --- a/src/pkcs1v15.rs +++ b/src/pkcs1v15.rs @@ -217,6 +217,27 @@ fn decrypt( Ok(em[k - length..].to_vec()) } +/// Sign with the default source of blinding randomness, when enabled. +fn sign_with_default_rng( + priv_key: &RsaPrivateKey, + prefix: &[u8], + hashed: &[u8], +) -> Result> { + #[cfg(feature = "getrandom")] + { + sign( + Some(&mut crypto_common::getrandom::SysRng), + priv_key, + prefix, + hashed, + ) + } + #[cfg(not(feature = "getrandom"))] + { + sign::(None, priv_key, prefix, hashed) + } +} + /// Calculates the signature of hashed using `RSASSA-PKCS1-V1_5-SIGN` from RSA PKCS#1 v1.5. /// Note that `hashed` must be the result of hashing the input message using the given hash /// function. If hash is `None`, hashed is signed directly. This isn't advisable except for diff --git a/src/pkcs1v15/signing_key.rs b/src/pkcs1v15/signing_key.rs index ce118248..5206f456 100644 --- a/src/pkcs1v15/signing_key.rs +++ b/src/pkcs1v15/signing_key.rs @@ -1,5 +1,5 @@ -use super::{Signature, VerifyingKey, pkcs1v15_generate_prefix, sign}; -use crate::{Result, RsaPrivateKey, dummy_rng::DummyRng}; +use super::{Signature, VerifyingKey, pkcs1v15_generate_prefix, sign, sign_with_default_rng}; +use crate::{Result, RsaPrivateKey}; use alloc::vec::Vec; use const_oid::AssociatedOid; use core::marker::PhantomData; @@ -28,6 +28,10 @@ use { /// Signing key for `RSASSA-PKCS1-v1_5` signatures as described in [RFC8017 § 8.2]. /// +/// With the `getrandom` feature (enabled by default), signing uses the operating system RNG for +/// RSA blinding. Without this feature, use the randomized signing traits to supply an RNG. +/// Blinding does not change the deterministic signature value. +/// /// [RFC8017 § 8.2]: https://datatracker.ietf.org/doc/html/rfc8017#section-8.2 #[derive(Debug, Clone)] pub struct SigningKey @@ -107,7 +111,7 @@ where ) -> signature::Result { let mut digest = D::default(); f(&mut digest)?; - sign::(None, &self.inner, &self.prefix, &digest.finalize_fixed())? + sign_with_default_rng(&self.inner, &self.prefix, &digest.finalize_fixed())? .as_slice() .try_into() } @@ -118,7 +122,7 @@ where D: Digest, { fn sign_prehash(&self, prehash: &[u8]) -> signature::Result { - sign::(None, &self.inner, &self.prefix, prehash)? + sign_with_default_rng(&self.inner, &self.prefix, prehash)? .as_slice() .try_into() } @@ -195,7 +199,7 @@ where fn try_multipart_sign(&self, msg: &[&[u8]]) -> signature::Result { let mut digest = D::new(); msg.iter().for_each(|slice| digest.update(slice)); - sign::(None, &self.inner, &self.prefix, &digest.finalize())? + sign_with_default_rng(&self.inner, &self.prefix, &digest.finalize())? .as_slice() .try_into() } diff --git a/src/pss.rs b/src/pss.rs index e139f5e8..7ffdfb56 100644 --- a/src/pss.rs +++ b/src/pss.rs @@ -9,15 +9,11 @@ //! [Probabilistic Signature Scheme]: https://en.wikipedia.org/wiki/Probabilistic_signature_scheme //! [RFC8017 § 8.1]: https://datatracker.ietf.org/doc/html/rfc8017#section-8.1 -mod blinded_signing_key; mod signature; mod signing_key; mod verifying_key; -pub use self::{ - blinded_signing_key::BlindedSigningKey, signature::Signature, signing_key::SigningKey, - verifying_key::VerifyingKey, -}; +pub use self::{signature::Signature, signing_key::SigningKey, verifying_key::VerifyingKey}; use alloc::vec::Vec; use core::fmt::{self, Debug}; @@ -33,6 +29,14 @@ use crate::traits::PublicKeyParts; use crate::traits::SignatureScheme; use crate::{RsaPrivateKey, RsaPublicKey}; +/// DEPRECATED: type alias for legacy blinded signing key. Blinding is now applied automatically +/// when available. +#[deprecated( + since = "0.10.0", + note = "use `SigningKey` which is blinded automatically" +)] +pub type BlindedSigningKey = SigningKey; + #[cfg(feature = "encoding")] use { crate::encoding::ID_RSASSA_PSS, @@ -46,7 +50,8 @@ use { /// Digital signatures using PSS padding. pub struct Pss { - /// Create blinded signatures. + /// Enable RSA base blinding for the private operation (enabled by default). + /// This does not change the RSASSA-PSS signature format. pub blinded: bool, /// Digest type to use. @@ -81,20 +86,20 @@ where #[must_use] pub fn new_with_salt(len: usize) -> Self { Self { - blinded: false, + blinded: true, digest: D::new(), salt_len: Some(len), } } - /// New PSS padding for blinded signatures (RSA-BSSA) for the given digest. + /// New PSS padding with RSA base blinding for the given digest. /// Digest output size is used as a salt length. #[must_use] pub fn new_blinded() -> Self { Self::new_blinded_with_salt(::output_size()) } - /// New PSS padding for blinded signatures (RSA-BSSA) for the given digest + /// New PSS padding with RSA base blinding for the given digest /// with a salt value of the given length. #[must_use] pub fn new_blinded_with_salt(len: usize) -> Self { @@ -309,7 +314,7 @@ where #[cfg(all(test, feature = "encoding"))] mod test { - use crate::pss::{BlindedSigningKey, Pss, Signature, SigningKey, VerifyingKey}; + use crate::pss::{Pss, Signature, SigningKey, VerifyingKey}; use crate::{RsaPrivateKey, RsaPublicKey}; use crate::traits::PublicKeyParts; @@ -509,21 +514,6 @@ tAboUGBxTDq3ZroNism3DaMIbKPyYrAqhKov1h5V .expect("failed to verify"); } - #[rstest] - #[case("test\n")] - fn test_sign_and_verify_roundtrip_blinded_signer(#[case] test: &str) { - let priv_key = get_private_key(); - - let mut rng = ChaCha8Rng::from_seed([42; 32]); - let signing_key = BlindedSigningKey::::new(priv_key); - let verifying_key = signing_key.verifying_key(); - - let sig = signing_key.sign_with_rng(&mut rng, test.as_bytes()); - verifying_key - .verify(test.as_bytes(), &sig) - .expect("failed to verify"); - } - #[rstest] #[case("test\n")] fn test_sign_and_verify_roundtrip_digest_signer(#[case] test: &str) { @@ -547,29 +537,6 @@ tAboUGBxTDq3ZroNism3DaMIbKPyYrAqhKov1h5V .expect("failed to verify"); } - #[rstest] - #[case("test\n")] - fn test_sign_and_verify_roundtrip_blinded_digest_signer(#[case] test: &str) { - let priv_key = get_private_key(); - - let mut rng = ChaCha8Rng::from_seed([42; 32]); - let signing_key = BlindedSigningKey::::new(priv_key); - let verifying_key = signing_key.verifying_key(); - - let sig = signing_key - .sign_digest_with_rng(&mut rng, |digest: &mut Sha1| digest.update(test.as_bytes())); - - verifying_key - .verify_digest( - |digest: &mut Sha1| { - digest.update(test.as_bytes()); - Ok(()) - }, - &sig, - ) - .expect("failed to verify"); - } - #[rstest] #[case( "test\n", @@ -622,24 +589,6 @@ tAboUGBxTDq3ZroNism3DaMIbKPyYrAqhKov1h5V .expect("failed to verify"); } - #[rstest] - #[case("test\n")] - fn test_sign_and_verify_pss_blinded_hazmat(#[case] test: &str) { - let priv_key = get_private_key(); - - let test = &Sha1::digest(test); - let mut rng = ChaCha8Rng::from_seed([42; 32]); - let signing_key = BlindedSigningKey::::new(priv_key); - let verifying_key = signing_key.verifying_key(); - - let sig = signing_key - .sign_prehash_with_rng(&mut rng, test) - .expect("failed to sign"); - verifying_key - .verify_prehash(test, &sig) - .expect("failed to verify"); - } - #[test] // Tests the corner case where the key is multiple of 8 + 1 bits long fn test_sign_and_verify_2049bit_key() { @@ -708,10 +657,6 @@ tAboUGBxTDq3ZroNism3DaMIbKPyYrAqhKov1h5V // would wrap to 0 let bad_key = SigningKey::::new_with_salt_len(priv_key.clone(), 256); assert!(bad_key.signature_algorithm_identifier().is_err()); - - // blinded key uses the same path - let bad_blinded = BlindedSigningKey::::new_with_salt_len(priv_key, 256); - assert!(bad_blinded.signature_algorithm_identifier().is_err()); } /// `signature_algorithm_identifier()` must advertise the configured salt length @@ -726,29 +671,20 @@ tAboUGBxTDq3ZroNism3DaMIbKPyYrAqhKov1h5V let priv_key = get_private_key(); let signing_key = SigningKey::::new_with_salt_len(priv_key.clone(), salt_len); - let blinded_signing_key = - BlindedSigningKey::::new_with_salt_len(priv_key.clone(), salt_len); - - let algo_ids = [ - signing_key - .signature_algorithm_identifier() - .expect("salt_len fits in a u8, so encoding must succeed"), - blinded_signing_key - .signature_algorithm_identifier() - .expect("salt_len fits in a u8, so encoding must succeed"), - ]; - for algo_id in &algo_ids { - let params_bytes = algo_id - .parameters - .as_ref() - .expect("RSA-PSS AlgorithmIdentifier carries parameters") - .to_der() - .expect("parameters are DER-encodable"); - let pss_params = - ::from_der(¶ms_bytes) - .expect("RSA-PSS parameters are DER-decodable"); - assert_eq!(pss_params.salt_len as usize, salt_len); - } + + let algo_id = signing_key + .signature_algorithm_identifier() + .expect("salt_len fits in a u8, so encoding must succeed"); + + let params_bytes = algo_id + .parameters + .as_ref() + .expect("RSA-PSS AlgorithmIdentifier carries parameters") + .to_der() + .expect("parameters are DER-encodable"); + let pss_params = ::from_der(¶ms_bytes) + .expect("RSA-PSS parameters are DER-decodable"); + assert_eq!(pss_params.salt_len as usize, salt_len); } #[rstest] @@ -757,16 +693,11 @@ tAboUGBxTDq3ZroNism3DaMIbKPyYrAqhKov1h5V #[case(usize::MAX)] fn test_pss_algo_id_salt_len_out_of_range(#[case] salt_len: usize) { let priv_key = get_private_key(); - let signing_key = SigningKey::::new_with_salt_len(priv_key.clone(), salt_len); - let blinded_signing_key = BlindedSigningKey::::new_with_salt_len(priv_key, salt_len); signing_key .signature_algorithm_identifier() .expect_err("salt_len overflows the u8 in RsaPssParams, so encoding must fail"); - blinded_signing_key - .signature_algorithm_identifier() - .expect_err("salt_len overflows the u8 in RsaPssParams, so encoding must fail"); } /// The digest-size default salt length is unchanged by the `u8` validation. diff --git a/src/pss/blinded_signing_key.rs b/src/pss/blinded_signing_key.rs deleted file mode 100644 index 7dcf54e1..00000000 --- a/src/pss/blinded_signing_key.rs +++ /dev/null @@ -1,312 +0,0 @@ -use super::{Signature, VerifyingKey, sign_digest}; -use crate::{Result, RsaPrivateKey}; -use core::marker::PhantomData; -use digest::{Digest, FixedOutputReset, HashMarker, Update}; -use rand_core::{CryptoRng, TryCryptoRng}; -use signature::{ - Keypair, RandomizedDigestSigner, RandomizedMultipartSigner, RandomizedSigner, - hazmat::RandomizedPrehashSigner, -}; -use zeroize::ZeroizeOnDrop; - -#[cfg(feature = "encoding")] -use { - super::get_pss_signature_algo_id, - const_oid::AssociatedOid, - pkcs8::{EncodePrivateKey, SecretDocument}, - spki::{ - AlgorithmIdentifierOwned, AlgorithmIdentifierRef, AssociatedAlgorithmIdentifier, - DynSignatureAlgorithmIdentifier, der::AnyRef, - }, -}; -#[cfg(feature = "serde")] -use { - pkcs8::DecodePrivateKey, - serdect::serde::{Deserialize, Serialize, de, ser}, -}; - -/// Signing key for producing "blinded" RSASSA-PSS signatures as described in -/// [draft-irtf-cfrg-rsa-blind-signatures](https://datatracker.ietf.org/doc/draft-irtf-cfrg-rsa-blind-signatures/). -#[derive(Debug, Clone)] -pub struct BlindedSigningKey -where - D: Digest, -{ - inner: RsaPrivateKey, - salt_len: usize, - phantom: PhantomData, -} - -impl BlindedSigningKey -where - D: Digest, -{ - /// Create a new RSASSA-PSS signing key which produces "blinded" - /// signatures. - /// Digest output size is used as a salt length. - #[must_use] - pub fn new(key: RsaPrivateKey) -> Self { - Self::new_with_salt_len(key, ::output_size()) - } - - /// Create a new RSASSA-PSS signing key which produces "blinded" - /// signatures with a salt of the given length. - #[must_use] - pub fn new_with_salt_len(key: RsaPrivateKey, salt_len: usize) -> Self { - Self { - inner: key, - salt_len, - phantom: Default::default(), - } - } - - /// Create a new random RSASSA-PSS signing key which produces "blinded" - /// signatures. - /// Digest output size is used as a salt length. - #[allow(clippy::missing_errors_doc, reason = "TODO")] - pub fn random(rng: &mut R, bit_size: usize) -> Result { - Self::random_with_salt_len(rng, bit_size, ::output_size()) - } - - /// Create a new random RSASSA-PSS signing key which produces "blinded" - /// signatures with a salt of the given length. - #[allow(clippy::missing_errors_doc, reason = "TODO")] - pub fn random_with_salt_len( - rng: &mut R, - bit_size: usize, - salt_len: usize, - ) -> Result { - Ok(Self { - inner: RsaPrivateKey::new(rng, bit_size)?, - salt_len, - phantom: Default::default(), - }) - } - - /// Return specified salt length for this key - #[must_use] - pub fn salt_len(&self) -> usize { - self.salt_len - } -} - -// -// `*Signer` trait impls -// - -impl RandomizedSigner for BlindedSigningKey -where - D: Digest + FixedOutputReset, -{ - fn try_sign_with_rng( - &self, - rng: &mut R, - msg: &[u8], - ) -> signature::Result { - self.try_multipart_sign_with_rng(rng, &[msg]) - } -} - -impl RandomizedMultipartSigner for BlindedSigningKey -where - D: Digest + FixedOutputReset, -{ - fn try_multipart_sign_with_rng( - &self, - rng: &mut R, - msg: &[&[u8]], - ) -> signature::Result { - let mut digest = D::new(); - msg.iter() - .for_each(|slice| ::update(&mut digest, slice)); - sign_digest::<_, D>(rng, true, &self.inner, &digest.finalize(), self.salt_len)? - .as_slice() - .try_into() - } -} - -impl RandomizedDigestSigner for BlindedSigningKey -where - D: Default + FixedOutputReset + HashMarker + Update, -{ - fn try_sign_digest_with_rng< - R: TryCryptoRng + ?Sized, - F: Fn(&mut D) -> signature::Result<()>, - >( - &self, - rng: &mut R, - f: F, - ) -> signature::Result { - let mut digest = D::default(); - f(&mut digest)?; - sign_digest::<_, D>(rng, true, &self.inner, &digest.finalize(), self.salt_len)? - .as_slice() - .try_into() - } -} - -impl RandomizedPrehashSigner for BlindedSigningKey -where - D: Digest + FixedOutputReset, -{ - fn sign_prehash_with_rng( - &self, - rng: &mut R, - prehash: &[u8], - ) -> signature::Result { - sign_digest::<_, D>(rng, true, &self.inner, prehash, self.salt_len)? - .as_slice() - .try_into() - } -} - -// -// Other trait impls -// - -impl AsRef for BlindedSigningKey -where - D: Digest, -{ - fn as_ref(&self) -> &RsaPrivateKey { - &self.inner - } -} - -#[cfg(feature = "encoding")] -impl AssociatedAlgorithmIdentifier for BlindedSigningKey -where - D: Digest, -{ - type Params = AnyRef<'static>; - - const ALGORITHM_IDENTIFIER: AlgorithmIdentifierRef<'static> = pkcs1::ALGORITHM_ID; -} - -#[cfg(feature = "encoding")] -impl DynSignatureAlgorithmIdentifier for BlindedSigningKey -where - D: Digest + AssociatedOid, -{ - fn signature_algorithm_identifier(&self) -> spki::Result { - get_pss_signature_algo_id::(self.salt_len) - } -} - -#[cfg(feature = "encoding")] -impl EncodePrivateKey for BlindedSigningKey -where - D: Digest, -{ - fn to_pkcs8_der(&self) -> pkcs8::Result { - self.inner.to_pkcs8_der() - } -} - -impl From for BlindedSigningKey -where - D: Digest, -{ - fn from(key: RsaPrivateKey) -> Self { - Self::new(key) - } -} - -impl From> for RsaPrivateKey -where - D: Digest, -{ - fn from(key: BlindedSigningKey) -> Self { - key.inner - } -} - -impl Keypair for BlindedSigningKey -where - D: Digest, -{ - type VerifyingKey = VerifyingKey; - fn verifying_key(&self) -> Self::VerifyingKey { - VerifyingKey { - inner: self.inner.to_public_key(), - salt_len: Some(self.salt_len), - phantom: Default::default(), - } - } -} - -#[cfg(feature = "encoding")] -impl TryFrom> for BlindedSigningKey -where - D: Digest + AssociatedOid, -{ - type Error = pkcs8::Error; - - fn try_from(private_key_info: pkcs8::PrivateKeyInfoRef<'_>) -> pkcs8::Result { - RsaPrivateKey::try_from(private_key_info).map(Self::new) - } -} - -impl ZeroizeOnDrop for BlindedSigningKey where D: Digest {} - -impl PartialEq for BlindedSigningKey -where - D: Digest, -{ - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner && self.salt_len == other.salt_len - } -} - -#[cfg(feature = "serde")] -impl Serialize for BlindedSigningKey -where - D: Digest, -{ - fn serialize(&self, serializer: S) -> core::result::Result - where - S: serde::Serializer, - { - let der = self.to_pkcs8_der().map_err(ser::Error::custom)?; - serdect::slice::serialize_hex_lower_or_bin(&der.as_bytes(), serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de, D> Deserialize<'de> for BlindedSigningKey -where - D: Digest + AssociatedOid, -{ - fn deserialize(deserializer: De) -> core::result::Result - where - De: serde::Deserializer<'de>, - { - let der_bytes = serdect::slice::deserialize_hex_or_bin_vec(deserializer)?; - Self::from_pkcs8_der(&der_bytes).map_err(de::Error::custom) - } -} - -#[cfg(test)] -mod tests { - #[test] - #[cfg(all(feature = "hazmat", feature = "serde"))] - fn test_serde() { - use super::*; - use rand::rngs::ChaCha8Rng; - use rand_core::SeedableRng; - use serde_test::{Configure, Token, assert_tokens}; - use sha2::Sha256; - - let mut rng = ChaCha8Rng::from_seed([42; 32]); - let signing_key = BlindedSigningKey::::new( - RsaPrivateKey::new_unchecked(&mut rng, 64).expect("failed to generate key"), - ); - - let tokens = [Token::Str(concat!( - "3056020100300d06092a864886f70d010101050004423040020100020900ab240c", - "3361d02e370203010001020811e54a15259d22f9020500ceff5cf3020500d3a7aa", - "ad020500ccaddf17020500cb529d3d020500bb526d6f" - ))]; - assert_tokens(&signing_key.readable(), &tokens); - } -} diff --git a/src/pss/signing_key.rs b/src/pss/signing_key.rs index bf079208..0b66fafc 100644 --- a/src/pss/signing_key.rs +++ b/src/pss/signing_key.rs @@ -35,6 +35,8 @@ use { /// Signing key for producing RSASSA-PSS signatures as described in /// [RFC8017 § 8.1]. /// +/// Signing uses the supplied RNG for both the salt and RSA base blinding. +/// /// [RFC8017 § 8.1]: https://datatracker.ietf.org/doc/html/rfc8017#section-8.1 #[derive(Debug, Clone)] pub struct SigningKey @@ -113,7 +115,7 @@ where ) -> signature::Result { let mut digest = D::new(); f(&mut digest)?; - sign_digest::<_, D>(rng, false, &self.inner, &digest.finalize(), self.salt_len)? + sign_digest::<_, D>(rng, true, &self.inner, &digest.finalize(), self.salt_len)? .as_slice() .try_into() } @@ -160,7 +162,7 @@ where rng: &mut R, prehash: &[u8], ) -> signature::Result { - sign_digest::<_, D>(rng, false, &self.inner, prehash, self.salt_len)? + sign_digest::<_, D>(rng, true, &self.inner, prehash, self.salt_len)? .as_slice() .try_into() } diff --git a/tests/signing_blinding.rs b/tests/signing_blinding.rs new file mode 100644 index 00000000..ea376107 --- /dev/null +++ b/tests/signing_blinding.rs @@ -0,0 +1,164 @@ +//! Default signing must pass blinding randomness through every signing API. +#![cfg(feature = "encoding")] + +use pkcs8::DecodePrivateKey; +use rand_core::{TryCryptoRng, TryRng}; +use rsa::{ + Error, Pkcs1v15Sign, Pss, RsaPrivateKey, RsaPublicKey, pkcs1v15, pss, traits::SignatureScheme, +}; +use sha2::{Digest, Sha256}; +use signature::{ + DigestSigner, MultipartSigner, RandomizedDigestSigner, RandomizedMultipartSigner, + RandomizedSigner, SignatureEncoding, Signer, + hazmat::{PrehashSigner, RandomizedPrehashSigner}, +}; + +fn key() -> RsaPrivateKey { + RsaPrivateKey::from_pkcs8_der(include_bytes!("examples/pkcs8/rsa2048-priv.der")) + .expect("valid fixture") +} + +#[derive(Debug)] +struct Exhausted; +impl core::fmt::Display for Exhausted { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("test entropy budget exhausted") + } +} +impl core::error::Error for Exhausted {} + +// Supplies the PSS salt, then fails when base blinding requests more randomness. +struct SaltOnlyRng(usize); +impl TryRng for SaltOnlyRng { + type Error = Exhausted; + fn try_next_u32(&mut self) -> Result { + let mut bytes = [0; 4]; + self.try_fill_bytes(&mut bytes)?; + Ok(u32::from_le_bytes(bytes)) + } + fn try_next_u64(&mut self) -> Result { + let mut bytes = [0; 8]; + self.try_fill_bytes(&mut bytes)?; + Ok(u64::from_le_bytes(bytes)) + } + fn try_fill_bytes(&mut self, bytes: &mut [u8]) -> Result<(), Self::Error> { + if bytes.len() > self.0 { + return Err(Exhausted); + } + self.0 -= bytes.len(); + bytes.fill(0); + Ok(()) + } +} +impl TryCryptoRng for SaltOnlyRng {} + +#[test] +fn pss_default_requests_blinding_randomness() { + let key = key(); + let digest = Sha256::digest(b"message"); + for salt_len in [0, 32] { + let padding = Pss::::new_with_salt(salt_len); + assert_eq!( + padding.sign(Some(&mut SaltOnlyRng(salt_len)), &key, &digest), + Err(Error::Rng) + ); + + // Control: the same salt, key and digest work when explicitly opting out of blinding. + let mut padding = Pss::::new_with_salt(salt_len); + padding.blinded = false; + let signature = padding + .sign(Some(&mut SaltOnlyRng(salt_len)), &key, &digest) + .expect("unblinded control"); + key.to_public_key() + .verify(Pss::::new_with_salt(salt_len), &digest, &signature) + .expect("valid control signature"); + + let signing = pss::SigningKey::::new_with_salt_len(key.clone(), salt_len); + assert!( + signing + .try_sign_with_rng(&mut SaltOnlyRng(salt_len), b"message") + .is_err() + ); + assert!( + signing + .try_multipart_sign_with_rng(&mut SaltOnlyRng(salt_len), &[b"mes", b"sage"]) + .is_err() + ); + assert!( + signing + .try_sign_digest_with_rng(&mut SaltOnlyRng(salt_len), |d| { + d.update(b"message"); + Ok(()) + }) + .is_err() + ); + assert!( + signing + .sign_prehash_with_rng(&mut SaltOnlyRng(salt_len), &digest) + .is_err() + ); + } +} + +#[test] +fn default_rng_reaches_signature_scheme() { + struct Probe; + impl SignatureScheme for Probe { + fn sign( + self, + rng: Option<&mut R>, + _: &RsaPrivateKey, + _: &[u8], + ) -> rsa::Result> { + assert_eq!(rng.is_some(), cfg!(feature = "getrandom")); + Ok(Vec::new()) + } + fn verify(self, _: &RsaPublicKey, _: &[u8], _: &[u8]) -> rsa::Result<()> { + unreachable!() + } + } + key().sign(Probe, b"").expect("default RNG dispatch"); +} + +#[test] +fn pkcs1v15_default_signatures_are_unchanged() { + let key = key(); + let digest = Sha256::digest(b"message"); + let expected = Pkcs1v15Sign::new::() + .sign::(None, &key, &digest) + .expect("unblinded reference"); + assert_eq!( + key.sign(Pkcs1v15Sign::new::(), &digest) + .expect("generic sign"), + expected + ); + let signing = pkcs1v15::SigningKey::::new(key); + assert_eq!( + signing.try_sign(b"message").expect("sign").to_vec(), + expected + ); + assert_eq!( + signing + .try_multipart_sign(&[b"mes", b"sage"]) + .expect("multipart sign") + .to_vec(), + expected + ); + assert_eq!( + signing + .sign_prehash(&digest) + .expect("prehash sign") + .to_vec(), + expected + ); + assert_eq!( + signing + .try_sign_digest(|d| { + d.update(b"message"); + Ok(()) + }) + .expect("digest sign") + .to_vec(), + expected + ); +}