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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## 0.4.2 - TBD

- Add `CoseKeyBuilder::new_mldsa_pub_key()` helper, with associated `MlDsaVariant` enum.

## 0.4.1 - 2026-01-19

- Bump MSRV to 1.81.
Expand Down
46 changes: 46 additions & 0 deletions src/key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,39 @@ impl core::fmt::Display for ParseSec1OctetStringError {
}
}

/// ML-DSA variants from FIPS 204.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MlDsaVariant {
/// ML-DSA-44.
MlDsa44,
/// ML-DSA-65.
MlDsa65,
/// ML-DSA-87.
MlDsa87,
}

impl From<MlDsaVariant> for iana::Algorithm {
fn from(value: MlDsaVariant) -> iana::Algorithm {
match value {
MlDsaVariant::MlDsa44 => iana::Algorithm::ML_DSA_44,
MlDsaVariant::MlDsa65 => iana::Algorithm::ML_DSA_65,
MlDsaVariant::MlDsa87 => iana::Algorithm::ML_DSA_87,
}
}
}

impl core::convert::TryFrom<iana::Algorithm> for MlDsaVariant {
type Error = CoseError;
fn try_from(alg: iana::Algorithm) -> Result<MlDsaVariant, CoseError> {
match alg {
iana::Algorithm::ML_DSA_44 => Ok(MlDsaVariant::MlDsa44),
iana::Algorithm::ML_DSA_65 => Ok(MlDsaVariant::MlDsa65),
iana::Algorithm::ML_DSA_87 => Ok(MlDsaVariant::MlDsa87),
_ => Err(CoseError::OutOfRangeIntegerValue),
}
}
}

impl CoseKey {
/// Re-order the contents of the key so that the contents will be emitted in one of the standard
/// CBOR sorted orders.
Expand Down Expand Up @@ -362,6 +395,19 @@ impl CoseKeyBuilder {
builder
}

/// Constructor for an ML-DSA public key.
pub fn new_mldsa_pub_key(variant: MlDsaVariant, k: Vec<u8>) -> Self {
Self(CoseKey {
kty: KeyType::Assigned(iana::KeyType::AKP),
alg: Some(Algorithm::Assigned(variant.into())),
params: vec![(
Label::Int(iana::AkpKeyParameter::Pub as i64),
Value::Bytes(k),
)],
..Default::default()
})
}

/// Constructor for a symmetric key specified by `k`.
pub fn new_symmetric_key(k: Vec<u8>) -> Self {
Self(CoseKey {
Expand Down
37 changes: 37 additions & 0 deletions src/key/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use super::*;
use crate::{cbor::value::Value, iana, util::expect_err, CborOrdering, CborSerializable};
use alloc::{borrow::ToOwned, format, string::ToString, vec};
use core::convert::TryFrom;

#[test]
fn test_cose_key_encode() {
Expand Down Expand Up @@ -807,6 +808,18 @@ fn test_key_builder() {
..Default::default()
},
),
(
CoseKeyBuilder::new_mldsa_pub_key(MlDsaVariant::MlDsa65, vec![1, 2, 3]).build(),
CoseKey {
kty: KeyType::Assigned(iana::KeyType::AKP),
alg: Some(Algorithm::Assigned(iana::Algorithm::ML_DSA_65)),
params: vec![(
Label::Int(iana::AkpKeyParameter::Pub as i64),
Value::Bytes(vec![1, 2, 3]),
)],
..Default::default()
},
),
];
for (got, want) in tests {
assert_eq!(got, want);
Expand Down Expand Up @@ -1159,3 +1172,27 @@ fn test_key_to_sec1_octet_string() {
}
}
}

#[test]
fn test_mldsa_variant_convert() {
let tests = [
(MlDsaVariant::MlDsa44, iana::Algorithm::ML_DSA_44),
(MlDsaVariant::MlDsa65, iana::Algorithm::ML_DSA_65),
(MlDsaVariant::MlDsa87, iana::Algorithm::ML_DSA_87),
];
for (variant, alg) in tests {
let Ok(got) = MlDsaVariant::try_from(alg) else {
panic!("conversion failed")
};
assert_eq!(got, variant, "for {alg:?}");

let got = iana::Algorithm::from(variant);
assert_eq!(got, alg, "for {variant:?}");
}
}

#[test]
fn test_mldsa_variant_convert_fail() {
let result = MlDsaVariant::try_from(iana::Algorithm::A256GCM);
assert!(matches!(result, Err(CoseError::OutOfRangeIntegerValue)));
}
Loading