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 or of unknown type on account: {account}")]
31    CredentialNotPresentOrUnknown {
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    #[error("Unknown stored credential for {cred_id}. Updating the rust-sdk to a version compatible with the node will resolve this issue.")]
40    UnknownCredential { cred_id: CredentialRegistrationID },
41}
42
43/// The public cryptographic data of a credential together with its current
44/// status.
45pub struct CredentialWithMetadata {
46    /// The status of the credential at a point in time.
47    pub status: CredentialStatus,
48    /// The extra public inputs needed for verification.
49    pub inputs: CredentialsInputs<ArCurve>,
50}
51
52/// Retrieve and validate credential metadata in a particular block.
53///
54/// This does not validate the cryptographic proofs, only the metadata. In
55/// particular it checks.
56///
57/// - credential exists
58/// - the credential's network is as supplied to this function
59/// - in case of account credentials, the credential issuer is as stated in the
60///   proof
61/// - credential commitments can be correctly parsed
62/// - credential is active and not expired at the timestamp of the supplied
63///   block
64/// - in case of an account credential, the credential is a normal credential,
65///   and not initial.
66///
67/// For web3id credentials the issuer contract is the source of truth, and this
68/// function does not perform additional validity checks apart from querying the
69/// contract.
70pub async fn verify_credential_metadata(
71    mut client: v2::Client,
72    network: web3id::did::Network,
73    metadata: &ProofMetadata,
74    bi: impl IntoBlockIdentifier,
75) -> Result<CredentialWithMetadata, CredentialLookupError> {
76    if metadata.network != network {
77        return Err(CredentialLookupError::IncorrectNetwork);
78    }
79    let bi = bi.into_block_identifier();
80    match metadata.cred_metadata {
81        CredentialMetadata::Account { issuer, cred_id } => {
82            let ai = client
83                .get_account_info(&cred_id.into(), BlockIdentifier::LastFinal)
84                .await?;
85            let Some(cred) = ai.response.account_credentials.values().find(|cred| {
86                cred.value
87                    .as_ref()
88                    .is_known_and(|c| c.cred_id() == cred_id.as_ref())
89            }) else {
90                return Err(CredentialLookupError::CredentialNotPresentOrUnknown {
91                    cred_id,
92                    account: ai.response.account_address,
93                });
94            };
95            let c = cred
96                .value
97                .as_ref()
98                .known_or(CredentialLookupError::UnknownCredential { cred_id })?;
99            if c.issuer() != issuer {
100                return Err(CredentialLookupError::InconsistentIssuer {
101                    stated: issuer,
102                    actual: c.issuer(),
103                });
104            }
105            match &c {
106                concordium_base::id::types::AccountCredentialWithoutProofs::Initial { .. } => {
107                    Err(CredentialLookupError::InitialCredential { cred_id })
108                }
109                concordium_base::id::types::AccountCredentialWithoutProofs::Normal {
110                    cdv,
111                    commitments,
112                } => {
113                    let now = client.get_block_info(bi).await?.response.block_slot_time;
114                    let valid_from = cdv.policy.created_at.lower().ok_or_else(|| {
115                        CredentialLookupError::InvalidResponse(
116                            "Credential creation date is not valid.".into(),
117                        )
118                    })?;
119                    let valid_until = cdv.policy.valid_to.upper().ok_or_else(|| {
120                        CredentialLookupError::InvalidResponse(
121                            "Credential creation date is not valid.".into(),
122                        )
123                    })?;
124                    let status = if valid_from > now {
125                        CredentialStatus::NotActivated
126                    } else if valid_until < now {
127                        CredentialStatus::Expired
128                    } else {
129                        CredentialStatus::Active
130                    };
131                    let inputs = CredentialsInputs::Account {
132                        commitments: commitments.cmm_attributes.clone(),
133                    };
134
135                    Ok(CredentialWithMetadata { status, inputs })
136                }
137            }
138        }
139        CredentialMetadata::Web3Id { contract, holder } => {
140            let mut contract_client = Cis4Contract::create(client, contract).await?;
141            let issuer_pk = contract_client.issuer(bi).await?;
142
143            let inputs = CredentialsInputs::Web3 { issuer_pk };
144
145            let status = contract_client.credential_status(holder, bi).await?;
146
147            Ok(CredentialWithMetadata { status, inputs })
148        }
149    }
150}
151
152/// Retrieve the public data of credentials validating any metadata that is
153/// part of the credentials.
154///
155/// If any credentials from the presentation are from a network different than
156/// the one supplied an error is returned.
157///
158/// See [`verify_credential_metadata`] for the checks performed on each of the
159/// credentials.
160pub async fn get_public_data(
161    client: &mut v2::Client,
162    network: web3id::did::Network,
163    presentation: &web3id::Presentation<ArCurve, web3id::Web3IdAttribute>,
164    bi: impl IntoBlockIdentifier,
165) -> Result<Vec<CredentialWithMetadata>, CredentialLookupError> {
166    let block = bi.into_block_identifier();
167    let stream = presentation
168        .metadata()
169        .map(|meta| {
170            let mainnet_client = client.clone();
171            async move { verify_credential_metadata(mainnet_client, network, &meta, block).await }
172        })
173        .collect::<futures::stream::FuturesOrdered<_>>();
174    stream.try_collect().await
175}