diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fcdfbef2..1d6e506295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ ## Unreleased changes -- Added `web3id::v1::anchor` module that contains functionality to create verification request anchor (VRA) and verification audit anchor (VAA) on chain. +- Implemented `web3id::v1` module that contains functionality to support the verification flow of Concordium verifiable presentations V1. + This includes + - creating verification request anchor (VRA) + - verifying presentations + - creating verification audit anchor (VAA) + + The main entrypoints in the module are `v1::create_verification_request_and_submit_request_anchor` + and `v1::verify_presentation_and_submit_audit_anchor`. ## 8.0.0 diff --git a/concordium-base b/concordium-base index e5749c7794..c96289e10b 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit e5749c7794a7cbb85ffef4b95d9e2630a71b0650 +Subproject commit c96289e10bdf720b4338b4f870238d39de9b37bd diff --git a/examples/create_validation_request_anchor.rs b/examples/create_validation_request_anchor.rs deleted file mode 100644 index 5dca71f459..0000000000 --- a/examples/create_validation_request_anchor.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Example that shows how to generate a verification request anchor. -//! -//! You can run this example as follows: -//! cargo run --example create_validation_request_anchor -- --node http://localhost:20100 --account 3nhMYfA59MWaxBRjfHPKSYH9S4W5HdZZ721jozVdeToBGvXTU8.export -use anyhow::Context as AnyhowContext; -use clap::AppSettings; -use concordium_base::web3id::v1::anchor::{ - CredentialType, IdentityProviderMethod, RequestedIdentitySubjectClaims, - UnfilledContextInformation, VerificationRequestData, -}; -use concordium_base::web3id::Web3IdAttribute; -use concordium_base::{ - common::cbor, - id::id_proof_types::{AtomicStatement, AttributeInRangeStatement}, - web3id::{ - did::Network, - // Web3IdAttribute, - }, -}; -use concordium_rust_sdk::web3id::v1::anchor; -use concordium_rust_sdk::web3id::v1::anchor::AnchorTransactionMetadata; -use concordium_rust_sdk::{ - common::types::TransactionTime, - types::WalletAccount, - v2::{self}, -}; -use rand::Rng; -use std::{collections::HashMap, marker::PhantomData, path::PathBuf}; -use structopt::*; - -#[derive(StructOpt)] -struct App { - #[structopt( - long = "node", - help = "GRPC interface of the node.", - default_value = "http://localhost:20100" - )] - endpoint: v2::Endpoint, - #[structopt(long = "account", help = "Path to the account key file.")] - keys_path: PathBuf, -} - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> anyhow::Result<()> { - let app = { - let app = App::clap().global_setting(AppSettings::ColoredHelp); - let matches = app.get_matches(); - App::from_clap(&matches) - }; - let mut client = v2::Client::new(app.endpoint).await?; - - // Load account keys and sender address from a file - let keys: WalletAccount = - WalletAccount::from_json_file(app.keys_path).context("Could not read the keys file.")?; - - // Get the initial nonce at the last finalized block. - let account_sequence_number = client - .get_next_account_sequence_number(&keys.address) - .await?; - let account_sequence_number = account_sequence_number.nonce; - - // Set expiry to now + 5min - let expiry: TransactionTime = - TransactionTime::from_seconds((chrono::Utc::now().timestamp() + 300) as u64); - - // First we generate the verification request. - // - // Generating the `context` and `credential_statements` will normally happen server-side. - let mut rng = rand::thread_rng(); - let nonce: [u8; 32] = rng.gen(); // Note: This nonce has to be generated fresh/randomly for each request. - let connection_id = "MyWalletConnectTopic".to_string(); // Note: Use the wallet connect topic in production. - let context_string = "MyGreateApp".to_string(); - let context = UnfilledContextInformation::new_simple(nonce, connection_id, context_string); - - let attribute_in_range_statement = AtomicStatement::AttributeInRange { - statement: AttributeInRangeStatement { - attribute_tag: 17.into(), - lower: Web3IdAttribute::Numeric(80), - upper: Web3IdAttribute::Numeric(1237), - _phantom: PhantomData, - }, - }; - - let verification_request_data = VerificationRequestData::new(context).add_statement_request( - RequestedIdentitySubjectClaims::default() - .add_issuer(IdentityProviderMethod::new(0u32, Network::Testnet)) - .add_source(CredentialType::IdentityCredential) - .add_statement(attribute_in_range_statement), - ); - - let mut public_info = HashMap::new(); - public_info.insert("key".to_string(), cbor::value::Value::Positive(4u64)); - - let anchor_transaction_metadata = AnchorTransactionMetadata { - signer: &keys, - sender: keys.address, - account_sequence_number, - expiry, - }; - - let verification_request = anchor::submit_verification_request_anchor( - &mut client, - anchor_transaction_metadata, - verification_request_data, - public_info, - ) - .await?; - - println!( - "Verification request anchor transaction hash: {}", - verification_request.anchor_transaction_hash - ); - - let (bh, bs) = client - .wait_until_finalized(&verification_request.anchor_transaction_hash) - .await?; - - println!("Verification request anchor finalized in block {}.", bh); - println!("The outcome is {:#?}", bs); - - println!( - "Generated Verification Request to be sent to the wallet/idApp: {:#?}", - verification_request - ); - - Ok(()) -} diff --git a/examples/web3id_v1_verification_flow.rs b/examples/web3id_v1_verification_flow.rs new file mode 100644 index 0000000000..ef415a76f1 --- /dev/null +++ b/examples/web3id_v1_verification_flow.rs @@ -0,0 +1,191 @@ +//! Example that shows how to verify a verifiable presentation +//! together with the verification request. See +//! [`crate::web3id::v1`]. +//! +//! You can run this example as follows: +//! cargo run --example web3id_v1_verification_flow -- --node http://localhost:20100 --account 3nhMYfA59MWaxBRjfHPKSYH9S4W5HdZZ721jozVdeToBGvXTU8.export +use anyhow::Context as AnyhowContext; +use clap::AppSettings; +use concordium_base::web3id::v1::anchor; +use concordium_base::web3id::v1::anchor::{ + IdentityCredentialType, IdentityProviderDid, RequestedIdentitySubjectClaimsBuilder, + RequestedStatement, UnfilledContextInformationBuilder, VerifiablePresentationV1, + VerificationRequest, VerificationRequestDataBuilder, +}; +use concordium_rust_sdk::v2::BlockIdentifier; +use concordium_rust_sdk::web3id::v1::{AnchorTransactionMetadata, AuditRecordArgument}; +use concordium_rust_sdk::{ + base::{ + common::cbor, + id::id_proof_types::AttributeInRangeStatement, + web3id::{did::Network, Web3IdAttribute}, + }, + common::types::TransactionTime, + types::WalletAccount, + v2::{self}, + web3id, +}; +use rand::Rng; +use std::{collections::HashMap, marker::PhantomData, path::PathBuf}; +use structopt::*; + +#[derive(StructOpt)] +struct App { + #[structopt( + long = "node", + help = "GRPC interface of the node.", + default_value = "http://localhost:20100" + )] + endpoint: v2::Endpoint, + #[structopt(long = "account", help = "Path to the account key file.")] + keys_path: PathBuf, +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> anyhow::Result<()> { + let app = { + let app = App::clap().global_setting(AppSettings::ColoredHelp); + let matches = app.get_matches(); + App::from_clap(&matches) + }; + let mut client = v2::Client::new(app.endpoint).await?; + let network = Network::Testnet; + + // Load account keys and sender address from a file + let keys: WalletAccount = + WalletAccount::from_json_file(app.keys_path).context("Could not read the keys file.")?; + + // Get the next account nonce + let account_sequence_number = client + .get_next_account_sequence_number(&keys.address) + .await?; + let account_sequence_number = account_sequence_number.nonce; + + // Set expiry to now + 5min + let expiry: TransactionTime = + TransactionTime::from_seconds((chrono::Utc::now().timestamp() + 300) as u64); + + // First we generate the verification request. + // + // Generating the `context` and `credential_statements` will normally happen server-side. + let mut rng = rand::thread_rng(); + let nonce_bytes: [u8; 32] = rng.gen(); // todo ar use nonce bytes or hash, change back? + let nonce = anchor::Nonce(nonce_bytes); // Note: This nonce has to be generated fresh/randomly for each request. + let connection_id = "MyWalletConnectTopic".to_string(); // Note: Use the wallet connect topic in production. + let context_string = "MyGreateApp".to_string(); + let context = + UnfilledContextInformationBuilder::new_simple(nonce, connection_id, context_string).build(); + + let attribute_in_range_statement = + RequestedStatement::AttributeInRange(AttributeInRangeStatement { + attribute_tag: 17.into(), + lower: Web3IdAttribute::Numeric(80), + upper: Web3IdAttribute::Numeric(1237), + _phantom: PhantomData, + }); + + let verification_request_data = VerificationRequestDataBuilder::new(context) + .subject_claim( + RequestedIdentitySubjectClaimsBuilder::default() + .issuer(IdentityProviderDid::new(0u32, network)) + .source(IdentityCredentialType::IdentityCredential) + .statement(attribute_in_range_statement) + .build(), + ) + .build(); + + let mut public_info = HashMap::new(); + public_info.insert("key".to_string(), cbor::value::Value::Positive(4u64)); + + let anchor_transaction_metadata = AnchorTransactionMetadata { + signer: &keys, + sender: keys.address, + account_sequence_number, + expiry, + }; + + let verification_request = web3id::v1::create_verification_request_and_submit_request_anchor( + &mut client, + anchor_transaction_metadata, + verification_request_data, + Some(public_info.clone()), + ) + .await?; + + println!("Verification request: {:#?}", verification_request); + + println!( + "Verification request anchor transaction hash: {}", + verification_request.anchor_transaction_hash + ); + + let (bh, _) = client + .wait_until_finalized(&verification_request.anchor_transaction_hash) + .await?; + + println!("Verification request anchor finalized in block {}.", bh); + + /// Send the verification request to the wallet/ID app and obtain the + /// verifiable presentation. + /// + /// The wallet/ID app fills in the requested context in [`VerificationRequest::context`] + /// and selects and identity in order to form a + /// [`VerifiablePresentationRequestV1`](concordium_base::web3id::v1::anchor::VerifiablePresentationRequestV1). + /// that is then used to generate and prove a [`VerifiablePresentationV1`]. + async fn send_request_and_receive_presentation( + _request: VerificationRequest, + ) -> VerifiablePresentationV1 { + todo!("send verification request to wallet/ID app and receive verifiable presentation") + } + + let presentation = send_request_and_receive_presentation(verification_request.clone()).await; + + let audit_record_id = "UUID".to_string(); + + let audit_record_anchor_transaction_metadata = AnchorTransactionMetadata { + signer: &keys, + sender: keys.address, + account_sequence_number: account_sequence_number.next(), // We have to increase the nonce as this is the second anchor tx. + expiry, + }; + + let audit_record_argument = AuditRecordArgument { + audit_record_id, + audit_record_anchor_transaction_metadata, + public_info: Some(public_info), + }; + + let verification_data = web3id::v1::verify_presentation_and_submit_audit_anchor( + &mut client, + network, + BlockIdentifier::LastFinal, + verification_request, + presentation, + audit_record_argument, + ) + .await?; + + println!( + "Verification result: {:#?}", + verification_data.verification_result + ); + + println!( + "Generated anchored verification audit record to be stored in database: {:#?}", + verification_data.audit_record + ); + + if let Some(anchor_transaction_hash) = &verification_data.anchor_transaction_hash { + println!( + "Verifiable audit anchor transaction hash: {}", + anchor_transaction_hash + ); + + let (bh, bs) = client.wait_until_finalized(anchor_transaction_hash).await?; + + println!("Verification request anchor finalized in block {}.", bh); + println!("The outcome is {:#?}", bs); + }; + + Ok(()) +} diff --git a/src/web3id.rs b/src/web3id.rs index 7bf81eca24..88e04e6e18 100644 --- a/src/web3id.rs +++ b/src/web3id.rs @@ -1,35 +1,20 @@ -//! Functionality for retrieving, verifying, and registering Web3Id credentials. - -/// Functionality for requesting and verifying V1 Web3Id credentials. -pub mod v1; - -use std::collections::BTreeMap; +//! Functionality for retrieving, verifying, and registering web3id credentials. use crate::{ cis4::{Cis4Contract, Cis4QueryError}, - types::queries::BlockInfo, - v2::{self, AccountIdentifier, BlockIdentifier, IntoBlockIdentifier}, + v2::{self, BlockIdentifier, IntoBlockIdentifier}, }; pub use concordium_base::web3id::*; use concordium_base::{ base::CredentialRegistrationID, cis4_types::CredentialStatus, - constants::SHA256, contracts_common::AccountAddress, - hashes::{HashBytes, TransactionHash}, - id::{ - constants::{ArCurve, IpPairing}, - types::{ArInfo, ArInfos, CredentialValidity, IpIdentity, IpInfo}, - }, - web3id::{ - self, - v1::{ - AccountCredentialVerificationMaterial, CredentialMetadataTypeV1, CredentialMetadataV1, - CredentialVerificationMaterial, IdentityCredentialVerificationMaterial, PresentationV1, - }, - }, + id::{constants::ArCurve, types::IpIdentity}, + web3id, }; -use futures::{TryFutureExt, TryStreamExt}; +use futures::TryStreamExt; + +pub mod v1; #[derive(thiserror::Error, Debug)] pub enum CredentialLookupError { @@ -127,23 +112,24 @@ pub async fn verify_credential_metadata( cdv, commitments, } => { - let block_info = client - .get_block_info(bi) - .map_err(|_e| { - CredentialLookupError::InvalidResponse( - "could not get block info".to_string(), - ) - }) - .await? - .response; - - let credential_validity = CredentialValidity { - created_at: cdv.policy.created_at, - valid_to: cdv.policy.valid_to, + let now = client.get_block_info(bi).await?.response.block_slot_time; + let valid_from = cdv.policy.created_at.lower().ok_or_else(|| { + CredentialLookupError::InvalidResponse( + "Credential creation date is not valid.".into(), + ) + })?; + let valid_until = cdv.policy.valid_to.upper().ok_or_else(|| { + CredentialLookupError::InvalidResponse( + "Credential creation date is not valid.".into(), + ) + })?; + let status = if valid_from > now { + CredentialStatus::NotActivated + } else if valid_until < now { + CredentialStatus::Expired + } else { + CredentialStatus::Active }; - let status = - determine_credential_validity_status(&credential_validity, &block_info)?; - let inputs = CredentialsInputs::Account { commitments: commitments.cmm_attributes.clone(), }; @@ -189,323 +175,3 @@ pub async fn get_public_data( .collect::>(); stream.try_collect().await } - -/// determine the credential validity based on the valid from and valid to date information. -/// The block info supplied has the slot time we will use as the current time, to check validity against -#[allow(clippy::result_large_err)] -fn determine_credential_validity_status( - validity: &CredentialValidity, - block_info: &BlockInfo, -) -> Result { - let valid_from = validity.created_at.lower().ok_or_else(|| { - CredentialLookupError::InvalidResponse( - "Could not get valid-from date for credential validity".to_string(), - ) - })?; - - let valid_to = validity.valid_to.upper().ok_or_else(|| { - CredentialLookupError::InvalidResponse("Credential valid-to date is not valid.".to_string()) - })?; - - let now = block_info.block_slot_time; - - if valid_from > now { - Ok(CredentialStatus::NotActivated) - } else if valid_to >= now { - Ok(CredentialStatus::Active) - } else { - Ok(CredentialStatus::Expired) - } -} - -/// TODO - this should contain the audit record data, and also the transaction hash for the anchored audit transaction on chain -#[allow(dead_code)] -pub struct AnchoredVerificationAuditResponse { - /// transaction hash of the audit anchor on chain - transaction_hash: TransactionHash, - /// TODO: this will become the Audit record, string for now for initial development work - audit_record_data: String, -} - -/// verify a presentation for the v1 proofs protocol. -pub async fn verify_presentation( - mut client: v2::Client, - presentation: web3id::v1::PresentationV1, - bi: impl IntoBlockIdentifier, - identity_providers: Vec>, - anonymity_revokers: Vec>, -) -> Result { - // get the global context - let block_identifier = bi.into_block_identifier(); - let global_context = client - .get_cryptographic_parameters(block_identifier) - .map_err(|_e| { - CredentialLookupError::InvalidResponse( - "could not get global context for block provided".to_string(), - ) - }) - .await? - .response; - - let block_info = client - .get_block_info(block_identifier) - .map_err(|_e| { - CredentialLookupError::InvalidResponse("Issue occured gettting block info".to_string()) - }) - .await? - .response; - - // build verification material by extracting the metadata for the credentials - let verification_material = get_public_data_v1( - client, - presentation.clone(), - identity_providers, - anonymity_revokers, - &block_info, - &block_identifier, - ) - .await?; - - // verification of the presentation - let _request_v1 = presentation - .verify(&global_context, verification_material.iter()) - .map_err(|_e| { - CredentialLookupError::InvalidResponse("presentation verification failed".to_string()) - })?; - - // TODO - audit anchor call goes here, and return AnchoredVerificationAuditRecord - let dummy_anchor_audit_record_result = AnchoredVerificationAuditResponse { - transaction_hash: HashBytes::new([0u8; SHA256]), - audit_record_data: "dummy data".to_string(), - }; - - Ok(dummy_anchor_audit_record_result) -} - -/// Retrieve the public data for the presentation. -/// Will call the cryptographic verification for each metadata of the presentation provided and also check the credential validity -pub async fn get_public_data_v1( - client: v2::Client, - presentation: PresentationV1, - identity_providers: Vec>, - anonymity_revokers: Vec>, - block_info: &BlockInfo, - block_identifier: &BlockIdentifier, -) -> Result>, CredentialLookupError> { - let credential_verification_materials = presentation - .metadata() - .map(|metadata| { - let idp_clone = identity_providers.clone(); - let ars_clone = anonymity_revokers.clone(); - let client = client.clone(); - async move { - verify_credential_metadata_v1( - client, - &metadata, - &idp_clone, - &ars_clone, - block_info, - block_identifier, - ) - .await - } - }) - .collect::>(); - credential_verification_materials.try_collect().await -} - -/// Verify metadata provided and return the CredentialVerificationMaterial -pub async fn verify_credential_metadata_v1( - mut client: v2::Client, - metadata: &CredentialMetadataV1, - identity_providers: &Vec>, - anonymity_revokers: &Vec>, - block_info: &BlockInfo, - block_identifier: &BlockIdentifier, -) -> Result, CredentialLookupError> { - match &metadata.cred_metadata { - CredentialMetadataTypeV1::Identity(identity_credential_metadata) => { - let credential_ip_identity = identity_credential_metadata.issuer; - let matching_ip_info = - find_matching_ip_info(credential_ip_identity, identity_providers)?; - - let ar_infos = get_ars_infos(anonymity_revokers); - - // credentials validity status - let _status = determine_credential_validity_status( - &identity_credential_metadata.validity, - block_info, - ); - - // build and return the verification material for the identity - Ok(CredentialVerificationMaterial::Identity( - IdentityCredentialVerificationMaterial { - ip_info: matching_ip_info, - ars_infos: ar_infos, - }, - )) - } - CredentialMetadataTypeV1::Account(account_credentials_metadata) => { - let account_identifier = AccountIdentifier::from(account_credentials_metadata.cred_id); - - let _account_info = client - .get_account_info(&account_identifier, block_identifier) - .await? - .response; - - // TODO - here we will need to create and build attribute commitments for the account credentials - let attribute_commitments = BTreeMap::new(); - - Ok(CredentialVerificationMaterial::Account( - AccountCredentialVerificationMaterial { - attribute_commitments, - }, - )) - } - } -} - -/// helper utility to make sure we find the matching identity provider for the issuer mentioned in the credential -#[allow(clippy::result_large_err)] -fn find_matching_ip_info( - ip_identity: IpIdentity, - identity_providers: &Vec>, -) -> Result, CredentialLookupError> { - for ip_info in identity_providers { - if ip_info.ip_identity == ip_identity { - return Ok(ip_info.clone()); - } - } - Err(CredentialLookupError::InvalidResponse( - "No Idp Match".to_string(), - )) -} - -/// Get anonynmity revokers helper utility which is used for building credential verification material -fn get_ars_infos(anonymity_revokers: &Vec>) -> ArInfos { - let mut ar_idenity_ar_info_btree = BTreeMap::new(); - - for ar in anonymity_revokers { - ar_idenity_ar_info_btree.insert(ar.ar_identity, ar.clone()); - } - - ArInfos { - anonymity_revokers: ar_idenity_ar_info_btree, - } -} - -#[cfg(test)] -mod tests { - use crate::types::queries::ProtocolVersionInt; - - use super::*; - use chrono::{DateTime, Utc}; - use concordium_base::{ - base::{AbsoluteBlockHeight, BlockHeight, Energy, GenesisIndex, ProtocolVersion}, - constants::SHA256, - hashes::HashBytes, - id::types::YearMonth, - }; - - #[test] - fn test_determine_credential_validity_status_as_active() { - let now = YearMonth::now(); - let now_time = now.lower().expect("expected now time"); - // create an 'active' credential validity, created last year, expires next year - let validity = CredentialValidity { - created_at: YearMonth { - month: now.month, - year: now.year - 1, - }, - valid_to: YearMonth { - month: now.month, - year: now.year + 1, - }, - }; - - // stub the current block information - let block_info = get_dummy_block_info(now_time); - - let credential_status_result = determine_credential_validity_status(&validity, &block_info) - .expect("expected credential status here"); - - assert_eq!(CredentialStatus::Active, credential_status_result); - } - - #[test] - fn test_determine_credential_validity_status_as_expired() { - let now = YearMonth::now(); - let now_time = now.lower().expect("expected now time"); - - // create an 'expired' credential validity, created last year, expires 2 month ago - let validity = CredentialValidity { - created_at: YearMonth { - month: now.month, - year: now.year - 1, - }, - valid_to: YearMonth { - month: now.month - 2, - year: now.year, - }, - }; - - // stub the current block information - let block_info = get_dummy_block_info(now_time); - - let credential_status_result = determine_credential_validity_status(&validity, &block_info) - .expect("expected credential status here"); - - assert_eq!(CredentialStatus::Expired, credential_status_result); - } - - #[test] - fn test_determine_credential_validity_status_as_not_active() { - let now = YearMonth::now(); - let now_time = now.lower().expect("expected now time"); - - // create a 'not active' credential validity, created 1 month in future, expires 1 year in future - let validity = CredentialValidity { - created_at: YearMonth { - month: now.month + 1, - year: now.year, - }, - valid_to: YearMonth { - month: now.month, - year: now.year + 1, - }, - }; - - // stub the current block information - let block_info = get_dummy_block_info(now_time); - - let credential_status_result = determine_credential_validity_status(&validity, &block_info) - .expect("expected credential status here"); - - assert_eq!(CredentialStatus::NotActivated, credential_status_result); - } - - // helper util to just get a dummy block based on a block slot time provided for credential validity testing - fn get_dummy_block_info(block_slot_time: DateTime) -> BlockInfo { - BlockInfo { - transactions_size: 0u64, - block_parent: HashBytes::new([1u8; SHA256]), - block_hash: HashBytes::new([1u8; SHA256]), - finalized: true, - block_state_hash: HashBytes::new([1u8; SHA256]), - block_arrive_time: block_slot_time, - block_receive_time: block_slot_time, - transaction_count: 0, - transaction_energy_cost: Energy::default(), - block_slot: None, - block_last_finalized: HashBytes::new([1u8; SHA256]), - block_slot_time, - block_height: AbsoluteBlockHeight { height: 1u64 }, - era_block_height: BlockHeight { height: 1u64 }, - genesis_index: GenesisIndex { height: 0u32 }, - block_baker: None, - protocol_version: ProtocolVersionInt::from_enum(ProtocolVersion::P9), - round: None, - epoch: None, - } - } -} diff --git a/src/web3id/v1.rs b/src/web3id/v1.rs index 7101c6e855..210f05c71e 100644 --- a/src/web3id/v1.rs +++ b/src/web3id/v1.rs @@ -1,2 +1,479 @@ -/// Functionality to create the verification request anchor (VRA) and verification audit anchor (VAA). -pub mod anchor; +//! Functionality for requesting and verifying V1 Concordium verifiable presentations. +//! +//! A verification flow consists of multiple stages: +//! +//! 1. Create a [`VerificationRequest`]: A verification flow is started by constructing [`VerificationRequestData`] +//! and creating the [`VerificationRequest`] with [`create_verification_request_and_submit_request_anchor`] which also +//! submits the corresponding [`VerificationRequestAnchor`] (VRA) on chain. +//! +//! 2. Generate and prove [`VerifiablePresentationV1`]: The claims in the [`VerificationRequest`] are +//! proved by a credential holder in the context specified in the request and +//! embedded in a [`VerifiablePresentationV1`] together with the context and proofs. +//! The prover is implemented in [`VerifiablePresentationRequestV1::prove`](anchor::VerifiablePresentationRequestV1::prove). +//! +//! 3. Verify a [`VerifiablePresentationV1`]: The presentation can be verified together with +//! the verification request with [`verify_presentation_and_submit_audit_anchor`], which submits +//! and [`VerificationAuditRecord`] (VAA) on chain and returns the [`VerificationAuditRecord`] to +//! be stored locally by the verifier. +//! +//! 4. Verify an [`VerificationAuditRecord`]: The stored audit record can be re-verified with +//! [`verify_audit_record`] if/when needed. +//! +//! The example `web3id_v1_verification_flow` demonstrates the verification flow. + +use crate::types::{AccountTransactionEffects, BlockItemSummaryDetails}; +use crate::v2; + +use crate::endpoints::RPCError; +use crate::smart_contracts::common::AccountAddress; +use crate::v2::{AccountIdentifier, BlockIdentifier, IntoBlockIdentifier, QueryError}; +use concordium_base::base::{CredentialRegistrationID, Nonce}; +use concordium_base::common::cbor; +use concordium_base::common::cbor::CborSerializationError; +use concordium_base::common::upward::UnknownDataError; +use concordium_base::hashes::TransactionHash; +use concordium_base::id::types; +use concordium_base::id::types::{AccountCredentialWithoutProofs, ArInfos, IpIdentity}; +use concordium_base::transactions::{ + send, BlockItem, ExactSizeTransactionSigner, RegisteredData, TooLargeError, +}; +use concordium_base::web3id::v1::anchor::{ + self as anchor, CredentialValidityType, PresentationVerificationResult, + VerifiablePresentationV1, VerificationAuditRecord, VerificationContext, VerificationMaterial, + VerificationMaterialWithValidity, VerificationRequest, VerificationRequestAnchor, + VerificationRequestAnchorAndBlockHash, VerificationRequestData, +}; +use concordium_base::web3id::v1::{ + AccountCredentialVerificationMaterial, CredentialMetadataTypeV1, CredentialMetadataV1, + IdentityCredentialVerificationMaterial, +}; +use concordium_base::{hashes, web3id}; + +use concordium_base::common::types::TransactionTime; +use futures::StreamExt; +use futures::{future, TryStreamExt}; +use std::collections::{BTreeMap, HashMap}; + +/// Data returned from verifying a presentation against the corresponding verification request. +/// Contains the verification result, the audit record and the transaction hash +/// for the transaction registering the verification audit anchor (VAA) on-chain in case +/// the verification was successful. +/// The audit record should be stored in an off-chain database for regulatory purposes +/// and should generally be kept private. +#[derive(Debug, Clone, PartialEq)] +pub struct PresentationVerificationData { + // Whether the verification was successful. If not [`PresentationVerificationResult::Verified`], + // the verifiable presentation is not valid and the credentials and claims in it are not verified to be true. + pub verification_result: PresentationVerificationResult, + /// The verification audit record. Notice that the existence of the audit record + /// does not mean that verification was successful, that is specified + /// by [`Self::verification_result`]. The audit record should be stored in an off-chain database for regulatory purposes + /// and should generally be kept private. + /// A corresponding [`VerificationRequestAnchor`] (VAA) is submitted + /// on chain, if the verification is successful. + pub audit_record: VerificationAuditRecord, + /// Blockchain transaction hash for the transaction that registers + /// the [verification audit record](anchor::VerificationAuditAnchor) (VAA) on-chain. + /// Notice that this transaction may not have been finalized yet. Also, the anchor is + /// only submitted if the verification is successful. + pub anchor_transaction_hash: Option, +} + +/// Error verifying presentation +#[derive(thiserror::Error, Debug)] +pub enum VerifyError { + #[error("on-chain request anchor transaction is of invalid type")] + InvalidRequestAnchor, + #[error("on-chain request anchor transaction not finalized yet")] + RequestAnchorNotFinalized, + #[error("unknown data error: {0}")] + UnknownDataError(#[from] UnknownDataError), + #[error("node query error: {0}")] + Query(#[from] v2::QueryError), + #[error("create anchor: {0}")] + Anchor(#[from] CreateAnchorError), + #[error("CBOR serialization error: {0}")] + CborSerialization(#[from] CborSerializationError), + #[error("unknown identity provider: {0}")] + UnknownIdentityProvider(IpIdentity), + #[error("credential {cred_id} no longer present on account: {account}")] + CredentialNotPresent { + cred_id: CredentialRegistrationID, + account: AccountAddress, + }, + #[error("initial credential {cred_id} cannot be used.")] + InitialCredential { cred_id: CredentialRegistrationID }, +} + +/// Metadata for transaction submission. +pub struct AuditRecordArgument { + /// Id of the audit record to create. Is fully determined by the verifier/caller. + pub audit_record_id: String, + /// Public information to be included in the audit record anchor (VAA) on-chain. + pub public_info: Option>, + /// Metadata for the anchor transaction that submits the audit record anchor (VAA) on-chain. + pub audit_record_anchor_transaction_metadata: AnchorTransactionMetadata, +} + +/// Verify the given verifiable presentation together with the given verification request. +/// The aspects validated are documented on [`anchor::verify_presentation_with_request_anchor`]. +/// The verification returns an audit record containing the presentation and verification request. +/// In case the verification is successful, a [verification audit record](anchor::VerificationAuditAnchor) +/// (VRA) is submitted on chain. +/// Notice that when this method returns, the anchor is only submitted. The returned anchor transaction +/// hash must be tracked in order to determine if it is finalized. +pub async fn verify_presentation_and_submit_audit_anchor( + client: &mut v2::Client, + network: web3id::did::Network, + block_identifier: impl IntoBlockIdentifier, + verification_request: VerificationRequest, + verifiable_presentation: VerifiablePresentationV1, + audit_record_arg: AuditRecordArgument, +) -> Result { + let block_identifier = block_identifier.into_block_identifier(); + let global_context = client + .get_cryptographic_parameters(block_identifier) + .await? + .response; + + let block_info = client.get_block_info(block_identifier).await?.response; + + let request_anchor = lookup_request_anchor(client, &verification_request).await?; + + let verification_material = lookup_verification_materials_and_validity( + client, + block_identifier, + &verifiable_presentation, + ) + .await?; + + let context = VerificationContext { + network, + validity_time: block_info.block_slot_time, + }; + + let verification_result = anchor::verify_presentation_with_request_anchor( + &global_context, + &context, + &verification_request, + &verifiable_presentation, + &request_anchor, + &verification_material, + ); + + let audit_record = VerificationAuditRecord::new( + audit_record_arg.audit_record_id, + verification_request, + verifiable_presentation, + ); + + let anchor_transaction_hash = if verification_result.is_success() { + let txn_hash = submit_verification_audit_record_anchor( + client, + audit_record_arg.audit_record_anchor_transaction_metadata, + &audit_record, + audit_record_arg.public_info, + ) + .await?; + Some(txn_hash) + } else { + None + }; + + Ok(PresentationVerificationData { + verification_result, + audit_record, + anchor_transaction_hash, + }) +} + +/// Verify the verifiable presentation in the audit record together with the +/// verification request in the audit record. The aspects validated are documented on +/// [`anchor::verify_presentation_with_request_anchor`]. +pub async fn verify_audit_record( + client: &mut v2::Client, + network: web3id::did::Network, + block_identifier: impl IntoBlockIdentifier, + verification_audit_record: &VerificationAuditRecord, +) -> Result { + let block_identifier = block_identifier.into_block_identifier(); + let global_context = client + .get_cryptographic_parameters(block_identifier) + .await? + .response; + + let block_info = client.get_block_info(block_identifier).await?.response; + + let request_anchor = lookup_request_anchor(client, &verification_audit_record.request).await?; + + let verification_material = lookup_verification_materials_and_validity( + client, + block_identifier, + &verification_audit_record.presentation, + ) + .await?; + + let context = VerificationContext { + network, + validity_time: block_info.block_slot_time, + }; + + Ok(anchor::verify_presentation_with_request_anchor( + &global_context, + &context, + &verification_audit_record.request, + &verification_audit_record.presentation, + &request_anchor, + &verification_material, + )) +} + +/// Looks up the verifiable request anchor (VRA) from the verification +/// request. +pub async fn lookup_request_anchor( + client: &mut v2::Client, + verification_request: &VerificationRequest, +) -> Result { + // Fetch the transaction + let item_status = client + .get_block_item_status(&verification_request.anchor_transaction_hash) + .await?; + + let (block_hash, summary) = item_status + .is_finalized() + .ok_or(VerifyError::RequestAnchorNotFinalized)?; + + // Extract account transaction + let BlockItemSummaryDetails::AccountTransaction(anchor_tx) = + summary.details.as_ref().known_or_err()? + else { + return Err(VerifyError::InvalidRequestAnchor); + }; + + // Extract data registered payload + let AccountTransactionEffects::DataRegistered { data } = + anchor_tx.effects.as_ref().known_or_err()? + else { + return Err(VerifyError::InvalidRequestAnchor); + }; + + // Decode anchor hash + let verification_request_anchor: VerificationRequestAnchor = cbor::cbor_decode(data.as_ref())?; + + Ok(VerificationRequestAnchorAndBlockHash { + verification_request_anchor, + block_hash: *block_hash, + }) +} + +/// Lookup the verification material needed to verify the presentation. +pub async fn lookup_verification_materials_and_validity( + client: &mut v2::Client, + block_identifier: BlockIdentifier, + presentation: &VerifiablePresentationV1, +) -> Result, VerifyError> { + let verification_material = + future::try_join_all(presentation.metadata().map(|cred_metadata| { + let mut client = client.clone(); + async move { + lookup_verification_material_and_validity( + &mut client, + block_identifier, + &cred_metadata, + ) + .await + } + })) + .await?; + Ok(verification_material) +} + +/// Lookup verification material for presentation +async fn lookup_verification_material_and_validity( + client: &mut v2::Client, + block_identifier: BlockIdentifier, + cred_metadata: &CredentialMetadataV1, +) -> Result { + Ok(match &cred_metadata.cred_metadata { + CredentialMetadataTypeV1::Account(metadata) => { + let account_info = client + .get_account_info( + &AccountIdentifier::CredId(metadata.cred_id), + block_identifier, + ) + .await?; + + let Some(account_cred) = + account_info + .response + .account_credentials + .values() + .find_map(|cred| { + cred.value + .as_ref() + .known() + .and_then(|c| (c.cred_id() == metadata.cred_id.as_ref()).then_some(c)) + }) + else { + return Err(VerifyError::CredentialNotPresent { + cred_id: metadata.cred_id, + account: account_info.response.account_address, + }); + }; + + match account_cred { + AccountCredentialWithoutProofs::Initial { .. } => { + return Err(VerifyError::InitialCredential { + cred_id: metadata.cred_id, + }) + } + AccountCredentialWithoutProofs::Normal { cdv, commitments } => { + let credential_validity = types::CredentialValidity { + created_at: account_cred.policy().created_at, + valid_to: cdv.policy.valid_to, + }; + + VerificationMaterialWithValidity { + verification_material: VerificationMaterial::Account( + AccountCredentialVerificationMaterial { + issuer: cdv.ip_identity, + attribute_commitments: commitments.cmm_attributes.clone(), + }, + ), + validity: CredentialValidityType::ValidityPeriod(credential_validity), + } + } + } + } + CredentialMetadataTypeV1::Identity(metadata) => { + let ip_info = client + .get_identity_providers(block_identifier) + .await? + .response + .try_filter(|ip| future::ready(ip.ip_identity == metadata.issuer)) + .next() + .await + .ok_or(VerifyError::UnknownIdentityProvider(metadata.issuer))? + .map_err(|status| QueryError::RPCError(RPCError::CallError(status)))?; + + let ars_infos: BTreeMap<_, _> = client + .get_anonymity_revokers(block_identifier) + .await? + .response + .map_ok(|ar_info| (ar_info.ar_identity, ar_info)) + .try_collect() + .await + .map_err(|status| QueryError::RPCError(RPCError::CallError(status)))?; + + VerificationMaterialWithValidity { + verification_material: VerificationMaterial::Identity( + IdentityCredentialVerificationMaterial { + ip_info, + ars_infos: ArInfos { + anonymity_revokers: ars_infos, + }, + }, + ), + validity: CredentialValidityType::ValidityPeriod(metadata.validity.clone()), + } + } + }) +} + +/// Error creating and registering anchor. +#[derive(thiserror::Error, Debug)] +pub enum CreateAnchorError { + #[error("node query error: {0}")] + Query(#[from] v2::QueryError), + #[error("data register transaction data is too large: {0}")] + TooLarge(#[from] TooLargeError), + #[error("CBOR serialization error: {0}")] + CborSerialization(#[from] CborSerializationError), +} + +impl From for CreateAnchorError { + fn from(err: RPCError) -> Self { + CreateAnchorError::Query(err.into()) + } +} + +/// Metadata for anchor transaction submission. +pub struct AnchorTransactionMetadata { + /// The signer object used to sign the on-chain anchor transaction. This must correspond to the `sender` account below. + pub signer: S, + /// The sender account of the anchor transaction. + pub sender: AccountAddress, + /// The sequence number for the sender account to use. + pub account_sequence_number: Nonce, + /// The transaction expiry time. + pub expiry: TransactionTime, +} + +/// Submit verification request anchor (VRA) and return the verification request. +/// +/// Notice that the VRA will only be submitted, it is not included on-chain yet when +/// the function returns. The transaction hash is returned +/// in [`VerificationRequest::anchor_transaction_hash`] and the transaction must +/// be tracked until finalization before the verification request is usable +/// (waiting for finalization can be done in the app that receives the verification request +/// to create a verifiable presentation). +pub async fn create_verification_request_and_submit_request_anchor< + S: ExactSizeTransactionSigner, +>( + client: &mut v2::Client, + anchor_transaction_metadata: AnchorTransactionMetadata, + verification_request_data: VerificationRequestData, + public_info: Option>, +) -> Result { + let verification_request_anchor = verification_request_data.to_anchor(public_info); + let cbor = cbor::cbor_encode(&verification_request_anchor)?; + let register_data = RegisteredData::try_from(cbor)?; + + let tx = send::register_data( + &anchor_transaction_metadata.signer, + anchor_transaction_metadata.sender, + anchor_transaction_metadata.account_sequence_number, + anchor_transaction_metadata.expiry, + register_data, + ); + let block_item = BlockItem::AccountTransaction(tx); + + // Submit the transaction to the chain. + let transaction_hash = client.send_block_item(&block_item).await?; + + Ok(VerificationRequest { + context: verification_request_data.context, + subject_claims: verification_request_data.subject_claims, + anchor_transaction_hash: transaction_hash, + }) +} + +/// Submit verification audit anchor (VAA). +/// +/// Notice that the VAA will only be submitted, it is not included on-chain yet when +/// the function returns. The transaction must +/// be tracked until finalization for the audit record to be registered successfully. +pub async fn submit_verification_audit_record_anchor( + client: &mut v2::Client, + anchor_transaction_metadata: AnchorTransactionMetadata, + verification_audit_record: &VerificationAuditRecord, + public_info: Option>, +) -> Result { + let verification_audit_anchor = verification_audit_record.to_anchor(public_info); + let cbor = cbor::cbor_encode(&verification_audit_anchor)?; + let register_data = RegisteredData::try_from(cbor)?; + + let tx = send::register_data( + &anchor_transaction_metadata.signer, + anchor_transaction_metadata.sender, + anchor_transaction_metadata.account_sequence_number, + anchor_transaction_metadata.expiry, + register_data, + ); + let item = BlockItem::AccountTransaction(tx); + + // Submit the transaction to the chain. + let transaction_hash = client.send_block_item(&item).await?; + + Ok(transaction_hash) +} diff --git a/src/web3id/v1/anchor.rs b/src/web3id/v1/anchor.rs deleted file mode 100644 index 19be9cca93..0000000000 --- a/src/web3id/v1/anchor.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Types and functions used in Concordium verifiable presentation protocol version 1. -use crate::v2::{self, RPCError}; -use concordium_base::web3id::v1::anchor::{ - VerificationAuditRecord, VerificationRequest, VerificationRequestData, -}; -use concordium_base::{ - base::Nonce, - common::{ - cbor::{self, CborSerializationError}, - types::TransactionTime, - }, - contracts_common::AccountAddress, - hashes::TransactionHash, - transactions::{send, BlockItem, ExactSizeTransactionSigner, TooLargeError}, -}; -use std::collections::HashMap; - -#[derive(thiserror::Error, Debug)] -pub enum CreateAnchorError { - #[error("node query error: {0}")] - Query(#[from] v2::QueryError), - #[error("data register transaction data is too large: {0}")] - TooLarge(#[from] TooLargeError), - #[error("CBOR serialization error: {0}")] - CborSerialization(#[from] CborSerializationError), -} - -impl From for CreateAnchorError { - fn from(err: RPCError) -> Self { - CreateAnchorError::Query(err.into()) - } -} - -/// Metadata for transaction submission. -pub struct AnchorTransactionMetadata<'a, S: ExactSizeTransactionSigner> { - /// The signer object used to sign the on-chain anchor transaction. This must correspond to the `sender` account below. - pub signer: &'a S, - /// The sender account of the anchor transaction. - pub sender: AccountAddress, - /// The sequence number for the sender account to use. - pub account_sequence_number: Nonce, - /// The transaction expiry time. - pub expiry: TransactionTime, -} - -/// Submit verification request anchor (VRA) and return the verification request. -/// Notice that the VRA will only be submitted, it is not included on-chain yet when -/// the function returns. The transaction hash is returned -/// in [`VerificationRequest::anchor_transaction_hash`] and the transaction must -/// be tracked until finalization before the verification request is usable -/// (waiting for finalization can be done in the app that receives the verification request -/// to create a verifiable presentation). -pub async fn submit_verification_request_anchor( - client: &mut v2::Client, - anchor_transaction_metadata: AnchorTransactionMetadata<'_, S>, - verification_request_data: VerificationRequestData, - public_info: HashMap, -) -> Result { - let verification_request_anchor = verification_request_data.to_anchor(public_info); - let cbor = cbor::cbor_encode(&verification_request_anchor)?; - let register_data = cbor.try_into()?; - - let tx = send::register_data( - &anchor_transaction_metadata.signer, - anchor_transaction_metadata.sender, - anchor_transaction_metadata.account_sequence_number, - anchor_transaction_metadata.expiry, - register_data, - ); - let block_item = BlockItem::AccountTransaction(tx); - - // Submit the transaction to the chain. - let transaction_hash = client.send_block_item(&block_item).await?; - - Ok(VerificationRequest { - context: verification_request_data.context, - subject_claims: verification_request_data.subject_claims, - anchor_transaction_hash: transaction_hash, - }) -} - -/// Submit verification audit anchor (VAA). -/// Notice that the VAA will only be submitted, it is not included on-chain yet when -/// the function returns. The transaction must -/// be tracked until finalization for the audit record to be registered successfully. -pub async fn submit_verification_audit_record_anchor( - client: &mut v2::Client, - anchor_transaction_metadata: AnchorTransactionMetadata<'_, S>, - verification_audit_record: &VerificationAuditRecord, - public_info: HashMap, -) -> Result { - let verification_audit_anchor = verification_audit_record.to_anchor(public_info); - let cbor = cbor::cbor_encode(&verification_audit_anchor)?; - let register_data = cbor.try_into()?; - - let tx = send::register_data( - &anchor_transaction_metadata.signer, - anchor_transaction_metadata.sender, - anchor_transaction_metadata.account_sequence_number, - anchor_transaction_metadata.expiry, - register_data, - ); - let item = BlockItem::AccountTransaction(tx); - - // Submit the transaction to the chain. - let transaction_hash = client.send_block_item(&item).await?; - - Ok(transaction_hash) -}