Skip to main content

concordium_rust_sdk/web3id/
v1.rs

1//! Functionality for requesting and verifying V1 Concordium verifiable presentations.
2//!
3//! A verification flow consists of multiple stages:
4//!
5//! 1. Create a [`VerificationRequest`]: A verification flow is started by constructing [`VerificationRequestData`]
6//!    and creating the [`VerificationRequest`] with [`create_verification_request_and_submit_request_anchor`] which also
7//!    submits the corresponding [`VerificationRequestAnchor`] (VRA) on chain.
8//!
9//! 2. Generate and prove [`VerifiablePresentationV1`]: The claims in the [`VerificationRequest`] are
10//!    proved by a credential holder in the context specified in the request and
11//!    embedded in a [`VerifiablePresentationV1`] together with the context and proofs.
12//!    The prover is implemented in [`VerifiablePresentationRequestV1::prove`](anchor::VerifiablePresentationRequestV1::prove).
13//!
14//! 3. Verify a [`VerifiablePresentationV1`]: The presentation can be verified together with
15//!    the verification request with [`verify_presentation_and_submit_audit_anchor`], which submits
16//!    and [`VerificationAuditRecord`] (VAA) on chain and returns the [`VerificationAuditRecord`] to
17//!    be stored locally by the verifier.
18//!
19//! 4. Verify an [`VerificationAuditRecord`]: The stored audit record can be re-verified with
20//!    [`verify_audit_record`] if/when needed.
21//!
22//! The example `web3id_v1_verification_flow` demonstrates the verification flow.
23
24use crate::types::{AccountTransactionEffects, BlockItemSummaryDetails};
25use crate::v2;
26
27use crate::endpoints::RPCError;
28use crate::smart_contracts::common::AccountAddress;
29use crate::v2::{AccountIdentifier, BlockIdentifier, IntoBlockIdentifier, QueryError};
30use concordium_base::base::{CredentialRegistrationID, Nonce};
31use concordium_base::common::cbor;
32use concordium_base::common::cbor::CborSerializationError;
33use concordium_base::common::upward::UnknownDataError;
34use concordium_base::hashes::TransactionHash;
35use concordium_base::id::types;
36use concordium_base::id::types::{AccountCredentialWithoutProofs, ArInfos, IpIdentity};
37use concordium_base::transactions::{
38    send, BlockItem, ExactSizeTransactionSigner, RegisteredData, TooLargeError,
39};
40use concordium_base::web3id::v1::anchor::{
41    self as anchor, CredentialValidityType, PresentationVerificationResult,
42    VerifiablePresentationV1, VerificationAuditRecord, VerificationContext, VerificationMaterial,
43    VerificationMaterialWithValidity, VerificationRequest, VerificationRequestAnchor,
44    VerificationRequestAnchorAndBlockHash, VerificationRequestData,
45};
46use concordium_base::web3id::v1::{
47    AccountCredentialVerificationMaterial, CredentialMetadataTypeV1, CredentialMetadataV1,
48    IdentityCredentialVerificationMaterial,
49};
50use concordium_base::{hashes, web3id};
51
52use concordium_base::common::types::TransactionTime;
53use futures::StreamExt;
54use futures::{future, TryStreamExt};
55use std::collections::{BTreeMap, HashMap};
56
57/// Data returned from verifying a presentation against the corresponding verification request.
58/// Contains the verification result, the audit record and the transaction hash
59/// for the transaction registering the verification audit anchor (VAA) on-chain in case
60/// the verification was successful.
61/// The audit record should be stored in an off-chain database for regulatory purposes
62/// and should generally be kept private.
63#[derive(Debug, Clone, PartialEq)]
64pub struct PresentationVerificationData {
65    // Whether the verification was successful. If not [`PresentationVerificationResult::Verified`],
66    // the verifiable presentation is not valid and the credentials and claims in it are not verified to be true.
67    pub verification_result: PresentationVerificationResult,
68    /// The verification audit record. Notice that the existence of the audit record
69    /// does not mean that verification was successful, that is specified
70    /// by [`Self::verification_result`]. The audit record should be stored in an off-chain database for regulatory purposes
71    /// and should generally be kept private.
72    /// A corresponding [`VerificationRequestAnchor`] (VAA) is submitted
73    /// on chain, if the verification is successful.
74    pub audit_record: VerificationAuditRecord,
75    /// Blockchain transaction hash for the transaction that registers
76    /// the [verification audit record](anchor::VerificationAuditAnchor) (VAA) on-chain.
77    /// Notice that this transaction may not have been finalized yet. Also, the anchor is
78    /// only submitted if the verification is successful.
79    pub anchor_transaction_hash: Option<hashes::TransactionHash>,
80}
81
82/// Error verifying presentation
83#[derive(thiserror::Error, Debug)]
84pub enum VerifyError {
85    #[error("on-chain request anchor transaction is of invalid type")]
86    InvalidRequestAnchor,
87    #[error("on-chain request anchor transaction not finalized yet")]
88    RequestAnchorNotFinalized,
89    #[error("unknown data error: {0}")]
90    UnknownDataError(#[from] UnknownDataError),
91    #[error("node query error: {0}")]
92    Query(#[from] v2::QueryError),
93    #[error("create anchor: {0}")]
94    Anchor(#[from] CreateAnchorError),
95    #[error("CBOR serialization error: {0}")]
96    CborSerialization(#[from] CborSerializationError),
97    #[error("unknown identity provider: {0}")]
98    UnknownIdentityProvider(IpIdentity),
99    #[error("credential {cred_id} no longer present on account: {account}")]
100    CredentialNotPresent {
101        cred_id: CredentialRegistrationID,
102        account: AccountAddress,
103    },
104    #[error("initial credential {cred_id} cannot be used.")]
105    InitialCredential { cred_id: CredentialRegistrationID },
106}
107
108/// Metadata for transaction submission.
109pub struct AuditRecordArgument<S: ExactSizeTransactionSigner> {
110    /// Id of the audit record to create. Is fully determined by the verifier/caller.
111    pub audit_record_id: String,
112    /// Public information to be included in the audit record anchor (VAA) on-chain.
113    pub public_info: Option<HashMap<String, cbor::value::Value>>,
114    /// Metadata for the anchor transaction that submits the audit record anchor (VAA) on-chain.
115    pub audit_record_anchor_transaction_metadata: AnchorTransactionMetadata<S>,
116}
117
118/// Verify the given verifiable presentation together with the given verification request.
119/// The aspects validated are documented on [`anchor::verify_presentation_with_request_anchor`].
120/// The verification returns an audit record containing the presentation and verification request.
121/// In case the verification is successful, a [verification audit record](anchor::VerificationAuditAnchor)
122/// (VRA) is submitted on chain.
123/// Notice that when this method returns, the anchor is only submitted. The returned anchor transaction
124/// hash must be tracked in order to determine if it is finalized.
125pub async fn verify_presentation_and_submit_audit_anchor(
126    client: &mut v2::Client,
127    network: web3id::did::Network,
128    block_identifier: impl IntoBlockIdentifier,
129    verification_request: VerificationRequest,
130    verifiable_presentation: VerifiablePresentationV1,
131    audit_record_arg: AuditRecordArgument<impl ExactSizeTransactionSigner>,
132) -> Result<PresentationVerificationData, VerifyError> {
133    let block_identifier = block_identifier.into_block_identifier();
134    let global_context = client
135        .get_cryptographic_parameters(block_identifier)
136        .await?
137        .response;
138
139    let block_info = client.get_block_info(block_identifier).await?.response;
140
141    let request_anchor = lookup_request_anchor(client, &verification_request).await?;
142
143    let verification_material = lookup_verification_materials_and_validity(
144        client,
145        block_identifier,
146        &verifiable_presentation,
147    )
148    .await?;
149
150    let context = VerificationContext {
151        network,
152        validity_time: block_info.block_slot_time,
153    };
154
155    let verification_result = anchor::verify_presentation_with_request_anchor(
156        &global_context,
157        &context,
158        &verification_request,
159        &verifiable_presentation,
160        &request_anchor,
161        &verification_material,
162    );
163
164    let audit_record = VerificationAuditRecord::new(
165        audit_record_arg.audit_record_id,
166        verification_request,
167        verifiable_presentation,
168    );
169
170    let anchor_transaction_hash = if verification_result.is_success() {
171        let txn_hash = submit_verification_audit_record_anchor(
172            client,
173            audit_record_arg.audit_record_anchor_transaction_metadata,
174            &audit_record,
175            audit_record_arg.public_info,
176        )
177        .await?;
178        Some(txn_hash)
179    } else {
180        None
181    };
182
183    Ok(PresentationVerificationData {
184        verification_result,
185        audit_record,
186        anchor_transaction_hash,
187    })
188}
189
190/// Verify the verifiable presentation in the audit record together with the
191/// verification request in the audit record. The aspects validated are documented on
192/// [`anchor::verify_presentation_with_request_anchor`].
193pub async fn verify_audit_record(
194    client: &mut v2::Client,
195    network: web3id::did::Network,
196    block_identifier: impl IntoBlockIdentifier,
197    verification_audit_record: &VerificationAuditRecord,
198) -> Result<PresentationVerificationResult, VerifyError> {
199    let block_identifier = block_identifier.into_block_identifier();
200    let global_context = client
201        .get_cryptographic_parameters(block_identifier)
202        .await?
203        .response;
204
205    let block_info = client.get_block_info(block_identifier).await?.response;
206
207    let request_anchor = lookup_request_anchor(client, &verification_audit_record.request).await?;
208
209    let verification_material = lookup_verification_materials_and_validity(
210        client,
211        block_identifier,
212        &verification_audit_record.presentation,
213    )
214    .await?;
215
216    let context = VerificationContext {
217        network,
218        validity_time: block_info.block_slot_time,
219    };
220
221    Ok(anchor::verify_presentation_with_request_anchor(
222        &global_context,
223        &context,
224        &verification_audit_record.request,
225        &verification_audit_record.presentation,
226        &request_anchor,
227        &verification_material,
228    ))
229}
230
231/// Looks up the verifiable request anchor (VRA) from the verification
232/// request.
233pub async fn lookup_request_anchor(
234    client: &mut v2::Client,
235    verification_request: &VerificationRequest,
236) -> Result<VerificationRequestAnchorAndBlockHash, VerifyError> {
237    // Fetch the transaction
238    let item_status = client
239        .get_block_item_status(&verification_request.anchor_transaction_hash)
240        .await?;
241
242    let (block_hash, summary) = item_status
243        .is_finalized()
244        .ok_or(VerifyError::RequestAnchorNotFinalized)?;
245
246    // Extract account transaction
247    let BlockItemSummaryDetails::AccountTransaction(anchor_tx) =
248        summary.details.as_ref().known_or_err()?
249    else {
250        return Err(VerifyError::InvalidRequestAnchor);
251    };
252
253    // Extract data registered payload
254    let AccountTransactionEffects::DataRegistered { data } =
255        anchor_tx.effects.as_ref().known_or_err()?
256    else {
257        return Err(VerifyError::InvalidRequestAnchor);
258    };
259
260    // Decode anchor hash
261    let verification_request_anchor: VerificationRequestAnchor = cbor::cbor_decode(data.as_ref())?;
262
263    Ok(VerificationRequestAnchorAndBlockHash {
264        verification_request_anchor,
265        block_hash: *block_hash,
266    })
267}
268
269/// Lookup the verification material needed to verify the presentation.
270pub async fn lookup_verification_materials_and_validity(
271    client: &mut v2::Client,
272    block_identifier: BlockIdentifier,
273    presentation: &VerifiablePresentationV1,
274) -> Result<Vec<VerificationMaterialWithValidity>, VerifyError> {
275    let verification_material =
276        future::try_join_all(presentation.metadata().map(|cred_metadata| {
277            let mut client = client.clone();
278            async move {
279                lookup_verification_material_and_validity(
280                    &mut client,
281                    block_identifier,
282                    &cred_metadata,
283                )
284                .await
285            }
286        }))
287        .await?;
288    Ok(verification_material)
289}
290
291/// Lookup verification material for presentation
292async fn lookup_verification_material_and_validity(
293    client: &mut v2::Client,
294    block_identifier: BlockIdentifier,
295    cred_metadata: &CredentialMetadataV1,
296) -> Result<VerificationMaterialWithValidity, VerifyError> {
297    Ok(match &cred_metadata.cred_metadata {
298        CredentialMetadataTypeV1::Account(metadata) => {
299            let account_info = client
300                .get_account_info(
301                    &AccountIdentifier::CredId(metadata.cred_id),
302                    block_identifier,
303                )
304                .await?;
305
306            let Some(account_cred) =
307                account_info
308                    .response
309                    .account_credentials
310                    .values()
311                    .find_map(|cred| {
312                        cred.value
313                            .as_ref()
314                            .known()
315                            .and_then(|c| (c.cred_id() == metadata.cred_id.as_ref()).then_some(c))
316                    })
317            else {
318                return Err(VerifyError::CredentialNotPresent {
319                    cred_id: metadata.cred_id,
320                    account: account_info.response.account_address,
321                });
322            };
323
324            match account_cred {
325                AccountCredentialWithoutProofs::Initial { .. } => {
326                    return Err(VerifyError::InitialCredential {
327                        cred_id: metadata.cred_id,
328                    })
329                }
330                AccountCredentialWithoutProofs::Normal { cdv, commitments } => {
331                    let credential_validity = types::CredentialValidity {
332                        created_at: account_cred.policy().created_at,
333                        valid_to: cdv.policy.valid_to,
334                    };
335
336                    VerificationMaterialWithValidity {
337                        verification_material: VerificationMaterial::Account(
338                            AccountCredentialVerificationMaterial {
339                                issuer: cdv.ip_identity,
340                                attribute_commitments: commitments.cmm_attributes.clone(),
341                            },
342                        ),
343                        validity: CredentialValidityType::ValidityPeriod(credential_validity),
344                    }
345                }
346            }
347        }
348        CredentialMetadataTypeV1::Identity(metadata) => {
349            let ip_info = client
350                .get_identity_providers(block_identifier)
351                .await?
352                .response
353                .try_filter(|ip| future::ready(ip.ip_identity == metadata.issuer))
354                .next()
355                .await
356                .ok_or(VerifyError::UnknownIdentityProvider(metadata.issuer))?
357                .map_err(|status| QueryError::RPCError(RPCError::CallError(status)))?;
358
359            let ars_infos: BTreeMap<_, _> = client
360                .get_anonymity_revokers(block_identifier)
361                .await?
362                .response
363                .map_ok(|ar_info| (ar_info.ar_identity, ar_info))
364                .try_collect()
365                .await
366                .map_err(|status| QueryError::RPCError(RPCError::CallError(status)))?;
367
368            VerificationMaterialWithValidity {
369                verification_material: VerificationMaterial::Identity(
370                    IdentityCredentialVerificationMaterial {
371                        ip_info,
372                        ars_infos: ArInfos {
373                            anonymity_revokers: ars_infos,
374                        },
375                    },
376                ),
377                validity: CredentialValidityType::ValidityPeriod(metadata.validity.clone()),
378            }
379        }
380    })
381}
382
383/// Error creating and registering anchor.
384#[derive(thiserror::Error, Debug)]
385pub enum CreateAnchorError {
386    #[error("node query error: {0}")]
387    Query(#[from] v2::QueryError),
388    #[error("data register transaction data is too large: {0}")]
389    TooLarge(#[from] TooLargeError),
390    #[error("CBOR serialization error: {0}")]
391    CborSerialization(#[from] CborSerializationError),
392}
393
394impl From<RPCError> for CreateAnchorError {
395    fn from(err: RPCError) -> Self {
396        CreateAnchorError::Query(err.into())
397    }
398}
399
400/// Metadata for anchor transaction submission.
401pub struct AnchorTransactionMetadata<S: ExactSizeTransactionSigner> {
402    /// The signer object used to sign the on-chain anchor transaction. This must correspond to the `sender` account below.
403    pub signer: S,
404    /// The sender account of the anchor transaction.
405    pub sender: AccountAddress,
406    /// The sequence number for the sender account to use.
407    pub account_sequence_number: Nonce,
408    /// The transaction expiry time.
409    pub expiry: TransactionTime,
410}
411
412/// Submit verification request anchor (VRA) and return the verification request.
413///
414/// Notice that the VRA will only be submitted, it is not included on-chain yet when
415/// the function returns. The transaction hash is returned
416/// in [`VerificationRequest::anchor_transaction_hash`] and the transaction must
417/// be tracked until finalization before the verification request is usable
418/// (waiting for finalization can be done in the app that receives the verification request
419/// to create a verifiable presentation).
420pub async fn create_verification_request_and_submit_request_anchor<
421    S: ExactSizeTransactionSigner,
422>(
423    client: &mut v2::Client,
424    anchor_transaction_metadata: AnchorTransactionMetadata<S>,
425    verification_request_data: VerificationRequestData,
426    public_info: Option<HashMap<String, cbor::value::Value>>,
427) -> Result<VerificationRequest, CreateAnchorError> {
428    let verification_request_anchor = verification_request_data.to_anchor(public_info);
429    let cbor = cbor::cbor_encode(&verification_request_anchor)?;
430    let register_data = RegisteredData::try_from(cbor)?;
431
432    let tx = send::register_data(
433        &anchor_transaction_metadata.signer,
434        anchor_transaction_metadata.sender,
435        anchor_transaction_metadata.account_sequence_number,
436        anchor_transaction_metadata.expiry,
437        register_data,
438    );
439    let block_item = BlockItem::AccountTransaction(tx);
440
441    // Submit the transaction to the chain.
442    let transaction_hash = client.send_block_item(&block_item).await?;
443
444    Ok(VerificationRequest {
445        context: verification_request_data.context,
446        subject_claims: verification_request_data.subject_claims,
447        anchor_transaction_hash: transaction_hash,
448    })
449}
450
451/// Submit verification audit anchor (VAA).
452///
453/// Notice that the VAA will only be submitted, it is not included on-chain yet when
454/// the function returns. The transaction must
455/// be tracked until finalization for the audit record to be registered successfully.
456pub async fn submit_verification_audit_record_anchor<S: ExactSizeTransactionSigner>(
457    client: &mut v2::Client,
458    anchor_transaction_metadata: AnchorTransactionMetadata<S>,
459    verification_audit_record: &VerificationAuditRecord,
460    public_info: Option<HashMap<String, cbor::value::Value>>,
461) -> Result<TransactionHash, CreateAnchorError> {
462    let verification_audit_anchor = verification_audit_record.to_anchor(public_info);
463    let cbor = cbor::cbor_encode(&verification_audit_anchor)?;
464    let register_data = RegisteredData::try_from(cbor)?;
465
466    let tx = send::register_data(
467        &anchor_transaction_metadata.signer,
468        anchor_transaction_metadata.sender,
469        anchor_transaction_metadata.account_sequence_number,
470        anchor_transaction_metadata.expiry,
471        register_data,
472    );
473    let item = BlockItem::AccountTransaction(tx);
474
475    // Submit the transaction to the chain.
476    let transaction_hash = client.send_block_item(&item).await?;
477
478    Ok(transaction_hash)
479}