From e996447a3a63c0825cbfb74216657e7c4a6a8c98 Mon Sep 17 00:00:00 2001 From: TYRONE AVNIT Date: Thu, 16 May 2024 16:17:41 -0500 Subject: [PATCH] TECH-156 - Add support for whitelisting issuers - Add support for initializing with an admin --- .github/workflows/ci-local.yaml | 5 - scripts/deploy-civic.sh | 12 +- .../civic_canister_backend.did | 5 + src/civic_canister_backend/src/config.rs | 111 ++++++-- src/civic_canister_backend/src/credential.rs | 252 +++++++++--------- .../tests/integration_tests.rs | 229 ++++++---------- 6 files changed, 313 insertions(+), 301 deletions(-) diff --git a/.github/workflows/ci-local.yaml b/.github/workflows/ci-local.yaml index 76f7466..0b2c0a2 100644 --- a/.github/workflows/ci-local.yaml +++ b/.github/workflows/ci-local.yaml @@ -1,11 +1,6 @@ name: CI Build and Test (Local setup) on: [push, pull_request] - # push: - # branches: [ "develop" ] - # pull_request: - # branches: [ "develop" ] - env: CARGO_TERM_COLOR: always diff --git a/scripts/deploy-civic.sh b/scripts/deploy-civic.sh index 54cc090..4ee3af5 100755 --- a/scripts/deploy-civic.sh +++ b/scripts/deploy-civic.sh @@ -35,6 +35,7 @@ EOF II_CANISTER_ID= DFX_NETWORK= +ADMIN_PRINCIPAL_ID=tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae while [[ $# -gt 0 ]] do @@ -123,6 +124,15 @@ mv src/civic_canister_backend/dist/.well-known/ii-alternative-origins ./ii-alter cat ./ii-alternative-origins-template | sed "s+ISSUER_FE_HOSTNAME_PLACEHOLDER+$ALTERNATIVE_ORIGINS+g" > src/civic_canister_backend/dist/.well-known/ii-alternative-origins rm ./ii-alternative-origins-template -dfx deploy civic_canister_backend --network "$DFX_NETWORK" --argument '(opt record { idp_canister_ids = vec{ principal "'"$II_CANISTER_ID"'" }; ic_root_key_der = vec '"$rootkey_did"'; derivation_origin = "'"$ISSUER_DERIVATION_ORIGIN"'"; frontend_hostname = "'"$ISSUER_FRONTEND_HOSTNAME"'"; })' +dfx deploy civic_canister_backend --network "$DFX_NETWORK" --argument '( + opt record { + idp_canister_ids = vec { principal "'"$II_CANISTER_ID"'" }; + ic_root_key_der = vec '"$rootkey_did"'; + derivation_origin = "'"$ISSUER_DERIVATION_ORIGIN"'"; + frontend_hostname = "'"$ISSUER_FRONTEND_HOSTNAME"'"; + admin = principal "'"$ADMIN_PRINCIPAL_ID"'"; + authorized_issuers = vec { principal "'"$ADMIN_PRINCIPAL_ID"'" }; + } +)' # Revert changes git checkout src/civic_canister_backend/dist/.well-known/ii-alternative-origins \ No newline at end of file diff --git a/src/civic_canister_backend/civic_canister_backend.did b/src/civic_canister_backend/civic_canister_backend.did index 5fcda01..fd61c8f 100644 --- a/src/civic_canister_backend/civic_canister_backend.did +++ b/src/civic_canister_backend/civic_canister_backend.did @@ -64,6 +64,8 @@ type IssuerInit = record { idp_canister_ids : vec principal; ic_root_key_der : blob; frontend_hostname : text; + admin: principal; + authorized_issuers: vec principal; }; type PrepareCredentialRequest = record { signed_id_alias : SignedIdAlias; @@ -103,4 +105,7 @@ service : (opt IssuerInit) -> { prepare_credential : (PrepareCredentialRequest) -> (Result_4); update_credential : (principal, text, StoredCredential) -> (Result); vc_consent_message : (Icrc21VcConsentMessageRequest) -> (Result_5); + get_admin : () -> (principal) query; + add_issuer: (principal) -> (); + remove_issuer: (principal) -> (); } diff --git a/src/civic_canister_backend/src/config.rs b/src/civic_canister_backend/src/config.rs index 87056f3..14ce86c 100644 --- a/src/civic_canister_backend/src/config.rs +++ b/src/civic_canister_backend/src/config.rs @@ -1,8 +1,7 @@ - +use crate::credential::{update_root_hash, StoredCredential}; +use canister_sig_util::signature_map::{SignatureMap, LABEL_SIG}; use std::cell::RefCell; use std::collections::HashMap; -use canister_sig_util::signature_map::{SignatureMap, LABEL_SIG}; -use crate::credential::{StoredCredential, update_root_hash}; use crate::consent_message::{get_vc_consent_message, SupportedLanguage}; @@ -10,7 +9,6 @@ use candid::{candid_method, CandidType, Deserialize, Principal}; // use ic_cdk::candid::candid_method; use canister_sig_util::{extract_raw_root_pk_from_der, IC_ROOT_PK_DER}; - use ic_cdk_macros::{init, query, update}; use ic_certification::{labeled_hash, pruned}; @@ -20,25 +18,19 @@ use include_dir::{include_dir, Dir}; use serde_bytes::ByteBuf; - -use std::borrow::Cow; use asset_util::{collect_assets, CertifiedAssets}; +use std::borrow::Cow; use vc_util::issuer_api::{ - DerivationOriginData, DerivationOriginError, - DerivationOriginRequest, Icrc21ConsentInfo, Icrc21Error, - Icrc21VcConsentMessageRequest + DerivationOriginData, DerivationOriginError, DerivationOriginRequest, Icrc21ConsentInfo, + Icrc21Error, Icrc21VcConsentMessageRequest, }; use ic_cdk::api; use ic_cdk_macros::post_upgrade; - - - const PROD_II_CANISTER_ID: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai"; - thread_local! { /// Static configuration of the canister set by init() or post_upgrade(). pub(crate) static CONFIG: RefCell = RefCell::new(ConfigCell::init(config_memory(), IssuerConfig::default()).expect("failed to initialize stable cell")); @@ -54,8 +46,6 @@ thread_local! { type Memory = RestrictedMemory; type ConfigCell = StableCell; - - /// Reserve the first stable memory page for the configuration stable cell. fn config_memory() -> Memory { RestrictedMemory::new(DefaultMemoryImpl::default(), 0..1) @@ -64,7 +54,7 @@ fn config_memory() -> Memory { #[cfg(target_arch = "wasm32")] use ic_cdk::println; -#[derive(CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize)] pub(crate) struct IssuerConfig { /// Root of trust for checking canister signatures. pub(crate) ic_root_key_raw: Vec, @@ -74,6 +64,10 @@ pub(crate) struct IssuerConfig { derivation_origin: String, /// Frontend hostname to be used by the issuer. frontend_hostname: String, + // Admin who can add authorized issuers + admin: Principal, + // List of authorized issuers who can issue credentials + pub authorized_issuers: Vec, } impl Storable for IssuerConfig { @@ -95,11 +89,12 @@ impl Default for IssuerConfig { idp_canister_ids: vec![Principal::from_text(PROD_II_CANISTER_ID).unwrap()], derivation_origin: derivation_origin.clone(), frontend_hostname: derivation_origin, + admin: ic_cdk::api::caller(), + authorized_issuers: vec![ic_cdk::api::caller()], } } } - impl From for IssuerConfig { fn from(init: IssuerInit) -> Self { Self { @@ -108,12 +103,13 @@ impl From for IssuerConfig { idp_canister_ids: init.idp_canister_ids, derivation_origin: init.derivation_origin, frontend_hostname: init.frontend_hostname, + admin: init.admin, + authorized_issuers: init.authorized_issuers, } } - } -#[derive(CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize)] struct IssuerInit { /// Root of trust for checking canister signatures. ic_root_key_der: Vec, @@ -123,6 +119,10 @@ struct IssuerInit { derivation_origin: String, /// Frontend hostname be used by the issuer. frontend_hostname: String, + // Admin who can add authorized issuers + admin: Principal, + // List of authorized issuers who can issue credentials + authorized_issuers: Vec, } #[init] @@ -130,11 +130,70 @@ struct IssuerInit { fn init(init_arg: Option) { if let Some(init) = init_arg { apply_config(init); - }; - + } else { + // Initialize with default values and a specified admin + let default_config = IssuerConfig::default(); + CONFIG.with(|config_cell| { + let mut config = config_cell.borrow_mut(); + *config = ConfigCell::init(config_memory(), default_config) + .expect("Failed to initialize config"); + }); + } init_assets(); } +#[update] +#[candid_method(update)] +fn add_issuer(new_issuer: Principal) { + let caller = ic_cdk::api::caller(); + CONFIG.with(|config_cell| { + let mut config = config_cell.borrow_mut(); + // Retrieve the current configuration + let mut current_config = config.get().clone(); // Clone into a mutable local variable + + // Check if the caller is the admin and modify the config + if caller == current_config.admin { + // Ensure no duplicates if that's intended + if !current_config.authorized_issuers.contains(&new_issuer) { + current_config.authorized_issuers.push(new_issuer); + } + // Save the updated configuration + let _ = config.set(current_config); // Pass the modified IssuerConfig back to set + } else { + ic_cdk::api::trap("Caller is not authorized as admin."); + } + }); +} + +#[update] +#[candid_method(update)] +fn remove_issuer(issuer: Principal) { + let caller = ic_cdk::api::caller(); + CONFIG.with(|config_cell| { + let mut config = config_cell.borrow_mut(); + // Retrieve the current configuration + let mut current_config = config.get().clone(); // Clone into a mutable local variable + + if caller == current_config.admin { + // Remove the issuer if they exist in the list + current_config.authorized_issuers.retain(|x| *x != issuer); + // Save the updated configuration + let _ = config.set(current_config); // Pass the modified IssuerConfig back to set + } else { + ic_cdk::api::trap("Caller is not authorized as admin."); + } + }); +} + +#[query] +fn get_admin() -> Principal { + CONFIG.with(|config| { + let config_borrowed = config.borrow(); // Obtain a read-only borrow + // Now you can access the config + return config_borrowed.get().admin; + }) +} + #[post_upgrade] fn post_upgrade(init_arg: Option) { init(init_arg); @@ -191,7 +250,6 @@ async fn vc_consent_message( ) } - #[update] #[candid_method] async fn derivation_origin( @@ -216,10 +274,7 @@ fn get_derivation_origin(hostname: &str) -> Result HttpResponse { @@ -249,8 +304,6 @@ pub fn http_request(req: HttpRequest) -> HttpResponse { } } - - #[derive(Clone, Debug, CandidType, Deserialize)] pub struct HttpRequest { pub method: String, @@ -267,4 +320,4 @@ pub struct HttpResponse { pub body: ByteBuf, } -ic_cdk::export_candid!(); \ No newline at end of file +ic_cdk::export_candid!(); diff --git a/src/civic_canister_backend/src/credential.rs b/src/civic_canister_backend/src/credential.rs index 1070532..6872e3c 100644 --- a/src/civic_canister_backend/src/credential.rs +++ b/src/civic_canister_backend/src/credential.rs @@ -1,42 +1,45 @@ -use std::fmt; -use candid::{CandidType, Deserialize, Principal}; use candid::candid_method; +use candid::{CandidType, Deserialize, Principal}; +use std::fmt; +use canister_sig_util::signature_map::LABEL_SIG; +use identity_core::common::Url; +use identity_credential::credential::{CredentialBuilder, Subject}; use serde::Serialize; use serde_json::Value; -use std::collections::{HashMap, BTreeMap}; -use identity_credential::credential::{CredentialBuilder, Subject}; -use identity_core::common::Url; -use std::iter::repeat; -use canister_sig_util::signature_map::LABEL_SIG; use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, HashMap}; +use std::iter::repeat; use ic_cdk::api::{caller, set_certified_data, time}; -use ic_cdk_macros::{update, query}; +use ic_cdk_macros::{query, update}; use vc_util::issuer_api::{ CredentialSpec, GetCredentialRequest, IssueCredentialError, IssuedCredentialData, - PrepareCredentialRequest, PreparedCredentialData, SignedIdAlias + PrepareCredentialRequest, PreparedCredentialData, SignedIdAlias, }; -use vc_util::{did_for_principal, get_verified_id_alias_from_jws, vc_jwt_to_jws, vc_signing_input, vc_signing_input_hash, AliasTuple}; use canister_sig_util::CanisterSigPublicKey; -use ic_certification::{Hash, fork_hash, labeled_hash}; +use ic_certification::{fork_hash, labeled_hash, Hash}; use serde_bytes::ByteBuf; +use vc_util::{ + did_for_principal, get_verified_id_alias_from_jws, vc_jwt_to_jws, vc_signing_input, + vc_signing_input_hash, AliasTuple, +}; use lazy_static::lazy_static; // Assuming these are defined in the same or another module that needs to be imported extern crate asset_util; -use crate::config::{CONFIG, CREDENTIALS, SIGNATURES, ASSETS}; +use crate::config::{ASSETS, CONFIG, CREDENTIALS, SIGNATURES}; use identity_core::common::Timestamp; // The expiration of issued verifiable credentials. const MINUTE_NS: u64 = 60 * 1_000_000_000; const VC_EXPIRATION_PERIOD_NS: u64 = 15 * MINUTE_NS; // Authorized Civic Principal - get this from the frontend -const AUTHORIZED_PRINCIPAL: &str = "tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae"; - +const AUTHORIZED_PRINCIPAL: &str = + "tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae"; lazy_static! { // Seed and public key used for signing the credentials. @@ -44,7 +47,6 @@ lazy_static! { static ref CANISTER_SIG_PK: CanisterSigPublicKey = CanisterSigPublicKey::new(ic_cdk::id(), CANISTER_SIG_SEED.clone()); } - #[derive(Debug)] pub enum SupportedCredentialType { VerifiedAdult, @@ -68,7 +70,7 @@ pub(crate) enum ClaimValue { #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] pub(crate) struct Claim { - pub(crate) claims:HashMap, + pub(crate) claims: HashMap, } impl From for Value { @@ -80,28 +82,29 @@ impl From for Value { ClaimValue::Number(n) => Value::Number(n.into()), ClaimValue::Claim(nested_claim) => { serde_json::to_value(nested_claim).unwrap_or(Value::Null) - }, + } } } } - impl Claim { pub(crate) fn into(self) -> Subject { - let btree_map: BTreeMap = self.claims.into_iter() - .map(|(k, v)| (k, v.into())) - .collect(); - Subject::with_properties(btree_map) + let btree_map: BTreeMap = self + .claims + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); + Subject::with_properties(btree_map) } } #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] pub(crate) struct StoredCredential { - pub(crate) id: String, - pub(crate) type_: Vec, - pub(crate) context: Vec, - pub(crate) issuer: String, - pub(crate) claim: Vec, + pub(crate) id: String, + pub(crate) type_: Vec, + pub(crate) context: Vec, + pub(crate) issuer: String, + pub(crate) claim: Vec, } #[derive(CandidType, Deserialize, Debug)] pub(crate) enum CredentialError { @@ -109,26 +112,34 @@ pub(crate) enum CredentialError { UnauthorizedSubject(String), } - -// Helper functions for constructing the credential that is returned from the canister +// Helper functions for constructing the credential that is returned from the canister /// Build a credentialSubject { -/// id: SubjectId, +/// id: SubjectId, /// otherData /// } -pub(crate) fn build_claims_into_credential_subjects(claims: Vec, subject: String) -> Vec { - claims.into_iter().zip(repeat(subject)).map(|(c, id )|{ - let mut sub = c.into(); - sub.id = Url::parse(id).ok(); - sub - }).collect() +pub(crate) fn build_claims_into_credential_subjects( + claims: Vec, + subject: String, +) -> Vec { + claims + .into_iter() + .zip(repeat(subject)) + .map(|(c, id)| { + let mut sub = c.into(); + sub.id = Url::parse(id).ok(); + sub + }) + .collect() } - -pub(crate) fn add_context(mut credential: CredentialBuilder, context: Vec) -> CredentialBuilder { +pub(crate) fn add_context( + mut credential: CredentialBuilder, + context: Vec, +) -> CredentialBuilder { for c in context { - credential = credential.context(Url::parse(c).unwrap()); + credential = credential.context(Url::parse(c).unwrap()); } credential } @@ -140,8 +151,8 @@ fn authorize_vc_request( ) -> Result { CONFIG.with_borrow(|config| { let config = config.get(); - - // check if the ID alias is legitimate and was issued by the internet identity canister + + // check if the ID alias is legitimate and was issued by the internet identity canister for idp_canister_id in &config.idp_canister_ids { if let Ok(alias_tuple) = get_verified_id_alias_from_jws( &alias.credential_jws, @@ -159,30 +170,43 @@ fn authorize_vc_request( }) } - - #[update] -#[candid_method] -fn add_credentials(principal: Principal, new_credentials: Vec) -> Result { - // Check if the caller is the authorized principal - if caller().to_text() != AUTHORIZED_PRINCIPAL { - return Err(CredentialError::UnauthorizedSubject("Unauthorized: You do not have permission to add credentials.".to_string())); +#[candid_method(update)] +fn add_credentials(principal: Principal, new_credentials: Vec) -> String { + let caller = ic_cdk::api::caller(); + + // Access the configuration and check if the caller is an authorized issuer + let is_authorized = CONFIG.with(|config_cell| { + let config = config_cell.borrow(); + let current_config = config.get(); + current_config.authorized_issuers.contains(&caller) + }); + + if !is_authorized { + return "Unauthorized: You do not have permission to add credentials.".to_string(); } + // If authorized, proceed to add credentials CREDENTIALS.with_borrow_mut(|credentials| { - let entry = credentials.entry(principal).or_insert_with(Vec::new); - entry.extend(new_credentials.clone()); - }); - let credential_info = format!("Added credentials: \n{:?}", new_credentials); - Ok(credential_info) + let entry = credentials.entry(principal).or_insert_with(Vec::new); + entry.extend(new_credentials.clone()); + }); + + format!("Added credentials: \n{:?}", new_credentials) } #[update] #[candid_method] -fn update_credential(principal: Principal, credential_id: String, updated_credential: StoredCredential) -> Result { +fn update_credential( + principal: Principal, + credential_id: String, + updated_credential: StoredCredential, +) -> Result { // Check if the caller is the authorized principal if caller().to_text() != AUTHORIZED_PRINCIPAL { - return Err(CredentialError::UnauthorizedSubject("Unauthorized: You do not have permission to update credentials.".to_string())); + return Err(CredentialError::UnauthorizedSubject( + "Unauthorized: You do not have permission to update credentials.".to_string(), + )); } // Access the credentials storage and attempt to update the specified credential @@ -190,10 +214,17 @@ fn update_credential(principal: Principal, credential_id: String, updated_creden if let Some(creds) = credentials.get_mut(&principal) { if let Some(pos) = creds.iter().position(|c| c.id == credential_id) { creds[pos] = updated_credential.clone(); - return Ok(format!("Credential updated successfully: {:?}", updated_credential)); + return Ok(format!( + "Credential updated successfully: {:?}", + updated_credential + )); } } - Err(CredentialError::NoCredentialFound(format!("No credential found with ID {} for principal {}", credential_id, principal.to_text()))) + Err(CredentialError::NoCredentialFound(format!( + "No credential found with ID {} for principal {}", + credential_id, + principal.to_text() + ))) }) } @@ -221,14 +252,13 @@ async fn prepare_credential( sigs.add_signature(&CANISTER_SIG_SEED, msg_hash); }); update_root_hash(); - // return a prepared context + // return a prepared context Ok(PreparedCredentialData { prepared_context: Some(ByteBuf::from(credential_jwt.as_bytes())), }) } - -pub (crate) fn update_root_hash() { +pub(crate) fn update_root_hash() { SIGNATURES.with_borrow(|sigs| { ASSETS.with_borrow(|assets| { let prefixed_root_hash = fork_hash( @@ -242,7 +272,6 @@ pub (crate) fn update_root_hash() { }) } - #[query] #[candid_method(query)] fn get_credential(req: GetCredentialRequest) -> Result { @@ -262,7 +291,7 @@ fn get_credential(req: GetCredentialRequest) -> Result s, Err(_) => { return Result::::Err(internal_error( @@ -299,12 +328,10 @@ fn get_credential(req: GetCredentialRequest) -> Result::Ok(IssuedCredentialData { vc_jws }) } - fn internal_error(msg: &str) -> IssueCredentialError { IssueCredentialError::Internal(String::from(msg)) } - fn prepare_credential_jwt( credential_spec: &CredentialSpec, alias_tuple: &AliasTuple, @@ -315,32 +342,29 @@ fn prepare_credential_jwt( return Err(IssueCredentialError::UnsupportedCredentialSpec(err)); } }; - //currently only supports VerifiedAdults spec + //currently only supports VerifiedAdults spec let credential = verify_authorized_principal(credential_type, alias_tuple)?; Ok(build_credential( alias_tuple.id_alias, credential_spec, - credential + credential, )) - } - - struct CredentialParams { - spec: CredentialSpec, - subject_id: String, - credential_id_url: String, - context: Vec, - issuer_url: String, - claims: Vec, - expiration_timestamp_s: u32, +struct CredentialParams { + spec: CredentialSpec, + subject_id: String, + credential_id_url: String, + context: Vec, + issuer_url: String, + claims: Vec, + expiration_timestamp_s: u32, } - fn build_credential( subject_principal: Principal, credential_spec: &CredentialSpec, - credential: StoredCredential + credential: StoredCredential, ) -> String { let params = CredentialParams { spec: credential_spec.clone(), @@ -353,7 +377,7 @@ fn build_credential( }; build_credential_jwt(params) } - + // checks if the user has a credential and returns it fn verify_authorized_principal( credential_type: SupportedCredentialType, @@ -366,44 +390,34 @@ fn verify_authorized_principal( }) { for c in credentials { if c.type_.contains(&credential_type.to_string()) { - return Ok(c) + return Ok(c); } } - } - // no (matching) credential found for this user - println!( - "*** principal {} it is not authorized for credential type {:?}", - alias_tuple.id_dapp.to_text(), - credential_type - ); - Err(IssueCredentialError::UnauthorizedSubject(format!( - "unauthorized principal {}", - alias_tuple.id_dapp.to_text() - ))) + } + // no (matching) credential found for this user + println!( + "*** principal {} it is not authorized for credential type {:?}", + alias_tuple.id_dapp.to_text(), + credential_type + ); + Err(IssueCredentialError::UnauthorizedSubject(format!( + "unauthorized principal {}", + alias_tuple.id_dapp.to_text() + ))) } - -pub (crate) fn verify_credential_spec(spec: &CredentialSpec) -> Result { +pub(crate) fn verify_credential_spec( + spec: &CredentialSpec, +) -> Result { match spec.credential_type.as_str() { - "VerifiedAdult" => { - Ok(SupportedCredentialType::VerifiedAdult) - } + "VerifiedAdult" => Ok(SupportedCredentialType::VerifiedAdult), other => Err(format!("Credential {} is not supported", other)), } } - /// Builds a verifiable credential with the given parameters and returns the credential as a JWT-string. fn build_credential_jwt(params: CredentialParams) -> String { - // let mut subject_json = json!({"id": params.subject_id}); - // subject_json.as_object_mut().unwrap().insert( - // params.spec.credential_type.clone(), - // credential_spec_args_to_json(¶ms.spec), - // ); - // let subject = Subject::from_json_value(subject_json).unwrap(); - - // build "credentialSubject" objects - let subjects = build_claims_into_credential_subjects(params.claims, params.subject_id); + let subjects = build_claims_into_credential_subjects(params.claims, params.subject_id); let expiration_date = Timestamp::from_unix(params.expiration_timestamp_s as i64) .expect("internal: failed computing expiration timestamp"); @@ -412,32 +426,29 @@ fn build_credential_jwt(params: CredentialParams) -> String { .issuer(Url::parse(params.issuer_url).unwrap()) .type_("VerifiedCredential".to_string()) .type_(params.spec.credential_type) - .subjects(subjects) // add objects to the credentialSubject + .subjects(subjects) // add objects to the credentialSubject .expiration_date(expiration_date); - // add all the context + // add all the context credential = add_context(credential, params.context); - // //add all the type data - // credential = add_types(credential, params.types_); let credential = credential.build().unwrap(); credential.serialize_jwt().unwrap() } - #[query] #[candid_method(query)] fn get_all_credentials(principal: Principal) -> Result, CredentialError> { - if let Some(c) = CREDENTIALS.with_borrow(|credentials| { - credentials.get(&principal).cloned() - }) { + if let Some(c) = CREDENTIALS.with_borrow(|credentials| credentials.get(&principal).cloned()) { Ok(c) } else { - Err(CredentialError::NoCredentialFound(format!("No credentials found for principal {}", principal.to_text()))) + Err(CredentialError::NoCredentialFound(format!( + "No credentials found for principal {}", + principal.to_text() + ))) } } - fn hash_bytes(value: impl AsRef<[u8]>) -> Hash { let mut hasher = Sha256::new(); hasher.update(value.as_ref()); @@ -447,12 +458,3 @@ fn hash_bytes(value: impl AsRef<[u8]>) -> Hash { fn exp_timestamp_s() -> u32 { ((time() + VC_EXPIRATION_PERIOD_NS) / 1_000_000_000) as u32 } - - - -// pub fn add_types(mut credential: CredentialBuilder, types: Vec) -> CredentialBuilder { -// for t in types { -// credential = credential.type_(t); -// } -// credential -// } \ No newline at end of file diff --git a/src/civic_canister_backend/tests/integration_tests.rs b/src/civic_canister_backend/tests/integration_tests.rs index e6067b7..bb5431c 100644 --- a/src/civic_canister_backend/tests/integration_tests.rs +++ b/src/civic_canister_backend/tests/integration_tests.rs @@ -4,41 +4,34 @@ use assert_matches::assert_matches; use candid::{CandidType, Deserialize, Principal}; use canister_sig_util::{extract_raw_root_pk_from_der, CanisterSigPublicKey}; -use canister_tests::api::http_request; use canister_tests::api::internet_identity::vc_mvp as ii_api; use canister_tests::flows; -use canister_tests::framework::{env, get_wasm_path, principal, principal_1, test_principal, time, II_WASM}; +use canister_tests::framework::{ + env, get_wasm_path, principal_1, test_principal, II_WASM, +}; use ic_cdk::api::management_canister::provisional::CanisterId; -use ic_response_verification::types::VerificationInfo; -use ic_response_verification::verify_request_response_pair; + use ic_test_state_machine_client::{call_candid, call_candid_as}; use ic_test_state_machine_client::{query_candid_as, CallError, StateMachine}; -use internet_identity_interface::http_gateway::{HttpRequest, HttpResponse}; - use internet_identity_interface::internet_identity::types::vc_mvp::{ GetIdAliasRequest, PrepareIdAliasRequest, }; use internet_identity_interface::internet_identity::types::FrontendHostname; use lazy_static::lazy_static; -use serde_bytes::ByteBuf; use std::collections::HashMap; use std::path::PathBuf; -use std::time::{Duration, UNIX_EPOCH}; +use std::time::UNIX_EPOCH; use vc_util::issuer_api::{ ArgumentValue, CredentialSpec, DerivationOriginData, DerivationOriginError, DerivationOriginRequest, GetCredentialRequest, Icrc21ConsentInfo, Icrc21ConsentPreferences, Icrc21Error, Icrc21VcConsentMessageRequest, IssueCredentialError, IssuedCredentialData, PrepareCredentialRequest, PreparedCredentialData, SignedIdAlias as SignedIssuerIdAlias, - -}; -use vc_util::{ - get_verified_id_alias_from_jws, verify_credential_jws_with_canister_id - }; +use vc_util::{get_verified_id_alias_from_jws, verify_credential_jws_with_canister_id}; const DUMMY_ROOT_KEY: &str ="308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100adf65638a53056b2222c91bb2457b0274bca95198a5acbdadfe7fd72178f069bdea8d99e9479d8087a2686fc81bf3c4b11fe275570d481f1698f79d468afe0e57acc1e298f8b69798da7a891bbec197093ec5f475909923d48bfed6843dbed1f"; const DUMMY_II_CANISTER_ID: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai"; @@ -105,7 +98,6 @@ pub fn install_issuer(env: &StateMachine, init: &IssuerInit) -> CanisterId { } mod api { - use super::*; @@ -155,8 +147,16 @@ mod api { user: Principal, credential: StoredCredential, ) -> Result, CallError> { - let civic_issuer = Principal::from_text("tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae").unwrap(); - call_candid_as(env, canister_id, civic_issuer, "add_credentials", (user, vec!(credential), )) + let civic_issuer = + Principal::from_text("tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae") + .unwrap(); + call_candid_as( + env, + canister_id, + civic_issuer, + "add_credentials", + (user, vec![credential]), + ) .map(|(x,)| x) } @@ -165,16 +165,19 @@ mod api { canister_id: CanisterId, user: Principal, credential_id: String, - updated_credential: StoredCredential + updated_credential: StoredCredential, ) -> Result, CallError> { - let civic_issuer = Principal::from_text("tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae").unwrap(); + let civic_issuer = + Principal::from_text("tglqb-kbqlj-to66e-3w5sg-kkz32-c6ffi-nsnta-vj2gf-vdcc5-5rzjk-jae") + .unwrap(); call_candid_as( env, canister_id, civic_issuer, "update_credential", (user, credential_id, updated_credential), - ).map(|(x,)| x) + ) + .map(|(x,)| x) } pub fn get_all_credentials( @@ -303,32 +306,37 @@ fn should_update_credential_successfully() { let principal = principal_1(); let original_credential = construct_adult_credential(); let mut updated_credential = construct_adult_credential(); - updated_credential.claim[0].claims.entry("Is over 18".to_string()).and_modify(|x| *x = ClaimValue::Boolean(false)); + updated_credential.claim[0] + .claims + .entry("Is over 18".to_string()) + .and_modify(|x| *x = ClaimValue::Boolean(false)); let id = original_credential.id.clone(); // Add a credential first to update it later - api::add_credentials(&env, issuer_id, principal, original_credential).expect("failed to add credential"); + let _ = api::add_credentials(&env, issuer_id, principal, original_credential) + .expect("failed to add credential"); // Update the credential - let response = api::update_credential( - &env, - issuer_id, - principal, - id.clone(), - updated_credential, - ) - .expect("API call failed"); + let response = + api::update_credential(&env, issuer_id, principal, id.clone(), updated_credential) + .expect("API call failed"); assert_matches!(response, Ok(_)); - let stored_updated_credential = api::get_all_credentials(&env, issuer_id, principal).expect("API call failed").expect("get_all_credentials error"); - // assert there is only one version of the VC + let stored_updated_credential = api::get_all_credentials(&env, issuer_id, principal) + .expect("API call failed") + .expect("get_all_credentials error"); + // assert there is only one version of the VC assert_eq!(stored_updated_credential.len(), 1); // that was changed to the updated_credential assert_eq!(stored_updated_credential[0].id, id); - assert_matches!(stored_updated_credential[0].claim[0].claims.get("Is over 18").unwrap(), &ClaimValue::Boolean(false)); - - + assert_matches!( + stored_updated_credential[0].claim[0] + .claims + .get("Is over 18") + .unwrap(), + &ClaimValue::Boolean(false) + ); } #[test] @@ -351,7 +359,6 @@ fn should_fail_update_credential_if_not_found() { assert_matches!(response, Err(CredentialError::NoCredentialFound(_))); } - #[test] fn should_fail_vc_consent_message_if_not_supported() { let env = env(); @@ -373,7 +380,6 @@ fn should_fail_vc_consent_message_if_not_supported() { assert_matches!(response, Err(Icrc21Error::UnsupportedCanisterCall(_))); } - #[test] fn should_fail_prepare_credential_for_unauthorized_principal() { let env = env(); @@ -418,7 +424,13 @@ fn should_fail_get_credential_for_wrong_sender() { let issuer_id = install_issuer(&env, &DUMMY_ISSUER_INIT); let signed_id_alias = DUMMY_SIGNED_ID_ALIAS.clone(); let authorized_principal = Principal::from_text(DUMMY_ALIAS_ID_DAPP_PRINCIPAL).unwrap(); - api::add_credentials(&env, issuer_id, authorized_principal, construct_adult_credential()).expect("failed to add employee"); + let _ = api::add_credentials( + &env, + issuer_id, + authorized_principal, + construct_adult_credential(), + ) + .expect("failed to add employee"); let unauthorized_principal = test_principal(2); let prepare_credential_response = api::prepare_credential( @@ -524,7 +536,8 @@ fn should_prepare_adult_credential_for_authorized_principal() { let issuer_id = install_issuer(&env, &DUMMY_ISSUER_INIT); let authorized_principal = Principal::from_text(DUMMY_ALIAS_ID_DAPP_PRINCIPAL).unwrap(); let credential = construct_adult_credential(); - api::add_credentials(&env, issuer_id, authorized_principal, credential).expect("API call failed"); + let _ = api::add_credentials(&env, issuer_id, authorized_principal, credential) + .expect("API call failed"); let response = api::prepare_credential( &env, issuer_id, @@ -537,6 +550,7 @@ fn should_prepare_adult_credential_for_authorized_principal() { .expect("API call failed"); assert_matches!(response, Ok(_)); } + /// Verifies that different credentials are being created including II interactions. #[test] fn should_issue_credential_e2e() -> Result<(), CallError> { @@ -592,11 +606,14 @@ fn should_issue_credential_e2e() -> Result<(), CallError> { ) .expect("Invalid ID alias"); - api::add_credentials(&env, issuer_id, alias_tuple.id_dapp, construct_adult_credential())?; + let _ = api::add_credentials( + &env, + issuer_id, + alias_tuple.id_dapp, + construct_adult_credential(), + )?; - for credential_spec in [ - adult_credential_spec(), - ] { + for credential_spec in [adult_credential_spec()] { let prepared_credential = api::prepare_credential( &env, issuer_id, @@ -637,9 +654,6 @@ fn should_issue_credential_e2e() -> Result<(), CallError> { .expect("credential verification failed"); let vc_claims = claims.vc().expect("missing VC claims"); println!("{:?}", vc_claims); - - // validate_claims_match_spec(vc_claims, &credential_spec).expect("Clam validation failed"); - } Ok(()) @@ -652,97 +666,34 @@ fn should_configure() { api::configure(&env, issuer_id, &DUMMY_ISSUER_INIT).expect("API call failed"); } -// /// Verifies that the expected assets is delivered and certified. -// #[test] -// fn issuer_canister_serves_http_assets() -> Result<(), CallError> { -// fn verify_response_certification( -// env: &StateMachine, -// canister_id: CanisterId, -// request: HttpRequest, -// http_response: HttpResponse, -// min_certification_version: u16, -// ) -> VerificationInfo { -// verify_request_response_pair( -// ic_http_certification::HttpRequest { -// method: request.method, -// url: request.url, -// headers: request.headers, -// body: request.body.into_vec(), -// }, -// ic_http_certification::HttpResponse { -// status_code: http_response.status_code, -// headers: http_response.headers, -// body: http_response.body.into_vec(), -// upgrade: http_response.upgrade, -// }, -// canister_id.as_slice(), -// time(env) as u128, -// Duration::from_secs(300).as_nanos(), -// &env.root_key(), -// min_certification_version as u8, -// ) -// .unwrap_or_else(|e| panic!("validation failed: {e}")) -// } - -// let env = env(); -// let canister_id = install_canister(&env, CIVIV_CANISTER_BACKEND_WASM.clone()); - -// // for each asset and certification version, fetch the asset, check the HTTP status code, headers and certificate. - -// for certification_version in 1..=2 { -// let request = HttpRequest { -// method: "GET".to_string(), -// url: "/".to_string(), -// headers: vec![], -// body: ByteBuf::new(), -// certificate_version: Some(certification_version), -// }; -// let http_response = http_request(&env, canister_id, &request)?; -// println!("{:?}", http_response); -// // assert_eq!(http_response.status_code, 200); - -// let _result = verify_response_certification( -// &env, -// canister_id, -// request, -// http_response, -// certification_version, -// ); -// // assert_eq!(result.verification_version, certification_version); -// } - -// Ok(()) -// } - - ic_cdk::export_candid!(); - // Helper functions -fn construct_adult_credential () -> StoredCredential { +fn construct_adult_credential() -> StoredCredential { let mut claim_map = HashMap::::new(); claim_map.insert("Is over 18".to_string(), ClaimValue::Boolean(true)); - StoredCredential { + StoredCredential { id: "http://example.edu/credentials/3732".to_string(), - type_: vec!["VerifiableCredential".to_string(), "VerifiedAdult".to_string()], + type_: vec![ + "VerifiableCredential".to_string(), + "VerifiedAdult".to_string(), + ], context: vec![ "https://www.w3.org/2018/credentials/v1".to_string(), "https://www.w3.org/2018/credentials/examples/v1".to_string(), ], issuer: "https://civic.com".to_string(), - claim: vec![Claim{claims: claim_map}], + claim: vec![Claim { claims: claim_map }], } } - // ================================================== - -use std::collections::BTreeMap; use identity_credential::credential::{CredentialBuilder, Subject}; use serde::Serialize; pub use serde_json::Value; +use std::collections::BTreeMap; // use candid::CandidType; use identity_core::common::Url; use std::iter::repeat; @@ -758,7 +709,7 @@ pub enum ClaimValue { #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] pub struct Claim { - pub claims:HashMap, + pub claims: HashMap, } impl From for Value { @@ -770,24 +721,25 @@ impl From for Value { ClaimValue::Number(n) => Value::Number(n.into()), ClaimValue::Claim(nested_claim) => { serde_json::to_value(nested_claim).unwrap_or(Value::Null) - }, + } } } } - impl Claim { pub fn into(self) -> Subject { - let btree_map: BTreeMap = self.claims.into_iter() - .map(|(k, v)| (k, v.into())) - .collect(); - Subject::with_properties(btree_map) + let btree_map: BTreeMap = self + .claims + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); + Subject::with_properties(btree_map) } } #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] pub struct StoredCredential { - pub id: String, + pub id: String, pub type_: Vec, pub context: Vec, pub issuer: String, @@ -799,32 +751,27 @@ pub enum CredentialError { UnauthorizedSubject(String), } - -// Helper functions for constructing the credential that is returned from the canister +// Helper functions for constructing the credential that is returned from the canister /// Build a credentialSubject { -/// id: SubjectId, +/// id: SubjectId, /// otherData /// } pub fn build_claims_into_credential_subjects(claims: Vec, subject: String) -> Vec { - claims.into_iter().zip(repeat(subject)).map(|(c, id )|{ - let mut sub = c.into(); - sub.id = Url::parse(id).ok(); - sub - }).collect() + claims + .into_iter() + .zip(repeat(subject)) + .map(|(c, id)| { + let mut sub = c.into(); + sub.id = Url::parse(id).ok(); + sub + }) + .collect() } - pub fn add_context(mut credential: CredentialBuilder, context: Vec) -> CredentialBuilder { for c in context { - credential = credential.context(Url::parse(c).unwrap()); + credential = credential.context(Url::parse(c).unwrap()); } credential } - -// pub fn add_types(mut credential: CredentialBuilder, types: Vec) -> CredentialBuilder { -// for t in types { -// credential = credential.type_(t); -// } -// credential -// } \ No newline at end of file