From f46e4f05c05fb7cce9e6729d0637af49e70f9a18 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Fri, 17 Apr 2026 07:23:12 +0000 Subject: [PATCH] feat: CoseAlgorithm enum with RFC 9864 fully-specified identifiers Add typed CoseAlgorithm enum replacing raw i64 for the COSE alg field in ProtectedCorimHeaderMap and SignedCorimBuilder. New fully-specified algorithms per RFC 9864: - ESP256 (-9), ESP384 (-51), ESP512 (-52) replace ES256/ES384/ES512 - Ed25519 (-19), Ed448 (-53) replace EdDSA Deprecated polymorphic algorithms retained for decode interop: - ES256 (-7), ES384 (-35), ES512 (-36), EdDSA (-8) - is_deprecated() method identifies these PS256/PS384/PS512 unchanged (not deprecated per RFC 9864 s6.1). Unknown(i64) variant for forward compatibility. SignedCorimBuilder::new() accepts impl Into. CLI: uses CoseAlgorithm::Display, removed duplicate cose_alg_name. RFC_REFERENCES.md: added RFC 9864 section. 631 tests, 0 clippy warnings. --- RFC_REFERENCES.md | 32 +++++- corim-cli/src/main.rs | 23 +---- corim/src/types/mod.rs | 4 +- corim/src/types/signed.rs | 158 ++++++++++++++++++++++++++++-- corim/tests/signed_corim_tests.rs | 34 +++---- 5 files changed, 203 insertions(+), 48 deletions(-) diff --git a/RFC_REFERENCES.md b/RFC_REFERENCES.md index 699974f..04ab0e7 100644 --- a/RFC_REFERENCES.md +++ b/RFC_REFERENCES.md @@ -6,7 +6,7 @@ This document tracks all RFCs and Internet-Drafts referenced by the `corim` crat - The implementation adds support for a new specification - An RFC errata affects our implementation -**Last reviewed**: April 13, 2026 +**Last reviewed**: April 17, 2026 --- @@ -181,11 +181,41 @@ This is an **Internet-Draft**, not a finalized RFC. Changes to watch for: | Registry | URL | Used in | |----------|-----|---------| | CBOR Tags | https://www.iana.org/assignments/cbor-tags | `types/tags.rs` — tags 1, 18, 37, 111, 501, 505, 506, 508, 550–564 | +| COSE Algorithms | https://www.iana.org/assignments/cose/cose.xhtml#algorithms | `types/signed.rs` → `CoseAlgorithm` enum (RFC 9864 fully-specified identifiers) | | Named Information Hash Algorithm | https://www.iana.org/assignments/named-information | `types/measurement.rs` — `Digest` algorithm IDs | | CoSWID Items | https://www.iana.org/assignments/coswid | `types/tags.rs` — CoSWID key indices 0–57 | --- +### RFC 9864 — Fully-Specified Algorithms for JOSE and COSE + +| | | +|-|-| +| **Status** | Standards Track — **Stable** (October 2025) | +| **URL** | https://www.rfc-editor.org/rfc/rfc9864.html | +| **Updates** | RFC 7518, RFC 8037, RFC 9053 | +| **Used in** | `types/signed.rs` → `CoseAlgorithm` enum | + +#### Impact on this crate + +RFC 9864 deprecates polymorphic COSE algorithm identifiers and defines +fully-specified replacements: + +| Deprecated | Value | Replacement | Value | Status in our enum | +|-----------|-------|-------------|-------|--------------------| +| ES256 | -7 | ESP256 | -9 | Both modeled; ES256 marked deprecated | +| ES384 | -35 | ESP384 | -51 | Both modeled; ES384 marked deprecated | +| ES512 | -36 | ESP512 | -52 | Both modeled; ES512 marked deprecated | +| EdDSA | -8 | Ed25519 / Ed448 | -19 / -53 | Both modeled; EdDSA marked deprecated | + +PS256/PS384/PS512 are NOT deprecated by RFC 9864 (§6.1). + +The deprecated variants are retained in `CoseAlgorithm` for decode interop +with existing signed CoRIM documents. `CoseAlgorithm::is_deprecated()` +returns `true` for the old polymorphic identifiers. + +--- + ## How to Update This Document When a new revision of `draft-ietf-rats-corim` is published: diff --git a/corim-cli/src/main.rs b/corim-cli/src/main.rs index c7bd18e..4803399 100644 --- a/corim-cli/src/main.rs +++ b/corim-cli/src/main.rs @@ -203,7 +203,7 @@ fn main() { /// Information extracted from a signed CoRIM's COSE_Sign1 wrapper. struct SignedInfo { - alg: i64, + alg: corim::types::signed::CoseAlgorithm, signer_name: Option, content_type: Option, signature_len: usize, @@ -282,7 +282,7 @@ fn try_decode_signed( fn print_signed_header_only(info: &SignedInfo) { println!("✓ Signed CoRIM (tag 18) — header decoded\n"); println!("═══ COSE_Sign1 Header ═══"); - println!(" Algorithm: {}", cose_alg_name(info.alg)); + println!(" Algorithm: {}", info.alg); if let Some(ref ct) = info.content_type { println!(" Content-Type: {}", ct); } @@ -364,7 +364,7 @@ fn print_text_output( // Show signed CoRIM info if present if let Some(ref info) = signed_info { println!(" [SIGNED] COSE_Sign1 (tag 18)"); - println!(" Algorithm: {}", cose_alg_name(info.alg)); + println!(" Algorithm: {}", info.alg); if let Some(ref ct) = info.content_type { println!(" Content-Type: {}", ct); } @@ -479,20 +479,3 @@ fn json_escape(s: &str) -> String { .replace('"', "\\\"") .replace('\n', "\\n") } - -/// Map a COSE algorithm identifier to a human-readable name. -fn cose_alg_name(alg: i64) -> String { - match alg { - -7 => "ES256 (-7)".into(), - -35 => "ES384 (-35)".into(), - -36 => "ES512 (-36)".into(), - -37 => "PS256 (-37)".into(), - -38 => "PS384 (-38)".into(), - -39 => "PS512 (-39)".into(), - -257 => "PS256 (-257)".into(), - -258 => "PS384 (-258)".into(), - -259 => "PS512 (-259)".into(), - -65535 => "HSS-LMS (-65535)".into(), - other => format!("{}", other), - } -} diff --git a/corim/src/types/mod.rs b/corim/src/types/mod.rs index f17b956..59d9b40 100644 --- a/corim/src/types/mod.rs +++ b/corim/src/types/mod.rs @@ -36,7 +36,9 @@ pub use self::measurement::{ Digest, FlagsMap, IntRangeChoice, IntegrityRegisters, IpAddr, MacAddr, MeasurementMap, MeasurementValuesMap, RawValueChoice, SvnChoice, }; -pub use self::signed::{CoseSign1Corim, CwtClaims, ProtectedCorimHeaderMap, SignedCorimBuilder}; +pub use self::signed::{ + CoseAlgorithm, CoseSign1Corim, CwtClaims, ProtectedCorimHeaderMap, SignedCorimBuilder, +}; pub use self::triples::{ AttestKeyTriple, CesCondition, ConditionalEndorsementSeriesTriple, ConditionalEndorsementTriple, ConditionalSeriesRecord, CoswidTriple, DomainDependencyTriple, diff --git a/corim/src/types/signed.rs b/corim/src/types/signed.rs index 70f3581..fc24643 100644 --- a/corim/src/types/signed.rs +++ b/corim/src/types/signed.rs @@ -30,6 +30,144 @@ use crate::cbor; use crate::cbor::value::Value; use crate::Validate; +// =================================================================== +// COSE Algorithm Identifiers (IANA "COSE Algorithms" registry) +// Updated per RFC 9864 — fully-specified algorithm identifiers. +// =================================================================== + +/// COSE signing algorithm identifier per +/// [IANA COSE Algorithms](https://www.iana.org/assignments/cose/cose.xhtml#algorithms), +/// updated by [RFC 9864](https://www.rfc-editor.org/rfc/rfc9864.html). +/// +/// RFC 9864 deprecates polymorphic algorithm identifiers (ES256, ES384, +/// ES512, EdDSA) and defines fully-specified replacements (ESP256, ESP384, +/// ESP512, Ed25519, Ed448). The deprecated variants are retained for +/// decode interop but marked with `#[deprecated]` doc attributes. +/// +/// Used in the `alg` (key 1) field of the COSE_Sign1 protected header. +/// The `Unknown` variant provides forward compatibility with algorithm +/// identifiers not yet modeled here. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum CoseAlgorithm { + // --- Fully-specified algorithms (RFC 9864 §2) --- + /// ESP256 (-9) — ECDSA using P-256 curve and SHA-256. Replaces ES256. + Esp256, + /// Ed25519 (-19) — EdDSA using the Ed25519 parameter set. Replaces EdDSA. + Ed25519, + /// PS256 (-37) — RSASSA-PSS w/ SHA-256. + Ps256, + /// PS384 (-38) — RSASSA-PSS w/ SHA-384. + Ps384, + /// PS512 (-39) — RSASSA-PSS w/ SHA-512. + Ps512, + /// ESP384 (-51) — ECDSA using P-384 curve and SHA-384. Replaces ES384. + Esp384, + /// ESP512 (-52) — ECDSA using P-521 curve and SHA-512. Replaces ES512. + Esp512, + /// Ed448 (-53) — EdDSA using the Ed448 parameter set. Replaces EdDSA. + Ed448, + + // --- Deprecated polymorphic algorithms (RFC 9864 §4.2.2) --- + // Retained for decode interop with existing signed CoRIM documents. + /// ES256 (-7) — **Deprecated per RFC 9864.** Use [`Esp256`](Self::Esp256). + Es256, + /// EdDSA (-8) — **Deprecated per RFC 9864.** Use [`Ed25519`](Self::Ed25519) or [`Ed448`](Self::Ed448). + EdDsa, + /// ES384 (-35) — **Deprecated per RFC 9864.** Use [`Esp384`](Self::Esp384). + Es384, + /// ES512 (-36) — **Deprecated per RFC 9864.** Use [`Esp512`](Self::Esp512). + Es512, + + /// An algorithm identifier not explicitly modeled above. + Unknown(i64), +} + +impl CoseAlgorithm { + /// Convert from the IANA integer identifier. + pub fn from_i64(n: i64) -> Self { + match n { + -7 => Self::Es256, + -8 => Self::EdDsa, + -9 => Self::Esp256, + -19 => Self::Ed25519, + -35 => Self::Es384, + -36 => Self::Es512, + -37 => Self::Ps256, + -38 => Self::Ps384, + -39 => Self::Ps512, + -51 => Self::Esp384, + -52 => Self::Esp512, + -53 => Self::Ed448, + other => Self::Unknown(other), + } + } + + /// Convert to the IANA integer identifier. + pub fn to_i64(self) -> i64 { + match self { + Self::Es256 => -7, + Self::EdDsa => -8, + Self::Esp256 => -9, + Self::Ed25519 => -19, + Self::Es384 => -35, + Self::Es512 => -36, + Self::Ps256 => -37, + Self::Ps384 => -38, + Self::Ps512 => -39, + Self::Esp384 => -51, + Self::Esp512 => -52, + Self::Ed448 => -53, + Self::Unknown(n) => n, + } + } + + /// Human-readable name for display. + pub fn name(&self) -> &'static str { + match self { + Self::Esp256 => "ESP256", + Self::Ed25519 => "Ed25519", + Self::Ps256 => "PS256", + Self::Ps384 => "PS384", + Self::Ps512 => "PS512", + Self::Esp384 => "ESP384", + Self::Esp512 => "ESP512", + Self::Ed448 => "Ed448", + Self::Es256 => "ES256 (deprecated)", + Self::EdDsa => "EdDSA (deprecated)", + Self::Es384 => "ES384 (deprecated)", + Self::Es512 => "ES512 (deprecated)", + Self::Unknown(_) => "Unknown", + } + } + + /// Returns `true` if this algorithm is deprecated per RFC 9864. + pub fn is_deprecated(&self) -> bool { + matches!(self, Self::Es256 | Self::EdDsa | Self::Es384 | Self::Es512) + } +} + +impl core::fmt::Display for CoseAlgorithm { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Unknown(n) => write!(f, "Unknown({})", n), + _ => write!(f, "{} ({})", self.name(), self.to_i64()), + } + } +} + +impl From for CoseAlgorithm { + fn from(n: i64) -> Self { + Self::from_i64(n) + } +} + +impl From for i64 { + fn from(alg: CoseAlgorithm) -> Self { + alg.to_i64() + } +} + // =================================================================== // COSE Header Label Constants (RFC 9052 / draft-ietf-rats-corim-10 §4.2) // =================================================================== @@ -260,7 +398,7 @@ fn value_to_epoch(v: &Value) -> Result { #[derive(Clone, Debug, PartialEq)] pub struct ProtectedCorimHeaderMap { /// COSE algorithm identifier (key 1). - pub alg: i64, + pub alg: CoseAlgorithm, /// Content type (key 3) — present for inline signing. /// Should be "application/rim+cbor" per the spec. pub content_type: Option, @@ -316,7 +454,7 @@ impl Serialize for ProtectedCorimHeaderMap { count += self.extra.len(); let mut map = s.serialize_map(Some(count))?; - map.serialize_entry(&COSE_HEADER_ALG, &self.alg)?; + map.serialize_entry(&COSE_HEADER_ALG, &self.alg.to_i64())?; if let Some(ref ct) = self.content_type { map.serialize_entry(&COSE_HEADER_CONTENT_TYPE, ct)?; } @@ -504,8 +642,9 @@ impl<'de> Deserialize<'de> for ProtectedCorimHeaderMap { } } - let alg = - alg.ok_or_else(|| serde::de::Error::custom("protected header: missing alg (key 1)"))?; + let alg = alg + .ok_or_else(|| serde::de::Error::custom("protected header: missing alg (key 1)"))? + .into(); // If CWT claims were found flat in the header (not under key 15), // synthesize a CwtClaims struct from the individual fields. @@ -928,7 +1067,7 @@ pub fn validate_signed_corim_payload_detached( /// ``` #[must_use] pub struct SignedCorimBuilder { - alg: i64, + alg: CoseAlgorithm, content_type: String, corim_meta: Option, cwt_claims: Option, @@ -942,14 +1081,15 @@ pub struct SignedCorimBuilder { impl SignedCorimBuilder { /// Create a new builder with the specified COSE algorithm and CoRIM payload bytes. /// - /// The `alg` parameter is the COSE algorithm identifier (e.g., -7 for ES256, - /// -35 for ES384, -36 for ES512, -257 for PS256). + /// The `alg` parameter is the COSE algorithm identifier. Use + /// [`CoseAlgorithm`] variants (e.g., `CoseAlgorithm::Es256`) or convert + /// from an integer with `.into()` (e.g., `(-7i64).into()`). /// /// The `corim_payload` must be the CBOR-encoded `tagged-unsigned-corim-map` /// (i.e., tag-501-wrapped bytes as produced by [`crate::builder::CorimBuilder::build_bytes`]). - pub fn new(alg: i64, corim_payload: Vec) -> Self { + pub fn new(alg: impl Into, corim_payload: Vec) -> Self { Self { - alg, + alg: alg.into(), content_type: CORIM_CONTENT_TYPE.into(), corim_meta: None, cwt_claims: None, diff --git a/corim/tests/signed_corim_tests.rs b/corim/tests/signed_corim_tests.rs index 2adf0eb..e02f196 100644 --- a/corim/tests/signed_corim_tests.rs +++ b/corim/tests/signed_corim_tests.rs @@ -123,7 +123,7 @@ fn cwt_claims_missing_iss_fails() { fn protected_header_with_corim_meta_round_trip() { let meta = make_corim_meta(); let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -136,7 +136,7 @@ fn protected_header_with_corim_meta_round_trip() { let bytes = cbor::encode(&header).unwrap(); let decoded: ProtectedCorimHeaderMap = cbor::decode(&bytes).unwrap(); - assert_eq!(decoded.alg, -7); + assert_eq!(decoded.alg, CoseAlgorithm::Es256); assert_eq!( decoded.content_type.as_deref(), Some("application/rim+cbor") @@ -154,7 +154,7 @@ fn protected_header_with_corim_meta_round_trip() { fn protected_header_with_cwt_claims_round_trip() { let claims = make_cwt_claims(); let header = ProtectedCorimHeaderMap { - alg: -35, + alg: CoseAlgorithm::Es384, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -167,7 +167,7 @@ fn protected_header_with_cwt_claims_round_trip() { let bytes = cbor::encode(&header).unwrap(); let decoded: ProtectedCorimHeaderMap = cbor::decode(&bytes).unwrap(); - assert_eq!(decoded.alg, -35); + assert_eq!(decoded.alg, CoseAlgorithm::Es384); assert!(decoded.cwt_claims.is_some()); let dc = decoded.cwt_claims.unwrap(); assert_eq!(dc.iss, "ACME Corp"); @@ -178,7 +178,7 @@ fn protected_header_with_cwt_claims_round_trip() { #[test] fn protected_header_with_both_meta_and_cwt() { let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -229,7 +229,7 @@ fn protected_header_missing_alg_fails_decode() { #[test] fn protected_header_validate_inline_mode() { let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -244,7 +244,7 @@ fn protected_header_validate_inline_mode() { #[test] fn protected_header_validate_inline_missing_content_type() { let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: None, payload_hash_alg: None, payload_preimage_content_type: None, @@ -259,7 +259,7 @@ fn protected_header_validate_inline_missing_content_type() { #[test] fn protected_header_validate_hash_envelope_mode() { let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: None, payload_hash_alg: Some(1), // SHA-256 payload_preimage_content_type: Some("application/rim+cbor".into()), @@ -277,7 +277,7 @@ fn protected_header_validate_meta_cwt_mismatch() { let mut meta = make_corim_meta(); meta.signer.signer_name = "Different Corp".into(); let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -335,7 +335,7 @@ fn signed_corim_builder_with_corim_meta() { // Decode it back let signed = decode_signed_corim(&signed_bytes).unwrap(); - assert_eq!(signed.protected.alg, -35); + assert_eq!(signed.protected.alg, CoseAlgorithm::Es384); assert!(signed.protected.corim_meta.is_some()); assert_eq!( signed @@ -424,7 +424,7 @@ fn decode_signed_corim_valid() { let signed_bytes = builder.build_with_signature(vec![0x42; 64]).unwrap(); let signed = decode_signed_corim(&signed_bytes).unwrap(); - assert_eq!(signed.protected.alg, -7); + assert_eq!(signed.protected.alg, CoseAlgorithm::Es256); assert!(signed.payload.is_some()); assert_eq!(signed.signature, vec![0x42; 64]); } @@ -562,7 +562,7 @@ fn encode_decode_round_trip() { let corim_bytes = build_sample_corim_bytes(); let protected = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -584,7 +584,7 @@ fn encode_decode_round_trip() { let encoded = encode_signed_corim(&signed).unwrap(); let decoded = decode_signed_corim(&encoded).unwrap(); - assert_eq!(decoded.protected.alg, -7); + assert_eq!(decoded.protected.alg, CoseAlgorithm::Es256); assert_eq!(decoded.protected_header_bytes, protected_header_bytes); assert_eq!(decoded.signature, vec![0xEE; 64]); assert!(decoded.payload.is_some()); @@ -686,7 +686,7 @@ fn full_signed_corim_workflow() { // Step 6: Decode the signed CoRIM let signed = decode_signed_corim(&signed_bytes).unwrap(); - assert_eq!(signed.protected.alg, -7); + assert_eq!(signed.protected.alg, CoseAlgorithm::Es256); assert!(signed.protected.cwt_claims.is_some()); assert_eq!(signed.signature, fake_signature); @@ -718,7 +718,7 @@ fn full_workflow_with_corim_meta() { let signed_bytes = builder.build_with_signature(vec![0x99; 48]).unwrap(); let signed = decode_signed_corim(&signed_bytes).unwrap(); - assert_eq!(signed.protected.alg, -35); + assert_eq!(signed.protected.alg, CoseAlgorithm::Es384); let meta = signed.protected.corim_meta.as_ref().unwrap(); assert_eq!(meta.signer.signer_name, "Meta Signer"); @@ -733,7 +733,7 @@ fn full_workflow_with_corim_meta() { #[test] fn protected_header_extra_fields_preserved() { let header = ProtectedCorimHeaderMap { - alg: -7, + alg: CoseAlgorithm::Es256, content_type: Some("application/rim+cbor".into()), payload_hash_alg: None, payload_preimage_content_type: None, @@ -987,7 +987,7 @@ fn full_detached_workflow() { // Step 4: Decode the envelope let envelope = decode_signed_corim(&envelope_bytes).unwrap(); assert!(envelope.is_detached()); - assert_eq!(envelope.protected.alg, -35); + assert_eq!(envelope.protected.alg, CoseAlgorithm::Es384); // Step 5: Reconstruct TBS from envelope + detached payload let tbs2 = envelope.to_be_signed_detached(&corim_bytes, &[]).unwrap();