identity_iota_core/rebased/assets/
public_available_vc.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::ops::Deref;
5
6use anyhow::Context as _;
7use identity_credential::credential::Credential;
8use identity_credential::credential::Jwt;
9use identity_credential::credential::JwtCredential;
10use identity_jose::jwt::JwtHeader;
11use identity_jose::jwu;
12use iota_interaction::types::base_types::ObjectID;
13use iota_interaction::IotaKeySignature;
14use iota_interaction::IotaVerifiableCredential;
15use iota_interaction::OptionalSync;
16use itertools::Itertools;
17use product_common::core_client::CoreClientReadOnly;
18use secret_storage::Signer;
19
20use crate::rebased::client::IdentityClient;
21use crate::rebased::client::IdentityClientReadOnly;
22
23use super::AuthenticatedAsset;
24use super::AuthenticatedAssetBuilder;
25
26/// A publicly available verifiable credential.
27#[derive(Debug, Clone)]
28pub struct PublicAvailableVC {
29  asset: AuthenticatedAsset<IotaVerifiableCredential>,
30  credential: Credential,
31}
32
33impl Deref for PublicAvailableVC {
34  type Target = Credential;
35  fn deref(&self) -> &Self::Target {
36    &self.credential
37  }
38}
39
40impl PublicAvailableVC {
41  /// Get the ID of the asset.
42  pub fn object_id(&self) -> ObjectID {
43    self.asset.id()
44  }
45
46  /// Get the JWT of the credential.
47  pub fn jwt(&self) -> Jwt {
48    String::from_utf8(self.asset.content().data().clone())
49      .map(Jwt::new)
50      .expect("JWT is valid UTF8")
51  }
52
53  /// Create a new publicly available VC.
54  ///
55  /// # Returns
56  /// A new `PublicAvailableVC`.
57  pub async fn new<S>(jwt: Jwt, gas_budget: Option<u64>, client: &IdentityClient<S>) -> Result<Self, anyhow::Error>
58  where
59    S: Signer<IotaKeySignature> + OptionalSync,
60  {
61    let jwt_bytes = String::from(jwt).into_bytes();
62    let credential = parse_jwt_credential(&jwt_bytes)?;
63    let tx_builder = AuthenticatedAssetBuilder::new(IotaVerifiableCredential::new(jwt_bytes))
64      .transferable(false)
65      .mutable(true)
66      .deletable(true)
67      .finish(client);
68
69    let tx_builder = if let Some(gas_budget) = gas_budget {
70      tx_builder.with_gas_budget(gas_budget)
71    } else {
72      tx_builder
73    };
74
75    let asset = tx_builder.build_and_execute(client).await?.output;
76
77    Ok(Self { credential, asset })
78  }
79
80  /// Get a publicly available VC by its ID.
81  pub async fn get_by_id(id: ObjectID, client: &IdentityClientReadOnly) -> Result<Self, crate::rebased::Error> {
82    let asset = client
83      .get_object_by_id::<AuthenticatedAsset<IotaVerifiableCredential>>(id)
84      .await?;
85
86    Self::try_from_asset(asset).map_err(|e| {
87      crate::rebased::Error::ObjectLookup(format!(
88        "object at address {id} is not a valid publicly available VC: {e}"
89      ))
90    })
91  }
92
93  fn try_from_asset(asset: AuthenticatedAsset<IotaVerifiableCredential>) -> Result<Self, anyhow::Error> {
94    let credential = parse_jwt_credential(asset.content().data())?;
95    Ok(Self { asset, credential })
96  }
97}
98
99fn parse_jwt_credential(bytes: &[u8]) -> Result<Credential, anyhow::Error> {
100  let [header, payload, _signature]: [Vec<u8>; 3] = bytes
101    .split(|c| *c == b'.')
102    .map(jwu::decode_b64)
103    .try_collect::<_, Vec<_>, _>()?
104    .try_into()
105    .map_err(|_| anyhow::anyhow!("invalid JWT"))?;
106  let _header = serde_json::from_slice::<JwtHeader>(&header)?;
107  let credential_claims = serde_json::from_slice::<JwtCredential>(&payload)?;
108  credential_claims.try_into().context("invalid jwt credential claims")
109}