identity_storage/storage/
hybrid_jws_document_ext.rs

1// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
2// SPDX-License-Identifier: Apache-2.0
3
4use super::JwkStorageDocumentError as Error;
5use crate::try_undo_key_generation;
6use crate::JwkGenOutput;
7use crate::JwkStorage;
8use crate::JwkStoragePQ;
9use crate::JwsSignatureOptions;
10use crate::KeyId;
11use crate::KeyIdStorage;
12use crate::KeyIdStorageErrorKind;
13use crate::KeyType;
14use crate::MethodDigest;
15use crate::Storage;
16use crate::StorageResult;
17use async_trait::async_trait;
18use identity_core::common::Object;
19use identity_credential::credential::Credential;
20use identity_credential::credential::Jws;
21use identity_credential::credential::Jwt;
22use identity_credential::presentation::JwtPresentationOptions;
23use identity_credential::presentation::Presentation;
24use identity_did::DIDUrl;
25use identity_document::document::CoreDocument;
26use identity_verification::jwk::CompositeAlgId;
27use identity_verification::jwk::CompositeJwk;
28use identity_verification::jwk::PostQuantumJwk;
29use identity_verification::jwk::TraditionalJwk;
30use identity_verification::jws::CharSet;
31use identity_verification::jws::CompactJwsEncoder;
32use identity_verification::jws::CompactJwsEncodingOptions;
33use identity_verification::jws::JwsAlgorithm;
34use identity_verification::jws::JwsHeader;
35use identity_verification::MethodData;
36use identity_verification::MethodScope;
37use identity_verification::VerificationMethod;
38use serde::de::DeserializeOwned;
39use serde::Serialize;
40
41macro_rules! generate_method_hybrid_for_document_type {
42  ($t:ty, $name:ident) => {
43    async fn $name<K, I>(
44      document: &mut $t,
45      storage: &Storage<K, I>,
46      alg_id: CompositeAlgId,
47      fragment: Option<&str>,
48      scope: MethodScope,
49    ) -> StorageResult<String>
50    where
51      K: JwkStorage + JwkStoragePQ,
52      I: KeyIdStorage,
53    {
54      let (pq_key_type, pq_alg, trad_key_type, trad_alg) = match alg_id {
55        CompositeAlgId::IdMldsa44Ed25519 => (
56          KeyType::from_static_str("AKP"),
57          JwsAlgorithm::ML_DSA_44,
58          KeyType::from_static_str("Ed25519"),
59          JwsAlgorithm::EdDSA,
60        ),
61        CompositeAlgId::IdMldsa65Ed25519 => (
62          KeyType::from_static_str("AKP"),
63          JwsAlgorithm::ML_DSA_65,
64          KeyType::from_static_str("Ed25519"),
65          JwsAlgorithm::EdDSA,
66        ),
67        _ => {
68          return Err(Error::InvalidJwsAlgorithm);
69        }
70      };
71
72      let JwkGenOutput {
73        key_id: t_key_id,
74        jwk: t_jwk,
75      } = K::generate(storage.key_storage(), trad_key_type, trad_alg)
76        .await
77        .map_err(Error::KeyStorageError)?;
78
79      let JwkGenOutput {
80        key_id: pq_key_id,
81        jwk: pq_jwk,
82      } = K::generate_pq_key(storage.key_storage(), pq_key_type, pq_alg)
83        .await
84        .map_err(Error::KeyStorageError)?;
85
86      let composite_kid = KeyId::new(format!("{}~{}", t_key_id.as_str(), pq_key_id.as_str()));
87
88      let pq_jwk = PostQuantumJwk::try_from(pq_jwk).map_err(|err| Error::EncodingError(Box::new(err)))?;
89
90      let traditional_jwk = TraditionalJwk::try_from(t_jwk).map_err(|err| Error::EncodingError(Box::new(err)))?;
91
92      let composite_pk = CompositeJwk::new(alg_id, traditional_jwk, pq_jwk);
93
94      let method: VerificationMethod = {
95        match VerificationMethod::new_from_compositejwk(document.id().clone(), composite_pk, fragment)
96          .map_err(Error::VerificationMethodConstructionError)
97        {
98          Ok(method) => method,
99          Err(source) => {
100            let error = try_undo_key_generation(storage, &t_key_id, source).await;
101            let error = try_undo_key_generation(storage, &pq_key_id, error).await;
102            return Err(error);
103          }
104        }
105      };
106
107      // Extract data from method before inserting it into the DID document.
108      let method_digest: MethodDigest = MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError)?;
109      let method_id: DIDUrl = method.id().clone();
110
111      // The fragment is always set on a method, so this error will never occur.
112      let fragment: String = method_id
113        .fragment()
114        .ok_or(identity_verification::Error::MissingIdFragment)
115        .map_err(Error::VerificationMethodConstructionError)?
116        .to_owned();
117
118      // Insert method into document and handle error upon failure.
119      if let Err(error) = document
120        .insert_method(method, scope)
121        .map_err(|_| Error::FragmentAlreadyExists)
122      {
123        let error = try_undo_key_generation(storage, &t_key_id, error).await;
124        let error = try_undo_key_generation(storage, &pq_key_id, error).await;
125        return Err(error);
126      };
127
128      // Insert the generated `KeyId` into storage under the computed method digest and handle the error if the
129      // operation fails.
130      if let Err(error) = <I as KeyIdStorage>::insert_key_id(&storage.key_id_storage(), method_digest, composite_kid)
131        .await
132        .map_err(Error::KeyIdStorageError)
133      {
134        // Remove the method from the document as it can no longer be used.
135        let _ = document.remove_method(&method_id);
136        let error = try_undo_key_generation(storage, &t_key_id, error).await;
137        let error = try_undo_key_generation(storage, &pq_key_id, error).await;
138        return Err(error);
139      }
140
141      Ok(fragment)
142    }
143  };
144}
145
146/// Extension trait to handle PQ/T hybrid operations.
147#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
148#[cfg_attr(feature = "send-sync-storage", async_trait)]
149pub trait JwkDocumentExtHybrid {
150  /// Generate an Verification Method containing a PQ/T hybrid key.
151  async fn generate_method_hybrid<K, I>(
152    &mut self,
153    storage: &Storage<K, I>,
154    alg_id: CompositeAlgId,
155    fragment: Option<&str>,
156    scope: MethodScope,
157  ) -> StorageResult<String>
158  where
159    K: JwkStorage + JwkStoragePQ,
160    I: KeyIdStorage;
161
162  /// Create a PQ/T hybrid JWS.
163  async fn create_jws<K, I>(
164    &self,
165    storage: &Storage<K, I>,
166    fragment: &str,
167    payload: &[u8],
168    options: &JwsSignatureOptions,
169  ) -> StorageResult<Jws>
170  where
171    K: JwkStorage + JwkStoragePQ,
172    I: KeyIdStorage;
173
174  /// Create a PQ/T hybrid Verifiable Credential.
175  async fn create_credential_jwt_hybrid<K, I, T>(
176    &self,
177    credential: &Credential<T>,
178    storage: &Storage<K, I>,
179    fragment: &str,
180    options: &JwsSignatureOptions,
181    custom_claims: Option<Object>,
182  ) -> StorageResult<Jwt>
183  where
184    K: JwkStorage + JwkStoragePQ,
185    I: KeyIdStorage,
186    T: Clone + Serialize + DeserializeOwned + Sync;
187
188  /// Create a PQ/T hybrid Verifiable Presentation.
189  async fn create_presentation_jwt_hybrid<K, I, CRED, T>(
190    &self,
191    presentation: &Presentation<CRED, T>,
192    storage: &Storage<K, I>,
193    fragment: &str,
194    signature_options: &JwsSignatureOptions,
195    presentation_options: &JwtPresentationOptions,
196  ) -> StorageResult<Jwt>
197  where
198    K: JwkStorage + JwkStoragePQ,
199    I: KeyIdStorage,
200    T: Clone + Serialize + DeserializeOwned + Sync,
201    CRED: Serialize + DeserializeOwned + Clone + Sync;
202}
203
204generate_method_hybrid_for_document_type!(CoreDocument, generate_method_hybrid_core_document);
205
206#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
207#[cfg_attr(feature = "send-sync-storage", async_trait)]
208impl JwkDocumentExtHybrid for CoreDocument {
209  async fn generate_method_hybrid<K, I>(
210    &mut self,
211    storage: &Storage<K, I>,
212    alg_id: CompositeAlgId,
213    fragment: Option<&str>,
214    scope: MethodScope,
215  ) -> StorageResult<String>
216  where
217    K: JwkStorage + JwkStoragePQ,
218    I: KeyIdStorage,
219  {
220    generate_method_hybrid_core_document(self, storage, alg_id, fragment, scope).await
221  }
222
223  /// Hybrid signature implementation
224  async fn create_jws<K, I>(
225    &self,
226    storage: &Storage<K, I>,
227    fragment: &str,
228    payload: &[u8],
229    options: &JwsSignatureOptions,
230  ) -> StorageResult<Jws>
231  where
232    K: JwkStorage + JwkStoragePQ,
233    I: KeyIdStorage,
234  {
235    // Obtain the method corresponding to the given fragment.
236    let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?;
237    let MethodData::CompositeJwk(ref composite) = method.data() else {
238      return Err(Error::NotCompositePublicKey);
239    };
240
241    let alg_id = composite.alg_id();
242    let t_jwk = composite.traditional_public_key();
243    let pq_jwk = composite.pq_public_key();
244
245    // Extract JwsAlgorithm.
246    let alg: JwsAlgorithm = alg_id.name().parse().map_err(|_| Error::InvalidJwsAlgorithm)?;
247
248    // Create JWS header in accordance with options.
249    let header: JwsHeader = {
250      let mut header = JwsHeader::new();
251
252      header.set_alg(alg.clone());
253      if let Some(custom) = &options.custom_header_parameters {
254        header.set_custom(custom.clone())
255      }
256
257      if let Some(ref kid) = options.kid {
258        header.set_kid(kid.clone());
259      } else {
260        header.set_kid(method.id().to_string());
261      }
262
263      if let Some(b64) = options.b64 {
264        // Follow recommendation in https://datatracker.ietf.org/doc/html/rfc7797#section-7.
265        if !b64 {
266          header.set_b64(b64);
267          header.set_crit(["b64"]);
268        }
269      };
270
271      if let Some(typ) = &options.typ {
272        header.set_typ(typ.clone())
273      } else {
274        // https://www.w3.org/TR/vc-data-model/#jwt-encoding
275        header.set_typ("JWT")
276      }
277
278      if let Some(cty) = &options.cty {
279        header.set_cty(cty.clone())
280      };
281
282      if let Some(url) = &options.url {
283        header.set_url(url.clone())
284      };
285
286      if let Some(nonce) = &options.nonce {
287        header.set_nonce(nonce.clone())
288      };
289
290      header
291    };
292
293    // Get the key identifier corresponding to the given method from the KeyId storage.
294    let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?;
295    let key_id: KeyId = <I as KeyIdStorage>::get_key_id(storage.key_id_storage(), &method_digest)
296      .await
297      .map_err(Error::KeyIdStorageError)?;
298
299    let (t_key_id, pq_key_id) = match key_id.as_str().split_once("~") {
300      Some(v) => (KeyId::new(v.0), KeyId::new(v.1)),
301      None => {
302        // If the key_id is not in the expected format, we return an error.
303        return Err(Error::KeyIdStorageError(KeyIdStorageErrorKind::Unspecified.into()));
304      }
305    };
306
307    // Extract Compact JWS encoding options.
308    let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload {
309      // We use this as a default and don't provide the extra UrlSafe check for now.
310      // Applications that require such checks can easily do so after JWS creation.
311      CompactJwsEncodingOptions::NonDetached {
312        charset_requirements: CharSet::Default,
313      }
314    } else {
315      CompactJwsEncodingOptions::Detached
316    };
317
318    let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options)
319      .map_err(|err| Error::EncodingError(err.into()))?;
320
321    let domain = match alg {
322      JwsAlgorithm::IdMldsa44Ed25519 => CompositeAlgId::IdMldsa44Ed25519.domain(),
323      JwsAlgorithm::IdMldsa65Ed25519 => CompositeAlgId::IdMldsa65Ed25519.domain(),
324      _ => return Err(Error::InvalidJwsAlgorithm),
325    };
326
327    //M' = Prefix || Domain || len(ctx) || ctx || M
328    //let prefix = b"CompositeAlgorithmSignatures2025";
329
330    //Prefix: CompositeAlgorithmSignatures2025
331    let mut input = CompositeAlgId::COMPOSITE_SIGNATURE_PREFIX.to_vec();
332
333    //Domain: id-MLDSA44-Ed25519 or id-MLDSA65-Ed25519
334    input.extend_from_slice(domain);
335
336    //len(ctx) = 0
337    input.push(0x00);
338
339    //M
340    input.extend(jws_encoder.signing_input());
341
342    let signature_t = <K as JwkStorage>::sign(storage.key_storage(), &t_key_id, &input, t_jwk)
343      .await
344      .map_err(Error::KeyStorageError)?;
345
346    let signature_pq = <K as JwkStoragePQ>::pq_sign(storage.key_storage(), &pq_key_id, &input, pq_jwk, Some(domain))
347      .await
348      .map_err(Error::KeyStorageError)?;
349
350    let signature = [signature_t, signature_pq].concat();
351
352    Ok(Jws::new(jws_encoder.into_jws(&signature)))
353  }
354
355  async fn create_credential_jwt_hybrid<K, I, T>(
356    &self,
357    credential: &Credential<T>,
358    storage: &Storage<K, I>,
359    fragment: &str,
360    options: &JwsSignatureOptions,
361    custom_claims: Option<Object>,
362  ) -> StorageResult<Jwt>
363  where
364    K: JwkStorage + JwkStoragePQ,
365    I: KeyIdStorage,
366    T: Clone + Serialize + DeserializeOwned + Sync,
367  {
368    if options.detached_payload {
369      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
370        "cannot use detached payload for credential signing",
371      )));
372    }
373
374    if !options.b64.unwrap_or(true) {
375      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
376      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
377        "cannot use `b64 = false` with JWTs",
378      )));
379    }
380
381    let payload = credential
382      .serialize_jwt(custom_claims)
383      .map_err(Error::ClaimsSerializationError)?;
384    self
385      .create_jws(storage, fragment, payload.as_bytes(), options)
386      .await
387      .map(|jws| Jwt::new(jws.into()))
388  }
389
390  async fn create_presentation_jwt_hybrid<K, I, CRED, T>(
391    &self,
392    presentation: &Presentation<CRED, T>,
393    storage: &Storage<K, I>,
394    fragment: &str,
395    jws_options: &JwsSignatureOptions,
396    jwt_options: &JwtPresentationOptions,
397  ) -> StorageResult<Jwt>
398  where
399    K: JwkStorage + JwkStoragePQ,
400    I: KeyIdStorage,
401    T: Clone + Serialize + DeserializeOwned + Sync,
402    CRED: Clone + Serialize + DeserializeOwned + Sync,
403  {
404    if jws_options.detached_payload {
405      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
406        "cannot use detached payload for presentation signing",
407      )));
408    }
409
410    if !jws_options.b64.unwrap_or(true) {
411      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
412      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
413        "cannot use `b64 = false` with JWTs",
414      )));
415    }
416    let payload = presentation
417      .serialize_jwt(jwt_options)
418      .map_err(Error::ClaimsSerializationError)?;
419    self
420      .create_jws(storage, fragment, payload.as_bytes(), jws_options)
421      .await
422      .map(|jws| Jwt::new(jws.into()))
423  }
424}
425
426// ====================================================================================================================
427// IotaDocument
428// ====================================================================================================================
429#[cfg(feature = "iota-document")]
430mod iota_document {
431  use crate::StorageResult;
432
433  use super::*;
434  use identity_credential::credential::Jwt;
435  use identity_iota_core::IotaDocument;
436
437  generate_method_hybrid_for_document_type!(IotaDocument, generate_method_hybrid_iota_document);
438
439  #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
440  #[cfg_attr(feature = "send-sync-storage", async_trait)]
441  impl JwkDocumentExtHybrid for IotaDocument {
442    async fn generate_method_hybrid<K, I>(
443      &mut self,
444      storage: &Storage<K, I>,
445      alg_id: CompositeAlgId,
446      fragment: Option<&str>,
447      scope: MethodScope,
448    ) -> StorageResult<String>
449    where
450      K: JwkStorage + JwkStoragePQ,
451      I: KeyIdStorage,
452    {
453      generate_method_hybrid_iota_document(self, storage, alg_id, fragment, scope).await
454    }
455
456    async fn create_jws<K, I>(
457      &self,
458      storage: &Storage<K, I>,
459      fragment: &str,
460      payload: &[u8],
461      options: &JwsSignatureOptions,
462    ) -> StorageResult<Jws>
463    where
464      K: JwkStorage + JwkStoragePQ,
465      I: KeyIdStorage,
466    {
467      self
468        .core_document()
469        .create_jws(storage, fragment, payload, options)
470        .await
471    }
472
473    async fn create_credential_jwt_hybrid<K, I, T>(
474      &self,
475      credential: &Credential<T>,
476      storage: &Storage<K, I>,
477      fragment: &str,
478      options: &JwsSignatureOptions,
479      custom_claims: Option<Object>,
480    ) -> StorageResult<Jwt>
481    where
482      K: JwkStorage + JwkStoragePQ,
483      I: KeyIdStorage,
484      T: Clone + Serialize + DeserializeOwned + Sync,
485    {
486      self
487        .core_document()
488        .create_credential_jwt_hybrid(credential, storage, fragment, options, custom_claims)
489        .await
490    }
491
492    async fn create_presentation_jwt_hybrid<K, I, CRED, T>(
493      &self,
494      presentation: &Presentation<CRED, T>,
495      storage: &Storage<K, I>,
496      fragment: &str,
497      options: &JwsSignatureOptions,
498      jwt_options: &JwtPresentationOptions,
499    ) -> StorageResult<Jwt>
500    where
501      K: JwkStorage + JwkStoragePQ,
502      I: KeyIdStorage,
503      T: Clone + Serialize + DeserializeOwned + Sync,
504      CRED: Clone + Serialize + DeserializeOwned + Sync,
505    {
506      self
507        .core_document()
508        .create_presentation_jwt_hybrid(presentation, storage, fragment, options, jwt_options)
509        .await
510    }
511  }
512}