concordium_rust_sdk/
web3id.rs

1//! Functionality for retrieving, verifying, and registering web3id credentials.
2
3use crate::{
4    cis4::{Cis4Contract, Cis4QueryError},
5    v2::{self, BlockIdentifier, IntoBlockIdentifier},
6};
7pub use concordium_base::web3id::*;
8use concordium_base::{
9    base::CredentialRegistrationID,
10    cis4_types::CredentialStatus,
11    contracts_common::AccountAddress,
12    id::{constants::ArCurve, types::IpIdentity},
13    web3id,
14};
15use futures::TryStreamExt;
16
17#[derive(thiserror::Error, Debug)]
18pub enum CredentialLookupError {
19    #[error("Credential network not supported.")]
20    IncorrectNetwork,
21    #[error("Credential issuer not as stated: {stated} != {actual}.")]
22    InconsistentIssuer {
23        stated: IpIdentity,
24        actual: IpIdentity,
25    },
26    #[error("Unable to look up account: {0}")]
27    QueryError(#[from] v2::QueryError),
28    #[error("Unable to query CIS4 contract: {0}")]
29    Cis4QueryError(#[from] Cis4QueryError),
30    #[error("Credential {cred_id} no longer present on account: {account}")]
31    CredentialNotPresent {
32        cred_id: CredentialRegistrationID,
33        account: AccountAddress,
34    },
35    #[error("Initial credential {cred_id} cannot be used.")]
36    InitialCredential { cred_id: CredentialRegistrationID },
37    #[error("Unexpected response from the node: {0}")]
38    InvalidResponse(String),
39}
40
41/// The public cryptographic data of a credential together with its current
42/// status.
43pub struct CredentialWithMetadata {
44    /// The status of the credential at a point in time.
45    pub status: CredentialStatus,
46    /// The extra public inputs needed for verification.
47    pub inputs: CredentialsInputs<ArCurve>,
48}
49
50/// Retrieve and validate credential metadata in a particular block.
51///
52/// This does not validate the cryptographic proofs, only the metadata. In
53/// particular it checks.
54///
55/// - credential exists
56/// - the credential's network is as supplied to this function
57/// - in case of account credentials, the credential issuer is as stated in the
58///   proof
59/// - credential commitments can be correctly parsed
60/// - credential is active and not expired at the timestamp of the supplied
61///   block
62/// - in case of an account credential, the credential is a normal credential,
63///   and not initial.
64///
65/// For web3id credentials the issuer contract is the source of truth, and this
66/// function does not perform additional validity checks apart from querying the
67/// contract.
68pub async fn verify_credential_metadata(
69    mut client: v2::Client,
70    network: web3id::did::Network,
71    metadata: &ProofMetadata,
72    bi: impl IntoBlockIdentifier,
73) -> Result<CredentialWithMetadata, CredentialLookupError> {
74    if metadata.network != network {
75        return Err(CredentialLookupError::IncorrectNetwork);
76    }
77    let bi = bi.into_block_identifier();
78    match metadata.cred_metadata {
79        CredentialMetadata::Account { issuer, cred_id } => {
80            let ai = client
81                .get_account_info(&cred_id.into(), BlockIdentifier::LastFinal)
82                .await?;
83            let Some(cred) = ai
84                .response
85                .account_credentials
86                .values()
87                .find(|cred| cred.value.cred_id() == cred_id.as_ref())
88            else {
89                return Err(CredentialLookupError::CredentialNotPresent {
90                    cred_id,
91                    account: ai.response.account_address,
92                });
93            };
94            if cred.value.issuer() != issuer {
95                return Err(CredentialLookupError::InconsistentIssuer {
96                    stated: issuer,
97                    actual: cred.value.issuer(),
98                });
99            }
100            match &cred.value {
101                concordium_base::id::types::AccountCredentialWithoutProofs::Initial { .. } => {
102                    Err(CredentialLookupError::InitialCredential { cred_id })
103                }
104                concordium_base::id::types::AccountCredentialWithoutProofs::Normal {
105                    cdv,
106                    commitments,
107                } => {
108                    let now = client.get_block_info(bi).await?.response.block_slot_time;
109                    let valid_from = cdv.policy.created_at.lower().ok_or_else(|| {
110                        CredentialLookupError::InvalidResponse(
111                            "Credential creation date is not valid.".into(),
112                        )
113                    })?;
114                    let valid_until = cdv.policy.valid_to.upper().ok_or_else(|| {
115                        CredentialLookupError::InvalidResponse(
116                            "Credential creation date is not valid.".into(),
117                        )
118                    })?;
119                    let status = if valid_from > now {
120                        CredentialStatus::NotActivated
121                    } else if valid_until < now {
122                        CredentialStatus::Expired
123                    } else {
124                        CredentialStatus::Active
125                    };
126                    let inputs = CredentialsInputs::Account {
127                        commitments: commitments.cmm_attributes.clone(),
128                    };
129
130                    Ok(CredentialWithMetadata { status, inputs })
131                }
132            }
133        }
134        CredentialMetadata::Web3Id { contract, holder } => {
135            let mut contract_client = Cis4Contract::create(client, contract).await?;
136            let issuer_pk = contract_client.issuer(bi).await?;
137
138            let inputs = CredentialsInputs::Web3 { issuer_pk };
139
140            let status = contract_client.credential_status(holder, bi).await?;
141
142            Ok(CredentialWithMetadata { status, inputs })
143        }
144    }
145}
146
147/// Retrieve the public data of credentials validating any metadata that is
148/// part of the credentials.
149///
150/// If any credentials from the presentation are from a network different than
151/// the one supplied an error is returned.
152///
153/// See [`verify_credential_metadata`] for the checks performed on each of the
154/// credentials.
155pub async fn get_public_data(
156    client: &mut v2::Client,
157    network: web3id::did::Network,
158    presentation: &web3id::Presentation<ArCurve, web3id::Web3IdAttribute>,
159    bi: impl IntoBlockIdentifier,
160) -> Result<Vec<CredentialWithMetadata>, CredentialLookupError> {
161    let block = bi.into_block_identifier();
162    let stream = presentation
163        .metadata()
164        .map(|meta| {
165            let mainnet_client = client.clone();
166            async move { verify_credential_metadata(mainnet_client, network, &meta, block).await }
167        })
168        .collect::<futures::stream::FuturesOrdered<_>>();
169    stream.try_collect().await
170}