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      // We need to disable this lint as JwsAlgorithm is `Copy` only when
253      // a certain feature flag is disabled.
254      #[allow(clippy::clone_on_copy)]
255      header.set_alg(alg.clone());
256      if let Some(custom) = &options.custom_header_parameters {
257        header.set_custom(custom.clone())
258      }
259
260      if let Some(ref kid) = options.kid {
261        header.set_kid(kid.clone());
262      } else {
263        header.set_kid(method.id().to_string());
264      }
265
266      if let Some(b64) = options.b64 {
267        // Follow recommendation in https://datatracker.ietf.org/doc/html/rfc7797#section-7.
268        if !b64 {
269          header.set_b64(b64);
270          header.set_crit(["b64"]);
271        }
272      };
273
274      if let Some(typ) = &options.typ {
275        header.set_typ(typ.clone())
276      } else {
277        // https://www.w3.org/TR/vc-data-model/#jwt-encoding
278        header.set_typ("JWT")
279      }
280
281      if let Some(cty) = &options.cty {
282        header.set_cty(cty.clone())
283      };
284
285      if let Some(url) = &options.url {
286        header.set_url(url.clone())
287      };
288
289      if let Some(nonce) = &options.nonce {
290        header.set_nonce(nonce.clone())
291      };
292
293      header
294    };
295
296    // Get the key identifier corresponding to the given method from the KeyId storage.
297    let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?;
298    let key_id: KeyId = <I as KeyIdStorage>::get_key_id(storage.key_id_storage(), &method_digest)
299      .await
300      .map_err(Error::KeyIdStorageError)?;
301
302    let (t_key_id, pq_key_id) = match key_id.as_str().split_once("~") {
303      Some(v) => (KeyId::new(v.0), KeyId::new(v.1)),
304      None => {
305        // If the key_id is not in the expected format, we return an error.
306        return Err(Error::KeyIdStorageError(KeyIdStorageErrorKind::Unspecified.into()));
307      }
308    };
309
310    // Extract Compact JWS encoding options.
311    let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload {
312      // We use this as a default and don't provide the extra UrlSafe check for now.
313      // Applications that require such checks can easily do so after JWS creation.
314      CompactJwsEncodingOptions::NonDetached {
315        charset_requirements: CharSet::Default,
316      }
317    } else {
318      CompactJwsEncodingOptions::Detached
319    };
320
321    let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options)
322      .map_err(|err| Error::EncodingError(err.into()))?;
323
324    let domain = match alg {
325      JwsAlgorithm::IdMldsa44Ed25519 => CompositeAlgId::IdMldsa44Ed25519.domain(),
326      JwsAlgorithm::IdMldsa65Ed25519 => CompositeAlgId::IdMldsa65Ed25519.domain(),
327      _ => return Err(Error::InvalidJwsAlgorithm),
328    };
329
330    //M' = Prefix || Domain || len(ctx) || ctx || M
331    //let prefix = b"CompositeAlgorithmSignatures2025";
332
333    //Prefix: CompositeAlgorithmSignatures2025
334    let mut input = CompositeAlgId::COMPOSITE_SIGNATURE_PREFIX.to_vec();
335
336    //Domain: id-MLDSA44-Ed25519 or id-MLDSA65-Ed25519
337    input.extend_from_slice(domain);
338
339    //len(ctx) = 0
340    input.push(0x00);
341
342    //M
343    input.extend(jws_encoder.signing_input());
344
345    let signature_t = <K as JwkStorage>::sign(storage.key_storage(), &t_key_id, &input, t_jwk)
346      .await
347      .map_err(Error::KeyStorageError)?;
348
349    let signature_pq = <K as JwkStoragePQ>::pq_sign(storage.key_storage(), &pq_key_id, &input, pq_jwk, Some(domain))
350      .await
351      .map_err(Error::KeyStorageError)?;
352
353    let signature = [signature_t, signature_pq].concat();
354
355    Ok(Jws::new(jws_encoder.into_jws(&signature)))
356  }
357
358  async fn create_credential_jwt_hybrid<K, I, T>(
359    &self,
360    credential: &Credential<T>,
361    storage: &Storage<K, I>,
362    fragment: &str,
363    options: &JwsSignatureOptions,
364    custom_claims: Option<Object>,
365  ) -> StorageResult<Jwt>
366  where
367    K: JwkStorage + JwkStoragePQ,
368    I: KeyIdStorage,
369    T: Clone + Serialize + DeserializeOwned + Sync,
370  {
371    if options.detached_payload {
372      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
373        "cannot use detached payload for credential signing",
374      )));
375    }
376
377    if !options.b64.unwrap_or(true) {
378      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
379      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
380        "cannot use `b64 = false` with JWTs",
381      )));
382    }
383
384    let payload = credential
385      .serialize_jwt(custom_claims)
386      .map_err(Error::ClaimsSerializationError)?;
387    self
388      .create_jws(storage, fragment, payload.as_bytes(), options)
389      .await
390      .map(|jws| Jwt::new(jws.into()))
391  }
392
393  async fn create_presentation_jwt_hybrid<K, I, CRED, T>(
394    &self,
395    presentation: &Presentation<CRED, T>,
396    storage: &Storage<K, I>,
397    fragment: &str,
398    jws_options: &JwsSignatureOptions,
399    jwt_options: &JwtPresentationOptions,
400  ) -> StorageResult<Jwt>
401  where
402    K: JwkStorage + JwkStoragePQ,
403    I: KeyIdStorage,
404    T: Clone + Serialize + DeserializeOwned + Sync,
405    CRED: Clone + Serialize + DeserializeOwned + Sync,
406  {
407    if jws_options.detached_payload {
408      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
409        "cannot use detached payload for presentation signing",
410      )));
411    }
412
413    if !jws_options.b64.unwrap_or(true) {
414      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
415      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
416        "cannot use `b64 = false` with JWTs",
417      )));
418    }
419    let payload = presentation
420      .serialize_jwt(jwt_options)
421      .map_err(Error::ClaimsSerializationError)?;
422    self
423      .create_jws(storage, fragment, payload.as_bytes(), jws_options)
424      .await
425      .map(|jws| Jwt::new(jws.into()))
426  }
427}
428
429// ====================================================================================================================
430// IotaDocument
431// ====================================================================================================================
432#[cfg(feature = "iota-document")]
433mod iota_document {
434  use crate::StorageResult;
435
436  use super::*;
437  use identity_credential::credential::Jwt;
438  use identity_iota_core::IotaDocument;
439
440  generate_method_hybrid_for_document_type!(IotaDocument, generate_method_hybrid_iota_document);
441
442  #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
443  #[cfg_attr(feature = "send-sync-storage", async_trait)]
444  impl JwkDocumentExtHybrid for IotaDocument {
445    async fn generate_method_hybrid<K, I>(
446      &mut self,
447      storage: &Storage<K, I>,
448      alg_id: CompositeAlgId,
449      fragment: Option<&str>,
450      scope: MethodScope,
451    ) -> StorageResult<String>
452    where
453      K: JwkStorage + JwkStoragePQ,
454      I: KeyIdStorage,
455    {
456      generate_method_hybrid_iota_document(self, storage, alg_id, fragment, scope).await
457    }
458
459    async fn create_jws<K, I>(
460      &self,
461      storage: &Storage<K, I>,
462      fragment: &str,
463      payload: &[u8],
464      options: &JwsSignatureOptions,
465    ) -> StorageResult<Jws>
466    where
467      K: JwkStorage + JwkStoragePQ,
468      I: KeyIdStorage,
469    {
470      self
471        .core_document()
472        .create_jws(storage, fragment, payload, options)
473        .await
474    }
475
476    async fn create_credential_jwt_hybrid<K, I, T>(
477      &self,
478      credential: &Credential<T>,
479      storage: &Storage<K, I>,
480      fragment: &str,
481      options: &JwsSignatureOptions,
482      custom_claims: Option<Object>,
483    ) -> StorageResult<Jwt>
484    where
485      K: JwkStorage + JwkStoragePQ,
486      I: KeyIdStorage,
487      T: Clone + Serialize + DeserializeOwned + Sync,
488    {
489      self
490        .core_document()
491        .create_credential_jwt_hybrid(credential, storage, fragment, options, custom_claims)
492        .await
493    }
494
495    async fn create_presentation_jwt_hybrid<K, I, CRED, T>(
496      &self,
497      presentation: &Presentation<CRED, T>,
498      storage: &Storage<K, I>,
499      fragment: &str,
500      options: &JwsSignatureOptions,
501      jwt_options: &JwtPresentationOptions,
502    ) -> StorageResult<Jwt>
503    where
504      K: JwkStorage + JwkStoragePQ,
505      I: KeyIdStorage,
506      T: Clone + Serialize + DeserializeOwned + Sync,
507      CRED: Clone + Serialize + DeserializeOwned + Sync,
508    {
509      self
510        .core_document()
511        .create_presentation_jwt_hybrid(presentation, storage, fragment, options, jwt_options)
512        .await
513    }
514  }
515}