identity_storage/storage/
jwk_document_ext.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use super::JwkStorageDocumentError as Error;
5use super::JwsSignatureOptions;
6use super::Storage;
7
8use crate::key_id_storage::KeyIdStorage;
9use crate::key_id_storage::KeyIdStorageResult;
10use crate::key_id_storage::MethodDigest;
11use crate::key_storage::JwkGenOutput;
12use crate::key_storage::JwkStorage;
13use crate::key_storage::KeyId;
14use crate::key_storage::KeyStorageResult;
15use crate::key_storage::KeyType;
16
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::jose::jws::CompactJwsEncoder;
27use identity_verification::jose::jws::CompactJwsEncodingOptions;
28use identity_verification::jose::jws::JwsAlgorithm;
29use identity_verification::jose::jws::JwsHeader;
30use identity_verification::jws::CharSet;
31use identity_verification::MethodData;
32use identity_verification::MethodScope;
33use identity_verification::VerificationMethod;
34use serde::de::DeserializeOwned;
35use serde::Serialize;
36
37/// Alias for a `Result` with the error type [`Error`].
38pub type StorageResult<T> = Result<T, Error>;
39
40/// Extension trait for JWK-based operations on DID documents.
41///
42/// This trait is deliberately sealed and cannot be implemented by external crates.
43/// The trait only exists as an extension of existing DID documents implemented in
44/// dependent crates. Because those crates cannot also depend on this crate,
45/// the extension trait is necessary. External crates however should simply wrap the methods
46/// on the trait if they wish to reexport them on their DID document type.
47/// This also allows them to use their own error type on those methods.
48#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
49#[cfg_attr(feature = "send-sync-storage", async_trait)]
50pub trait JwkDocumentExt: private::Sealed {
51  /// Generate new key material in the given `storage` and insert a new verification method with the corresponding
52  /// public key material into the DID document.
53  ///
54  /// - If no fragment is given the `kid` of the generated JWK is used, if it is set, otherwise an error is returned.
55  /// - The `key_type` must be compatible with the given `storage`. [`Storage`]s are expected to export key type
56  ///   constants for that use case.
57  ///
58  /// The fragment of the generated method is returned.
59  async fn generate_method<K, I>(
60    &mut self,
61    storage: &Storage<K, I>,
62    key_type: KeyType,
63    alg: JwsAlgorithm,
64    fragment: Option<&str>,
65    scope: MethodScope,
66  ) -> StorageResult<String>
67  where
68    K: JwkStorage,
69    I: KeyIdStorage;
70
71  /// Remove the method identified by the given `id` from the document and delete the corresponding key material in
72  /// the given `storage`.
73  ///
74  /// ## Warning
75  ///
76  /// This will delete the key material permanently and irrecoverably.
77  async fn purge_method<K, I>(&mut self, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
78  where
79    K: JwkStorage,
80    I: KeyIdStorage;
81
82  /// Sign the arbitrary `payload` according to `options` with the storage backed private key corresponding to the
83  /// public key material in the verification method identified by the given `fragment.
84  ///
85  /// Upon success a string representing a JWS encoded according to the Compact JWS Serialization format is returned.
86  /// See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1).
87  async fn create_jws<K, I>(
88    &self,
89    storage: &Storage<K, I>,
90    fragment: &str,
91    payload: &[u8],
92    options: &JwsSignatureOptions,
93  ) -> StorageResult<Jws>
94  where
95    K: JwkStorage,
96    I: KeyIdStorage;
97
98  /// Produces a JWT where the payload is produced from the given `credential`
99  /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
100  ///
101  /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id`
102  /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding
103  /// private key backed by the `storage` in accordance with the passed `options`.
104  ///
105  /// The `custom_claims` can be used to set additional claims on the resulting JWT.
106  async fn create_credential_jwt<K, I, T>(
107    &self,
108    credential: &Credential<T>,
109    storage: &Storage<K, I>,
110    fragment: &str,
111    options: &JwsSignatureOptions,
112    custom_claims: Option<Object>,
113  ) -> StorageResult<Jwt>
114  where
115    K: JwkStorage,
116    I: KeyIdStorage,
117    T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync;
118
119  /// Produces a JWT where the payload is produced from the given `presentation`
120  /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
121  ///
122  /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id`
123  /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding
124  /// private key backed by the `storage` in accordance with the passed `options`.
125  async fn create_presentation_jwt<K, I, CRED, T>(
126    &self,
127    presentation: &Presentation<CRED, T>,
128    storage: &Storage<K, I>,
129    fragment: &str,
130    signature_options: &JwsSignatureOptions,
131    presentation_options: &JwtPresentationOptions,
132  ) -> StorageResult<Jwt>
133  where
134    K: JwkStorage,
135    I: KeyIdStorage,
136    T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
137    CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone + Sync;
138}
139
140mod private {
141  pub trait Sealed {}
142  impl Sealed for identity_document::document::CoreDocument {}
143  #[cfg(feature = "iota-document")]
144  impl Sealed for identity_iota_core::IotaDocument {}
145}
146
147// ====================================================================================================================
148// Implementation
149// ====================================================================================================================
150
151// We want to implement this trait both for CoreDocument and IotaDocument, but the methods that take `&mut self` cannot
152// be implemented in terms of &mut CoreDocument for IotaDocument. To work around this limitation we use macros to avoid
153// copious amounts of repetition.
154// NOTE: If such use of macros becomes very common it is probably better to use the duplicate crate: https://docs.rs/duplicate/latest/duplicate/
155macro_rules! generate_method_for_document_type {
156  ($t:ty, $a:ty, $k:path, $f:path, $name:ident) => {
157    async fn $name<K, I>(
158      document: &mut $t,
159      storage: &Storage<K, I>,
160      key_type: KeyType,
161      alg: $a,
162      fragment: Option<&str>,
163      scope: MethodScope,
164    ) -> StorageResult<String>
165    where
166      K: $k,
167      I: KeyIdStorage,
168    {
169      let JwkGenOutput { key_id, jwk } = $f(storage.key_storage(), key_type, alg)
170        .await
171        .map_err(Error::KeyStorageError)?;
172
173      // Produce a new verification method containing the generated JWK. If this operation fails we handle the error
174      // by attempting to revert key generation before returning an error.
175      let method: VerificationMethod = {
176        match VerificationMethod::new_from_jwk(document.id().clone(), jwk, fragment)
177          .map_err(Error::VerificationMethodConstructionError)
178        {
179          Ok(method) => method,
180          Err(source) => {
181            return Err(try_undo_key_generation(storage, &key_id, source).await);
182          }
183        }
184      };
185
186      // Extract data from method before inserting it into the DID document.
187      let method_digest: MethodDigest = MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError)?;
188      let method_id: DIDUrl = method.id().clone();
189
190      // The fragment is always set on a method, so this error will never occur.
191      let fragment: String = method_id
192        .fragment()
193        .ok_or(identity_verification::Error::MissingIdFragment)
194        .map_err(Error::VerificationMethodConstructionError)?
195        .to_owned();
196
197      // Insert method into document and handle error upon failure.
198      if let Err(error) = document
199        .insert_method(method, scope)
200        .map_err(|_| Error::FragmentAlreadyExists)
201      {
202        return Err(try_undo_key_generation(storage, &key_id, error).await);
203      };
204
205      // Insert the generated `KeyId` into storage under the computed method digest and handle the error if the
206      // operation fails.
207      if let Err(error) = <I as KeyIdStorage>::insert_key_id(&storage.key_id_storage(), method_digest, key_id.clone())
208        .await
209        .map_err(Error::KeyIdStorageError)
210      {
211        // Remove the method from the document as it can no longer be used.
212        let _ = document.remove_method(&method_id);
213        return Err(try_undo_key_generation(storage, &key_id, error).await);
214      }
215
216      Ok(fragment)
217    }
218  };
219}
220
221macro_rules! purge_method_for_document_type {
222  ($t:ty, $name:ident) => {
223    async fn $name<K, I>(document: &mut $t, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
224    where
225      K: JwkStorage,
226      I: KeyIdStorage,
227    {
228      let (method, scope) = document.remove_method_and_scope(id).ok_or(Error::MethodNotFound)?;
229
230      // Obtain method digest and handle error if this operation fails.
231      let method_digest: MethodDigest = match MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError) {
232        Ok(digest) => digest,
233        Err(error) => {
234          // Revert state by reinserting the method before returning the error.
235          let _ = document.insert_method(method, scope);
236          return Err(error);
237        }
238      };
239
240      // Obtain key id and handle error upon failure.
241      let key_id: KeyId = match <I as KeyIdStorage>::get_key_id(&storage.key_id_storage(), &method_digest)
242        .await
243        .map_err(Error::KeyIdStorageError)
244      {
245        Ok(key_id) => key_id,
246        Err(error) => {
247          // Reinsert method before returning.
248          let _ = document.insert_method(method, scope);
249          return Err(error);
250        }
251      };
252
253      // Delete key and key id concurrently.
254      let key_deletion_fut = <K as JwkStorage>::delete(&storage.key_storage(), &key_id);
255      let key_id_deletion_fut = <I as KeyIdStorage>::delete_key_id(&storage.key_id_storage(), &method_digest);
256      let (key_deletion_result, key_id_deletion_result): (KeyStorageResult<()>, KeyIdStorageResult<()>) =
257        futures::join!(key_deletion_fut, key_id_deletion_fut);
258
259      // Check for any errors that may have occurred. Unfortunately this is somewhat involved.
260      match (key_deletion_result, key_id_deletion_result) {
261        (Ok(_), Ok(_)) => Ok(()),
262        (Ok(_), Err(key_id_deletion_error)) => {
263          // Cannot attempt to revert this operation as the JwkStorage may not return the same KeyId when
264          // JwkStorage::insert is called.
265          Err(Error::UndoOperationFailed {
266            message: format!(
267              "cannot undo key deletion: this results in a stray key id stored under packed method digest: {:?}",
268              &method_digest.pack()
269            ),
270            source: Box::new(Error::KeyIdStorageError(key_id_deletion_error)),
271            undo_error: None,
272          })
273        }
274        (Err(key_deletion_error), Ok(_)) => {
275          // Attempt to revert: Reinsert key id and method if possible.
276          if let Err(key_id_insertion_error) =
277            <I as KeyIdStorage>::insert_key_id(&storage.key_id_storage(), (&method_digest).clone(), key_id.clone())
278              .await
279              .map_err(Error::KeyIdStorageError)
280          {
281            Err(Error::UndoOperationFailed {
282              message: format!("cannot revert key id deletion: this results in stray key with key id: {key_id}"),
283              source: Box::new(Error::KeyStorageError(key_deletion_error)),
284              undo_error: Some(Box::new(key_id_insertion_error)),
285            })
286          } else {
287            // KeyId reinsertion succeeded. Now reinsert method.
288            let _ = document.insert_method(method, scope);
289            Err(Error::KeyStorageError(key_deletion_error))
290          }
291        }
292        (Err(_key_deletion_error), Err(key_id_deletion_error)) => {
293          // We assume this means nothing got deleted. Reinsert the method and return one of the errors (perhaps
294          // key_id_deletion_error as we really expect the key id storage to work as expected at this point).
295          let _ = document.insert_method(method, scope);
296          Err(Error::KeyIdStorageError(key_id_deletion_error))
297        }
298      }
299    }
300  };
301}
302
303// ====================================================================================================================
304// CoreDocument
305// ====================================================================================================================
306
307generate_method_for_document_type!(
308  CoreDocument,
309  JwsAlgorithm,
310  JwkStorage,
311  JwkStorage::generate,
312  generate_method_core_document
313);
314purge_method_for_document_type!(CoreDocument, purge_method_core_document);
315
316#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
317#[cfg_attr(feature = "send-sync-storage", async_trait)]
318impl JwkDocumentExt for CoreDocument {
319  async fn generate_method<K, I>(
320    &mut self,
321    storage: &Storage<K, I>,
322    key_type: KeyType,
323    alg: JwsAlgorithm,
324    fragment: Option<&str>,
325    scope: MethodScope,
326  ) -> StorageResult<String>
327  where
328    K: JwkStorage,
329    I: KeyIdStorage,
330  {
331    generate_method_core_document(self, storage, key_type, alg, fragment, scope).await
332  }
333
334  async fn purge_method<K, I>(&mut self, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
335  where
336    K: JwkStorage,
337    I: KeyIdStorage,
338  {
339    purge_method_core_document(self, storage, id).await
340  }
341
342  async fn create_jws<K, I>(
343    &self,
344    storage: &Storage<K, I>,
345    fragment: &str,
346    payload: &[u8],
347    options: &JwsSignatureOptions,
348  ) -> StorageResult<Jws>
349  where
350    K: JwkStorage,
351    I: KeyIdStorage,
352  {
353    // Obtain the method corresponding to the given fragment.
354    let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?;
355    let MethodData::PublicKeyJwk(ref jwk) = method.data() else {
356      return Err(Error::NotPublicKeyJwk);
357    };
358
359    // Extract JwsAlgorithm.
360    let alg: JwsAlgorithm = jwk
361      .alg()
362      .unwrap_or("")
363      .parse()
364      .map_err(|_| Error::InvalidJwsAlgorithm)?;
365
366    // Create JWS header in accordance with options.
367    let header: JwsHeader = {
368      let mut header = JwsHeader::new();
369
370      header.set_alg(alg);
371      if let Some(custom) = &options.custom_header_parameters {
372        header.set_custom(custom.clone())
373      }
374
375      if let Some(ref kid) = options.kid {
376        header.set_kid(kid.clone());
377      } else {
378        header.set_kid(method.id().to_string());
379      }
380
381      if options.attach_jwk {
382        header.set_jwk(jwk.clone())
383      };
384
385      if let Some(b64) = options.b64 {
386        // Follow recommendation in https://datatracker.ietf.org/doc/html/rfc7797#section-7.
387        if !b64 {
388          header.set_b64(b64);
389          header.set_crit(["b64"]);
390        }
391      };
392
393      if let Some(typ) = &options.typ {
394        header.set_typ(typ.clone())
395      } else {
396        // https://www.w3.org/TR/vc-data-model/#jwt-encoding
397        header.set_typ("JWT")
398      }
399
400      if let Some(cty) = &options.cty {
401        header.set_cty(cty.clone())
402      };
403
404      if let Some(url) = &options.url {
405        header.set_url(url.clone())
406      };
407
408      if let Some(nonce) = &options.nonce {
409        header.set_nonce(nonce.clone())
410      };
411
412      header
413    };
414
415    // Get the key identifier corresponding to the given method from the KeyId storage.
416    let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?;
417    let key_id = <I as KeyIdStorage>::get_key_id(storage.key_id_storage(), &method_digest)
418      .await
419      .map_err(Error::KeyIdStorageError)?;
420
421    // Extract Compact JWS encoding options.
422    let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload {
423      // We use this as a default and don't provide the extra UrlSafe check for now.
424      // Applications that require such checks can easily do so after JWS creation.
425      CompactJwsEncodingOptions::NonDetached {
426        charset_requirements: CharSet::Default,
427      }
428    } else {
429      CompactJwsEncodingOptions::Detached
430    };
431
432    let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options)
433      .map_err(|err| Error::EncodingError(err.into()))?;
434    let signature = <K as JwkStorage>::sign(storage.key_storage(), &key_id, jws_encoder.signing_input(), jwk)
435      .await
436      .map_err(Error::KeyStorageError)?;
437    Ok(Jws::new(jws_encoder.into_jws(&signature)))
438  }
439
440  async fn create_credential_jwt<K, I, T>(
441    &self,
442    credential: &Credential<T>,
443    storage: &Storage<K, I>,
444    fragment: &str,
445    options: &JwsSignatureOptions,
446    custom_claims: Option<Object>,
447  ) -> StorageResult<Jwt>
448  where
449    K: JwkStorage,
450    I: KeyIdStorage,
451    T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
452  {
453    if options.detached_payload {
454      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
455        "cannot use detached payload for credential signing",
456      )));
457    }
458
459    if !options.b64.unwrap_or(true) {
460      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
461      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
462        "cannot use `b64 = false` with JWTs",
463      )));
464    }
465
466    let payload = credential
467      .serialize_jwt(custom_claims)
468      .map_err(Error::ClaimsSerializationError)?;
469    self
470      .create_jws(storage, fragment, payload.as_bytes(), options)
471      .await
472      .map(|jws| Jwt::new(jws.into()))
473  }
474
475  async fn create_presentation_jwt<K, I, CRED, T>(
476    &self,
477    presentation: &Presentation<CRED, T>,
478    storage: &Storage<K, I>,
479    fragment: &str,
480    jws_options: &JwsSignatureOptions,
481    jwt_options: &JwtPresentationOptions,
482  ) -> StorageResult<Jwt>
483  where
484    K: JwkStorage,
485    I: KeyIdStorage,
486    T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
487    CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone + Sync,
488  {
489    if jws_options.detached_payload {
490      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
491        "cannot use detached payload for presentation signing",
492      )));
493    }
494
495    if !jws_options.b64.unwrap_or(true) {
496      // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7.
497      return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
498        "cannot use `b64 = false` with JWTs",
499      )));
500    }
501    let payload = presentation
502      .serialize_jwt(jwt_options)
503      .map_err(Error::ClaimsSerializationError)?;
504    self
505      .create_jws(storage, fragment, payload.as_bytes(), jws_options)
506      .await
507      .map(|jws| Jwt::new(jws.into()))
508  }
509}
510
511/// Attempt to revert key generation. If this succeeds the original `source_error` is returned,
512/// otherwise [`JwkStorageDocumentError::UndoOperationFailed`] is returned with the `source_error` attached as
513/// `source`.
514pub(crate) async fn try_undo_key_generation<K, I>(storage: &Storage<K, I>, key_id: &KeyId, source_error: Error) -> Error
515where
516  K: JwkStorage,
517  I: KeyIdStorage,
518{
519  // Undo key generation
520  if let Err(err) = <K as JwkStorage>::delete(storage.key_storage(), key_id).await {
521    Error::UndoOperationFailed {
522      message: format!("unable to delete stray key with id: {}", &key_id),
523      source: Box::new(source_error),
524      undo_error: Some(Box::new(Error::KeyStorageError(err))),
525    }
526  } else {
527    source_error
528  }
529}
530
531// ====================================================================================================================
532// IotaDocument
533// ====================================================================================================================
534#[cfg(feature = "iota-document")]
535mod iota_document {
536  use super::*;
537  use identity_credential::credential::Jwt;
538  use identity_iota_core::IotaDocument;
539
540  generate_method_for_document_type!(
541    IotaDocument,
542    JwsAlgorithm,
543    JwkStorage,
544    JwkStorage::generate,
545    generate_method_iota_document
546  );
547  purge_method_for_document_type!(IotaDocument, purge_method_iota_document);
548
549  #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
550  #[cfg_attr(feature = "send-sync-storage", async_trait)]
551  impl JwkDocumentExt for IotaDocument {
552    async fn generate_method<K, I>(
553      &mut self,
554      storage: &Storage<K, I>,
555      key_type: KeyType,
556      alg: JwsAlgorithm,
557      fragment: Option<&str>,
558      scope: MethodScope,
559    ) -> StorageResult<String>
560    where
561      K: JwkStorage,
562      I: KeyIdStorage,
563    {
564      generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await
565    }
566
567    async fn purge_method<K, I>(&mut self, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
568    where
569      K: JwkStorage,
570      I: KeyIdStorage,
571    {
572      purge_method_iota_document(self, storage, id).await
573    }
574
575    async fn create_jws<K, I>(
576      &self,
577      storage: &Storage<K, I>,
578      fragment: &str,
579      payload: &[u8],
580      options: &JwsSignatureOptions,
581    ) -> StorageResult<Jws>
582    where
583      K: JwkStorage,
584      I: KeyIdStorage,
585    {
586      self
587        .core_document()
588        .create_jws(storage, fragment, payload, options)
589        .await
590    }
591
592    async fn create_credential_jwt<K, I, T>(
593      &self,
594      credential: &Credential<T>,
595      storage: &Storage<K, I>,
596      fragment: &str,
597      options: &JwsSignatureOptions,
598      custom_claims: Option<Object>,
599    ) -> StorageResult<Jwt>
600    where
601      K: JwkStorage,
602      I: KeyIdStorage,
603      T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
604    {
605      self
606        .core_document()
607        .create_credential_jwt(credential, storage, fragment, options, custom_claims)
608        .await
609    }
610    async fn create_presentation_jwt<K, I, CRED, T>(
611      &self,
612      presentation: &Presentation<CRED, T>,
613      storage: &Storage<K, I>,
614      fragment: &str,
615      options: &JwsSignatureOptions,
616      jwt_options: &JwtPresentationOptions,
617    ) -> StorageResult<Jwt>
618    where
619      K: JwkStorage,
620      I: KeyIdStorage,
621      T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
622      CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone + Sync,
623    {
624      self
625        .core_document()
626        .create_presentation_jwt(presentation, storage, fragment, options, jwt_options)
627        .await
628    }
629  }
630}