diff --git a/CHANGELOG.md b/CHANGELOG.md index b5306ed..222f1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/key/mod.rs b/src/key/mod.rs index ba1af82..f69b9fc 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -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 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 for MlDsaVariant { + type Error = CoseError; + fn try_from(alg: iana::Algorithm) -> Result { + 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. @@ -362,6 +395,19 @@ impl CoseKeyBuilder { builder } + /// Constructor for an ML-DSA public key. + pub fn new_mldsa_pub_key(variant: MlDsaVariant, k: Vec) -> 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) -> Self { Self(CoseKey { diff --git a/src/key/tests.rs b/src/key/tests.rs index 5ead5f7..98fcc24 100644 --- a/src/key/tests.rs +++ b/src/key/tests.rs @@ -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() { @@ -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); @@ -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))); +}