Skip to main content

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