From 71d743ce91a5e911dc84851def68e7ee1613ba38 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Sun, 5 Nov 2023 20:37:43 -0800 Subject: [PATCH] rsa: Adds a signer for PSS and PKCS1#1.5 This adds a mockhsm implementation and provides an integration test for both signers. --- Cargo.lock | 3 + Cargo.toml | 6 +- src/client.rs | 64 ++++++------ src/mockhsm/command.rs | 115 +++++++++++++++++++++- src/mockhsm/object/payload.rs | 10 ++ src/rsa.rs | 6 -- src/rsa/algorithm.rs | 25 ++++- src/rsa/pkcs1.rs | 5 +- src/rsa/pkcs1/commands.rs | 2 +- src/rsa/pkcs1/signature.rs | 7 ++ src/rsa/pkcs1/signer.rs | 87 ++++++++++++++++ src/rsa/pss.rs | 6 +- src/rsa/pss/commands.rs | 2 +- src/rsa/pss/signature.rs | 7 ++ src/rsa/pss/signer.rs | 91 +++++++++++++++++ tests/rsa/mod.rs | 180 +++++++++++++++++++++++++++++++++- 16 files changed, 564 insertions(+), 52 deletions(-) create mode 100644 src/rsa/pkcs1/signer.rs create mode 100644 src/rsa/pss/signer.rs diff --git a/Cargo.lock b/Cargo.lock index f41f2698..ec52c682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -697,6 +697,8 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", + "sha1", + "sha2", "signature", "spki", "subtle", @@ -1024,6 +1026,7 @@ dependencies = [ "rusb", "serde", "serde_json", + "sha1", "sha2", "signature", "spki", diff --git a/Cargo.toml b/Cargo.toml index 797c7b2a..e04dabf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ bitflags = "2" cmac = "0.7" cbc = "0.1" ccm = { version = "0.5", features = ["std"] } +digest = { version = "0.10", default-features = false } ecdsa = { version = "0.16", default-features = false, features = ["pkcs8"] } ed25519 = "2" log = "0.4" @@ -32,6 +33,7 @@ serde = { version = "1", features = ["serde_derive"] } rand_core = { version = "0.6", features = ["std"] } rsa = "0.9.6" signature = { version = "2", features = ["derive"] } +sha1 = { version = "0.10", features = ["oid"] } sha2 = { version = "0.10", features = ["oid"] } spki = { version = "0.7.3", default-features = false } subtle = "2" @@ -41,7 +43,6 @@ uuid = { version = "1", default-features = false } zeroize = { version = "1", features = ["zeroize_derive"] } # optional dependencies -digest = { version = "0.10", optional = true, default-features = false } ed25519-dalek = { version = "2", optional = true, features = ["rand_core"] } hmac = { version = "0.12", optional = true } k256 = { version = "0.13", optional = true, features = ["ecdsa", "sha256"] } @@ -53,6 +54,7 @@ tiny_http = { version = "0.12", optional = true } [dev-dependencies] ed25519-dalek = "2" once_cell = "1" +rsa = { version = "0.9.6", features = ["sha1", "sha2"] } p256 = { version = "0.13", features = ["ecdsa"] } x509-cert = { version = "0.2.5", features = ["builder"] } @@ -60,7 +62,7 @@ x509-cert = { version = "0.2.5", features = ["builder"] } default = ["http", "passwords", "setup"] http-server = ["tiny_http"] http = [] -mockhsm = ["digest", "ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"] +mockhsm = ["ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"] passwords = ["hmac", "pbkdf2"] secp256k1 = ["k256"] setup = ["passwords", "serde_json", "uuid/serde"] diff --git a/src/client.rs b/src/client.rs index c62b4db5..852dc42b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -28,13 +28,14 @@ use crate::{ object::{self, commands::*, generate}, opaque::{self, commands::*}, otp::{self, commands::*}, - rsa::{self, oaep::commands::*}, + rsa::{self, oaep::commands::*, pkcs1::commands::*, pss::commands::*, SignatureAlgorithm}, serialization::{deserialize, serialize}, session::{self, Session}, template::{commands::*, Template}, uuid, wrap::{self, commands::*}, }; +use sha2::Sha256; use std::{ sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -44,14 +45,10 @@ use std::{ use std::{thread, time::SystemTime}; #[cfg(feature = "untested")] -use { - crate::{ - algorithm::Algorithm, - ecdh::{self, commands::*}, - rsa::{pkcs1::commands::*, pss::commands::*}, - ssh::{self, commands::*}, - }, - sha2::{Digest, Sha256}, +use crate::{ + algorithm::Algorithm, + ecdh::{self, commands::*}, + ssh::{self, commands::*}, }; #[cfg(any(doc, docsrs))] @@ -1019,14 +1016,8 @@ impl Client { /// Compute an RSASSA-PKCS#1v1.5 signature of the SHA-256 hash of the given data. /// - /// **WARNING**: This functionality has not been tested and has not yet been - /// confirmed to actually work! USE AT YOUR OWN RISK! - /// - /// You will need to enable the `untested` cargo feature to use it. - /// /// - #[cfg(feature = "untested")] - pub fn sign_rsa_pkcs1v15_sha256( + pub(crate) fn sign_rsa_pkcs1v15( &self, key_id: object::Id, data: &[u8], @@ -1034,49 +1025,62 @@ impl Client { Ok(self .send_command(SignPkcs1Command { key_id, - digest: Sha256::digest(data).as_slice().into(), + digest: S::digest(data).as_slice().into(), })? .into()) } - /// Compute an RSASSA-PSS signature of the SHA-256 hash of the given data with the given key ID. - /// - /// **WARNING**: This functionality has not been tested and has not yet been - /// confirmed to actually work! USE AT YOUR OWN RISK! + /// Compute an RSASSA-PKCS#1v1.5 signature of the SHA-256 hash of the given data. /// - /// You will need to enable the `untested` cargo feature to use it. + /// + pub fn sign_rsa_pkcs1v15_sha256( + &self, + key_id: object::Id, + data: &[u8], + ) -> Result { + self.sign_rsa_pkcs1v15::(key_id, data) + } + + /// Compute an RSASSA-PSS signature of the SHA-256 hash of the given data with the given key ID. /// /// - #[cfg(feature = "untested")] - pub fn sign_rsa_pss_sha256( + pub(crate) fn sign_rsa_pss( &self, key_id: object::Id, data: &[u8], ) -> Result { ensure!( - data.len() > rsa::pss::MAX_MESSAGE_SIZE, + data.len() < rsa::pss::MAX_MESSAGE_SIZE, ErrorKind::ProtocolError, "message too large to be signed (max: {})", rsa::pss::MAX_MESSAGE_SIZE ); - let mut hasher = Sha256::default(); - - let length = data.len() as u16; - hasher.update(length.to_be_bytes()); + let mut hasher = S::new(); hasher.update(data); let digest = hasher.finalize(); Ok(self .send_command(SignPssCommand { key_id, - mgf1_hash_alg: rsa::mgf::Algorithm::Sha256, + mgf1_hash_alg: S::MGF_ALGORITHM, salt_len: digest.as_slice().len() as u16, digest: digest.as_slice().into(), })? .into()) } + /// Compute an RSASSA-PSS signature of the SHA-256 hash of the given data with the given key ID. + /// + /// + pub fn sign_rsa_pss_sha256( + &self, + key_id: object::Id, + data: &[u8], + ) -> Result { + self.sign_rsa_pss::(key_id, data) + } + /// Sign an SSH certificate using the given template. /// /// **WARNING**: This functionality has not been tested and has not yet been diff --git a/src/mockhsm/command.rs b/src/mockhsm/command.rs index 23088d0c..048b4eec 100644 --- a/src/mockhsm/command.rs +++ b/src/mockhsm/command.rs @@ -17,7 +17,7 @@ use crate::{ opaque::{self, commands::*}, otp, response::{self, Response}, - rsa, + rsa::{self, pkcs1::commands::*, pss::commands::*}, serialization::deserialize, session::{self, commands::*}, template, @@ -29,9 +29,18 @@ use ::ecdsa::{ hazmat::SignPrimitive, }; use ::hmac::{Hmac, Mac}; +use ::rsa::{pkcs1v15, pss, RsaPrivateKey}; +use digest::{ + const_oid::AssociatedOid, crypto_common::OutputSizeUser, typenum::Unsigned, Digest, + FixedOutputReset, +}; use rand_core::{OsRng, RngCore}; -use sha2::Sha256; -use signature::Signer; +use sha1::Sha1; +use sha2::{Sha256, Sha384, Sha512}; +use signature::{ + hazmat::{PrehashSigner, RandomizedPrehashSigner}, + Signer, +}; use std::{io::Cursor, str::FromStr}; use subtle::ConstantTimeEq; @@ -119,6 +128,8 @@ pub(crate) fn session_message( Code::SignEddsa => sign_eddsa(state, &command.data), Code::GetStorageInfo => get_storage_info(), Code::VerifyHmac => verify_hmac(state, &command.data), + Code::SignPss => sign_pss(state, &command.data), + Code::SignPkcs1 => sign_pkcs1v15(state, &command.data), unsupported => panic!("unsupported command type: {unsupported:?}"), }; @@ -707,6 +718,104 @@ fn sign_hmac(state: &State, cmd_data: &[u8]) -> response::Message { } } +/// Sign a message using the RSASSA-PSS signature algorithm +fn sign_pss(state: &State, cmd_data: &[u8]) -> response::Message { + #[inline] + fn sign_pss_digest( + private_key: &RsaPrivateKey, + msg: &[u8], + ) -> pss::Signature { + let signing_key = pss::SigningKey::::new(private_key.clone()); + signing_key + .sign_prehash_with_rng(&mut OsRng, msg) + .expect("unable to sign with prehash, wrong payload length?") + } + + let command: SignPssCommand = + deserialize(cmd_data).unwrap_or_else(|e| panic!("error parsing Code::SignPss: {e:?}")); + + if let Some(obj) = state + .objects + .get(command.key_id, object::Type::AsymmetricKey) + { + if let Payload::RsaKey(private_key) = &obj.payload { + let signature = match command.mgf1_hash_alg { + rsa::mgf::Algorithm::Sha1 => { + sign_pss_digest::(private_key, command.digest.as_ref()) + } + rsa::mgf::Algorithm::Sha256 => { + sign_pss_digest::(private_key, command.digest.as_ref()) + } + rsa::mgf::Algorithm::Sha384 => { + sign_pss_digest::(private_key, command.digest.as_ref()) + } + rsa::mgf::Algorithm::Sha512 => { + sign_pss_digest::(private_key, command.digest.as_ref()) + } + }; + + SignPssResponse((&signature).into()).serialize() + } else { + debug!("not an Rsa key: {:?}", obj.algorithm()); + device::ErrorKind::InvalidCommand.into() + } + } else { + debug!("no such object ID: {:?}", command.key_id); + device::ErrorKind::ObjectNotFound.into() + } +} + +/// Sign a message using the RSASSA-PKCS1-v1_5 signature algorithm +fn sign_pkcs1v15(state: &State, cmd_data: &[u8]) -> response::Message { + #[inline] + fn sign_pkcs1v15_prehash( + private_key: &RsaPrivateKey, + prehash: &[u8], + ) -> pkcs1v15::Signature { + let signing_key = pkcs1v15::SigningKey::::new(private_key.clone()); + signing_key + .sign_prehash(prehash) + .expect("unable to sign with prehash, wrong payload length?") + } + + let command: SignPkcs1Command = + deserialize(cmd_data).unwrap_or_else(|e| panic!("error parsing Code::SignPss: {e:?}")); + + if let Some(obj) = state + .objects + .get(command.key_id, object::Type::AsymmetricKey) + { + if let Payload::RsaKey(private_key) = &obj.payload { + let signature = match command.digest.len() { + len if len == ::OutputSize::USIZE => { + sign_pkcs1v15_prehash::(private_key, command.digest.as_ref()) + } + len if len == ::OutputSize::USIZE => { + sign_pkcs1v15_prehash::(private_key, command.digest.as_ref()) + } + len if len == ::OutputSize::USIZE => { + sign_pkcs1v15_prehash::(private_key, command.digest.as_ref()) + } + len if len == ::OutputSize::USIZE => { + sign_pkcs1v15_prehash::(private_key, command.digest.as_ref()) + } + len => { + debug!("invalid digest length: {}", len); + return device::ErrorKind::InvalidCommand.into(); + } + }; + + SignPkcs1Response((&signature).into()).serialize() + } else { + debug!("not an Rsa key: {:?}", obj.algorithm()); + device::ErrorKind::InvalidCommand.into() + } + } else { + debug!("no such object ID: {:?}", command.key_id); + device::ErrorKind::ObjectNotFound.into() + } +} + /// Verify the HMAC tag for the given data fn verify_hmac(state: &State, cmd_data: &[u8]) -> response::Message { let command: VerifyHmacCommand = diff --git a/src/mockhsm/object/payload.rs b/src/mockhsm/object/payload.rs index a6e9b5ac..721eb943 100644 --- a/src/mockhsm/object/payload.rs +++ b/src/mockhsm/object/payload.rs @@ -80,6 +80,13 @@ impl Payload { /// Generate a new key with the given algorithm pub fn generate(algorithm: Algorithm) -> Self { + fn gen_rsa(len: usize) -> Payload { + let private_key = + rsa::RsaPrivateKey::new(&mut OsRng, len).expect("failed to generate a key"); + + Payload::RsaKey(private_key) + } + match algorithm { Algorithm::Wrap(wrap_alg) => { let mut bytes = vec![0u8; wrap_alg.key_len()]; @@ -96,6 +103,9 @@ impl Payload { asymmetric::Algorithm::Ed25519 => { Payload::Ed25519Key(ed25519::SigningKey::generate(&mut OsRng)) } + asymmetric::Algorithm::Rsa2048 => gen_rsa(2048), + asymmetric::Algorithm::Rsa3072 => gen_rsa(3072), + asymmetric::Algorithm::Rsa4096 => gen_rsa(4096), _ => { panic!("MockHsm doesn't support this asymmetric algorithm: {asymmetric_alg:?}") } diff --git a/src/rsa.rs b/src/rsa.rs index a5011951..0fefc1f2 100644 --- a/src/rsa.rs +++ b/src/rsa.rs @@ -1,11 +1,5 @@ //! RSA (Rivest–Shamir–Adleman) asymmetric cryptosystem support //! (signing/encryption). -//! -//! NOTE: This functionality has not been properly tested and is therefore -//! not enabled by default! Enable the `untested` cargo feature if you would -//! like to use it (please report success or bugs!) - -// TODO(tarcieri): finalize and test RSA support mod algorithm; pub mod mgf; diff --git a/src/rsa/algorithm.rs b/src/rsa/algorithm.rs index 1d7084a4..dbd049eb 100644 --- a/src/rsa/algorithm.rs +++ b/src/rsa/algorithm.rs @@ -1,7 +1,8 @@ //! RSA-related algorithms -use super::{oaep, pkcs1, pss}; +use super::{mgf, oaep, pkcs1, pss}; use crate::algorithm; +use digest::{const_oid::AssociatedOid, Digest}; /// RSA algorithms (signing and encryption) #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -61,3 +62,25 @@ impl From for Algorithm { Algorithm::Pss(alg) } } + +/// [`SignatureAlgorithm`] marks the digest algorithm support for RSA signature (PSS or PKCS#1v1.5). +pub trait SignatureAlgorithm: Digest + AssociatedOid { + /// Mask Generation Function to use when talking to the YubiHSM. + const MGF_ALGORITHM: mgf::Algorithm; +} + +impl SignatureAlgorithm for sha1::Sha1 { + const MGF_ALGORITHM: mgf::Algorithm = mgf::Algorithm::Sha1; +} + +impl SignatureAlgorithm for sha2::Sha256 { + const MGF_ALGORITHM: mgf::Algorithm = mgf::Algorithm::Sha256; +} + +impl SignatureAlgorithm for sha2::Sha384 { + const MGF_ALGORITHM: mgf::Algorithm = mgf::Algorithm::Sha384; +} + +impl SignatureAlgorithm for sha2::Sha512 { + const MGF_ALGORITHM: mgf::Algorithm = mgf::Algorithm::Sha512; +} diff --git a/src/rsa/pkcs1.rs b/src/rsa/pkcs1.rs index e4be8628..4593a141 100644 --- a/src/rsa/pkcs1.rs +++ b/src/rsa/pkcs1.rs @@ -4,11 +4,10 @@ //! non-RSA algorithms like Ed25519 or ECDSA, or RSA-PSS if RSA is required. mod algorithm; -#[cfg(feature = "untested")] pub(crate) mod commands; -#[cfg(feature = "untested")] mod signature; +mod signer; pub use self::algorithm::Algorithm; -#[cfg(feature = "untested")] pub use self::signature::Signature; +pub use self::signer::Signer; diff --git a/src/rsa/pkcs1/commands.rs b/src/rsa/pkcs1/commands.rs index cdb12c09..c784889d 100644 --- a/src/rsa/pkcs1/commands.rs +++ b/src/rsa/pkcs1/commands.rs @@ -24,7 +24,7 @@ impl Command for SignPkcs1Command { /// RSASSA-PKCS#1v1.5 signatures (ASN.1 DER encoded) #[derive(Serialize, Deserialize, Debug)] -pub struct SignPkcs1Response(rsa::pkcs1::Signature); +pub struct SignPkcs1Response(pub(crate) rsa::pkcs1::Signature); impl Response for SignPkcs1Response { const COMMAND_CODE: command::Code = command::Code::SignPkcs1; diff --git a/src/rsa/pkcs1/signature.rs b/src/rsa/pkcs1/signature.rs index 6b60bf3b..b404c015 100644 --- a/src/rsa/pkcs1/signature.rs +++ b/src/rsa/pkcs1/signature.rs @@ -1,6 +1,7 @@ //! RSASSA-PKCS#1v1.5 signatures use serde::{Deserialize, Serialize}; +use signature::SignatureEncoding; /// RSASSA-PKCS#1v1.5 signatures (ASN.1 DER encoded) #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -35,3 +36,9 @@ impl Into> for Signature { self.0 } } + +impl From<&::rsa::pkcs1v15::Signature> for Signature { + fn from(s: &::rsa::pkcs1v15::Signature) -> Self { + Self(<::rsa::pkcs1v15::Signature as SignatureEncoding>::to_vec(s)) + } +} diff --git a/src/rsa/pkcs1/signer.rs b/src/rsa/pkcs1/signer.rs new file mode 100644 index 00000000..f7114451 --- /dev/null +++ b/src/rsa/pkcs1/signer.rs @@ -0,0 +1,87 @@ +use crate::{object, rsa::SignatureAlgorithm, Client}; +use rsa::{ + pkcs1v15::{RsaSignatureAssociatedOid, Signature, VerifyingKey}, + RsaPublicKey, +}; +use signature::Error; +use spki::{AlgorithmIdentifier, SignatureAlgorithmIdentifier}; +use std::marker::PhantomData; + +/// RSA signature provider for yubihsm-client +pub struct Signer +where + S: SignatureAlgorithm, +{ + /// YubiHSM client. + client: Client, + + /// ID of an ECDSA key to perform signatures with. + signing_key_id: object::Id, + + /// Verifying key which corresponds to this signer. + verifying_key: VerifyingKey, + + /// Algorithm used when signing messages + _algorithm: PhantomData, +} + +impl Signer +where + S: SignatureAlgorithm, +{ + /// Create a new YubiHSM-backed RSA-PSS signer + pub fn create(client: Client, signing_key_id: object::Id) -> Result { + let public_key = client + .get_public_key(signing_key_id)? + .rsa() + .ok_or_else(Error::new)?; + + let verifying_key = VerifyingKey::::new(public_key); + + Ok(Self { + client, + signing_key_id, + verifying_key, + _algorithm: PhantomData, + }) + } + + /// Return the RSA public key used by this signer + pub fn public_key(&self) -> RsaPublicKey { + let verifying_key = self.verifying_key.clone(); + verifying_key.into() + } +} + +impl signature::Signer for Signer +where + S: SignatureAlgorithm, +{ + fn try_sign(&self, msg: &[u8]) -> Result { + self.client + .sign_rsa_pkcs1v15::(self.signing_key_id, msg)? + .as_slice() + .try_into() + } +} + +impl signature::Keypair for Signer +where + S: SignatureAlgorithm, +{ + type VerifyingKey = VerifyingKey; + + fn verifying_key(&self) -> VerifyingKey { + self.verifying_key.clone() + } +} + +impl SignatureAlgorithmIdentifier for Signer +where + S: SignatureAlgorithm + RsaSignatureAssociatedOid, +{ + type Params = as SignatureAlgorithmIdentifier>::Params; + + const SIGNATURE_ALGORITHM_IDENTIFIER: AlgorithmIdentifier = + as SignatureAlgorithmIdentifier>::SIGNATURE_ALGORITHM_IDENTIFIER; +} diff --git a/src/rsa/pss.rs b/src/rsa/pss.rs index 516f5afd..dc6c4029 100644 --- a/src/rsa/pss.rs +++ b/src/rsa/pss.rs @@ -2,15 +2,13 @@ //! primitives with the EMSA-PSS encoding method. mod algorithm; -#[cfg(feature = "untested")] pub(crate) mod commands; -#[cfg(feature = "untested")] mod signature; +mod signer; /// Maximum message size supported for RSASSA-PSS -#[cfg(feature = "untested")] pub const MAX_MESSAGE_SIZE: usize = 0xFFFF; pub use self::algorithm::Algorithm; -#[cfg(feature = "untested")] pub use self::signature::Signature; +pub use self::signer::Signer; diff --git a/src/rsa/pss/commands.rs b/src/rsa/pss/commands.rs index 53d28a2a..2376784e 100644 --- a/src/rsa/pss/commands.rs +++ b/src/rsa/pss/commands.rs @@ -30,7 +30,7 @@ impl Command for SignPssCommand { /// RSASSA-PSS signatures (ASN.1 DER encoded) #[derive(Serialize, Deserialize, Debug)] -pub struct SignPssResponse(rsa::pss::Signature); +pub struct SignPssResponse(pub(crate) rsa::pss::Signature); impl Response for SignPssResponse { const COMMAND_CODE: command::Code = command::Code::SignPss; diff --git a/src/rsa/pss/signature.rs b/src/rsa/pss/signature.rs index 5ba62a60..ca57d354 100644 --- a/src/rsa/pss/signature.rs +++ b/src/rsa/pss/signature.rs @@ -1,6 +1,7 @@ //! RSA-PSS signatures use serde::{Deserialize, Serialize}; +use signature::SignatureEncoding; /// RSASSA-PSS signatures (ASN.1 DER encoded) #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -35,3 +36,9 @@ impl Into> for Signature { self.0 } } + +impl From<&::rsa::pss::Signature> for Signature { + fn from(s: &::rsa::pss::Signature) -> Self { + Self(<::rsa::pss::Signature as SignatureEncoding>::to_vec(s)) + } +} diff --git a/src/rsa/pss/signer.rs b/src/rsa/pss/signer.rs new file mode 100644 index 00000000..0e287a8b --- /dev/null +++ b/src/rsa/pss/signer.rs @@ -0,0 +1,91 @@ +use crate::{object, rsa::SignatureAlgorithm, Client}; +use rsa::{ + pss::{get_default_pss_signature_algo_id, Signature, VerifyingKey}, + RsaPublicKey, +}; +use signature::Error; +use spki::{der::oid::AssociatedOid, AlgorithmIdentifierOwned, DynSignatureAlgorithmIdentifier}; +use std::marker::PhantomData; + +/// RSA signature provider for yubihsm-client +pub struct Signer +where + S: SignatureAlgorithm, +{ + /// YubiHSM client. + client: Client, + + /// ID of an ECDSA key to perform signatures with. + signing_key_id: object::Id, + + /// Verifying key which corresponds to this signer. + verifying_key: VerifyingKey, + + /// Algorithm used when signing messages + _algorithm: PhantomData, +} + +impl Signer +where + S: SignatureAlgorithm, +{ + /// Create a new YubiHSM-backed RSA-PSS signer + pub fn create(client: Client, signing_key_id: object::Id) -> Result { + let public_key = client + .get_public_key(signing_key_id)? + .rsa() + .ok_or_else(Error::new)?; + + let verifying_key = VerifyingKey::::new(public_key); + + Ok(Self { + client, + signing_key_id, + verifying_key, + _algorithm: PhantomData, + }) + } + + /// Return the RSA public key used by this signer + pub fn public_key(&self) -> RsaPublicKey { + let verifying_key = self.verifying_key.clone(); + verifying_key.into() + } + + /// Return the RSASSA-PSS verifier attached to the key of this instance + pub fn verifying_key(&self) -> VerifyingKey { + self.verifying_key.clone() + } +} + +impl signature::Signer for Signer +where + S: SignatureAlgorithm, +{ + fn try_sign(&self, msg: &[u8]) -> Result { + self.client + .sign_rsa_pss::(self.signing_key_id, msg)? + .as_slice() + .try_into() + } +} + +impl signature::Keypair for Signer +where + S: SignatureAlgorithm, +{ + type VerifyingKey = VerifyingKey; + + fn verifying_key(&self) -> VerifyingKey { + self.verifying_key.clone() + } +} + +impl DynSignatureAlgorithmIdentifier for Signer +where + S: SignatureAlgorithm + AssociatedOid, +{ + fn signature_algorithm_identifier(&self) -> spki::Result { + get_default_pss_signature_algo_id::() + } +} diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 44cf85e4..aefceb51 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -5,7 +5,21 @@ use crate::{ TEST_KEY_LABEL, }; use ::rsa::{pkcs8::DecodePrivateKey, traits::PrivateKeyParts, RsaPrivateKey}; -use yubihsm::{object, wrap, Capability}; +use signature::{Keypair, Verifier}; +use spki::SubjectPublicKeyInfoOwned; +use std::{str::FromStr, time::Duration}; +use x509_cert::{ + builder::{Builder, CertificateBuilder, Profile}, + name::Name, + serial_number::SerialNumber, + time::Validity, +}; +use yubihsm::{ + asymmetric::signature::Signer as _, + object, + rsa::{pkcs1, pss, SignatureAlgorithm}, + wrap, Capability, Client, +}; /// Domain IDs for test key const TEST_SIGNING_KEY_DOMAINS: yubihsm::Domain = yubihsm::Domain::DOM1; @@ -95,3 +109,167 @@ fn rsa_import_wrapped_key() { assert_eq!(public, key.as_ref().clone()); } + +/// Example message to sign +const TEST_MESSAGE: &[u8] = + b"RSA (Rivest-Shamir-Adleman) is a public-key cryptosystem, one of the oldest, \ + that is widely used for secure data transmission."; + +fn create_pss_signer(key_id: object::Id) -> pss::Signer +where + S: SignatureAlgorithm, +{ + let client = crate::get_hsm_client(); + create_yubihsm_key(&client, key_id, yubihsm::asymmetric::Algorithm::Rsa2048); + pss::Signer::create(client.clone(), key_id).unwrap() +} + +fn create_pkcs_signer(key_id: object::Id) -> pkcs1::Signer +where + S: SignatureAlgorithm, +{ + let client = crate::get_hsm_client(); + create_yubihsm_key(&client, key_id, yubihsm::asymmetric::Algorithm::Rsa2048); + pkcs1::Signer::create(client.clone(), key_id).unwrap() +} + +/// Create the key on the YubiHSM to use for this test +// TODO(baloo): this is a duplicate from ecdsa tests +fn create_yubihsm_key(client: &Client, key_id: object::Id, alg: yubihsm::asymmetric::Algorithm) { + // Delete the key in TEST_KEY_ID slot it exists + // Ignore errors since the object may not exist yet + let _ = client.delete_object(key_id, yubihsm::object::Type::AsymmetricKey); + + // Create a new key for testing + let _key = client + .generate_asymmetric_key( + key_id, + TEST_SIGNING_KEY_LABEL.into(), + TEST_SIGNING_KEY_DOMAINS, + yubihsm::Capability::SIGN_PSS + | yubihsm::Capability::SIGN_PKCS + | Capability::EXPORTABLE_UNDER_WRAP, + alg, + ) + .unwrap(); +} + +#[test] +fn rsa_pss_sha256_sign_test() { + let signer = create_pss_signer::(221); + let verifying_key = signer.verifying_key(); + let verifying_key_from_public = + ::rsa::pss::VerifyingKey::::new(signer.public_key()); + + let signature = signer.sign(TEST_MESSAGE); + + assert!(verifying_key.verify(TEST_MESSAGE, &signature).is_ok()); + assert!(verifying_key_from_public + .verify(TEST_MESSAGE, &signature) + .is_ok()); +} + +#[test] +fn rsa_pkcs1_sha256_sign_test() { + let signer = create_pkcs_signer::(222); + let verifying_key = signer.verifying_key(); + let verifying_key_from_public = + ::rsa::pkcs1v15::VerifyingKey::::new(signer.public_key()); + + let signature = signer.sign(TEST_MESSAGE); + + assert!(verifying_key.verify(TEST_MESSAGE, &signature).is_ok()); + assert!(verifying_key_from_public + .verify(TEST_MESSAGE, &signature) + .is_ok()); +} + +#[test] +fn rsa_pss_sha1_ca() { + let signer = create_pss_signer::(223); + + let serial_number = SerialNumber::from(42u32); + let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); + let profile = Profile::Root; + let subject = + Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); + let pub_key = SubjectPublicKeyInfoOwned::from_key(signer.verifying_key()).unwrap(); + + let builder = + CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer) + .expect("Create certificate"); + + builder.build().unwrap(); +} + +#[test] +fn rsa_pss_sha256_ca() { + let signer = create_pss_signer::(224); + + let serial_number = SerialNumber::from(42u32); + let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); + let profile = Profile::Root; + let subject = + Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); + let pub_key = SubjectPublicKeyInfoOwned::from_key(signer.verifying_key()).unwrap(); + + let builder = + CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer) + .expect("Create certificate"); + + builder.build().unwrap(); +} + +#[test] +fn rsa_pkcs1_sha256_ca() { + let signer = create_pkcs_signer::(225); + + let serial_number = SerialNumber::from(42u32); + let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); + let profile = Profile::Root; + let subject = + Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); + let pub_key = SubjectPublicKeyInfoOwned::from_key(signer.verifying_key()).unwrap(); + + let builder = + CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer) + .expect("Create certificate"); + + builder.build().unwrap(); +} + +#[test] +fn rsa_raw_pkcs1_sha256_sign_test() { + let client = crate::get_hsm_client(); + create_yubihsm_key(&client, 226, yubihsm::asymmetric::Algorithm::Rsa2048); + + let signature = client + .sign_rsa_pkcs1v15_sha256(226, TEST_MESSAGE) + .expect("sign message"); + let public_key = client.get_public_key(226).unwrap().rsa().unwrap(); + let verifying_key = ::rsa::pkcs1v15::VerifyingKey::::new(public_key); + assert!(verifying_key + .verify( + TEST_MESSAGE, + &::rsa::pkcs1v15::Signature::try_from(signature.as_slice()).unwrap() + ) + .is_ok()); +} + +#[test] +fn rsa_raw_pss_sha256_sign_test() { + let client = crate::get_hsm_client(); + create_yubihsm_key(&client, 227, yubihsm::asymmetric::Algorithm::Rsa2048); + + let signature = client + .sign_rsa_pss_sha256(227, TEST_MESSAGE) + .expect("sign message"); + let public_key = client.get_public_key(227).unwrap().rsa().unwrap(); + let verifying_key = ::rsa::pss::VerifyingKey::::new(public_key); + assert!(verifying_key + .verify( + TEST_MESSAGE, + &::rsa::pss::Signature::try_from(signature.as_slice()).unwrap() + ) + .is_ok()); +}