From b2c682887a6e143657ec8375ff5ceaeed005cd0e Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 31 Oct 2025 16:01:02 +0200 Subject: [PATCH 01/42] feat(encoders): add text encoder for ML-DSA-65 public keys Now the public key material is displayed in the text output from e.g. `openssl x509`. --- Cargo.toml | 1 + src/adapters/pqclean.rs | 1 + src/adapters/pqclean/MLDSA65.rs | 1 + .../pqclean/MLDSA65/encoder_functions.rs | 107 ++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 051fcbf..c4b6575 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,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" diff --git a/src/adapters/pqclean.rs b/src/adapters/pqclean.rs index f6f94bf..9e59941 100644 --- a/src/adapters/pqclean.rs +++ b/src/adapters/pqclean.rs @@ -90,6 +90,7 @@ impl AdapterContextTrait for PQCleanAdapter { encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA65, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA65, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA65, ENCODER_Structureless2Text), // MLDSA87 encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2PEM), diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index 293d9ba..af45cf3 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -399,5 +399,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; diff --git a/src/adapters/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index 607adc0..db6bd64 100644 --- a/src/adapters/pqclean/MLDSA65/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65/encoder_functions.rs @@ -722,3 +722,110 @@ 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. + +pub(crate) struct Structureless2Text(); +impl Encoder for Structureless2Text { + const PROPERTY_DEFINITION: &'static CStr = + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; + + const DISPATCH_TABLE: &'static [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: &[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, + encoder_functions::does_selection_SPKI + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encoder_functions::encodeStructurelessToText + ), + OSSL_DISPATCH::END, + ]; + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; +} + +#[named] +pub(super) unsafe extern "C" fn encodeStructurelessToText( + vencoderctx: *mut c_void, + out: *mut OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const OSSL_PARAM, + selection: c_int, + _cb: OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, +) -> c_int { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + + let keypair: &KeyPair = handleResult!(obj_raw.try_into()); + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + use itertools::Itertools; + let formatted_key_bytes: String = key_bytes + .iter() + .map(|b| format!("{:02x}", b)) + .chunks(15) + .into_iter() + .map(|mut row| " ".to_owned() + &row.join(":")) + .join(":\n"); + let output = "Public key bytes:\n".to_owned() + &formatted_key_bytes + "\n"; + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } +} + +impl DoesSelection for Structureless2Text { + const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; +} + +// We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to +// call the make_does_selection_fn macro again. From 11baff482c22a48d102891a03c4a992f2154e7f6 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 3 Nov 2025 11:28:34 +0200 Subject: [PATCH 02/42] refactor(encoders): extract a format_hex_bytes helper function This isn't specific to the pqclean adapter, so I created a new `helpers` module in `adapters::common` for it. --- src/adapters/common.rs | 2 ++ src/adapters/common/helpers.rs | 13 +++++++++++++ src/adapters/pqclean/MLDSA65/encoder_functions.rs | 12 +++--------- 3 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 src/adapters/common/helpers.rs diff --git a/src/adapters/common.rs b/src/adapters/common.rs index c1d4109..9779f98 100644 --- a/src/adapters/common.rs +++ b/src/adapters/common.rs @@ -1,5 +1,7 @@ use crate::named; +pub mod helpers; + pub mod keymgmt_functions; pub mod macros; 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/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index db6bd64..6715f1f 100644 --- a/src/adapters/pqclean/MLDSA65/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65/encoder_functions.rs @@ -798,15 +798,9 @@ pub(super) unsafe extern "C" fn encodeStructurelessToText( match &keypair.public { Some(key) => { let key_bytes = key.encode(); - use itertools::Itertools; - let formatted_key_bytes: String = key_bytes - .iter() - .map(|b| format!("{:02x}", b)) - .chunks(15) - .into_iter() - .map(|mut row| " ".to_owned() + &row.join(":")) - .join(":\n"); - let output = "Public key bytes:\n".to_owned() + &formatted_key_bytes + "\n"; + use crate::adapters::common::helpers::format_hex_bytes; + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { Ok(_bytes_written) => {} Err(e) => { From 45e967fd608a46ed3b374c747327670bb165d966 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 3 Nov 2025 11:41:05 +0200 Subject: [PATCH 03/42] feat(encoders): add text encoder for ML-DSA-{44,87} public keys --- src/adapters/pqclean.rs | 2 + src/adapters/pqclean/MLDSA44.rs | 1 + .../pqclean/MLDSA44/encoder_functions.rs | 101 ++++++++++++++++++ src/adapters/pqclean/MLDSA87.rs | 1 + .../pqclean/MLDSA87/encoder_functions.rs | 101 ++++++++++++++++++ 5 files changed, 206 insertions(+) diff --git a/src/adapters/pqclean.rs b/src/adapters/pqclean.rs index 9e59941..15a9e16 100644 --- a/src/adapters/pqclean.rs +++ b/src/adapters/pqclean.rs @@ -85,6 +85,7 @@ impl AdapterContextTrait for PQCleanAdapter { encoder_to_register!(MLDSA44, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA44, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA44, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA44, ENCODER_Structureless2Text), // MLDSA65 encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2PEM), @@ -96,6 +97,7 @@ impl AdapterContextTrait for PQCleanAdapter { encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA87, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA87, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA87, ENCODER_Structureless2Text), // MLDSA65_Ed25519 encoder_to_register!(MLDSA65_Ed25519, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA65_Ed25519, ENCODER_PrivateKeyInfo2PEM), diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index 7cbf15e..9ace1ab 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -399,5 +399,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; diff --git a/src/adapters/pqclean/MLDSA44/encoder_functions.rs b/src/adapters/pqclean/MLDSA44/encoder_functions.rs index a4c85ff..1d1296c 100644 --- a/src/adapters/pqclean/MLDSA44/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44/encoder_functions.rs @@ -722,3 +722,104 @@ 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. + +pub(crate) struct Structureless2Text(); +impl Encoder for Structureless2Text { + const PROPERTY_DEFINITION: &'static CStr = + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; + + const DISPATCH_TABLE: &'static [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: &[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, + encoder_functions::does_selection_SPKI + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encoder_functions::encodeStructurelessToText + ), + OSSL_DISPATCH::END, + ]; + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; +} + +#[named] +pub(super) unsafe extern "C" fn encodeStructurelessToText( + vencoderctx: *mut c_void, + out: *mut OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const OSSL_PARAM, + selection: c_int, + _cb: OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, +) -> c_int { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + + let keypair: &KeyPair = handleResult!(obj_raw.try_into()); + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + use crate::adapters::common::helpers::format_hex_bytes; + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } +} + +impl DoesSelection for Structureless2Text { + const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; +} + +// We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to +// call the make_does_selection_fn macro again. diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index 953f072..0ccc40a 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -399,5 +399,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; diff --git a/src/adapters/pqclean/MLDSA87/encoder_functions.rs b/src/adapters/pqclean/MLDSA87/encoder_functions.rs index a4c85ff..1d1296c 100644 --- a/src/adapters/pqclean/MLDSA87/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA87/encoder_functions.rs @@ -722,3 +722,104 @@ 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. + +pub(crate) struct Structureless2Text(); +impl Encoder for Structureless2Text { + const PROPERTY_DEFINITION: &'static CStr = + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; + + const DISPATCH_TABLE: &'static [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: &[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, + encoder_functions::does_selection_SPKI + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encoder_functions::encodeStructurelessToText + ), + OSSL_DISPATCH::END, + ]; + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; +} + +#[named] +pub(super) unsafe extern "C" fn encodeStructurelessToText( + vencoderctx: *mut c_void, + out: *mut OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const OSSL_PARAM, + selection: c_int, + _cb: OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, +) -> c_int { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + + let keypair: &KeyPair = handleResult!(obj_raw.try_into()); + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + use crate::adapters::common::helpers::format_hex_bytes; + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } +} + +impl DoesSelection for Structureless2Text { + const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; +} + +// We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to +// call the make_does_selection_fn macro again. From ab59efb1d1378ac0b78ca4b8052b55195444bd09 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 3 Nov 2025 13:28:19 +0200 Subject: [PATCH 04/42] feat(encoders): add text encoder for ML-DSA-{44,65}-Ed25519 public keys --- src/adapters/pqclean.rs | 2 + src/adapters/pqclean/MLDSA44_Ed25519.rs | 1 + .../MLDSA44_Ed25519/encoder_functions.rs | 101 ++++++++++++++++++ src/adapters/pqclean/MLDSA65_Ed25519.rs | 1 + .../MLDSA65_Ed25519/encoder_functions.rs | 101 ++++++++++++++++++ 5 files changed, 206 insertions(+) diff --git a/src/adapters/pqclean.rs b/src/adapters/pqclean.rs index 15a9e16..f31a42b 100644 --- a/src/adapters/pqclean.rs +++ b/src/adapters/pqclean.rs @@ -103,11 +103,13 @@ impl AdapterContextTrait for PQCleanAdapter { encoder_to_register!(MLDSA65_Ed25519, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA65_Ed25519, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA65_Ed25519, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA65_Ed25519, ENCODER_Structureless2Text), // MLDSA44_Ed25519 encoder_to_register!(MLDSA44_Ed25519, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA44_Ed25519, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA44_Ed25519, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA44_Ed25519, ENCODER_SubjectPublicKeyInfo2PEM), + encoder_to_register!(MLDSA44_Ed25519, ENCODER_Structureless2Text), ]); handle.register_algorithms(OSSL_OP_ENCODER, encoder_algorithms.into_iter())?; diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 4a733e1..2e83eb7 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -406,5 +406,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs index a4c85ff..1d1296c 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs @@ -722,3 +722,104 @@ 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. + +pub(crate) struct Structureless2Text(); +impl Encoder for Structureless2Text { + const PROPERTY_DEFINITION: &'static CStr = + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; + + const DISPATCH_TABLE: &'static [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: &[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, + encoder_functions::does_selection_SPKI + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encoder_functions::encodeStructurelessToText + ), + OSSL_DISPATCH::END, + ]; + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; +} + +#[named] +pub(super) unsafe extern "C" fn encodeStructurelessToText( + vencoderctx: *mut c_void, + out: *mut OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const OSSL_PARAM, + selection: c_int, + _cb: OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, +) -> c_int { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + + let keypair: &KeyPair = handleResult!(obj_raw.try_into()); + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + use crate::adapters::common::helpers::format_hex_bytes; + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } +} + +impl DoesSelection for Structureless2Text { + const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; +} + +// We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to +// call the make_does_selection_fn macro again. diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 336e1eb..ed13b69 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -406,5 +406,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; pub(super) use encoder_functions::SubjectPublicKeyInfo2DER as ENCODER_SubjectPublicKeyInfo2DER; pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPublicKeyInfo2PEM; diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs index a4c85ff..1d1296c 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs @@ -722,3 +722,104 @@ 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. + +pub(crate) struct Structureless2Text(); +impl Encoder for Structureless2Text { + const PROPERTY_DEFINITION: &'static CStr = + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; + + const DISPATCH_TABLE: &'static [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: &[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, + encoder_functions::does_selection_SPKI + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encoder_functions::encodeStructurelessToText + ), + OSSL_DISPATCH::END, + ]; + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; +} + +#[named] +pub(super) unsafe extern "C" fn encodeStructurelessToText( + vencoderctx: *mut c_void, + out: *mut OSSL_CORE_BIO, + obj_raw: *const c_void, + _obj_abstract: *const OSSL_PARAM, + selection: c_int, + _cb: OSSL_PASSPHRASE_CALLBACK, + _cbarg: *mut c_void, +) -> c_int { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); + + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); + return ERROR_RET; + } + + let keypair: &KeyPair = handleResult!(obj_raw.try_into()); + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + use crate::adapters::common::helpers::format_hex_bytes; + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } +} + +impl DoesSelection for Structureless2Text { + const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; +} + +// We can use the same does_selection function as SubjectPublicKeyInfo2DER, so there's no need to +// call the make_does_selection_fn macro again. From dd223050fc26791958783d6b68685a31e6040512 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Tue, 25 Nov 2025 15:47:21 +0200 Subject: [PATCH 05/42] refactor(encoders): use a macro to generate plain text encoders for public keys This is also a minor bugfix: the encoder now includes a terminating null byte when writing out the string. The lack of one wasn't causing any visible problems before, but since we're writing a string to memory for a C application to use, I'm fairly certain it should be there. --- src/adapters/common.rs | 2 + src/adapters/common/transcoders.rs | 132 ++++++++++++++++++ .../pqclean/MLDSA44/encoder_functions.rs | 103 +------------- .../MLDSA44_Ed25519/encoder_functions.rs | 103 +------------- .../pqclean/MLDSA65/encoder_functions.rs | 103 +------------- .../MLDSA65_Ed25519/encoder_functions.rs | 103 +------------- .../pqclean/MLDSA87/encoder_functions.rs | 103 +------------- 7 files changed, 149 insertions(+), 500 deletions(-) create mode 100644 src/adapters/common/transcoders.rs diff --git a/src/adapters/common.rs b/src/adapters/common.rs index 9779f98..f9e8607 100644 --- a/src/adapters/common.rs +++ b/src/adapters/common.rs @@ -5,3 +5,5 @@ pub mod helpers; pub mod keymgmt_functions; pub mod macros; + +pub mod transcoders; diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs new file mode 100644 index 0000000..a736e73 --- /dev/null +++ b/src/adapters/common/transcoders.rs @@ -0,0 +1,132 @@ +/// Make a text encoder for a public key. +/// +/// The encoder outputs the bytes of the key as colon-separated hex values. +/// It takes one argument, `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`). +/// +/// The resulting encoder struct is named `Structureless2Text`, and like all other encoders it 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +/// ``` +macro_rules! make_pubkey_text_encoder { + ($property_definition:literal) => { + pub(crate) struct Structureless2Text(); + impl $crate::forge::operations::transcoders::Encoder for Structureless2Text { + 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, + encoder_functions::does_selection_text + ), + dispatch_table_entry!( + OSSL_FUNC_ENCODER_ENCODE, + OSSL_FUNC_encoder_encode_fn, + encoder_functions::encodeStructurelessToText + ), + $crate::forge::bindings::OSSL_DISPATCH::END, + ]; + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; + } + + #[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 { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & ($crate::forge::bindings::OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); + + 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()); + match &keypair.public { + Some(key) => { + let key_bytes = key.encode(); + use $crate::adapters::common::helpers::format_hex_bytes; + let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); + let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); + let output = handleResult!(CString::new(output)); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } + } + + impl $crate::forge::operations::transcoders::DoesSelection for Structureless2Text { + const SELECTION_MASK: $crate::forge::operations::keymgmt::selection::Selection = + $crate::forge::operations::keymgmt::selection::Selection::PUBLIC_KEY; + } + + $crate::forge::operations::transcoders::make_does_selection_fn!( + does_selection_text, + Structureless2Text, + ProviderInstance + ); + + } +} +pub(crate) use make_pubkey_text_encoder; diff --git a/src/adapters/pqclean/MLDSA44/encoder_functions.rs b/src/adapters/pqclean/MLDSA44/encoder_functions.rs index 1d1296c..394d956 100644 --- a/src/adapters/pqclean/MLDSA44/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44/encoder_functions.rs @@ -723,103 +723,6 @@ 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. -pub(crate) struct Structureless2Text(); -impl Encoder for Structureless2Text { - const PROPERTY_DEFINITION: &'static CStr = - c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; - - const DISPATCH_TABLE: &'static [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: &[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, - encoder_functions::does_selection_SPKI - ), - dispatch_table_entry!( - OSSL_FUNC_ENCODER_ENCODE, - OSSL_FUNC_encoder_encode_fn, - encoder_functions::encodeStructurelessToText - ), - OSSL_DISPATCH::END, - ]; - } - - dispatch_table_module::TEXT_ENCODER_FUNCTIONS - }; -} - -#[named] -pub(super) unsafe extern "C" fn encodeStructurelessToText( - vencoderctx: *mut c_void, - out: *mut OSSL_CORE_BIO, - obj_raw: *const c_void, - _obj_abstract: *const OSSL_PARAM, - selection: c_int, - _cb: OSSL_PASSPHRASE_CALLBACK, - _cbarg: *mut c_void, -) -> c_int { - const SUCCESS: c_int = 1; - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } - - let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); - - if obj_raw.is_null() { - error!(target: log_target!(), "No provider-native object passed to encoder"); - return ERROR_RET; - } - - let keypair: &KeyPair = handleResult!(obj_raw.try_into()); - match &keypair.public { - Some(key) => { - let key_bytes = key.encode(); - use crate::adapters::common::helpers::format_hex_bytes; - let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); - let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; - } - } -} - -impl DoesSelection for Structureless2Text { - const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; -} - -// 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs index 1d1296c..394d956 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs @@ -723,103 +723,6 @@ 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. -pub(crate) struct Structureless2Text(); -impl Encoder for Structureless2Text { - const PROPERTY_DEFINITION: &'static CStr = - c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; - - const DISPATCH_TABLE: &'static [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: &[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, - encoder_functions::does_selection_SPKI - ), - dispatch_table_entry!( - OSSL_FUNC_ENCODER_ENCODE, - OSSL_FUNC_encoder_encode_fn, - encoder_functions::encodeStructurelessToText - ), - OSSL_DISPATCH::END, - ]; - } - - dispatch_table_module::TEXT_ENCODER_FUNCTIONS - }; -} - -#[named] -pub(super) unsafe extern "C" fn encodeStructurelessToText( - vencoderctx: *mut c_void, - out: *mut OSSL_CORE_BIO, - obj_raw: *const c_void, - _obj_abstract: *const OSSL_PARAM, - selection: c_int, - _cb: OSSL_PASSPHRASE_CALLBACK, - _cbarg: *mut c_void, -) -> c_int { - const SUCCESS: c_int = 1; - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } - - let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); - - if obj_raw.is_null() { - error!(target: log_target!(), "No provider-native object passed to encoder"); - return ERROR_RET; - } - - let keypair: &KeyPair = handleResult!(obj_raw.try_into()); - match &keypair.public { - Some(key) => { - let key_bytes = key.encode(); - use crate::adapters::common::helpers::format_hex_bytes; - let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); - let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; - } - } -} - -impl DoesSelection for Structureless2Text { - const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; -} - -// 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); diff --git a/src/adapters/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index 6715f1f..f31f35e 100644 --- a/src/adapters/pqclean/MLDSA65/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65/encoder_functions.rs @@ -723,103 +723,6 @@ 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. -pub(crate) struct Structureless2Text(); -impl Encoder for Structureless2Text { - const PROPERTY_DEFINITION: &'static CStr = - c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; - - const DISPATCH_TABLE: &'static [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: &[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, - encoder_functions::does_selection_SPKI - ), - dispatch_table_entry!( - OSSL_FUNC_ENCODER_ENCODE, - OSSL_FUNC_encoder_encode_fn, - encoder_functions::encodeStructurelessToText - ), - OSSL_DISPATCH::END, - ]; - } - - dispatch_table_module::TEXT_ENCODER_FUNCTIONS - }; -} - -#[named] -pub(super) unsafe extern "C" fn encodeStructurelessToText( - vencoderctx: *mut c_void, - out: *mut OSSL_CORE_BIO, - obj_raw: *const c_void, - _obj_abstract: *const OSSL_PARAM, - selection: c_int, - _cb: OSSL_PASSPHRASE_CALLBACK, - _cbarg: *mut c_void, -) -> c_int { - const SUCCESS: c_int = 1; - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } - - let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); - - if obj_raw.is_null() { - error!(target: log_target!(), "No provider-native object passed to encoder"); - return ERROR_RET; - } - - let keypair: &KeyPair = handleResult!(obj_raw.try_into()); - match &keypair.public { - Some(key) => { - let key_bytes = key.encode(); - use crate::adapters::common::helpers::format_hex_bytes; - let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); - let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; - } - } -} - -impl DoesSelection for Structureless2Text { - const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; -} - -// 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs index 1d1296c..394d956 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs @@ -723,103 +723,6 @@ 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. -pub(crate) struct Structureless2Text(); -impl Encoder for Structureless2Text { - const PROPERTY_DEFINITION: &'static CStr = - c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; - - const DISPATCH_TABLE: &'static [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: &[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, - encoder_functions::does_selection_SPKI - ), - dispatch_table_entry!( - OSSL_FUNC_ENCODER_ENCODE, - OSSL_FUNC_encoder_encode_fn, - encoder_functions::encodeStructurelessToText - ), - OSSL_DISPATCH::END, - ]; - } - - dispatch_table_module::TEXT_ENCODER_FUNCTIONS - }; -} - -#[named] -pub(super) unsafe extern "C" fn encodeStructurelessToText( - vencoderctx: *mut c_void, - out: *mut OSSL_CORE_BIO, - obj_raw: *const c_void, - _obj_abstract: *const OSSL_PARAM, - selection: c_int, - _cb: OSSL_PASSPHRASE_CALLBACK, - _cbarg: *mut c_void, -) -> c_int { - const SUCCESS: c_int = 1; - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } - - let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); - - if obj_raw.is_null() { - error!(target: log_target!(), "No provider-native object passed to encoder"); - return ERROR_RET; - } - - let keypair: &KeyPair = handleResult!(obj_raw.try_into()); - match &keypair.public { - Some(key) => { - let key_bytes = key.encode(); - use crate::adapters::common::helpers::format_hex_bytes; - let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); - let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; - } - } -} - -impl DoesSelection for Structureless2Text { - const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; -} - -// 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); diff --git a/src/adapters/pqclean/MLDSA87/encoder_functions.rs b/src/adapters/pqclean/MLDSA87/encoder_functions.rs index 1d1296c..394d956 100644 --- a/src/adapters/pqclean/MLDSA87/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA87/encoder_functions.rs @@ -723,103 +723,6 @@ 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. -pub(crate) struct Structureless2Text(); -impl Encoder for Structureless2Text { - const PROPERTY_DEFINITION: &'static CStr = - c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"; - - const DISPATCH_TABLE: &'static [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: &[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, - encoder_functions::does_selection_SPKI - ), - dispatch_table_entry!( - OSSL_FUNC_ENCODER_ENCODE, - OSSL_FUNC_encoder_encode_fn, - encoder_functions::encodeStructurelessToText - ), - OSSL_DISPATCH::END, - ]; - } - - dispatch_table_module::TEXT_ENCODER_FUNCTIONS - }; -} - -#[named] -pub(super) unsafe extern "C" fn encodeStructurelessToText( - vencoderctx: *mut c_void, - out: *mut OSSL_CORE_BIO, - obj_raw: *const c_void, - _obj_abstract: *const OSSL_PARAM, - selection: c_int, - _cb: OSSL_PASSPHRASE_CALLBACK, - _cbarg: *mut c_void, -) -> c_int { - const SUCCESS: c_int = 1; - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } - - let encoderctx: &EncoderContext = handleResult!(vencoderctx.try_into()); - - if obj_raw.is_null() { - error!(target: log_target!(), "No provider-native object passed to encoder"); - return ERROR_RET; - } - - let keypair: &KeyPair = handleResult!(obj_raw.try_into()); - match &keypair.public { - Some(key) => { - let key_bytes = key.encode(); - use crate::adapters::common::helpers::format_hex_bytes; - let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); - let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; - } - } -} - -impl DoesSelection for Structureless2Text { - const SELECTION_MASK: Selection = Selection::PUBLIC_KEY; -} - -// 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); From a78123b8e8be033500d6800a766a347702deb6dc Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Tue, 9 Dec 2025 15:05:58 +0200 Subject: [PATCH 06/42] refactor(encoders): take encoder name as argument in text encoder generator macro --- src/adapters/common/transcoders.rs | 23 +++++++++++-------- .../pqclean/MLDSA44/encoder_functions.rs | 5 +++- .../MLDSA44_Ed25519/encoder_functions.rs | 5 +++- .../pqclean/MLDSA65/encoder_functions.rs | 5 +++- .../MLDSA65_Ed25519/encoder_functions.rs | 5 +++- .../pqclean/MLDSA87/encoder_functions.rs | 5 +++- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs index a736e73..f76e3cb 100644 --- a/src/adapters/common/transcoders.rs +++ b/src/adapters/common/transcoders.rs @@ -1,7 +1,11 @@ /// Make a text encoder for a public key. /// /// The encoder outputs the bytes of the key as colon-separated hex values. -/// It takes one argument, `property_definition`, which should be an OpenSSL property query string +/// 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. @@ -9,20 +13,19 @@ /// must have the type `Option`, where `PublicKey` has an `encode` method that returns /// the key data as something coercible to `&[u8]` (e.g. `Vec`). /// -/// The resulting encoder struct is named `Structureless2Text`, and like all other encoders it must -/// be registered in the adapter module's `AdapterContextTrait::register_algorithms` -/// implementation. +/// 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!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +/// make_pubkey_text_encoder!(Structureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); /// ``` macro_rules! make_pubkey_text_encoder { - ($property_definition:literal) => { - pub(crate) struct Structureless2Text(); - impl $crate::forge::operations::transcoders::Encoder for Structureless2Text { + ($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; @@ -116,14 +119,14 @@ macro_rules! make_pubkey_text_encoder { } } - impl $crate::forge::operations::transcoders::DoesSelection for Structureless2Text { + 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; } $crate::forge::operations::transcoders::make_does_selection_fn!( does_selection_text, - Structureless2Text, + $encoder_struct, ProviderInstance ); diff --git a/src/adapters/pqclean/MLDSA44/encoder_functions.rs b/src/adapters/pqclean/MLDSA44/encoder_functions.rs index 394d956..f699bce 100644 --- a/src/adapters/pqclean/MLDSA44/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44/encoder_functions.rs @@ -725,4 +725,7 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; -make_pubkey_text_encoder!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +make_pubkey_text_encoder!( + Structureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs index 394d956..f699bce 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs @@ -725,4 +725,7 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; -make_pubkey_text_encoder!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +make_pubkey_text_encoder!( + Structureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index f31f35e..e276871 100644 --- a/src/adapters/pqclean/MLDSA65/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65/encoder_functions.rs @@ -725,4 +725,7 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; -make_pubkey_text_encoder!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +make_pubkey_text_encoder!( + Structureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs index 394d956..f699bce 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs @@ -725,4 +725,7 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; -make_pubkey_text_encoder!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +make_pubkey_text_encoder!( + Structureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); diff --git a/src/adapters/pqclean/MLDSA87/encoder_functions.rs b/src/adapters/pqclean/MLDSA87/encoder_functions.rs index 394d956..f699bce 100644 --- a/src/adapters/pqclean/MLDSA87/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA87/encoder_functions.rs @@ -725,4 +725,7 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; -make_pubkey_text_encoder!(c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +make_pubkey_text_encoder!( + Structureless2Text, + c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" +); From 8cb94d4da55478859ad2dbe139fa4ceab3356a7e Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Thu, 11 Dec 2025 16:21:05 +0200 Subject: [PATCH 07/42] feat(adapters/common/transcoders): Do not clutter the current namespace when calling `make_pubkey_text_encoder!` Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964001d50feeee0e099d332ab3ce1bd9af8 --- src/adapters/common/transcoders.rs | 112 +++++++++++++++-------------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs index f76e3cb..e6603ff 100644 --- a/src/adapters/common/transcoders.rs +++ b/src/adapters/common/transcoders.rs @@ -32,6 +32,7 @@ macro_rules! make_pubkey_text_encoder { 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}; @@ -53,82 +54,83 @@ macro_rules! make_pubkey_text_encoder { dispatch_table_entry!( OSSL_FUNC_ENCODER_DOES_SELECTION, OSSL_FUNC_encoder_does_selection_fn, - encoder_functions::does_selection_text + does_selection_text ), dispatch_table_entry!( OSSL_FUNC_ENCODER_ENCODE, OSSL_FUNC_encoder_encode_fn, - encoder_functions::encodeStructurelessToText + encodeStructurelessToText ), $crate::forge::bindings::OSSL_DISPATCH::END, ]; - } - dispatch_table_module::TEXT_ENCODER_FUNCTIONS - }; - } + #[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 { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "{}", "Called!"); - #[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 { - const SUCCESS: c_int = 1; - const ERROR_RET: c_int = 0; - trace!(target: log_target!(), "{}", "Called!"); - - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & ($crate::forge::bindings::OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } - - let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & ($crate::forge::bindings::OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } - if obj_raw.is_null() { - error!(target: log_target!(), "No provider-native object passed to encoder"); - return ERROR_RET; - } + let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); - let keypair: &KeyPair = $crate::handleResult!(obj_raw.try_into()); - match &keypair.public { - Some(key) => { - let key_bytes = key.encode(); - use $crate::adapters::common::helpers::format_hex_bytes; - let formatted_key_bytes = format_hex_bytes(15, 4, &key_bytes); - let output = format!("Public key bytes:\n{}\n", formatted_key_bytes); - let output = handleResult!(CString::new(output)); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + if obj_raw.is_null() { + error!(target: log_target!(), "No provider-native object passed to encoder"); return ERROR_RET; } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; + + let keypair: &KeyPair = $crate::handleResult!(obj_raw.try_into()); + 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 = handleResult!(CString::new(output)); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No public key"); + return ERROR_RET; + } + } + } + + $crate::forge::operations::transcoders::make_does_selection_fn!( + does_selection_text, + $encoder_struct, + ProviderInstance + ); } - } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; } + 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; } - $crate::forge::operations::transcoders::make_does_selection_fn!( - does_selection_text, - $encoder_struct, - ProviderInstance - ); } } From 41c720b93a15b9113ba383fb0d7fd3adcd1bbee6 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Thu, 11 Dec 2025 16:21:05 +0200 Subject: [PATCH 08/42] refactor(common/transcoders): make explicit that the Structureless2Text encoder is specific for public keys only Signed-off-by: Nicola Tuveri Change-Id: I6a6a696472f0c21fb074aa6882204c57729f9557 --- src/adapters/common/transcoders.rs | 2 +- src/adapters/pqclean.rs | 10 +++++----- src/adapters/pqclean/MLDSA44.rs | 2 +- src/adapters/pqclean/MLDSA44/encoder_functions.rs | 2 +- src/adapters/pqclean/MLDSA44_Ed25519.rs | 2 +- .../pqclean/MLDSA44_Ed25519/encoder_functions.rs | 2 +- src/adapters/pqclean/MLDSA65.rs | 2 +- src/adapters/pqclean/MLDSA65/encoder_functions.rs | 2 +- src/adapters/pqclean/MLDSA65_Ed25519.rs | 2 +- .../pqclean/MLDSA65_Ed25519/encoder_functions.rs | 2 +- src/adapters/pqclean/MLDSA87.rs | 2 +- src/adapters/pqclean/MLDSA87/encoder_functions.rs | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs index e6603ff..6b65ed7 100644 --- a/src/adapters/common/transcoders.rs +++ b/src/adapters/common/transcoders.rs @@ -20,7 +20,7 @@ /// /// ``` /// use crate::adapters::common::transcoders::make_pubkey_text_encoder; -/// make_pubkey_text_encoder!(Structureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'"); +/// 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) => { diff --git a/src/adapters/pqclean.rs b/src/adapters/pqclean.rs index f31a42b..3f5b12f 100644 --- a/src/adapters/pqclean.rs +++ b/src/adapters/pqclean.rs @@ -85,31 +85,31 @@ impl AdapterContextTrait for PQCleanAdapter { encoder_to_register!(MLDSA44, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA44, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA44, ENCODER_SubjectPublicKeyInfo2PEM), - encoder_to_register!(MLDSA44, ENCODER_Structureless2Text), + encoder_to_register!(MLDSA44, ENCODER_PubKeyStructureless2Text), // MLDSA65 encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA65, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA65, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA65, ENCODER_SubjectPublicKeyInfo2PEM), - encoder_to_register!(MLDSA65, ENCODER_Structureless2Text), + encoder_to_register!(MLDSA65, ENCODER_PubKeyStructureless2Text), // MLDSA87 encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2DER), encoder_to_register!(MLDSA87, ENCODER_PrivateKeyInfo2PEM), encoder_to_register!(MLDSA87, ENCODER_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA87, ENCODER_SubjectPublicKeyInfo2PEM), - encoder_to_register!(MLDSA87, ENCODER_Structureless2Text), + 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_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA65_Ed25519, ENCODER_SubjectPublicKeyInfo2PEM), - encoder_to_register!(MLDSA65_Ed25519, ENCODER_Structureless2Text), + 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_SubjectPublicKeyInfo2DER), encoder_to_register!(MLDSA44_Ed25519, ENCODER_SubjectPublicKeyInfo2PEM), - encoder_to_register!(MLDSA44_Ed25519, ENCODER_Structureless2Text), + 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 9ace1ab..cc2da07 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -399,6 +399,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; +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; diff --git a/src/adapters/pqclean/MLDSA44/encoder_functions.rs b/src/adapters/pqclean/MLDSA44/encoder_functions.rs index f699bce..5a6f65f 100644 --- a/src/adapters/pqclean/MLDSA44/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44/encoder_functions.rs @@ -726,6 +726,6 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; make_pubkey_text_encoder!( - Structureless2Text, + PubKeyStructureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" ); diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 2e83eb7..f4f639e 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -406,6 +406,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; +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; diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs index f699bce..5a6f65f 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs @@ -726,6 +726,6 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; make_pubkey_text_encoder!( - Structureless2Text, + PubKeyStructureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" ); diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index af45cf3..3e48b42 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -399,6 +399,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; +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; diff --git a/src/adapters/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index e276871..fd7bd04 100644 --- a/src/adapters/pqclean/MLDSA65/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65/encoder_functions.rs @@ -726,6 +726,6 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; make_pubkey_text_encoder!( - Structureless2Text, + PubKeyStructureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" ); diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index ed13b69..dda3011 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -406,6 +406,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; +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; diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs index f699bce..5a6f65f 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs @@ -726,6 +726,6 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; make_pubkey_text_encoder!( - Structureless2Text, + PubKeyStructureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" ); diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index 0ccc40a..f2639c7 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -399,6 +399,6 @@ 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::Structureless2Text as ENCODER_Structureless2Text; +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; diff --git a/src/adapters/pqclean/MLDSA87/encoder_functions.rs b/src/adapters/pqclean/MLDSA87/encoder_functions.rs index f699bce..5a6f65f 100644 --- a/src/adapters/pqclean/MLDSA87/encoder_functions.rs +++ b/src/adapters/pqclean/MLDSA87/encoder_functions.rs @@ -726,6 +726,6 @@ impl DoesSelection for SubjectPublicKeyInfo2PEM { // generate the plain text encoder use crate::adapters::common::transcoders::make_pubkey_text_encoder; make_pubkey_text_encoder!( - Structureless2Text, + PubKeyStructureless2Text, c"x.author='QUBIP',x.qubip.adapter='pqclean',output='text'" ); From 9a4e713af40bd29fb394d966a5b355666618f87b Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Thu, 11 Dec 2025 16:21:05 +0200 Subject: [PATCH 09/42] feat(pqclean): Add `ENCODER_PrivateKeyInfo2Text` for MLDSA and Composite MLDSA Signed-off-by: Nicola Tuveri Change-Id: I6a6a69646ca9b10cb2dbf10e29ef781b7f8b9512 --- src/adapters/common/transcoders.rs | 137 ++++++++++++++++++ src/adapters/pqclean.rs | 5 + src/adapters/pqclean/MLDSA44.rs | 1 + .../pqclean/MLDSA44/encoder_functions.rs | 7 + src/adapters/pqclean/MLDSA44_Ed25519.rs | 1 + .../MLDSA44_Ed25519/encoder_functions.rs | 7 + src/adapters/pqclean/MLDSA65.rs | 1 + .../pqclean/MLDSA65/encoder_functions.rs | 7 + src/adapters/pqclean/MLDSA65_Ed25519.rs | 1 + .../MLDSA65_Ed25519/encoder_functions.rs | 7 + src/adapters/pqclean/MLDSA87.rs | 1 + .../pqclean/MLDSA87/encoder_functions.rs | 7 + 12 files changed, 182 insertions(+) diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs index 6b65ed7..095c3b3 100644 --- a/src/adapters/common/transcoders.rs +++ b/src/adapters/common/transcoders.rs @@ -135,3 +135,140 @@ macro_rules! make_pubkey_text_encoder { } } 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 $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, + 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 { + const SUCCESS: c_int = 1; + const ERROR_RET: c_int = 0; + trace!(target: log_target!(), "Called!"); + + debug!(target: log_target!(), "Got selection: {selection:#b}"); + if (selection & ($crate::forge::bindings::OSSL_KEYMGMT_SELECT_PRIVATE_KEY as c_int)) == 0 { + error!(target: log_target!(), "Invalid selection: {selection:#?}"); + return ERROR_RET; + } + + let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); + + 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()); + 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 = handleResult!(CString::new(output)); + match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul()) { + Ok(_bytes_written) => {} + Err(e) => { + error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); + return ERROR_RET; + } + }; + return SUCCESS; + } + None => { + error!(target: log_target!(), "No private key"); + return ERROR_RET; + } + } + } + + $crate::forge::operations::transcoders::make_does_selection_fn!( + does_selection_text, + $encoder_struct, + ProviderInstance + ); + } + + dispatch_table_module::TEXT_ENCODER_FUNCTIONS + }; + } + + 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/pqclean.rs b/src/adapters/pqclean.rs index 3f5b12f..7610d70 100644 --- a/src/adapters/pqclean.rs +++ b/src/adapters/pqclean.rs @@ -83,30 +83,35 @@ 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), diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index cc2da07..9a955ba 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -399,6 +399,7 @@ 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; diff --git a/src/adapters/pqclean/MLDSA44/encoder_functions.rs b/src/adapters/pqclean/MLDSA44/encoder_functions.rs index 5a6f65f..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 = diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index f4f639e..83a9fcc 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -406,6 +406,7 @@ 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; diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA44_Ed25519/encoder_functions.rs index 5a6f65f..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 = diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index 3e48b42..1e88d83 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -399,6 +399,7 @@ 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; diff --git a/src/adapters/pqclean/MLDSA65/encoder_functions.rs b/src/adapters/pqclean/MLDSA65/encoder_functions.rs index fd7bd04..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 = diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index dda3011..64a13e8 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -406,6 +406,7 @@ 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; diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs b/src/adapters/pqclean/MLDSA65_Ed25519/encoder_functions.rs index 5a6f65f..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 = diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index f2639c7..da21fe7 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -399,6 +399,7 @@ 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; diff --git a/src/adapters/pqclean/MLDSA87/encoder_functions.rs b/src/adapters/pqclean/MLDSA87/encoder_functions.rs index 5a6f65f..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 = From 614ce5f56f4d13131c917b28608dab1fbb1b0a0a Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Thu, 11 Dec 2025 16:21:05 +0200 Subject: [PATCH 10/42] refactor(common/transcoders/make_privkey_text_encoder): C functions should only do argument parsing and delegate logic to safe rust abstractions. Signed-off-by: Nicola Tuveri Change-Id: I6a6a696422f724d9619f94c9cf3f433c658fb1be --- src/adapters/common/transcoders.rs | 84 ++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs index 095c3b3..41c1245 100644 --- a/src/adapters/common/transcoders.rs +++ b/src/adapters/common/transcoders.rs @@ -170,7 +170,6 @@ macro_rules! make_privkey_text_encoder { 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}; @@ -212,44 +211,31 @@ macro_rules! make_privkey_text_encoder { _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!"); - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & ($crate::forge::bindings::OSSL_KEYMGMT_SELECT_PRIVATE_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); - return ERROR_RET; - } + 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()); - 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 = handleResult!(CString::new(output)); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No private key"); - return ERROR_RET; - } - } + + 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!( @@ -263,6 +249,48 @@ macro_rules! make_privkey_text_encoder { }; } + 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; From 9313a0ff25ef31f93e3f580aa9d3a69d91dbfd13 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Thu, 11 Dec 2025 16:21:05 +0200 Subject: [PATCH 11/42] refactor(common/transcoders/make_pubkey_text_encoder): C functions should only do argument parsing and delegate logic to safe rust abstractions. Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964239025a5abaf99d00d2014b971efb143 --- src/adapters/common/transcoders.rs | 81 ++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/src/adapters/common/transcoders.rs b/src/adapters/common/transcoders.rs index 41c1245..7da4197 100644 --- a/src/adapters/common/transcoders.rs +++ b/src/adapters/common/transcoders.rs @@ -74,44 +74,30 @@ macro_rules! make_pubkey_text_encoder { _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!"); + trace!(target: log_target!(), " Called!"); - debug!(target: log_target!(), "Got selection: {selection:#b}"); - if (selection & ($crate::forge::bindings::OSSL_KEYMGMT_SELECT_PUBLIC_KEY as c_int)) == 0 { - error!(target: log_target!(), "Invalid selection: {selection:#?}"); + 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; } - let encoderctx: &EncoderContext = $crate::handleResult!(vencoderctx.try_into()); - 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()); - 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 = handleResult!(CString::new(output)); - match encoderctx.provctx.BIO_write_ex(out, &output.into_bytes_with_nul()) { - Ok(_bytes_written) => {} - Err(e) => { - error!(target: log_target!(), "Failure using BIO_write_ex() upcall pointer: {e:?}"); - return ERROR_RET; - } - }; - return SUCCESS; - } - None => { - error!(target: log_target!(), "No public key"); - return ERROR_RET; - } - } + + 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!( @@ -125,6 +111,47 @@ macro_rules! make_pubkey_text_encoder { }; } + 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 = From 35722d923a1d6c76d6cc5fdd968e1b153ef8efd4 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Wed, 22 Oct 2025 14:10:12 +0300 Subject: [PATCH 12/42] test: add basic known-answer tests for composite signatures Kind of a proof-of-concept to get the base64 strings in there. Can't do anything with the secret key until we have decoding for the seed-only format implemented. Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964b3fe303177d8a9c1cb8a6c5b162ba646 --- Cargo.toml | 1 + .../keymgmt_functions_draft12.rs | 108 ++++++++++++++ .../keymgmt_functions_draft12.rs | 137 ++++++++++++++++++ 3 files changed, 246 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index c4b6575..b87cd13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ 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" +base64 = "0.22.1" [build-dependencies] rasn-compiler = "0.11.0" diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index 799b5f9..88e024a 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -1119,4 +1119,112 @@ mod tests { assert_eq!(SECURITY_BITS, 128); } + + use base64::{engine::general_purpose, Engine}; + + /* + * The following test keys and signature are taken as an example from + * https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-test-vectors + */ + const PK: &str = "MGuttJkrCtubxkMPmdME1/Ni\ + luVggzbeawbjHTzExidEYs0gcfhC7jLCNY9CGbYrtcySXBjhb+58fzVMzYcmy99DNgxx\ + 6zJngRsxQOmK2JGXmTMR85vkLyZp+h9ZmaWkla8cb6errJ5JvIeZPGqLdufpqG4l24ry\ + WfY1zi0wazXj1BZJZWdEu3/HzeEYfLvGZyjEQulqGdoqkUZj6RTEAO/nJ+kaN/3nmozu\ + tKAX+1sQbO6oocDZ1sjTuE/48Exc0diHygPyMptD50feFCPUshYFYeS3Cmf2cWR88qre\ + 9wrpu/2nydzeLf3GXF7KO735Jw3M1R+flSKwsfGtEMdp4OVsfPEcXvAhQH40Hlq0Z6B2\ + 1CXNTvMSUccKzH8qDRtX8H+hVA4CI90NT7N+eOGSLp44+jP63R4vXoqwI6116udwcveU\ + IhmiasHB9Msap4TSMm8LpANiUZjwfe7pBkpVNuA8czKrb6/I7zDEkGotlHYOrCroNJ+G\ + j+l0xsLxrsQ/OJzkWPVH+ydOsvDebHlatyyrdUXdBu/VYHibmAHqr9q14wtwpEsGeZF6\ + xRviOvTSDXBDwoGLC9OOrLf5+w8669xfkGfLLMDsAYCpqjWsa4jmcKC7gYhVgWfQxfnI\ + FGlAtl1hXsJifBs8z+bwI7vbN5IuWyrxSvUN6Tnn3HqzwNGtT/6gOdCEqOZbReXdvIJN\ + CzIkqm+SrqCvVun+DdaHziGQdwpjHv3Baf9jXBBCw8azWnYmD9z/c5HbcjLnTQ4PkL6K\ + QWmXkSnTU1fOE1UjqoE8+yQckNC7eU/NdBxtri5yUtbF6l9vToywovcpjALVWr7eq7fF\ + nCJ2sjb9UPk4i5AmwGKScLCuUFx3etjgcWfwbcuwXe3ErlVMGXBNvkd+xUfACOyWepf4\ + VixlZDbfb2P9ILF0PYyHEdU1R5FM3uDAoPXUfVQ/zN+cf9jqhWcOB11LL63pade9IIkS\ + XN5AySHWiU3GqjTe81X5Ta5EITm7asCcAzwBYlACFweLO+qUe9RAz0FhKnsOGce0pA7e\ + w94+mbae14v24k0PHLw2/vvfskB0kopPJa3x+jC8TijPg1SDYyj4tyllmfjh+0zEuTkW\ + kT/C9co7m0Sd8N5xogLYFR4v6wxWpPBvGRi1iX3hybZvYClTXurBnvMGXzEtcowBCJrx\ + V3WZ3gG6sKx/m18VdMzma5IesD1R9+Vn9cLVupe73/XVLhF//CL109qLe1jgx2NRVQeG\ + ourqvJ6qDWM6Y4JFvAjto/Pzvj7AH4HTAvUEBNGY3X5ACUQoSMtJyMsQTeFsziV8JhrG\ + figjpZDUBRN3Rmn0ts/yROotqodTmG5CKXLZWyN6aDBpaQv+91jd8QoXf8Gq6JsXA2pN\ + 70+Z6Nu2Kx+qmr7WEOQ6rzOs30eqWPt9yKbR3OB1f9atj596OjW7x5nV73pwaW8IHrlv\ + UP26+pj5xcMFv8kNfPHaNbSZkxf7CBp+WoZVMdVemuewrzJPp5rsbOMjpiddC8RLt5f5\ + Yznq2xdDcqelZu/Rrq/j/LNuozsxtZNeWeOaHFd6PcsSY27+dvrph57qenI0kauvCn5H\ + tCIRaViT8fXmEg0YNsZPOdZD99EFiPtj94jpCXbZcoFXbXti6Ht/F69u7nU9XnaHvNMl\ + 8pV8R3U+VbU5FaHk/B8OJCld8mqFkTVMPVWpGqWDFgtmvzp2g+r7mEh4HzdnEOIOwVxa\ + 8lddM6IoDU2guXpcBNdCWekH3/0873n7lSbWaHrjslmAiew/tHUTCEvrIWIRYlq2H3c9"; + + // TODO: add a way to derive the expanded secret key from the seed so we can actually use this + const _SK: &str = "BPF\ + hmtxufqsWNGCyg9kQwRGp4Z8nlIWggybYyiYHd6vkiyBsVB9m3mhTHMmFlKGC8xfC0Nl\ + YL9LpGJu7G+Qiyw=="; + + const SIG: &str = "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\ + oN"; + + const MSG: &str = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4="; + + #[test] + fn test_import_pubkey_verify_signature() { + let input_sig_bytes = general_purpose::STANDARD.decode(SIG).unwrap(); + let sig = + Signature::try_from(input_sig_bytes.as_slice()).expect("Signature decoding failed"); + + let input_pk = general_purpose::STANDARD.decode(PK).unwrap(); + let pk = PublicKey::decode(input_pk.as_slice()).expect("Failure while decoding Public Key"); + + eprintln!("\n\nPublic Key: {pk:?}\n"); + + let msg = general_purpose::STANDARD.decode(MSG).unwrap(); + + pk.verify(&msg, &sig).expect("Verify failed"); + } } diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index 0da0d90..1dfc929 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -1119,4 +1119,141 @@ mod tests { assert_eq!(SECURITY_BITS, 192); } + + use base64::{engine::general_purpose, Engine}; + + /* + * The following test keys and signature are taken as an example from + * https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-test-vectors + */ + const PK: &str = "rvG6fbi3GP3DBRHsmQRl4YZuMg0uDjyO\ + 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\ + Y4m4ItmV7TQFEJZdBSz5ygVBOqcPSA=="; + + // TODO: add a way to derive the expanded secret key from the seed so we can actually use this + const _SK: &str = "EkUToDUvNsneugbHg799w72nJfVUserx92MZD7j\ + A+0hZ4KFiurjdBrQPCwinrht9O4uhkdU13atErq6x6IjWMw=="; + + const SIG: &str = "A7yvehKyIbxyO0\ + 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"; + + const MSG: &str = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4="; + + #[test] + fn test_import_pubkey_verify_signature() { + let input_sig_bytes = general_purpose::STANDARD.decode(SIG).unwrap(); + let sig = + Signature::try_from(input_sig_bytes.as_slice()).expect("Signature decoding failed"); + + let input_pk = general_purpose::STANDARD.decode(PK).unwrap(); + let pk = PublicKey::decode(input_pk.as_slice()).expect("Failure while decoding Public Key"); + + eprintln!("\n\nPublic Key: {pk:?}\n"); + + let msg = general_purpose::STANDARD.decode(MSG).unwrap(); + + pk.verify(&msg, &sig).expect("Verify failed"); + } } From 3f4a24ffbeb5d9d73c45a4a1fe50266661de3b5c Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 21 Nov 2025 15:43:18 +0200 Subject: [PATCH 13/42] feat(tests): add basic wycheproof test for MLDSA65_Ed25519 --- Cargo.toml | 1 + .../keymgmt_functions_draft12.rs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index b87cd13..8216a56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ pqcrypto-traits = { version = "0.3.5", optional = true } sha2 = { version = "0.10.9", optional = true } itertools = "0.14.0" base64 = "0.22.1" +wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft" } [build-dependencies] rasn-compiler = "0.11.0" diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index 1dfc929..cef5cb2 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -1256,4 +1256,27 @@ mod tests { pk.verify(&msg, &sig).expect("Verify failed"); } + + use wycheproof::composite_mldsa_verify; + + #[test] + fn test_import_pubkey_verify_signature_wycheproof() { + let test_set = + composite_mldsa_verify::TestSet::load(composite_mldsa_verify::TestName::MlDsa65Ed25519) + .unwrap_or_else(|e| panic!("Failed to load verify test set: {e}")); + + // right now the only composite verification test in wycheproof is one that should pass; if + // others are added later that should fail, this code will need more conditionals + for group in test_set.test_groups { + let input_pk = group.pubkey; + let pk = PublicKey::decode(&input_pk).expect("Failure while decoding Public Key"); + eprintln!("\n\nPublic Key: {pk:?}\n"); + for test in &group.tests { + let sig = + Signature::try_from(test.sig.as_ref()).expect("Signature decoding failed"); + + pk.verify(&test.msg, &sig).expect("Verify failed"); + } + } + } } From 2455ef825ddf291d5e657341113e375e6671eb78 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 24 Nov 2025 11:21:49 +0200 Subject: [PATCH 14/42] feat(tests): add wycheproof verify tests for pure ML-DSA This places the tests in the modules themselves (e.g. `adapters::pqclean::MLDSA65`) instead of in the `signature` or submodule, which is a bit awkward; it would be nice to figure out how to move them there/if it's possible to make them generic enough to work in that file. Signed-off-by: Nicola Tuveri Change-Id: I6a6a69647cb96f538961f26f61a1f5227a8da805 --- src/adapters/common.rs | 1 + src/adapters/common/wycheproof.rs | 173 ++++++++++++++++++++++++++++++ src/adapters/pqclean/MLDSA44.rs | 47 ++++++++ src/adapters/pqclean/MLDSA65.rs | 47 ++++++++ src/adapters/pqclean/MLDSA87.rs | 47 ++++++++ 5 files changed, 315 insertions(+) create mode 100644 src/adapters/common/wycheproof.rs diff --git a/src/adapters/common.rs b/src/adapters/common.rs index f9e8607..ca34f39 100644 --- a/src/adapters/common.rs +++ b/src/adapters/common.rs @@ -7,3 +7,4 @@ pub mod keymgmt_functions; pub mod macros; pub mod transcoders; +pub mod wycheproof; diff --git a/src/adapters/common/wycheproof.rs b/src/adapters/common/wycheproof.rs new file mode 100644 index 0000000..f84be90 --- /dev/null +++ b/src/adapters/common/wycheproof.rs @@ -0,0 +1,173 @@ +use crate::forge::crypto::signature; +use wycheproof::mldsa_verify::{self, *}; +use wycheproof::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>; +} + +// currently unused; since none of our adapters implement verify_with_ctx, we have manual impls for +// the SigAlgVerifyVariant trait that return an error there instead of calling a method +#[macro_export] +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) + } + } + }; +} + +/// 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: TestName) { + let test_set = mldsa_verify::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 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 { + 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"); +} diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index 9a955ba..2f27b1a 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -403,3 +403,50 @@ pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2 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; + use wycheproof::mldsa_verify::TestName; + + struct Mldsa44; + + impl SigAlgVerifyVariant for Mldsa44 { + type PublicKey = keymgmt_functions::PublicKey; + + type Signature = signature::Signature; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { + Self::PublicKey::decode(bytes) + } + + fn decode_signature(bytes: &[u8]) -> anyhow::Result { + Self::Signature::try_from(bytes) + } + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error> { + pubkey.verify(msg, sig) + } + + // This adapter does not support signatures with a non-empty ctx. + fn verify_with_ctx( + _pubkey: &Self::PublicKey, + _msg: &[u8], + _sig: &Self::Signature, + _ctx: &[u8], + ) -> Result<(), signature::Error> { + Err(signature::Error::new()) + } + } + + #[test] + fn test_mldsa_44_verify_from_wycheproof() { + run_mldsa_wycheproof_verify_tests::(TestName::MlDsa44Verify); + } +} diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index 1e88d83..2d1de26 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -403,3 +403,50 @@ pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2 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; + use wycheproof::mldsa_verify::TestName; + + struct Mldsa65; + + impl SigAlgVerifyVariant for Mldsa65 { + type PublicKey = keymgmt_functions::PublicKey; + + type Signature = signature::Signature; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { + Self::PublicKey::decode(bytes) + } + + fn decode_signature(bytes: &[u8]) -> anyhow::Result { + Self::Signature::try_from(bytes) + } + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error> { + pubkey.verify(msg, sig) + } + + // This adapter does not support signatures with a non-empty ctx. + fn verify_with_ctx( + _pubkey: &Self::PublicKey, + _msg: &[u8], + _sig: &Self::Signature, + _ctx: &[u8], + ) -> Result<(), signature::Error> { + Err(signature::Error::new()) + } + } + + #[test] + fn test_mldsa_65_verify_from_wycheproof() { + run_mldsa_wycheproof_verify_tests::(TestName::MlDsa65Verify); + } +} diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index da21fe7..46931f2 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -403,3 +403,50 @@ pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2 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; + use wycheproof::mldsa_verify::TestName; + + struct Mldsa87; + + impl SigAlgVerifyVariant for Mldsa87 { + type PublicKey = keymgmt_functions::PublicKey; + + type Signature = signature::Signature; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { + Self::PublicKey::decode(bytes) + } + + fn decode_signature(bytes: &[u8]) -> anyhow::Result { + Self::Signature::try_from(bytes) + } + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error> { + pubkey.verify(msg, sig) + } + + // This adapter does not support signatures with a non-empty ctx. + fn verify_with_ctx( + _pubkey: &Self::PublicKey, + _msg: &[u8], + _sig: &Self::Signature, + _ctx: &[u8], + ) -> Result<(), signature::Error> { + Err(signature::Error::new()) + } + } + + #[test] + fn test_mldsa_87_verify_from_wycheproof() { + run_mldsa_wycheproof_verify_tests::(TestName::MlDsa87Verify); + } +} From 158015dbf372d434347cba3fe36b8290fa6d1f66 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 24 Nov 2025 14:55:51 +0200 Subject: [PATCH 15/42] feat(tests): use a testing harness for wycheproof mldsa65ed25519 tests Also removes the old test for this algorithm that used hardcoded base64 strings. However, the dependency on the base64 crate remains, because the mldsa44ed25519 test still uses them. --- src/adapters/common/wycheproof.rs | 108 +++++++++++- src/adapters/pqclean/MLDSA65_Ed25519.rs | 48 ++++++ .../keymgmt_functions_draft12.rs | 160 ------------------ 3 files changed, 154 insertions(+), 162 deletions(-) diff --git a/src/adapters/common/wycheproof.rs b/src/adapters/common/wycheproof.rs index f84be90..064efee 100644 --- a/src/adapters/common/wycheproof.rs +++ b/src/adapters/common/wycheproof.rs @@ -1,5 +1,6 @@ use crate::forge::crypto::signature; -use wycheproof::mldsa_verify::{self, *}; +use wycheproof::composite_mldsa_verify; +use wycheproof::mldsa_verify; use wycheproof::TestResult; pub trait SigAlgVerifyVariant { @@ -68,7 +69,9 @@ macro_rules! impl_sigalg_verify_variant { /// /// 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: TestName) { +pub fn run_mldsa_wycheproof_verify_tests( + test_name: mldsa_verify::TestName, +) { let test_set = mldsa_verify::TestSet::load(test_name) .unwrap_or_else(|e| panic!("Failed to load sign test set: {e}")); let mut passed = 0; @@ -171,3 +174,104 @@ pub fn run_mldsa_wycheproof_verify_tests(tes ); 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 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 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"); +} diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 64a13e8..9c875ac 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -410,3 +410,51 @@ pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2 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; + use wycheproof::composite_mldsa_verify::TestName; + + #[allow(non_camel_case_types)] + struct Mldsa65_Ed25519; + + impl SigAlgVerifyVariant for Mldsa65_Ed25519 { + type PublicKey = keymgmt_functions::PublicKey; + + type Signature = signature::Signature; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { + Self::PublicKey::decode(bytes) + } + + fn decode_signature(bytes: &[u8]) -> anyhow::Result { + Self::Signature::try_from(bytes) + } + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error> { + pubkey.verify(msg, sig) + } + + // This adapter does not support signatures with a non-empty ctx. + fn verify_with_ctx( + _pubkey: &Self::PublicKey, + _msg: &[u8], + _sig: &Self::Signature, + _ctx: &[u8], + ) -> Result<(), signature::Error> { + Err(signature::Error::new()) + } + } + + #[test] + fn test_mldsa_65_ed_25519_verify_from_wycheproof() { + run_composite_mldsa_wycheproof_verify_tests::(TestName::MlDsa65Ed25519); + } +} diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index cef5cb2..0da0d90 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -1119,164 +1119,4 @@ mod tests { assert_eq!(SECURITY_BITS, 192); } - - use base64::{engine::general_purpose, Engine}; - - /* - * The following test keys and signature are taken as an example from - * https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-test-vectors - */ - const PK: &str = "rvG6fbi3GP3DBRHsmQRl4YZuMg0uDjyO\ - 2Dq8ZTgCgefQbit+7iYSXcSTxQHP4AE6DSOfjXfx8SrZFzFiiuwC6cCMX5UBOKzba+pz\ - oUZW9dQX6AkGnhotB55OCzwZAaa4CifaigVCPK/mlY31Smcc+HY0alwRvL/svVpaaK6d\ - 8ZWOWrSNKfBUysGPazZ/XBggKzQDJjtOUwweW4GJqLiFvJfn4WKa9kspKS7s8eqHWwzo\ - rrdJta+SpCc5GtHj3LgQ+Ocee79yXI0NJuBuPL/PTuWguyV7Pz6JJOKFl3/oRgcQBVq9\ - okCbZGudvfTS2B+3QmdiR2CXJ9feiqXJTs4Dgml3TW8xIeYhN2rVzKZwhp+aE0p7x150\ - Lsm4KVGNSHON5LFtRTlvwDhbfAgkI/J4Mu8bMhVlXfW6uHijo0CZXU8eWEyVt+p1kEi2\ - 4RwG4Zg7bPY3a0sMbtfT3faC6TLRRZgYQizyGtb8plEUV0y3bCtrZeTxnAdOmFVDisfc\ - 2J6G8MSi2QOUJHMBq3o3fETmpf486KZTUVSGO2rgMtWhJoKRJhGImcaH4W/3HEJx34vg\ - frvWU+6AzWAqAak2xMqV9lwMhi3C9oOUfcKYtjjS57H34ID4+J4CQbv0ppukBhKrabBT\ - H7U7BLIj4Y5SNxX9V19goLBgnJrSqsiT+lJya6qDlNZ81I4Vh0coehEeM44LqIYTQRAi\ - 9Vu1Izc7DVELDGEMalNf/iLA+1kF2j0T6GrNUEvD+vZH3SX0tJz3eM/qBBqZFYsbOf8z\ - iIOuyQ5EzZ1S6RwsBLtQriroC2rj3Hh9MSPWcKYZOaEIqC8oJ9qBIbWjXgN2dIKI6yWJ\ - YZlKitLRtsRUfLUJDUuaktUbdvnfautUNTj7yPpiAE5y8c1XFqPQEqukisWD0H9MWKLV\ - MNYW+FVzdc/VL9AkH3SWPln7hX0hVtTU5atD1KM8ZAMiGb8uDSRD9qCGyL1viQD2Czvs\ - lB/iEqpbD9aAWGh7LbuS1A7HjeQPyw9GLQH/AHqxfKgr2IpwRC3hAvH8bk/dkRyWvN7l\ - JkWs7qs4BGY71UtPG2xcE/N3N9ur254N4P60bwp1uV1/H0o0fbjHwMqTH5uGY1DuYVnK\ - mfdTuBi8vjv3NCbVpF1E7yym6TuiSl6MMbgHnyWyvUfpm8ROkP1N+yRiUxDEr1l0NvPB\ - Yn5lUyUtjWVBYOLwE9pLZbL3M41VNN+6ks+GjpVUMUlVWSb8QMYb1psCxWrV9CzJJZYX\ - 7qPfe5FbnrYn84AfLWjcrfAvtCL1y1C66xJ5MoOJqfZr+MwUsm9HyYr6h00yRgqXSABY\ - VNXjFouXrlW9cvq7Qb4XHndXeUQNpPHAfTq3CzOKutc0+CIjvij7zM1+zvnrLhZCmxkH\ - OXMXgbzmrQP3bhdDSQfRWNp1uzYUox2Mu6uUpSCAdckCb/5qGBeou5K2iV6YHDlGU7e1\ - dBDLN3X5yPEVXJnHXGIrLZl7Dpm8pnjkKEQ063XmNyWXtvYfVdLXgLvsHqHltVWyNFtW\ - Zgrgm5WmMofhWaW6BXcgKpePp8CxfWpBpFFMctmfonpE4nN11xR8NkDk4q2KY2BOK0em\ - QgPxmfU124U5QcjgA5kQFISr2PUn1292xNBjayY9m0wnNgQKdzToOu6CDNSDKIDUINIv\ - kwBq18+2iHFg1PQkYJpDw4lV88jx35g9LyKGbJ5KDlIY1yuHL5bg2C4VQyDwih8En+5+\ - 3s0Zafhb7UnyLNdgpCEmk1vjrvsiKtTkYpJEcnP4wkppbURZJGz8St+zt5MfQkc9muQB\ - iVS0gFxGNGAr9EnvpAK6wxICGh5j038vqxiIGqgMwzL5RZGD1aS7m01V0KlHqvVcjYi3\ - Fxyk8rjXTn+xy2BgIOWEkr0asApDMsfj92DaTPw70CrPiYwalU0s0iSUamptEHzUMw7F\ - rGiKkYCdHZO6j9w7aA4J5IwCLgoEnl2Mz/9gJvsXW1J0GjRKIwtcrK0hN/crLXsir2ZJ\ - opv4piNr5qzhKDi1KuOh7qAZf7woJyY+QznJOXccW8CmBv4rPqXjipoAmqpgokWdX7SF\ - whXAqbsPiZ6A/mY+BukdlGUF0ddghH7SuyNt/5uRhU+3fyY/5xuond7l+8v2JZZKMfFz\ - Coe1/hhrI+Ws4N2uRGk44cdCSqECxTav+tS+x4rS8RpBT0qJ0lq7cpWvLsIXGfIojLD0\ - LgeuyHUhNt7b2qXLu+Qzuu+jrZnPE1uBVZmRNIjLslPyEsdVUIdV6p5MgLsjDVKs65SQ\ - 3T7BVWyB+Oh1Kp+w7sBoEsraczlzGvk5ADGU8aRfTbOeCAeKnBNkZDBdmJ+l+5PWXaYN\ - U99Rv0Rj75tCCA5QbDaPXo+XTAH0H+kKjNN3vHI6IIrZrgft2saq7K0j23ngac+mp03I\ - 1NO9ntBx8JrOIM6jjH1lS3xyXS3Ch7d/bM+3UT26E8ECywRoOtnKaavq3LdUl0xm8VKe\ - Zt5pGQHYbk5pNIyqWKTx3KhPVdfeJBS7vYZXLuUgcUC373JVXdrY+wDmaLtMCZ/Ug4ym\ - p7q6mPkvYI2xjfSAxI5oTnD6CyD1H0MTpI0D+jRumZYJwf7qwzHI/F6A0ZLhvkI45s5C\ - Y4m4ItmV7TQFEJZdBSz5ygVBOqcPSA=="; - - // TODO: add a way to derive the expanded secret key from the seed so we can actually use this - const _SK: &str = "EkUToDUvNsneugbHg799w72nJfVUserx92MZD7j\ - A+0hZ4KFiurjdBrQPCwinrht9O4uhkdU13atErq6x6IjWMw=="; - - const SIG: &str = "A7yvehKyIbxyO0\ - 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"; - - const MSG: &str = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4="; - - #[test] - fn test_import_pubkey_verify_signature() { - let input_sig_bytes = general_purpose::STANDARD.decode(SIG).unwrap(); - let sig = - Signature::try_from(input_sig_bytes.as_slice()).expect("Signature decoding failed"); - - let input_pk = general_purpose::STANDARD.decode(PK).unwrap(); - let pk = PublicKey::decode(input_pk.as_slice()).expect("Failure while decoding Public Key"); - - eprintln!("\n\nPublic Key: {pk:?}\n"); - - let msg = general_purpose::STANDARD.decode(MSG).unwrap(); - - pk.verify(&msg, &sig).expect("Verify failed"); - } - - use wycheproof::composite_mldsa_verify; - - #[test] - fn test_import_pubkey_verify_signature_wycheproof() { - let test_set = - composite_mldsa_verify::TestSet::load(composite_mldsa_verify::TestName::MlDsa65Ed25519) - .unwrap_or_else(|e| panic!("Failed to load verify test set: {e}")); - - // right now the only composite verification test in wycheproof is one that should pass; if - // others are added later that should fail, this code will need more conditionals - for group in test_set.test_groups { - let input_pk = group.pubkey; - let pk = PublicKey::decode(&input_pk).expect("Failure while decoding Public Key"); - eprintln!("\n\nPublic Key: {pk:?}\n"); - for test in &group.tests { - let sig = - Signature::try_from(test.sig.as_ref()).expect("Signature decoding failed"); - - pk.verify(&test.msg, &sig).expect("Verify failed"); - } - } - } } From ac5cbbfed86ce725812e78391ad57d0d68b474ea Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Thu, 27 Nov 2025 11:58:29 +0200 Subject: [PATCH 16/42] feat(tests): add wycheproof signing tests for ML-DSA (pure & composite) --- src/adapters/common/wycheproof.rs | 283 +++++++++++++++++++++++- src/adapters/pqclean/MLDSA44.rs | 48 +++- src/adapters/pqclean/MLDSA44_Ed25519.rs | 94 ++++++++ src/adapters/pqclean/MLDSA65.rs | 44 ++++ src/adapters/pqclean/MLDSA65_Ed25519.rs | 44 ++++ src/adapters/pqclean/MLDSA87.rs | 44 ++++ 6 files changed, 552 insertions(+), 5 deletions(-) diff --git a/src/adapters/common/wycheproof.rs b/src/adapters/common/wycheproof.rs index 064efee..46d1a21 100644 --- a/src/adapters/common/wycheproof.rs +++ b/src/adapters/common/wycheproof.rs @@ -1,7 +1,7 @@ use crate::forge::crypto::signature; -use wycheproof::composite_mldsa_verify; -use wycheproof::mldsa_verify; -use wycheproof::TestResult; +use wycheproof::{ + composite_mldsa_sign, composite_mldsa_verify, mldsa_sign, mldsa_verify, TestResult, +}; pub trait SigAlgVerifyVariant { type PublicKey; @@ -275,3 +275,280 @@ pub fn run_composite_mldsa_wycheproof_verify_tests 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; +} + +/// 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, +) { + let test_set = 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. + */ + + /* 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 { + 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 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/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index 2f27b1a..d954581 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -409,7 +409,7 @@ mod tests { use super::*; use crate::adapters::common::wycheproof::*; use signature::Verifier; - use wycheproof::mldsa_verify::TestName; + use wycheproof::mldsa_verify; struct Mldsa44; @@ -447,6 +447,50 @@ mod tests { #[test] fn test_mldsa_44_verify_from_wycheproof() { - run_mldsa_wycheproof_verify_tests::(TestName::MlDsa44Verify); + run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa44Verify); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer}; + use wycheproof::mldsa_sign; + + impl SigAlgSignVariant for Mldsa44 { + type PrivateKey = keymgmt_functions::PrivateKey; + + type Signature = signature::Signature; + + fn decode_privkey(bytes: &[u8]) -> anyhow::Result { + Self::PrivateKey::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 { + // this adapter doesn't implement signing with ctx yet + Err(signature::Error::new()) + } + + fn encode_signature(sig: &Self::Signature) -> Vec { + Vec::from(sig.to_bytes().as_ref()) + } + } + + #[test] + fn test_mldsa_44_sign_from_wycheproof() { + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa44SignNoSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); } } diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 83a9fcc..f43ac60 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -410,3 +410,97 @@ pub(super) use encoder_functions::PrivateKeyInfo2Text as ENCODER_PrivateKeyInfo2 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; + use wycheproof::composite_mldsa_verify; + + #[allow(non_camel_case_types)] + struct Mldsa44_Ed25519; + + impl SigAlgVerifyVariant for Mldsa44_Ed25519 { + type PublicKey = keymgmt_functions::PublicKey; + + type Signature = signature::Signature; + + fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { + Self::PublicKey::decode(bytes) + } + + fn decode_signature(bytes: &[u8]) -> anyhow::Result { + Self::Signature::try_from(bytes) + } + + fn verify( + pubkey: &Self::PublicKey, + msg: &[u8], + sig: &Self::Signature, + ) -> Result<(), signature::Error> { + pubkey.verify(msg, sig) + } + + // This adapter does not support signatures with a non-empty ctx. + fn verify_with_ctx( + _pubkey: &Self::PublicKey, + _msg: &[u8], + _sig: &Self::Signature, + _ctx: &[u8], + ) -> Result<(), signature::Error> { + Err(signature::Error::new()) + } + } + + #[test] + fn test_mldsa_44_ed_25519_verify_from_wycheproof() { + run_composite_mldsa_wycheproof_verify_tests::( + composite_mldsa_verify::TestName::MlDsa44Ed25519, + ); + } + + use signature::{SignatureBytes, SignatureEncoding, Signer}; + use wycheproof::composite_mldsa_sign; + + impl SigAlgSignVariant for Mldsa44_Ed25519 { + type PrivateKey = keymgmt_functions::PrivateKey; + + type Signature = signature::Signature; + + fn decode_privkey(bytes: &[u8]) -> anyhow::Result { + Self::PrivateKey::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 { + // this adapter doesn't implement signing with ctx yet + Err(signature::Error::new()) + } + + fn encode_signature(sig: &Self::Signature) -> Vec { + Vec::from(sig.to_bytes().as_ref()) + } + } + + #[test] + fn test_mldsa_44_ed_25519_sign_from_wycheproof() { + 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/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index 2d1de26..a7ebee0 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -449,4 +449,48 @@ mod tests { fn test_mldsa_65_verify_from_wycheproof() { run_mldsa_wycheproof_verify_tests::(TestName::MlDsa65Verify); } + + use signature::{SignatureBytes, SignatureEncoding, Signer}; + use wycheproof::mldsa_sign; + + impl SigAlgSignVariant for Mldsa65 { + type PrivateKey = keymgmt_functions::PrivateKey; + + type Signature = signature::Signature; + + fn decode_privkey(bytes: &[u8]) -> anyhow::Result { + Self::PrivateKey::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 { + // this adapter doesn't implement signing with ctx yet + Err(signature::Error::new()) + } + + fn encode_signature(sig: &Self::Signature) -> Vec { + Vec::from(sig.to_bytes().as_ref()) + } + } + + #[test] + fn test_mldsa_65_sign_from_wycheproof() { + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa65SignNoSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } } diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 9c875ac..0a2c805 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -457,4 +457,48 @@ mod tests { fn test_mldsa_65_ed_25519_verify_from_wycheproof() { run_composite_mldsa_wycheproof_verify_tests::(TestName::MlDsa65Ed25519); } + + use signature::{SignatureBytes, SignatureEncoding, Signer}; + use wycheproof::composite_mldsa_sign; + + impl SigAlgSignVariant for Mldsa65_Ed25519 { + type PrivateKey = keymgmt_functions::PrivateKey; + + type Signature = signature::Signature; + + fn decode_privkey(bytes: &[u8]) -> anyhow::Result { + Self::PrivateKey::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 { + // this adapter doesn't implement signing with ctx yet + Err(signature::Error::new()) + } + + fn encode_signature(sig: &Self::Signature) -> Vec { + Vec::from(sig.to_bytes().as_ref()) + } + } + + #[test] + fn test_mldsa_65_ed_25519_sign_from_wycheproof() { + 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/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index 46931f2..c1c1852 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -449,4 +449,48 @@ mod tests { fn test_mldsa_87_verify_from_wycheproof() { run_mldsa_wycheproof_verify_tests::(TestName::MlDsa87Verify); } + + use signature::{SignatureBytes, SignatureEncoding, Signer}; + use wycheproof::mldsa_sign; + + impl SigAlgSignVariant for Mldsa87 { + type PrivateKey = keymgmt_functions::PrivateKey; + + type Signature = signature::Signature; + + fn decode_privkey(bytes: &[u8]) -> anyhow::Result { + Self::PrivateKey::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 { + // this adapter doesn't implement signing with ctx yet + Err(signature::Error::new()) + } + + fn encode_signature(sig: &Self::Signature) -> Vec { + Vec::from(sig.to_bytes().as_ref()) + } + } + + #[test] + fn test_mldsa_87_sign_from_wycheproof() { + run_mldsa_wycheproof_sign_tests::( + mldsa_sign::TestName::MlDsa87SignNoSeed, + // pqclean doesn't support deterministic ML-DSA + false, + ); + } } From 803ba762bc52b90808200e53035dd216013ef3e5 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Thu, 27 Nov 2025 13:57:36 +0200 Subject: [PATCH 17/42] fix(tests): don't refer to verify tests as sign tests in error message --- src/adapters/common/wycheproof.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/common/wycheproof.rs b/src/adapters/common/wycheproof.rs index 46d1a21..41c856b 100644 --- a/src/adapters/common/wycheproof.rs +++ b/src/adapters/common/wycheproof.rs @@ -73,7 +73,7 @@ pub fn run_mldsa_wycheproof_verify_tests( test_name: mldsa_verify::TestName, ) { let test_set = mldsa_verify::TestSet::load(test_name) - .unwrap_or_else(|e| panic!("Failed to load sign test set: {e}")); + .unwrap_or_else(|e| panic!("Failed to load verify test set: {e}")); let mut passed = 0; let mut failed = 0; @@ -179,7 +179,7 @@ pub fn run_composite_mldsa_wycheproof_verify_tests Date: Thu, 27 Nov 2025 14:00:05 +0200 Subject: [PATCH 18/42] fix(tests): check test flags before describing key decoding error as "expected" Previously, if decoding of the pubkey or privkey failed, the ML-DSA Wycheproof tests would report that the attempt to decode the key "failed as expected" (and mark the test as passed) for every test case that was meant to fail, including test cases that were meant to successfully decode the key but then fail for some other reason (e.g. the "context too long" error expected in test case 5 of the signing tests). (In practice, this only showed up for the privkey in the signing tests, since tc5 was unexpectedly passing when I started running the seed-only signing tests which aurora shouldn't yet be able to pass any of, but I've preemptively fixed the same type of problem for the pubkey in the verification tests as well.) --- src/adapters/common/wycheproof.rs | 40 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/adapters/common/wycheproof.rs b/src/adapters/common/wycheproof.rs index 41c856b..32bf7ae 100644 --- a/src/adapters/common/wycheproof.rs +++ b/src/adapters/common/wycheproof.rs @@ -72,8 +72,10 @@ macro_rules! impl_sigalg_verify_variant { pub fn run_mldsa_wycheproof_verify_tests( test_name: mldsa_verify::TestName, ) { - let test_set = mldsa_verify::TestSet::load(test_name) - .unwrap_or_else(|e| panic!("Failed to load verify test set: {e}")); + 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; @@ -94,7 +96,9 @@ pub fn run_mldsa_wycheproof_verify_tests( Ok(pk) => pk, Err(e) => { for test in &group.tests { - if test.result == TestResult::Invalid { + if test.result == TestResult::Invalid + && test.flags.contains(&TestFlag::IncorrectPublicKeyLength) + { println!( "✅ tcId {}: {} — pubkey decode failed as expected", test.tc_id, test.comment, @@ -312,8 +316,10 @@ pub fn run_mldsa_wycheproof_sign_tests( test_name: mldsa_sign::TestName, deterministic: bool, ) { - let test_set = mldsa_sign::TestSet::load(test_name) - .unwrap_or_else(|e| panic!("Failed to load sign test set: {e}")); + 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; @@ -344,12 +350,24 @@ pub fn run_mldsa_wycheproof_sign_tests( 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; + 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 \ From 071694bfb0f4b4add7c1c4e5a5081939fe8cabd5 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Thu, 27 Nov 2025 14:21:43 +0200 Subject: [PATCH 19/42] feat(tests): run signing tests with seed-only keys --- src/adapters/pqclean/MLDSA44.rs | 11 ++++++++++- src/adapters/pqclean/MLDSA65.rs | 11 ++++++++++- src/adapters/pqclean/MLDSA87.rs | 11 ++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index d954581..bbb2bb7 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -486,7 +486,16 @@ mod tests { } #[test] - fn test_mldsa_44_sign_from_wycheproof() { + fn test_mldsa_44_sign_seed_from_wycheproof() { + 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() { run_mldsa_wycheproof_sign_tests::( mldsa_sign::TestName::MlDsa44SignNoSeed, // pqclean doesn't support deterministic ML-DSA diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index a7ebee0..f6db852 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -486,7 +486,16 @@ mod tests { } #[test] - fn test_mldsa_65_sign_from_wycheproof() { + fn test_mldsa_65_sign_seed_from_wycheproof() { + 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() { run_mldsa_wycheproof_sign_tests::( mldsa_sign::TestName::MlDsa65SignNoSeed, // pqclean doesn't support deterministic ML-DSA diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index c1c1852..c5cc5dc 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -486,7 +486,16 @@ mod tests { } #[test] - fn test_mldsa_87_sign_from_wycheproof() { + fn test_mldsa_87_sign_seed_from_wycheproof() { + 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() { run_mldsa_wycheproof_sign_tests::( mldsa_sign::TestName::MlDsa87SignNoSeed, // pqclean doesn't support deterministic ML-DSA From cdbf18c74be3521d3ba1a8a20d043d6daaff2c1d Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 12 Dec 2025 15:30:22 +0200 Subject: [PATCH 20/42] cleanup(tests): remove base64 dependency and hardcoded MLDSA44_Ed25519 test vectors --- Cargo.toml | 1 - .../keymgmt_functions_draft12.rs | 108 ------------------ 2 files changed, 109 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8216a56..2ede285 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,6 @@ 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" -base64 = "0.22.1" wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft" } [build-dependencies] diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index 88e024a..799b5f9 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -1119,112 +1119,4 @@ mod tests { assert_eq!(SECURITY_BITS, 128); } - - use base64::{engine::general_purpose, Engine}; - - /* - * The following test keys and signature are taken as an example from - * https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-sigs-12#name-test-vectors - */ - const PK: &str = "MGuttJkrCtubxkMPmdME1/Ni\ - luVggzbeawbjHTzExidEYs0gcfhC7jLCNY9CGbYrtcySXBjhb+58fzVMzYcmy99DNgxx\ - 6zJngRsxQOmK2JGXmTMR85vkLyZp+h9ZmaWkla8cb6errJ5JvIeZPGqLdufpqG4l24ry\ - WfY1zi0wazXj1BZJZWdEu3/HzeEYfLvGZyjEQulqGdoqkUZj6RTEAO/nJ+kaN/3nmozu\ - tKAX+1sQbO6oocDZ1sjTuE/48Exc0diHygPyMptD50feFCPUshYFYeS3Cmf2cWR88qre\ - 9wrpu/2nydzeLf3GXF7KO735Jw3M1R+flSKwsfGtEMdp4OVsfPEcXvAhQH40Hlq0Z6B2\ - 1CXNTvMSUccKzH8qDRtX8H+hVA4CI90NT7N+eOGSLp44+jP63R4vXoqwI6116udwcveU\ - IhmiasHB9Msap4TSMm8LpANiUZjwfe7pBkpVNuA8czKrb6/I7zDEkGotlHYOrCroNJ+G\ - j+l0xsLxrsQ/OJzkWPVH+ydOsvDebHlatyyrdUXdBu/VYHibmAHqr9q14wtwpEsGeZF6\ - xRviOvTSDXBDwoGLC9OOrLf5+w8669xfkGfLLMDsAYCpqjWsa4jmcKC7gYhVgWfQxfnI\ - FGlAtl1hXsJifBs8z+bwI7vbN5IuWyrxSvUN6Tnn3HqzwNGtT/6gOdCEqOZbReXdvIJN\ - CzIkqm+SrqCvVun+DdaHziGQdwpjHv3Baf9jXBBCw8azWnYmD9z/c5HbcjLnTQ4PkL6K\ - QWmXkSnTU1fOE1UjqoE8+yQckNC7eU/NdBxtri5yUtbF6l9vToywovcpjALVWr7eq7fF\ - nCJ2sjb9UPk4i5AmwGKScLCuUFx3etjgcWfwbcuwXe3ErlVMGXBNvkd+xUfACOyWepf4\ - VixlZDbfb2P9ILF0PYyHEdU1R5FM3uDAoPXUfVQ/zN+cf9jqhWcOB11LL63pade9IIkS\ - XN5AySHWiU3GqjTe81X5Ta5EITm7asCcAzwBYlACFweLO+qUe9RAz0FhKnsOGce0pA7e\ - w94+mbae14v24k0PHLw2/vvfskB0kopPJa3x+jC8TijPg1SDYyj4tyllmfjh+0zEuTkW\ - kT/C9co7m0Sd8N5xogLYFR4v6wxWpPBvGRi1iX3hybZvYClTXurBnvMGXzEtcowBCJrx\ - V3WZ3gG6sKx/m18VdMzma5IesD1R9+Vn9cLVupe73/XVLhF//CL109qLe1jgx2NRVQeG\ - ourqvJ6qDWM6Y4JFvAjto/Pzvj7AH4HTAvUEBNGY3X5ACUQoSMtJyMsQTeFsziV8JhrG\ - figjpZDUBRN3Rmn0ts/yROotqodTmG5CKXLZWyN6aDBpaQv+91jd8QoXf8Gq6JsXA2pN\ - 70+Z6Nu2Kx+qmr7WEOQ6rzOs30eqWPt9yKbR3OB1f9atj596OjW7x5nV73pwaW8IHrlv\ - UP26+pj5xcMFv8kNfPHaNbSZkxf7CBp+WoZVMdVemuewrzJPp5rsbOMjpiddC8RLt5f5\ - Yznq2xdDcqelZu/Rrq/j/LNuozsxtZNeWeOaHFd6PcsSY27+dvrph57qenI0kauvCn5H\ - tCIRaViT8fXmEg0YNsZPOdZD99EFiPtj94jpCXbZcoFXbXti6Ht/F69u7nU9XnaHvNMl\ - 8pV8R3U+VbU5FaHk/B8OJCld8mqFkTVMPVWpGqWDFgtmvzp2g+r7mEh4HzdnEOIOwVxa\ - 8lddM6IoDU2guXpcBNdCWekH3/0873n7lSbWaHrjslmAiew/tHUTCEvrIWIRYlq2H3c9"; - - // TODO: add a way to derive the expanded secret key from the seed so we can actually use this - const _SK: &str = "BPF\ - hmtxufqsWNGCyg9kQwRGp4Z8nlIWggybYyiYHd6vkiyBsVB9m3mhTHMmFlKGC8xfC0Nl\ - YL9LpGJu7G+Qiyw=="; - - const SIG: &str = "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\ - oN"; - - const MSG: &str = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4="; - - #[test] - fn test_import_pubkey_verify_signature() { - let input_sig_bytes = general_purpose::STANDARD.decode(SIG).unwrap(); - let sig = - Signature::try_from(input_sig_bytes.as_slice()).expect("Signature decoding failed"); - - let input_pk = general_purpose::STANDARD.decode(PK).unwrap(); - let pk = PublicKey::decode(input_pk.as_slice()).expect("Failure while decoding Public Key"); - - eprintln!("\n\nPublic Key: {pk:?}\n"); - - let msg = general_purpose::STANDARD.decode(MSG).unwrap(); - - pk.verify(&msg, &sig).expect("Verify failed"); - } } From cfe2a08cf571a49ab768cef28e42bfc9393cae46 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 12 Dec 2025 15:59:45 +0200 Subject: [PATCH 21/42] cleanup(tests): build wycheproof module in test mode only This fixes a bunch of "unused code" warnings. Also, the wycheproof crate should only be marked as a dev-dependency as we need it only for tests. Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964541da7ddcfef1aa3b7387b9eaaf217d5 --- Cargo.toml | 2 +- src/adapters/common.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2ede285..e3a7db9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,6 @@ 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" -wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft" } [build-dependencies] rasn-compiler = "0.11.0" @@ -101,3 +100,4 @@ regex = "1" tempfile = "3" serde_json = "1.0.140" paste = "1.0" +wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft" } diff --git a/src/adapters/common.rs b/src/adapters/common.rs index ca34f39..6eede55 100644 --- a/src/adapters/common.rs +++ b/src/adapters/common.rs @@ -7,4 +7,6 @@ pub mod keymgmt_functions; pub mod macros; pub mod transcoders; + +#[cfg(test)] pub mod wycheproof; From 3057df33492add1e5f3ed081e667172ac4ecc503 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Tue, 16 Dec 2025 09:38:23 +0200 Subject: [PATCH 22/42] feat(pqclean): fail gracefully on length error when decoding composite ML-DSA private keys This replaces commit 865da66e130867545a5587750e20708e0729b61f which was doing too much. Signed-off-by: Nicola Tuveri Change-Id: I6a6a69643a74cc33d89de5ef7c77427822a7a069 --- .../pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs | 4 +++- .../pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index 799b5f9..8089951 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -287,7 +287,9 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - let (pq_bytes, trad_bytes) = bytes.split_at(pq_backend_module::secret_key_bytes()); + let (pq_bytes, trad_bytes) = bytes + .split_at_checked(pq_backend_module::secret_key_bytes()) + .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; let pq_private_key = ::from_bytes( pq_bytes, diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index 0da0d90..f88bc64 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -287,7 +287,9 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - let (pq_bytes, trad_bytes) = bytes.split_at(pq_backend_module::secret_key_bytes()); + let (pq_bytes, trad_bytes) = bytes + .split_at_checked(pq_backend_module::secret_key_bytes()) + .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; let pq_private_key = ::from_bytes( pq_bytes, From 4b4fd433a411541d37931f99fa59451eecd438b9 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Tue, 16 Dec 2025 10:14:53 +0200 Subject: [PATCH 23/42] chore(Cargo.lock): update local Cargo.lock Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964369686d749a864b024a3cfa566cf3b60 --- Cargo.lock | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index faba6db..9576d39 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" @@ -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-draft#de6d7386cc74b1f83e48c00d6836f56530212812" +dependencies = [ + "data-encoding", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "wyz" version = "0.5.1" From 6c4998c1794e66240f589c499ce3c21ba29dcea1 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 13:59:14 +0200 Subject: [PATCH 24/42] chore(release): bump version to 0.10.0+dev Done in preparation of breaking changes to composite mldsa private key decoding Signed-off-by: Nicola Tuveri Change-Id: I6a6a696493118910816276b7497e5713308888bd --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9576d39..a74e3d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "qubip_aurora" -version = "0.9.1+dev" +version = "0.10.0+dev" dependencies = [ "anyhow", "asn1", diff --git a/Cargo.toml b/Cargo.toml index e3a7db9..6a6aec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qubip_aurora" -version = "0.9.1+dev" +version = "0.10.0+dev" edition = "2021" description = "A framework to build OpenSSL Providers tailored for the transition to post-quantum cryptography" license = "Apache-2.0" From 95eb580d3220c563fa9089cb1cc5d66775543fe1 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 1 Dec 2025 11:51:55 +0200 Subject: [PATCH 25/42] feat(pqclean): implement sign and verify with ctx for pure ML-DSA --- src/adapters/common/signature.rs | 20 ++++++ src/adapters/common/wycheproof.rs | 39 +++++++++- src/adapters/pqclean/MLDSA44.rs | 68 ++---------------- .../pqclean/MLDSA44/keymgmt_functions.rs | 36 +++++++++- src/adapters/pqclean/MLDSA65.rs | 72 ++----------------- .../pqclean/MLDSA65/keymgmt_functions.rs | 36 +++++++++- src/adapters/pqclean/MLDSA87.rs | 72 ++----------------- .../pqclean/MLDSA87/keymgmt_functions.rs | 36 +++++++++- 8 files changed, 177 insertions(+), 202 deletions(-) diff --git a/src/adapters/common/signature.rs b/src/adapters/common/signature.rs index 968764f..83c0175 100644 --- a/src/adapters/common/signature.rs +++ b/src/adapters/common/signature.rs @@ -81,3 +81,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/wycheproof.rs b/src/adapters/common/wycheproof.rs index 32bf7ae..f9e9d4c 100644 --- a/src/adapters/common/wycheproof.rs +++ b/src/adapters/common/wycheproof.rs @@ -25,9 +25,6 @@ pub trait SigAlgVerifyVariant { ) -> Result<(), signature::Error>; } -// currently unused; since none of our adapters implement verify_with_ctx, we have manual impls for -// the SigAlgVerifyVariant trait that return an error there instead of calling a method -#[macro_export] macro_rules! impl_sigalg_verify_variant { ($variant:ident, $pubkey:ty, $sig:ty) => { impl $crate::adapters::common::wycheproof::SigAlgVerifyVariant for $variant { @@ -61,6 +58,7 @@ macro_rules! impl_sigalg_verify_variant { } }; } +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 @@ -305,6 +303,41 @@ pub trait SigAlgSignVariant { 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 diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index bbb2bb7..35853be 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -408,82 +408,22 @@ pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPub mod tests { use super::*; use crate::adapters::common::wycheproof::*; - use signature::Verifier; + use signature::{Verifier, VerifierWithCtx}; use wycheproof::mldsa_verify; struct Mldsa44; - impl SigAlgVerifyVariant for Mldsa44 { - type PublicKey = keymgmt_functions::PublicKey; - - type Signature = signature::Signature; - - fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { - Self::PublicKey::decode(bytes) - } - - fn decode_signature(bytes: &[u8]) -> anyhow::Result { - Self::Signature::try_from(bytes) - } - - fn verify( - pubkey: &Self::PublicKey, - msg: &[u8], - sig: &Self::Signature, - ) -> Result<(), signature::Error> { - pubkey.verify(msg, sig) - } - - // This adapter does not support signatures with a non-empty ctx. - fn verify_with_ctx( - _pubkey: &Self::PublicKey, - _msg: &[u8], - _sig: &Self::Signature, - _ctx: &[u8], - ) -> Result<(), signature::Error> { - Err(signature::Error::new()) - } - } + impl_sigalg_verify_variant!(Mldsa44, keymgmt_functions::PublicKey, signature::Signature); #[test] fn test_mldsa_44_verify_from_wycheproof() { run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa44Verify); } - use signature::{SignatureBytes, SignatureEncoding, Signer}; + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; use wycheproof::mldsa_sign; - impl SigAlgSignVariant for Mldsa44 { - type PrivateKey = keymgmt_functions::PrivateKey; - - type Signature = signature::Signature; - - fn decode_privkey(bytes: &[u8]) -> anyhow::Result { - Self::PrivateKey::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 { - // this adapter doesn't implement signing with ctx yet - Err(signature::Error::new()) - } - - fn encode_signature(sig: &Self::Signature) -> Vec { - Vec::from(sig.to_bytes().as_ref()) - } - } + impl_sigalg_sign_variant!(Mldsa44, keymgmt_functions::PrivateKey, signature::Signature); #[test] fn test_mldsa_44_sign_seed_from_wycheproof() { diff --git a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs index c1b1563..a20c3ac 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(); @@ -133,6 +135,29 @@ impl Verifier for PublicKey { } } +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + let sig = sig.to_bytes(); + let sig = sig.as_ref(); + use pqcrypto_traits::sign::DetachedSignature; + let sig = backend_module::DetachedSignature::from_bytes(sig).map_err(|e| { + error!(target: log_target!(), "{e:?}"); + forge::crypto::signature::Error::from_source( + VerificationError::GenericVerificationError, + ) + })?; + backend_module::verify_detached_signature_ctx(&sig, msg, ctx, &self.0) + .map_err(map_into_VerificationError) + .map_err(forge::crypto::signature::Error::from_source) + } +} + #[named] fn map_into_VerificationError( value: pqcrypto_traits::sign::VerificationError, @@ -245,6 +270,15 @@ impl Signer for PrivateKey { } } +impl SignerWithCtx for PrivateKey { + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result { + let Self(ref sk) = self; + 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)) + } +} + #[expect(dead_code)] pub struct KeyPair<'a> { pub private: Option, diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index f6db852..4d6af24 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -408,82 +408,22 @@ pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPub mod tests { use super::*; use crate::adapters::common::wycheproof::*; - use signature::Verifier; - use wycheproof::mldsa_verify::TestName; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::mldsa_verify; struct Mldsa65; - impl SigAlgVerifyVariant for Mldsa65 { - type PublicKey = keymgmt_functions::PublicKey; - - type Signature = signature::Signature; - - fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { - Self::PublicKey::decode(bytes) - } - - fn decode_signature(bytes: &[u8]) -> anyhow::Result { - Self::Signature::try_from(bytes) - } - - fn verify( - pubkey: &Self::PublicKey, - msg: &[u8], - sig: &Self::Signature, - ) -> Result<(), signature::Error> { - pubkey.verify(msg, sig) - } - - // This adapter does not support signatures with a non-empty ctx. - fn verify_with_ctx( - _pubkey: &Self::PublicKey, - _msg: &[u8], - _sig: &Self::Signature, - _ctx: &[u8], - ) -> Result<(), signature::Error> { - Err(signature::Error::new()) - } - } + impl_sigalg_verify_variant!(Mldsa65, keymgmt_functions::PublicKey, signature::Signature); #[test] fn test_mldsa_65_verify_from_wycheproof() { - run_mldsa_wycheproof_verify_tests::(TestName::MlDsa65Verify); + run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa65Verify); } - use signature::{SignatureBytes, SignatureEncoding, Signer}; + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; use wycheproof::mldsa_sign; - impl SigAlgSignVariant for Mldsa65 { - type PrivateKey = keymgmt_functions::PrivateKey; - - type Signature = signature::Signature; - - fn decode_privkey(bytes: &[u8]) -> anyhow::Result { - Self::PrivateKey::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 { - // this adapter doesn't implement signing with ctx yet - Err(signature::Error::new()) - } - - fn encode_signature(sig: &Self::Signature) -> Vec { - Vec::from(sig.to_bytes().as_ref()) - } - } + impl_sigalg_sign_variant!(Mldsa65, keymgmt_functions::PrivateKey, signature::Signature); #[test] fn test_mldsa_65_sign_seed_from_wycheproof() { diff --git a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs index 6eb2d2b..0413d6b 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(); @@ -133,6 +135,29 @@ impl Verifier for PublicKey { } } +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + let sig = sig.to_bytes(); + let sig = sig.as_ref(); + use pqcrypto_traits::sign::DetachedSignature; + let sig = backend_module::DetachedSignature::from_bytes(sig).map_err(|e| { + error!(target: log_target!(), "{e:?}"); + forge::crypto::signature::Error::from_source( + VerificationError::GenericVerificationError, + ) + })?; + backend_module::verify_detached_signature_ctx(&sig, msg, ctx, &self.0) + .map_err(map_into_VerificationError) + .map_err(forge::crypto::signature::Error::from_source) + } +} + #[named] fn map_into_VerificationError( value: pqcrypto_traits::sign::VerificationError, @@ -245,6 +270,15 @@ impl Signer for PrivateKey { } } +impl SignerWithCtx for PrivateKey { + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result { + let Self(ref sk) = self; + 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)) + } +} + #[expect(dead_code)] pub struct KeyPair<'a> { pub private: Option, diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index c5cc5dc..00b372b 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -408,82 +408,22 @@ pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPub mod tests { use super::*; use crate::adapters::common::wycheproof::*; - use signature::Verifier; - use wycheproof::mldsa_verify::TestName; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::mldsa_verify; struct Mldsa87; - impl SigAlgVerifyVariant for Mldsa87 { - type PublicKey = keymgmt_functions::PublicKey; - - type Signature = signature::Signature; - - fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { - Self::PublicKey::decode(bytes) - } - - fn decode_signature(bytes: &[u8]) -> anyhow::Result { - Self::Signature::try_from(bytes) - } - - fn verify( - pubkey: &Self::PublicKey, - msg: &[u8], - sig: &Self::Signature, - ) -> Result<(), signature::Error> { - pubkey.verify(msg, sig) - } - - // This adapter does not support signatures with a non-empty ctx. - fn verify_with_ctx( - _pubkey: &Self::PublicKey, - _msg: &[u8], - _sig: &Self::Signature, - _ctx: &[u8], - ) -> Result<(), signature::Error> { - Err(signature::Error::new()) - } - } + impl_sigalg_verify_variant!(Mldsa87, keymgmt_functions::PublicKey, signature::Signature); #[test] fn test_mldsa_87_verify_from_wycheproof() { - run_mldsa_wycheproof_verify_tests::(TestName::MlDsa87Verify); + run_mldsa_wycheproof_verify_tests::(mldsa_verify::TestName::MlDsa87Verify); } - use signature::{SignatureBytes, SignatureEncoding, Signer}; + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; use wycheproof::mldsa_sign; - impl SigAlgSignVariant for Mldsa87 { - type PrivateKey = keymgmt_functions::PrivateKey; - - type Signature = signature::Signature; - - fn decode_privkey(bytes: &[u8]) -> anyhow::Result { - Self::PrivateKey::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 { - // this adapter doesn't implement signing with ctx yet - Err(signature::Error::new()) - } - - fn encode_signature(sig: &Self::Signature) -> Vec { - Vec::from(sig.to_bytes().as_ref()) - } - } + impl_sigalg_sign_variant!(Mldsa87, keymgmt_functions::PrivateKey, signature::Signature); #[test] fn test_mldsa_87_sign_seed_from_wycheproof() { diff --git a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs index 373a1cd..809d002 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(); @@ -133,6 +135,29 @@ impl Verifier for PublicKey { } } +impl VerifierWithCtx for PublicKey { + #[named] + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), signature::Error> { + let sig = sig.to_bytes(); + let sig = sig.as_ref(); + use pqcrypto_traits::sign::DetachedSignature; + let sig = backend_module::DetachedSignature::from_bytes(sig).map_err(|e| { + error!(target: log_target!(), "{e:?}"); + forge::crypto::signature::Error::from_source( + VerificationError::GenericVerificationError, + ) + })?; + backend_module::verify_detached_signature_ctx(&sig, msg, ctx, &self.0) + .map_err(map_into_VerificationError) + .map_err(forge::crypto::signature::Error::from_source) + } +} + #[named] fn map_into_VerificationError( value: pqcrypto_traits::sign::VerificationError, @@ -245,6 +270,15 @@ impl Signer for PrivateKey { } } +impl SignerWithCtx for PrivateKey { + fn try_sign_with_ctx(&self, msg: &[u8], ctx: &[u8]) -> Result { + let Self(ref sk) = self; + 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)) + } +} + #[expect(dead_code)] pub struct KeyPair<'a> { pub private: Option, From 9c506b74822f9eca5205285908d0ec0e5006a9a9 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Mon, 1 Dec 2025 13:10:59 +0200 Subject: [PATCH 26/42] feat(pqclean): implement sign and verify with ctx for composite ML-DSA Unfortunately there aren't any test vectors that actually use the optional ctx field. --- src/adapters/pqclean/MLDSA44_Ed25519.rs | 76 +++-------------- .../keymgmt_functions_draft12.rs | 53 ++++++++++-- src/adapters/pqclean/MLDSA65_Ed25519.rs | 82 ++++--------------- .../keymgmt_functions_draft12.rs | 53 ++++++++++-- 4 files changed, 120 insertions(+), 144 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index f43ac60..27984e3 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -415,43 +415,17 @@ pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPub mod tests { use super::*; use crate::adapters::common::wycheproof::*; - use signature::Verifier; + use signature::{Verifier, VerifierWithCtx}; use wycheproof::composite_mldsa_verify; #[allow(non_camel_case_types)] struct Mldsa44_Ed25519; - impl SigAlgVerifyVariant for Mldsa44_Ed25519 { - type PublicKey = keymgmt_functions::PublicKey; - - type Signature = signature::Signature; - - fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { - Self::PublicKey::decode(bytes) - } - - fn decode_signature(bytes: &[u8]) -> anyhow::Result { - Self::Signature::try_from(bytes) - } - - fn verify( - pubkey: &Self::PublicKey, - msg: &[u8], - sig: &Self::Signature, - ) -> Result<(), signature::Error> { - pubkey.verify(msg, sig) - } - - // This adapter does not support signatures with a non-empty ctx. - fn verify_with_ctx( - _pubkey: &Self::PublicKey, - _msg: &[u8], - _sig: &Self::Signature, - _ctx: &[u8], - ) -> Result<(), signature::Error> { - Err(signature::Error::new()) - } - } + impl_sigalg_verify_variant!( + Mldsa44_Ed25519, + keymgmt_functions::PublicKey, + signature::Signature + ); #[test] fn test_mldsa_44_ed_25519_verify_from_wycheproof() { @@ -460,40 +434,14 @@ mod tests { ); } - use signature::{SignatureBytes, SignatureEncoding, Signer}; + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; use wycheproof::composite_mldsa_sign; - impl SigAlgSignVariant for Mldsa44_Ed25519 { - type PrivateKey = keymgmt_functions::PrivateKey; - - type Signature = signature::Signature; - - fn decode_privkey(bytes: &[u8]) -> anyhow::Result { - Self::PrivateKey::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 { - // this adapter doesn't implement signing with ctx yet - Err(signature::Error::new()) - } - - fn encode_signature(sig: &Self::Signature) -> Vec { - Vec::from(sig.to_bytes().as_ref()) - } - } + impl_sigalg_sign_variant!( + Mldsa44_Ed25519, + keymgmt_functions::PrivateKey, + signature::Signature + ); #[test] fn test_mldsa_44_ed_25519_sign_from_wycheproof() { diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index 8089951..cce8e6f 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -32,7 +32,9 @@ 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(); @@ -183,8 +185,25 @@ const LABEL: &[u8] = "COMPSIG-MLDSA44-Ed25519-SHA512".as_bytes(); // 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 { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.verify_with_ctx(msg, signature, &[]) + } +} + +impl VerifierWithCtx for PublicKey { #[named] - fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), forge::crypto::signature::Error> { + // validate ctx length + let ctx_len: u8 = ctx + .len() + .try_into() + .map_err(forge::crypto::signature::Error::from_source)?; + // get at the public keys let Self { pq_public_key, @@ -207,12 +226,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 +243,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(), @@ -405,12 +427,29 @@ impl PrivateKey { // 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 { + fn try_sign_with_ctx( + &self, + msg: &[u8], + ctx: &[u8], + ) -> Result { + // validate ctx length + let ctx_len: u8 = ctx + .len() + .try_into() + .map_err(forge::crypto::signature::Error::from_source)?; + // 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 @@ -420,8 +459,8 @@ 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) + // 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); // sign with Ed25519 diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 0a2c805..91770ce 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -415,83 +415,33 @@ pub(super) use encoder_functions::SubjectPublicKeyInfo2PEM as ENCODER_SubjectPub mod tests { use super::*; use crate::adapters::common::wycheproof::*; - use signature::Verifier; - use wycheproof::composite_mldsa_verify::TestName; + use signature::{Verifier, VerifierWithCtx}; + use wycheproof::composite_mldsa_verify; #[allow(non_camel_case_types)] struct Mldsa65_Ed25519; - impl SigAlgVerifyVariant for Mldsa65_Ed25519 { - type PublicKey = keymgmt_functions::PublicKey; - - type Signature = signature::Signature; - - fn decode_pubkey(bytes: &[u8]) -> anyhow::Result { - Self::PublicKey::decode(bytes) - } - - fn decode_signature(bytes: &[u8]) -> anyhow::Result { - Self::Signature::try_from(bytes) - } - - fn verify( - pubkey: &Self::PublicKey, - msg: &[u8], - sig: &Self::Signature, - ) -> Result<(), signature::Error> { - pubkey.verify(msg, sig) - } - - // This adapter does not support signatures with a non-empty ctx. - fn verify_with_ctx( - _pubkey: &Self::PublicKey, - _msg: &[u8], - _sig: &Self::Signature, - _ctx: &[u8], - ) -> Result<(), signature::Error> { - Err(signature::Error::new()) - } - } + impl_sigalg_verify_variant!( + Mldsa65_Ed25519, + keymgmt_functions::PublicKey, + signature::Signature + ); #[test] fn test_mldsa_65_ed_25519_verify_from_wycheproof() { - run_composite_mldsa_wycheproof_verify_tests::(TestName::MlDsa65Ed25519); + run_composite_mldsa_wycheproof_verify_tests::( + composite_mldsa_verify::TestName::MlDsa65Ed25519, + ); } - use signature::{SignatureBytes, SignatureEncoding, Signer}; + use signature::{SignatureBytes, SignatureEncoding, Signer, SignerWithCtx}; use wycheproof::composite_mldsa_sign; - impl SigAlgSignVariant for Mldsa65_Ed25519 { - type PrivateKey = keymgmt_functions::PrivateKey; - - type Signature = signature::Signature; - - fn decode_privkey(bytes: &[u8]) -> anyhow::Result { - Self::PrivateKey::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 { - // this adapter doesn't implement signing with ctx yet - Err(signature::Error::new()) - } - - fn encode_signature(sig: &Self::Signature) -> Vec { - Vec::from(sig.to_bytes().as_ref()) - } - } + impl_sigalg_sign_variant!( + Mldsa65_Ed25519, + keymgmt_functions::PrivateKey, + signature::Signature + ); #[test] fn test_mldsa_65_ed_25519_sign_from_wycheproof() { diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index f88bc64..3176413 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -32,7 +32,9 @@ 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(); @@ -183,8 +185,25 @@ const LABEL: &[u8] = "COMPSIG-MLDSA65-Ed25519-SHA512".as_bytes(); // 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 { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.verify_with_ctx(msg, signature, &[]) + } +} + +impl VerifierWithCtx for PublicKey { #[named] - fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + fn verify_with_ctx( + &self, + msg: &[u8], + sig: &Signature, + ctx: &[u8], + ) -> Result<(), forge::crypto::signature::Error> { + // validate ctx length + let ctx_len: u8 = ctx + .len() + .try_into() + .map_err(forge::crypto::signature::Error::from_source)?; + // get at the public keys let Self { pq_public_key, @@ -207,12 +226,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 +243,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(), @@ -405,12 +427,29 @@ impl PrivateKey { // 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 { + fn try_sign_with_ctx( + &self, + msg: &[u8], + ctx: &[u8], + ) -> Result { + // validate ctx length + let ctx_len: u8 = ctx + .len() + .try_into() + .map_err(forge::crypto::signature::Error::from_source)?; + // 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 @@ -420,8 +459,8 @@ 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) + // 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); // sign with Ed25519 From 851a88c6721a0e065cacfe4d5f0cc37e839a12c2 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Tue, 16 Dec 2025 11:24:01 +0200 Subject: [PATCH 27/42] feat(pqclean): consistently implement Signer/Verifier as a wrapper for SignerWithCtx/VerifierWithCtx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ideally one would actually make a blanket implementation like: ``` impl Signer for T where T: SignerWithCtx { fn try_sign(&self, msg: &[u8]) -> Result { self.try_sign_with_ctx(msg, &[]) } } ``` Unfortunately this fails because of Rust’s orphan/coherence rules: you can’t write a blanket impl of a foreign trait (Signer) for an unconstrained type parameter (T), even if T is required to implement your local trait (SignerWithCtx). Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964a1cd35113c5955988044576a19e50a36 --- .../pqclean/MLDSA44/keymgmt_functions.rs | 20 ++++--------------- .../keymgmt_functions_draft12.rs | 12 ++++------- .../pqclean/MLDSA65/keymgmt_functions.rs | 19 +++--------------- .../keymgmt_functions_draft12.rs | 12 ++++------- .../pqclean/MLDSA87/keymgmt_functions.rs | 19 +++--------------- 5 files changed, 18 insertions(+), 64 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs index a20c3ac..dabdcae 100644 --- a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs @@ -120,18 +120,8 @@ impl PublicKey { impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - let sig = sig.to_bytes(); - let sig = sig.as_ref(); - use pqcrypto_traits::sign::DetachedSignature; - let sig = backend_module::DetachedSignature::from_bytes(sig).map_err(|e| { - error!(target: log_target!(), "{e:?}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - backend_module::verify_detached_signature(&sig, msg, &self.0) - .map_err(map_into_VerificationError) - .map_err(forge::crypto::signature::Error::from_source) + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) } } @@ -143,6 +133,7 @@ impl VerifierWithCtx for PublicKey { sig: &Signature, ctx: &[u8], ) -> Result<(), signature::Error> { + trace!(target: log_target!(), "Called"); let sig = sig.to_bytes(); let sig = sig.as_ref(); use pqcrypto_traits::sign::DetachedSignature; @@ -263,10 +254,7 @@ impl PrivateKey { impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { - let Self(ref sk) = self; - let signature = backend_module::detached_sign(msg, sk); - Signature::try_from(signature.as_bytes()) - .map_err(|e| forge::crypto::signature::Error::from_source(e)) + self.try_sign_with_ctx(msg, &[]) } } diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index cce8e6f..4cfe524 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -181,12 +181,11 @@ impl PublicKey { 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 { - fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { - self.verify_with_ctx(msg, signature, &[]) + #[named] + fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) } } @@ -422,9 +421,6 @@ 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, &[]) diff --git a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs index 0413d6b..a99b9b3 100644 --- a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs @@ -120,18 +120,8 @@ impl PublicKey { impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - let sig = sig.to_bytes(); - let sig = sig.as_ref(); - use pqcrypto_traits::sign::DetachedSignature; - let sig = backend_module::DetachedSignature::from_bytes(sig).map_err(|e| { - error!(target: log_target!(), "{e:?}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - backend_module::verify_detached_signature(&sig, msg, &self.0) - .map_err(map_into_VerificationError) - .map_err(forge::crypto::signature::Error::from_source) + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) } } @@ -263,10 +253,7 @@ impl PrivateKey { impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { - let Self(ref sk) = self; - let signature = backend_module::detached_sign(msg, sk); - Signature::try_from(signature.as_bytes()) - .map_err(|e| forge::crypto::signature::Error::from_source(e)) + self.try_sign_with_ctx(msg, &[]) } } diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index 3176413..f5b573d 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -181,12 +181,11 @@ impl PublicKey { 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 { - fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { - self.verify_with_ctx(msg, signature, &[]) + #[named] + fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) } } @@ -422,9 +421,6 @@ 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, &[]) diff --git a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs index 809d002..473f0ff 100644 --- a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs @@ -120,18 +120,8 @@ impl PublicKey { impl Verifier for PublicKey { #[named] fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), forge::crypto::signature::Error> { - let sig = sig.to_bytes(); - let sig = sig.as_ref(); - use pqcrypto_traits::sign::DetachedSignature; - let sig = backend_module::DetachedSignature::from_bytes(sig).map_err(|e| { - error!(target: log_target!(), "{e:?}"); - forge::crypto::signature::Error::from_source( - VerificationError::GenericVerificationError, - ) - })?; - backend_module::verify_detached_signature(&sig, msg, &self.0) - .map_err(map_into_VerificationError) - .map_err(forge::crypto::signature::Error::from_source) + trace!(target: log_target!(), "Called"); + self.verify_with_ctx(msg, sig, &[]) } } @@ -263,10 +253,7 @@ impl PrivateKey { impl Signer for PrivateKey { fn try_sign(&self, msg: &[u8]) -> Result { - let Self(ref sk) = self; - let signature = backend_module::detached_sign(msg, sk); - Signature::try_from(signature.as_bytes()) - .map_err(|e| forge::crypto::signature::Error::from_source(e)) + self.try_sign_with_ctx(msg, &[]) } } From 056ffac587397e6dc08b8b238b15ee29578fb499 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Tue, 16 Dec 2025 11:38:25 +0200 Subject: [PATCH 28/42] cleanup(pqclean/composites): remove all legacy draft07 stuff Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964af0749dc6f3a74cfdec39a03fee0de91 --- Cargo.toml | 1 - src/adapters/pqclean/MLDSA44_Ed25519.rs | 22 - .../keymgmt_functions_draft07.rs | 1134 ----------------- src/adapters/pqclean/MLDSA65_Ed25519.rs | 22 - .../keymgmt_functions_draft07.rs | 1134 ----------------- 5 files changed, 2313 deletions(-) delete mode 100644 src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft07.rs delete mode 100644 src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft07.rs diff --git a/Cargo.toml b/Cargo.toml index 6a6aec8..dad947e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ 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 = [] diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 27984e3..246234b 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -43,9 +43,6 @@ 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; @@ -62,25 +59,6 @@ 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; 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/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 91770ce..7b6f42b 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -43,9 +43,6 @@ 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; @@ -62,25 +59,6 @@ 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; 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); - } -} From 685fdd902e466070d4d5dcdc241c16bf2abc6436 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Wed, 3 Dec 2025 10:56:57 +0200 Subject: [PATCH 29/42] feat(pqclean): derive private key from seed using rustcrypto-based helper All of the Wycheproof tests for signing with a seed-only key now pass. However, none of the encoder/decoder code has been updated yet, and the pqclean adapter still has no ability to store the seed; this commit only adds a step during the initial "raw bytes to PrivateKey object" conversion that derives the expanded key from seed if the bytes are a seed (previously it required that the key already be in expanded form). Signed-off-by: Nicola Tuveri Change-Id: I6a6a696454b47fbed251e59c4810364d7d3572ef --- .../pqclean/MLDSA44/keymgmt_functions.rs | 11 +++----- .../keymgmt_functions_draft12.rs | 11 +------- .../pqclean/MLDSA65/keymgmt_functions.rs | 11 +++----- .../keymgmt_functions_draft12.rs | 12 ++------- .../pqclean/MLDSA87/keymgmt_functions.rs | 11 +++----- src/adapters/pqclean/helpers.rs | 27 +++++++++++++++++++ 6 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs index dabdcae..65eef3a 100644 --- a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs @@ -174,14 +174,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_secret_key(bytes) + .map(Self) + .ok_or(anyhow!("Unable to decode private key")) } pub const fn byte_len() -> usize { diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index 4cfe524..630e811 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -311,16 +311,7 @@ impl PrivateKey { let (pq_bytes, trad_bytes) = bytes .split_at_checked(pq_backend_module::secret_key_bytes()) .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; - let pq_private_key = - ::from_bytes( - pq_bytes, - ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA44) returned {:?}", - e - ) - })?; + 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"))?; diff --git a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs index a99b9b3..aa691ce 100644 --- a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs @@ -173,14 +173,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_secret_key(bytes) + .map(Self) + .ok_or(anyhow!("Unable to decode private key")) } pub const fn byte_len() -> usize { diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index f5b573d..7b161d7 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -311,16 +311,8 @@ impl PrivateKey { let (pq_bytes, trad_bytes) = bytes .split_at_checked(pq_backend_module::secret_key_bytes()) .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; - let pq_private_key = - ::from_bytes( - pq_bytes, - ) - .map_err(|e| { - anyhow!( - "pqcrypto_traits::sign::SecretKey::from_bytes (MLDSA65) returned {:?}", - e - ) - })?; + let pq_private_key = super::helpers::derive_secret_key_from_seed(pq_bytes) + .ok_or(anyhow!("Unable to decode ML-DSA-65 private key seed"))?; let trad_private_key = trad_bytes .try_into() .map_err(|_| anyhow!("Ed25519 secret key should be 32 bytes"))?; diff --git a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs index 473f0ff..08c7783 100644 --- a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs @@ -173,14 +173,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_secret_key(bytes) + .map(Self) + .ok_or(anyhow!("Unable to decode private key")) } pub const fn byte_len() -> usize { diff --git a/src/adapters/pqclean/helpers.rs b/src/adapters/pqclean/helpers.rs index 5e68a14..834e43e 100644 --- a/src/adapters/pqclean/helpers.rs +++ b/src/adapters/pqclean/helpers.rs @@ -55,3 +55,30 @@ where } } } + +/// Derive the expanded secret key from a seed +#[named] +pub(super) fn derive_secret_key_from_seed(seed: &[u8]) -> Option +where + T: SupportedSecretKey, +{ + let seed: &[u8; 32] = seed.try_into().ok()?; + 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; + } + } +} + +/// Decode the bytes as a secret key, deriving from seed if necessary +pub(super) fn decode_secret_key(bytes: &[u8]) -> Option +where + T: SupportedSecretKey, +{ + derive_secret_key_from_seed(bytes).or_else(|| T::from_bytes(bytes).ok()) +} From b7c47f839b462537bc0cad4646bd58b98178e831 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Tue, 9 Dec 2025 15:17:22 +0200 Subject: [PATCH 30/42] refactor(pqclean): rename SupportedSecretKey trait to SupportedMlDsaSecretKey --- src/adapters/pqclean/helpers.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/adapters/pqclean/helpers.rs b/src/adapters/pqclean/helpers.rs index 834e43e..2f12a7e 100644 --- a/src/adapters/pqclean/helpers.rs +++ b/src/adapters/pqclean/helpers.rs @@ -3,23 +3,23 @@ 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) 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; } @@ -27,8 +27,8 @@ impl SupportedSecretKey for pqcrypto_mldsa::mldsa87::SecretKey { #[named] pub(super) fn derive_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 +38,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 { @@ -60,10 +60,11 @@ where #[named] pub(super) fn derive_secret_key_from_seed(seed: &[u8]) -> Option where - T: SupportedSecretKey, + T: SupportedMlDsaSecretKey, { let seed: &[u8; 32] = seed.try_into().ok()?; - let foreign_key = >::from_seed(seed.into()); + let foreign_key = + >::from_seed(seed.into()); let key_bytes = foreign_key.encode(); let res = ::from_bytes(&key_bytes); match res { @@ -78,7 +79,7 @@ where /// Decode the bytes as a secret key, deriving from seed if necessary pub(super) fn decode_secret_key(bytes: &[u8]) -> Option where - T: SupportedSecretKey, + T: SupportedMlDsaSecretKey, { derive_secret_key_from_seed(bytes).or_else(|| T::from_bytes(bytes).ok()) } From 873b5109ba19b5627e3756b70651d437fcdbe8c3 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 12 Dec 2025 12:09:48 +0200 Subject: [PATCH 31/42] refactor(pqclean): define ML-DSA seed type alias and enforce at callsite Signed-off-by: Nicola Tuveri Change-Id: I6a6a69642a7106c52069a795c8eb8766bdba8197 --- src/adapters/pqclean/helpers.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/adapters/pqclean/helpers.rs b/src/adapters/pqclean/helpers.rs index 2f12a7e..44ecaad 100644 --- a/src/adapters/pqclean/helpers.rs +++ b/src/adapters/pqclean/helpers.rs @@ -5,6 +5,10 @@ use function_name::named; /// RustCrypto/signatures/ml-dsa to work around this use ml_dsa as foreign_mldsa_module; +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; @@ -58,11 +62,10 @@ where /// Derive the expanded secret key from a seed #[named] -pub(super) fn derive_secret_key_from_seed(seed: &[u8]) -> Option +pub(super) fn derive_secret_key_from_seed(seed: &MlDsaSeed) -> Option where T: SupportedMlDsaSecretKey, { - let seed: &[u8; 32] = seed.try_into().ok()?; let foreign_key = >::from_seed(seed.into()); let key_bytes = foreign_key.encode(); @@ -81,5 +84,8 @@ pub(super) fn decode_secret_key(bytes: &[u8]) -> Option where T: SupportedMlDsaSecretKey, { - derive_secret_key_from_seed(bytes).or_else(|| T::from_bytes(bytes).ok()) + match TryInto::<&MlDsaSeed>::try_into(bytes) { + Ok(seed) => derive_secret_key_from_seed(seed), + Err(_) => T::from_bytes(bytes).ok(), + } } From 2352facecf12e9c4b8bba57bc745d170cb46914f Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 12 Dec 2025 13:07:29 +0200 Subject: [PATCH 32/42] feat(pqclean): switch to seed-only format for MLDSA44_Ed25519 - The type `PQPrivateKey` is now a struct that contains both the seed and the expanded form of the private key, instead of a type alias to the backend module's expanded-only key type. - `PrivateKey::{decode,encode}` expect the key's encoded format to be a 32-byte ML-DSA seed followed by a 32-byte Ed25519 key. - `helpers::derive_secret_key_from_seed` is now `pub(super)`, so we can use it here without changing the more permissive (seed-only and expanded format both accepted) logic of the `helpers::decode_secret_key` function used by the pure ML-DSA algorithms in this adapter. - The `to_DER` and `from_DER` methods on `PrivateKey` use `ASNPrivateKey::seed` instead of `ASNPrivateKey::expandedKey`. I don't think these methods were implemented correctly to begin with -- they use the pure ML-DSA key type from the `asn_definitions` module to wrap up bytes that represent a composite key -- hence why I bound the bytes to the name `bytes` instead of `seed` in the pattern-match, but this seemed to be the most sensible way to change the existing code for now to accompany the rest of this commit. Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964338980eb891819e5cabf8e9c6ea1b642 --- .../keymgmt_functions_draft12.rs | 73 +++++++++++++------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs index 630e811..1a12f37 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs @@ -25,7 +25,14 @@ 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; @@ -287,8 +294,19 @@ fn map_PQError_into_VerificationError( } } +impl PQPrivateKey { + pub fn new(seed: &MlDsaSeed) -> Result { + helpers::derive_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; @@ -298,18 +316,20 @@ 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 { + if bytes.len() != Self::byte_len() { + anyhow::bail!( + "Cannot decode MLDSA44-Ed25519 private key of length {}", + bytes.len() + ) + } let (pq_bytes, trad_bytes) = bytes - .split_at_checked(pq_backend_module::secret_key_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 @@ -330,7 +350,7 @@ impl PrivateKey { } fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key) + super::helpers::derive_public_key(&self.pq_private_key.expanded) } /// Derive a matching public key from this private key @@ -372,12 +392,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 => { @@ -389,6 +408,7 @@ impl PrivateKey { }; (privkey, Some(pubkey)) } + ASNPrivateKey::expandedKey(_expandedKey) => unimplemented!(), ASNPrivateKey::both(_private_key_both) => unimplemented!(), }; Ok((privkey, opt_pubkey)) @@ -400,7 +420,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) => { @@ -448,7 +468,8 @@ impl SignerWithCtx for PrivateKey { // sign with ML-DSA // 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); + let pq_signature = + pq_backend_module::detached_sign_ctx(&M_prime, LABEL, &pq_private_key.expanded); // sign with Ed25519 let trad_signature = @@ -524,12 +545,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_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(); @@ -1139,10 +1168,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, 1344); - assert_eq!(SECRETKEY_LEN, 2592); + assert_eq!(SECRETKEY_LEN, 64); assert_eq!(SIGNATURE_LEN, 2484); assert_eq!(SECURITY_BITS, 128); From 19ca72ba4676a66c8af67e25e52a5597701a0c81 Mon Sep 17 00:00:00 2001 From: Alex Shaindlin Date: Fri, 12 Dec 2025 14:45:10 +0200 Subject: [PATCH 33/42] feat(pqclean): switch to seed-only format for MLDSA65_Ed25519 Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964ef6ceaf1fb03a2244f084cf915c16dec --- .../keymgmt_functions_draft12.rs | 76 +++++++++++++------ 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs index 7b161d7..7d1be58 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs @@ -25,7 +25,14 @@ 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; @@ -287,8 +294,19 @@ fn map_PQError_into_VerificationError( } } +impl PQPrivateKey { + pub fn new(seed: &MlDsaSeed) -> Result { + helpers::derive_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; @@ -298,21 +316,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 { + if bytes.len() != Self::byte_len() { + anyhow::bail!( + "Cannot decode MLDSA65-Ed25519 private key of length {}", + bytes.len() + ) + } let (pq_bytes, trad_bytes) = bytes - .split_at_checked(pq_backend_module::secret_key_bytes()) + .split_at_checked(Self::PQ_PRIVATE_KEY_LEN) .ok_or_else(|| anyhow!("Unexpected lenght on decode"))?; - let pq_private_key = super::helpers::derive_secret_key_from_seed(pq_bytes) - .ok_or(anyhow!("Unable to decode ML-DSA-65 private key seed"))?; + 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"))?; @@ -331,7 +350,7 @@ impl PrivateKey { } fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key) + super::helpers::derive_public_key(&self.pq_private_key.expanded) } /// Derive a matching public key from this private key @@ -373,12 +392,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 => { @@ -390,6 +408,7 @@ impl PrivateKey { }; (privkey, Some(pubkey)) } + ASNPrivateKey::expandedKey(_expandedKey) => unimplemented!(), ASNPrivateKey::both(_private_key_both) => unimplemented!(), }; Ok((privkey, opt_pubkey)) @@ -401,7 +420,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) => { @@ -449,7 +468,8 @@ impl SignerWithCtx for PrivateKey { // sign with ML-DSA // 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); + let pq_signature = + pq_backend_module::detached_sign_ctx(&M_prime, LABEL, &pq_private_key.expanded); // sign with Ed25519 let trad_signature = @@ -525,12 +545,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_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(); @@ -1140,10 +1168,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); From b647a0dc4240e64c82bbb57440cfd16c9a493079 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 09:19:08 +0200 Subject: [PATCH 34/42] chore(Cargo.toml): revert to wycheproof-rs revision without the temporary extension for expanded private keys Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964171d33da0d9bc01cde0f67602ae9ec0d --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a74e3d2..2f5ff93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2048,7 +2048,7 @@ dependencies = [ [[package]] name = "wycheproof" version = "0.6.0" -source = "git+https://github.com/eferollo/wycheproof-rs?branch=composite-mldsa-draft#de6d7386cc74b1f83e48c00d6836f56530212812" +source = "git+https://github.com/eferollo/wycheproof-rs?branch=composite-mldsa-draft13#c501af1648742658b0c8cd0dec990dd5121422e6" dependencies = [ "data-encoding", "serde", diff --git a/Cargo.toml b/Cargo.toml index dad947e..ec7694a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,4 +99,4 @@ regex = "1" tempfile = "3" serde_json = "1.0.140" paste = "1.0" -wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft" } +wycheproof = { git = "https://github.com/eferollo/wycheproof-rs", branch = "composite-mldsa-draft13" } From 6f9c8fc04b545d8eee2d92a871bfe5b36f4c5ea0 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 09:31:14 +0200 Subject: [PATCH 35/42] chore(doc,pqclean): refer to pq-composite-sigs-13 everywhere Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964159a65a1c86278fb4395b4fe1b8de68a --- Cargo.toml | 5 +-- README.md | 10 ++--- src/adapters/pqclean/MLDSA44_Ed25519.rs | 39 +++++-------------- ...raft12.rs => keymgmt_functions_draft13.rs} | 4 +- src/adapters/pqclean/MLDSA65_Ed25519.rs | 39 +++++-------------- ...raft12.rs => keymgmt_functions_draft13.rs} | 2 +- 6 files changed, 28 insertions(+), 71 deletions(-) rename src/adapters/pqclean/MLDSA44_Ed25519/{keymgmt_functions_draft12.rs => keymgmt_functions_draft13.rs} (99%) rename src/adapters/pqclean/MLDSA65_Ed25519/{keymgmt_functions_draft12.rs => keymgmt_functions_draft13.rs} (99%) diff --git a/Cargo.toml b/Cargo.toml index ec7694a..82302c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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,8 +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_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 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/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 246234b..66a865f 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -43,11 +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_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"] @@ -59,42 +56,23 @@ mod signature_functions; pub(crate) type OurError = anyhow::Error; pub(crate) use anyhow::anyhow; -#[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"; @@ -187,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; diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs similarity index 99% rename from src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs rename to src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs index 1a12f37..cb97016 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs @@ -184,7 +184,7 @@ 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(); @@ -1167,7 +1167,7 @@ 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 + // 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, 64); assert_eq!(SIGNATURE_LEN, 2484); diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index 7b6f42b..dd0ac76 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -43,11 +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_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"] @@ -59,42 +56,23 @@ mod signature_functions; pub(crate) type OurError = anyhow::Error; pub(crate) use anyhow::anyhow; -#[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"; @@ -187,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; diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs similarity index 99% rename from src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs rename to src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs index 7d1be58..64236f8 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft12.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs @@ -184,7 +184,7 @@ 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(); From 94f141056a3ee00870343836b0112e21468f8df0 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 12:31:07 +0200 Subject: [PATCH 36/42] fix(tests): remember to initialize crate::tests::common::setup for wycheproof tests Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964ea550087043150c7ad272187baa0ca0e --- src/adapters/pqclean/MLDSA44.rs | 3 +++ src/adapters/pqclean/MLDSA44_Ed25519.rs | 2 ++ src/adapters/pqclean/MLDSA65.rs | 3 +++ src/adapters/pqclean/MLDSA65_Ed25519.rs | 2 ++ src/adapters/pqclean/MLDSA87.rs | 3 +++ 5 files changed, 13 insertions(+) diff --git a/src/adapters/pqclean/MLDSA44.rs b/src/adapters/pqclean/MLDSA44.rs index 35853be..4e8ed96 100644 --- a/src/adapters/pqclean/MLDSA44.rs +++ b/src/adapters/pqclean/MLDSA44.rs @@ -417,6 +417,7 @@ mod tests { #[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); } @@ -427,6 +428,7 @@ mod tests { #[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 @@ -436,6 +438,7 @@ mod tests { #[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 diff --git a/src/adapters/pqclean/MLDSA44_Ed25519.rs b/src/adapters/pqclean/MLDSA44_Ed25519.rs index 66a865f..453acca 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519.rs @@ -386,6 +386,7 @@ mod tests { #[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, ); @@ -402,6 +403,7 @@ mod tests { #[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 diff --git a/src/adapters/pqclean/MLDSA65.rs b/src/adapters/pqclean/MLDSA65.rs index 4d6af24..21ae5ca 100644 --- a/src/adapters/pqclean/MLDSA65.rs +++ b/src/adapters/pqclean/MLDSA65.rs @@ -417,6 +417,7 @@ mod tests { #[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); } @@ -427,6 +428,7 @@ mod tests { #[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 @@ -436,6 +438,7 @@ mod tests { #[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 diff --git a/src/adapters/pqclean/MLDSA65_Ed25519.rs b/src/adapters/pqclean/MLDSA65_Ed25519.rs index dd0ac76..5f3183a 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519.rs @@ -386,6 +386,7 @@ mod tests { #[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, ); @@ -402,6 +403,7 @@ mod tests { #[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 diff --git a/src/adapters/pqclean/MLDSA87.rs b/src/adapters/pqclean/MLDSA87.rs index 00b372b..988dbb1 100644 --- a/src/adapters/pqclean/MLDSA87.rs +++ b/src/adapters/pqclean/MLDSA87.rs @@ -417,6 +417,7 @@ mod tests { #[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); } @@ -427,6 +428,7 @@ mod tests { #[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 @@ -436,6 +438,7 @@ mod tests { #[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 From 2dd4c853a56796eed630c73fe2a97a98f3ba9783 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 12:47:33 +0200 Subject: [PATCH 37/42] chore(common/signature): improve error message on expected signature length mismatch Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964e69e555d3e9bcbc25ff23a1e187d866c --- src/adapters/common/signature.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/adapters/common/signature.rs b/src/adapters/common/signature.rs index 83c0175..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() From ca5da0c0950e4dbdc71c85a684b170c5ef8a193b Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 12:47:33 +0200 Subject: [PATCH 38/42] fix(pqclean): validate ctx length before calling the backend Amend `try_sign_with_ctx` and `verify_with_ctx` to check the input ctx length before calling the backend. Signed-off-by: Nicola Tuveri Change-Id: I6a6a6964725f30821054fa2671f606eb1429c47a --- .../pqclean/MLDSA44/keymgmt_functions.rs | 21 ++++++++++++++ .../keymgmt_functions_draft13.rs | 29 ++++++++++++++----- .../pqclean/MLDSA65/keymgmt_functions.rs | 22 ++++++++++++++ .../keymgmt_functions_draft13.rs | 29 ++++++++++++++----- .../pqclean/MLDSA87/keymgmt_functions.rs | 22 ++++++++++++++ 5 files changed, 107 insertions(+), 16 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs index 65eef3a..8f2ed6e 100644 --- a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs @@ -134,6 +134,15 @@ impl VerifierWithCtx for PublicKey { 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; @@ -254,8 +263,20 @@ impl Signer for PrivateKey { } 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; + + // 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/keymgmt_functions_draft13.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs index cb97016..996e13b 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs @@ -204,11 +204,17 @@ impl VerifierWithCtx for PublicKey { sig: &Signature, ctx: &[u8], ) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + // validate ctx length - let ctx_len: u8 = ctx - .len() - .try_into() - .map_err(forge::crypto::signature::Error::from_source)?; + // + // 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 { @@ -439,16 +445,23 @@ impl Signer for PrivateKey { } impl SignerWithCtx for PrivateKey { + #[named] fn try_sign_with_ctx( &self, msg: &[u8], ctx: &[u8], ) -> Result { + trace!(target: log_target!(), "Called"); + // validate ctx length - let ctx_len: u8 = ctx - .len() - .try_into() - .map_err(forge::crypto::signature::Error::from_source)?; + // + // 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) diff --git a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs index aa691ce..a979c62 100644 --- a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs @@ -133,6 +133,16 @@ impl VerifierWithCtx for PublicKey { 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; @@ -253,8 +263,20 @@ impl Signer for PrivateKey { } 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; + + // 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/keymgmt_functions_draft13.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs index 64236f8..d6f5ae8 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs @@ -204,11 +204,17 @@ impl VerifierWithCtx for PublicKey { sig: &Signature, ctx: &[u8], ) -> Result<(), forge::crypto::signature::Error> { + trace!(target: log_target!(), "Called"); + // validate ctx length - let ctx_len: u8 = ctx - .len() - .try_into() - .map_err(forge::crypto::signature::Error::from_source)?; + // + // 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 { @@ -439,16 +445,23 @@ impl Signer for PrivateKey { } impl SignerWithCtx for PrivateKey { + #[named] fn try_sign_with_ctx( &self, msg: &[u8], ctx: &[u8], ) -> Result { + trace!(target: log_target!(), "Called"); + // validate ctx length - let ctx_len: u8 = ctx - .len() - .try_into() - .map_err(forge::crypto::signature::Error::from_source)?; + // + // 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) diff --git a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs index 08c7783..ffba5a8 100644 --- a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs @@ -133,6 +133,16 @@ impl VerifierWithCtx for PublicKey { 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; @@ -253,8 +263,20 @@ impl Signer for PrivateKey { } 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; + + // 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)) From 510120cd2498686007d8496de2820854124fd0da Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 19:31:46 +0200 Subject: [PATCH 39/42] feat(pqclean): validate decoding of private keys through foreign module Currently PQClean is too lenient in parsing private keys. We can use a more strict-on-decode foreign module to try and correctly decode the input, before asking PQClean to decode. This is controlled at build time via `VALIDATE_PRIVKEY_DECODING_VIA_FOREIGN_MODULE`, defined as a const in src/adapters/pqclean/helpers.rs. Currently it defaults to true. Signed-off-by: Nicola Tuveri Change-Id: I6a6a69649e6496591f506313a77d902220dae0bc --- src/adapters/pqclean/helpers.rs | 83 ++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/adapters/pqclean/helpers.rs b/src/adapters/pqclean/helpers.rs index 44ecaad..ad11739 100644 --- a/src/adapters/pqclean/helpers.rs +++ b/src/adapters/pqclean/helpers.rs @@ -79,13 +79,92 @@ where } } +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_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_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) => derive_secret_key_from_seed(seed), - Err(_) => T::from_bytes(bytes).ok(), + Ok(seed) => { + return derive_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_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() } From 4aa37b8f30c05af21c2bbc6b08a3b011139aea58 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 21:12:28 +0200 Subject: [PATCH 40/42] cleanup(pqclean): rename helpers to make explicit they operate on mldsa Signed-off-by: Nicola Tuveri Change-Id: I6a6a69642e56d44e528bfd0dd86be82b7ee9e6c3 --- src/adapters/pqclean/MLDSA44/keymgmt_functions.rs | 4 ++-- .../MLDSA44_Ed25519/keymgmt_functions_draft13.rs | 10 +++++----- src/adapters/pqclean/MLDSA65/keymgmt_functions.rs | 4 ++-- .../MLDSA65_Ed25519/keymgmt_functions_draft13.rs | 10 +++++----- src/adapters/pqclean/MLDSA87/keymgmt_functions.rs | 4 ++-- src/adapters/pqclean/helpers.rs | 12 ++++++------ 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs index 8f2ed6e..3e83db2 100644 --- a/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA44/keymgmt_functions.rs @@ -183,7 +183,7 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - super::helpers::decode_secret_key(bytes) + super::helpers::decode_mldsa_secret_key(bytes) .map(Self) .ok_or(anyhow!("Unable to decode private key")) } @@ -198,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)) } diff --git a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs index 996e13b..7c07601 100644 --- a/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs +++ b/src/adapters/pqclean/MLDSA44_Ed25519/keymgmt_functions_draft13.rs @@ -302,7 +302,7 @@ fn map_PQError_into_VerificationError( impl PQPrivateKey { pub fn new(seed: &MlDsaSeed) -> Result { - helpers::derive_secret_key_from_seed(seed) + helpers::derive_mldsa_secret_key_from_seed(seed) .map(|k| Self { seed: *seed, expanded: k, @@ -356,7 +356,7 @@ impl PrivateKey { } fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key.expanded) + super::helpers::derive_mldsa_public_key(&self.pq_private_key.expanded) } /// Derive a matching public key from this private key @@ -567,9 +567,9 @@ impl<'a> KeyPair<'a> { }; // derive PQ public key from it - let pq_public_key = helpers::derive_public_key(&pq_private_key.expanded).ok_or(anyhow!( - "Unable to derive public ML-DSA key from private key" - ))?; + 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"), + )?; // generate traditional keypair let trad_keypair = trad_backend_module::SigningKey::generate(provctx.get_rng()); diff --git a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs index a979c62..842b499 100644 --- a/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA65/keymgmt_functions.rs @@ -183,7 +183,7 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - super::helpers::decode_secret_key(bytes) + super::helpers::decode_mldsa_secret_key(bytes) .map(Self) .ok_or(anyhow!("Unable to decode private key")) } @@ -198,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)) } diff --git a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs index d6f5ae8..40ae12d 100644 --- a/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs +++ b/src/adapters/pqclean/MLDSA65_Ed25519/keymgmt_functions_draft13.rs @@ -302,7 +302,7 @@ fn map_PQError_into_VerificationError( impl PQPrivateKey { pub fn new(seed: &MlDsaSeed) -> Result { - helpers::derive_secret_key_from_seed(seed) + helpers::derive_mldsa_secret_key_from_seed(seed) .map(|k| Self { seed: *seed, expanded: k, @@ -356,7 +356,7 @@ impl PrivateKey { } fn derive_PQ_public_key(&self) -> Option { - super::helpers::derive_public_key(&self.pq_private_key.expanded) + super::helpers::derive_mldsa_public_key(&self.pq_private_key.expanded) } /// Derive a matching public key from this private key @@ -567,9 +567,9 @@ impl<'a> KeyPair<'a> { }; // derive PQ public key from it - let pq_public_key = helpers::derive_public_key(&pq_private_key.expanded).ok_or(anyhow!( - "Unable to derive public ML-DSA key from private key" - ))?; + 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"), + )?; // generate traditional keypair let trad_keypair = trad_backend_module::SigningKey::generate(provctx.get_rng()); diff --git a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs index ffba5a8..396c1c8 100644 --- a/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs +++ b/src/adapters/pqclean/MLDSA87/keymgmt_functions.rs @@ -183,7 +183,7 @@ impl PrivateKey { } pub fn decode(bytes: &[u8]) -> Result { - super::helpers::decode_secret_key(bytes) + super::helpers::decode_mldsa_secret_key(bytes) .map(Self) .ok_or(anyhow!("Unable to decode private key")) } @@ -198,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)) } diff --git a/src/adapters/pqclean/helpers.rs b/src/adapters/pqclean/helpers.rs index ad11739..828fdb0 100644 --- a/src/adapters/pqclean/helpers.rs +++ b/src/adapters/pqclean/helpers.rs @@ -29,7 +29,7 @@ impl SupportedMlDsaSecretKey for pqcrypto_mldsa::mldsa87::SecretKey { /// 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: SupportedMlDsaSecretKey, ::PublicKey: pqcrypto_traits::sign::PublicKey, @@ -62,7 +62,7 @@ where /// Derive the expanded secret key from a seed #[named] -pub(super) fn derive_secret_key_from_seed(seed: &MlDsaSeed) -> Option +pub(super) fn derive_mldsa_secret_key_from_seed(seed: &MlDsaSeed) -> Option where T: SupportedMlDsaSecretKey, { @@ -83,7 +83,7 @@ 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_secret_key( +fn foreign_decode_mldsa_secret_key( bytes: &[u8], ) -> std::thread::Result< foreign_mldsa_module::SigningKey<::ForeignParamSet>, @@ -126,14 +126,14 @@ where /// Decode the bytes as a secret key, deriving from seed if necessary #[named] -pub(super) fn decode_secret_key(bytes: &[u8]) -> Option +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_secret_key_from_seed(seed); + return derive_mldsa_secret_key_from_seed(seed); } Err(_) => (), } @@ -145,7 +145,7 @@ where // 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_secret_key::(bytes); + let foreign_result = foreign_decode_mldsa_secret_key::(bytes); match foreign_result { Ok(_) => (), // we discard the foreign module object From fa17d54a1b30b464cd6b4f361d448c4e3c1a4d7f Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 21:31:43 +0200 Subject: [PATCH 41/42] chore(CHANGELOG): Update CHANGELOG for v0.10.0 Signed-off-by: Nicola Tuveri Change-Id: I6a6a69643261ffd92e91e851413559aa38cbc7fa --- CHANGELOG.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) 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 From 7da83ea62381467843fc8162ae1b7132fcf63759 Mon Sep 17 00:00:00 2001 From: Nicola Tuveri Date: Wed, 17 Dec 2025 21:35:05 +0200 Subject: [PATCH 42/42] chore(release): bump version to 0.10.0 Signed-off-by: Nicola Tuveri Change-Id: I6a6a696407572af50cb79db6e4abf28e0d3a7acd --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f5ff93..59934a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "qubip_aurora" -version = "0.10.0+dev" +version = "0.10.0" dependencies = [ "anyhow", "asn1", diff --git a/Cargo.toml b/Cargo.toml index 82302c2..ca455c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qubip_aurora" -version = "0.10.0+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"