From 81e3d335476a9dc15ce27ccd975638a8c4c8ce4c Mon Sep 17 00:00:00 2001 From: Kevin Guthrie Date: Tue, 14 Apr 2026 11:29:26 -0400 Subject: [PATCH 1/5] PINGORA-2987 - Add rust wrapper code to support new PQ certificate validation algorithms --- boring-sys/build/main.rs | 1 + boring/src/lib.rs | 1 + boring/src/mldsa.rs | 456 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 458 insertions(+) create mode 100644 boring/src/mldsa.rs diff --git a/boring-sys/build/main.rs b/boring-sys/build/main.rs index d192a0f8c..72380a072 100644 --- a/boring-sys/build/main.rs +++ b/boring-sys/build/main.rs @@ -790,6 +790,7 @@ fn generate_bindings(config: &Config) -> Result ffi::CBS { + ffi::CBS { + data: data.as_ptr(), + len: data.len(), + } +} + +/// Seed size (32 bytes, shared across all ML-DSA parameter sets). +pub const SEED_BYTES: usize = ffi::MLDSA_SEED_BYTES as usize; + +/// Raw bytes of a private key seed ([`SEED_BYTES`] long). +pub type MlDsaSeed = [u8; SEED_BYTES]; + +/// ML-DSA parameter set selection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Algorithm { + /// NIST security level 2 (AES-128 equivalent). + MlDsa44, + /// Recommended. NIST security level 3 (AES-192 equivalent). + MlDsa65, + /// NIST security level 5 (AES-256 equivalent). + MlDsa87, +} + +impl Algorithm { + /// Returns the encoded public key size in bytes. + #[must_use] + pub const fn public_key_bytes(&self) -> usize { + match self { + Self::MlDsa44 => ffi::MLDSA44_PUBLIC_KEY_BYTES as usize, + Self::MlDsa65 => ffi::MLDSA65_PUBLIC_KEY_BYTES as usize, + Self::MlDsa87 => ffi::MLDSA87_PUBLIC_KEY_BYTES as usize, + } + } + + /// Returns the signature size in bytes. + #[must_use] + pub const fn signature_bytes(&self) -> usize { + match self { + Self::MlDsa44 => ffi::MLDSA44_SIGNATURE_BYTES as usize, + Self::MlDsa65 => ffi::MLDSA65_SIGNATURE_BYTES as usize, + Self::MlDsa87 => ffi::MLDSA87_SIGNATURE_BYTES as usize, + } + } +} + +/// An ML-DSA public key (any parameter set). +pub struct MlDsaPublicKey { + algorithm: Algorithm, + inner: PublicKeyInner, +} + +enum PublicKeyInner { + MlDsa44(Box), + MlDsa65(Box), + MlDsa87(Box), +} + +/// An ML-DSA private key (any parameter set). +pub struct MlDsaPrivateKey { + algorithm: Algorithm, + seed: MlDsaSeed, + inner: PrivateKeyInner, +} + +enum PrivateKeyInner { + MlDsa44(Box), + MlDsa65(Box), + MlDsa87(Box), +} + +impl MlDsaPrivateKey { + /// Generates a random ML-DSA key pair. + /// + /// Returns `(public_key, private_key)`. + pub fn generate(algorithm: Algorithm) -> Result<(MlDsaPublicKey, MlDsaPrivateKey), ErrorStack> { + unsafe { + ffi::init(); + match algorithm { + Algorithm::MlDsa44 => { + let mut pub_bytes = [0u8; ffi::MLDSA44_PUBLIC_KEY_BYTES as usize]; + let mut seed = [0u8; SEED_BYTES]; + let mut priv_key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA44_generate_key( + pub_bytes.as_mut_ptr(), + seed.as_mut_ptr(), + priv_key.as_mut_ptr(), + ))?; + let public_key = MlDsaPublicKey::from_bytes(algorithm, &pub_bytes)?; + Ok(( + public_key, + MlDsaPrivateKey { + algorithm, + seed, + inner: PrivateKeyInner::MlDsa44(Box::new(priv_key.assume_init())), + }, + )) + } + Algorithm::MlDsa65 => { + let mut pub_bytes = [0u8; ffi::MLDSA65_PUBLIC_KEY_BYTES as usize]; + let mut seed = [0u8; SEED_BYTES]; + let mut priv_key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA65_generate_key( + pub_bytes.as_mut_ptr(), + seed.as_mut_ptr(), + priv_key.as_mut_ptr(), + ))?; + let public_key = MlDsaPublicKey::from_bytes(algorithm, &pub_bytes)?; + Ok(( + public_key, + MlDsaPrivateKey { + algorithm, + seed, + inner: PrivateKeyInner::MlDsa65(Box::new(priv_key.assume_init())), + }, + )) + } + Algorithm::MlDsa87 => { + let mut pub_bytes = [0u8; ffi::MLDSA87_PUBLIC_KEY_BYTES as usize]; + let mut seed = [0u8; SEED_BYTES]; + let mut priv_key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA87_generate_key( + pub_bytes.as_mut_ptr(), + seed.as_mut_ptr(), + priv_key.as_mut_ptr(), + ))?; + let public_key = MlDsaPublicKey::from_bytes(algorithm, &pub_bytes)?; + Ok(( + public_key, + MlDsaPrivateKey { + algorithm, + seed, + inner: PrivateKeyInner::MlDsa87(Box::new(priv_key.assume_init())), + }, + )) + } + } + } + } + + /// Regenerates a private key from a seed value. + pub fn from_seed(algorithm: Algorithm, seed: &MlDsaSeed) -> Result { + unsafe { + ffi::init(); + match algorithm { + Algorithm::MlDsa44 => { + let mut priv_key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA44_private_key_from_seed( + priv_key.as_mut_ptr(), + seed.as_ptr(), + seed.len(), + ))?; + Ok(Self { + algorithm, + seed: *seed, + inner: PrivateKeyInner::MlDsa44(Box::new(priv_key.assume_init())), + }) + } + Algorithm::MlDsa65 => { + let mut priv_key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA65_private_key_from_seed( + priv_key.as_mut_ptr(), + seed.as_ptr(), + seed.len(), + ))?; + Ok(Self { + algorithm, + seed: *seed, + inner: PrivateKeyInner::MlDsa65(Box::new(priv_key.assume_init())), + }) + } + Algorithm::MlDsa87 => { + let mut priv_key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA87_private_key_from_seed( + priv_key.as_mut_ptr(), + seed.as_ptr(), + seed.len(), + ))?; + Ok(Self { + algorithm, + seed: *seed, + inner: PrivateKeyInner::MlDsa87(Box::new(priv_key.assume_init())), + }) + } + } + } + } + + /// Returns the algorithm for this key. + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// Returns the seed bytes for this private key. + pub fn seed(&self) -> &MlDsaSeed { + &self.seed + } + + /// Signs `msg` and returns the signature bytes. + pub fn sign(&self, msg: &[u8]) -> Result, ErrorStack> { + unsafe { + ffi::init(); + match &self.inner { + PrivateKeyInner::MlDsa44(key) => { + let mut sig = vec![0u8; ffi::MLDSA44_SIGNATURE_BYTES as usize]; + cvt(ffi::MLDSA44_sign( + sig.as_mut_ptr(), + key.as_ref(), + msg.as_ptr(), + msg.len(), + core::ptr::null(), + 0, + ))?; + Ok(sig) + } + PrivateKeyInner::MlDsa65(key) => { + let mut sig = vec![0u8; ffi::MLDSA65_SIGNATURE_BYTES as usize]; + cvt(ffi::MLDSA65_sign( + sig.as_mut_ptr(), + key.as_ref(), + msg.as_ptr(), + msg.len(), + core::ptr::null(), + 0, + ))?; + Ok(sig) + } + PrivateKeyInner::MlDsa87(key) => { + let mut sig = vec![0u8; ffi::MLDSA87_SIGNATURE_BYTES as usize]; + cvt(ffi::MLDSA87_sign( + sig.as_mut_ptr(), + key.as_ref(), + msg.as_ptr(), + msg.len(), + core::ptr::null(), + 0, + ))?; + Ok(sig) + } + } + } + } +} + +impl MlDsaPublicKey { + /// Parses a public key from its serialized form. + pub fn from_bytes(algorithm: Algorithm, bytes: &[u8]) -> Result { + unsafe { + ffi::init(); + match algorithm { + Algorithm::MlDsa44 => { + let mut cbs = cbs_init(bytes); + let mut key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA44_parse_public_key(key.as_mut_ptr(), &mut cbs))?; + if cbs.len != 0 { + return Err(ErrorStack::internal_error_str( + "trailing bytes after ML-DSA-44 public key", + )); + } + Ok(Self { + algorithm, + inner: PublicKeyInner::MlDsa44(Box::new(key.assume_init())), + }) + } + Algorithm::MlDsa65 => { + let mut cbs = cbs_init(bytes); + let mut key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA65_parse_public_key(key.as_mut_ptr(), &mut cbs))?; + if cbs.len != 0 { + return Err(ErrorStack::internal_error_str( + "trailing bytes after ML-DSA-65 public key", + )); + } + Ok(Self { + algorithm, + inner: PublicKeyInner::MlDsa65(Box::new(key.assume_init())), + }) + } + Algorithm::MlDsa87 => { + let mut cbs = cbs_init(bytes); + let mut key: MaybeUninit = MaybeUninit::uninit(); + cvt(ffi::MLDSA87_parse_public_key(key.as_mut_ptr(), &mut cbs))?; + if cbs.len != 0 { + return Err(ErrorStack::internal_error_str( + "trailing bytes after ML-DSA-87 public key", + )); + } + Ok(Self { + algorithm, + inner: PublicKeyInner::MlDsa87(Box::new(key.assume_init())), + }) + } + } + } + } + + /// Returns the algorithm for this key. + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// Verifies `signature` over `msg` using this public key. + pub fn verify(&self, msg: &[u8], signature: &[u8]) -> Result<(), ErrorStack> { + unsafe { + ffi::init(); + match &self.inner { + PublicKeyInner::MlDsa44(key) => { + cvt(ffi::MLDSA44_verify( + key.as_ref(), + signature.as_ptr(), + signature.len(), + msg.as_ptr(), + msg.len(), + core::ptr::null(), + 0, + ))?; + } + PublicKeyInner::MlDsa65(key) => { + cvt(ffi::MLDSA65_verify( + key.as_ref(), + signature.as_ptr(), + signature.len(), + msg.as_ptr(), + msg.len(), + core::ptr::null(), + 0, + ))?; + } + PublicKeyInner::MlDsa87(key) => { + cvt(ffi::MLDSA87_verify( + key.as_ref(), + signature.as_ptr(), + signature.len(), + msg.as_ptr(), + msg.len(), + core::ptr::null(), + 0, + ))?; + } + } + Ok(()) + } + } +} + +impl fmt::Debug for MlDsaPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MlDsaPrivateKey") + .field("algorithm", &self.algorithm) + .field("seed", &"[redacted]") + .finish() + } +} + +impl Drop for MlDsaPrivateKey { + fn drop(&mut self) { + unsafe { + ffi::OPENSSL_cleanse(self.seed.as_mut_ptr().cast(), self.seed.len()); + } + } +} + +impl fmt::Debug for MlDsaPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MlDsaPublicKey") + .field("algorithm", &self.algorithm) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! mldsa_tests { + ($name:ident, $alg:expr) => { + mod $name { + use super::*; + + #[test] + fn sign_and_verify() { + let (pk, sk) = MlDsaPrivateKey::generate($alg).unwrap(); + let msg = b"test message"; + let sig = sk.sign(msg).unwrap(); + assert_eq!(sig.len(), $alg.signature_bytes()); + assert!(pk.verify(msg, &sig).is_ok()); + } + + #[test] + fn bad_signature_fails() { + let (pk, sk) = MlDsaPrivateKey::generate($alg).unwrap(); + let msg = b"test message"; + let mut sig = sk.sign(msg).unwrap(); + sig[5] ^= 1; + assert!(pk.verify(msg, &sig).is_err()); + } + + #[test] + fn wrong_message_fails() { + let (pk, sk) = MlDsaPrivateKey::generate($alg).unwrap(); + let sig = sk.sign(b"correct").unwrap(); + assert!(pk.verify(b"wrong", &sig).is_err()); + } + + #[test] + fn seed_roundtrip() { + let (pk, sk) = MlDsaPrivateKey::generate($alg).unwrap(); + let sk2 = MlDsaPrivateKey::from_seed($alg, sk.seed()).unwrap(); + let msg = b"seed roundtrip"; + let sig = sk2.sign(msg).unwrap(); + assert!(pk.verify(msg, &sig).is_ok()); + } + + #[test] + fn debug_redacts_seed() { + let (_, sk) = MlDsaPrivateKey::generate($alg).unwrap(); + let dbg = format!("{:?}", sk); + assert!(dbg.contains("redacted")); + assert!(!dbg.contains(&format!("{:?}", sk.seed()))); + } + } + }; + } + + mldsa_tests!(mldsa44, Algorithm::MlDsa44); + mldsa_tests!(mldsa65, Algorithm::MlDsa65); + mldsa_tests!(mldsa87, Algorithm::MlDsa87); +} From b399e0f26911752bb22624082de99826425b2468 Mon Sep 17 00:00:00 2001 From: Kornel Date: Tue, 21 Apr 2026 18:22:39 +0100 Subject: [PATCH 2/5] Feature flag for mldsa --- .github/workflows/ci.yml | 6 +++--- boring/Cargo.toml | 2 ++ boring/src/lib.rs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bea4db064..d9f549e5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: key: index-${{ steps.rust-version.outputs.version }}-${{ hashFiles('Cargo.toml') }} enableCrossOsArchive: true - name: Run clippy - run: cargo clippy --all --all-targets --features rpk,prf,mlkem + run: cargo clippy --all --all-targets --features rpk,prf,mlkem,mldsa - name: Check docs run: cargo doc --no-deps -p boring -p boring-sys -p hyper-boring -p tokio-boring --features rpk,underscore-wildcards env: @@ -438,7 +438,7 @@ jobs: shell: bash - run: cargo check --no-default-features name: Check `--no-default-features` - - run: cargo check --features prf,mlkem,credential + - run: cargo check --features prf,mlkem,mldsa,credential name: Check `mlkem,credential` - run: cargo test --features rpk,prf name: Run `rpk` tests @@ -446,5 +446,5 @@ jobs: name: Run `underscore-wildcards` tests - run: cargo test --features rpk,underscore-wildcards name: Run `rpk,underscore-wildcards` tests - - run: cargo test --features rpk,underscore-wildcards,mlkem + - run: cargo test --features rpk,underscore-wildcards,mlkem,mldsa name: Run `rpk,underscore-wildcards` tests diff --git a/boring/Cargo.toml b/boring/Cargo.toml index 0aa34592f..282da9760 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -31,6 +31,8 @@ pq-experimental = [] # Interface for ML-KEM (FIPS 203) post-quantum key encapsulation. Does not affect ciphers used in TLS. mlkem = [] +# ML-DSA (FIPS 204) post-quantum digital signature. +mldsa = [] # Expose internal `tls1_prf` prf = [] diff --git a/boring/src/lib.rs b/boring/src/lib.rs index d8395bc5b..dc47c1f67 100644 --- a/boring/src/lib.rs +++ b/boring/src/lib.rs @@ -138,6 +138,7 @@ pub mod hash; pub mod hmac; pub mod hpke; pub mod memcmp; +#[cfg(feature = "mldsa")] pub mod mldsa; #[cfg(feature = "mlkem")] pub mod mlkem; From b275161cb36284ab1501740bc8ff86772c6526c5 Mon Sep 17 00:00:00 2001 From: Kornel Date: Tue, 21 Apr 2026 18:25:15 +0100 Subject: [PATCH 3/5] Move cbs_init to boring-sys --- boring-sys/src/lib.rs | 9 +++++++++ boring/src/mldsa.rs | 10 +--------- boring/src/mlkem.rs | 10 +--------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/boring-sys/src/lib.rs b/boring-sys/src/lib.rs index 1810d174c..79fa90886 100644 --- a/boring-sys/src/lib.rs +++ b/boring-sys/src/lib.rs @@ -68,6 +68,15 @@ pub fn init() { } } +// CBS_init is inline in BoringSSL, so bindgen can't generate bindings for it. +#[inline] +pub fn cbs_init(data: &[u8]) -> CBS { + CBS { + data: data.as_ptr(), + len: data.len(), + } +} + pub mod internal { use super::EVP_MD; use std::os::raw::c_int; diff --git a/boring/src/mldsa.rs b/boring/src/mldsa.rs index 9facd97d8..fe9fbfcd8 100644 --- a/boring/src/mldsa.rs +++ b/boring/src/mldsa.rs @@ -23,15 +23,7 @@ use std::mem::MaybeUninit; use crate::cvt; use crate::error::ErrorStack; use crate::ffi; - -// CBS_init is inline in BoringSSL, so bindgen can't generate bindings for it. -#[inline] -fn cbs_init(data: &[u8]) -> ffi::CBS { - ffi::CBS { - data: data.as_ptr(), - len: data.len(), - } -} +use crate::ffi::cbs_init; /// Seed size (32 bytes, shared across all ML-DSA parameter sets). pub const SEED_BYTES: usize = ffi::MLDSA_SEED_BYTES as usize; diff --git a/boring/src/mlkem.rs b/boring/src/mlkem.rs index ead96b86e..d8bdf9f02 100644 --- a/boring/src/mlkem.rs +++ b/boring/src/mlkem.rs @@ -21,15 +21,7 @@ use std::mem::MaybeUninit; use crate::cvt; use crate::error::ErrorStack; use crate::ffi; - -// CBS_init is inline in BoringSSL, so bindgen can't generate bindings for it. -#[inline] -fn cbs_init(data: &[u8]) -> ffi::CBS { - ffi::CBS { - data: data.as_ptr(), - len: data.len(), - } -} +use crate::ffi::cbs_init; /// Private key seed size (64 bytes). pub const PRIVATE_KEY_SEED_BYTES: usize = ffi::MLKEM_SEED_BYTES as usize; From b978fd05ccfdb6d7b218c5c53a0913f1bedb27a9 Mon Sep 17 00:00:00 2001 From: Kornel Date: Thu, 23 Apr 2026 14:00:09 +0100 Subject: [PATCH 4/5] MLDSA comments --- boring/src/mldsa.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/boring/src/mldsa.rs b/boring/src/mldsa.rs index fe9fbfcd8..a06b5ff78 100644 --- a/boring/src/mldsa.rs +++ b/boring/src/mldsa.rs @@ -1,8 +1,5 @@ //! ML-DSA (FIPS 204) post-quantum digital signature. //! -//! ML-DSA-65 is the recommended parameter set, offering NIST security level 3 -//! (roughly equivalent to AES-192). -//! //! ``` //! use boring::mldsa::{MlDsaPrivateKey, MlDsaPublicKey, Algorithm}; //! @@ -36,7 +33,7 @@ pub type MlDsaSeed = [u8; SEED_BYTES]; pub enum Algorithm { /// NIST security level 2 (AES-128 equivalent). MlDsa44, - /// Recommended. NIST security level 3 (AES-192 equivalent). + /// NIST security level 3 (AES-192 equivalent). MlDsa65, /// NIST security level 5 (AES-256 equivalent). MlDsa87, From c4ca7b7e5bfc085022f42aae8d7bb6d9b0b57a46 Mon Sep 17 00:00:00 2001 From: Kornel Date: Thu, 23 Apr 2026 15:28:40 +0100 Subject: [PATCH 5/5] API consistency --- boring/Cargo.toml | 2 +- boring/src/mldsa.rs | 65 +++++++++++++++++++++++++++++---------------- boring/src/mlkem.rs | 4 +++ 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/boring/Cargo.toml b/boring/Cargo.toml index 282da9760..a09350472 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -13,7 +13,7 @@ edition = { workspace = true } rust-version = { workspace = true } [package.metadata.docs.rs] -features = ["rpk", "underscore-wildcards"] +features = ["rpk", "underscore-wildcards", "mlkem", "mldsa"] rustdoc-args = ["--cfg", "docsrs"] [features] diff --git a/boring/src/mldsa.rs b/boring/src/mldsa.rs index a06b5ff78..db3ace0a2 100644 --- a/boring/src/mldsa.rs +++ b/boring/src/mldsa.rs @@ -23,10 +23,10 @@ use crate::ffi; use crate::ffi::cbs_init; /// Seed size (32 bytes, shared across all ML-DSA parameter sets). -pub const SEED_BYTES: usize = ffi::MLDSA_SEED_BYTES as usize; +pub const PRIVATE_KEY_SEED_BYTES: usize = ffi::MLDSA_SEED_BYTES as usize; /// Raw bytes of a private key seed ([`SEED_BYTES`] long). -pub type MlDsaSeed = [u8; SEED_BYTES]; +pub type MlDsaPrivateKeySeed = [u8; PRIVATE_KEY_SEED_BYTES]; /// ML-DSA parameter set selection. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -62,11 +62,13 @@ impl Algorithm { } /// An ML-DSA public key (any parameter set). +#[derive(Clone)] pub struct MlDsaPublicKey { algorithm: Algorithm, inner: PublicKeyInner, } +#[derive(Clone)] enum PublicKeyInner { MlDsa44(Box), MlDsa65(Box), @@ -76,7 +78,7 @@ enum PublicKeyInner { /// An ML-DSA private key (any parameter set). pub struct MlDsaPrivateKey { algorithm: Algorithm, - seed: MlDsaSeed, + seed: MlDsaPrivateKeySeed, inner: PrivateKeyInner, } @@ -86,6 +88,12 @@ enum PrivateKeyInner { MlDsa87(Box), } +impl Clone for MlDsaPrivateKey { + fn clone(&self) -> Self { + Self::from_seed(self.algorithm, &self.seed).unwrap() + } +} + impl MlDsaPrivateKey { /// Generates a random ML-DSA key pair. /// @@ -96,14 +104,14 @@ impl MlDsaPrivateKey { match algorithm { Algorithm::MlDsa44 => { let mut pub_bytes = [0u8; ffi::MLDSA44_PUBLIC_KEY_BYTES as usize]; - let mut seed = [0u8; SEED_BYTES]; + let mut seed = [0u8; PRIVATE_KEY_SEED_BYTES]; let mut priv_key: MaybeUninit = MaybeUninit::uninit(); cvt(ffi::MLDSA44_generate_key( pub_bytes.as_mut_ptr(), seed.as_mut_ptr(), priv_key.as_mut_ptr(), ))?; - let public_key = MlDsaPublicKey::from_bytes(algorithm, &pub_bytes)?; + let public_key = MlDsaPublicKey::from_slice(algorithm, &pub_bytes)?; Ok(( public_key, MlDsaPrivateKey { @@ -115,14 +123,14 @@ impl MlDsaPrivateKey { } Algorithm::MlDsa65 => { let mut pub_bytes = [0u8; ffi::MLDSA65_PUBLIC_KEY_BYTES as usize]; - let mut seed = [0u8; SEED_BYTES]; + let mut seed = [0u8; PRIVATE_KEY_SEED_BYTES]; let mut priv_key: MaybeUninit = MaybeUninit::uninit(); cvt(ffi::MLDSA65_generate_key( pub_bytes.as_mut_ptr(), seed.as_mut_ptr(), priv_key.as_mut_ptr(), ))?; - let public_key = MlDsaPublicKey::from_bytes(algorithm, &pub_bytes)?; + let public_key = MlDsaPublicKey::from_slice(algorithm, &pub_bytes)?; Ok(( public_key, MlDsaPrivateKey { @@ -134,14 +142,14 @@ impl MlDsaPrivateKey { } Algorithm::MlDsa87 => { let mut pub_bytes = [0u8; ffi::MLDSA87_PUBLIC_KEY_BYTES as usize]; - let mut seed = [0u8; SEED_BYTES]; + let mut seed = [0u8; PRIVATE_KEY_SEED_BYTES]; let mut priv_key: MaybeUninit = MaybeUninit::uninit(); cvt(ffi::MLDSA87_generate_key( pub_bytes.as_mut_ptr(), seed.as_mut_ptr(), priv_key.as_mut_ptr(), ))?; - let public_key = MlDsaPublicKey::from_bytes(algorithm, &pub_bytes)?; + let public_key = MlDsaPublicKey::from_slice(algorithm, &pub_bytes)?; Ok(( public_key, MlDsaPrivateKey { @@ -156,7 +164,7 @@ impl MlDsaPrivateKey { } /// Regenerates a private key from a seed value. - pub fn from_seed(algorithm: Algorithm, seed: &MlDsaSeed) -> Result { + pub fn from_seed(algorithm: Algorithm, seed: &MlDsaPrivateKeySeed) -> Result { unsafe { ffi::init(); match algorithm { @@ -209,7 +217,7 @@ impl MlDsaPrivateKey { } /// Returns the seed bytes for this private key. - pub fn seed(&self) -> &MlDsaSeed { + pub fn seed_bytes(&self) -> &MlDsaPrivateKeySeed { &self.seed } @@ -261,12 +269,20 @@ impl MlDsaPrivateKey { impl MlDsaPublicKey { /// Parses a public key from its serialized form. - pub fn from_bytes(algorithm: Algorithm, bytes: &[u8]) -> Result { + pub fn from_slice( + algorithm: Algorithm, + serialized_public_key: &[u8], + ) -> Result { + ffi::init(); + + if serialized_public_key.len() != algorithm.public_key_bytes() { + return Err(ErrorStack::internal_error_str("invalid public key length")); + } + let mut cbs = cbs_init(serialized_public_key); + unsafe { - ffi::init(); match algorithm { Algorithm::MlDsa44 => { - let mut cbs = cbs_init(bytes); let mut key: MaybeUninit = MaybeUninit::uninit(); cvt(ffi::MLDSA44_parse_public_key(key.as_mut_ptr(), &mut cbs))?; if cbs.len != 0 { @@ -280,7 +296,6 @@ impl MlDsaPublicKey { }) } Algorithm::MlDsa65 => { - let mut cbs = cbs_init(bytes); let mut key: MaybeUninit = MaybeUninit::uninit(); cvt(ffi::MLDSA65_parse_public_key(key.as_mut_ptr(), &mut cbs))?; if cbs.len != 0 { @@ -294,7 +309,6 @@ impl MlDsaPublicKey { }) } Algorithm::MlDsa87 => { - let mut cbs = cbs_init(bytes); let mut key: MaybeUninit = MaybeUninit::uninit(); cvt(ffi::MLDSA87_parse_public_key(key.as_mut_ptr(), &mut cbs))?; if cbs.len != 0 { @@ -398,9 +412,12 @@ mod tests { fn sign_and_verify() { let (pk, sk) = MlDsaPrivateKey::generate($alg).unwrap(); let msg = b"test message"; - let sig = sk.sign(msg).unwrap(); - assert_eq!(sig.len(), $alg.signature_bytes()); - assert!(pk.verify(msg, &sig).is_ok()); + let sig1 = sk.sign(msg).unwrap(); + let sig2 = sk.clone().sign(msg).unwrap(); + assert_eq!(sig1.len(), $alg.signature_bytes()); + assert!(pk.verify(msg, &sig1).is_ok()); + assert!(pk.verify(msg, &sig2).is_ok()); + assert!(pk.clone().verify(msg, &sig1).is_ok()); } #[test] @@ -422,10 +439,12 @@ mod tests { #[test] fn seed_roundtrip() { let (pk, sk) = MlDsaPrivateKey::generate($alg).unwrap(); - let sk2 = MlDsaPrivateKey::from_seed($alg, sk.seed()).unwrap(); + let sk2 = MlDsaPrivateKey::from_seed($alg, sk.seed_bytes()).unwrap(); let msg = b"seed roundtrip"; - let sig = sk2.sign(msg).unwrap(); - assert!(pk.verify(msg, &sig).is_ok()); + let sig1 = sk2.sign(msg).unwrap(); + let sig2 = sk2.clone().sign(msg).unwrap(); + assert!(pk.verify(msg, &sig1).is_ok()); + assert!(pk.verify(msg, &sig2).is_ok()); } #[test] @@ -433,7 +452,7 @@ mod tests { let (_, sk) = MlDsaPrivateKey::generate($alg).unwrap(); let dbg = format!("{:?}", sk); assert!(dbg.contains("redacted")); - assert!(!dbg.contains(&format!("{:?}", sk.seed()))); + assert!(!dbg.contains(&format!("{:?}", sk.seed_bytes()))); } } }; diff --git a/boring/src/mlkem.rs b/boring/src/mlkem.rs index d8bdf9f02..5f3861303 100644 --- a/boring/src/mlkem.rs +++ b/boring/src/mlkem.rs @@ -688,7 +688,11 @@ mod tests { let (pk, sk) = MlKemPrivateKey::generate($algorithm).unwrap(); let (ct, ss1) = pk.encapsulate().unwrap(); let ss2 = sk.decapsulate(&ct).unwrap(); + let ss3 = sk.clone().decapsulate(&ct).unwrap(); + let ss4 = sk.decapsulate(&ct.clone()).unwrap(); assert_eq!(ss1, ss2); + assert_eq!(ss1, ss3); + assert_eq!(ss1, ss4); } #[test]