identity_jose/jwk/
composite_jwk.rs

1// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::Display;
5use std::str::FromStr;
6
7use crate::jwk::PostQuantumJwk;
8use crate::jwk::TraditionalJwk;
9
10/// Algorithms used to generate hybrid signatures.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
12#[non_exhaustive]
13pub enum CompositeAlgId {
14  /// DER encoded value in hex = 060B6086480186FA6B5008013E
15  #[serde(rename = "id-MLDSA44-Ed25519")]
16  IdMldsa44Ed25519,
17  /// DER encoded value in hex = 060B6086480186FA6B50080147
18  #[serde(rename = "id-MLDSA65-Ed25519")]
19  IdMldsa65Ed25519,
20}
21
22impl CompositeAlgId {
23  /// Returns the JWS algorithm as a `str` slice.
24  pub const fn name(self) -> &'static str {
25    match self {
26      Self::IdMldsa44Ed25519 => "id-MLDSA44-Ed25519",
27      Self::IdMldsa65Ed25519 => "id-MLDSA65-Ed25519",
28    }
29  }
30
31  /// Returns the [CompositeAlgId]'s domain as a byte slice.
32  pub const fn domain(self) -> &'static [u8] {
33    match self {
34      Self::IdMldsa44Ed25519 => &[
35        0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x3E,
36      ],
37      Self::IdMldsa65Ed25519 => &[
38        0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x47,
39      ],
40    }
41  }
42
43  /// Returns the prefix used for composite signatures.
44  pub const COMPOSITE_SIGNATURE_PREFIX: &[u8] = b"CompositeAlgorithmSignatures2025";
45}
46
47/// Represent a combination of a traditional public key and a post-quantum public key both in Jwk format.
48#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct CompositeJwk {
51  alg_id: CompositeAlgId,
52  traditional_public_key: TraditionalJwk,
53  pq_public_key: PostQuantumJwk,
54}
55
56impl CompositeJwk {
57  /// Create a new CompositePublicKey structure.
58  pub fn new(alg_id: CompositeAlgId, traditional_public_key: TraditionalJwk, pq_public_key: PostQuantumJwk) -> Self {
59    Self {
60      alg_id,
61      traditional_public_key: traditional_public_key.into_public().unwrap(),
62      pq_public_key: pq_public_key.into_public().unwrap(),
63    }
64  }
65  /// Get the `algId` value.
66  pub fn alg_id(&self) -> CompositeAlgId {
67    self.alg_id
68  }
69  /// Get the post-quantum public key in Jwk format.
70  pub fn pq_public_key(&self) -> &PostQuantumJwk {
71    &self.pq_public_key
72  }
73  /// Get the traditional public key in Jwk format.
74  pub fn traditional_public_key(&self) -> &TraditionalJwk {
75    &self.traditional_public_key
76  }
77}
78
79impl FromStr for CompositeAlgId {
80  type Err = CompositeAlgParsingError;
81
82  fn from_str(string: &str) -> std::result::Result<Self, Self::Err> {
83    match string {
84      "id-MLDSA44-Ed25519" => Ok(Self::IdMldsa44Ed25519),
85      "id-MLDSA65-Ed25519" => Ok(Self::IdMldsa65Ed25519),
86      invalid => Err(CompositeAlgParsingError {
87        input: invalid.to_owned(),
88      }),
89    }
90  }
91}
92
93/// Error that might occure when parsing a [CompositeAlgId] out of a string.
94#[derive(Debug, thiserror::Error)]
95#[non_exhaustive]
96pub struct CompositeAlgParsingError {
97  /// The input string.
98  pub input: String,
99}
100
101impl Display for CompositeAlgParsingError {
102  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103    write!(
104      f,
105      "invalid CompositeAlgId `{}`; valid values are: `{}`, `{}`",
106      self.input,
107      CompositeAlgId::IdMldsa44Ed25519.name(),
108      CompositeAlgId::IdMldsa65Ed25519.name()
109    )
110  }
111}
112
113impl From<CompositeAlgParsingError> for crate::error::Error {
114  fn from(_: CompositeAlgParsingError) -> Self {
115    crate::error::Error::JwsAlgorithmParsingError
116  }
117}