use super::JwkStorageDocumentError as Error;
use super::JwsSignatureOptions;
use super::Storage;
use crate::key_id_storage::KeyIdStorage;
use crate::key_id_storage::KeyIdStorageResult;
use crate::key_id_storage::MethodDigest;
use crate::key_storage::JwkGenOutput;
use crate::key_storage::JwkStorage;
use crate::key_storage::KeyId;
use crate::key_storage::KeyStorageResult;
use crate::key_storage::KeyType;
use async_trait::async_trait;
use identity_core::common::Object;
use identity_credential::credential::Credential;
use identity_credential::credential::Jws;
use identity_credential::credential::Jwt;
use identity_credential::presentation::JwtPresentationOptions;
use identity_credential::presentation::Presentation;
use identity_did::DIDUrl;
use identity_document::document::CoreDocument;
use identity_verification::jose::jws::CompactJwsEncoder;
use identity_verification::jose::jws::CompactJwsEncodingOptions;
use identity_verification::jose::jws::JwsAlgorithm;
use identity_verification::jose::jws::JwsHeader;
use identity_verification::jws::CharSet;
use identity_verification::MethodData;
use identity_verification::MethodScope;
use identity_verification::VerificationMethod;
use serde::de::DeserializeOwned;
use serde::Serialize;
pub type StorageResult<T> = Result<T, Error>;
#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
#[cfg_attr(feature = "send-sync-storage", async_trait)]
pub trait JwkDocumentExt: private::Sealed {
async fn generate_method<K, I>(
&mut self,
storage: &Storage<K, I>,
key_type: KeyType,
alg: JwsAlgorithm,
fragment: Option<&str>,
scope: MethodScope,
) -> StorageResult<String>
where
K: JwkStorage,
I: KeyIdStorage;
async fn purge_method<K, I>(&mut self, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
where
K: JwkStorage,
I: KeyIdStorage;
async fn create_jws<K, I>(
&self,
storage: &Storage<K, I>,
fragment: &str,
payload: &[u8],
options: &JwsSignatureOptions,
) -> StorageResult<Jws>
where
K: JwkStorage,
I: KeyIdStorage;
async fn create_credential_jwt<K, I, T>(
&self,
credential: &Credential<T>,
storage: &Storage<K, I>,
fragment: &str,
options: &JwsSignatureOptions,
custom_claims: Option<Object>,
) -> StorageResult<Jwt>
where
K: JwkStorage,
I: KeyIdStorage,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync;
async fn create_presentation_jwt<K, I, CRED, T>(
&self,
presentation: &Presentation<CRED, T>,
storage: &Storage<K, I>,
fragment: &str,
signature_options: &JwsSignatureOptions,
presentation_options: &JwtPresentationOptions,
) -> StorageResult<Jwt>
where
K: JwkStorage,
I: KeyIdStorage,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone + Sync;
}
mod private {
pub trait Sealed {}
impl Sealed for identity_document::document::CoreDocument {}
#[cfg(feature = "iota-document")]
impl Sealed for identity_iota_core::IotaDocument {}
}
macro_rules! generate_method_for_document_type {
($t:ty, $a:ty, $k:path, $f:path, $name:ident) => {
async fn $name<K, I>(
document: &mut $t,
storage: &Storage<K, I>,
key_type: KeyType,
alg: $a,
fragment: Option<&str>,
scope: MethodScope,
) -> StorageResult<String>
where
K: $k,
I: KeyIdStorage,
{
let JwkGenOutput { key_id, jwk } = $f(storage.key_storage(), key_type, alg)
.await
.map_err(Error::KeyStorageError)?;
let method: VerificationMethod = {
match VerificationMethod::new_from_jwk(document.id().clone(), jwk, fragment)
.map_err(Error::VerificationMethodConstructionError)
{
Ok(method) => method,
Err(source) => {
return Err(try_undo_key_generation(storage, &key_id, source).await);
}
}
};
let method_digest: MethodDigest = MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError)?;
let method_id: DIDUrl = method.id().clone();
let fragment: String = method_id
.fragment()
.ok_or(identity_verification::Error::MissingIdFragment)
.map_err(Error::VerificationMethodConstructionError)?
.to_owned();
if let Err(error) = document
.insert_method(method, scope)
.map_err(|_| Error::FragmentAlreadyExists)
{
return Err(try_undo_key_generation(storage, &key_id, error).await);
};
if let Err(error) = <I as KeyIdStorage>::insert_key_id(&storage.key_id_storage(), method_digest, key_id.clone())
.await
.map_err(Error::KeyIdStorageError)
{
let _ = document.remove_method(&method_id);
return Err(try_undo_key_generation(storage, &key_id, error).await);
}
Ok(fragment)
}
};
}
macro_rules! purge_method_for_document_type {
($t:ty, $name:ident) => {
async fn $name<K, I>(document: &mut $t, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
where
K: JwkStorage,
I: KeyIdStorage,
{
let (method, scope) = document.remove_method_and_scope(id).ok_or(Error::MethodNotFound)?;
let method_digest: MethodDigest = match MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError) {
Ok(digest) => digest,
Err(error) => {
let _ = document.insert_method(method, scope);
return Err(error);
}
};
let key_id: KeyId = match <I as KeyIdStorage>::get_key_id(&storage.key_id_storage(), &method_digest)
.await
.map_err(Error::KeyIdStorageError)
{
Ok(key_id) => key_id,
Err(error) => {
let _ = document.insert_method(method, scope);
return Err(error);
}
};
let key_deletion_fut = <K as JwkStorage>::delete(&storage.key_storage(), &key_id);
let key_id_deletion_fut = <I as KeyIdStorage>::delete_key_id(&storage.key_id_storage(), &method_digest);
let (key_deletion_result, key_id_deletion_result): (KeyStorageResult<()>, KeyIdStorageResult<()>) =
futures::join!(key_deletion_fut, key_id_deletion_fut);
match (key_deletion_result, key_id_deletion_result) {
(Ok(_), Ok(_)) => Ok(()),
(Ok(_), Err(key_id_deletion_error)) => {
Err(Error::UndoOperationFailed {
message: format!(
"cannot undo key deletion: this results in a stray key id stored under packed method digest: {:?}",
&method_digest.pack()
),
source: Box::new(Error::KeyIdStorageError(key_id_deletion_error)),
undo_error: None,
})
}
(Err(key_deletion_error), Ok(_)) => {
if let Err(key_id_insertion_error) =
<I as KeyIdStorage>::insert_key_id(&storage.key_id_storage(), (&method_digest).clone(), key_id.clone())
.await
.map_err(Error::KeyIdStorageError)
{
Err(Error::UndoOperationFailed {
message: format!("cannot revert key id deletion: this results in stray key with key id: {key_id}"),
source: Box::new(Error::KeyStorageError(key_deletion_error)),
undo_error: Some(Box::new(key_id_insertion_error)),
})
} else {
let _ = document.insert_method(method, scope);
Err(Error::KeyStorageError(key_deletion_error))
}
}
(Err(_key_deletion_error), Err(key_id_deletion_error)) => {
let _ = document.insert_method(method, scope);
Err(Error::KeyIdStorageError(key_id_deletion_error))
}
}
}
};
}
generate_method_for_document_type!(
CoreDocument,
JwsAlgorithm,
JwkStorage,
JwkStorage::generate,
generate_method_core_document
);
purge_method_for_document_type!(CoreDocument, purge_method_core_document);
#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
#[cfg_attr(feature = "send-sync-storage", async_trait)]
impl JwkDocumentExt for CoreDocument {
async fn generate_method<K, I>(
&mut self,
storage: &Storage<K, I>,
key_type: KeyType,
alg: JwsAlgorithm,
fragment: Option<&str>,
scope: MethodScope,
) -> StorageResult<String>
where
K: JwkStorage,
I: KeyIdStorage,
{
generate_method_core_document(self, storage, key_type, alg, fragment, scope).await
}
async fn purge_method<K, I>(&mut self, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
where
K: JwkStorage,
I: KeyIdStorage,
{
purge_method_core_document(self, storage, id).await
}
async fn create_jws<K, I>(
&self,
storage: &Storage<K, I>,
fragment: &str,
payload: &[u8],
options: &JwsSignatureOptions,
) -> StorageResult<Jws>
where
K: JwkStorage,
I: KeyIdStorage,
{
let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?;
let MethodData::PublicKeyJwk(ref jwk) = method.data() else {
return Err(Error::NotPublicKeyJwk);
};
let alg: JwsAlgorithm = jwk
.alg()
.unwrap_or("")
.parse()
.map_err(|_| Error::InvalidJwsAlgorithm)?;
let header: JwsHeader = {
let mut header = JwsHeader::new();
header.set_alg(alg);
if let Some(custom) = &options.custom_header_parameters {
header.set_custom(custom.clone())
}
if let Some(ref kid) = options.kid {
header.set_kid(kid.clone());
} else {
header.set_kid(method.id().to_string());
}
if options.attach_jwk {
header.set_jwk(jwk.clone())
};
if let Some(b64) = options.b64 {
if !b64 {
header.set_b64(b64);
header.set_crit(["b64"]);
}
};
if let Some(typ) = &options.typ {
header.set_typ(typ.clone())
} else {
header.set_typ("JWT")
}
if let Some(cty) = &options.cty {
header.set_cty(cty.clone())
};
if let Some(url) = &options.url {
header.set_url(url.clone())
};
if let Some(nonce) = &options.nonce {
header.set_nonce(nonce.clone())
};
header
};
let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?;
let key_id = <I as KeyIdStorage>::get_key_id(storage.key_id_storage(), &method_digest)
.await
.map_err(Error::KeyIdStorageError)?;
let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload {
CompactJwsEncodingOptions::NonDetached {
charset_requirements: CharSet::Default,
}
} else {
CompactJwsEncodingOptions::Detached
};
let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options)
.map_err(|err| Error::EncodingError(err.into()))?;
let signature = <K as JwkStorage>::sign(storage.key_storage(), &key_id, jws_encoder.signing_input(), jwk)
.await
.map_err(Error::KeyStorageError)?;
Ok(Jws::new(jws_encoder.into_jws(&signature)))
}
async fn create_credential_jwt<K, I, T>(
&self,
credential: &Credential<T>,
storage: &Storage<K, I>,
fragment: &str,
options: &JwsSignatureOptions,
custom_claims: Option<Object>,
) -> StorageResult<Jwt>
where
K: JwkStorage,
I: KeyIdStorage,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
{
if options.detached_payload {
return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
"cannot use detached payload for credential signing",
)));
}
if !options.b64.unwrap_or(true) {
return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
"cannot use `b64 = false` with JWTs",
)));
}
let payload = credential
.serialize_jwt(custom_claims)
.map_err(Error::ClaimsSerializationError)?;
self
.create_jws(storage, fragment, payload.as_bytes(), options)
.await
.map(|jws| Jwt::new(jws.into()))
}
async fn create_presentation_jwt<K, I, CRED, T>(
&self,
presentation: &Presentation<CRED, T>,
storage: &Storage<K, I>,
fragment: &str,
jws_options: &JwsSignatureOptions,
jwt_options: &JwtPresentationOptions,
) -> StorageResult<Jwt>
where
K: JwkStorage,
I: KeyIdStorage,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone + Sync,
{
if jws_options.detached_payload {
return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
"cannot use detached payload for presentation signing",
)));
}
if !jws_options.b64.unwrap_or(true) {
return Err(Error::EncodingError(Box::<dyn std::error::Error + Send + Sync>::from(
"cannot use `b64 = false` with JWTs",
)));
}
let payload = presentation
.serialize_jwt(jwt_options)
.map_err(Error::ClaimsSerializationError)?;
self
.create_jws(storage, fragment, payload.as_bytes(), jws_options)
.await
.map(|jws| Jwt::new(jws.into()))
}
}
pub(crate) async fn try_undo_key_generation<K, I>(storage: &Storage<K, I>, key_id: &KeyId, source_error: Error) -> Error
where
K: JwkStorage,
I: KeyIdStorage,
{
if let Err(err) = <K as JwkStorage>::delete(storage.key_storage(), key_id).await {
Error::UndoOperationFailed {
message: format!("unable to delete stray key with id: {}", &key_id),
source: Box::new(source_error),
undo_error: Some(Box::new(Error::KeyStorageError(err))),
}
} else {
source_error
}
}
#[cfg(feature = "iota-document")]
mod iota_document {
use super::*;
use identity_credential::credential::Jwt;
use identity_iota_core::IotaDocument;
generate_method_for_document_type!(
IotaDocument,
JwsAlgorithm,
JwkStorage,
JwkStorage::generate,
generate_method_iota_document
);
purge_method_for_document_type!(IotaDocument, purge_method_iota_document);
#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
#[cfg_attr(feature = "send-sync-storage", async_trait)]
impl JwkDocumentExt for IotaDocument {
async fn generate_method<K, I>(
&mut self,
storage: &Storage<K, I>,
key_type: KeyType,
alg: JwsAlgorithm,
fragment: Option<&str>,
scope: MethodScope,
) -> StorageResult<String>
where
K: JwkStorage,
I: KeyIdStorage,
{
generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await
}
async fn purge_method<K, I>(&mut self, storage: &Storage<K, I>, id: &DIDUrl) -> StorageResult<()>
where
K: JwkStorage,
I: KeyIdStorage,
{
purge_method_iota_document(self, storage, id).await
}
async fn create_jws<K, I>(
&self,
storage: &Storage<K, I>,
fragment: &str,
payload: &[u8],
options: &JwsSignatureOptions,
) -> StorageResult<Jws>
where
K: JwkStorage,
I: KeyIdStorage,
{
self
.core_document()
.create_jws(storage, fragment, payload, options)
.await
}
async fn create_credential_jwt<K, I, T>(
&self,
credential: &Credential<T>,
storage: &Storage<K, I>,
fragment: &str,
options: &JwsSignatureOptions,
custom_claims: Option<Object>,
) -> StorageResult<Jwt>
where
K: JwkStorage,
I: KeyIdStorage,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
{
self
.core_document()
.create_credential_jwt(credential, storage, fragment, options, custom_claims)
.await
}
async fn create_presentation_jwt<K, I, CRED, T>(
&self,
presentation: &Presentation<CRED, T>,
storage: &Storage<K, I>,
fragment: &str,
options: &JwsSignatureOptions,
jwt_options: &JwtPresentationOptions,
) -> StorageResult<Jwt>
where
K: JwkStorage,
I: KeyIdStorage,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned + Sync,
CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone + Sync,
{
self
.core_document()
.create_presentation_jwt(presentation, storage, fragment, options, jwt_options)
.await
}
}
}