Skip to content

Commit 2864758

Browse files
committed
Add support for ML-DSA.
See #2393.
1 parent f60f7b7 commit 2864758

File tree

3 files changed

+306
-0
lines changed

3 files changed

+306
-0
lines changed

openssl/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ pub mod pkcs7;
186186
pub mod pkey;
187187
pub mod pkey_ctx;
188188
#[cfg(ossl350)]
189+
pub mod pkey_ml_dsa;
190+
#[cfg(ossl350)]
189191
pub mod pkey_ml_kem;
190192
#[cfg(ossl300)]
191193
pub mod provider;

openssl/src/pkey_ml_dsa.rs

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
//! Module-Lattice-Based Digital Signatures.
2+
//!
3+
//! ML-DSA is a signature algorithm that is believed to be secure
4+
//! against adversaries with quantum computers. It has been
5+
//! standardized by NIST as [FIPS 204].
6+
//!
7+
//! [FIPS 204]: https://csrc.nist.gov/pubs/fips/204/final
8+
9+
use std::ffi::CStr;
10+
11+
use foreign_types::ForeignType;
12+
use libc::c_int;
13+
use std::ptr;
14+
15+
use crate::error::ErrorStack;
16+
use crate::ossl_param::{OsslParam, OsslParamBuilder};
17+
use crate::pkey::{PKey, Private, Public};
18+
use crate::pkey_ctx::PkeyCtx;
19+
use crate::{cvt, cvt_p};
20+
use openssl_macros::corresponds;
21+
22+
const OSSL_PKEY_PARAM_SEED: &[u8; 5] = b"seed\0";
23+
const OSSL_PKEY_PARAM_PUB_KEY: &[u8; 4] = b"pub\0";
24+
const OSSL_PKEY_PARAM_PRIV_KEY: &[u8; 5] = b"priv\0";
25+
26+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
27+
pub enum Variant {
28+
MlDsa44,
29+
MlDsa65,
30+
MlDsa87,
31+
}
32+
33+
impl Variant {
34+
pub(crate) fn as_str(&self) -> &'static str {
35+
match self {
36+
Variant::MlDsa44 => "ML-DSA-44",
37+
Variant::MlDsa65 => "ML-DSA-65",
38+
Variant::MlDsa87 => "ML-DSA-87",
39+
}
40+
}
41+
42+
pub(crate) fn as_cstr(&self) -> &'static CStr {
43+
match self {
44+
Variant::MlDsa44 => CStr::from_bytes_with_nul(b"ML-DSA-44\0"),
45+
Variant::MlDsa65 => CStr::from_bytes_with_nul(b"ML-DSA-65\0"),
46+
Variant::MlDsa87 => CStr::from_bytes_with_nul(b"ML-DSA-87\0"),
47+
}.unwrap()
48+
}
49+
}
50+
51+
pub struct PKeyMlDsaBuilder<T> {
52+
bld: OsslParamBuilder,
53+
variant: Variant,
54+
_m: ::std::marker::PhantomData<T>,
55+
}
56+
57+
impl<T> PKeyMlDsaBuilder<T> {
58+
/// Creates a new `PKeyMlDsaBuilder` to build ML-DSA private or
59+
/// public keys.
60+
pub fn new(
61+
variant: Variant,
62+
public: &[u8],
63+
private: Option<&[u8]>,
64+
) -> Result<PKeyMlDsaBuilder<T>, ErrorStack> {
65+
let bld = OsslParamBuilder::new()?;
66+
bld.add_octet_string(OSSL_PKEY_PARAM_PUB_KEY, public)?;
67+
if let Some(private) = private {
68+
bld.add_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, private)?
69+
};
70+
Ok(PKeyMlDsaBuilder::<T> {
71+
bld,
72+
variant,
73+
_m: ::std::marker::PhantomData,
74+
})
75+
}
76+
77+
/// Creates a new `PKeyMlDsaBuilder` to build ML-DSA private keys
78+
/// from a seed.
79+
pub fn from_seed(
80+
variant: Variant,
81+
seed: &[u8],
82+
) -> Result<PKeyMlDsaBuilder<T>, ErrorStack> {
83+
let bld = OsslParamBuilder::new()?;
84+
bld.add_octet_string(OSSL_PKEY_PARAM_SEED, seed)?;
85+
Ok(PKeyMlDsaBuilder::<T> {
86+
bld,
87+
variant,
88+
_m: ::std::marker::PhantomData,
89+
})
90+
}
91+
92+
/// Build PKey. Internal.
93+
#[corresponds(EVP_PKEY_fromdata)]
94+
fn build_internal(self, selection: c_int)
95+
-> Result<PKey<T>, ErrorStack>
96+
{
97+
let mut ctx = PkeyCtx::new_from_name(
98+
None, self.variant.as_str(), None)?;
99+
ctx.fromdata_init()?;
100+
let params = self.bld.to_param()?;
101+
unsafe {
102+
let evp = cvt_p(ffi::EVP_PKEY_new())?;
103+
let pkey = PKey::from_ptr(evp);
104+
cvt(ffi::EVP_PKEY_fromdata(
105+
ctx.as_ptr(),
106+
&mut pkey.as_ptr(),
107+
selection,
108+
params.as_ptr(),
109+
))?;
110+
Ok(pkey)
111+
}
112+
}
113+
}
114+
115+
impl PKeyMlDsaBuilder<Private> {
116+
/// Returns the Private ML-DSA PKey from the provided parameters.
117+
#[corresponds(EVP_PKEY_fromdata)]
118+
pub fn build(self) -> Result<PKey<Private>, ErrorStack> {
119+
self.build_internal(ffi::EVP_PKEY_KEYPAIR)
120+
}
121+
122+
/// Creates a new `PKeyRsaBuilder` to generate a new ML-DSA key
123+
/// pair.
124+
pub fn new_generate(variant: Variant)
125+
-> Result<PKeyMlDsaBuilder<Private>, ErrorStack>
126+
{
127+
let bld = OsslParamBuilder::new()?;
128+
Ok(PKeyMlDsaBuilder::<Private> {
129+
bld,
130+
variant,
131+
_m: ::std::marker::PhantomData,
132+
})
133+
}
134+
135+
/// Generate an ML-DSA PKey.
136+
pub fn generate(self) -> Result<PKey<Private>, ErrorStack> {
137+
let mut ctx = PkeyCtx::new_from_name(
138+
None, self.variant.as_str(), None)?;
139+
ctx.keygen_init()?;
140+
let params = self.bld.to_param()?;
141+
unsafe {
142+
cvt(ffi::EVP_PKEY_CTX_set_params(ctx.as_ptr(), params.as_ptr()))?;
143+
}
144+
ctx.generate()
145+
}
146+
}
147+
148+
impl PKeyMlDsaBuilder<Public> {
149+
/// Returns the Public ML-DSA PKey from the provided parameters.
150+
#[corresponds(EVP_PKEY_fromdata)]
151+
pub fn build(self) -> Result<PKey<Public>, ErrorStack> {
152+
self.build_internal(ffi::EVP_PKEY_PUBLIC_KEY)
153+
}
154+
}
155+
156+
pub struct PKeyMlDsaParams<T> {
157+
params: OsslParam,
158+
_m: ::std::marker::PhantomData<T>,
159+
}
160+
161+
impl<T> PKeyMlDsaParams<T> {
162+
/// Creates a new `PKeyMlDsaParams` from existing ECDSA PKey. Internal.
163+
#[corresponds(EVP_PKEY_todata)]
164+
fn _new_from_pkey<S>(pkey: &PKey<S>, selection: c_int)
165+
-> Result<PKeyMlDsaParams<T>, ErrorStack>
166+
{
167+
unsafe {
168+
let mut params: *mut ffi::OSSL_PARAM = ptr::null_mut();
169+
cvt(ffi::EVP_PKEY_todata(pkey.as_ptr(), selection, &mut params))?;
170+
Ok(PKeyMlDsaParams::<T> {
171+
params: OsslParam::from_ptr(params),
172+
_m: ::std::marker::PhantomData,
173+
})
174+
}
175+
}
176+
}
177+
178+
impl PKeyMlDsaParams<Public> {
179+
/// Creates a new `PKeyMlDsaParams` from existing Public ECDSA PKey.
180+
#[corresponds(EVP_PKEY_todata)]
181+
pub fn from_pkey<S>(pkey: &PKey<S>) -> Result<PKeyMlDsaParams<Public>, ErrorStack> {
182+
Self::_new_from_pkey(pkey, ffi::EVP_PKEY_PUBLIC_KEY)
183+
}
184+
185+
/// Returns a reference to the public key.
186+
pub fn public_key(&self) -> Result<&[u8], ErrorStack> {
187+
self.params
188+
.locate(OSSL_PKEY_PARAM_PUB_KEY).unwrap()
189+
.get_octet_string()
190+
}
191+
}
192+
193+
impl PKeyMlDsaParams<Private> {
194+
/// Creates a new `PKeyMlDsaParams` from existing Private ECDSA PKey.
195+
#[corresponds(EVP_PKEY_todata)]
196+
pub fn from_pkey(pkey: &PKey<Private>) -> Result<PKeyMlDsaParams<Private>, ErrorStack> {
197+
Self::_new_from_pkey(pkey, ffi::EVP_PKEY_KEYPAIR)
198+
}
199+
200+
/// Returns the private key seed.
201+
pub fn private_key_seed(&self) -> Result<&[u8], ErrorStack> {
202+
self.params.locate(OSSL_PKEY_PARAM_SEED)?.get_octet_string()
203+
}
204+
205+
/// Returns the private key.
206+
pub fn private_key(&self) -> Result<&[u8], ErrorStack> {
207+
self.params.locate(OSSL_PKEY_PARAM_PRIV_KEY)?.get_octet_string()
208+
}
209+
}
210+
211+
#[cfg(test)]
212+
mod tests {
213+
214+
use super::*;
215+
use crate::signature::Signature;
216+
217+
#[test]
218+
fn test_generate_ml_dsa_44() {
219+
test_generate(Variant::MlDsa44);
220+
}
221+
222+
#[test]
223+
fn test_generate_ml_dsa_65() {
224+
test_generate(Variant::MlDsa65);
225+
}
226+
227+
#[test]
228+
fn test_generate_ml_dsa_87() {
229+
test_generate(Variant::MlDsa87);
230+
}
231+
232+
fn test_generate(variant: Variant) {
233+
let bld = PKeyMlDsaBuilder::<Private>::new_generate(variant).unwrap();
234+
let key = bld.generate().unwrap();
235+
236+
let mut algo = Signature::for_ml_dsa(variant).unwrap();
237+
238+
let data = b"Some Crypto Text";
239+
let bad_data = b"Some Crypto text";
240+
241+
let mut signature = vec![];
242+
let mut ctx = PkeyCtx::new(&key).unwrap();
243+
ctx.sign_message_init(&mut algo).unwrap();
244+
ctx.sign_to_vec(&data[..], &mut signature).unwrap();
245+
246+
// Verify good version with the original PKEY.
247+
ctx.verify_message_init(&mut algo).unwrap();
248+
let valid = ctx.verify(&data[..], &signature);
249+
assert!(matches!(valid, Ok(true)));
250+
assert!(ErrorStack::get().errors().is_empty());
251+
252+
// Verify bad version with the original PKEY.
253+
ctx.verify_message_init(&mut algo).unwrap();
254+
let valid = ctx.verify(&bad_data[..], &signature);
255+
assert!(matches!(valid, Ok(false) | Err(_)));
256+
assert!(ErrorStack::get().errors().is_empty());
257+
258+
// Derive a new PKEY with only the public bits.
259+
let public_params =
260+
PKeyMlDsaParams::<Public>::from_pkey(&key).unwrap();
261+
let key_pub = PKeyMlDsaBuilder::<Public>::new(
262+
variant, public_params.public_key().unwrap(), None).unwrap()
263+
.build().unwrap();
264+
let mut ctx = PkeyCtx::new(&key_pub).unwrap();
265+
let mut algo = Signature::for_ml_dsa(variant).unwrap();
266+
267+
// Verify good version with the public PKEY.
268+
ctx.verify_message_init(&mut algo).unwrap();
269+
let valid = ctx.verify(&data[..], &signature);
270+
assert!(matches!(valid, Ok(true)));
271+
assert!(ErrorStack::get().errors().is_empty());
272+
273+
// Verify bad version with the public PKEY.
274+
ctx.verify_message_init(&mut algo).unwrap();
275+
let valid = ctx.verify(&bad_data[..], &signature);
276+
assert!(matches!(valid, Ok(false) | Err(_)));
277+
assert!(ErrorStack::get().errors().is_empty());
278+
}
279+
}

openssl/src/signature.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,28 @@ impl Clone for Signature {
6060
SignatureRef::to_owned(self)
6161
}
6262
}
63+
64+
impl Signature {
65+
/// Creates a new `Signature` for use with ML-DSA.
66+
pub fn for_ml_dsa(variant: crate::pkey_ml_dsa::Variant)
67+
-> Result<Signature, ErrorStack>
68+
{
69+
unsafe {
70+
Ok(Signature(cvt_p(ffi::EVP_SIGNATURE_fetch(
71+
ptr::null_mut(), variant.as_cstr().as_ptr(), ptr::null()))?))
72+
}
73+
}
74+
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
79+
use super::*;
80+
81+
#[test]
82+
fn test_alloc_free() {
83+
let sig =
84+
Signature::for_ml_dsa(crate::pkey_ml_dsa::Variant::MlDsa44).unwrap();
85+
drop(sig);
86+
}
87+
}

0 commit comments

Comments
 (0)