From 8e57ce2fe9da744ece9dc487c8b7e0d36f8bd171 Mon Sep 17 00:00:00 2001 From: Hanson Char Date: Sat, 11 Nov 2023 16:34:38 -0800 Subject: [PATCH] Add support for signature verification using public key in X509 DER encoding format. This includes RSA, Ed25519 and EC. Specifically, the changes include: * Add signature::UnparsedPublicKey.new_with_x509() * Add verify_sig_with_x509_pubkey in addition to verify_sig * Add verify_ed25519_signature * Check ed25519 X509 public key prefix --- aws-lc-rs/Cargo.toml | 1 + aws-lc-rs/src/ec.rs | 124 +++++++++++++++++++++++++++++---- aws-lc-rs/src/ed25519.rs | 44 +++++++++++- aws-lc-rs/src/ptr.rs | 7 +- aws-lc-rs/src/public_key.rs | 2 +- aws-lc-rs/src/rsa.rs | 37 ++++++++++ aws-lc-rs/src/rsa/signature.rs | 18 +++++ aws-lc-rs/src/signature.rs | 62 ++++++++++++++--- 8 files changed, 268 insertions(+), 27 deletions(-) diff --git a/aws-lc-rs/Cargo.toml b/aws-lc-rs/Cargo.toml index 1d1cc61927..cdadc0180f 100644 --- a/aws-lc-rs/Cargo.toml +++ b/aws-lc-rs/Cargo.toml @@ -53,3 +53,4 @@ regex = "1.6.0" lazy_static = "1.4.0" clap = { version = "4.1.8", features = ["derive"] } hex = "0.4.3" +base64 = "0" diff --git a/aws-lc-rs/src/ec.rs b/aws-lc-rs/src/ec.rs index 0d08a509d9..b5436bf507 100644 --- a/aws-lc-rs/src/ec.rs +++ b/aws-lc-rs/src/ec.rs @@ -8,13 +8,18 @@ use std::fmt::{Debug, Formatter}; use std::mem::MaybeUninit; use std::ops::Deref; use std::os::raw::{c_int, c_uint}; + #[cfg(test)] use std::ptr::null; + use std::ptr::null_mut; #[cfg(feature = "ring-sig-verify")] use untrusted::Input; +use crate::fips::indicator_check; +use crate::signature::{Signature, VerificationAlgorithm}; +use crate::{digest, public_key, sealed}; #[cfg(feature = "fips")] use aws_lc::EC_KEY_check_fips; #[cfg(not(feature = "fips"))] @@ -22,9 +27,9 @@ use aws_lc::EC_KEY_check_key; #[cfg(test)] use aws_lc::EC_POINT_mul; use aws_lc::{ - point_conversion_form_t, BN_bn2bin_padded, BN_num_bytes, ECDSA_SIG_from_bytes, - ECDSA_SIG_get0_r, ECDSA_SIG_get0_s, ECDSA_SIG_new, ECDSA_SIG_set0, ECDSA_SIG_to_bytes, - EC_GROUP_get_curve_name, EC_GROUP_new_by_curve_name, EC_KEY_get0_group, + point_conversion_form_t, BIO_new, BIO_s_mem, BN_bn2bin_padded, BN_num_bytes, + ECDSA_SIG_from_bytes, ECDSA_SIG_get0_r, ECDSA_SIG_get0_s, ECDSA_SIG_new, ECDSA_SIG_set0, + ECDSA_SIG_to_bytes, EC_GROUP_get_curve_name, EC_GROUP_new_by_curve_name, EC_KEY_get0_group, EC_KEY_get0_private_key, EC_KEY_get0_public_key, EC_KEY_new, EC_KEY_set_group, EC_KEY_set_private_key, EC_KEY_set_public_key, EC_POINT_new, EC_POINT_oct2point, EC_POINT_point2oct, EVP_DigestVerify, EVP_DigestVerifyInit, EVP_PKEY_CTX_new_id, @@ -37,10 +42,12 @@ use crate::buffer::Buffer; use crate::digest::digest_ctx::DigestContext; use crate::encoding::AsDer; use crate::error::{KeyRejected, Unspecified}; -use crate::fips::indicator_check; +use crate::hex; use crate::ptr::{ConstPointer, DetachableLcPtr, LcPtr, Pointer}; -use crate::signature::{Signature, VerificationAlgorithm}; -use crate::{digest, hex, sealed}; +use aws_lc::d2i_PUBKEY_bio; +use aws_lc::BIO_write; + +use std::os::raw::c_void; pub(crate) mod key_pair; @@ -200,12 +207,48 @@ impl VerificationAlgorithm for EcdsaVerificationAlgorithm { signature: &[u8], ) -> Result<(), Unspecified> { match self.sig_format { - EcdsaSignatureFormat::ASN1 => { - verify_asn1_signature(self.id, self.digest, public_key, msg, signature) - } - EcdsaSignatureFormat::Fixed => { - verify_fixed_signature(self.id, self.digest, public_key, msg, signature) - } + EcdsaSignatureFormat::ASN1 => verify_asn1_signature( + self.id, + self.digest, + public_key, + &public_key::OCTET_STRING, + msg, + signature, + ), + EcdsaSignatureFormat::Fixed => verify_fixed_signature( + self.id, + self.digest, + public_key, + &public_key::OCTET_STRING, + msg, + signature, + ), + } + } + + fn verify_sig_with_x509_pubkey( + &self, + public_key: &[u8], + msg: &[u8], + signature: &[u8], + ) -> Result<(), crate::error::Unspecified> { + match self.sig_format { + EcdsaSignatureFormat::ASN1 => verify_asn1_signature( + self.id, + self.digest, + public_key, + &public_key::X509, + msg, + signature, + ), + EcdsaSignatureFormat::Fixed => verify_fixed_signature( + self.id, + self.digest, + public_key, + &public_key::X509, + msg, + signature, + ), } } } @@ -214,6 +257,7 @@ fn verify_fixed_signature( alg: &'static AlgorithmID, digest: &'static digest::Algorithm, public_key: &[u8], + encoding: &public_key::Encoding, msg: &[u8], signature: &[u8], ) -> Result<(), Unspecified> { @@ -227,17 +271,21 @@ fn verify_fixed_signature( } let out_bytes = LcPtr::new(out_bytes)?; let signature = unsafe { out_bytes.as_slice(out_bytes_len.assume_init()) }; - verify_asn1_signature(alg, digest, public_key, msg, signature) + verify_asn1_signature(alg, digest, public_key, encoding, msg, signature) } fn verify_asn1_signature( alg: &'static AlgorithmID, digest: &'static digest::Algorithm, public_key: &[u8], + encoding: &public_key::Encoding, msg: &[u8], signature: &[u8], ) -> Result<(), Unspecified> { - let pkey = evp_pkey_from_public_key(alg, public_key)?; + let pkey = match encoding.id { + public_key::EncodingID::OctetString => evp_pkey_from_public_key(alg, public_key)?, + public_key::EncodingID::X509 => evp_pkey_from_x509_pubkey(public_key)?, + }; let mut md_ctx = DigestContext::new_uninit(); @@ -382,6 +430,23 @@ pub(crate) unsafe fn evp_pkey_from_public_point( Ok(pkey) } +#[inline] +pub(crate) fn evp_pkey_from_x509_pubkey( + pubkey_data: &[u8], +) -> Result, Unspecified> { + // Create a memory BIO and write the public key data to it + let mem_bio = LcPtr::new(unsafe { BIO_new(BIO_s_mem()) })?; + let len = match c_int::try_from(pubkey_data.len()) { + Ok(len) => len, + Err(_) => return Err(Unspecified), + }; + if unsafe { BIO_write(*mem_bio, pubkey_data.as_ptr().cast::(), len) } <= 0 { + return Err(Unspecified); + } + // Use d2i_PUBKEY_bio to read the public key from the memory BIO + Ok(LcPtr::new(unsafe { d2i_PUBKEY_bio(*mem_bio, null_mut()) })?) +} + #[cfg(test)] pub(crate) unsafe fn evp_pkey_from_private( ec_group: &ConstPointer, @@ -589,6 +654,9 @@ mod tests { use crate::encoding::AsDer; use crate::signature::EcPublicKeyX509Der; use crate::signature::EcdsaKeyPair; + use base64::engine::general_purpose; + use base64::Engine; + use crate::signature::{KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}; use crate::test::from_dirty_hex; use crate::{signature, test}; @@ -651,4 +719,32 @@ mod tests { let actual_result = unparsed_pub_key.verify(msg.as_bytes(), &sig); assert!(actual_result.is_ok(), "Key: {}", test::to_hex(public_key)); } + + #[test] + fn test_p384_verify_with_x509_pubkey() { + let alg = &signature::ECDSA_P384_SHA384_ASN1; + let msg = "hello, world"; + + // Generated from bc-fips using "SHA384withECDSA" + let x509_pubkey_b64 = concat!( + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPhS06qHiqNSnyanUSHHMMebqu2h4Ho3oSwlNfLOtCXlIsm91", + "684Hor2X1b056aWGymprw8W6cXn/d6O2Y6x0FGu0uJnEEkvsIAwzu+stRHzgxuiky633R7zsSIfI+rsc", + ); + let x509_pubkey = general_purpose::STANDARD + .decode(x509_pubkey_b64) + .expect("Invalid base64 encoding"); + let unparsed_pub_key = + signature::UnparsedPublicKey::new_with_x509(alg, x509_pubkey.as_slice()); + + // Output from bc-fips + let sig_b64 = concat!( + "MGYCMQCBzjtJuLZol+KWCN6Tsv+FBENp1QpOlfVpOSlBB82LpGn3fMcQcAnopkrTwqJA0gICMQDfHqOr", + "Kebqy7qOj26odws7oROlIParYYl9FfLWGjIysipMk51ZbakcUVDVuDHu3QY=" + ); + let sig = general_purpose::STANDARD + .decode(sig_b64) + .expect("Invalid base64 encoding"); + let actual_result = unparsed_pub_key.verify(msg.as_bytes(), sig.as_slice()); + assert!(actual_result.is_ok(), "Key: {x509_pubkey_b64}"); + } } diff --git a/aws-lc-rs/src/ed25519.rs b/aws-lc-rs/src/ed25519.rs index 7804a6cfa8..79b989fe1b 100644 --- a/aws-lc-rs/src/ed25519.rs +++ b/aws-lc-rs/src/ed25519.rs @@ -35,6 +35,10 @@ pub(crate) const ED25519_PRIVATE_KEY_SEED_LEN: usize = aws_lc::ED25519_PRIVATE_KEY_SEED_LEN as usize; const ED25519_SIGNATURE_LEN: usize = aws_lc::ED25519_SIGNATURE_LEN as usize; const ED25519_SEED_LEN: usize = 32; +// Hex equivalent: 0x302a300506032b6570032100 +const ED25519_X509_PUBLIC_KEY_PREFIX: [u8; 12] = [48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]; +const ED25519_X509_PUBLIC_KEY_LEN: usize = + ED25519_X509_PUBLIC_KEY_PREFIX.len() + ED25519_PUBLIC_KEY_LEN; /// Parameters for `EdDSA` signing and verification. #[derive(Debug)] @@ -77,6 +81,21 @@ impl VerificationAlgorithm for EdDSAParameters { crate::fips::set_fips_service_status_unapproved(); Ok(()) } + + fn verify_sig_with_x509_pubkey( + &self, + public_key: &[u8], + msg: &[u8], + signature: &[u8], + ) -> Result<(), Unspecified> { + if public_key.len() == ED25519_X509_PUBLIC_KEY_LEN + && public_key[..12] == ED25519_X509_PUBLIC_KEY_PREFIX + { + self.verify_sig(&public_key[12..], msg, signature) + } else { + Err(Unspecified) + } + } } /// An Ed25519 key pair, for signing. @@ -419,9 +438,12 @@ impl Ed25519KeyPair { #[cfg(test)] mod tests { + use base64::engine::general_purpose; + use base64::Engine; + use crate::ed25519::Ed25519KeyPair; use crate::rand::SystemRandom; - use crate::test; + use crate::{signature, test}; #[test] fn test_generate_pkcs8() { @@ -486,4 +508,24 @@ mod tests { ); } } + + #[test] + fn verify_ed25519_signature() { + const MESSAGE: &[u8] = b"hello, world"; + // Generated using "ED25519" from BC-FIPS + let b64_x509_pubkey = "MCowBQYDK2VwAyEAo7urSMCwnkczfz1hxyj15uE+ja/nK0aOenoNCtqE+9Q="; + let b64_sig = "ZLiMhsOeHjGmnO+B3CzUe8B6Hl1O9j/aaDJqPU4lZZHtmxZxCQIN1lGqvFTP9c6AxRblTlftPK/oZF2xNM+5Ag=="; + let x509_pubkey = general_purpose::STANDARD + .decode(b64_x509_pubkey) + .expect("Invalid base64 encoding"); + let signature = general_purpose::STANDARD + .decode(b64_sig) + .expect("Invalid base64 encoding"); + // Verify the signature. + let public_key = + signature::UnparsedPublicKey::new_with_x509(&signature::ED25519, &x509_pubkey); + public_key + .verify(MESSAGE, &signature) + .expect("Signature verification failure"); + } } diff --git a/aws-lc-rs/src/ptr.rs b/aws-lc-rs/src/ptr.rs index 5103cc5f14..29f32e438e 100644 --- a/aws-lc-rs/src/ptr.rs +++ b/aws-lc-rs/src/ptr.rs @@ -4,9 +4,9 @@ use std::ops::Deref; use aws_lc::{ - BN_free, ECDSA_SIG_free, EC_GROUP_free, EC_KEY_free, EC_POINT_free, EVP_AEAD_CTX_free, - EVP_PKEY_CTX_free, EVP_PKEY_free, OPENSSL_free, RSA_free, BIGNUM, ECDSA_SIG, EC_GROUP, EC_KEY, - EC_POINT, EVP_AEAD_CTX, EVP_PKEY, EVP_PKEY_CTX, RSA, + BIO_free, BN_free, ECDSA_SIG_free, EC_GROUP_free, EC_KEY_free, EC_POINT_free, + EVP_AEAD_CTX_free, EVP_PKEY_CTX_free, EVP_PKEY_free, OPENSSL_free, RSA_free, BIGNUM, BIO, + ECDSA_SIG, EC_GROUP, EC_KEY, EC_POINT, EVP_AEAD_CTX, EVP_PKEY, EVP_PKEY_CTX, RSA, }; use mirai_annotations::verify_unreachable; @@ -201,6 +201,7 @@ macro_rules! create_pointer { // freed. This is different than functions of the same name in OpenSSL which generally do not zero // memory. create_pointer!(u8, OPENSSL_free); +create_pointer!(BIO, BIO_free); create_pointer!(EC_GROUP, EC_GROUP_free); create_pointer!(EC_POINT, EC_POINT_free); create_pointer!(EC_KEY, EC_KEY_free); diff --git a/aws-lc-rs/src/public_key.rs b/aws-lc-rs/src/public_key.rs index e73d0e0a71..0169a21038 100644 --- a/aws-lc-rs/src/public_key.rs +++ b/aws-lc-rs/src/public_key.rs @@ -22,7 +22,7 @@ pub(crate) enum EncodingID { /// Encoding of bytes #[derive(Debug, PartialEq, Eq)] -pub(crate) struct Encoding { +pub struct Encoding { /// Encoding ID pub(crate) id: EncodingID, } diff --git a/aws-lc-rs/src/rsa.rs b/aws-lc-rs/src/rsa.rs index e836d8b5e8..fb699c53d8 100644 --- a/aws-lc-rs/src/rsa.rs +++ b/aws-lc-rs/src/rsa.rs @@ -21,6 +21,10 @@ pub(crate) use self::signature::RsaVerificationAlgorithmId; #[cfg(test)] mod tests { + use base64::{engine::general_purpose, Engine}; + + use crate::signature; + #[cfg(feature = "ring-io")] #[test] fn test_rsa() { @@ -75,4 +79,37 @@ mod tests { format!("{:?}", signature::RSA_PSS_2048_8192_SHA256) ); } + + #[test] + fn verify_rsa_signature() { + const MESSAGE: &[u8] = b"hello, world"; + // Generated using "SHA384withRSA" from BC-FIPS + let b64_x509_pubkey = concat!( + "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzmMMJclDHPi+YvGUNjDtvGV+x40Fou5Nv0+c+uBH/xaO3+D", + "VDp/psg11UsA/haODPTNdFASqxu76/m1BLGmf+2MiLc1RNhvFJYTTJADsPtzObroBhXOhv1HYycB14JYFDFGJ0Bn/uf", + "tGyEn7wWHulcclTPRUg4F5i/MzzfiXimsn8sMme2hZXvlFvwjuSp6BGHCqu2DfXHY16BDZsEtK+DpABz8mGCN/D7ilc", + "8feSeyCc5wbFSbRgkJXRPYxuqg0icTQDCwx6OWdcNPRaQq5ty2HLG2PXmSDomtwsKPuAwvEqOy0BgZ0VjW0nCwMH1e/", + "jFvZqXDI7Sf9dWw3eMtc6jk9cTat+AdcvgtvFIEEcH9k48q+pFvfGQ8YBSbZBrYyCWZxPKuDOSuQHO682WX2mAsG1cY", + "Y+cLrsH2asqsCKCAwKcz43IucqqvEFqtnwpnkCIlGxqHWGbVrWTxmcC1qEM9Xkik2xJcXHGKmtMnoPgnsHiV7vVoglK", + "iDUfDyi5VTAgMBAAE="); + let b64_sig = concat!( + "y0NNWwHRLA0B2MvQ63QKcBYvlIx7gOxQXDh0IRdTryQ2pYuod3w1k5STGR3f+CTm83wp/MvBnCEEvnlxruK5Vrz3g1h", + "ThneUnZGwPo1MwCgvVLmR6RtoXy9+5M4BBcl/PqJTQcWo92RsbxEGVEVzsH6CNBAf4QDVW5ox1g5HjH4HsTutyezdE0", + "cmsHwz6IRiMeyuGWvs9sXECiIeripVpoHD7qz1TpvJPHA+wpvwDOUxJppk2naP5cVY+B0jRzTJ1TmX0IM1KMSrN/J5m", + "vkHm5TsyWkRBj6GD+4fsibSsF5W6vLSLJtQAk4dFpaafGcCgKsGSzpU605DeaHhrVOUnrAmLLCN5uIBRkGmekKowPkJ", + "AsdHP04nJ7QwQoIjxcaX9WJpPotcaqaKGdGf4xy2QIYRnhoRDq9DXfl6Wckqh6z4wz+R6gUr1wS3X5SiYvSgjEYQOYE", + "6A1xo+cpuPBpWuj9eL77QZBsn0rGwJSJNr4ds7gfIk7tYfkNcI/C7MFOh"); + let x509_pubkey = general_purpose::STANDARD + .decode(b64_x509_pubkey) + .expect("Invalid base64 encoding"); + let signature = general_purpose::STANDARD + .decode(b64_sig) + .expect("Invalid base64 encoding"); + // Verify the signature. + let public_key = signature::UnparsedPublicKey::new_with_x509( + &signature::RSA_PKCS1_2048_8192_SHA384, + &x509_pubkey, + ); + public_key.verify(MESSAGE, &signature).unwrap(); + } } diff --git a/aws-lc-rs/src/rsa/signature.rs b/aws-lc-rs/src/rsa/signature.rs index f0420232af..41ff354d5b 100644 --- a/aws-lc-rs/src/rsa/signature.rs +++ b/aws-lc-rs/src/rsa/signature.rs @@ -20,6 +20,7 @@ use crate::{ error::Unspecified, fips::indicator_check, ptr::{ConstPointer, DetachableLcPtr, LcPtr}, + public_key::evp_pkey_from_x509_pubkey, sealed::Sealed, signature::VerificationAlgorithm, }; @@ -93,6 +94,23 @@ impl VerificationAlgorithm for RsaParameters { ) } } + + fn verify_sig_with_x509_pubkey( + &self, + public_key: &[u8], + msg: &[u8], + signature: &[u8], + ) -> Result<(), Unspecified> { + let rsa = evp_pkey_from_x509_pubkey(public_key)?; + verify_RSA( + self.digest_algorithm(), + self.padding(), + &rsa, + msg, + signature, + self.bit_size_range(), + ) + } } impl Sealed for RsaParameters {} diff --git a/aws-lc-rs/src/signature.rs b/aws-lc-rs/src/signature.rs index 67c92c4646..b52e0c47f7 100644 --- a/aws-lc-rs/src/signature.rs +++ b/aws-lc-rs/src/signature.rs @@ -250,9 +250,12 @@ pub use crate::rsa::{ KeyPair as RsaKeyPair, PublicKey as RsaSubjectPublicKey, RsaParameters, }; -use crate::rsa::{ - signature::{RsaSignatureEncoding, RsaSigningAlgorithmId}, - RsaVerificationAlgorithmId, +use crate::{ + public_key, + rsa::{ + signature::{RsaSignatureEncoding, RsaSigningAlgorithmId}, + RsaVerificationAlgorithmId, + }, }; pub use crate::ec::key_pair::{EcdsaKeyPair, PrivateKey as EcdsaPrivateKey}; @@ -335,7 +338,7 @@ pub trait VerificationAlgorithm: Debug + Sync + sealed::Sealed { ) -> Result<(), error::Unspecified>; /// Verify the signature `signature` of message `msg` with the public key - /// `public_key`. + /// `public_key` in Octet String encoding. /// // # FIPS // The following conditions must be met: @@ -351,6 +354,24 @@ pub trait VerificationAlgorithm: Debug + Sync + sealed::Sealed { msg: &[u8], signature: &[u8], ) -> Result<(), error::Unspecified>; + + /// Verify the signature `signature` of message `msg` with the public key + /// `public_key` in X509 DER encoding. + /// + // # FIPS + // The following conditions must be met: + // * RSA Key Sizes: 1024, 2048, 3072, 4096 + // * NIST Elliptic Curves: P256, P384, P521 + // * Digest Algorithms: SHA1, SHA256, SHA384, SHA512 + // + /// # Errors + /// `error::Unspecified` if inputs not verified. + fn verify_sig_with_x509_pubkey( + &self, + public_key: &[u8], + msg: &[u8], + signature: &[u8], + ) -> Result<(), error::Unspecified>; } /// An unparsed, possibly malformed, public key for signature verification. @@ -358,6 +379,7 @@ pub trait VerificationAlgorithm: Debug + Sync + sealed::Sealed { pub struct UnparsedPublicKey> { algorithm: &'static dyn VerificationAlgorithm, bytes: B, + encoding: &'static public_key::Encoding, } impl> Copy for UnparsedPublicKey {} @@ -373,12 +395,28 @@ impl> Debug for UnparsedPublicKey { } impl> UnparsedPublicKey { - /// Construct a new `UnparsedPublicKey`. + /// Construct a new `UnparsedPublicKey` with [`public_key::OCTET_STRING`] encoding. /// /// No validation of `bytes` is done until `verify()` is called. #[inline] pub fn new(algorithm: &'static dyn VerificationAlgorithm, bytes: B) -> Self { - Self { algorithm, bytes } + Self { + algorithm, + bytes, + encoding: &public_key::OCTET_STRING, + } + } + + /// Construct a new `UnparsedPublicKey` with [`public_key::X509`] encoding. + /// + /// No validation of `bytes` is done until `verify()` is called. + #[inline] + pub fn new_with_x509(algorithm: &'static dyn VerificationAlgorithm, bytes: B) -> Self { + Self { + algorithm, + bytes, + encoding: &public_key::X509, + } } /// Parses the public key and verifies `signature` is a valid signature of @@ -396,8 +434,16 @@ impl> UnparsedPublicKey { /// `error::Unspecified` if inputs not verified. #[inline] pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), error::Unspecified> { - self.algorithm - .verify_sig(self.bytes.as_ref(), message, signature) + match &self.encoding.id { + public_key::EncodingID::OctetString => { + self.algorithm + .verify_sig(self.bytes.as_ref(), message, signature) + } + public_key::EncodingID::X509 => { + self.algorithm + .verify_sig_with_x509_pubkey(self.bytes.as_ref(), message, signature) + } + } } }