diff --git a/openssl/src/lib.rs b/openssl/src/lib.rs index 5ae46337e..e1afb7797 100644 --- a/openssl/src/lib.rs +++ b/openssl/src/lib.rs @@ -185,12 +185,16 @@ pub mod pkcs5; pub mod pkcs7; pub mod pkey; pub mod pkey_ctx; +#[cfg(ossl350)] +pub mod pkey_ml_dsa; #[cfg(ossl300)] pub mod provider; pub mod rand; pub mod rsa; pub mod sha; pub mod sign; +#[cfg(ossl300)] +pub mod signature; pub mod srtp; pub mod ssl; pub mod stack; diff --git a/openssl/src/pkey_ctx.rs b/openssl/src/pkey_ctx.rs index 1b58108ae..c8eef20a1 100644 --- a/openssl/src/pkey_ctx.rs +++ b/openssl/src/pkey_ctx.rs @@ -68,6 +68,8 @@ use crate::bn::BigNumRef; #[cfg(not(any(boringssl, awslc)))] use crate::cipher::CipherRef; use crate::error::ErrorStack; +#[cfg(ossl300)] +use crate::lib_ctx::LibCtxRef; use crate::md::MdRef; use crate::nid::Nid; use crate::pkey::{HasPrivate, HasPublic, Id, PKey, PKeyRef, Params, Private}; @@ -84,6 +86,8 @@ use openssl_macros::corresponds; use std::convert::TryFrom; #[cfg(ossl320)] use std::ffi::CStr; +#[cfg(ossl300)] +use std::ffi::CString; use std::ptr; /// HKDF modes of operation. @@ -159,6 +163,26 @@ impl PkeyCtx<()> { Ok(PkeyCtx::from_ptr(ptr)) } } + + /// Creates a new pkey context from the algorithm name. + #[corresponds(EVP_PKEY_CTX_new_from_name)] + #[cfg(ossl300)] + pub fn new_from_name( + libctx: Option<&LibCtxRef>, + name: &str, + propquery: Option<&str>, + ) -> Result { + unsafe { + let propquery = propquery.map(|s| CString::new(s).unwrap()); + let name = CString::new(name).unwrap(); + let ptr = cvt_p(ffi::EVP_PKEY_CTX_new_from_name( + libctx.map_or(ptr::null_mut(), ForeignTypeRef::as_ptr), + name.as_ptr(), + propquery.map_or(ptr::null_mut(), |s| s.as_ptr()), + ))?; + Ok(PkeyCtx::from_ptr(ptr)) + } + } } impl PkeyCtxRef @@ -187,6 +211,26 @@ where Ok(()) } + /// Prepares the context for signature verification over a message + /// using the public key. + #[cfg(ossl340)] + #[corresponds(EVP_PKEY_verify_message_init)] + #[inline] + pub fn verify_message_init( + &mut self, + algo: &mut crate::signature::Signature, + ) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::EVP_PKEY_verify_message_init( + self.as_ptr(), + algo.as_ptr(), + ptr::null(), + ))?; + } + + Ok(()) + } + /// Prepares the context for signature recovery using the public key. #[corresponds(EVP_PKEY_verify_recover_init)] #[inline] @@ -319,6 +363,25 @@ where Ok(()) } + /// Prepares the context for signing a message using the private key. + #[cfg(ossl340)] + #[corresponds(EVP_PKEY_sign_message_init)] + #[inline] + pub fn sign_message_init( + &mut self, + algo: &mut crate::signature::Signature, + ) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::EVP_PKEY_sign_message_init( + self.as_ptr(), + algo.as_ptr(), + ptr::null(), + ))?; + } + + Ok(()) + } + /// Sets the peer key used for secret derivation. #[corresponds(EVP_PKEY_derive_set_peer)] pub fn derive_set_peer(&mut self, key: &PKeyRef) -> Result<(), ErrorStack> @@ -889,6 +952,19 @@ impl PkeyCtxRef { Ok(()) } + /// Performs the generation operation and returns the resulting + /// key parameters or key. + #[corresponds(EVP_PKEY_generate)] + #[cfg(ossl300)] + #[inline] + pub fn generate(&mut self) -> Result, ErrorStack> { + unsafe { + let mut key = ptr::null_mut(); + cvt(ffi::EVP_PKEY_generate(self.as_ptr(), &mut key))?; + Ok(PKey::from_ptr(key)) + } + } + /// Gets the nonce type for a private key context. /// /// The nonce for DSA and ECDSA can be either random (the default) or deterministic (as defined by RFC 6979). @@ -913,6 +989,14 @@ impl PkeyCtxRef { } Ok(NonceType(nonce_type)) } + + /// Initializes a conversion from `OsslParam` to `PKey` on given `PkeyCtx`. + #[corresponds(EVP_PKEY_fromdata_init)] + #[cfg(ossl300)] + pub fn fromdata_init(&mut self) -> Result<(), ErrorStack> { + unsafe { cvt(ffi::EVP_PKEY_fromdata_init(self.as_ptr()))? }; + Ok(()) + } } #[cfg(test)] diff --git a/openssl/src/pkey_ml_dsa.rs b/openssl/src/pkey_ml_dsa.rs new file mode 100644 index 000000000..638d6778d --- /dev/null +++ b/openssl/src/pkey_ml_dsa.rs @@ -0,0 +1,254 @@ +//! Module-Lattice-Based Digital Signatures. +//! +//! ML-DSA is a signature algorithm that is believed to be secure +//! against adversaries with quantum computers. It has been +//! standardized by NIST as [FIPS 204]. +//! +//! [FIPS 204]: https://csrc.nist.gov/pubs/fips/204/final + +use crate::error::ErrorStack; +use crate::ossl_param::{OsslParamArray, OsslParamBuilder}; +use crate::pkey::{PKey, Private, Public}; +use crate::pkey_ctx::PkeyCtx; +use crate::{cvt, cvt_p}; +use foreign_types::ForeignType; +use libc::c_int; +use openssl_macros::corresponds; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::ptr; + +// Safety: these all have null terminators. +// We cen remove these CStr::from_bytes_with_nul_unchecked calls +// when we upgrade to Rust 1.77+ with literal c"" syntax. + +const OSSL_PKEY_PARAM_SEED: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"seed\0") }; +const OSSL_PKEY_PARAM_PUB_KEY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"pub\0") }; +const OSSL_PKEY_PARAM_PRIV_KEY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"priv\0") }; +const MLDSA44_CSTR: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"ML-DSA-44\0") }; +const MLDSA65_CSTR: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"ML-DSA-65\0") }; +const MLDSA87_CSTR: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"ML-DSA-87\0") }; + +const MLDSA44_STR: &str = "ML-DSA-44"; +const MLDSA65_STR: &str = "ML-DSA-65"; +const MLDSA87_STR: &str = "ML-DSA-87"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Variant { + MlDsa44, + MlDsa65, + MlDsa87, +} + +impl Variant { + pub(crate) fn as_str(&self) -> &'static str { + match self { + Variant::MlDsa44 => MLDSA44_STR, + Variant::MlDsa65 => MLDSA65_STR, + Variant::MlDsa87 => MLDSA87_STR, + } + } + + pub(crate) fn as_cstr(&self) -> &'static CStr { + match self { + Variant::MlDsa44 => MLDSA44_CSTR, + Variant::MlDsa65 => MLDSA65_CSTR, + Variant::MlDsa87 => MLDSA87_CSTR, + } + } +} + +/// Generate an ML-DSA PKey. +#[corresponds(EVP_PKEY_fromdata)] +pub fn new_generate(variant: Variant) -> Result, ErrorStack> { + let mut ctx = PkeyCtx::new_from_name(None, variant.as_str(), None)?; + ctx.keygen_init()?; + let params = OsslParamBuilder::new()?.to_param()?; + unsafe { + cvt(ffi::EVP_PKEY_CTX_set_params(ctx.as_ptr(), params.as_ptr()))?; + } + ctx.generate() +} + +/// Returns the Private ML-DSA PKey from the provided parameters. +/// Creates a new `PKeyMlDsaBuilder` to generate a new ML-DSA key +/// pair. +#[corresponds(EVP_PKEY_fromdata)] +pub fn new_from_seed(variant: Variant, seed: &[u8]) -> Result, ErrorStack> { + let mut bld = OsslParamBuilder::new()?; + bld.add_octet_string(OSSL_PKEY_PARAM_SEED, seed)?; + let mut ctx = PkeyCtx::new_from_name(None, variant.as_str(), None)?; + ctx.fromdata_init()?; + let params = bld.to_param()?; + unsafe { + let evp = cvt_p(ffi::EVP_PKEY_new())?; + let pkey = PKey::from_ptr(evp); + cvt(ffi::EVP_PKEY_fromdata( + ctx.as_ptr(), + &mut pkey.as_ptr(), + ffi::EVP_PKEY_KEYPAIR, + params.as_ptr(), + ))?; + Ok(pkey) + } +} + +/// Returns the Private ML-DSA PKey from the provided parameters. +/// Creates a new `PKeyMlDsaBuilder` to generate a new ML-DSA key +/// pair. +#[corresponds(EVP_PKEY_fromdata)] +pub fn new_from_public(variant: Variant, public: &[u8]) -> Result, ErrorStack> { + let mut bld = OsslParamBuilder::new()?; + bld.add_octet_string(OSSL_PKEY_PARAM_PUB_KEY, public)?; + let mut ctx = PkeyCtx::new_from_name(None, variant.as_str(), None)?; + ctx.fromdata_init()?; + let params = bld.to_param()?; + unsafe { + let evp = cvt_p(ffi::EVP_PKEY_new())?; + let pkey = PKey::from_ptr(evp); + cvt(ffi::EVP_PKEY_fromdata( + ctx.as_ptr(), + &mut pkey.as_ptr(), + ffi::EVP_PKEY_PUBLIC_KEY, + params.as_ptr(), + ))?; + Ok(pkey) + } +} + +pub struct PKeyMlDsaParams { + params: OsslParamArray, + _m: PhantomData, +} + +impl PKeyMlDsaParams { + /// Creates a new `PKeyMlDsaParams` from existing ML-DSA PKey. Internal. + #[corresponds(EVP_PKEY_todata)] + fn _new_from_pkey( + pkey: &PKey, + selection: c_int, + ) -> Result, ErrorStack> { + unsafe { + let mut params: *mut ffi::OSSL_PARAM = ptr::null_mut(); + cvt(ffi::EVP_PKEY_todata(pkey.as_ptr(), selection, &mut params))?; + Ok(PKeyMlDsaParams:: { + params: OsslParamArray::from_ptr(params), + _m: PhantomData, + }) + } + } +} + +impl PKeyMlDsaParams { + /// Creates a new `PKeyMlDsaParams` from existing Public ML-DSA PKey. + #[corresponds(EVP_PKEY_todata)] + pub fn from_pkey(pkey: &PKey) -> Result, ErrorStack> { + Self::_new_from_pkey(pkey, ffi::EVP_PKEY_PUBLIC_KEY) + } + + /// Returns a reference to the public key. + pub fn public_key(&self) -> Result<&[u8], ErrorStack> { + self.params.locate_octet_string(OSSL_PKEY_PARAM_PUB_KEY) + } +} + +impl PKeyMlDsaParams { + /// Creates a new `PKeyMlDsaParams` from existing Private ML-DSA PKey. + #[corresponds(EVP_PKEY_todata)] + pub fn from_pkey(pkey: &PKey) -> Result, ErrorStack> { + Self::_new_from_pkey(pkey, ffi::EVP_PKEY_KEYPAIR) + } + + /// Returns the private key seed. + pub fn private_key_seed(&self) -> Result<&[u8], ErrorStack> { + self.params.locate_octet_string(OSSL_PKEY_PARAM_SEED) + } + + /// Returns the private key. + pub fn private_key(&self) -> Result<&[u8], ErrorStack> { + self.params.locate_octet_string(OSSL_PKEY_PARAM_PRIV_KEY) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::signature::Signature; + + #[test] + fn test_generate_ml_dsa_44() { + test_generate(Variant::MlDsa44); + } + + #[test] + fn test_generate_ml_dsa_65() { + test_generate(Variant::MlDsa65); + } + + #[test] + fn test_generate_ml_dsa_87() { + test_generate(Variant::MlDsa87); + } + + fn test_generate(variant: Variant) { + let key = new_generate(variant).unwrap(); + + let mut algo = Signature::for_ml_dsa(variant).unwrap(); + + let data = b"Some Crypto Text"; + let bad_data = b"Some Crypto text"; + + let mut signature = vec![]; + let mut ctx = PkeyCtx::new(&key).unwrap(); + ctx.sign_message_init(&mut algo).unwrap(); + ctx.sign_to_vec(&data[..], &mut signature).unwrap(); + + // Verify good version with the original PKEY. + ctx.verify_message_init(&mut algo).unwrap(); + let valid = ctx.verify(&data[..], &signature); + assert!(matches!(valid, Ok(true))); + assert!(ErrorStack::get().errors().is_empty()); + + // Verify bad version with the original PKEY. + ctx.verify_message_init(&mut algo).unwrap(); + let valid = ctx.verify(&bad_data[..], &signature); + assert!(matches!(valid, Ok(false) | Err(_))); + assert!(ErrorStack::get().errors().is_empty()); + + // Derive a new PKEY with only the public bits. + let public_params = PKeyMlDsaParams::::from_pkey(&key).unwrap(); + let key_pub = new_from_public(variant, public_params.public_key().unwrap()).unwrap(); + let mut ctx = PkeyCtx::new(&key_pub).unwrap(); + let mut algo = Signature::for_ml_dsa(variant).unwrap(); + + // Verify good version with the public PKEY. + ctx.verify_message_init(&mut algo).unwrap(); + let valid = ctx.verify(&data[..], &signature); + assert!(matches!(valid, Ok(true))); + assert!(ErrorStack::get().errors().is_empty()); + + // Verify bad version with the public PKEY. + ctx.verify_message_init(&mut algo).unwrap(); + let valid = ctx.verify(&bad_data[..], &signature); + assert!(matches!(valid, Ok(false) | Err(_))); + assert!(ErrorStack::get().errors().is_empty()); + + // Derive a new PKEY with the seed. + let private_params = PKeyMlDsaParams::::from_pkey(&key).unwrap(); + // Derive a new PKEY from the private seed. + let key_priv = new_from_seed(variant, private_params.private_key_seed().unwrap()).unwrap(); + + // And redo the signature and verification. + let mut signature = vec![]; + let mut ctx = PkeyCtx::new(&key_priv).unwrap(); + ctx.sign_message_init(&mut algo).unwrap(); + ctx.sign_to_vec(&data[..], &mut signature).unwrap(); + + // Verify good version with the public PKEY. + ctx.verify_message_init(&mut algo).unwrap(); + let valid = ctx.verify(&data[..], &signature); + assert!(matches!(valid, Ok(true))); + assert!(ErrorStack::get().errors().is_empty()); + } +} diff --git a/openssl/src/signature.rs b/openssl/src/signature.rs new file mode 100644 index 000000000..91974c291 --- /dev/null +++ b/openssl/src/signature.rs @@ -0,0 +1,93 @@ +//! Wraps `EVP_SIGNATURE` objects. + +#[cfg(ossl350)] +use crate::cvt_p; +#[cfg(ossl350)] +use crate::error::ErrorStack; +use foreign_types::{ForeignType, ForeignTypeRef}; +use openssl_macros::corresponds; +use std::ffi::CStr; +use std::fmt; +#[cfg(ossl350)] +use std::ptr; + +foreign_type_and_impl_send_sync! { + type CType = ffi::EVP_SIGNATURE; + fn drop = ffi::EVP_SIGNATURE_free; + + /// A signature algorithm. + pub struct Signature; + + /// Reference to `Signature`. + pub struct SignatureRef; +} + +impl ToOwned for SignatureRef { + type Owned = Signature; + + fn to_owned(&self) -> Signature { + unsafe { + ffi::EVP_SIGNATURE_up_ref(self.as_ptr()); + Signature::from_ptr(self.as_ptr()) + } + } +} + +impl SignatureRef { + /// Returns the name of the signature algorithm. + #[corresponds(EVP_SIGNATURE_get0_name)] + pub fn name(&self) -> &str { + unsafe { CStr::from_ptr(ffi::EVP_SIGNATURE_get0_name(self.as_ptr())) } + .to_str() + .expect("identifier to be in UTF8") + } + + /// Returns a human-readable description of the signature + /// algorithm. + #[corresponds(EVP_SIGNATURE_get0_description)] + pub fn description(&self) -> &str { + unsafe { CStr::from_ptr(ffi::EVP_SIGNATURE_get0_description(self.as_ptr())) } + .to_str() + .expect("description to be in UTF8") + } +} + +impl fmt::Debug for Signature { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str(self.description()) + } +} + +impl Clone for Signature { + fn clone(&self) -> Signature { + SignatureRef::to_owned(self) + } +} + +impl Signature { + /// Creates a new `Signature` for use with ML-DSA. + #[cfg(ossl350)] + pub fn for_ml_dsa(variant: crate::pkey_ml_dsa::Variant) -> Result { + unsafe { + Ok(Signature(cvt_p(ffi::EVP_SIGNATURE_fetch( + ptr::null_mut(), + variant.as_cstr().as_ptr(), + ptr::null(), + ))?)) + } + } +} + +#[cfg(test)] +mod tests { + + #[cfg(ossl350)] + use super::*; + + #[cfg(ossl350)] + #[test] + fn test_alloc_free() { + let sig = Signature::for_ml_dsa(crate::pkey_ml_dsa::Variant::MlDsa44).unwrap(); + drop(sig); + } +}