diff --git a/Cargo.lock b/Cargo.lock index 48fef4e3..bda44bc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,6 +752,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -939,6 +950,8 @@ dependencies = [ "hmac", "k256", "log", + "num-bigint-dig", + "num-traits", "once_cell", "p256", "p384", @@ -948,6 +961,7 @@ dependencies = [ "rusb", "serde", "serde_json", + "sha1", "sha2", "signature", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 1134cf96..1952bd63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,14 +21,19 @@ aes = "0.8" bitflags = "2" cmac = "0.7" cbc = "0.1" +digest = { version = "0.10", default-features = false } ecdsa = { version = "0.16", default-features = false } ed25519 = "2" log = "0.4" +num-traits = "0.2" p256 = { version = "0.13", default-features = false, features = ["ecdsa"] } p384 = { version = "0.13", default-features = false, features = ["ecdsa"] } serde = { version = "1", features = ["serde_derive"] } rand_core = { version = "0.6", features = ["std"] } +rsa = "0.9.2" signature = { version = "2", features = ["derive"] } +sha1 = { version = "0.10", features = ["oid"] } +sha2 = { version = "0.10", features = ["oid"] } subtle = "2" thiserror = "1" time = { version = "0.3", features = ["serde"] } @@ -37,31 +42,29 @@ zeroize = { version = "1", features = ["zeroize_derive"] } # optional dependencies ccm = { version = "0.5", optional = true, features = ["std"] } -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"] } pbkdf2 = { version = "0.12", optional = true, default-features = false, features = ["hmac"] } serde_json = { version = "1", optional = true } rusb = { version = "0.9", optional = true } -sha2 = { version = "0.10", optional = true } tiny_http = { version = "0.12", optional = true } [dev-dependencies] ed25519-dalek = "2" +num-bigint = { version = "0.8.2", features = ["i128", "prime", "zeroize"], default-features = false, package = "num-bigint-dig" } once_cell = "1" p256 = { version = "0.13", features = ["ecdsa"] } -rsa = "0.9" [features] default = ["http", "passwords", "setup"] http-server = ["tiny_http"] http = [] -mockhsm = ["ccm", "digest", "ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"] -passwords = ["hmac", "pbkdf2", "sha2"] +mockhsm = ["ccm", "ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"] +passwords = ["hmac", "pbkdf2"] secp256k1 = ["k256"] setup = ["passwords", "serde_json", "uuid/serde"] -untested = ["sha2"] +untested = [] usb = ["rusb"] [package.metadata.docs.rs] diff --git a/src/asymmetric/algorithm.rs b/src/asymmetric/algorithm.rs index 6e4407e4..c0fd1d21 100644 --- a/src/asymmetric/algorithm.rs +++ b/src/asymmetric/algorithm.rs @@ -89,6 +89,14 @@ impl Algorithm { Algorithm::EcBp512 => 64, } } + + /// Return the size of the given key (as expected by the `YubiHSM 2`) in bytes + pub fn is_rsa(self) -> bool { + matches!( + self, + Algorithm::Rsa2048 | Algorithm::Rsa3072 | Algorithm::Rsa4096 + ) + } } impl_algorithm_serializers!(Algorithm); diff --git a/src/asymmetric/public_key.rs b/src/asymmetric/public_key.rs index 227a6634..09ab0175 100644 --- a/src/asymmetric/public_key.rs +++ b/src/asymmetric/public_key.rs @@ -5,6 +5,8 @@ use ::ecdsa::elliptic_curve::{ bigint::Integer, generic_array::GenericArray, point::PointCompression, sec1, FieldBytesSize, PrimeCurve, }; +use num_traits::FromPrimitive; +use rsa::{BigUint, RsaPublicKey}; use serde::{Deserialize, Serialize}; /// Response from `command::get_public_key` @@ -74,6 +76,20 @@ impl PublicKey { None } } + + /// Return the RSA public key + pub fn rsa(&self) -> Option { + if !self.algorithm.is_rsa() { + return None; + } + + const EXP: u64 = 65537; + + let modulus = BigUint::from_bytes_be(&self.bytes); + let exp = BigUint::from_u64(EXP).expect("invalid static exponent"); + + RsaPublicKey::new(modulus, exp).ok() + } } impl AsRef<[u8]> for PublicKey { diff --git a/src/authentication/key.rs b/src/authentication/key.rs index bd185148..013d4cfa 100644 --- a/src/authentication/key.rs +++ b/src/authentication/key.rs @@ -5,12 +5,12 @@ use rand_core::{OsRng, RngCore}; use std::fmt::{self, Debug}; use zeroize::Zeroize; +#[cfg(feature = "passwords")] +use sha2::Sha256; + #[cfg(feature = "pbkdf2")] use pbkdf2::pbkdf2_hmac; -#[cfg(feature = "sha2")] -use sha2::Sha256; - /// Auth keys are 2 * AES-128 keys pub const SIZE: usize = 32; diff --git a/src/client.rs b/src/client.rs index 038e63fd..0dae6bca 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(docsrs)] @@ -1019,14 +1016,10 @@ 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 +1027,64 @@ 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 02af4303..2eb27050 100644 --- a/src/mockhsm/object/payload.rs +++ b/src/mockhsm/object/payload.rs @@ -5,6 +5,7 @@ use crate::{algorithm::Algorithm, asymmetric, authentication, hmac, opaque, wrap use ecdsa::elliptic_curve::sec1::ToEncodedPoint; use ed25519_dalek as ed25519; use rand_core::{OsRng, RngCore}; +use rsa::traits::PublicKeyParts; /// Loaded instances of a cryptographic primitives in the MockHsm #[derive(Debug)] @@ -21,6 +22,9 @@ pub(crate) enum Payload { /// Ed25519 signing key Ed25519Key(ed25519::SigningKey), + /// Rsa private key + RsaKey(rsa::RsaPrivateKey), + /// HMAC key HmacKey(hmac::Algorithm, Vec), @@ -64,6 +68,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()]; @@ -80,6 +91,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:?}") } @@ -102,6 +116,12 @@ impl Payload { Payload::EcdsaNistP256(_) => Algorithm::Asymmetric(asymmetric::Algorithm::EcP256), Payload::EcdsaSecp256k1(_) => Algorithm::Asymmetric(asymmetric::Algorithm::EcK256), Payload::Ed25519Key(_) => Algorithm::Asymmetric(asymmetric::Algorithm::Ed25519), + Payload::RsaKey(ref k) => match k.size() { + 256 => Algorithm::Asymmetric(asymmetric::Algorithm::Rsa2048), + 384 => Algorithm::Asymmetric(asymmetric::Algorithm::Rsa3072), + 512 => Algorithm::Asymmetric(asymmetric::Algorithm::Rsa4096), + other => panic!("MockHsm doesn't support rsa key size {} bits", other * 8), + }, Payload::HmacKey(alg, _) => alg.into(), Payload::Opaque(alg, _) => alg.into(), Payload::WrapKey(alg, _) => alg.into(), @@ -114,6 +134,7 @@ impl Payload { Payload::AuthenticationKey(_) => authentication::key::SIZE, Payload::EcdsaNistP256(_) | Payload::EcdsaSecp256k1(_) => 32, Payload::Ed25519Key(_) => ed25519::SECRET_KEY_LENGTH, + Payload::RsaKey(k) => k.size(), Payload::HmacKey(_, ref data) => data.len(), Payload::Opaque(_, ref data) => data.len(), Payload::WrapKey(_, ref data) => data.len(), @@ -131,6 +152,7 @@ impl Payload { Some(secret_key.public_key().to_encoded_point(false).as_bytes()[1..].into()) } Payload::Ed25519Key(signing_key) => Some(signing_key.verifying_key().to_bytes().into()), + Payload::RsaKey(private_key) => Some(private_key.n().to_bytes_be()), _ => None, } } @@ -150,6 +172,34 @@ impl Payload { Payload::EcdsaNistP256(k) => k.to_bytes().to_vec(), Payload::EcdsaSecp256k1(k) => k.to_bytes().to_vec(), Payload::Ed25519Key(k) => k.verifying_key().to_bytes().into(), + Payload::RsaKey(k) => { + use rsa::traits::PrivateKeyParts; + let mut out = Vec::new(); + + { + let primes = k.primes(); + // p + out.extend_from_slice(&primes[0].to_bytes_be()); + // q + out.extend_from_slice(&primes[1].to_bytes_be()); + } + + // dp + if let Some(dp) = k.dp() { + out.extend_from_slice(&dp.to_bytes_be()) + } + // dq + if let Some(dq) = k.dq() { + out.extend_from_slice(&dq.to_bytes_be()) + } + // qinv + // Note(baloo): The sign is just dropped here. + if let Some(qinv) = k.qinv() { + out.extend_from_slice(&qinv.to_bytes_be().1) + } + + out + } Payload::HmacKey(_, data) => data.clone(), Payload::Opaque(_, data) => data.clone(), Payload::WrapKey(_, data) => data.clone(), 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..35d8f146 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 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..90c5c628 --- /dev/null +++ b/src/rsa/pkcs1/signer.rs @@ -0,0 +1,70 @@ +use crate::{object, rsa::SignatureAlgorithm, Client}; +use rsa::{ + pkcs1v15::{Signature, VerifyingKey}, + RsaPublicKey, +}; +use signature::Error; +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_pkcs1v15::(self.signing_key_id, msg)? + .as_slice() + .try_into() + } +} 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..d5fd67a6 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 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..56ee4ea7 --- /dev/null +++ b/src/rsa/pss/signer.rs @@ -0,0 +1,70 @@ +use crate::{object, rsa::SignatureAlgorithm, Client}; +use rsa::{ + pss::{Signature, VerifyingKey}, + RsaPublicKey, +}; +use signature::Error; +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() + } +} diff --git a/tests/integration.rs b/tests/integration.rs index a066d43e..c759fe6b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -13,6 +13,9 @@ mod ecdsa; /// Ed25519 tests mod ed25519; +/// RSA tests +mod rsa; + /// Cryptographic test vectors taken from standards documents mod test_vectors; diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs new file mode 100644 index 00000000..51d2195d --- /dev/null +++ b/tests/rsa/mod.rs @@ -0,0 +1,89 @@ +//! RSA (Rivest–Shamir–Adleman) asymmetric cryptosystem tests + +use signature::Verifier; +use yubihsm::{ + asymmetric::signature::Signer as _, + object, + rsa::{pkcs1, pss, SignatureAlgorithm}, + Capability, Client, +}; + +/// Domain IDs for test key +const TEST_SIGNING_KEY_DOMAINS: yubihsm::Domain = yubihsm::Domain::DOM1; + +/// Label for test key +const TEST_SIGNING_KEY_LABEL: &str = "Signatory test key"; + +/// 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 duplicates 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()); +}