diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1d263..b43b020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,70 @@ All notable changes to this project will be documented in this file. +## [0.10.0] - 2025-12-17 + +### โš  BREAKING CHANGES + +- *(pqclean)* Switch to seed-only format for MLDSA44_Ed25519 private keys +- *(pqclean)* Switch to seed-only format for MLDSA65_Ed25519 private keys + +### ๐Ÿš€ Features + +- Disallow building other profiles than debug +- *(encoders)* Add text encoder for ML-DSA-65 public keys +- *(encoders)* Add text encoder for ML-DSA-{44,87} public keys +- *(encoders)* Add text encoder for ML-DSA-{44,65}-Ed25519 public keys +- *(adapters/common/transcoders)* Do not clutter the current namespace when calling `make_pubkey_text_encoder!` +- *(pqclean)* Add `ENCODER_PrivateKeyInfo2Text` for MLDSA and Composite MLDSA +- *(tests)* Add basic wycheproof test for MLDSA65_Ed25519 +- *(tests)* Add wycheproof verify tests for pure ML-DSA +- *(tests)* Use a testing harness for wycheproof mldsa65ed25519 tests +- *(tests)* Add wycheproof signing tests for ML-DSA (pure & composite) +- *(tests)* Run signing tests with seed-only keys +- *(pqclean)* Fail gracefully on length error when decoding composite ML-DSA private keys +- *(pqclean)* Implement sign and verify with ctx for pure ML-DSA +- *(pqclean)* Implement sign and verify with ctx for composite ML-DSA +- *(pqclean)* Consistently implement Signer/Verifier as a wrapper for SignerWithCtx/VerifierWithCtx +- *(pqclean)* Derive private key from seed using rustcrypto-based helper +- *(pqclean)* Validate decoding of private keys through foreign module + +### ๐Ÿ› Bug Fixes + +- *(tests)* Don't refer to verify tests as sign tests in error message +- *(tests)* Check test flags before describing key decoding error as "expected" +- *(tests)* Remember to initialize crate::tests::common::setup for wycheproof tests +- *(pqclean)* Validate ctx length before calling the backend + +### ๐Ÿšœ Refactor + +- *(encoders)* Extract a format_hex_bytes helper function +- *(encoders)* Use a macro to generate plain text encoders for public keys +- *(encoders)* Take encoder name as argument in text encoder generator macro +- *(common/transcoders)* Make explicit that the Structureless2Text encoder is specific for public keys only +- *(common/transcoders/make_privkey_text_encoder)* C functions should only do argument parsing and delegate logic to safe rust abstractions. +- *(common/transcoders/make_pubkey_text_encoder)* C functions should only do argument parsing and delegate logic to safe rust abstractions. +- *(pqclean)* Rename SupportedSecretKey trait to SupportedMlDsaSecretKey +- *(pqclean)* Define ML-DSA seed type alias and enforce at callsite + +### ๐Ÿ“š Documentation + +- *(README)* Add notes about SLH-DSA and hybrids +- *(readme)* Fix typos and clarify project description +- *(doc,pqclean)* Refer to pq-composite-sigs-13 everywhere + +### ๐Ÿงช Testing + +- Add basic known-answer tests for composite signatures +- *(Cargo.toml)* Revert to wycheproof-rs revision without the temporary extension for expanded private keys +- *(common/signature)* Improve error message on expected signature length mismatch + +### Cleanup + +- *(tests)* Remove base64 dependency and hardcoded MLDSA44_Ed25519 test vectors +- *(tests)* Build wycheproof module in test mode only +- *(pqclean/composites)* Remove all legacy draft07 stuff +- *(pqclean)* Rename helpers to make explicit they operate on mldsa + ## [0.9.0] - 2025-10-24 ### ๐Ÿš€ Features @@ -426,6 +490,7 @@ All notable changes to this project will be documented in this file. +[0.10.0]: https://github.com/QUBIP/aurora/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/QUBIP/aurora/compare/v0.8.5...v0.9.0 [0.8.5]: https://github.com/QUBIP/aurora/compare/v0.8.4...v0.8.5 [0.8.4]: https://github.com/QUBIP/aurora/compare/v0.8.3...v0.8.4 diff --git a/Cargo.lock b/Cargo.lock index faba6db..59934a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -400,6 +400,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "der" version = "0.7.10" @@ -781,6 +787,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1273,7 +1288,7 @@ dependencies = [ [[package]] name = "qubip_aurora" -version = "0.9.1+dev" +version = "0.10.0" dependencies = [ "anyhow", "asn1", @@ -1281,6 +1296,7 @@ dependencies = [ "ed25519-dalek", "env_logger", "function_name", + "itertools 0.14.0", "kem", "lazy_static", "libc", @@ -1303,6 +1319,7 @@ dependencies = [ "slh-dsa", "slhdsa-c-rs", "tempfile", + "wycheproof", ] [[package]] @@ -1441,7 +1458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bded21caf3a1a0647947c2b22fc14b535213d9c3a5646968171a39b012ebaaaa" dependencies = [ "either", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.100", @@ -2028,6 +2045,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "wycheproof" +version = "0.6.0" +source = "git+https://github.com/eferollo/wycheproof-rs?branch=composite-mldsa-draft13#c501af1648742658b0c8cd0dec990dd5121422e6" +dependencies = [ + "data-encoding", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 051fcbf..ca455c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qubip_aurora" -version = "0.9.1+dev" +version = "0.10.0" edition = "2021" description = "A framework to build OpenSSL Providers tailored for the transition to post-quantum cryptography" license = "Apache-2.0" @@ -17,7 +17,7 @@ name = "aurora" default = [ "env_logger", "default_adapters", - "_composite_sigs_draft_12_postWGLC", + "_composite_sigs_draft_13", ] default_adapters = [ "libcrux_adapter", @@ -31,9 +31,7 @@ export = [] # One (and not more than one!) of the _draft_N features must be enabled to support `_composite_mldsa_eddsa`! # These features are _experimental_: they will be removed in an upcoming release once the composite_sigs draft graduates to RFC. # You have been warned: DO NOT DEPEND ON THEM! -_composite_sigs_draft_07 = [] -_composite_sigs_draft_12 = [] -_composite_sigs_draft_12_postWGLC = [] +_composite_sigs_draft_13 = [] # These features are _private_: they are automatically enabled by the selection of adapters. # DO NOT MANUALLY ENABLE THEM IN CARGO.TOML @@ -90,6 +88,7 @@ slhdsa-c-rs = { version = "0.0.3", optional = true } pqcrypto-mldsa = { version = "0.1.0", optional = true } pqcrypto-traits = { version = "0.3.5", optional = true } sha2 = { version = "0.10.9", optional = true } +itertools = "0.14.0" [build-dependencies] rasn-compiler = "0.11.0" @@ -99,3 +98,4 @@ regex = "1" tempfile = "3" serde_json = "1.0.140" paste = "1.0" +wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft13" } diff --git a/README.md b/README.md index 98b43a4..b7c7bff 100644 --- a/README.md +++ b/README.md @@ -121,17 +121,17 @@ The current supported algorithms are summarized in the following tables. | _SLH-DSA-SHAKE-128f_ | rustcrypto | โŽ Exempt | [`0x0918` (`2328`)][ID-reddy-tls-slhdsa-01:sigscheme] โš ๏ธ | [`2.16.840.1.101.3.4.3.27`][ID-lamps-x509-slhdsa-09:s3.7] | | _SLH-DSA-SHAKE-192f_ | slhdsa_c | โŽ Exempt | [`0x091A` (`2330`)][ID-reddy-tls-slhdsa-01:sigscheme] โš ๏ธ | [`2.16.840.1.101.3.4.3.29`][ID-lamps-x509-slhdsa-09:s3.7] | | _SLH-DSA-SHAKE-256s_ | slhdsa_c | โŽ Exempt | [`0x091B` (`2331`)][ID-reddy-tls-slhdsa-01:sigscheme] โš ๏ธ | [`2.16.840.1.101.3.4.3.30`][ID-lamps-x509-slhdsa-09:s3.7] | -| _ML-DSA-44_ED25519_ | pqclean | โœ… Composite [`ID-lamps-pq-composite-sigs@12`][ID-lamps-pq-composite-sigs-12] | [`0x090A` (`2314`)][ID-reddy-tls-composite-mldsa-05:sigscheme] | [`1.3.6.1.5.5.7.6.39`][ID-lamps-pq-composite-sigs:GH:post-WGLC:params] | -| _ML-DSA-65_ED25519_ | pqclean | โœ… Composite [`ID-lamps-pq-composite-sigs@12`][ID-lamps-pq-composite-sigs-12] | [`0x090B` (`2315`)][ID-reddy-tls-composite-mldsa-05:sigscheme] | [`1.3.6.1.5.5.7.6.48`][ID-lamps-pq-composite-sigs:GH:post-WGLC:params] | +| _ML-DSA-44_ED25519_ | pqclean | โœ… Composite [`ID-lamps-pq-composite-sigs@13`][ID-lamps-pq-composite-sigs-13] | [`0x090A` (`2314`)][ID-reddy-tls-composite-mldsa-05:sigscheme] | [`1.3.6.1.5.5.7.6.39`][ID-lamps-pq-composite-sigs-13:params] | +| _ML-DSA-65_ED25519_ | pqclean | โœ… Composite [`ID-lamps-pq-composite-sigs@13`][ID-lamps-pq-composite-sigs-13] | [`0x090B` (`2315`)][ID-reddy-tls-composite-mldsa-05:sigscheme] | [`1.3.6.1.5.5.7.6.48`][ID-lamps-pq-composite-sigs-13:params] | [iana:tls:sigscheme]: https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme [ID-tls-mldsa-01:sigscheme]: https://datatracker.ietf.org/doc/html/draft-ietf-tls-mldsa-01#name-ml-dsa-signaturescheme-valu [ID-reddy-tls-slhdsa-01:sigscheme]: https://datatracker.ietf.org/doc/html/draft-reddy-tls-slhdsa-01#name-iana-considerations -[ID-lamps-pq-composite-sigs-12]: https://datatracker.ietf.org/doc/draft-ietf-lamps-pq-composite-sigs/12/ +[ID-lamps-pq-composite-sigs-13]: https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs/13/ +[ID-lamps-pq-composite-sigs-13:params]: https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-13#name-algorithm-identifiers-and-p [ID-reddy-tls-composite-mldsa-05:sigscheme]: https://datatracker.ietf.org/doc/html/draft-reddy-tls-composite-mldsa-05#name-iana-considerations [nist:csor:algs]: https://csrc.nist.gov/projects/computer-security-objects-register/algorithm-registration [ID-lamps-x509-slhdsa-09:s3.7]: https://datatracker.ietf.org/doc/html/draft-ietf-lamps-x509-slhdsa-09#section-3-7 -[ID-lamps-pq-composite-sigs:GH:post-WGLC:params]: https://github.com/lamps-wg/draft-composite-sigs/blob/5ba4655fa1ae3b3b4c112c6cd8c97a93e6d900c3/src/algParams.md > [!NOTE] > - The `ML-DSA-{44,65}_ED25519` algorithms also use `ed25519-dalek` @@ -141,7 +141,7 @@ The current supported algorithms are summarized in the following tables. > experimentation only. > In QUBIP's Internet Browsing Pilot we avoid pure `ML-DSA` > deployments in favor of -> ["Composite `ML-DSA`"][ID-lamps-pq-composite-sigs-12] +> ["Composite `ML-DSA`"][ID-lamps-pq-composite-sigs-13] > and consistently recommend this approach. > - Transition recommendations that mandate hybrids for the PQC > transition usually mark `SLH-DSA` as explicitly exempt from the diff --git a/src/adapters/common.rs b/src/adapters/common.rs index c1d4109..6eede55 100644 --- a/src/adapters/common.rs +++ b/src/adapters/common.rs @@ -1,5 +1,12 @@ use crate::named; +pub mod helpers; + pub mod keymgmt_functions; pub mod macros; + +pub mod transcoders; + +#[cfg(test)] +pub mod wycheproof; diff --git a/src/adapters/common/helpers.rs b/src/adapters/common/helpers.rs new file mode 100644 index 0000000..72f06a0 --- /dev/null +++ b/src/adapters/common/helpers.rs @@ -0,0 +1,13 @@ +use itertools::Itertools; + +/// Formats `bytes` as a colon-separated string of hex values, with `bytes_per_line` elements on +/// each line, indented by `indent` spaces. +pub(crate) fn format_hex_bytes(bytes_per_line: usize, indent: usize, bytes: &[u8]) -> String { + bytes + .iter() + .map(|b| format!("{:02x}", b)) + .chunks(bytes_per_line) + .into_iter() + .map(|mut row| format!("{:indent$}{}", "", row.join(":"))) + .join(":\n") +} diff --git a/src/adapters/common/signature.rs b/src/adapters/common/signature.rs index 968764f..4116fd9 100644 --- a/src/adapters/common/signature.rs +++ b/src/adapters/common/signature.rs @@ -46,7 +46,10 @@ impl<'a> TryFrom<&'a [u8]> for Signature { fn try_from(value: &'a [u8]) -> Result { if value.len() != SIGNATURE_LEN { - log::error!("Signature is expected to be exactly {SIGNATURE_LEN} bytes"); + log::error!( + "Signature is expected to be exactly {SIGNATURE_LEN} bytes, got {}", + value.len() + ); return Err(anyhow!( "signature length mismatch, got {}, expected {SIGNATURE_LEN}", value.len() @@ -81,3 +84,23 @@ impl TryInto for Signature { impl SignatureEncoding for Signature { type Repr = SignatureBytes; } + +/// Verify the provided message bytestring using `Self` (typically a public key) +pub(crate) trait VerifierWithCtx { + /// Use `Self` to verify that the provided signature for a given message + /// bytestring is authentic. + fn verify_with_ctx(&self, msg: &[u8], signature: &S, ctx: &[u8]) -> Result<(), Error>; +} + +/// Sign the provided message bytestring using `Self`, returning a digital signature. +pub(crate) trait SignerWithCtx { + /// Sign the given message and return a digital signature + fn sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> S { + self.try_sign_with_ctx(msg, ctx) + .expect("signature operation failed") + } + + /// Attempt to sign the given message, returning a digital signature on + /// success, or an error if something went wrong. + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result; +} diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs new file mode 100644 index 0000000..7da4197 --- /dev/null +++ b/src/adapters/common/transcoders.rs @@ -0,0 +1,329 @@ +/// Make a text encoder for a public key. +/// +/// The encoder outputs the bytes of the key as colon-separated hex values. +/// This macro takes two arguments: +/// +/// - `encoder_struct`, the name of the encoder. The macro will define an empty struct with this +/// name and implement the `Encoder` trait on it. +/// - `property_definition`, which should be an OpenSSL property query string +/// as described in [property(7)](https://docs.openssl.org/3.2/man7/property/). +/// +/// This macro should be called in the `encoder_functions` submodule of an algorithm module. +/// The `EncoderContext` and `KeyPair` types must be defined and in scope, and `KeyPair.public` +/// must have the type `Option`, where `PublicKey` has an `encode` method that returns +/// the key data as something coercible to `&[u8]` (e.g. `Vec`). +/// +/// Like all other encoders, the resulting encoder must be registered in the adapter module's +/// `AdapterContextTrait::register_algorithms` implementation. +/// +/// # Example +/// +/// ``` +/// use crate::adapters::common::transcoders::make_pubkey_text_encoder; +/// make_pubkey_text_encoder!(PubKeyStructureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +/// ``` +macro_rules! make_pubkey_text_encoder { + ($encoder_struct:ident, $property_definition:literal) => { + pub(crate) struct $encoder_struct(); + impl $crate::forge::operations::transcoders::Encoder for $encoder_struct { + const PROPERTY_DEFINITION: &'static CStr = + $property_definition; + + const DISPATCH_TABLE: &'static [$crate::forge::bindings::OSSL_DISPATCH] = { + mod dispatch_table_module { + use super::*; + use $crate::adapters::common::helpers::format_hex_bytes; + use bindings::{OSSL_FUNC_encoder_does_selection_fn, OSSL_FUNC_ENCODER_DOES_SELECTION}; + use bindings::{OSSL_FUNC_encoder_encode_fn, OSSL_FUNC_ENCODER_ENCODE}; + use bindings::{OSSL_FUNC_encoder_freectx_fn, OSSL_FUNC_ENCODER_FREECTX}; + use bindings::{OSSL_FUNC_encoder_newctx_fn, OSSL_FUNC_ENCODER_NEWCTX}; + + // TODO reenable typechecking in dispatch_table_entry macro and make sure these still compile! + // https://docs.openssl.org/3.2/man7/provider-decoder/ + pub(super) const TEXT_ENCODER_FUNCTIONS: &[$crate::forge::bindings::OSSL_DISPATCH] = &[ + dispatch_table_entry!( + OSSL_FUNC_ENCODER_NEWCTX, + OSSL_FUNC_encoder_newctx_fn, + encoder_functions::newctx + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_FREECTX, + OSSL_FUNC_encoder_freectx_fn, + encoder_functions::freectx + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_DOES_SELECTION, + OSSL_FUNC_encoder_does_selection_fn, + does_selection_text + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encodeStructurelessToText + ), + $crate::forge::bindings::OSSL_DISPATCH::END, + ]; + + #[named] + pub(super) unsafe extern "C" fn encodeStructurelessToText( + vencoderctx: *mut c_void, + out: *mut $crate::forge::bindings::OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const $crate::forge::bindings::OSSL_PARAM, + selection: c_int, + _cb: $crate::forge::bindings::OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, + ) -> c_int { + use $crate::forge::operations::keymgmt::selection::Selection; + + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "๎ฑ Called!"); + + let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); + + if out.is_null() { + error!(target: log_target!(), "No OSSL_CORE_BIO passed to encoder"); + return ERROR_RET; + } + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + let keypair: &KeyPair = $crate::handleResult!(obj_raw.try_into()); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + let selection = $crate::handleResult!(Selection::try_from(selection as u32)); + + $crate::handleResult!($encoder_struct::encodeToText(encoderctx, out, keypair, &selection)); + return SUCCESS; + } + + $crate::forge::operations::transcoders::make_does_selection_fn!( + does_selection_text, + $encoder_struct, + ProviderInstance + ); + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; + } + + impl $encoder_struct { + + // Actually this should call keypair.pubkey.to_text similar to how we have to_DER there. + #[named] + pub(self) fn encodeToText( + encoderctx: &EncoderContext, + out: *mut $crate::forge::bindings::OSSL_CORE_BIO, + keypair: &KeyPair, + selection: &$crate::forge::operations::keymgmt::selection::Selection, + ) -> OurResult<()> { + use $crate::forge::operations::keymgmt::selection::Selection; + use $crate::adapters::common::helpers::format_hex_bytes; + + trace!(target: log_target!(), "๎žจ Called!"); + + if !selection.contains(Selection::PUBLIC_KEY) { + return Err(anyhow!("Invalid selection: {selection:#?}")); + } + + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); + let output = CString::new(output)?; + let ret = unsafe {encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul())}; + match ret { + Ok(_bytes_written) => { + return Ok(()) + } + Err(e) => { + return Err(anyhow!("Failure using BIO_write_ex() upcall pointer: {e:?}")); + } + }; + } + None => { + return Err(anyhow!("No public key")); + } + } + } + } + + impl $crate::forge::operations::transcoders::DoesSelection for $encoder_struct { + const SELECTION_MASK: $crate::forge::operations::keymgmt::selection::Selection = + $crate::forge::operations::keymgmt::selection::Selection::PUBLIC_KEY; + } + + + } +} +pub(crate) use make_pubkey_text_encoder; + +/// Make a text encoder for a private key. +/// +/// The encoder outputs the bytes of the key as colon-separated hex values. +/// This macro takes two arguments: +/// +/// - `encoder_struct`, the name of the encoder. The macro will define an empty struct with this +/// name and implement the `Encoder` trait on it. +/// - `property_definition`, which should be an OpenSSL property query string +/// as described in [property(7)](https://docs.openssl.org/3.2/man7/property/). +/// +/// This macro should be called in the `encoder_functions` submodule of an algorithm module. +/// The `EncoderContext` and `KeyPair` types must be defined and in scope, and `KeyPair.private` +/// must have the type `Option`, where `PrivateKey` has an `encode` method that returns +/// the key data as something coercible to `&[u8]` (e.g. `Vec`). +/// +/// Like all other encoders, the resulting encoder must be registered in the adapter module's +/// `AdapterContextTrait::register_algorithms` implementation. +/// +/// # Example +/// +/// ``` +/// use crate::adapters::common::transcoders::make_privkey_text_encoder; +/// make_privkey_text_encoder!(PrivateKeyInfo2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text',structure='PrivateKeyInfo'"); +/// ``` +macro_rules! make_privkey_text_encoder { + ($encoder_struct:ident, $property_definition:literal) => { + pub(crate) struct $encoder_struct(); + impl $crate::forge::operations::transcoders::Encoder for $encoder_struct { + const PROPERTY_DEFINITION: &'static CStr = + $property_definition; + + const DISPATCH_TABLE: &'static [$crate::forge::bindings::OSSL_DISPATCH] = { + mod dispatch_table_module { + use super::*; + use bindings::{OSSL_FUNC_encoder_does_selection_fn, OSSL_FUNC_ENCODER_DOES_SELECTION}; + use bindings::{OSSL_FUNC_encoder_encode_fn, OSSL_FUNC_ENCODER_ENCODE}; + use bindings::{OSSL_FUNC_encoder_freectx_fn, OSSL_FUNC_ENCODER_FREECTX}; + use bindings::{OSSL_FUNC_encoder_newctx_fn, OSSL_FUNC_ENCODER_NEWCTX}; + + // TODO reenable typechecking in dispatch_table_entry macro and make sure these still compile! + // https://docs.openssl.org/3.2/man7/provider-decoder/ + pub(super) const TEXT_ENCODER_FUNCTIONS: &[$crate::forge::bindings::OSSL_DISPATCH] = &[ + dispatch_table_entry!( + OSSL_FUNC_ENCODER_NEWCTX, + OSSL_FUNC_encoder_newctx_fn, + encoder_functions::newctx + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_FREECTX, + OSSL_FUNC_encoder_freectx_fn, + encoder_functions::freectx + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_DOES_SELECTION, + OSSL_FUNC_encoder_does_selection_fn, + does_selection_text + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encodeToText + ), + $crate::forge::bindings::OSSL_DISPATCH::END, + ]; + + #[named] + pub(super) unsafe extern "C" fn encodeToText( + vencoderctx: *mut c_void, + out: *mut $crate::forge::bindings::OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const $crate::forge::bindings::OSSL_PARAM, + selection: c_int, + _cb: $crate::forge::bindings::OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, + ) -> c_int { + use $crate::forge::operations::keymgmt::selection::Selection; + + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + + trace!(target: log_target!(), "๎ฑ Called!"); + + let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); + + if out.is_null() { + error!(target: log_target!(), "No OSSL_CORE_BIO passed to encoder"); + return ERROR_RET; + } + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + let keypair: &KeyPair = $crate::handleResult!(obj_raw.try_into()); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + let selection = $crate::handleResult!(Selection::try_from(selection as u32)); + + $crate::handleResult!($encoder_struct::encodeToText(encoderctx, out, keypair, &selection)); + return SUCCESS; + } + + $crate::forge::operations::transcoders::make_does_selection_fn!( + does_selection_text, + $encoder_struct, + ProviderInstance + ); + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; + } + + impl $encoder_struct { + + // Actually this should call keypair.privkey.to_text similar to how we have to_DER there. + #[named] + pub(self) fn encodeToText( + encoderctx: &EncoderContext, + out: *mut $crate::forge::bindings::OSSL_CORE_BIO, + keypair: &KeyPair, + selection: &$crate::forge::operations::keymgmt::selection::Selection, + ) -> OurResult<()> { + use $crate::forge::operations::keymgmt::selection::Selection; + use $crate::adapters::common::helpers::format_hex_bytes; + + trace!(target: log_target!(), "๎žจ Called!"); + + if !selection.contains(Selection::PRIVATE_KEY) { + return Err(anyhow!("Invalid selection: {selection:#?}")); + } + + match &keypair.private { + Some(key) => { + let key_bytes = key.encode(); + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Private key bytes:\n{}\n", formatted_key_bytes); + let output = CString::new(output)?; + let ret = unsafe {encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul())}; + match ret { + Ok(_bytes_written) => { + return Ok(()) + } + Err(e) => { + return Err(anyhow!("Failure using BIO_write_ex() upcall pointer: {e:?}")); + } + }; + } + None => { + return Err(anyhow!("No private key")); + } + } + } + } + + impl $crate::forge::operations::transcoders::DoesSelection for $encoder_struct { + const SELECTION_MASK: $crate::forge::operations::keymgmt::selection::Selection = + $crate::forge::operations::keymgmt::selection::Selection::PRIVATE_KEY; + } + + + } +} +pub(crate) use make_privkey_text_encoder; diff --git a/src/adapters/common/wycheproof.rs b/src/adapters/common/wycheproof.rs new file mode 100644 index 0000000..f9e9d4c --- /dev/null +++ b/src/adapters/common/wycheproof.rs @@ -0,0 +1,605 @@ +use crate::forge::crypto::signature; +use wycheproof::{ + composite_mldsa_sign, composite_mldsa_verify, mldsa_sign, mldsa_verify, TestResult, +}; + +pub trait SigAlgVerifyVariant { + type PublicKey; + type Signature; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result; + + fn decode_signature(bytes: &[u8]) -> anyhow::Result; + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error>; + + fn verify_with_ctx( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ctx: &[u8], + ) -> Result<(), signature::Error>; +} + +macro_rules! impl_sigalg_verify_variant { + ($variant:ident, $pubkey:ty, $sig:ty) => { + impl $crate::adapters::common::wycheproof::SigAlgVerifyVariant for $variant { + type PublicKey = $pubkey; + type Signature = $sig; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { + <$pubkey>::decode(bytes) + } + + fn decode_signature(bytes: &[u8]) -> anyhow::Result { + <$sig>::try_from(bytes) + } + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error> { + pubkey.verify(msg, sig) + } + + fn verify_with_ctx( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + pubkey.verify_with_ctx(msg, sig, ctx) + } + } + }; +} +pub(crate) use impl_sigalg_verify_variant; + +/// Borrowed from https://gitlab.com/nisec/qubip/qryptotoken/-/tree/06725b053d280c91a51d8b31775c5360aed9dc50/src/mldsa/wycheproof +/// with some changes, most notably that the tests for error conditions are much shorter because +/// the errors returned from our adapters aren't represented with a principled enum type like the +/// ones in qryptotoken are (so we don't have branching to check against specific error flags). +/// +/// Tests are designed to continue running after a failure rather than panicking, so that all +/// failures can be reported together. +pub fn run_mldsa_wycheproof_verify_tests( + test_name: mldsa_verify::TestName, +) { + use mldsa_verify::{TestFlag, TestSet}; + + let test_set = + TestSet::load(test_name).unwrap_or_else(|e| panic!("Failed to load verify test set: {e}")); + let mut passed = 0; + let mut failed = 0; + + for group in test_set.test_groups { + /* + * In Wycheproof, each entry in "testGroups" defines a public key that + * is used for all tests in the associated "tests" array. If the public + * key is invalid, the "tests" array usually contains only one test + * case explaining the reason for the invalid key. + * + * Therefore, when public key decoding fails, we immediately validate + * that this failure matches the expected outcome for all tests in the + * group, then skip to the next "testGroup". If the public key is + * valid, we continue and execute all tests within that group. + */ + let pubkey_bytes = group.pubkey.as_ref(); + let pubkey = match MlDsaParamSet::decode_pubkey(&pubkey_bytes) { + Ok(pk) => pk, + Err(e) => { + for test in &group.tests { + if test.result == TestResult::Invalid + && test.flags.contains(&TestFlag::IncorrectPublicKeyLength) + { + println!( + "โœ… tcId {}: {} โ€” pubkey decode failed as expected", + test.tc_id, test.comment, + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” expected Valid, but pubkey \ + decode failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + } + /* Jump to next group */ + continue; + } + }; + + for test in &group.tests { + let msg = test.msg.as_ref(); + let input_sig = test.sig.as_ref(); + let sig = match MlDsaParamSet::decode_signature(&input_sig) { + Ok(sig) => sig, + Err(e) => { + let invalid = TestResult::Invalid == test.result; + if invalid { + println!( + "โœ… tcId {}: {} โ€” signature decode failed as \ + expected", + test.tc_id, test.comment + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” Expected Valid, but signature \ + decode failed: {}", + test.tc_id, test.comment, e + ); + failed += 1; + } + continue; + } + }; + + let ctx = test.ctx.as_ref().map_or(&[][..], |c| c.as_ref()); + + let result = if !ctx.is_empty() { + MlDsaParamSet::verify_with_ctx(&pubkey, &msg, &sig, &ctx) + } else { + MlDsaParamSet::verify(&pubkey, &msg, &sig) + }; + + let expected = &test.result; + let passed_case = match (expected, result.is_ok()) { + (TestResult::Valid, true) => true, + (TestResult::Invalid, false) => true, + _ => false, + }; + if passed_case { + println!("โœ… tcId {}: {}", test.tc_id, test.comment); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” expected {:?}, got {:?}", + test.tc_id, test.comment, expected, result + ); + failed += 1; + } + continue; + } + } + + println!( + "\nโœ”๏ธ Passed: {passed} | โŒ Failed: {failed} | Total: {}", + passed + failed + ); + assert_eq!(failed, 0, "Some Wycheproof test cases failed"); +} + +pub fn run_composite_mldsa_wycheproof_verify_tests( + test_name: composite_mldsa_verify::TestName, +) { + let test_set = composite_mldsa_verify::TestSet::load(test_name) + .unwrap_or_else(|e| panic!("Failed to load verify test set: {e}")); + let mut passed = 0; + let mut failed = 0; + + for group in test_set.test_groups { + /* + * In Wycheproof, each entry in "testGroups" defines a public key that + * is used for all tests in the associated "tests" array. If the public + * key is invalid, the "tests" array usually contains only one test + * case explaining the reason for the invalid key. + * + * Therefore, when public key decoding fails, we immediately validate + * that this failure matches the expected outcome for all tests in the + * group, then skip to the next "testGroup". If the public key is + * valid, we continue and execute all tests within that group. + */ + let pubkey_bytes = group.pubkey.as_ref(); + let pubkey = match CompositeMlDsaParamSet::decode_pubkey(&pubkey_bytes) { + Ok(pk) => pk, + Err(e) => { + for test in &group.tests { + if test.result == TestResult::Invalid { + println!( + "โœ… tcId {}: {} โ€” pubkey decode failed as expected", + test.tc_id, test.comment, + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” expected Valid, but pubkey \ + decode failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + } + /* Jump to next group */ + continue; + } + }; + + for test in &group.tests { + let msg = test.msg.as_ref(); + let input_sig = test.sig.as_ref(); + let sig = match CompositeMlDsaParamSet::decode_signature(&input_sig) { + Ok(sig) => sig, + Err(e) => { + let invalid = TestResult::Invalid == test.result; + if invalid { + println!( + "โœ… tcId {}: {} โ€” signature decode failed as \ + expected", + test.tc_id, test.comment + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” Expected Valid, but signature \ + decode failed: {}", + test.tc_id, test.comment, e + ); + failed += 1; + } + continue; + } + }; + + // the composite tests don't have a `ctx` field, so we always use the "plain" `verify` + let result = CompositeMlDsaParamSet::verify(&pubkey, &msg, &sig); + + let expected = &test.result; + let passed_case = match (expected, result.is_ok()) { + (TestResult::Valid, true) => true, + (TestResult::Invalid, false) => true, + _ => false, + }; + if passed_case { + println!("โœ… tcId {}: {}", test.tc_id, test.comment); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” expected {:?}, got {:?}", + test.tc_id, test.comment, expected, result + ); + failed += 1; + } + continue; + } + } + + println!( + "\nโœ”๏ธ Passed: {passed} | โŒ Failed: {failed} | Total: {}", + passed + failed + ); + assert_eq!(failed, 0, "Some Wycheproof test cases failed"); +} + +pub trait SigAlgSignVariant { + type PrivateKey; + type Signature; + + /* It's up to the implementation to decide what to do with these bytes, e.g. in the ML-DSA case + * whether to treat them as a seed or as an expanded key. + */ + fn decode_privkey(bytes: &[u8]) -> anyhow::Result; + + fn try_sign( + privkey: &Self::PrivateKey, + msg: &[u8], + //deterministic: bool, + ) -> Result; + + fn try_sign_with_ctx( + privkey: &Self::PrivateKey, + msg: &[u8], + ctx: &[u8], + //deterministic: bool, + ) -> Result; + + fn encode_signature(sig: &Self::Signature) -> Vec; +} + +macro_rules! impl_sigalg_sign_variant { + ($variant:ident, $privkey:ty, $sig:ty) => { + impl $crate::adapters::common::wycheproof::SigAlgSignVariant for $variant { + type PrivateKey = $privkey; + type Signature = $sig; + + fn decode_privkey(bytes: &[u8]) -> anyhow::Result { + <$privkey>::decode(bytes) + } + + fn try_sign( + privkey: &Self::PrivateKey, + msg: &[u8], + //deterministic: bool, + ) -> Result { + Self::PrivateKey::try_sign(privkey, msg) + } + + fn try_sign_with_ctx( + privkey: &Self::PrivateKey, + msg: &[u8], + ctx: &[u8], + //deterministic: bool, + ) -> Result { + Self::PrivateKey::try_sign_with_ctx(privkey, msg, ctx) + } + + fn encode_signature(sig: &Self::Signature) -> Vec { + Vec::from(sig.to_bytes().as_ref()) + } + } + }; +} +pub(crate) use impl_sigalg_sign_variant; + +/// Borrowed from https://gitlab.com/nisec/qubip/qryptotoken/-/tree/06725b053d280c91a51d8b31775c5360aed9dc50/src/mldsa/wycheproof +/// with some changes, most notably that the tests for error conditions are much shorter because +/// the errors returned from our adapters aren't represented with a principled enum type like the +/// ones in qryptotoken are (so we don't have branching to check against specific error flags). +/// +/// Tests are designed to continue running after a failure rather than panicking, so that all +/// failures can be reported together. +pub fn run_mldsa_wycheproof_sign_tests( + test_name: mldsa_sign::TestName, + deterministic: bool, +) { + use mldsa_sign::{TestFlag, TestSet}; + + let test_set = + TestSet::load(test_name).unwrap_or_else(|e| panic!("Failed to load sign test set: {e}")); + let mut passed = 0; + let mut failed = 0; + + for group in test_set.test_groups { + /* + * In Wycheproof, each entry in "testGroups" defines a private key that + * is used for all tests in the associated "tests" array. If the + * private key is invalid, the "tests" array usually contains only one + * test case explaining the reason for the invalid key. + * + * Therefore, when private key decoding fails (or generation from seed) + * we immediately validate that this failure matches the expected + * outcome for all tests in the group, then skip to the next + * "testGroup". If the private key is valid, we continue and execute + * all tests within that group. + */ + + /* Use privseed first, otherwise fallback to privkey */ + let priv_bytes = group + .privseed + .as_ref() + .or(group.privkey.as_ref()) + .map(|b| b.as_slice().to_vec()) + .unwrap_or_else(|| panic!("Neither privateKey nor privateSeed present in test group")); + + let privkey = match MlDsaParamSet::decode_privkey(&priv_bytes) { + Ok(sk) => sk, + Err(e) => { + for test in &group.tests { + if test.result == TestResult::Invalid { + if test.flags.iter().any(|&flag| { + flag == TestFlag::IncorrectPrivateKeyLength + || flag == TestFlag::InvalidPrivateKey + }) { + println!( + "โœ… tcId {}: {} โ€” privkey decode failed \ + as expected", + test.tc_id, test.comment + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” expected Invalid (with acceptable privkey), \ + but privkey decode failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + } else { + println!( + "โŒ tcId {}: {} โ€” expected Valid, but privkey \ + decode failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + } + /* Jump to next group */ + continue; + } + }; + + for test in &group.tests { + let msg = test.msg.as_ref(); + let ctx = test.ctx.as_ref().map_or(&[][..], |c| c.as_ref()); + let sig_res = if ctx.is_empty() { + MlDsaParamSet::try_sign(&privkey, &msg) + } else { + MlDsaParamSet::try_sign_with_ctx(&privkey, &msg, &ctx) + }; + + match (&sig_res, test.result) { + (Err(_), TestResult::Invalid) => { + println!( + "โœ… tcId {}: {} โ€” signing failed as expected", + test.tc_id, test.comment + ); + passed += 1; + } + (Err(e), TestResult::Valid) => { + println!( + "โŒ tcId {}: {} โ€” expected Valid, but signing \ + failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + (Ok(_), TestResult::Invalid) => { + println!( + "โŒ tcId {}: {} โ€” expected Invalid, but signing \ + succeeded", + test.tc_id, test.comment + ); + failed += 1; + } + (Ok(sig), TestResult::Valid) => { + if deterministic { + let expected = test.sig.as_ref(); + let actual = MlDsaParamSet::encode_signature(sig); + if actual == expected { + println!( + "โœ… tcId {}: {} โ€” signature matches expected", + test.tc_id, test.comment + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” signature mismatch", + test.tc_id, test.comment + ); + failed += 1; + } + } else { + println!("โœ… tcId {}: {}", test.tc_id, test.comment); + passed += 1; + } + } + _ => { + println!( + "โŒ tcId {}: {} โ€” 'Acceptable' case not covered", + test.tc_id, test.comment + ); + failed += 1; + } + } + } + } + + println!( + "\nโœ”๏ธ Passed: {passed} | โŒ Failed: {failed} | Total: {}", + passed + failed + ); + assert_eq!(failed, 0, "Some Wycheproof signing test cases failed"); +} + +pub fn run_composite_mldsa_wycheproof_sign_tests( + test_name: composite_mldsa_sign::TestName, + deterministic: bool, +) { + let test_set = composite_mldsa_sign::TestSet::load(test_name) + .unwrap_or_else(|e| panic!("Failed to load sign test set: {e}")); + let mut passed = 0; + let mut failed = 0; + + for group in test_set.test_groups { + /* + * In Wycheproof, each entry in "testGroups" defines a private key that + * is used for all tests in the associated "tests" array. If the + * private key is invalid, the "tests" array usually contains only one + * test case explaining the reason for the invalid key. + * + * Therefore, when private key decoding fails (or generation from seed) + * we immediately validate that this failure matches the expected + * outcome for all tests in the group, then skip to the next + * "testGroup". If the private key is valid, we continue and execute + * all tests within that group. + */ + + let privkey = match CompositeMlDsaParamSet::decode_privkey(&group.privkey) { + Ok(sk) => sk, + Err(e) => { + for test in &group.tests { + if test.result == TestResult::Invalid { + println!( + "โœ… tcId {}: {} โ€” privkey decode failed \ + as expected", + test.tc_id, test.comment + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” expected Valid, but privkey \ + decode failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + } + /* Jump to next group */ + continue; + } + }; + + for test in &group.tests { + let msg = test.msg.as_ref(); + let sig_res = CompositeMlDsaParamSet::try_sign(&privkey, &msg); + + match (&sig_res, test.result) { + (Err(_), TestResult::Invalid) => { + println!( + "โœ… tcId {}: {} โ€” signing failed as expected", + test.tc_id, test.comment + ); + passed += 1; + } + (Err(e), TestResult::Valid) => { + println!( + "โŒ tcId {}: {} โ€” expected Valid, but signing \ + failed: {:?}", + test.tc_id, test.comment, e + ); + failed += 1; + } + (Ok(_), TestResult::Invalid) => { + println!( + "โŒ tcId {}: {} โ€” expected Invalid, but signing \ + succeeded", + test.tc_id, test.comment + ); + failed += 1; + } + (Ok(sig), TestResult::Valid) => { + if deterministic { + let expected = test.sig.as_ref(); + let actual = CompositeMlDsaParamSet::encode_signature(sig); + if actual == expected { + println!( + "โœ… tcId {}: {} โ€” signature matches expected", + test.tc_id, test.comment + ); + passed += 1; + } else { + println!( + "โŒ tcId {}: {} โ€” signature mismatch", + test.tc_id, test.comment + ); + failed += 1; + } + } else { + println!("โœ… tcId {}: {}", test.tc_id, test.comment); + passed += 1; + } + } + _ => { + println!( + "โŒ tcId {}: {} โ€” 'Acceptable' case not covered", + test.tc_id, test.comment + ); + failed += 1; + } + } + } + } + + println!( + "\nโœ”๏ธ Passed: {passed} | โŒ Failed: {failed} | Total: {}", + passed + failed + ); + assert_eq!(failed, 0, "Some Wycheproof signing test cases failed"); +} diff --git a/src/adapters/pqclean.rs b/src/adapters/pqclean.rs index f6f94bf..7610d70 100644 --- a/src/adapters/pqclean.rs +++ b/src/adapters/pqclean.rs @@ -83,28 +83,38 @@ impl AdapterContextTrait for PQCleanAdapter { // MLDSA44 encoder_to_register!(MLDSA44, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA44, ENCODER_PrivateKeyInfo2PEM), + encoder_to_register!(MLDSA44, ENCODER_PrivateKeyInfo2Text), encoder_to_register!(MLDSA44, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA44, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA44, ENCODER_PubKeyStructureless2Text), // MLDSA65 encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2PEM), + encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2Text), encoder_to_register!(MLDSA65, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA65, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA65, ENCODER_PubKeyStructureless2Text), // MLDSA87 encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2PEM), + encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2Text), encoder_to_register!(MLDSA87, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA87, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA87, ENCODER_PubKeyStructureless2Text), // MLDSA65_Ed25519 encoder_to_register!(MLDSA65_Ed25519, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA65_Ed25519, ENCODER_PrivateKeyInfo2PEM), + encoder_to_register!(MLDSA65_Ed25519, ENCODER_PrivateKeyInfo2Text), encoder_to_register!(MLDSA65_Ed25519, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA65_Ed25519, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA65_Ed25519, ENCODER_PubKeyStructureless2Text), // MLDSA44_Ed25519 encoder_to_register!(MLDSA44_Ed25519, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA44_Ed25519, ENCODER_PrivateKeyInfo2PEM), + encoder_to_register!(MLDSA44_Ed25519, ENCODER_PrivateKeyInfo2Text), encoder_to_register!(MLDSA44_Ed25519, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA44_Ed25519, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA44_Ed25519, ENCODER_PubKeyStructureless2Text), ]); handle.register_algorithms(OSSL_OP_ENCODER, encoder_algorithms.into_iter())?; diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index 7cbf15e..4e8ed96 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -399,5 +399,50 @@ pub(super) use decoder_functions::DER2PrivateKeyInfo as DECODER_DER2PrivateKeyIn pub(super) use decoder_functions::DER2SubjectPublicKeyInfo as DECODER_DER2SubjectPublicKeyInfo; pub(super) use encoder_functions::PrivateKeyInfo2DER as ENCODER_PrivateKeyInfo2DER; pub(super) use encoder_functions::PrivateKeyInfo2PEM as ENCODER_PrivateKeyInfo2PEM; +pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2Text; +pub(super) use encoder_functions::PubKeyStructureless2Text as ENCODER_PubKeyStructureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; + +#[cfg(test)] +mod tests { + use super::*; + use crate::adapters::common::wycheproof::*; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::mldsa_verify; + + struct Mldsa44; + + impl_sigalg_verify_variant!(Mldsa44, keymgmt_functions::PublicKey, signature::Signature); + + #[test] + fn test_mldsa_44_verify_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa44Verify); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; + use wycheproof::mldsa_sign; + + impl_sigalg_sign_variant!(Mldsa44, keymgmt_functions::PrivateKey, signature::Signature); + + #[test] + fn test_mldsa_44_sign_seed_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa44SignSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } + + #[test] + fn test_mldsa_44_sign_noseed_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa44SignNoSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } +} diff --git a/src/adapters/pqclean/MLDSA44/encoder_functions.rs b/src/adapters/pqclean/MLDSA44/encoder_functions.rs index a4c85ff..0be7e26 100644 --- a/src/adapters/pqclean/MLDSA44/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44/encoder_functions.rs @@ -464,6 +464,13 @@ impl DoesSelection for PrivateKeyInfo2PEM { // We can use the same does_selection function as PrivateKeyInfo2DER, so there's no need to call // the make_does_selection_fn macro again. +// generate the plain text encoder +use crate::adapters::common::transcoders::make_privkey_text_encoder; +make_privkey_text_encoder!( + PrivateKeyInfo2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text',structure='PrivateKeyInfo'" +); + pub(crate) struct SubjectPublicKeyInfo2DER(); impl Encoder for SubjectPublicKeyInfo2DER { const PROPERTY_DEFINITION: &'static CStr = @@ -722,3 +729,10 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to // call the make_does_selection_fn macro again. + +// generate the plain text encoder +use crate::adapters::common::transcoders::make_pubkey_text_encoder; +make_pubkey_text_encoder!( + PubKeyStructureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs index c1b1563..3e83db2 100644 --- a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs @@ -23,7 +23,9 @@ use pqcrypto_mldsa::mldsa44 as backend_module; use super::OurError as KMGMTError; type OurResult = anyhow::Result; -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; +use super::signature::{ + Signature, SignatureBytes, SignatureEncoding, SignerWithCtx, VerifierWithCtx, +}; pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); @@ -118,6 +120,29 @@ impl PublicKey { impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) + } +} + +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // FIPS 204 specifies maximum ctx len is 255 bytes, so it should + // fit into a u8 + let _ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + let sig = sig.to_bytes(); let sig = sig.as_ref(); use pqcrypto_traits::sign::DetachedSignature; @@ -127,7 +152,7 @@ impl Verifier for PublicKey { VerificationError::GenericVerificationError, ) })?; - backend_module::verify_detached_signature(&sig, msg, &self.0) + backend_module::verify_detached_signature_ctx(&sig, msg, ctx, &self.0) .map_err(map_into_VerificationError) .map_err(forge::crypto::signature::Error::from_source) } @@ -158,14 +183,9 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - let k = ::from_bytes(bytes) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA44) returned {:?}", - e - ) - })?; - Ok(Self(k)) + super::helpers::decode_mldsa_secret_key(bytes) + .map(Self) + .ok_or(anyhow!("Unable to decode private key")) } pub const fn byte_len() -> usize { @@ -178,7 +198,7 @@ impl PrivateKey { /// Derive a matching public key from this private key pub fn derive_public_key(&self) -> Option { - let pk = super::helpers::derive_public_key(&self.0); + let pk = super::helpers::derive_mldsa_public_key(&self.0); pk.map(|inner| PublicKey(inner)) } @@ -238,8 +258,26 @@ impl PrivateKey { impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { + self.try_sign_with_ctx(msg, &[]) + } +} + +impl SignerWithCtx for PrivateKey { + #[named] + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result { + trace!(target: log_target!(), "Called"); + let Self(ref sk) = self; - let signature = backend_module::detached_sign(msg, sk); + + // validate ctx length + // FIPS 204 specifies maximum ctx len is 255 bytes, so it should + // fit into a u8 + let _ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + + let signature = backend_module::detached_sign_ctx(msg, ctx, sk); Signature::try_from(signature.as_bytes()) .map_err(|e| forge::crypto::signature::Error::from_source(e)) } diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 4a733e1..453acca 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -43,14 +43,8 @@ use bindings::{OSSL_FUNC_signature_verify_init_fn, OSSL_FUNC_SIGNATURE_VERIFY_IN mod decoder_functions; mod encoder_functions; -#[cfg(feature = "_composite_sigs_draft_07")] -#[path = "./MLDSA44_Ed25519/keymgmt_functions_draft07.rs"] -mod keymgmt_functions; -#[cfg(feature = "_composite_sigs_draft_12")] -#[path = "./MLDSA44_Ed25519/keymgmt_functions_draft12.rs"] -mod keymgmt_functions; -#[cfg(feature = "_composite_sigs_draft_12_postWGLC")] -#[path = "./MLDSA44_Ed25519/keymgmt_functions_draft12.rs"] +#[cfg(feature = "_composite_sigs_draft_13")] +#[path = "./MLDSA44_Ed25519/keymgmt_functions_draft13.rs"] mod keymgmt_functions; #[path = "../common/signature.rs"] @@ -62,61 +56,23 @@ mod signature_functions; pub(crate) type OurError = anyhow::Error; pub(crate) use anyhow::anyhow; -#[cfg(feature = "_composite_sigs_draft_07")] -mod consts_composite_sigs_draft_07 { - use super::CStr; - - // Ensure proper null-terminated C string - // https://docs.openssl.org/master/man7/provider/#algorithm-naming - pub const NAMES: &CStr = - c"id-MLDSA44-Ed25519-SHA512:mldsa44_ed25519:2.16.840.1.114027.80.9.1.2"; - - // OID from - // OID should be a substring of NAMES - pub const OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 114027, 80, 9, 1, 2); - pub const OID_PKCS8: pkcs8::ObjectIdentifier = - pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.114027.80.9.1.2"); - pub const SIGALG_OID: Option<&CStr> = Some(c"2.16.840.1.114027.80.9.1.2"); -} -#[cfg(feature = "_composite_sigs_draft_07")] -pub use consts_composite_sigs_draft_07::{NAMES, OID, OID_PKCS8, SIGALG_OID}; - -#[cfg(feature = "_composite_sigs_draft_12")] -mod consts_composite_sigs_draft_12 { - use super::CStr; - - // Ensure proper null-terminated C string - // https://docs.openssl.org/master/man7/provider/#algorithm-naming - pub const NAMES: &CStr = - c"id-MLDSA44-Ed25519-SHA512:mldsa44_ed25519:2.16.840.1.114027.80.9.1.22"; - - // OID from - // OID should be a substring of NAMES - pub const OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 114027, 80, 9, 1, 22); - pub const OID_PKCS8: pkcs8::ObjectIdentifier = - pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.114027.80.9.1.22"); - pub const SIGALG_OID: Option<&CStr> = Some(c"2.16.840.1.114027.80.9.1.22"); -} -#[cfg(feature = "_composite_sigs_draft_12")] -pub use consts_composite_sigs_draft_12::{NAMES, OID, OID_PKCS8, SIGALG_OID}; - -#[cfg(feature = "_composite_sigs_draft_12_postWGLC")] -mod consts_composite_sigs_draft_12_postWGLC { +#[cfg(feature = "_composite_sigs_draft_13")] +mod consts_composite_sigs_draft_13 { use super::CStr; // Ensure proper null-terminated C string // https://docs.openssl.org/master/man7/provider/#algorithm-naming pub const NAMES: &CStr = c"id-MLDSA44-Ed25519-SHA512:mldsa44_ed25519:1.3.6.1.5.5.7.6.39"; - // OID from + // OID from // OID should be a substring of NAMES pub const OID: asn1::ObjectIdentifier = asn1::oid!(1, 3, 6, 1, 5, 5, 7, 6, 39); pub const OID_PKCS8: pkcs8::ObjectIdentifier = pkcs8::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.6.39"); pub const SIGALG_OID: Option<&CStr> = Some(c"1.3.6.1.5.5.7.6.39"); } -#[cfg(feature = "_composite_sigs_draft_12_postWGLC")] -pub use consts_composite_sigs_draft_12_postWGLC::{NAMES, OID, OID_PKCS8, SIGALG_OID}; +#[cfg(feature = "_composite_sigs_draft_13")] +pub use consts_composite_sigs_draft_13::{NAMES, OID, OID_PKCS8, SIGALG_OID}; /// NAME should be a substring of NAMES pub(crate) const NAME: &CStr = c"mldsa44_ed25519"; @@ -209,7 +165,8 @@ pub(crate) mod capabilities { /// /// # NOTE /// - /// > The OID for mldsa44_ed25519 comes from the [IETF LAMPS draft](https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-07#name-algorithm-identifiers). + /// > The OID for mldsa44_ed25519 comes from the + /// [IETF LAMPS draft](https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-13#name-algorithm-identifiers-and-p). const SIGALG_OID: Option<&CStr> = super::super::SIGALG_OID; const SECURITY_BITS: u32 = super::super::SECURITY_BITS; @@ -406,5 +363,51 @@ pub(super) use decoder_functions::DER2PrivateKeyInfo as DECODER_DER2PrivateKeyIn pub(super) use decoder_functions::DER2SubjectPublicKeyInfo as DECODER_DER2SubjectPublicKeyInfo; pub(super) use encoder_functions::PrivateKeyInfo2DER as ENCODER_PrivateKeyInfo2DER; pub(super) use encoder_functions::PrivateKeyInfo2PEM as ENCODER_PrivateKeyInfo2PEM; +pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2Text; +pub(super) use encoder_functions::PubKeyStructureless2Text as ENCODER_PubKeyStructureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; + +#[cfg(test)] +mod tests { + use super::*; + use crate::adapters::common::wycheproof::*; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::composite_mldsa_verify; + + #[allow(non_camel_case_types)] + struct Mldsa44_Ed25519; + + impl_sigalg_verify_variant!( + Mldsa44_Ed25519, + keymgmt_functions::PublicKey, + signature::Signature + ); + + #[test] + fn test_mldsa_44_ed_25519_verify_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_composite_mldsa_wycheproof_verify_tests::( + composite_mldsa_verify::TestName::MlDsa44Ed25519, + ); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; + use wycheproof::composite_mldsa_sign; + + impl_sigalg_sign_variant!( + Mldsa44_Ed25519, + keymgmt_functions::PrivateKey, + signature::Signature + ); + + #[test] + fn test_mldsa_44_ed_25519_sign_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_composite_mldsa_wycheproof_sign_tests::( + composite_mldsa_sign::TestName::MlDsa44Ed25519, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } +} diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs index a4c85ff..0be7e26 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs @@ -464,6 +464,13 @@ impl DoesSelection for PrivateKeyInfo2PEM { // We can use the same does_selection function as PrivateKeyInfo2DER, so there's no need to call // the make_does_selection_fn macro again. +// generate the plain text encoder +use crate::adapters::common::transcoders::make_privkey_text_encoder; +make_privkey_text_encoder!( + PrivateKeyInfo2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text',structure='PrivateKeyInfo'" +); + pub(crate) struct SubjectPublicKeyInfo2DER(); impl Encoder for SubjectPublicKeyInfo2DER { const PROPERTY_DEFINITION: &'static CStr = @@ -722,3 +729,10 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to // call the make_does_selection_fn macro again. + +// generate the plain text encoder +use crate::adapters::common::transcoders::make_pubkey_text_encoder; +make_pubkey_text_encoder!( + PubKeyStructureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft07.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft07.rs deleted file mode 100644 index 35bb6d2..0000000 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft07.rs +++ /dev/null @@ -1,1134 +0,0 @@ -#![allow(unreachable_code)] - -use super::*; -use bindings::{ - OSSL_CALLBACK, OSSL_KEYMGMT_SELECT_KEYPAIR, OSSL_KEYMGMT_SELECT_PRIVATE_KEY, - OSSL_KEYMGMT_SELECT_PUBLIC_KEY, OSSL_PKEY_PARAM_BITS, OSSL_PKEY_PARAM_MANDATORY_DIGEST, - OSSL_PKEY_PARAM_MAX_SIZE, OSSL_PKEY_PARAM_PRIV_KEY, OSSL_PKEY_PARAM_PUB_KEY, - OSSL_PKEY_PARAM_SECURITY_BITS, -}; -use forge::{ - bindings, - operations::keymgmt::selection::Selection, - operations::signature::{Signer, VerificationError, Verifier}, - ossl_callback::OSSLCallback, - osslparams::*, -}; -use pqcrypto_traits::sign::DetachedSignature; -use sha2::{Digest, Sha512}; -use std::{ - ffi::{c_int, c_void}, - fmt::Debug, -}; - -use ed25519_dalek as trad_backend_module; -use pqcrypto_mldsa::mldsa44 as pq_backend_module; - -type PQPublicKey = pq_backend_module::PublicKey; -type PQPrivateKey = pq_backend_module::SecretKey; -type TPublicKey = trad_backend_module::VerifyingKey; -type TPrivateKey = trad_backend_module::SecretKey; - -use super::OurError as KMGMTError; -type OurResult = anyhow::Result; - -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; - -pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); -pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); -pub(crate) const SIGNATURE_LEN: usize = PrivateKey::signature_bytes(); - -// The wrapped key from the pqcrypto crate has to be public, or else we can't access it to use it -// with the pqcrypto sign and verify functions. -#[derive(PartialEq)] -pub struct PublicKey { - pq_public_key: PQPublicKey, - trad_public_key: TPublicKey, -} - -#[derive(PartialEq)] -pub struct PrivateKey { - pq_private_key: PQPrivateKey, - trad_private_key: TPrivateKey, -} - -impl core::fmt::Debug for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PublicKey") - .field("pq_public_key", &"") - .field("trad_public_key", &self.trad_public_key) - .finish() - } -} - -impl PublicKey { - const PQ_PUBLIC_KEY_LEN: usize = pq_backend_module::public_key_bytes(); - const T_PUBLIC_KEY_LEN: usize = trad_backend_module::PUBLIC_KEY_LENGTH; - const PQ_SIGNATURE_LEN: usize = pq_backend_module::signature_bytes(); - const T_SIGNATURE_LEN: usize = trad_backend_module::SIGNATURE_LENGTH; - - pub fn decode(bytes: &[u8]) -> Result { - if bytes.len() != Self::byte_len() { - return Err(anyhow!( - "Public key should be {:?} bytes (got {:?})", - Self::byte_len(), - bytes.len() - )); - } - - // if we're here, then the length is correct, and we can safely split_at() and expect() - let (pq_bytes, trad_bytes) = bytes.split_at(Self::PQ_PUBLIC_KEY_LEN); - let pq_bytes: &[u8; Self::PQ_PUBLIC_KEY_LEN] = - pq_bytes.try_into().expect("slice has unexpected size"); - let trad_bytes: &[u8; Self::T_PUBLIC_KEY_LEN] = - trad_bytes.try_into().expect("slice has unexpected size"); - - let pq_public_key = - ::from_bytes( - pq_bytes, - ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::PublicKey::from_bytes (MLDSA44) returned {:?}", - e - ) - })?; - let trad_public_key = - trad_backend_module::VerifyingKey::from_bytes(trad_bytes).map_err(|e| { - anyhow!( - "trad_backend_module::VerifyingKey::from_bytes (Ed25519) returned {:?}", - e - ) - })?; - Ok(Self { - pq_public_key, - trad_public_key, - }) - } - - pub fn encode(&self) -> Vec { - let Self { - pq_public_key, - trad_public_key, - } = self; - let mut bytes = - ::as_bytes( - pq_public_key, - ) - .to_vec(); - bytes.extend(trad_public_key.as_bytes()); - bytes - } - - pub const fn byte_len() -> usize { - Self::PQ_PUBLIC_KEY_LEN + Self::T_PUBLIC_KEY_LEN - } - - pub const fn signature_bytes() -> usize { - PrivateKey::signature_bytes() - } - - #[named] - pub fn from_DER(pk_der_bytes: &[u8]) -> OurResult { - trace!(target: log_target!(), "{}", "Called!"); - - use asn_definitions::PublicKey as ASNPublicKey; - - let decodedpubkey: ASNPublicKey; - let slice = match pk_der_bytes.len() { - PUBKEY_LEN => pk_der_bytes, - - #[cfg(any())] - _ => { - decodedpubkey = match rasn::der::decode(pk_der_bytes) { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "Failed to decode the inner public key: {e:?}"); - return Err(OurError::from(e)); - } - }; - - debug!(target: log_target!(), "Parsed public key material out of ASN.1 for decoding!"); - - let slice: &[u8] = decodedpubkey.0.as_slice(); - slice - } - - #[cfg(not(any()))] - _ => { - let _ = decodedpubkey; - unreachable!(); - } - }; - - debug_assert_eq!(slice.len(), PUBKEY_LEN); - let pubkey = Self::decode(slice)?; - - Ok(pubkey) - } - - #[named] - pub fn to_DER(&self) -> OurResult> { - trace!(target: log_target!(), "{}", "Called!"); - - Ok(self.encode()) - } -} - -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-prefix-domain-separators-an -const PREFIX: &[u8] = "CompositeAlgorithmSignatures2025".as_bytes(); -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-domain-separator-values -const DOMAIN_SEPARATOR: &[u8] = &[ - 0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0B, -]; -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-pre-hashing-and-randomizer -const RANDOMIZER_LEN: usize = 32; - -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-verify -// There's no way to pass additional context info (`ctx` in the linked spec) into this Verifier -// trait's verify function, so we take `ctx` to be the empty string. -impl Verifier for PublicKey { - #[named] - fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - // get at the public keys - let Self { - pq_public_key, - trad_public_key, - } = self; - - // separate the parts of the signature - let sig = sig.to_bytes(); - let sig = sig.as_ref(); - if sig.len() != SIGNATURE_LEN { - error!(target: log_target!(), "Signature should be {SIGNATURE_LEN:} bytes (got {})", sig.len()); - return Err(forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - )); - } - // if we get here, we know we have the right number of bytes, so these calls to split_at() - // and expect() won't panic - let (randomizer_bytes, tail_bytes) = sig.split_at(RANDOMIZER_LEN); - let (pq_sig, trad_sig) = tail_bytes.split_at(Self::PQ_SIGNATURE_LEN); - let pq_sig: &[u8; Self::PQ_SIGNATURE_LEN] = pq_sig.try_into().expect("Unexpected length"); - let trad_sig: &[u8; Self::T_SIGNATURE_LEN] = - trad_sig.try_into().expect("Unexpected length"); - - // M' := Prefix || Domain || len(ctx) || ctx || r || PH( M ) - // (here M is our `msg` argument) - let msg_hash = Sha512::digest(msg); - let mut M_prime = PREFIX.to_vec(); - M_prime.extend_from_slice(DOMAIN_SEPARATOR); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment at top of impl) - M_prime.extend_from_slice(&randomizer_bytes); - M_prime.extend(msg_hash); - - // verify with ML-DSA - use pqcrypto_traits::sign::DetachedSignature; - let pq_sig = pq_backend_module::DetachedSignature::from_bytes(pq_sig).map_err(|e| { - error!(target: log_target!(), "Error when verifying PQ signature: {e:?}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - pq_backend_module::verify_detached_signature_ctx( - &pq_sig, - M_prime.as_slice(), - DOMAIN_SEPARATOR, - pq_public_key, - ) - .map_err(map_PQError_into_VerificationError) - .map_err(forge::crypto::signature::Error::from_source)?; - - // verify with Ed25519 - let trad_sig = trad_backend_module::Signature::from_bytes(trad_sig); - trad_public_key - .verify_strict(M_prime.as_slice(), &trad_sig) - // this backend uses an opaque error type, so no need for a separate fn with a match arm - .map_err(|e| { - error!(target: log_target!(), "Error when verifying traditional signature: {e:?}"); - VerificationError::GenericVerificationError - }) - .map_err(forge::crypto::signature::Error::from_source)?; - - // if we got here, both verifications passed - Ok(()) - } -} - -#[named] -fn map_PQError_into_VerificationError( - value: pqcrypto_traits::sign::VerificationError, -) -> VerificationError { - match value { - pqcrypto_traits::sign::VerificationError::InvalidSignature => { - VerificationError::InvalidSignature - } - pqcrypto_traits::sign::VerificationError::UnknownVerificationError => { - VerificationError::GenericVerificationError - } - e => { - warn!(target: log_target!(), "Unknown error {e:#?}"); - VerificationError::GenericVerificationError - } - } -} - -impl PrivateKey { - const PQ_PRIVATE_KEY_LEN: usize = pq_backend_module::secret_key_bytes(); - const T_PRIVATE_KEY_LEN: usize = trad_backend_module::SECRET_KEY_LENGTH; - const PQ_SIGNATURE_LEN: usize = PublicKey::PQ_SIGNATURE_LEN; - const T_SIGNATURE_LEN: usize = PublicKey::T_SIGNATURE_LEN; - - pub fn encode(&self) -> Vec { - let Self { - pq_private_key, - trad_private_key, - } = self; - let mut bytes = - ::as_bytes( - pq_private_key, - ) - .to_vec(); - bytes.extend(trad_private_key); - bytes - } - - pub fn decode(bytes: &[u8]) -> Result { - let (pq_bytes, trad_bytes) = bytes.split_at(pq_backend_module::secret_key_bytes()); - let pq_private_key = - ::from_bytes( - pq_bytes, - ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA44) returned {:?}", - e - ) - })?; - let trad_private_key = trad_bytes - .try_into() - .map_err(|_| anyhow!("Ed25519 secret key should be 32 bytes"))?; - Ok(Self { - pq_private_key, - trad_private_key, - }) - } - - pub const fn byte_len() -> usize { - Self::PQ_PRIVATE_KEY_LEN + Self::T_PRIVATE_KEY_LEN - } - - pub const fn signature_bytes() -> usize { - RANDOMIZER_LEN + Self::PQ_SIGNATURE_LEN + Self::T_SIGNATURE_LEN - } - - fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key) - } - - /// Derive a matching public key from this private key - #[named] - pub fn derive_public_key(&self) -> Option { - trace!(target: log_target!(), "Called"); - - let t_sk = &self.trad_private_key; - let t_sk = trad_backend_module::SigningKey::from_bytes(t_sk); - let t_pk = t_sk.verifying_key(); - - let pq_pk = match self.derive_PQ_public_key() { - Some(pk) => pk, - None => { - return None; - } - }; - - let pk = PublicKey { - pq_public_key: pq_pk, - trad_public_key: t_pk, - }; - Some(pk) - } - - #[named] - pub fn from_DER(sk_der_bytes: &[u8]) -> OurResult<(Self, Option)> { - use asn_definitions::PrivateKey as ASNPrivateKey; - trace!(target: log_target!(), "Called"); - - let decodedprivkey = match rasn::der::decode::(sk_der_bytes) { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "Failed to decode the inner private key: {e:?}"); - return Err(OurError::from(e)); - } - }; - - debug!(target: log_target!(), "Parsed private key material out of ASN.1 for decoding!"); - - let (privkey, opt_pubkey) = match decodedprivkey { - ASNPrivateKey::seed(_seed) => unimplemented!(), - ASNPrivateKey::expandedKey(expandedKey) => { - let slice: &[u8] = &expandedKey; - let privkey = keymgmt_functions::PrivateKey::decode(slice)?; - - // We need to derive a public key from the private key, without a seed - let pubkey = match privkey.derive_public_key() { - Some(k) => k, - None => { - error!(target: log_target!(), "Could not derive the public key from the inner private key"); - return Err(anyhow!( - "Could not derive the public key from the inner private key" - )); - } - }; - (privkey, Some(pubkey)) - } - ASNPrivateKey::both(_private_key_both) => unimplemented!(), - }; - Ok((privkey, opt_pubkey)) - } - - #[named] - pub fn to_DER(&self) -> OurResult> { - trace!(target: log_target!(), "Called"); - use asn_definitions::PrivateKey as ASNPrivateKey; - - let raw_sk_bytes = self.encode(); - let asn_sk = ASNPrivateKey::expandedKey(raw_sk_bytes.into()); - let asn_sk_bytes = match rasn::der::encode(&asn_sk) { - Ok(v) => v, - Err(e) => { - error!(target: log_target!(), "Failed to encode private key: {e:?}"); - return Err(OurError::from(e)); - } - }; - Ok(asn_sk_bytes) - } -} - -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-sign -// Just like with the Verifier above, there's no way to pass additional context info (`ctx` in the -// linked spec) into this Signer trait's try_sign function, so we take `ctx` to be the empty string. -impl Signer for PrivateKey { - fn try_sign(&self, msg: &[u8]) -> Result { - // randomizer = Random(32) - let randomizer: [u8; RANDOMIZER_LEN] = rand::random(); - // M' := Prefix || Domain || len(ctx) || ctx || r || PH( M ) - // (here M is our `msg` argument) - let msg_hash = Sha512::digest(msg); - let mut M_prime = PREFIX.to_vec(); - M_prime.extend_from_slice(DOMAIN_SEPARATOR); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment above) - M_prime.extend_from_slice(&randomizer); - M_prime.extend(msg_hash); - - // get at the private keys - let Self { - pq_private_key, - trad_private_key, - } = self; - - // sign with ML-DSA - // (the domain separator being used as the `ctx` here refers to the underlying ML-DSA - // signature operation, and has nothing to do with the empty `ctx` string from the spec) - let pq_signature = - pq_backend_module::detached_sign_ctx(&M_prime, DOMAIN_SEPARATOR, pq_private_key); - - // sign with Ed25519 - let trad_signature = - trad_backend_module::SigningKey::from_bytes(trad_private_key).sign(&M_prime); - - // build the result - let mut signature = randomizer.to_vec(); - signature.extend_from_slice(pq_signature.as_bytes()); - signature.extend_from_slice(&trad_signature.to_bytes()); - - Signature::try_from(signature.as_slice()) - .map_err(|e| forge::crypto::signature::Error::from_source(e)) - } -} - -#[expect(dead_code)] -pub struct KeyPair<'a> { - pub private: Option, - pub public: Option, - provctx: &'a ProviderInstance<'a>, -} - -impl<'a> Debug for KeyPair<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let private = match &self.private { - #[cfg(not(debug_assertions))] // code compiled only in release builds - Some(_) => { - todo!("remove private key printing also from development builds"); - format!("{}", "present") - } - #[cfg(debug_assertions)] // code compiled only in development builds - Some(p) => { - format!("{:02x?}", p.encode()) - } - None => format!("{:?}", None::<()>), - }; - let public = match &self.public { - Some(p) => format!("{:02x?}", p.encode()), - None => format!("{:?}", None::<()>), - }; - f.debug_struct("KeyPair") - .field("private", &private) - .field("public", &public) - .finish() - } -} - -impl<'a> KeyPair<'a> { - #[named] - fn new(provctx: &'a ProviderInstance) -> Self { - trace!(target: log_target!(), "Called"); - KeyPair { - private: None, - public: None, - provctx: provctx, - } - } - - #[named] - pub(super) fn from_parts( - provctx: &'a ProviderInstance, - private: Option, - public: Option, - ) -> Self { - trace!(target: log_target!(), "Called"); - KeyPair { - private, - public, - provctx, - } - } - - #[named] - fn generate(provctx: &'a ProviderInstance) -> Result { - trace!(target: log_target!(), "Called"); - - // Isn't it weird that this operation can't fail? What does the pqclean implementation do if - // it can't find a randomness source or it can't allocate memory or something? - let (pq_public_key, pq_private_key) = pq_backend_module::keypair(); - - // Similarly, it seems weird that this can't fail. Hopefully a different layer can handle it - // if something goes wrong here. - let trad_keypair = trad_backend_module::SigningKey::generate(provctx.get_rng()); - let trad_private_key = trad_keypair.to_bytes(); - let trad_public_key = trad_keypair.verifying_key(); - - Ok(KeyPair { - private: Some(PrivateKey { - pq_private_key, - trad_private_key, - }), - public: Some(PublicKey { - pq_public_key, - trad_public_key, - }), - provctx, - }) - } - - #[cfg(test)] - #[named] - pub(crate) fn generate_new(provctx: &'a ProviderInstance) -> Result { - trace!(target: log_target!(), "Called"); - let genctx = GenCTX::new(provctx, Selection::KEYPAIR); - let r = genctx.generate()?; - - Ok(Self { - private: r.private, - public: r.public, - provctx, - }) - } -} - -impl<'a> Signer for KeyPair<'a> { - #[named] - fn try_sign(&self, msg: &[u8]) -> Result { - trace!(target: log_target!(), "Called"); - - let sk = self - .private - .as_ref() - .ok_or_else(|| { - anyhow!( - "This keypair does not have a private key, so it cannot generate signatures" - ) - }) - .map_err(forge::crypto::signature::Error::from_source)?; - Ok(sk.try_sign(msg)?) - } -} - -impl<'a> Verifier for KeyPair<'a> { - #[named] - fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - trace!(target: log_target!(), "Called"); - - let pk = self - .public - .as_ref() - .ok_or_else(|| { - anyhow!("This keypair does not have a public key, so it cannot verify signatures") - }) - .map_err(|e| { - error!("{e:#}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - pk.verify(msg, sig) - } -} - -impl TryFrom<*mut c_void> for &mut KeyPair<'_> { - type Error = KMGMTError; - - #[named] - fn try_from(vptr: *mut c_void) -> Result { - trace!(target: log_target!(), "Called for {}", - "impl TryFrom<*mut c_void> for &mut KeyPair" - ); - let ptr = vptr as *mut KeyPair; - if ptr.is_null() { - return Err(anyhow::anyhow!("vptr was null")); - } - Ok(unsafe { &mut *ptr }) - } -} - -impl TryFrom<*mut c_void> for &KeyPair<'_> { - type Error = KMGMTError; - - #[named] - fn try_from(vptr: *mut core::ffi::c_void) -> Result { - trace!(target: log_target!(), "Called for {}", "impl<'a> TryFrom<*mut core::ffi::c_void> for &KeyPair<'a>"); - let r: &mut KeyPair = vptr.try_into()?; - Ok(r) - } -} - -impl TryFrom<*const c_void> for &KeyPair<'_> { - type Error = KMGMTError; - - #[named] - fn try_from(vptr: *const c_void) -> Result { - trace!(target: log_target!(), "Called for {}", "impl<'a> TryFrom<*const c_void> for &KeyPair<'a>"); - let mut_vptr = vptr as *mut c_void; - let r: &mut KeyPair = mut_vptr.try_into()?; - Ok(r) - } -} - -#[named] -pub(super) unsafe extern "C" fn new(vprovctx: *mut c_void) -> *mut c_void { - trace!(target: log_target!(), "{}", "Called!"); - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - let provctx: &ProviderInstance<'_> = handleResult!(vprovctx.try_into()); - - let keypair: Box> = Box::new(KeyPair::new(provctx)); - return Box::into_raw(keypair).cast(); -} - -#[named] -pub(super) unsafe extern "C" fn free(vkey: *mut c_void) { - trace!(target: log_target!(), "{}", "Called!"); - let /* mut */kp: Box = unsafe { Box::from_raw(vkey.cast()) }; - //todo!("Cleanse the private key data") - //todo!("Free the key data") - drop(kp); -} - -#[named] -pub(super) unsafe extern "C" fn has(vkeydata: *const c_void, selection: c_int) -> c_int { - const ERROR_RET: c_int = 0; - - trace!(target: log_target!(), "{}", "Called!"); - - let selection: u32 = selection.try_into().unwrap(); - - // From https://github.com/openssl/openssl/blob/fb55383c65bb47eef3bf5f73be5a0ad41d81bb3f/providers/implementations/keymgmt/ml_dsa_kmgmt.c#L145-L155 - if (selection & OSSL_KEYMGMT_SELECT_KEYPAIR) == 0 { - return 1; // the selection is not missing - } - - let keydata: &KeyPair = handleResult!(vkeydata.try_into()); - - // from https://github.com/openssl/openssl/blob/fb55383c65bb47eef3bf5f73be5a0ad41d81bb3f/crypto/ml_dsa/ml_dsa_key.c#L285-L297 - if (selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0 { - // Note that the public key always exists if there is a private key - if keydata.public.is_none() { - return 0; // No public key - } - if (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0 && keydata.private.is_none() { - return 0; // No private key - } - return 1; - } - - return 0; -} - -#[named] -pub(super) unsafe extern "C" fn gen( - vgenctx: *mut c_void, - _cb: OSSL_CALLBACK, - _cbarg: *mut c_void, -) -> *mut c_void { - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - trace!(target: log_target!(), "{}", "Called!"); - let genctx: &mut GenCTX<'_> = handleResult!(vgenctx.try_into()); - - let keypair = handleResult!(genctx.generate()); - let keypair: Box> = Box::new(keypair); - - let keypair_ptr = Box::into_raw(keypair); - - return keypair_ptr.cast(); -} - -#[named] -pub(super) unsafe extern "C" fn gen_cleanup(vgenctx: *mut c_void) { - trace!(target: log_target!(), "{}", "Called!"); - let /* mut */genctx: Box = unsafe { Box::from_raw(vgenctx.cast()) }; - //todo!("clean up and free the key object generation context genctx"); - drop(genctx); -} - -struct GenCTX<'a> { - provctx: &'a ProviderInstance<'a>, - selection: Selection, -} - -impl<'a> GenCTX<'a> { - fn new(provctx: &'a ProviderInstance, selection: Selection) -> Self { - Self { - provctx: provctx, - selection: selection, - } - } - - #[named] - fn generate(&self) -> Result, KMGMTError> { - trace!(target: log_target!(), "Called"); - if !self.selection.contains(Selection::KEYPAIR) { - trace!(target: log_target!(), "Returning empty keypair due to selection bits {:?}", self.selection); - return Ok(KeyPair::new(self.provctx)); - } - trace!(target: log_target!(), "Generating a new KeyPair"); - - KeyPair::generate(self.provctx) - } -} - -impl<'a> TryFrom<*mut c_void> for &mut GenCTX<'a> { - type Error = KMGMTError; - - #[named] - fn try_from(vctx: *mut c_void) -> Result { - trace!(target: log_target!(), "Called for {}", - "impl<'a> TryFrom<*mut c_void> for &mut GenCTX<'a>" - ); - let ctxp = vctx as *mut GenCTX; - if ctxp.is_null() { - panic!("vctx was null"); - } - Ok(unsafe { &mut *ctxp }) - } -} - -#[named] -pub(super) unsafe extern "C" fn gen_init( - vprovctx: *mut c_void, - selection: c_int, - params: *const OSSL_PARAM, -) -> *mut c_void { - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - trace!(target: log_target!(), "{}", "Called!"); - let provctx: &ProviderInstance<'_> = handleResult!(vprovctx.try_into()); - let selection: Selection = handleResult!((selection as u32).try_into()); - let newctx = Box::new(GenCTX::new(provctx, selection)); - - if !params.is_null() { - warn!(target: log_target!(), "Ignoring params!"); - //todo!("set params on the context if params is not null") - } - - let newctx_raw_ptr = Box::into_raw(newctx); - - return newctx_raw_ptr.cast(); -} - -#[named] -pub(super) unsafe extern "C" fn import( - _keydata: *mut c_void, - _selection: c_int, - _params: *const OSSL_PARAM, -) -> c_int { - trace!(target: log_target!(), "{}", "Called!"); - todo!("import data indicated by selection into keydata with values taken from the params array") -} - -#[cfg(not(feature = "export"))] -pub(super) use crate::adapters::common::keymgmt_functions::export_forbidden as export; - -const HANDLED_KEY_TYPES: [OSSL_PARAM; 3] = [ - OSSL_PARAM { - key: OSSL_PKEY_PARAM_PUB_KEY.as_ptr(), - data_type: OSSL_PARAM_OCTET_STRING, - data: std::ptr::null::() as *mut std::ffi::c_void, - data_size: 0, - return_size: 0, - }, - OSSL_PARAM { - key: OSSL_PKEY_PARAM_PRIV_KEY.as_ptr(), - data_type: OSSL_PARAM_OCTET_STRING, - data: std::ptr::null::() as *mut std::ffi::c_void, - data_size: 0, - return_size: 0, - }, - OSSL_PARAM::END, -]; - -// I think using {import,export}_types_ex instead of the non-_ex variant means we only -// support OSSL 3.2 and up, but I also think that's fine...? -#[named] -pub(super) unsafe extern "C" fn import_types_ex( - vprovctx: *mut c_void, - selection: c_int, -) -> *const OSSL_PARAM { - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - trace!(target: log_target!(), "{}", "Called!"); - let _provctx: &ProviderInstance<'_> = handleResult!(vprovctx.try_into()); - let selection: Selection = handleResult!((selection as u32).try_into()); - - if selection.intersects(Selection::KEYPAIR) { - return HANDLED_KEY_TYPES.as_ptr(); - } - ERROR_RET -} - -#[cfg(not(feature = "export"))] -pub(super) use crate::adapters::common::keymgmt_functions::export_types_ex_forbidden as export_types_ex; - -#[named] -pub(super) unsafe extern "C" fn gen_set_params( - _vgenctx: *mut c_void, - _params: *const OSSL_PARAM, -) -> c_int { - trace!(target: log_target!(), "{}", "Called!"); - - #[cfg(not(debug_assertions))] // code compiled only in release builds - { - todo!("set genctx params"); - } - - #[cfg(debug_assertions)] // code compiled only in development builds - { - warn!(target: log_target!(), "{}", "Ignoring params!"); - return 1; - } -} - -#[named] -pub(super) unsafe extern "C" fn gen_settable_params( - _vgenctx: *mut c_void, - vprovctx: *mut c_void, -) -> *const OSSL_PARAM { - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - trace!(target: log_target!(), "{}", "Called!"); - let _provctx: &ProviderInstance<'_> = match vprovctx.try_into() { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "{}", e); - return ERROR_RET; - } - }; - - #[cfg(not(debug_assertions))] // code compiled only in release builds - { - todo!("return pointer to array of settable genctx params") - } - - #[cfg(debug_assertions)] // code compiled only in development builds - { - warn!(target: log_target!(), "{}", "TODO: return pointer to (non-empty) array of settable genctx params"); - - crate::osslparams::EMPTY_PARAMS.as_ptr() - } -} - -#[named] -pub(super) unsafe extern "C" fn get_params( - vkeydata: *mut c_void, - params: *mut OSSL_PARAM, -) -> c_int { - const ERROR_RET: c_int = 0; - const SUCCESS: c_int = 1; - - trace!(target: log_target!(), "{}", "Called!"); - let _keydata: &KeyPair = handleResult!(vkeydata.try_into()); - - let params = match OSSLParam::try_from(params) { - Ok(params) => params, - Err(e) => { - error!(target: log_target!(), "Failed decoding params: {:?}", e); - return ERROR_RET; - } - }; - - for mut p in params { - let key = match p.get_key() { - Some(key) => key, - None => { - error!(target: log_target!(), "Param without valid key {:?}", p); - return ERROR_RET; - } - }; - - if key == OSSL_PKEY_PARAM_BITS { - //const BITS: c_int = 8 * (PUBKEY_LEN as c_int); - //let _ = handleResult!(p.set(BITS)); - let _ = handleResult!(p.set(super::SECURITY_BITS as c_int)); - } else if key == OSSL_PKEY_PARAM_MAX_SIZE { - let _ = handleResult!(p.set(SIGNATURE_LEN as c_int)); - } else if key == OSSL_PKEY_PARAM_SECURITY_BITS { - let _ = handleResult!(p.set(super::SECURITY_BITS as c_int)); - } else if key == OSSL_PKEY_PARAM_MANDATORY_DIGEST { - let _ = handleResult!(p.set(c"")); - } else { - debug!(target: log_target!(), "Ignoring param {:?}", key); - } - } - return SUCCESS; -} - -#[named] -pub(super) unsafe extern "C" fn gettable_params(vprovctx: *mut c_void) -> *const OSSL_PARAM { - trace!(target: log_target!(), "{}", "Called!"); - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - let _provctx: &ProviderInstance<'_> = match vprovctx.try_into() { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "{}", e); - return ERROR_RET; - } - }; - - static LIST: &[CONST_OSSL_PARAM] = &[ - OSSLParam::new_const_int::(OSSL_PKEY_PARAM_BITS, None), - OSSLParam::new_const_int::(OSSL_PKEY_PARAM_MAX_SIZE, None), - OSSLParam::new_const_int::(OSSL_PKEY_PARAM_SECURITY_BITS, None), - OSSLParam::new_const_utf8string(OSSL_PKEY_PARAM_MANDATORY_DIGEST, None), - CONST_OSSL_PARAM::END, - ]; - - let first: &bindings::OSSL_PARAM = &LIST[0]; - let ptr: *const bindings::OSSL_PARAM = std::ptr::from_ref(first); - - return ptr; -} - -#[named] -pub(super) unsafe extern "C" fn set_params( - vkeydata: *mut c_void, - params: *const OSSL_PARAM, -) -> c_int { - const ERROR_RET: c_int = 0; - const SUCCESS: c_int = 1; - - trace!(target: log_target!(), "{}", "Called!"); - let _keydata: &mut KeyPair = handleResult!(vkeydata.try_into()); - - let params = match OSSLParam::try_from(params) { - Ok(params) => params, - Err(e) => { - error!(target: log_target!(), "Failed decoding params: {:?}", e); - return ERROR_RET; - } - }; - - for p in params { - let key = match p.get_key() { - Some(key) => key, - None => { - error!(target: log_target!(), "Param without valid key {:?}", p); - return ERROR_RET; - } - }; - - if false && key == OSSL_PKEY_PARAM_SECURITY_BITS { - unreachable!(); - //let bytes: &[u8] = match p.get() { - // Some(bytes) => bytes, - // None => handleResult!(Err(anyhow!("Invalid ENCODED_PUBLIC_KEY"))), - //}; - //debug!(target: log_target!(), "The received encoded public key is (len: {}): {:X?}", bytes.len(), bytes); - - //keydata.public = Some(handleResult!(PublicKey::decode(bytes))); - } else { - debug!(target: log_target!(), "Ignoring param {:?}", key); - } - } - return SUCCESS; -} - -#[named] -pub(super) unsafe extern "C" fn settable_params(vprovctx: *mut c_void) -> *const OSSL_PARAM { - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - trace!(target: log_target!(), "{}", "Called!"); - let _provctx: &ProviderInstance<'_> = match vprovctx.try_into() { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "{}", e); - return ERROR_RET; - } - }; - - static LIST: &[CONST_OSSL_PARAM] = &[CONST_OSSL_PARAM::END]; - - let first: &bindings::OSSL_PARAM = &LIST[0]; - let ptr: *const bindings::OSSL_PARAM = std::ptr::from_ref(first); - - return ptr; -} - -#[named] -/// Implements key loading by object reference, also a constructor for a new Key object -/// -/// Refer to [provider-keymgmt(7ossl)] and [provider-object(7ossl)]. -/// -/// # Notes -/// -/// This function is tightly integrated with the -/// [`OSSL_FUNC_decoder_decode_fn`][provider-decoder(7ossl)] -/// exposed by [decoders registered][`super::decoder_functions`] -/// for [this algorithm][`super`] -/// by [this adapter][`super::super`]. -/// -/// Eventually this function is called by the callback passed to OSSL_FUNC_decoder_decode_fn -/// hence they must agree on how the reference is being passed around. -/// -/// [provider-keymgmt(7ossl)]: https://docs.openssl.org/master/man7/provider-keymgmt/ -/// [provider-object(7ossl)]: https://docs.openssl.org/master/man7/provider-object/ -/// [provider-decoder(7ossl)]: https://docs.openssl.org/master/man7/provider-decoder/ -pub(super) unsafe extern "C" fn load(reference: *const c_void, reference_sz: usize) -> *mut c_void { - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - trace!(target: log_target!(), "{}", "Called!"); - - assert_eq!(reference_sz, std::mem::size_of::()); - if reference.is_null() { - error!(target: log_target!(), "reference should not be NULL"); - unreachable!() - } - - let keypair = handleResult!(<&KeyPair>::try_from(reference as *mut c_void)); - debug!(target: log_target!(), "keypair: {keypair:#?}"); - - return std::ptr::from_ref(keypair).cast_mut() as *mut c_void; -} - -// based on OpenSSL 3.5's crypto/ml_dsa/ml_dsa_key.c:ossl_ml_dsa_key_equal() -// (and we can't just call it "match", because that's a Rust keyword) -#[named] -pub(super) unsafe extern "C" fn match_( - keydata1: *const c_void, - keydata2: *const c_void, - selection: c_int, -) -> c_int { - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - let keypair1 = handleResult!(<&KeyPair>::try_from(keydata1 as *mut c_void)); - let keypair2 = handleResult!(<&KeyPair>::try_from(keydata2 as *mut c_void)); - let mut key_checked = false; - - if (selection & OSSL_KEYMGMT_SELECT_KEYPAIR as c_int) != 0 { - if (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int) != 0 { - if keypair1.public != keypair2.public { - return ERROR_RET; - } - key_checked = true; - } - if !key_checked && (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY as c_int) != 0 { - if keypair1.private != keypair2.private { - return ERROR_RET; - } - key_checked = true; - } - return key_checked as c_int; - } - - return 1; -} - -pub(super) mod asn_definitions { - pub use crate::asn_definitions::x509_ml_dsa_2025 as defns; - - pub use defns::MLDSA44PrivateKey as PrivateKey; - pub use defns::MLDSA44PublicKey as PublicKey; -} - -#[cfg(test)] -mod tests { - use super::*; - - struct TestCTX<'a> { - provctx: ProviderInstance<'a>, - } - - fn setup<'a>() -> Result, OurError> { - use crate::tests::new_provctx_for_testing; - - crate::tests::common::setup()?; - - let provctx = new_provctx_for_testing(); - - let testctx = TestCTX { provctx }; - - Ok(testctx) - } - - #[test] - fn test_roundtrip_encode_decode() { - let testctx = setup().expect("Failed to initialize test setup"); - - let provctx = testctx.provctx; - - let keypair = KeyPair::generate_new(&provctx).expect("Failed to generate keypair"); - - match (keypair.public, keypair.private) { - (None, None) => panic!("No public or private key generated"), - (None, Some(_)) => panic!("No public key generated"), - (Some(_), None) => panic!("No private key generated"), - (Some(pk), Some(sk)) => { - let encoded_pk = pk.encode(); - let roundtripped_pk = PublicKey::decode(&encoded_pk).unwrap(); - // we can't use assert_eq! without having a Debug impl for both arguments - assert!(pk == roundtripped_pk); - - let encoded_sk = sk.encode(); - let roundtripped_sk = PrivateKey::decode(&encoded_sk).unwrap(); - assert!(sk == roundtripped_sk); - } - } - } - - #[test] - fn const_sanity_assertions() { - crate::tests::common::setup().expect("Failed to initialize test setup"); - - // Compare against https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-approximate-key-and-signatu - // except the SECRETKEY_LEN, which is 64 in that table because that document uses the - // assumption that only the seed of the ML-DSA secret key should be stored - assert_eq!(PUBKEY_LEN, 1344); - assert_eq!(SECRETKEY_LEN, 2592); - assert_eq!(SIGNATURE_LEN, 2516); - - assert_eq!(SECURITY_BITS, 128); - } -} diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs similarity index 88% rename from src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs rename to src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs index 799b5f9..7c07601 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs @@ -25,14 +25,23 @@ use ed25519_dalek as trad_backend_module; use pqcrypto_mldsa::mldsa44 as pq_backend_module; type PQPublicKey = pq_backend_module::PublicKey; -type PQPrivateKey = pq_backend_module::SecretKey; + +use helpers::MlDsaSeed; +#[derive(PartialEq)] +pub struct PQPrivateKey { + seed: MlDsaSeed, + expanded: pq_backend_module::SecretKey, +} + type TPublicKey = trad_backend_module::VerifyingKey; type TPrivateKey = trad_backend_module::SecretKey; use super::OurError as KMGMTError; type OurResult = anyhow::Result; -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; +use super::signature::{ + Signature, SignatureBytes, SignatureEncoding, SignerWithCtx, VerifierWithCtx, +}; pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); @@ -175,16 +184,38 @@ impl PublicKey { } } -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-prefix-label-and-ctx +// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-13#name-prefix-label-and-ctx const PREFIX: &[u8] = "CompositeAlgorithmSignatures2025".as_bytes(); const LABEL: &[u8] = "COMPSIG-MLDSA44-Ed25519-SHA512".as_bytes(); -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-verify -// There's no way to pass additional context info (`ctx` in the linked spec) into this Verifier -// trait's verify function, so we take `ctx` to be the empty string. impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) + } +} + +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // + // mandates the ctx maximum length should fit in a single byte. + // Note: this also matches FIPS 204 restriction on the `ctx` + // maximum allowed length of 255 bytes. + let ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + // get at the public keys let Self { pq_public_key, @@ -207,12 +238,13 @@ impl Verifier for PublicKey { let trad_sig: &[u8; Self::T_SIGNATURE_LEN] = trad_sig.try_into().expect("Unexpected length"); - // M' := Prefix || Domain || len(ctx) || ctx || r || PH( M ) + // M' := Prefix || Label || len(ctx) || ctx || r || PH( M ) // (here M is our `msg` argument) let msg_hash = Sha512::digest(msg); let mut M_prime = PREFIX.to_vec(); M_prime.extend_from_slice(LABEL); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment at top of impl) + M_prime.push(ctx_len); + M_prime.extend_from_slice(ctx); M_prime.extend(msg_hash); // verify with ML-DSA @@ -223,6 +255,8 @@ impl Verifier for PublicKey { VerificationError::GenericVerificationError, ) })?; + // the so-called `ctx` that gets passed to the ML-DSA verifier here is actually the Label, + // not the ctx that was prepended to the message hash pq_backend_module::verify_detached_signature_ctx( &pq_sig, M_prime.as_slice(), @@ -266,8 +300,19 @@ fn map_PQError_into_VerificationError( } } +impl PQPrivateKey { + pub fn new(seed: &MlDsaSeed) -> Result { + helpers::derive_mldsa_secret_key_from_seed(seed) + .map(|k| Self { + seed: *seed, + expanded: k, + }) + .ok_or(anyhow!("Unable to decode private key")) + } +} + impl PrivateKey { - const PQ_PRIVATE_KEY_LEN: usize = pq_backend_module::secret_key_bytes(); + const PQ_PRIVATE_KEY_LEN: usize = helpers::ML_DSA_SEED_SIZE; const T_PRIVATE_KEY_LEN: usize = trad_backend_module::SECRET_KEY_LENGTH; const PQ_SIGNATURE_LEN: usize = PublicKey::PQ_SIGNATURE_LEN; const T_SIGNATURE_LEN: usize = PublicKey::T_SIGNATURE_LEN; @@ -277,27 +322,22 @@ impl PrivateKey { pq_private_key, trad_private_key, } = self; - let mut bytes = - ::as_bytes( - pq_private_key, - ) - .to_vec(); + let mut bytes = pq_private_key.seed.to_vec(); bytes.extend(trad_private_key); bytes } pub fn decode(bytes: &[u8]) -> Result { - let (pq_bytes, trad_bytes) = bytes.split_at(pq_backend_module::secret_key_bytes()); - let pq_private_key = - ::from_bytes( - pq_bytes, + if bytes.len() != Self::byte_len() { + anyhow::bail!( + "Cannot decode MLDSA44-Ed25519 private key of length {}", + bytes.len() ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA44) returned {:?}", - e - ) - })?; + } + let (pq_bytes, trad_bytes) = bytes + .split_at_checked(Self::PQ_PRIVATE_KEY_LEN) + .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; + let pq_private_key = PQPrivateKey::new(pq_bytes.try_into()?)?; let trad_private_key = trad_bytes .try_into() .map_err(|_| anyhow!("Ed25519 secret key should be 32 bytes"))?; @@ -316,7 +356,7 @@ impl PrivateKey { } fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key) + super::helpers::derive_mldsa_public_key(&self.pq_private_key.expanded) } /// Derive a matching public key from this private key @@ -358,12 +398,11 @@ impl PrivateKey { debug!(target: log_target!(), "Parsed private key material out of ASN.1 for decoding!"); let (privkey, opt_pubkey) = match decodedprivkey { - ASNPrivateKey::seed(_seed) => unimplemented!(), - ASNPrivateKey::expandedKey(expandedKey) => { - let slice: &[u8] = &expandedKey; + ASNPrivateKey::seed(bytes) => { + let slice: &[u8] = &bytes; let privkey = keymgmt_functions::PrivateKey::decode(slice)?; - // We need to derive a public key from the private key, without a seed + // We need to derive a public key from the private key let pubkey = match privkey.derive_public_key() { Some(k) => k, None => { @@ -375,6 +414,7 @@ impl PrivateKey { }; (privkey, Some(pubkey)) } + ASNPrivateKey::expandedKey(_expandedKey) => unimplemented!(), ASNPrivateKey::both(_private_key_both) => unimplemented!(), }; Ok((privkey, opt_pubkey)) @@ -386,7 +426,7 @@ impl PrivateKey { use asn_definitions::PrivateKey as ASNPrivateKey; let raw_sk_bytes = self.encode(); - let asn_sk = ASNPrivateKey::expandedKey(raw_sk_bytes.into()); + let asn_sk = ASNPrivateKey::seed(raw_sk_bytes.into()); let asn_sk_bytes = match rasn::der::encode(&asn_sk) { Ok(v) => v, Err(e) => { @@ -398,17 +438,38 @@ impl PrivateKey { } } -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-sign -// Just like with the Verifier above, there's no way to pass additional context info (`ctx` in the -// linked spec) into this Signer trait's try_sign function, so we take `ctx` to be the empty string. impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { + self.try_sign_with_ctx(msg, &[]) + } +} + +impl SignerWithCtx for PrivateKey { + #[named] + fn try_sign_with_ctx( + &self, + msg: &[u8], + ctx: &[u8], + ) -> Result { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // + // mandates the ctx maximum length should fit in a single byte. + // Note: this also matches FIPS 204 restriction on the `ctx` + // maximum allowed length of 255 bytes. + let ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + // M' := Prefix || Label || len(ctx) || ctx || PH( M ) // (here M is our `msg` argument) let msg_hash = Sha512::digest(msg); let mut M_prime = PREFIX.to_vec(); M_prime.extend_from_slice(LABEL); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment above) + M_prime.push(ctx_len); + M_prime.extend_from_slice(ctx); M_prime.extend(msg_hash); // get at the private keys @@ -418,9 +479,10 @@ impl Signer for PrivateKey { } = self; // sign with ML-DSA - // (the Label being used as the `ctx` here refers to the underlying ML-DSA - // signature operation, and has nothing to do with the empty `ctx` string from the spec) - let pq_signature = pq_backend_module::detached_sign_ctx(&M_prime, LABEL, pq_private_key); + // the so-called `ctx` that gets passed to the ML-DSA signer here is actually the Label, + // not the ctx that was prepended to the message hash + let pq_signature = + pq_backend_module::detached_sign_ctx(&M_prime, LABEL, &pq_private_key.expanded); // sign with Ed25519 let trad_signature = @@ -496,12 +558,20 @@ impl<'a> KeyPair<'a> { fn generate(provctx: &'a ProviderInstance) -> Result { trace!(target: log_target!(), "Called"); - // Isn't it weird that this operation can't fail? What does the pqclean implementation do if - // it can't find a randomness source or it can't allocate memory or something? - let (pq_public_key, pq_private_key) = pq_backend_module::keypair(); + // generate PQ private key + let prng = provctx.get_rng(); + let mut seed_buf = [0u8; helpers::ML_DSA_SEED_SIZE]; + let pq_private_key = match prng.try_fill_bytes(&mut seed_buf) { + Ok(_) => PQPrivateKey::new(&seed_buf)?, + Err(_) => anyhow::bail!("Unable to generate randomness for ML-DSA keygen"), + }; + + // derive PQ public key from it + let pq_public_key = helpers::derive_mldsa_public_key(&pq_private_key.expanded).ok_or( + anyhow!("Unable to derive public ML-DSA key from private key"), + )?; - // Similarly, it seems weird that this can't fail. Hopefully a different layer can handle it - // if something goes wrong here. + // generate traditional keypair let trad_keypair = trad_backend_module::SigningKey::generate(provctx.get_rng()); let trad_private_key = trad_keypair.to_bytes(); let trad_public_key = trad_keypair.verifying_key(); @@ -1110,11 +1180,9 @@ mod tests { fn const_sanity_assertions() { crate::tests::common::setup().expect("Failed to initialize test setup"); - // Compare against https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-maximum-key-and-signature-s - // except the SECRETKEY_LEN, which is 64 in that table because that document uses the - // assumption that only the seed of the ML-DSA secret key should be stored + // Compare against https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-13#name-maximum-key-and-signature-s assert_eq!(PUBKEY_LEN, 1344); - assert_eq!(SECRETKEY_LEN, 2592); + assert_eq!(SECRETKEY_LEN, 64); assert_eq!(SIGNATURE_LEN, 2484); assert_eq!(SECURITY_BITS, 128); diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index 293d9ba..21ae5ca 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -399,5 +399,50 @@ pub(super) use decoder_functions::DER2PrivateKeyInfo as DECODER_DER2PrivateKeyIn pub(super) use decoder_functions::DER2SubjectPublicKeyInfo as DECODER_DER2SubjectPublicKeyInfo; pub(super) use encoder_functions::PrivateKeyInfo2DER as ENCODER_PrivateKeyInfo2DER; pub(super) use encoder_functions::PrivateKeyInfo2PEM as ENCODER_PrivateKeyInfo2PEM; +pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2Text; +pub(super) use encoder_functions::PubKeyStructureless2Text as ENCODER_PubKeyStructureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; + +#[cfg(test)] +mod tests { + use super::*; + use crate::adapters::common::wycheproof::*; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::mldsa_verify; + + struct Mldsa65; + + impl_sigalg_verify_variant!(Mldsa65, keymgmt_functions::PublicKey, signature::Signature); + + #[test] + fn test_mldsa_65_verify_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa65Verify); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; + use wycheproof::mldsa_sign; + + impl_sigalg_sign_variant!(Mldsa65, keymgmt_functions::PrivateKey, signature::Signature); + + #[test] + fn test_mldsa_65_sign_seed_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa65SignSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } + + #[test] + fn test_mldsa_65_sign_noseed_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa65SignNoSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } +} diff --git a/src/adapters/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index 607adc0..3f478d2 100644 --- a/src/adapters/pqclean/MLDSA65/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65/encoder_functions.rs @@ -464,6 +464,13 @@ impl DoesSelection for PrivateKeyInfo2PEM { // We can use the same does_selection function as PrivateKeyInfo2DER, so there's no need to call // the make_does_selection_fn macro again. +// generate the plain text encoder +use crate::adapters::common::transcoders::make_privkey_text_encoder; +make_privkey_text_encoder!( + PrivateKeyInfo2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text',structure='PrivateKeyInfo'" +); + pub(crate) struct SubjectPublicKeyInfo2DER(); impl Encoder for SubjectPublicKeyInfo2DER { const PROPERTY_DEFINITION: &'static CStr = @@ -722,3 +729,10 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to // call the make_does_selection_fn macro again. + +// generate the plain text encoder +use crate::adapters::common::transcoders::make_pubkey_text_encoder; +make_pubkey_text_encoder!( + PubKeyStructureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs index 6eb2d2b..842b499 100644 --- a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs @@ -23,7 +23,9 @@ use pqcrypto_mldsa::mldsa65 as backend_module; use super::OurError as KMGMTError; type OurResult = anyhow::Result; -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; +use super::signature::{ + Signature, SignatureBytes, SignatureEncoding, SignerWithCtx, VerifierWithCtx, +}; pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); @@ -118,6 +120,29 @@ impl PublicKey { impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) + } +} + +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // FIPS 204 specifies maximum ctx len is 255 bytes, so it should + // fit into a u8 + let _ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + let sig = sig.to_bytes(); let sig = sig.as_ref(); use pqcrypto_traits::sign::DetachedSignature; @@ -127,7 +152,7 @@ impl Verifier for PublicKey { VerificationError::GenericVerificationError, ) })?; - backend_module::verify_detached_signature(&sig, msg, &self.0) + backend_module::verify_detached_signature_ctx(&sig, msg, ctx, &self.0) .map_err(map_into_VerificationError) .map_err(forge::crypto::signature::Error::from_source) } @@ -158,14 +183,9 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - let k = ::from_bytes(bytes) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA65) returned {:?}", - e - ) - })?; - Ok(Self(k)) + super::helpers::decode_mldsa_secret_key(bytes) + .map(Self) + .ok_or(anyhow!("Unable to decode private key")) } pub const fn byte_len() -> usize { @@ -178,7 +198,7 @@ impl PrivateKey { /// Derive a matching public key from this private key pub fn derive_public_key(&self) -> Option { - let pk = super::helpers::derive_public_key(&self.0); + let pk = super::helpers::derive_mldsa_public_key(&self.0); pk.map(|inner| PublicKey(inner)) } @@ -238,8 +258,26 @@ impl PrivateKey { impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { + self.try_sign_with_ctx(msg, &[]) + } +} + +impl SignerWithCtx for PrivateKey { + #[named] + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result { + trace!(target: log_target!(), "Called"); + let Self(ref sk) = self; - let signature = backend_module::detached_sign(msg, sk); + + // validate ctx length + // FIPS 204 specifies maximum ctx len is 255 bytes, so it should + // fit into a u8 + let _ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + + let signature = backend_module::detached_sign_ctx(msg, ctx, sk); Signature::try_from(signature.as_bytes()) .map_err(|e| forge::crypto::signature::Error::from_source(e)) } diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 336e1eb..5f3183a 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -43,14 +43,8 @@ use bindings::{OSSL_FUNC_signature_verify_init_fn, OSSL_FUNC_SIGNATURE_VERIFY_IN mod decoder_functions; mod encoder_functions; -#[cfg(feature = "_composite_sigs_draft_07")] -#[path = "./MLDSA65_Ed25519/keymgmt_functions_draft07.rs"] -mod keymgmt_functions; -#[cfg(feature = "_composite_sigs_draft_12")] -#[path = "./MLDSA65_Ed25519/keymgmt_functions_draft12.rs"] -mod keymgmt_functions; -#[cfg(feature = "_composite_sigs_draft_12_postWGLC")] -#[path = "./MLDSA65_Ed25519/keymgmt_functions_draft12.rs"] +#[cfg(feature = "_composite_sigs_draft_13")] +#[path = "./MLDSA65_Ed25519/keymgmt_functions_draft13.rs"] mod keymgmt_functions; #[path = "../common/signature.rs"] @@ -62,61 +56,23 @@ mod signature_functions; pub(crate) type OurError = anyhow::Error; pub(crate) use anyhow::anyhow; -#[cfg(feature = "_composite_sigs_draft_07")] -mod consts_composite_sigs_draft_07 { - use super::CStr; - - // Ensure proper null-terminated C string - // https://docs.openssl.org/master/man7/provider/#algorithm-naming - pub const NAMES: &CStr = - c"id-MLDSA65-Ed25519-SHA512:mldsa65_ed25519:2.16.840.1.114027.80.9.1.11"; - - // OID from - // OID should be a substring of NAMES - pub const OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 114027, 80, 9, 1, 11); - pub const OID_PKCS8: pkcs8::ObjectIdentifier = - pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.114027.80.9.1.11"); - pub const SIGALG_OID: Option<&CStr> = Some(c"2.16.840.1.114027.80.9.1.11"); -} -#[cfg(feature = "_composite_sigs_draft_07")] -pub use consts_composite_sigs_draft_07::{NAMES, OID, OID_PKCS8, SIGALG_OID}; - -#[cfg(feature = "_composite_sigs_draft_12")] -mod consts_composite_sigs_draft_12 { - use super::CStr; - - // Ensure proper null-terminated C string - // https://docs.openssl.org/master/man7/provider/#algorithm-naming - pub const NAMES: &CStr = - c"id-MLDSA65-Ed25519-SHA512:mldsa65_ed25519:2.16.840.1.114027.80.9.1.31"; - - // OID from - // OID should be a substring of NAMES - pub const OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 114027, 80, 9, 1, 31); - pub const OID_PKCS8: pkcs8::ObjectIdentifier = - pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.114027.80.9.1.31"); - pub const SIGALG_OID: Option<&CStr> = Some(c"2.16.840.1.114027.80.9.1.31"); -} -#[cfg(feature = "_composite_sigs_draft_12")] -pub use consts_composite_sigs_draft_12::{NAMES, OID, OID_PKCS8, SIGALG_OID}; - -#[cfg(feature = "_composite_sigs_draft_12_postWGLC")] -mod consts_composite_sigs_draft_12_postWGLC { +#[cfg(feature = "_composite_sigs_draft_13")] +mod consts_composite_sigs_draft_13 { use super::CStr; // Ensure proper null-terminated C string // https://docs.openssl.org/master/man7/provider/#algorithm-naming pub const NAMES: &CStr = c"id-MLDSA65-Ed25519-SHA512:mldsa65_ed25519:1.3.6.1.5.5.7.6.48"; - // OID from + // OID from // OID should be a substring of NAMES pub const OID: asn1::ObjectIdentifier = asn1::oid!(1, 3, 6, 1, 5, 5, 7, 6, 48); pub const OID_PKCS8: pkcs8::ObjectIdentifier = pkcs8::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.6.48"); pub const SIGALG_OID: Option<&CStr> = Some(c"1.3.6.1.5.5.7.6.48"); } -#[cfg(feature = "_composite_sigs_draft_12_postWGLC")] -pub use consts_composite_sigs_draft_12_postWGLC::{NAMES, OID, OID_PKCS8, SIGALG_OID}; +#[cfg(feature = "_composite_sigs_draft_13")] +pub use consts_composite_sigs_draft_13::{NAMES, OID, OID_PKCS8, SIGALG_OID}; /// NAME should be a substring of NAMES pub(crate) const NAME: &CStr = c"mldsa65_ed25519"; @@ -209,7 +165,8 @@ pub(crate) mod capabilities { /// /// # NOTE /// - /// > The OID for mldsa65_ed25519 comes from the [IETF LAMPS draft](https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-07#name-algorithm-identifiers). + /// > The OID for mldsa65_ed25519 comes from the + /// [IETF LAMPS draft](https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-13#name-algorithm-identifiers-and-p). const SIGALG_OID: Option<&CStr> = super::super::SIGALG_OID; const SECURITY_BITS: u32 = super::super::SECURITY_BITS; @@ -406,5 +363,51 @@ pub(super) use decoder_functions::DER2PrivateKeyInfo as DECODER_DER2PrivateKeyIn pub(super) use decoder_functions::DER2SubjectPublicKeyInfo as DECODER_DER2SubjectPublicKeyInfo; pub(super) use encoder_functions::PrivateKeyInfo2DER as ENCODER_PrivateKeyInfo2DER; pub(super) use encoder_functions::PrivateKeyInfo2PEM as ENCODER_PrivateKeyInfo2PEM; +pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2Text; +pub(super) use encoder_functions::PubKeyStructureless2Text as ENCODER_PubKeyStructureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; + +#[cfg(test)] +mod tests { + use super::*; + use crate::adapters::common::wycheproof::*; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::composite_mldsa_verify; + + #[allow(non_camel_case_types)] + struct Mldsa65_Ed25519; + + impl_sigalg_verify_variant!( + Mldsa65_Ed25519, + keymgmt_functions::PublicKey, + signature::Signature + ); + + #[test] + fn test_mldsa_65_ed_25519_verify_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_composite_mldsa_wycheproof_verify_tests::( + composite_mldsa_verify::TestName::MlDsa65Ed25519, + ); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; + use wycheproof::composite_mldsa_sign; + + impl_sigalg_sign_variant!( + Mldsa65_Ed25519, + keymgmt_functions::PrivateKey, + signature::Signature + ); + + #[test] + fn test_mldsa_65_ed_25519_sign_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_composite_mldsa_wycheproof_sign_tests::( + composite_mldsa_sign::TestName::MlDsa65Ed25519, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } +} diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs index a4c85ff..0be7e26 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs @@ -464,6 +464,13 @@ impl DoesSelection for PrivateKeyInfo2PEM { // We can use the same does_selection function as PrivateKeyInfo2DER, so there's no need to call // the make_does_selection_fn macro again. +// generate the plain text encoder +use crate::adapters::common::transcoders::make_privkey_text_encoder; +make_privkey_text_encoder!( + PrivateKeyInfo2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text',structure='PrivateKeyInfo'" +); + pub(crate) struct SubjectPublicKeyInfo2DER(); impl Encoder for SubjectPublicKeyInfo2DER { const PROPERTY_DEFINITION: &'static CStr = @@ -722,3 +729,10 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to // call the make_does_selection_fn macro again. + +// generate the plain text encoder +use crate::adapters::common::transcoders::make_pubkey_text_encoder; +make_pubkey_text_encoder!( + PubKeyStructureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft07.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft07.rs deleted file mode 100644 index db41839..0000000 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft07.rs +++ /dev/null @@ -1,1134 +0,0 @@ -#![allow(unreachable_code)] - -use super::*; -use bindings::{ - OSSL_CALLBACK, OSSL_KEYMGMT_SELECT_KEYPAIR, OSSL_KEYMGMT_SELECT_PRIVATE_KEY, - OSSL_KEYMGMT_SELECT_PUBLIC_KEY, OSSL_PKEY_PARAM_BITS, OSSL_PKEY_PARAM_MANDATORY_DIGEST, - OSSL_PKEY_PARAM_MAX_SIZE, OSSL_PKEY_PARAM_PRIV_KEY, OSSL_PKEY_PARAM_PUB_KEY, - OSSL_PKEY_PARAM_SECURITY_BITS, -}; -use forge::{ - bindings, - operations::keymgmt::selection::Selection, - operations::signature::{Signer, VerificationError, Verifier}, - ossl_callback::OSSLCallback, - osslparams::*, -}; -use pqcrypto_traits::sign::DetachedSignature; -use sha2::{Digest, Sha512}; -use std::{ - ffi::{c_int, c_void}, - fmt::Debug, -}; - -use ed25519_dalek as trad_backend_module; -use pqcrypto_mldsa::mldsa65 as pq_backend_module; - -type PQPublicKey = pq_backend_module::PublicKey; -type PQPrivateKey = pq_backend_module::SecretKey; -type TPublicKey = trad_backend_module::VerifyingKey; -type TPrivateKey = trad_backend_module::SecretKey; - -use super::OurError as KMGMTError; -type OurResult = anyhow::Result; - -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; - -pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); -pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); -pub(crate) const SIGNATURE_LEN: usize = PrivateKey::signature_bytes(); - -// The wrapped key from the pqcrypto crate has to be public, or else we can't access it to use it -// with the pqcrypto sign and verify functions. -#[derive(PartialEq)] -pub struct PublicKey { - pq_public_key: PQPublicKey, - trad_public_key: TPublicKey, -} - -#[derive(PartialEq)] -pub struct PrivateKey { - pq_private_key: PQPrivateKey, - trad_private_key: TPrivateKey, -} - -impl core::fmt::Debug for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PublicKey") - .field("pq_public_key", &"") - .field("trad_public_key", &self.trad_public_key) - .finish() - } -} - -impl PublicKey { - const PQ_PUBLIC_KEY_LEN: usize = pq_backend_module::public_key_bytes(); - const T_PUBLIC_KEY_LEN: usize = trad_backend_module::PUBLIC_KEY_LENGTH; - const PQ_SIGNATURE_LEN: usize = pq_backend_module::signature_bytes(); - const T_SIGNATURE_LEN: usize = trad_backend_module::SIGNATURE_LENGTH; - - pub fn decode(bytes: &[u8]) -> Result { - if bytes.len() != Self::byte_len() { - return Err(anyhow!( - "Public key should be {:?} bytes (got {:?})", - Self::byte_len(), - bytes.len() - )); - } - - // if we're here, then the length is correct, and we can safely split_at() and expect() - let (pq_bytes, trad_bytes) = bytes.split_at(Self::PQ_PUBLIC_KEY_LEN); - let pq_bytes: &[u8; Self::PQ_PUBLIC_KEY_LEN] = - pq_bytes.try_into().expect("slice has unexpected size"); - let trad_bytes: &[u8; Self::T_PUBLIC_KEY_LEN] = - trad_bytes.try_into().expect("slice has unexpected size"); - - let pq_public_key = - ::from_bytes( - pq_bytes, - ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::PublicKey::from_bytes (MLDSA65) returned {:?}", - e - ) - })?; - let trad_public_key = - trad_backend_module::VerifyingKey::from_bytes(trad_bytes).map_err(|e| { - anyhow!( - "trad_backend_module::VerifyingKey::from_bytes (Ed25519) returned {:?}", - e - ) - })?; - Ok(Self { - pq_public_key, - trad_public_key, - }) - } - - pub fn encode(&self) -> Vec { - let Self { - pq_public_key, - trad_public_key, - } = self; - let mut bytes = - ::as_bytes( - pq_public_key, - ) - .to_vec(); - bytes.extend(trad_public_key.as_bytes()); - bytes - } - - pub const fn byte_len() -> usize { - Self::PQ_PUBLIC_KEY_LEN + Self::T_PUBLIC_KEY_LEN - } - - pub const fn signature_bytes() -> usize { - PrivateKey::signature_bytes() - } - - #[named] - pub fn from_DER(pk_der_bytes: &[u8]) -> OurResult { - trace!(target: log_target!(), "{}", "Called!"); - - use asn_definitions::PublicKey as ASNPublicKey; - - let decodedpubkey: ASNPublicKey; - let slice = match pk_der_bytes.len() { - PUBKEY_LEN => pk_der_bytes, - - #[cfg(any())] - _ => { - decodedpubkey = match rasn::der::decode(pk_der_bytes) { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "Failed to decode the inner public key: {e:?}"); - return Err(OurError::from(e)); - } - }; - - debug!(target: log_target!(), "Parsed public key material out of ASN.1 for decoding!"); - - let slice: &[u8] = decodedpubkey.0.as_slice(); - slice - } - - #[cfg(not(any()))] - _ => { - let _ = decodedpubkey; - unreachable!(); - } - }; - - debug_assert_eq!(slice.len(), PUBKEY_LEN); - let pubkey = Self::decode(slice)?; - - Ok(pubkey) - } - - #[named] - pub fn to_DER(&self) -> OurResult> { - trace!(target: log_target!(), "{}", "Called!"); - - Ok(self.encode()) - } -} - -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-prefix-domain-separators-an -const PREFIX: &[u8] = "CompositeAlgorithmSignatures2025".as_bytes(); -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-domain-separator-values -const DOMAIN_SEPARATOR: &[u8] = &[ - 0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0B, -]; -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-pre-hashing-and-randomizer -const RANDOMIZER_LEN: usize = 32; - -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-verify -// There's no way to pass additional context info (`ctx` in the linked spec) into this Verifier -// trait's verify function, so we take `ctx` to be the empty string. -impl Verifier for PublicKey { - #[named] - fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - // get at the public keys - let Self { - pq_public_key, - trad_public_key, - } = self; - - // separate the parts of the signature - let sig = sig.to_bytes(); - let sig = sig.as_ref(); - if sig.len() != SIGNATURE_LEN { - error!(target: log_target!(), "Signature should be {SIGNATURE_LEN:} bytes (got {})", sig.len()); - return Err(forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - )); - } - // if we get here, we know we have the right number of bytes, so these calls to split_at() - // and expect() won't panic - let (randomizer_bytes, tail_bytes) = sig.split_at(RANDOMIZER_LEN); - let (pq_sig, trad_sig) = tail_bytes.split_at(Self::PQ_SIGNATURE_LEN); - let pq_sig: &[u8; Self::PQ_SIGNATURE_LEN] = pq_sig.try_into().expect("Unexpected length"); - let trad_sig: &[u8; Self::T_SIGNATURE_LEN] = - trad_sig.try_into().expect("Unexpected length"); - - // M' := Prefix || Domain || len(ctx) || ctx || r || PH( M ) - // (here M is our `msg` argument) - let msg_hash = Sha512::digest(msg); - let mut M_prime = PREFIX.to_vec(); - M_prime.extend_from_slice(DOMAIN_SEPARATOR); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment at top of impl) - M_prime.extend_from_slice(&randomizer_bytes); - M_prime.extend(msg_hash); - - // verify with ML-DSA - use pqcrypto_traits::sign::DetachedSignature; - let pq_sig = pq_backend_module::DetachedSignature::from_bytes(pq_sig).map_err(|e| { - error!(target: log_target!(), "Error when verifying PQ signature: {e:?}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - pq_backend_module::verify_detached_signature_ctx( - &pq_sig, - M_prime.as_slice(), - DOMAIN_SEPARATOR, - pq_public_key, - ) - .map_err(map_PQError_into_VerificationError) - .map_err(forge::crypto::signature::Error::from_source)?; - - // verify with Ed25519 - let trad_sig = trad_backend_module::Signature::from_bytes(trad_sig); - trad_public_key - .verify_strict(M_prime.as_slice(), &trad_sig) - // this backend uses an opaque error type, so no need for a separate fn with a match arm - .map_err(|e| { - error!(target: log_target!(), "Error when verifying traditional signature: {e:?}"); - VerificationError::GenericVerificationError - }) - .map_err(forge::crypto::signature::Error::from_source)?; - - // if we got here, both verifications passed - Ok(()) - } -} - -#[named] -fn map_PQError_into_VerificationError( - value: pqcrypto_traits::sign::VerificationError, -) -> VerificationError { - match value { - pqcrypto_traits::sign::VerificationError::InvalidSignature => { - VerificationError::InvalidSignature - } - pqcrypto_traits::sign::VerificationError::UnknownVerificationError => { - VerificationError::GenericVerificationError - } - e => { - warn!(target: log_target!(), "Unknown error {e:#?}"); - VerificationError::GenericVerificationError - } - } -} - -impl PrivateKey { - const PQ_PRIVATE_KEY_LEN: usize = pq_backend_module::secret_key_bytes(); - const T_PRIVATE_KEY_LEN: usize = trad_backend_module::SECRET_KEY_LENGTH; - const PQ_SIGNATURE_LEN: usize = PublicKey::PQ_SIGNATURE_LEN; - const T_SIGNATURE_LEN: usize = PublicKey::T_SIGNATURE_LEN; - - pub fn encode(&self) -> Vec { - let Self { - pq_private_key, - trad_private_key, - } = self; - let mut bytes = - ::as_bytes( - pq_private_key, - ) - .to_vec(); - bytes.extend(trad_private_key); - bytes - } - - pub fn decode(bytes: &[u8]) -> Result { - let (pq_bytes, trad_bytes) = bytes.split_at(pq_backend_module::secret_key_bytes()); - let pq_private_key = - ::from_bytes( - pq_bytes, - ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA65) returned {:?}", - e - ) - })?; - let trad_private_key = trad_bytes - .try_into() - .map_err(|_| anyhow!("Ed25519 secret key should be 32 bytes"))?; - Ok(Self { - pq_private_key, - trad_private_key, - }) - } - - pub const fn byte_len() -> usize { - Self::PQ_PRIVATE_KEY_LEN + Self::T_PRIVATE_KEY_LEN - } - - pub const fn signature_bytes() -> usize { - RANDOMIZER_LEN + Self::PQ_SIGNATURE_LEN + Self::T_SIGNATURE_LEN - } - - fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key) - } - - /// Derive a matching public key from this private key - #[named] - pub fn derive_public_key(&self) -> Option { - trace!(target: log_target!(), "Called"); - - let t_sk = &self.trad_private_key; - let t_sk = trad_backend_module::SigningKey::from_bytes(t_sk); - let t_pk = t_sk.verifying_key(); - - let pq_pk = match self.derive_PQ_public_key() { - Some(pk) => pk, - None => { - return None; - } - }; - - let pk = PublicKey { - pq_public_key: pq_pk, - trad_public_key: t_pk, - }; - Some(pk) - } - - #[named] - pub fn from_DER(sk_der_bytes: &[u8]) -> OurResult<(Self, Option)> { - use asn_definitions::PrivateKey as ASNPrivateKey; - trace!(target: log_target!(), "Called"); - - let decodedprivkey = match rasn::der::decode::(sk_der_bytes) { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "Failed to decode the inner private key: {e:?}"); - return Err(OurError::from(e)); - } - }; - - debug!(target: log_target!(), "Parsed private key material out of ASN.1 for decoding!"); - - let (privkey, opt_pubkey) = match decodedprivkey { - ASNPrivateKey::seed(_seed) => unimplemented!(), - ASNPrivateKey::expandedKey(expandedKey) => { - let slice: &[u8] = &expandedKey; - let privkey = keymgmt_functions::PrivateKey::decode(slice)?; - - // We need to derive a public key from the private key, without a seed - let pubkey = match privkey.derive_public_key() { - Some(k) => k, - None => { - error!(target: log_target!(), "Could not derive the public key from the inner private key"); - return Err(anyhow!( - "Could not derive the public key from the inner private key" - )); - } - }; - (privkey, Some(pubkey)) - } - ASNPrivateKey::both(_private_key_both) => unimplemented!(), - }; - Ok((privkey, opt_pubkey)) - } - - #[named] - pub fn to_DER(&self) -> OurResult> { - trace!(target: log_target!(), "Called"); - use asn_definitions::PrivateKey as ASNPrivateKey; - - let raw_sk_bytes = self.encode(); - let asn_sk = ASNPrivateKey::expandedKey(raw_sk_bytes.into()); - let asn_sk_bytes = match rasn::der::encode(&asn_sk) { - Ok(v) => v, - Err(e) => { - error!(target: log_target!(), "Failed to encode private key: {e:?}"); - return Err(OurError::from(e)); - } - }; - Ok(asn_sk_bytes) - } -} - -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-sign -// Just like with the Verifier above, there's no way to pass additional context info (`ctx` in the -// linked spec) into this Signer trait's try_sign function, so we take `ctx` to be the empty string. -impl Signer for PrivateKey { - fn try_sign(&self, msg: &[u8]) -> Result { - // randomizer = Random(32) - let randomizer: [u8; RANDOMIZER_LEN] = rand::random(); - // M' := Prefix || Domain || len(ctx) || ctx || r || PH( M ) - // (here M is our `msg` argument) - let msg_hash = Sha512::digest(msg); - let mut M_prime = PREFIX.to_vec(); - M_prime.extend_from_slice(DOMAIN_SEPARATOR); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment above) - M_prime.extend_from_slice(&randomizer); - M_prime.extend(msg_hash); - - // get at the private keys - let Self { - pq_private_key, - trad_private_key, - } = self; - - // sign with ML-DSA - // (the domain separator being used as the `ctx` here refers to the underlying ML-DSA - // signature operation, and has nothing to do with the empty `ctx` string from the spec) - let pq_signature = - pq_backend_module::detached_sign_ctx(&M_prime, DOMAIN_SEPARATOR, pq_private_key); - - // sign with Ed25519 - let trad_signature = - trad_backend_module::SigningKey::from_bytes(trad_private_key).sign(&M_prime); - - // build the result - let mut signature = randomizer.to_vec(); - signature.extend_from_slice(pq_signature.as_bytes()); - signature.extend_from_slice(&trad_signature.to_bytes()); - - Signature::try_from(signature.as_slice()) - .map_err(|e| forge::crypto::signature::Error::from_source(e)) - } -} - -#[expect(dead_code)] -pub struct KeyPair<'a> { - pub private: Option, - pub public: Option, - provctx: &'a ProviderInstance<'a>, -} - -impl<'a> Debug for KeyPair<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let private = match &self.private { - #[cfg(not(debug_assertions))] // code compiled only in release builds - Some(_) => { - todo!("remove private key printing also from development builds"); - format!("{}", "present") - } - #[cfg(debug_assertions)] // code compiled only in development builds - Some(p) => { - format!("{:02x?}", p.encode()) - } - None => format!("{:?}", None::<()>), - }; - let public = match &self.public { - Some(p) => format!("{:02x?}", p.encode()), - None => format!("{:?}", None::<()>), - }; - f.debug_struct("KeyPair") - .field("private", &private) - .field("public", &public) - .finish() - } -} - -impl<'a> KeyPair<'a> { - #[named] - fn new(provctx: &'a ProviderInstance) -> Self { - trace!(target: log_target!(), "Called"); - KeyPair { - private: None, - public: None, - provctx: provctx, - } - } - - #[named] - pub(super) fn from_parts( - provctx: &'a ProviderInstance, - private: Option, - public: Option, - ) -> Self { - trace!(target: log_target!(), "Called"); - KeyPair { - private, - public, - provctx, - } - } - - #[named] - fn generate(provctx: &'a ProviderInstance) -> Result { - trace!(target: log_target!(), "Called"); - - // Isn't it weird that this operation can't fail? What does the pqclean implementation do if - // it can't find a randomness source or it can't allocate memory or something? - let (pq_public_key, pq_private_key) = pq_backend_module::keypair(); - - // Similarly, it seems weird that this can't fail. Hopefully a different layer can handle it - // if something goes wrong here. - let trad_keypair = trad_backend_module::SigningKey::generate(provctx.get_rng()); - let trad_private_key = trad_keypair.to_bytes(); - let trad_public_key = trad_keypair.verifying_key(); - - Ok(KeyPair { - private: Some(PrivateKey { - pq_private_key, - trad_private_key, - }), - public: Some(PublicKey { - pq_public_key, - trad_public_key, - }), - provctx, - }) - } - - #[cfg(test)] - #[named] - pub(crate) fn generate_new(provctx: &'a ProviderInstance) -> Result { - trace!(target: log_target!(), "Called"); - let genctx = GenCTX::new(provctx, Selection::KEYPAIR); - let r = genctx.generate()?; - - Ok(Self { - private: r.private, - public: r.public, - provctx, - }) - } -} - -impl<'a> Signer for KeyPair<'a> { - #[named] - fn try_sign(&self, msg: &[u8]) -> Result { - trace!(target: log_target!(), "Called"); - - let sk = self - .private - .as_ref() - .ok_or_else(|| { - anyhow!( - "This keypair does not have a private key, so it cannot generate signatures" - ) - }) - .map_err(forge::crypto::signature::Error::from_source)?; - Ok(sk.try_sign(msg)?) - } -} - -impl<'a> Verifier for KeyPair<'a> { - #[named] - fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - trace!(target: log_target!(), "Called"); - - let pk = self - .public - .as_ref() - .ok_or_else(|| { - anyhow!("This keypair does not have a public key, so it cannot verify signatures") - }) - .map_err(|e| { - error!("{e:#}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - pk.verify(msg, sig) - } -} - -impl TryFrom<*mut c_void> for &mut KeyPair<'_> { - type Error = KMGMTError; - - #[named] - fn try_from(vptr: *mut c_void) -> Result { - trace!(target: log_target!(), "Called for {}", - "impl TryFrom<*mut c_void> for &mut KeyPair" - ); - let ptr = vptr as *mut KeyPair; - if ptr.is_null() { - return Err(anyhow::anyhow!("vptr was null")); - } - Ok(unsafe { &mut *ptr }) - } -} - -impl TryFrom<*mut c_void> for &KeyPair<'_> { - type Error = KMGMTError; - - #[named] - fn try_from(vptr: *mut core::ffi::c_void) -> Result { - trace!(target: log_target!(), "Called for {}", "impl<'a> TryFrom<*mut core::ffi::c_void> for &KeyPair<'a>"); - let r: &mut KeyPair = vptr.try_into()?; - Ok(r) - } -} - -impl TryFrom<*const c_void> for &KeyPair<'_> { - type Error = KMGMTError; - - #[named] - fn try_from(vptr: *const c_void) -> Result { - trace!(target: log_target!(), "Called for {}", "impl<'a> TryFrom<*const c_void> for &KeyPair<'a>"); - let mut_vptr = vptr as *mut c_void; - let r: &mut KeyPair = mut_vptr.try_into()?; - Ok(r) - } -} - -#[named] -pub(super) unsafe extern "C" fn new(vprovctx: *mut c_void) -> *mut c_void { - trace!(target: log_target!(), "{}", "Called!"); - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - let provctx: &ProviderInstance<'_> = handleResult!(vprovctx.try_into()); - - let keypair: Box> = Box::new(KeyPair::new(provctx)); - return Box::into_raw(keypair).cast(); -} - -#[named] -pub(super) unsafe extern "C" fn free(vkey: *mut c_void) { - trace!(target: log_target!(), "{}", "Called!"); - let /* mut */kp: Box = unsafe { Box::from_raw(vkey.cast()) }; - //todo!("Cleanse the private key data") - //todo!("Free the key data") - drop(kp); -} - -#[named] -pub(super) unsafe extern "C" fn has(vkeydata: *const c_void, selection: c_int) -> c_int { - const ERROR_RET: c_int = 0; - - trace!(target: log_target!(), "{}", "Called!"); - - let selection: u32 = selection.try_into().unwrap(); - - // From https://github.com/openssl/openssl/blob/fb55383c65bb47eef3bf5f73be5a0ad41d81bb3f/providers/implementations/keymgmt/ml_dsa_kmgmt.c#L145-L155 - if (selection & OSSL_KEYMGMT_SELECT_KEYPAIR) == 0 { - return 1; // the selection is not missing - } - - let keydata: &KeyPair = handleResult!(vkeydata.try_into()); - - // from https://github.com/openssl/openssl/blob/fb55383c65bb47eef3bf5f73be5a0ad41d81bb3f/crypto/ml_dsa/ml_dsa_key.c#L285-L297 - if (selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0 { - // Note that the public key always exists if there is a private key - if keydata.public.is_none() { - return 0; // No public key - } - if (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0 && keydata.private.is_none() { - return 0; // No private key - } - return 1; - } - - return 0; -} - -#[named] -pub(super) unsafe extern "C" fn gen( - vgenctx: *mut c_void, - _cb: OSSL_CALLBACK, - _cbarg: *mut c_void, -) -> *mut c_void { - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - trace!(target: log_target!(), "{}", "Called!"); - let genctx: &mut GenCTX<'_> = handleResult!(vgenctx.try_into()); - - let keypair = handleResult!(genctx.generate()); - let keypair: Box> = Box::new(keypair); - - let keypair_ptr = Box::into_raw(keypair); - - return keypair_ptr.cast(); -} - -#[named] -pub(super) unsafe extern "C" fn gen_cleanup(vgenctx: *mut c_void) { - trace!(target: log_target!(), "{}", "Called!"); - let /* mut */genctx: Box = unsafe { Box::from_raw(vgenctx.cast()) }; - //todo!("clean up and free the key object generation context genctx"); - drop(genctx); -} - -struct GenCTX<'a> { - provctx: &'a ProviderInstance<'a>, - selection: Selection, -} - -impl<'a> GenCTX<'a> { - fn new(provctx: &'a ProviderInstance, selection: Selection) -> Self { - Self { - provctx: provctx, - selection: selection, - } - } - - #[named] - fn generate(&self) -> Result, KMGMTError> { - trace!(target: log_target!(), "Called"); - if !self.selection.contains(Selection::KEYPAIR) { - trace!(target: log_target!(), "Returning empty keypair due to selection bits {:?}", self.selection); - return Ok(KeyPair::new(self.provctx)); - } - trace!(target: log_target!(), "Generating a new KeyPair"); - - KeyPair::generate(self.provctx) - } -} - -impl<'a> TryFrom<*mut c_void> for &mut GenCTX<'a> { - type Error = KMGMTError; - - #[named] - fn try_from(vctx: *mut c_void) -> Result { - trace!(target: log_target!(), "Called for {}", - "impl<'a> TryFrom<*mut c_void> for &mut GenCTX<'a>" - ); - let ctxp = vctx as *mut GenCTX; - if ctxp.is_null() { - panic!("vctx was null"); - } - Ok(unsafe { &mut *ctxp }) - } -} - -#[named] -pub(super) unsafe extern "C" fn gen_init( - vprovctx: *mut c_void, - selection: c_int, - params: *const OSSL_PARAM, -) -> *mut c_void { - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - trace!(target: log_target!(), "{}", "Called!"); - let provctx: &ProviderInstance<'_> = handleResult!(vprovctx.try_into()); - let selection: Selection = handleResult!((selection as u32).try_into()); - let newctx = Box::new(GenCTX::new(provctx, selection)); - - if !params.is_null() { - warn!(target: log_target!(), "Ignoring params!"); - //todo!("set params on the context if params is not null") - } - - let newctx_raw_ptr = Box::into_raw(newctx); - - return newctx_raw_ptr.cast(); -} - -#[named] -pub(super) unsafe extern "C" fn import( - _keydata: *mut c_void, - _selection: c_int, - _params: *const OSSL_PARAM, -) -> c_int { - trace!(target: log_target!(), "{}", "Called!"); - todo!("import data indicated by selection into keydata with values taken from the params array") -} - -#[cfg(not(feature = "export"))] -pub(super) use crate::adapters::common::keymgmt_functions::export_forbidden as export; - -const HANDLED_KEY_TYPES: [OSSL_PARAM; 3] = [ - OSSL_PARAM { - key: OSSL_PKEY_PARAM_PUB_KEY.as_ptr(), - data_type: OSSL_PARAM_OCTET_STRING, - data: std::ptr::null::() as *mut std::ffi::c_void, - data_size: 0, - return_size: 0, - }, - OSSL_PARAM { - key: OSSL_PKEY_PARAM_PRIV_KEY.as_ptr(), - data_type: OSSL_PARAM_OCTET_STRING, - data: std::ptr::null::() as *mut std::ffi::c_void, - data_size: 0, - return_size: 0, - }, - OSSL_PARAM::END, -]; - -// I think using {import,export}_types_ex instead of the non-_ex variant means we only -// support OSSL 3.2 and up, but I also think that's fine...? -#[named] -pub(super) unsafe extern "C" fn import_types_ex( - vprovctx: *mut c_void, - selection: c_int, -) -> *const OSSL_PARAM { - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - trace!(target: log_target!(), "{}", "Called!"); - let _provctx: &ProviderInstance<'_> = handleResult!(vprovctx.try_into()); - let selection: Selection = handleResult!((selection as u32).try_into()); - - if selection.intersects(Selection::KEYPAIR) { - return HANDLED_KEY_TYPES.as_ptr(); - } - ERROR_RET -} - -#[cfg(not(feature = "export"))] -pub(super) use crate::adapters::common::keymgmt_functions::export_types_ex_forbidden as export_types_ex; - -#[named] -pub(super) unsafe extern "C" fn gen_set_params( - _vgenctx: *mut c_void, - _params: *const OSSL_PARAM, -) -> c_int { - trace!(target: log_target!(), "{}", "Called!"); - - #[cfg(not(debug_assertions))] // code compiled only in release builds - { - todo!("set genctx params"); - } - - #[cfg(debug_assertions)] // code compiled only in development builds - { - warn!(target: log_target!(), "{}", "Ignoring params!"); - return 1; - } -} - -#[named] -pub(super) unsafe extern "C" fn gen_settable_params( - _vgenctx: *mut c_void, - vprovctx: *mut c_void, -) -> *const OSSL_PARAM { - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - trace!(target: log_target!(), "{}", "Called!"); - let _provctx: &ProviderInstance<'_> = match vprovctx.try_into() { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "{}", e); - return ERROR_RET; - } - }; - - #[cfg(not(debug_assertions))] // code compiled only in release builds - { - todo!("return pointer to array of settable genctx params") - } - - #[cfg(debug_assertions)] // code compiled only in development builds - { - warn!(target: log_target!(), "{}", "TODO: return pointer to (non-empty) array of settable genctx params"); - - crate::osslparams::EMPTY_PARAMS.as_ptr() - } -} - -#[named] -pub(super) unsafe extern "C" fn get_params( - vkeydata: *mut c_void, - params: *mut OSSL_PARAM, -) -> c_int { - const ERROR_RET: c_int = 0; - const SUCCESS: c_int = 1; - - trace!(target: log_target!(), "{}", "Called!"); - let _keydata: &KeyPair = handleResult!(vkeydata.try_into()); - - let params = match OSSLParam::try_from(params) { - Ok(params) => params, - Err(e) => { - error!(target: log_target!(), "Failed decoding params: {:?}", e); - return ERROR_RET; - } - }; - - for mut p in params { - let key = match p.get_key() { - Some(key) => key, - None => { - error!(target: log_target!(), "Param without valid key {:?}", p); - return ERROR_RET; - } - }; - - if key == OSSL_PKEY_PARAM_BITS { - //const BITS: c_int = 8 * (PUBKEY_LEN as c_int); - //let _ = handleResult!(p.set(BITS)); - let _ = handleResult!(p.set(super::SECURITY_BITS as c_int)); - } else if key == OSSL_PKEY_PARAM_MAX_SIZE { - let _ = handleResult!(p.set(SIGNATURE_LEN as c_int)); - } else if key == OSSL_PKEY_PARAM_SECURITY_BITS { - let _ = handleResult!(p.set(super::SECURITY_BITS as c_int)); - } else if key == OSSL_PKEY_PARAM_MANDATORY_DIGEST { - let _ = handleResult!(p.set(c"")); - } else { - debug!(target: log_target!(), "Ignoring param {:?}", key); - } - } - return SUCCESS; -} - -#[named] -pub(super) unsafe extern "C" fn gettable_params(vprovctx: *mut c_void) -> *const OSSL_PARAM { - trace!(target: log_target!(), "{}", "Called!"); - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - let _provctx: &ProviderInstance<'_> = match vprovctx.try_into() { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "{}", e); - return ERROR_RET; - } - }; - - static LIST: &[CONST_OSSL_PARAM] = &[ - OSSLParam::new_const_int::(OSSL_PKEY_PARAM_BITS, None), - OSSLParam::new_const_int::(OSSL_PKEY_PARAM_MAX_SIZE, None), - OSSLParam::new_const_int::(OSSL_PKEY_PARAM_SECURITY_BITS, None), - OSSLParam::new_const_utf8string(OSSL_PKEY_PARAM_MANDATORY_DIGEST, None), - CONST_OSSL_PARAM::END, - ]; - - let first: &bindings::OSSL_PARAM = &LIST[0]; - let ptr: *const bindings::OSSL_PARAM = std::ptr::from_ref(first); - - return ptr; -} - -#[named] -pub(super) unsafe extern "C" fn set_params( - vkeydata: *mut c_void, - params: *const OSSL_PARAM, -) -> c_int { - const ERROR_RET: c_int = 0; - const SUCCESS: c_int = 1; - - trace!(target: log_target!(), "{}", "Called!"); - let _keydata: &mut KeyPair = handleResult!(vkeydata.try_into()); - - let params = match OSSLParam::try_from(params) { - Ok(params) => params, - Err(e) => { - error!(target: log_target!(), "Failed decoding params: {:?}", e); - return ERROR_RET; - } - }; - - for p in params { - let key = match p.get_key() { - Some(key) => key, - None => { - error!(target: log_target!(), "Param without valid key {:?}", p); - return ERROR_RET; - } - }; - - if false && key == OSSL_PKEY_PARAM_SECURITY_BITS { - unreachable!(); - //let bytes: &[u8] = match p.get() { - // Some(bytes) => bytes, - // None => handleResult!(Err(anyhow!("Invalid ENCODED_PUBLIC_KEY"))), - //}; - //debug!(target: log_target!(), "The received encoded public key is (len: {}): {:X?}", bytes.len(), bytes); - - //keydata.public = Some(handleResult!(PublicKey::decode(bytes))); - } else { - debug!(target: log_target!(), "Ignoring param {:?}", key); - } - } - return SUCCESS; -} - -#[named] -pub(super) unsafe extern "C" fn settable_params(vprovctx: *mut c_void) -> *const OSSL_PARAM { - const ERROR_RET: *const OSSL_PARAM = std::ptr::null(); - trace!(target: log_target!(), "{}", "Called!"); - let _provctx: &ProviderInstance<'_> = match vprovctx.try_into() { - Ok(p) => p, - Err(e) => { - error!(target: log_target!(), "{}", e); - return ERROR_RET; - } - }; - - static LIST: &[CONST_OSSL_PARAM] = &[CONST_OSSL_PARAM::END]; - - let first: &bindings::OSSL_PARAM = &LIST[0]; - let ptr: *const bindings::OSSL_PARAM = std::ptr::from_ref(first); - - return ptr; -} - -#[named] -/// Implements key loading by object reference, also a constructor for a new Key object -/// -/// Refer to [provider-keymgmt(7ossl)] and [provider-object(7ossl)]. -/// -/// # Notes -/// -/// This function is tightly integrated with the -/// [`OSSL_FUNC_decoder_decode_fn`][provider-decoder(7ossl)] -/// exposed by [decoders registered][`super::decoder_functions`] -/// for [this algorithm][`super`] -/// by [this adapter][`super::super`]. -/// -/// Eventually this function is called by the callback passed to OSSL_FUNC_decoder_decode_fn -/// hence they must agree on how the reference is being passed around. -/// -/// [provider-keymgmt(7ossl)]: https://docs.openssl.org/master/man7/provider-keymgmt/ -/// [provider-object(7ossl)]: https://docs.openssl.org/master/man7/provider-object/ -/// [provider-decoder(7ossl)]: https://docs.openssl.org/master/man7/provider-decoder/ -pub(super) unsafe extern "C" fn load(reference: *const c_void, reference_sz: usize) -> *mut c_void { - const ERROR_RET: *mut c_void = std::ptr::null_mut(); - trace!(target: log_target!(), "{}", "Called!"); - - assert_eq!(reference_sz, std::mem::size_of::()); - if reference.is_null() { - error!(target: log_target!(), "reference should not be NULL"); - unreachable!() - } - - let keypair = handleResult!(<&KeyPair>::try_from(reference as *mut c_void)); - debug!(target: log_target!(), "keypair: {keypair:#?}"); - - return std::ptr::from_ref(keypair).cast_mut() as *mut c_void; -} - -// based on OpenSSL 3.5's crypto/ml_dsa/ml_dsa_key.c:ossl_ml_dsa_key_equal() -// (and we can't just call it "match", because that's a Rust keyword) -#[named] -pub(super) unsafe extern "C" fn match_( - keydata1: *const c_void, - keydata2: *const c_void, - selection: c_int, -) -> c_int { - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - let keypair1 = handleResult!(<&KeyPair>::try_from(keydata1 as *mut c_void)); - let keypair2 = handleResult!(<&KeyPair>::try_from(keydata2 as *mut c_void)); - let mut key_checked = false; - - if (selection & OSSL_KEYMGMT_SELECT_KEYPAIR as c_int) != 0 { - if (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int) != 0 { - if keypair1.public != keypair2.public { - return ERROR_RET; - } - key_checked = true; - } - if !key_checked && (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY as c_int) != 0 { - if keypair1.private != keypair2.private { - return ERROR_RET; - } - key_checked = true; - } - return key_checked as c_int; - } - - return 1; -} - -pub(super) mod asn_definitions { - pub use crate::asn_definitions::x509_ml_dsa_2025 as defns; - - pub use defns::MLDSA65PrivateKey as PrivateKey; - pub use defns::MLDSA65PublicKey as PublicKey; -} - -#[cfg(test)] -mod tests { - use super::*; - - struct TestCTX<'a> { - provctx: ProviderInstance<'a>, - } - - fn setup<'a>() -> Result, OurError> { - use crate::tests::new_provctx_for_testing; - - crate::tests::common::setup()?; - - let provctx = new_provctx_for_testing(); - - let testctx = TestCTX { provctx }; - - Ok(testctx) - } - - #[test] - fn test_roundtrip_encode_decode() { - let testctx = setup().expect("Failed to initialize test setup"); - - let provctx = testctx.provctx; - - let keypair = KeyPair::generate_new(&provctx).expect("Failed to generate keypair"); - - match (keypair.public, keypair.private) { - (None, None) => panic!("No public or private key generated"), - (None, Some(_)) => panic!("No public key generated"), - (Some(_), None) => panic!("No private key generated"), - (Some(pk), Some(sk)) => { - let encoded_pk = pk.encode(); - let roundtripped_pk = PublicKey::decode(&encoded_pk).unwrap(); - // we can't use assert_eq! without having a Debug impl for both arguments - assert!(pk == roundtripped_pk); - - let encoded_sk = sk.encode(); - let roundtripped_sk = PrivateKey::decode(&encoded_sk).unwrap(); - assert!(sk == roundtripped_sk); - } - } - } - - #[test] - fn const_sanity_assertions() { - crate::tests::common::setup().expect("Failed to initialize test setup"); - - // Compare against https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-approximate-key-and-signatu - // except the SECRETKEY_LEN, which is 64 in that table because that document uses the - // assumption that only the seed of the ML-DSA secret key should be stored - assert_eq!(PUBKEY_LEN, 1984); - assert_eq!(SECRETKEY_LEN, 4064); - assert_eq!(SIGNATURE_LEN, 3405); - - assert_eq!(SECURITY_BITS, 192); - } -} diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs similarity index 88% rename from src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs rename to src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs index 0da0d90..40ae12d 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs @@ -25,14 +25,23 @@ use ed25519_dalek as trad_backend_module; use pqcrypto_mldsa::mldsa65 as pq_backend_module; type PQPublicKey = pq_backend_module::PublicKey; -type PQPrivateKey = pq_backend_module::SecretKey; + +use helpers::MlDsaSeed; +#[derive(PartialEq)] +pub struct PQPrivateKey { + seed: MlDsaSeed, + expanded: pq_backend_module::SecretKey, +} + type TPublicKey = trad_backend_module::VerifyingKey; type TPrivateKey = trad_backend_module::SecretKey; use super::OurError as KMGMTError; type OurResult = anyhow::Result; -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; +use super::signature::{ + Signature, SignatureBytes, SignatureEncoding, SignerWithCtx, VerifierWithCtx, +}; pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); @@ -175,16 +184,38 @@ impl PublicKey { } } -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-prefix-label-and-ctx +// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-13#name-prefix-label-and-ctx const PREFIX: &[u8] = "CompositeAlgorithmSignatures2025".as_bytes(); const LABEL: &[u8] = "COMPSIG-MLDSA65-Ed25519-SHA512".as_bytes(); -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-verify -// There's no way to pass additional context info (`ctx` in the linked spec) into this Verifier -// trait's verify function, so we take `ctx` to be the empty string. impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) + } +} + +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // + // mandates the ctx maximum length should fit in a single byte. + // Note: this also matches FIPS 204 restriction on the `ctx` + // maximum allowed length of 255 bytes. + let ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + // get at the public keys let Self { pq_public_key, @@ -207,12 +238,13 @@ impl Verifier for PublicKey { let trad_sig: &[u8; Self::T_SIGNATURE_LEN] = trad_sig.try_into().expect("Unexpected length"); - // M' := Prefix || Domain || len(ctx) || ctx || r || PH( M ) + // M' := Prefix || Label || len(ctx) || ctx || r || PH( M ) // (here M is our `msg` argument) let msg_hash = Sha512::digest(msg); let mut M_prime = PREFIX.to_vec(); M_prime.extend_from_slice(LABEL); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment at top of impl) + M_prime.push(ctx_len); + M_prime.extend_from_slice(ctx); M_prime.extend(msg_hash); // verify with ML-DSA @@ -223,6 +255,8 @@ impl Verifier for PublicKey { VerificationError::GenericVerificationError, ) })?; + // the so-called `ctx` that gets passed to the ML-DSA verifier here is actually the Label, + // not the ctx that was prepended to the message hash pq_backend_module::verify_detached_signature_ctx( &pq_sig, M_prime.as_slice(), @@ -266,8 +300,19 @@ fn map_PQError_into_VerificationError( } } +impl PQPrivateKey { + pub fn new(seed: &MlDsaSeed) -> Result { + helpers::derive_mldsa_secret_key_from_seed(seed) + .map(|k| Self { + seed: *seed, + expanded: k, + }) + .ok_or(anyhow!("Unable to decode private key")) + } +} + impl PrivateKey { - const PQ_PRIVATE_KEY_LEN: usize = pq_backend_module::secret_key_bytes(); + const PQ_PRIVATE_KEY_LEN: usize = helpers::ML_DSA_SEED_SIZE; const T_PRIVATE_KEY_LEN: usize = trad_backend_module::SECRET_KEY_LENGTH; const PQ_SIGNATURE_LEN: usize = PublicKey::PQ_SIGNATURE_LEN; const T_SIGNATURE_LEN: usize = PublicKey::T_SIGNATURE_LEN; @@ -277,27 +322,22 @@ impl PrivateKey { pq_private_key, trad_private_key, } = self; - let mut bytes = - ::as_bytes( - pq_private_key, - ) - .to_vec(); + let mut bytes = pq_private_key.seed.to_vec(); bytes.extend(trad_private_key); bytes } pub fn decode(bytes: &[u8]) -> Result { - let (pq_bytes, trad_bytes) = bytes.split_at(pq_backend_module::secret_key_bytes()); - let pq_private_key = - ::from_bytes( - pq_bytes, + if bytes.len() != Self::byte_len() { + anyhow::bail!( + "Cannot decode MLDSA65-Ed25519 private key of length {}", + bytes.len() ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA65) returned {:?}", - e - ) - })?; + } + let (pq_bytes, trad_bytes) = bytes + .split_at_checked(Self::PQ_PRIVATE_KEY_LEN) + .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; + let pq_private_key = PQPrivateKey::new(pq_bytes.try_into()?)?; let trad_private_key = trad_bytes .try_into() .map_err(|_| anyhow!("Ed25519 secret key should be 32 bytes"))?; @@ -316,7 +356,7 @@ impl PrivateKey { } fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key) + super::helpers::derive_mldsa_public_key(&self.pq_private_key.expanded) } /// Derive a matching public key from this private key @@ -358,12 +398,11 @@ impl PrivateKey { debug!(target: log_target!(), "Parsed private key material out of ASN.1 for decoding!"); let (privkey, opt_pubkey) = match decodedprivkey { - ASNPrivateKey::seed(_seed) => unimplemented!(), - ASNPrivateKey::expandedKey(expandedKey) => { - let slice: &[u8] = &expandedKey; + ASNPrivateKey::seed(bytes) => { + let slice: &[u8] = &bytes; let privkey = keymgmt_functions::PrivateKey::decode(slice)?; - // We need to derive a public key from the private key, without a seed + // We need to derive a public key from the private key let pubkey = match privkey.derive_public_key() { Some(k) => k, None => { @@ -375,6 +414,7 @@ impl PrivateKey { }; (privkey, Some(pubkey)) } + ASNPrivateKey::expandedKey(_expandedKey) => unimplemented!(), ASNPrivateKey::both(_private_key_both) => unimplemented!(), }; Ok((privkey, opt_pubkey)) @@ -386,7 +426,7 @@ impl PrivateKey { use asn_definitions::PrivateKey as ASNPrivateKey; let raw_sk_bytes = self.encode(); - let asn_sk = ASNPrivateKey::expandedKey(raw_sk_bytes.into()); + let asn_sk = ASNPrivateKey::seed(raw_sk_bytes.into()); let asn_sk_bytes = match rasn::der::encode(&asn_sk) { Ok(v) => v, Err(e) => { @@ -398,17 +438,38 @@ impl PrivateKey { } } -// https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-06#name-sign -// Just like with the Verifier above, there's no way to pass additional context info (`ctx` in the -// linked spec) into this Signer trait's try_sign function, so we take `ctx` to be the empty string. impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { + self.try_sign_with_ctx(msg, &[]) + } +} + +impl SignerWithCtx for PrivateKey { + #[named] + fn try_sign_with_ctx( + &self, + msg: &[u8], + ctx: &[u8], + ) -> Result { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // + // mandates the ctx maximum length should fit in a single byte. + // Note: this also matches FIPS 204 restriction on the `ctx` + // maximum allowed length of 255 bytes. + let ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + // M' := Prefix || Label || len(ctx) || ctx || PH( M ) // (here M is our `msg` argument) let msg_hash = Sha512::digest(msg); let mut M_prime = PREFIX.to_vec(); M_prime.extend_from_slice(LABEL); - M_prime.push(0); // len(ctx) is 0, since ctx is the empty string (see comment above) + M_prime.push(ctx_len); + M_prime.extend_from_slice(ctx); M_prime.extend(msg_hash); // get at the private keys @@ -418,9 +479,10 @@ impl Signer for PrivateKey { } = self; // sign with ML-DSA - // (the Label being used as the `ctx` here refers to the underlying ML-DSA - // signature operation, and has nothing to do with the empty `ctx` string from the spec) - let pq_signature = pq_backend_module::detached_sign_ctx(&M_prime, LABEL, pq_private_key); + // the so-called `ctx` that gets passed to the ML-DSA signer here is actually the Label, + // not the ctx that was prepended to the message hash + let pq_signature = + pq_backend_module::detached_sign_ctx(&M_prime, LABEL, &pq_private_key.expanded); // sign with Ed25519 let trad_signature = @@ -496,12 +558,20 @@ impl<'a> KeyPair<'a> { fn generate(provctx: &'a ProviderInstance) -> Result { trace!(target: log_target!(), "Called"); - // Isn't it weird that this operation can't fail? What does the pqclean implementation do if - // it can't find a randomness source or it can't allocate memory or something? - let (pq_public_key, pq_private_key) = pq_backend_module::keypair(); + // generate PQ private key + let prng = provctx.get_rng(); + let mut seed_buf = [0u8; helpers::ML_DSA_SEED_SIZE]; + let pq_private_key = match prng.try_fill_bytes(&mut seed_buf) { + Ok(_) => PQPrivateKey::new(&seed_buf)?, + Err(_) => anyhow::bail!("Unable to generate randomness for ML-DSA keygen"), + }; + + // derive PQ public key from it + let pq_public_key = helpers::derive_mldsa_public_key(&pq_private_key.expanded).ok_or( + anyhow!("Unable to derive public ML-DSA key from private key"), + )?; - // Similarly, it seems weird that this can't fail. Hopefully a different layer can handle it - // if something goes wrong here. + // generate traditional keypair let trad_keypair = trad_backend_module::SigningKey::generate(provctx.get_rng()); let trad_private_key = trad_keypair.to_bytes(); let trad_public_key = trad_keypair.verifying_key(); @@ -1111,10 +1181,8 @@ mod tests { crate::tests::common::setup().expect("Failed to initialize test setup"); // Compare against https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-maximum-key-and-signature-s - // except the SECRETKEY_LEN, which is 64 in that table because that document uses the - // assumption that only the seed of the ML-DSA secret key should be stored assert_eq!(PUBKEY_LEN, 1984); - assert_eq!(SECRETKEY_LEN, 4064); + assert_eq!(SECRETKEY_LEN, 64); assert_eq!(SIGNATURE_LEN, 3373); assert_eq!(SECURITY_BITS, 192); diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index 953f072..988dbb1 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -399,5 +399,50 @@ pub(super) use decoder_functions::DER2PrivateKeyInfo as DECODER_DER2PrivateKeyIn pub(super) use decoder_functions::DER2SubjectPublicKeyInfo as DECODER_DER2SubjectPublicKeyInfo; pub(super) use encoder_functions::PrivateKeyInfo2DER as ENCODER_PrivateKeyInfo2DER; pub(super) use encoder_functions::PrivateKeyInfo2PEM as ENCODER_PrivateKeyInfo2PEM; +pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2Text; +pub(super) use encoder_functions::PubKeyStructureless2Text as ENCODER_PubKeyStructureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; + +#[cfg(test)] +mod tests { + use super::*; + use crate::adapters::common::wycheproof::*; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::mldsa_verify; + + struct Mldsa87; + + impl_sigalg_verify_variant!(Mldsa87, keymgmt_functions::PublicKey, signature::Signature); + + #[test] + fn test_mldsa_87_verify_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa87Verify); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; + use wycheproof::mldsa_sign; + + impl_sigalg_sign_variant!(Mldsa87, keymgmt_functions::PrivateKey, signature::Signature); + + #[test] + fn test_mldsa_87_sign_seed_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa87SignSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } + + #[test] + fn test_mldsa_87_sign_noseed_from_wycheproof() { + crate::tests::common::setup().expect("Failed to initialize test setup"); + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa87SignNoSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } +} diff --git a/src/adapters/pqclean/MLDSA87/encoder_functions.rs b/src/adapters/pqclean/MLDSA87/encoder_functions.rs index a4c85ff..0be7e26 100644 --- a/src/adapters/pqclean/MLDSA87/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA87/encoder_functions.rs @@ -464,6 +464,13 @@ impl DoesSelection for PrivateKeyInfo2PEM { // We can use the same does_selection function as PrivateKeyInfo2DER, so there's no need to call // the make_does_selection_fn macro again. +// generate the plain text encoder +use crate::adapters::common::transcoders::make_privkey_text_encoder; +make_privkey_text_encoder!( + PrivateKeyInfo2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text',structure='PrivateKeyInfo'" +); + pub(crate) struct SubjectPublicKeyInfo2DER(); impl Encoder for SubjectPublicKeyInfo2DER { const PROPERTY_DEFINITION: &'static CStr = @@ -722,3 +729,10 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to // call the make_does_selection_fn macro again. + +// generate the plain text encoder +use crate::adapters::common::transcoders::make_pubkey_text_encoder; +make_pubkey_text_encoder!( + PubKeyStructureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs index 373a1cd..396c1c8 100644 --- a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs @@ -23,7 +23,9 @@ use pqcrypto_mldsa::mldsa87 as backend_module; use super::OurError as KMGMTError; type OurResult = anyhow::Result; -use super::signature::{Signature, SignatureBytes, SignatureEncoding}; +use super::signature::{ + Signature, SignatureBytes, SignatureEncoding, SignerWithCtx, VerifierWithCtx, +}; pub(crate) const PUBKEY_LEN: usize = PublicKey::byte_len(); pub(crate) const SECRETKEY_LEN: usize = PrivateKey::byte_len(); @@ -118,6 +120,29 @@ impl PublicKey { impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) + } +} + +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + trace!(target: log_target!(), "Called"); + + // validate ctx length + // FIPS 204 specifies maximum ctx len is 255 bytes, so it should + // fit into a u8 + let _ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + let sig = sig.to_bytes(); let sig = sig.as_ref(); use pqcrypto_traits::sign::DetachedSignature; @@ -127,7 +152,7 @@ impl Verifier for PublicKey { VerificationError::GenericVerificationError, ) })?; - backend_module::verify_detached_signature(&sig, msg, &self.0) + backend_module::verify_detached_signature_ctx(&sig, msg, ctx, &self.0) .map_err(map_into_VerificationError) .map_err(forge::crypto::signature::Error::from_source) } @@ -158,14 +183,9 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - let k = ::from_bytes(bytes) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA87) returned {:?}", - e - ) - })?; - Ok(Self(k)) + super::helpers::decode_mldsa_secret_key(bytes) + .map(Self) + .ok_or(anyhow!("Unable to decode private key")) } pub const fn byte_len() -> usize { @@ -178,7 +198,7 @@ impl PrivateKey { /// Derive a matching public key from this private key pub fn derive_public_key(&self) -> Option { - let pk = super::helpers::derive_public_key(&self.0); + let pk = super::helpers::derive_mldsa_public_key(&self.0); pk.map(|inner| PublicKey(inner)) } @@ -238,8 +258,26 @@ impl PrivateKey { impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { + self.try_sign_with_ctx(msg, &[]) + } +} + +impl SignerWithCtx for PrivateKey { + #[named] + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result { + trace!(target: log_target!(), "Called"); + let Self(ref sk) = self; - let signature = backend_module::detached_sign(msg, sk); + + // validate ctx length + // FIPS 204 specifies maximum ctx len is 255 bytes, so it should + // fit into a u8 + let _ctx_len: u8 = ctx.len().try_into().map_err(|e| { + log::error!("Invalid ctx_len: {} (maximum 255 bytes)", ctx.len()); + forge::crypto::signature::Error::from_source(e) + })?; + + let signature = backend_module::detached_sign_ctx(msg, ctx, sk); Signature::try_from(signature.as_bytes()) .map_err(|e| forge::crypto::signature::Error::from_source(e)) } diff --git a/src/adapters/pqclean/helpers.rs b/src/adapters/pqclean/helpers.rs index 5e68a14..828fdb0 100644 --- a/src/adapters/pqclean/helpers.rs +++ b/src/adapters/pqclean/helpers.rs @@ -3,32 +3,36 @@ use function_name::named; /// pqclean does not provide support to derive the public key from an /// expanded private key so we resort to /// RustCrypto/signatures/ml-dsa to work around this -use ml_dsa as foreign_module; +use ml_dsa as foreign_mldsa_module; -pub(super) trait SupportedSecretKey: pqcrypto_traits::sign::SecretKey { - type ForeignParamSet: foreign_module::MlDsaParams; +pub(super) const ML_DSA_SEED_SIZE: usize = 32; + +pub(super) type MlDsaSeed = [u8; ML_DSA_SEED_SIZE]; + +pub(super) trait SupportedMlDsaSecretKey: pqcrypto_traits::sign::SecretKey { + type ForeignParamSet: foreign_mldsa_module::MlDsaParams; type PublicKey; } -impl SupportedSecretKey for pqcrypto_mldsa::mldsa44::SecretKey { - type ForeignParamSet = foreign_module::MlDsa44; +impl SupportedMlDsaSecretKey for pqcrypto_mldsa::mldsa44::SecretKey { + type ForeignParamSet = foreign_mldsa_module::MlDsa44; type PublicKey = pqcrypto_mldsa::mldsa44::PublicKey; } -impl SupportedSecretKey for pqcrypto_mldsa::mldsa65::SecretKey { - type ForeignParamSet = foreign_module::MlDsa65; +impl SupportedMlDsaSecretKey for pqcrypto_mldsa::mldsa65::SecretKey { + type ForeignParamSet = foreign_mldsa_module::MlDsa65; type PublicKey = pqcrypto_mldsa::mldsa65::PublicKey; } -impl SupportedSecretKey for pqcrypto_mldsa::mldsa87::SecretKey { - type ForeignParamSet = foreign_module::MlDsa87; +impl SupportedMlDsaSecretKey for pqcrypto_mldsa::mldsa87::SecretKey { + type ForeignParamSet = foreign_mldsa_module::MlDsa87; type PublicKey = pqcrypto_mldsa::mldsa87::PublicKey; } /// Derive the matching public key from a secret key #[named] -pub(super) fn derive_public_key(sk: &T) -> Option +pub(super) fn derive_mldsa_public_key(sk: &T) -> Option where - T: SupportedSecretKey, - ::PublicKey: pqcrypto_traits::sign::PublicKey, + T: SupportedMlDsaSecretKey, + ::PublicKey: pqcrypto_traits::sign::PublicKey, { let encoded_sk = ::as_bytes(sk); let encoded_sk = match encoded_sk.try_into() { @@ -38,13 +42,13 @@ where return None; } }; - let csk = >::decode(encoded_sk); + let csk = >::decode(encoded_sk); let cpk = csk.verifying_key(); let pk_bytes = cpk.encode(); let pk_bytes = pk_bytes.as_slice(); let res = - <::PublicKey as pqcrypto_traits::sign::PublicKey>::from_bytes( + <::PublicKey as pqcrypto_traits::sign::PublicKey>::from_bytes( pk_bytes, ); match res { @@ -55,3 +59,112 @@ where } } } + +/// Derive the expanded secret key from a seed +#[named] +pub(super) fn derive_mldsa_secret_key_from_seed(seed: &MlDsaSeed) -> Option +where + T: SupportedMlDsaSecretKey, +{ + let foreign_key = + >::from_seed(seed.into()); + let key_bytes = foreign_key.encode(); + let res = ::from_bytes(&key_bytes); + match res { + Ok(sk) => Some(sk), + Err(e) => { + error!(target: log_target!(), "Failed to derive the expanded private key from the seed: {e:?}"); + return None; + } + } +} + +const VALIDATE_PRIVKEY_DECODING_VIA_FOREIGN_MODULE: bool = true; + +/// Use the foreign_mldsa_module to decode bytes as a secret key +#[named] +fn foreign_decode_mldsa_secret_key( + bytes: &[u8], +) -> std::thread::Result< + foreign_mldsa_module::SigningKey<::ForeignParamSet>, +> +where + T: SupportedMlDsaSecretKey, +{ + // The `>::decode(a)` + // call can panic internally. + // We want to catch those errors, and handle them gracefully, hence catch_unwind + use std::panic::{self, catch_unwind, AssertUnwindSafe}; + + let a = match bytes.try_into() { + Ok(a) => a, + Err(e) => { + error!(target: log_target!(), "Found wrong length when decoding EncodedPrivateKey: {e:?}"); + return Err(Box::new(e)); + } + }; + + // Before calling decode within the catch_unwind block, we temporarily + // replace the `panic` hook, to avoid polluting the output. + + // Take the current hook so we can restore it later + let prev_hook = panic::take_hook(); + + panic::set_hook(Box::new(|info| { + trace!(target: log_target!(), "Caught panic: {}", info); + })); + + let result = catch_unwind(AssertUnwindSafe(|| { + >::decode(a) + })); + + // Restore the previous hook + panic::set_hook(prev_hook); + + result +} + +/// Decode the bytes as a secret key, deriving from seed if necessary +#[named] +pub(super) fn decode_mldsa_secret_key(bytes: &[u8]) -> Option +where + T: SupportedMlDsaSecretKey, +{ + // First we check if the EncodedBytes match the expected length for seed format + match TryInto::<&MlDsaSeed>::try_into(bytes) { + Ok(seed) => { + return derive_mldsa_secret_key_from_seed(seed); + } + Err(_) => (), + } + + // If we reach here, the key was not in seed format, and we exepct an + // expanded private key + + if VALIDATE_PRIVKEY_DECODING_VIA_FOREIGN_MODULE { + // Currently PQClean is too lenient in parsing private keys. + // We use a more strict-on-decode foreign module to try and correctly decode + // the input, before asking PQClean to decode. + let foreign_result = foreign_decode_mldsa_secret_key::(bytes); + + match foreign_result { + Ok(_) => (), // we discard the foreign module object + Err(e) => { + if let Some(s) = e.downcast_ref::<&str>() { + error!(target: log_target!(), "Failed to decode the EncodedPrivateKey: {s}"); + } else if let Some(s) = e.downcast_ref::() { + error!(target: log_target!(), "Failed to decode the EncodedPrivateKey: {s}"); + } else { + error!(target: log_target!(), "Failed to decode the EncodedPrivateKey"); + } + return None; + } + } + + // Finally if we reached this point we know that the `foreign_mldsa_module` + // could decode the EncodedPrivateKey. We can proceed with the lenient + // decoding routines of PQClean + } + + T::from_bytes(bytes).ok() +}