identity_storage/storage/
pqc_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::key_id_storage::MethodDigest;
6use crate::try_undo_key_generation;
7use crate::JwkGenOutput;
8use crate::JwkStoragePQ;
9use crate::JwsSignatureOptions;
10use crate::KeyIdStorage;
11use crate::KeyType;
12use crate::Storage;
13use crate::StorageResult;
14use async_trait::async_trait;
15use identity_core::common::Object;
16use identity_credential::credential::Credential;
17use identity_credential::credential::Jws;
18use identity_credential::credential::Jwt;
19use identity_credential::presentation::JwtPresentationOptions;
20use identity_credential::presentation::Presentation;
21use identity_did::DIDUrl;
22use identity_document::document::CoreDocument;
23use identity_verification::jwk::PostQuantumJwk;
24use identity_verification::jws::CharSet;
25use identity_verification::jws::CompactJwsEncoder;
26use identity_verification::jws::CompactJwsEncodingOptions;
27use identity_verification::jws::JwsAlgorithm;
28use identity_verification::jws::JwsHeader;
29use identity_verification::MethodData;
30use identity_verification::MethodScope;
31use identity_verification::VerificationMethod;
32use serde::de::DeserializeOwned;
33use serde::Serialize;
34
35///New trait to handle PQ-based operations on DID Documents
36#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
37#[cfg_attr(feature = "send-sync-storage", async_trait)]
38pub trait JwsDocumentExtPQC {
39  /// Generate new key material in the given `storage` and insert a new verification method with the corresponding PQ
40  /// public key material into the DID document.
41  async fn generate_method_pqc<K, I>(
42    &mut self,
43    storage: &Storage<K, I>,
44    key_type: KeyType,
45    alg: JwsAlgorithm,
46    fragment: Option<&str>,
47    scope: MethodScope,
48  ) -> StorageResult<String>
49  where
50    K: JwkStoragePQ,
51    I: KeyIdStorage;
52
53  /// Create a PQ JWS
54  async fn create_jws_pqc<K, I>(
55    &self,
56    storage: &Storage<K, I>,
57    fragment: &str,
58    payload: &[u8],
59    options: &JwsSignatureOptions,
60  ) -> StorageResult<Jws>
61  where
62    K: JwkStoragePQ,
63    I: KeyIdStorage;
64
65  /// Produces a JWT using PQC algorithms where the payload is produced from the given `credential`
66  /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
67  ///
68  /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id`
69  /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding
70  /// private key backed by the `storage` in accordance with the passed `options`.
71  ///
72  /// The `custom_claims` can be used to set additional claims on the resulting JWT.
73  async fn create_credential_jwt_pqc<K, I, T>(
74    &self,
75    credential: &Credential<T>,
76    storage: &Storage<K, I>,
77    fragment: &str,
78    options: &JwsSignatureOptions,
79    custom_claims: Option<Object>,
80  ) -> StorageResult<Jwt>
81  where
82    K: JwkStoragePQ,
83    I: KeyIdStorage,
84    T: Clone + Serialize + DeserializeOwned + Sync;
85
86  /// Produces a JWT using PQC algorithms where the payload is produced from the given `presentation`
87  /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
88  ///
89  /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id`
90  /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding
91  /// private key backed by the `storage` in accordance with the passed `options`.
92  async fn create_presentation_jwt_pqc<K, I, CRED, T>(
93    &self,
94    presentation: &Presentation<CRED, T>,
95    storage: &Storage<K, I>,
96    fragment: &str,
97    signature_options: &JwsSignatureOptions,
98    presentation_options: &JwtPresentationOptions,
99  ) -> StorageResult<Jwt>
100  where
101    K: JwkStoragePQ,
102    I: KeyIdStorage,
103    T: Clone + Serialize + DeserializeOwned + Sync,
104    CRED: Clone + Serialize + DeserializeOwned + Sync;
105}
106
107// ====================================================================================================================
108// CoreDocument
109// ====================================================================================================================
110
111generate_method_for_document_type!(
112  CoreDocument,
113  JwsAlgorithm,
114  JwkStoragePQ,
115  JwkStoragePQ::generate_pq_key,
116  generate_method_core_document
117);
118
119#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
120#[cfg_attr(feature = "send-sync-storage", async_trait)]
121impl JwsDocumentExtPQC for CoreDocument {
122  async fn generate_method_pqc<K, I>(
123    &mut self,
124    storage: &Storage<K, I>,
125    key_type: KeyType,
126    alg: JwsAlgorithm,
127    fragment: Option<&str>,
128    scope: MethodScope,
129  ) -> StorageResult<String>
130  where
131    K: JwkStoragePQ,
132    I: KeyIdStorage,
133  {
134    generate_method_core_document(self, storage, key_type, alg, fragment, scope).await
135  }
136
137  async fn create_jws_pqc<K, I>(
138    &self,
139    storage: &Storage<K, I>,
140    fragment: &str,
141    payload: &[u8],
142    options: &JwsSignatureOptions,
143  ) -> StorageResult<Jws>
144  where
145    K: JwkStoragePQ,
146    I: KeyIdStorage,
147  {
148    // Obtain the method corresponding to the given fragment.
149    let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?;
150    let MethodData::PublicKeyJwk(ref jwk) = method.data() else {
151      return Err(Error::NotPublicKeyJwk);
152    };
153
154    // Extract JwsAlgorithm.
155    let alg: JwsAlgorithm = jwk
156      .alg()
157      .unwrap_or("")
158      .parse()
159      .map_err(|_| Error::InvalidJwsAlgorithm)?;
160
161    // Create JWS header in accordance with options.
162    let header: JwsHeader = {
163      let mut header = JwsHeader::new();
164
165      header.set_alg(alg);
166      if let Some(custom) = &options.custom_header_parameters {
167        header.set_custom(custom.clone())
168      }
169
170      if let Some(ref kid) = options.kid {
171        header.set_kid(kid.clone());
172      } else {
173        header.set_kid(method.id().to_string());
174      }
175
176      if options.attach_jwk {
177        header.set_jwk(jwk.clone())
178      };
179
180      if let Some(b64) = options.b64 {
181        // Follow recommendation in https://datatracker.ietf.org/doc/html/rfc7797#section-7.
182        if !b64 {
183          header.set_b64(b64);
184          header.set_crit(["b64"]);
185        }
186      };
187
188      if let Some(typ) = &options.typ {
189        header.set_typ(typ.clone())
190      } else {
191        // https://www.w3.org/TR/vc-data-model/#jwt-encoding
192        header.set_typ("JWT")
193      }
194
195      if let Some(cty) = &options.cty {
196        header.set_cty(cty.clone())
197      };
198
199      if let Some(url) = &options.url {
200        header.set_url(url.clone())
201      };
202
203      if let Some(nonce) = &options.nonce {
204        header.set_nonce(nonce.clone())
205      };
206
207      header
208    };
209
210    // Get the key identifier corresponding to the given method from the KeyId storage.
211    let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?;
212    let key_id = <I as KeyIdStorage>::get_key_id(storage.key_id_storage(), &method_digest)
213      .await
214      .map_err(Error::KeyIdStorageError)?;
215
216    // Extract Compact JWS encoding options.
217    let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload {
218      // We use this as a default and don't provide the extra UrlSafe check for now.
219      // Applications that require such checks can easily do so after JWS creation.
220      CompactJwsEncodingOptions::NonDetached {
221        charset_requirements: CharSet::Default,
222      }
223    } else {
224      CompactJwsEncodingOptions::Detached
225    };
226
227    let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options)
228      .map_err(|err| Error::EncodingError(err.into()))?;
229
230    let pq_jwk = PostQuantumJwk::try_from(jwk.clone()).map_err(|err| Error::EncodingError(Box::new(err)))?;
231
232    let signature = <K as JwkStoragePQ>::pq_sign(
233      storage.key_storage(),
234      &key_id,
235      jws_encoder.signing_input(),
236      &pq_jwk,
237      None,
238    )
239    .await
240    .map_err(Error::KeyStorageError)?;
241    Ok(Jws::new(jws_encoder.into_jws(&signature)))
242  }
243
244  async fn create_credential_jwt_pqc<K, I, T>(
245    &self,
246    credential: &Credential<T>,
247    storage: &Storage<K, I>,
248    fragment: &str,
249    options: &JwsSignatureOptions,
250    custom_claims: Option<Object>,
251  ) -> StorageResult<Jwt>
252  where
253    K: JwkStoragePQ,
254    I: KeyIdStorage,
255    T: Clone + Serialize + DeserializeOwned + Sync,
256  {
257    if options.detached_payload {
258      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
259        "cannot use detached payload for credential signing",
260      )));
261    }
262
263    if !options.b64.unwrap_or(true) {
264      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
265      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
266        "cannot use `b64 = false` with JWTs",
267      )));
268    }
269
270    let payload = credential
271      .serialize_jwt(custom_claims)
272      .map_err(Error::ClaimsSerializationError)?;
273    self
274      .create_jws_pqc(storage, fragment, payload.as_bytes(), options)
275      .await
276      .map(|jws| Jwt::new(jws.into()))
277  }
278
279  async fn create_presentation_jwt_pqc<K, I, CRED, T>(
280    &self,
281    presentation: &Presentation<CRED, T>,
282    storage: &Storage<K, I>,
283    fragment: &str,
284    jws_options: &JwsSignatureOptions,
285    jwt_options: &JwtPresentationOptions,
286  ) -> StorageResult<Jwt>
287  where
288    K: JwkStoragePQ,
289    I: KeyIdStorage,
290    T: Clone + Serialize + DeserializeOwned + Sync,
291    CRED: Clone + Serialize + DeserializeOwned + Sync,
292  {
293    if jws_options.detached_payload {
294      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
295        "cannot use detached payload for presentation signing",
296      )));
297    }
298
299    if !jws_options.b64.unwrap_or(true) {
300      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
301      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
302        "cannot use `b64 = false` with JWTs",
303      )));
304    }
305    let payload = presentation
306      .serialize_jwt(jwt_options)
307      .map_err(Error::ClaimsSerializationError)?;
308    self
309      .create_jws_pqc(storage, fragment, payload.as_bytes(), jws_options)
310      .await
311      .map(|jws| Jwt::new(jws.into()))
312  }
313}
314
315// ====================================================================================================================
316// IotaDocument
317// ====================================================================================================================
318#[cfg(feature = "iota-document")]
319mod iota_document {
320
321  use super::*;
322  use identity_iota_core::IotaDocument;
323
324  generate_method_for_document_type!(
325    IotaDocument,
326    JwsAlgorithm,
327    JwkStoragePQ,
328    JwkStoragePQ::generate_pq_key,
329    generate_method_iota_document
330  );
331
332  #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
333  #[cfg_attr(feature = "send-sync-storage", async_trait)]
334  impl JwsDocumentExtPQC for IotaDocument {
335    async fn generate_method_pqc<K, I>(
336      &mut self,
337      storage: &Storage<K, I>,
338      key_type: KeyType,
339      alg: JwsAlgorithm,
340      fragment: Option<&str>,
341      scope: MethodScope,
342    ) -> StorageResult<String>
343    where
344      K: JwkStoragePQ,
345      I: KeyIdStorage,
346    {
347      generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await
348    }
349
350    async fn create_jws_pqc<K, I>(
351      &self,
352      storage: &Storage<K, I>,
353      fragment: &str,
354      payload: &[u8],
355      options: &JwsSignatureOptions,
356    ) -> StorageResult<Jws>
357    where
358      K: JwkStoragePQ,
359      I: KeyIdStorage,
360    {
361      self
362        .core_document()
363        .create_jws_pqc(storage, fragment, payload, options)
364        .await
365    }
366
367    async fn create_credential_jwt_pqc<K, I, T>(
368      &self,
369      credential: &Credential<T>,
370      storage: &Storage<K, I>,
371      fragment: &str,
372      options: &JwsSignatureOptions,
373      custom_claims: Option<Object>,
374    ) -> StorageResult<Jwt>
375    where
376      K: JwkStoragePQ,
377      I: KeyIdStorage,
378      T: Clone + Serialize + DeserializeOwned + Sync,
379    {
380      self
381        .core_document()
382        .create_credential_jwt_pqc(credential, storage, fragment, options, custom_claims)
383        .await
384    }
385
386    async fn create_presentation_jwt_pqc<K, I, CRED, T>(
387      &self,
388      presentation: &Presentation<CRED, T>,
389      storage: &Storage<K, I>,
390      fragment: &str,
391      jws_options: &JwsSignatureOptions,
392      jwt_options: &JwtPresentationOptions,
393    ) -> StorageResult<Jwt>
394    where
395      K: JwkStoragePQ,
396      I: KeyIdStorage,
397      T: Clone + Serialize + DeserializeOwned + Sync,
398      CRED: Clone + Serialize + DeserializeOwned + Sync,
399    {
400      self
401        .core_document()
402        .create_presentation_jwt_pqc(presentation, storage, fragment, jws_options, jwt_options)
403        .await
404    }
405  }
406}