Skip to content

Commit 80e111a

Browse files
committed
Add mu generator support for ML-DSA
Add support for SHAKE-128 and SHAKE-256 digests along with XOF output length configuration. Implement `MuGenerator` and the `mldsa_with_mu` parameter helper to compute and supply the FIPS 204 `mu` value directly for ML-DSA signatures and verification, and add corresponding test vectors. Assisted-by: Gemini:Gemini 3.7 Flash Signed-off-by: Simo Sorce <simo@redhat.com>
1 parent 444c69f commit 80e111a

5 files changed

Lines changed: 560 additions & 0 deletions

File tree

ossl/src/digest.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ pub enum DigestAlg {
187187
Sha3_256,
188188
Sha3_384,
189189
Sha3_512,
190+
Shake128,
191+
Shake256,
190192
#[cfg(feature = "rfc9580")]
191193
Md5,
192194
#[cfg(feature = "rfc9580")]
@@ -206,6 +208,8 @@ pub(crate) fn digest_to_string(digest: DigestAlg) -> &'static CStr {
206208
DigestAlg::Sha3_256 => cstr!(OSSL_DIGEST_NAME_SHA3_256),
207209
DigestAlg::Sha3_384 => cstr!(OSSL_DIGEST_NAME_SHA3_384),
208210
DigestAlg::Sha3_512 => cstr!(OSSL_DIGEST_NAME_SHA3_512),
211+
DigestAlg::Shake128 => c"SHAKE-128",
212+
DigestAlg::Shake256 => c"SHAKE-256",
209213
#[cfg(feature = "rfc9580")]
210214
DigestAlg::Md5 => cstr!(OSSL_DIGEST_NAME_MD5),
211215
#[cfg(feature = "rfc9580")]
@@ -226,6 +230,8 @@ pub(crate) fn string_to_digest(digest: &CStr) -> Result<DigestAlg, Error> {
226230
b"SHA3-256" => Ok(DigestAlg::Sha3_256),
227231
b"SHA3-384" => Ok(DigestAlg::Sha3_384),
228232
b"SHA3-512" => Ok(DigestAlg::Sha3_512),
233+
b"SHAKE-128" => Ok(DigestAlg::Shake128),
234+
b"SHAKE-256" => Ok(DigestAlg::Shake256),
229235
#[cfg(feature = "rfc9580")]
230236
b"MD5" => Ok(DigestAlg::Md5),
231237
#[cfg(feature = "rfc9580")]
@@ -335,6 +341,22 @@ impl OsslDigest {
335341
Ok(usize::try_from(retlen)?)
336342
}
337343

344+
/// Sets the requested output length for an XOF algorithm like SHAKE.
345+
#[cfg(ossl_v350)]
346+
pub fn set_xoflen(&mut self, len: usize) -> Result<(), Error> {
347+
let mut params_builder = crate::OsslParamBuilder::with_capacity(1);
348+
params_builder
349+
.add_owned_uint(cstr!(OSSL_DIGEST_PARAM_XOFLEN), len as c_uint)?;
350+
let params = params_builder.finalize();
351+
if unsafe {
352+
EVP_MD_CTX_set_params(self.ctx.as_mut_ptr(), params.as_ptr())
353+
} != 1
354+
{
355+
return Err(Error::new(ErrorKind::OsslError));
356+
}
357+
Ok(())
358+
}
359+
338360
/// Provides the size of the expected output digest
339361
pub fn size(&self) -> usize {
340362
self.size

ossl/src/non-openssl-sys-bindings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub const OSSL_PARAM_UTF8_STRING: u32 = 4;
55
// Signature + AsymCipher
66
pub const OSSL_SIGNATURE_PARAM_CONTEXT_STRING: &[u8; 15] = b"context-string\0";
77
pub const OSSL_SIGNATURE_PARAM_DETERMINISTIC: &[u8; 14] = b"deterministic\0";
8+
pub const OSSL_SIGNATURE_PARAM_MU: &[u8; 3] = b"mu\0";
89
pub const OSSL_SIGNATURE_PARAM_PSS_SALTLEN: &[u8; 8] = b"saltlen\0";
910
pub const OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING: &[u8; 17] =
1011
b"message-encoding\0";
@@ -36,6 +37,7 @@ pub const OSSL_DIGEST_NAME_SHA3_224: &[u8; 9] = b"SHA3-224\0";
3637
pub const OSSL_DIGEST_NAME_SHA3_256: &[u8; 9] = b"SHA3-256\0";
3738
pub const OSSL_DIGEST_NAME_SHA3_384: &[u8; 9] = b"SHA3-384\0";
3839
pub const OSSL_DIGEST_NAME_SHA3_512: &[u8; 9] = b"SHA3-512\0";
40+
pub const OSSL_DIGEST_PARAM_XOFLEN: &[u8; 7] = b"xoflen\0";
3941

4042
// Rand
4143
pub const OSSL_RAND_PARAM_STRENGTH: &[u8; 9] = b"strength\0";

ossl/src/signature.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,126 @@ pub fn mldsa_params<'a>(
650650
return Ok(Some(params));
651651
}
652652

653+
/// Generator for the exact `mu` (μ) value required for ML-DSA signatures
654+
/// according to FIPS 204.
655+
#[cfg(ossl_v350)]
656+
pub struct MuGenerator {
657+
shake_mu: crate::digest::OsslDigest,
658+
tr: Vec<u8>,
659+
context: Option<Vec<u8>>,
660+
}
661+
662+
#[cfg(ossl_v350)]
663+
impl MuGenerator {
664+
pub fn new(
665+
libctx: &OsslContext,
666+
pkey: &EvpPkey,
667+
context: Option<&[u8]>,
668+
) -> Result<Self, Error> {
669+
if let Some(c) = context {
670+
if c.len() > 255 {
671+
return Err(Error::new(ErrorKind::BadArg));
672+
}
673+
}
674+
675+
let pkey_data = pkey.export_public()?;
676+
let pk_bytes = match pkey_data {
677+
crate::pkey::PkeyData::Mlkey(mlk) => {
678+
mlk.pubkey.clone().ok_or(Error::new(ErrorKind::NullPtr))?
679+
}
680+
_ => return Err(Error::new(ErrorKind::WrapperError)),
681+
};
682+
683+
let mut tr = vec![0u8; 64];
684+
let mut shake_tr =
685+
crate::digest::OsslDigest::new(libctx, DigestAlg::Shake256, None)?;
686+
shake_tr.update(&pk_bytes)?;
687+
shake_tr.set_xoflen(64)?;
688+
shake_tr.finalize(&mut tr)?;
689+
690+
let mut mugen = MuGenerator {
691+
shake_mu: crate::digest::OsslDigest::new(
692+
libctx,
693+
DigestAlg::Shake256,
694+
None,
695+
)?,
696+
tr,
697+
context: context.map(|c| c.to_vec()),
698+
};
699+
mugen._init()?;
700+
Ok(mugen)
701+
}
702+
703+
fn _init(&mut self) -> Result<(), Error> {
704+
self.shake_mu.update(&self.tr)?;
705+
706+
let ctx_len = match &self.context {
707+
Some(c) => c.len() as u8,
708+
None => 0,
709+
};
710+
711+
let mut m_prime = Vec::with_capacity(2 + ctx_len as usize);
712+
m_prime.push(0x00);
713+
m_prime.push(ctx_len);
714+
if let Some(c) = &self.context {
715+
m_prime.extend_from_slice(c);
716+
}
717+
self.shake_mu.update(&m_prime)?;
718+
719+
Ok(())
720+
}
721+
722+
pub fn reinit(&mut self) -> Result<(), Error> {
723+
self.shake_mu.reset(None)?;
724+
self._init()
725+
}
726+
727+
pub fn update(&mut self, data: &[u8]) -> Result<(), Error> {
728+
self.shake_mu.update(data)
729+
}
730+
731+
pub fn finalize(
732+
&mut self,
733+
data: Option<&[u8]>,
734+
out_mu: &mut [u8],
735+
) -> Result<(), Error> {
736+
if out_mu.len() != 64 {
737+
return Err(Error::new(ErrorKind::BadArg));
738+
}
739+
if let Some(d) = data {
740+
self.shake_mu.update(d)?;
741+
}
742+
self.shake_mu.set_xoflen(64)?;
743+
self.shake_mu.finalize(out_mu)?;
744+
Ok(())
745+
}
746+
}
747+
748+
/// Helper to generate OsslParam arrays for Mldsa initialization with mu.
749+
/// When this helper is used, the signature will only be one shot and only a
750+
/// properly calculated MU buffer (which must be exactly 64 bytes in length)
751+
/// can be used as data for the sign()/verify() calls.
752+
#[cfg(ossl_v350)]
753+
pub fn mldsa_with_mu<'a>(
754+
context: Option<&'a Vec<u8>>,
755+
deterministic: bool,
756+
) -> Result<Option<OsslParam<'a>>, Error> {
757+
let mut params_builder = OsslParamBuilder::with_capacity(3);
758+
params_builder.add_owned_int(cstr!(OSSL_SIGNATURE_PARAM_MU), 1)?;
759+
if let Some(ctx) = context {
760+
params_builder.add_octet_string(
761+
cstr!(OSSL_SIGNATURE_PARAM_CONTEXT_STRING),
762+
ctx,
763+
)?;
764+
}
765+
if deterministic {
766+
params_builder
767+
.add_owned_int(cstr!(OSSL_SIGNATURE_PARAM_DETERMINISTIC), 1)?;
768+
}
769+
let params = params_builder.finalize();
770+
return Ok(Some(params));
771+
}
772+
653773
/// Helper to generate OsslParam arrays for SLH-DSA initialization
654774
/// FIXME: The same as mldsa_params?
655775
#[cfg(ossl_v350)]

0 commit comments

Comments
 (0)