Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pem = { version = "3", optional = true }
simple_asn1 = { version = "0.6", optional = true }

# "aws_lc_rs" feature
aws-lc-rs = { version = "1.15.0", optional = true }
aws-lc-rs = { version = "1.18.0", optional = true }

# "rust_crypto" feature
ed25519-dalek = { version = "2.1.1", optional = true, features = ["pkcs8"] }
Expand All @@ -46,10 +46,15 @@ rand = { version = "0.8.5", optional = true, features = [
rsa = { version = "0.9.6", optional = true }
sha2 = { version = "0.10.7", optional = true, features = ["oid"] }
zeroize = { version = "1.8.2", features = ["derive"] }
ml-dsa = { version = "0.1", optional = true, features = ["pkcs8", "rand_core", "zeroize"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3"
getrandom = "0.2"

# Two getrandom versions for wasm: 0.2 for ed25519-dalek,
# p256/p384, rsa and 0.4 for ml-dsa. Each needs its own wasm feature
getrandom = { version = "0.2", features = ["js"] }
getrandom_v04 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }

[dev-dependencies]
wasm-bindgen-test = "0.3.1"
Expand Down Expand Up @@ -77,6 +82,7 @@ rust_crypto = [
"dep:rand",
"dep:rsa",
"dep:sha2",
"dep:ml-dsa",
]
aws_lc_rs = ["dep:aws-lc-rs"]

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ This library currently supports the following:
- ES256
- ES384
- EdDSA
- ML-DSA-44
- ML-DSA-65
- ML-DSA-87


## How to use
Expand Down
72 changes: 72 additions & 0 deletions src/algorithms.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::errors::{Error, ErrorKind, Result};

/// Public-key lengths (in bytes) for the ML-DSA parameter sets, as fixed by
/// US NIST FIPS 204.
pub(crate) const ML_DSA_44_PUBLIC_KEY_LEN: usize = 1312;
pub(crate) const ML_DSA_65_PUBLIC_KEY_LEN: usize = 1952;
pub(crate) const ML_DSA_87_PUBLIC_KEY_LEN: usize = 2592;

#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
/// Supported families of algorithms.
pub enum AlgorithmFamily {
Expand All @@ -15,6 +22,8 @@ pub enum AlgorithmFamily {
Ec,
/// Edwards curve public key family.
Ed,
/// ML-DSA public key family.
Mldsa,
}

impl AlgorithmFamily {
Expand All @@ -32,6 +41,7 @@ impl AlgorithmFamily {
],
Self::Ec => &[Algorithm::ES256, Algorithm::ES384],
Self::Ed => &[Algorithm::EdDSA],
Self::Mldsa => &[Algorithm::MLDSA44, Algorithm::MLDSA65, Algorithm::MLDSA87],
}
}
}
Expand Down Expand Up @@ -70,6 +80,16 @@ pub enum Algorithm {

/// Edwards-curve Digital Signature Algorithm (EdDSA)
EdDSA,

/// ML-DSA-44 as described in US NIST FIPS 204
#[serde(rename = "ML-DSA-44")]
MLDSA44,
/// ML-DSA-65 as described in US NIST FIPS 204
#[serde(rename = "ML-DSA-65")]
MLDSA65,
/// ML-DSA-87 as described in US NIST FIPS 204
#[serde(rename = "ML-DSA-87")]
MLDSA87,
}

impl FromStr for Algorithm {
Expand All @@ -88,11 +108,37 @@ impl FromStr for Algorithm {
"PS512" => Ok(Algorithm::PS512),
"RS512" => Ok(Algorithm::RS512),
"EdDSA" => Ok(Algorithm::EdDSA),
"ML-DSA-44" => Ok(Algorithm::MLDSA44),
"ML-DSA-65" => Ok(Algorithm::MLDSA65),
"ML-DSA-87" => Ok(Algorithm::MLDSA87),
_ => Err(ErrorKind::InvalidAlgorithmName.into()),
}
}
}

impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Algorithm::HS256 => "HS256",
Algorithm::HS384 => "HS384",
Algorithm::HS512 => "HS512",
Algorithm::ES256 => "ES256",
Algorithm::ES384 => "ES384",
Algorithm::RS256 => "RS256",
Algorithm::RS384 => "RS384",
Algorithm::RS512 => "RS512",
Algorithm::PS256 => "PS256",
Algorithm::PS384 => "PS384",
Algorithm::PS512 => "PS512",
Algorithm::EdDSA => "EdDSA",
Algorithm::MLDSA44 => "ML-DSA-44",
Algorithm::MLDSA65 => "ML-DSA-65",
Algorithm::MLDSA87 => "ML-DSA-87",
};
f.write_str(s)
}
}

impl Algorithm {
/// The family of the algorithm.
pub fn family(self) -> AlgorithmFamily {
Expand All @@ -106,6 +152,7 @@ impl Algorithm {
| Algorithm::PS512 => AlgorithmFamily::Rsa,
Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec,
Algorithm::EdDSA => AlgorithmFamily::Ed,
Algorithm::MLDSA44 | Algorithm::MLDSA65 | Algorithm::MLDSA87 => AlgorithmFamily::Mldsa,
}
}
}
Expand All @@ -128,6 +175,31 @@ mod tests {
assert!(Algorithm::from_str("PS256").is_ok());
assert!(Algorithm::from_str("PS384").is_ok());
assert!(Algorithm::from_str("PS512").is_ok());
assert!(Algorithm::from_str("EdDSA").is_ok());
assert!(Algorithm::from_str("ML-DSA-44").is_ok());
assert!(Algorithm::from_str("ML-DSA-65").is_ok());
assert!(Algorithm::from_str("ML-DSA-87").is_ok());
assert!(Algorithm::from_str("").is_err());
}

#[test]
#[wasm_bindgen_test]
fn ml_dsa_wire_format_roundtrip() {
// Locks the JWT `alg` header wire-format for ML-DSA variants
// (RFC 9964 names use hyphens, not the Rust identifier spelling).
let pairs = [
(Algorithm::MLDSA44, "ML-DSA-44"),
(Algorithm::MLDSA65, "ML-DSA-65"),
(Algorithm::MLDSA87, "ML-DSA-87"),
];

for (alg, wire) in pairs {
// Serialize -> exact wire string.
assert_eq!(serde_json::to_string(&alg).unwrap(), format!("\"{wire}\""));
// Deserialize -> back to the same variant.
assert_eq!(serde_json::from_str::<Algorithm>(&format!("\"{wire}\"")).unwrap(), alg);
// FromStr round-trip.
assert_eq!(Algorithm::from_str(wire).unwrap(), alg);
}
}
}
90 changes: 90 additions & 0 deletions src/crypto/aws_lc/ml_dsa.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Implementations of the [`JwtSigner`] and [`JwtVerifier`] traits for the
//! ML-DSA family of algorithms (US NIST FIPS 204) using [`aws_lc_rs`]

use crate::algorithms::AlgorithmFamily;
use crate::crypto::{JwtSigner, JwtVerifier};
use crate::errors::{ErrorKind, Result, new_error};
use crate::{Algorithm, DecodingKey, EncodingKey};
use aws_lc_rs::signature::{
ML_DSA_44, ML_DSA_44_SIGNING, ML_DSA_65, ML_DSA_65_SIGNING, ML_DSA_87, ML_DSA_87_SIGNING,
PqdsaKeyPair, VerificationAlgorithm,
};
use signature::{Error, Signer, Verifier};

macro_rules! define_ml_dsa_signer {
($name:ident, $alg:expr, $signing_alg:expr) => {
pub struct $name(PqdsaKeyPair);

impl $name {
pub(crate) fn new(encoding_key: &EncodingKey) -> Result<Self> {
if encoding_key.family() != AlgorithmFamily::Mldsa {
return Err(new_error(ErrorKind::InvalidKeyFormat));
}

Ok(Self(
PqdsaKeyPair::from_pkcs8($signing_alg, encoding_key.as_bytes())
.map_err(|_| ErrorKind::InvalidKeyFormat)?,
))
}
}

impl Signer<Vec<u8>> for $name {
fn try_sign(&self, msg: &[u8]) -> std::result::Result<Vec<u8>, Error> {
let mut signature = vec![0u8; self.0.algorithm().signature_len()];
let len = self.0.sign(msg, &mut signature).map_err(Error::from_source)?;
signature.truncate(len);
Ok(signature)
}
}

impl JwtSigner for $name {
fn algorithm(&self) -> Algorithm {
$alg
}
}
};
}

macro_rules! define_ml_dsa_verifier {
($name:ident, $alg:expr, $verification_alg:expr) => {
pub struct $name(DecodingKey);

impl $name {
pub(crate) fn new(decoding_key: &DecodingKey) -> Result<Self> {
if decoding_key.family() != AlgorithmFamily::Mldsa {
return Err(new_error(ErrorKind::InvalidKeyFormat));
}

Ok(Self(decoding_key.clone()))
}
}

impl Verifier<Vec<u8>> for $name {
fn verify(&self, msg: &[u8], signature: &Vec<u8>) -> std::result::Result<(), Error> {
$verification_alg
.verify_sig(
self.0.try_get_as_bytes().map_err(Error::from_source)?,
msg,
signature,
)
.map_err(Error::from_source)?;
Ok(())
}
}

impl JwtVerifier for $name {
fn algorithm(&self) -> Algorithm {
$alg
}
}
};
}

define_ml_dsa_signer!(MlDsa44Signer, Algorithm::MLDSA44, &ML_DSA_44_SIGNING);
define_ml_dsa_verifier!(MlDsa44Verifier, Algorithm::MLDSA44, ML_DSA_44);

define_ml_dsa_signer!(MlDsa65Signer, Algorithm::MLDSA65, &ML_DSA_65_SIGNING);
define_ml_dsa_verifier!(MlDsa65Verifier, Algorithm::MLDSA65, ML_DSA_65);

define_ml_dsa_signer!(MlDsa87Signer, Algorithm::MLDSA87, &ML_DSA_87_SIGNING);
define_ml_dsa_verifier!(MlDsa87Verifier, Algorithm::MLDSA87, ML_DSA_87);
28 changes: 27 additions & 1 deletion src/crypto/aws_lc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use aws_lc_rs::{
digest,
signature::{
self as aws_sig, ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING,
EcdsaKeyPair, Ed25519KeyPair, KeyPair,
EcdsaKeyPair, Ed25519KeyPair, KeyPair, ML_DSA_44_SIGNING, ML_DSA_65_SIGNING,
ML_DSA_87_SIGNING, PqdsaKeyPair,
},
};

Expand All @@ -16,6 +17,7 @@ use crate::{
mod ecdsa;
mod eddsa;
mod hmac;
mod ml_dsa;
mod rsa;

fn rsa_components_from_private_key(key_content: &[u8]) -> errors::Result<(Vec<u8>, Vec<u8>)> {
Expand Down Expand Up @@ -70,6 +72,23 @@ fn ed_pub_components_from_private_key(
}
}

fn mldsa_pub_components_from_private_key(
encoding_key: &[u8],
alg: Algorithm,
) -> errors::Result<Vec<u8>> {
let signing_alg = match alg {
Algorithm::MLDSA44 => &ML_DSA_44_SIGNING,
Algorithm::MLDSA65 => &ML_DSA_65_SIGNING,
Algorithm::MLDSA87 => &ML_DSA_87_SIGNING,
_ => return Err(ErrorKind::InvalidAlgorithm.into()),
};

let key_pair = PqdsaKeyPair::from_pkcs8(signing_alg, encoding_key)
.map_err(|_| ErrorKind::InvalidKeyFormat)?;

Ok(key_pair.public_key().as_ref().to_vec())
}

fn compute_digest(data: &[u8], hash_function: ThumbprintHash) -> errors::Result<Vec<u8>> {
let algorithm = match hash_function {
ThumbprintHash::SHA256 => &digest::SHA256,
Expand All @@ -93,6 +112,9 @@ fn new_signer(algorithm: &Algorithm, key: &EncodingKey) -> Result<Box<dyn JwtSig
Algorithm::PS384 => Box::new(rsa::RsaPss384Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::PS512 => Box::new(rsa::RsaPss512Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::EdDSA => Box::new(eddsa::EdDSASigner::new(key)?) as Box<dyn JwtSigner>,
Algorithm::MLDSA44 => Box::new(ml_dsa::MlDsa44Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::MLDSA65 => Box::new(ml_dsa::MlDsa65Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::MLDSA87 => Box::new(ml_dsa::MlDsa87Signer::new(key)?) as Box<dyn JwtSigner>,
};

Ok(jwt_signer)
Expand All @@ -115,6 +137,9 @@ fn new_verifier(
Algorithm::PS384 => Box::new(rsa::RsaPss384Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::PS512 => Box::new(rsa::RsaPss512Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::EdDSA => Box::new(eddsa::EdDSAVerifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::MLDSA44 => Box::new(ml_dsa::MlDsa44Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::MLDSA65 => Box::new(ml_dsa::MlDsa65Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::MLDSA87 => Box::new(ml_dsa::MlDsa87Verifier::new(key)?) as Box<dyn JwtVerifier>,
};

Ok(jwt_verifier)
Expand All @@ -129,6 +154,7 @@ pub static DEFAULT_PROVIDER: CryptoProvider = CryptoProvider {
rsa_pub_components_from_public_key: rsa_components_from_public_key,
ec_pub_components_from_private_key: ec_components_from_private_key,
ed_pub_components_from_private_key,
mldsa_pub_components_from_private_key,
compute_digest,
},
};
6 changes: 6 additions & 0 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ pub struct KeyUtils {
fn(&[u8], Algorithm) -> Result<(EllipticCurve, Vec<u8>, Vec<u8>)>,
/// Given a DER encoded private key and the curve type, extract the ED public key component (x)
pub ed_pub_components_from_private_key: fn(&[u8], &EllipticCurve) -> Result<Vec<u8>>,
/// Given a PKCS#8 DER encoded ML-DSA private key, extract the raw ML-DSA public key
/// (the fixed-size encoding used by RFC 9964).
pub mldsa_pub_components_from_private_key: fn(&[u8], Algorithm) -> Result<Vec<u8>>,
/// Given some data and a name of a hash function, compute hash_function(data)
pub compute_digest: fn(&[u8], ThumbprintHash) -> Result<Vec<u8>>,
}
Expand All @@ -174,6 +177,9 @@ See the documentation of the CryptoProvider type for more information.
ed_pub_components_from_private_key: |_, _| {
panic!("{}", NOT_INSTALLED_OR_UNIMPLEMENTED_ERROR)
},
mldsa_pub_components_from_private_key: |_, _| {
panic!("{}", NOT_INSTALLED_OR_UNIMPLEMENTED_ERROR)
},
compute_digest: |_, _| panic!("{}", NOT_INSTALLED_OR_UNIMPLEMENTED_ERROR),
}
}
Expand Down
Loading
Loading