diff --git a/Cargo.lock b/Cargo.lock index b1e0d8adc6..7d4ed2ccf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,7 @@ dependencies = [ "ic-cdk", "ic-representation-independent-hash", "ic-test-state-machine-client", + "identity_jose", "internet_identity_interface", "lazy_static", "regex", @@ -340,6 +341,7 @@ dependencies = [ "serde_bytes", "serde_cbor", "sha2 0.10.8", + "vc_util", ] [[package]] @@ -2042,6 +2044,9 @@ dependencies = [ "ic-response-verification", "ic-stable-structures", "ic-test-state-machine-client", + "identity_core", + "identity_credential", + "identity_jose", "include_dir", "internet_identity_interface", "lazy_static", @@ -2053,7 +2058,9 @@ dependencies = [ "serde", "serde_bytes", "serde_cbor", + "serde_json", "sha2 0.10.8", + "vc_util", ] [[package]] diff --git a/src/canister_tests/Cargo.toml b/src/canister_tests/Cargo.toml index 22b5355dd0..21853c0241 100644 --- a/src/canister_tests/Cargo.toml +++ b/src/canister_tests/Cargo.toml @@ -16,6 +16,8 @@ serde_bytes = "0.11" sha2 = "0.10" internet_identity_interface = { path = "../internet_identity_interface" } +vc_util = { path = "../vc_util"} +identity_jose = { git = "https://github.com/frederikrothenberger/identity.rs.git", branch = "frederik/wasm-test", default-features = false, features = ["iccs"]} # All IC deps candid = "0.9" diff --git a/src/canister_tests/src/api/internet_identity.rs b/src/canister_tests/src/api/internet_identity.rs index 507a46dbcd..d955245aa8 100644 --- a/src/canister_tests/src/api/internet_identity.rs +++ b/src/canister_tests/src/api/internet_identity.rs @@ -9,6 +9,9 @@ use internet_identity_interface::internet_identity::types; /// The experimental v2 API pub mod api_v2; +// API of verifiable credentials MVP. +pub mod vc_mvp; + /** The functions here are derived (manually) from Internet Identity's Candid file */ /// A fake "health check" method that just checks the canister is alive a well. diff --git a/src/canister_tests/src/api/internet_identity/vc_mvp.rs b/src/canister_tests/src/api/internet_identity/vc_mvp.rs new file mode 100644 index 0000000000..87877a0837 --- /dev/null +++ b/src/canister_tests/src/api/internet_identity/vc_mvp.rs @@ -0,0 +1,38 @@ +use candid::Principal; +use ic_cdk::api::management_canister::main::CanisterId; +use ic_test_state_machine_client::{call_candid_as, query_candid_as, CallError, StateMachine}; +use internet_identity_interface::internet_identity::types::vc_mvp::{ + GetIdAliasRequest, GetIdAliasResponse, PrepareIdAliasRequest, PrepareIdAliasResponse, +}; + +pub fn prepare_id_alias( + env: &StateMachine, + canister_id: CanisterId, + sender: Principal, + prepare_id_alias_req: PrepareIdAliasRequest, +) -> Result, CallError> { + call_candid_as( + env, + canister_id, + sender, + "prepare_id_alias", + (prepare_id_alias_req,), + ) + .map(|(x,)| x) +} + +pub fn get_id_alias( + env: &StateMachine, + canister_id: CanisterId, + sender: Principal, + get_id_alias_req: GetIdAliasRequest, +) -> Result, CallError> { + query_candid_as( + env, + canister_id, + sender, + "get_id_alias", + (get_id_alias_req,), + ) + .map(|(x,)| x) +} diff --git a/src/canister_tests/src/framework.rs b/src/canister_tests/src/framework.rs index 0b7aeeb753..ce782ab758 100644 --- a/src/canister_tests/src/framework.rs +++ b/src/canister_tests/src/framework.rs @@ -6,8 +6,10 @@ use flate2::{Compression, GzBuilder}; use ic_cdk::api::management_canister::main::CanisterId; use ic_representation_independent_hash::Value; use ic_test_state_machine_client::{CallError, ErrorCode, StateMachine}; +use identity_jose::jws::Decoder; use internet_identity_interface::archive::types::*; use internet_identity_interface::http_gateway::{HeaderField, HttpRequest}; +use internet_identity_interface::internet_identity::types::vc_mvp::SignedIdAlias; use internet_identity_interface::internet_identity::types::*; use lazy_static::lazy_static; use regex::Regex; @@ -553,6 +555,32 @@ pub fn verify_delegation( .expect("delegation signature invalid"); } +pub fn verify_id_alias_credential_via_env( + env: &StateMachine, + canister_sig_pk_der: CanisterSigPublicKeyDer, + signed_id_alias: &SignedIdAlias, + root_key: &[u8], +) { + const DOMAIN_SEPARATOR: &[u8] = b"iccs_verifiable_credential"; + + let decoder: Decoder = Decoder::new(); + let jws = decoder + .decode_compact_serialization(signed_id_alias.credential_jws.as_bytes(), None) + .expect("Failure decoding JWS credential"); + let sig = jws.decoded_signature(); + let mut msg: Vec = Vec::from([(DOMAIN_SEPARATOR.len() as u8)]); + msg.extend_from_slice(DOMAIN_SEPARATOR); + msg.extend_from_slice(jws.signing_input()); + + env.verify_canister_signature( + msg.to_vec(), + sig.to_vec(), + canister_sig_pk_der.into_vec(), + root_key.to_vec(), + ) + .expect("id_alias signature invalid"); +} + pub fn deploy_archive_via_ii(env: &StateMachine, ii_canister: CanisterId) -> CanisterId { match api::internet_identity::deploy_archive(env, ii_canister, &ARCHIVE_WASM) { Ok(DeployArchiveResult::Success(archive_principal)) => archive_principal, diff --git a/src/frontend/generated/internet_identity_idl.js b/src/frontend/generated/internet_identity_idl.js index 9c27fe1cea..f63412a68e 100644 --- a/src/frontend/generated/internet_identity_idl.js +++ b/src/frontend/generated/internet_identity_idl.js @@ -153,6 +153,27 @@ export const idlFactory = ({ IDL }) => { 'no_such_delegation' : IDL.Null, 'signed_delegation' : SignedDelegation, }); + const GetIdAliasRequest = IDL.Record({ + 'rp_id_alias_jwt' : IDL.Text, + 'issuer' : FrontendHostname, + 'issuer_id_alias_jwt' : IDL.Text, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, + }); + const SignedIdAlias = IDL.Record({ + 'credential_jws' : IDL.Text, + 'id_alias' : IDL.Principal, + 'id_dapp' : IDL.Principal, + }); + const IdAliasCredentials = IDL.Record({ + 'rp_id_alias_credential' : SignedIdAlias, + 'issuer_id_alias_credential' : SignedIdAlias, + }); + const GetIdAliasResponse = IDL.Variant({ + 'ok' : IdAliasCredentials, + 'authentication_failed' : IDL.Text, + 'no_such_credentials' : IDL.Text, + }); const HeaderField = IDL.Tuple(IDL.Text, IDL.Text); const HttpRequest = IDL.Record({ 'url' : IDL.Text, @@ -195,6 +216,20 @@ export const idlFactory = ({ IDL }) => { const IdentityInfoResponse = IDL.Variant({ 'ok' : IdentityInfo }); const IdentityMetadataReplaceResponse = IDL.Variant({ 'ok' : IDL.Null }); const UserKey = PublicKey; + const PrepareIdAliasRequest = IDL.Record({ + 'issuer' : FrontendHostname, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, + }); + const PreparedIdAlias = IDL.Record({ + 'rp_id_alias_jwt' : IDL.Text, + 'issuer_id_alias_jwt' : IDL.Text, + 'canister_sig_pk_der' : PublicKey, + }); + const PrepareIdAliasResponse = IDL.Variant({ + 'ok' : PreparedIdAlias, + 'authentication_failed' : IDL.Text, + }); const ChallengeResult = IDL.Record({ 'key' : ChallengeKey, 'chars' : IDL.Text, @@ -257,6 +292,11 @@ export const idlFactory = ({ IDL }) => { [GetDelegationResponse], ['query'], ), + 'get_id_alias' : IDL.Func( + [GetIdAliasRequest], + [IDL.Opt(GetIdAliasResponse)], + ['query'], + ), 'get_principal' : IDL.Func( [UserNumber, FrontendHostname], [IDL.Principal], @@ -281,6 +321,11 @@ export const idlFactory = ({ IDL }) => { [UserKey, Timestamp], [], ), + 'prepare_id_alias' : IDL.Func( + [PrepareIdAliasRequest], + [IDL.Opt(PrepareIdAliasResponse)], + [], + ), 'register' : IDL.Func( [DeviceData, ChallengeResult, IDL.Opt(IDL.Principal)], [RegisterResponse], diff --git a/src/frontend/generated/internet_identity_types.d.ts b/src/frontend/generated/internet_identity_types.d.ts index 847e4d63c1..0387254d37 100644 --- a/src/frontend/generated/internet_identity_types.d.ts +++ b/src/frontend/generated/internet_identity_types.d.ts @@ -96,6 +96,16 @@ export interface DeviceWithUsage { export type FrontendHostname = string; export type GetDelegationResponse = { 'no_such_delegation' : null } | { 'signed_delegation' : SignedDelegation }; +export interface GetIdAliasRequest { + 'rp_id_alias_jwt' : string, + 'issuer' : FrontendHostname, + 'issuer_id_alias_jwt' : string, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, +} +export type GetIdAliasResponse = { 'ok' : IdAliasCredentials } | + { 'authentication_failed' : string } | + { 'no_such_credentials' : string }; export type HeaderField = [string, string]; export interface HttpRequest { 'url' : string, @@ -111,6 +121,10 @@ export interface HttpResponse { 'streaming_strategy' : [] | [StreamingStrategy], 'status_code' : number, } +export interface IdAliasCredentials { + 'rp_id_alias_credential' : SignedIdAlias, + 'issuer_id_alias_credential' : SignedIdAlias, +} export interface IdentityAnchorInfo { 'devices' : Array, 'device_registration' : [] | [DeviceRegistrationInfo], @@ -153,6 +167,18 @@ export type MetadataMap = Array< { 'bytes' : Uint8Array | number[] }, ] >; +export interface PrepareIdAliasRequest { + 'issuer' : FrontendHostname, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, +} +export type PrepareIdAliasResponse = { 'ok' : PreparedIdAlias } | + { 'authentication_failed' : string }; +export interface PreparedIdAlias { + 'rp_id_alias_jwt' : string, + 'issuer_id_alias_jwt' : string, + 'canister_sig_pk_der' : PublicKey, +} export type PublicKey = Uint8Array | number[]; export interface PublicKeyAuthn { 'pubkey' : PublicKey } export type Purpose = { 'authentication' : null } | @@ -169,6 +195,11 @@ export interface SignedDelegation { 'signature' : Uint8Array | number[], 'delegation' : Delegation, } +export interface SignedIdAlias { + 'credential_jws' : string, + 'id_alias' : Principal, + 'id_dapp' : Principal, +} export interface StreamingCallbackHttpResponse { 'token' : [] | [Token], 'body' : Uint8Array | number[], @@ -220,6 +251,7 @@ export interface _SERVICE { [UserNumber, FrontendHostname, SessionKey, Timestamp], GetDelegationResponse >, + 'get_id_alias' : ActorMethod<[GetIdAliasRequest], [] | [GetIdAliasResponse]>, 'get_principal' : ActorMethod<[UserNumber, FrontendHostname], Principal>, 'http_request' : ActorMethod<[HttpRequest], HttpResponse>, 'http_request_update' : ActorMethod<[HttpRequest], HttpResponse>, @@ -234,6 +266,10 @@ export interface _SERVICE { [UserNumber, FrontendHostname, SessionKey, [] | [bigint]], [UserKey, Timestamp] >, + 'prepare_id_alias' : ActorMethod< + [PrepareIdAliasRequest], + [] | [PrepareIdAliasResponse] + >, 'register' : ActorMethod< [DeviceData, ChallengeResult, [] | [Principal]], RegisterResponse diff --git a/src/internet_identity/Cargo.toml b/src/internet_identity/Cargo.toml index 2835f8c5ee..92f06e5331 100644 --- a/src/internet_identity/Cargo.toml +++ b/src/internet_identity/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] - -canister_sig_util = { path = "../canister_sig_util" } internet_identity_interface = { path = "../internet_identity_interface" } hex = "0.4" @@ -14,6 +12,7 @@ lazy_static = "1.4" serde = { version = "1", features = ["rc"] } serde_bytes = "0.11" serde_cbor = "0.11" +serde_json = { version = "1.0", default-features = false, features = ["std"] } sha2 = "^0.10" # set bound to match ic-certified-map bound # Captcha deps @@ -34,6 +33,14 @@ ic-certified-map = "0.4" ic-metrics-encoder = "1" ic-stable-structures = "0.5" +# VC deps +canister_sig_util = { path = "../canister_sig_util" } +vc_util = { path = "../vc_util" } +identity_core = { git = "https://github.com/frederikrothenberger/identity.rs.git", branch = "frederik/wasm-test" } +identity_credential = { git = "https://github.com/frederikrothenberger/identity.rs.git", branch = "frederik/wasm-test", default-features = false, features = ["credential"]} +identity_jose = { git = "https://github.com/frederikrothenberger/identity.rs.git", branch = "frederik/wasm-test", default-features = false, features = ["iccs"]} + + [target.'cfg(all(target_arch = "wasm32", target_vendor = "unknown", target_os = "unknown"))'.dependencies] getrandom = { version = "0.2", features = ["custom"] } diff --git a/src/internet_identity/internet_identity.did b/src/internet_identity/internet_identity.did index b929c671ae..9db935b258 100644 --- a/src/internet_identity/internet_identity.did +++ b/src/internet_identity/internet_identity.did @@ -374,6 +374,62 @@ type IdentityMetadataReplaceResponse = variant { ok; }; +type PrepareIdAliasRequest = record { + /// Origin of the issuer in the attribute sharing flow. + issuer : FrontendHostname; + /// Origin of the relying party in the attribute sharing flow. + relying_party : FrontendHostname; + /// Identity for which the IdAlias should be generated. + identity_number : IdentityNumber; +}; + +type PrepareIdAliasResponse = variant { + /// Credentials prepared successfully, can be retrieved via `get_id_alias` + ok : PreparedIdAlias; + /// Caller authentication failed. + authentication_failed : text; +}; + +/// The prepared id alias contains two (still unsigned) credentials in JWT format, +/// certifying the id alias for the issuer resp. the relying party. +type PreparedIdAlias = record { + rp_id_alias_jwt : text; + issuer_id_alias_jwt : text; + canister_sig_pk_der : PublicKey; +}; + +/// The request to retrieve the actual signed id alias credentials. +/// The field values should be equal to the values of corresponding +/// fields from the preceding `PrepareIdAliasRequest` and `PrepareIdAliasResponse`. +type GetIdAliasRequest = record { + rp_id_alias_jwt : text; + issuer : FrontendHostname; + issuer_id_alias_jwt : text; + relying_party : FrontendHostname; + identity_number : IdentityNumber; +}; + +type GetIdAliasResponse = variant { + /// The signed id alias credentials + ok : IdAliasCredentials; + /// Caller authentication failed. + authentication_failed : text; + /// The credential(s) are not available: may be expired or not prepared yet (call prepare_id_alias to prepare). + no_such_credentials : text; +}; + +/// The signed id alias credentials for each involved party. +type IdAliasCredentials = record { + rp_id_alias_credential : SignedIdAlias; + issuer_id_alias_credential : SignedIdAlias; +}; + +type SignedIdAlias = record { + credential_jws : text; + id_alias : principal; + id_dapp : principal; +}; + service : (opt InternetIdentityInit) -> { init_salt: () -> (); create_challenge : () -> (Challenge); @@ -434,4 +490,9 @@ service : (opt InternetIdentityInit) -> { // Removes the authentication method associated with the public key from the identity. // Requires authentication. authn_method_remove: (IdentityNumber, PublicKey) -> (opt AuthnMethodRemoveResponse); + + // Attribute Sharing MVP API + // The methods below are used to generate ID-alias credentials during attribute sharing flow. + prepare_id_alias : (PrepareIdAliasRequest) -> (opt PrepareIdAliasResponse); + get_id_alias : (GetIdAliasRequest) -> (opt GetIdAliasResponse) query; } diff --git a/src/internet_identity/src/delegation.rs b/src/internet_identity/src/delegation.rs index d79de70e97..a7116033ba 100644 --- a/src/internet_identity/src/delegation.rs +++ b/src/internet_identity/src/delegation.rs @@ -175,7 +175,7 @@ fn calculate_seed(anchor_number: AnchorNumber, frontend: &FrontendHostname) -> H hash::hash_bytes(blob) } -fn der_encode_canister_sig_key(seed: Vec) -> Vec { +pub(crate) fn der_encode_canister_sig_key(seed: Vec) -> Vec { let my_canister_id = id(); CanisterSigPublicKey::new(my_canister_id, seed).to_der() } @@ -269,7 +269,7 @@ pub fn prune_expired_signatures() { } } -fn check_frontend_length(frontend: &FrontendHostname) { +pub(crate) fn check_frontend_length(frontend: &FrontendHostname) { const FRONTEND_HOSTNAME_LIMIT: usize = 255; let n = frontend.len(); diff --git a/src/internet_identity/src/main.rs b/src/internet_identity/src/main.rs index 5b028bc72c..b3e2d2a307 100644 --- a/src/internet_identity/src/main.rs +++ b/src/internet_identity/src/main.rs @@ -9,6 +9,9 @@ use ic_cdk::api::{caller, set_certified_data, trap}; use ic_cdk_macros::{init, post_upgrade, pre_upgrade, query, update}; use internet_identity_interface::archive::types::{BufferedEntry, Operation}; use internet_identity_interface::http_gateway::{HttpRequest, HttpResponse}; +use internet_identity_interface::internet_identity::types::vc_mvp::{ + GetIdAliasRequest, GetIdAliasResponse, PrepareIdAliasRequest, PrepareIdAliasResponse, +}; use internet_identity_interface::internet_identity::types::*; use serde_bytes::ByteBuf; use std::collections::HashMap; @@ -26,6 +29,7 @@ mod ii_domain; mod nested_tree; mod state; mod storage; +mod vc_mvp; // Some time helpers const fn secs_to_nanos(secs: u64) -> u64 { @@ -571,6 +575,47 @@ mod v2_api { } } +/// API for the attribute sharing mvp +mod attribute_sharing_mvp { + use super::*; + + #[update] + #[candid_method] + async fn prepare_id_alias(req: PrepareIdAliasRequest) -> Option { + let _maybe_ii_domain = authenticate_and_record_activity(req.identity_number); + let prepared_id_alias = vc_mvp::prepare_id_alias( + req.identity_number, + vc_mvp::InvolvedDapps { + relying_party: req.relying_party.clone(), + issuer: req.issuer.clone(), + }, + ) + .await; + Some(PrepareIdAliasResponse::Ok(prepared_id_alias)) + } + + #[query] + #[candid_method(query)] + fn get_id_alias(req: GetIdAliasRequest) -> Option { + let Ok(_) = check_authentication(req.identity_number) else { + return Some(GetIdAliasResponse::AuthenticationFailed(format!( + "{} could not be authenticated.", + caller() + ))); + }; + let response = vc_mvp::get_id_alias( + req.identity_number, + vc_mvp::InvolvedDapps { + relying_party: req.relying_party, + issuer: req.issuer, + }, + &req.rp_id_alias_jwt, + &req.issuer_id_alias_jwt, + ); + Some(response) + } +} + fn main() {} // Order dependent: do not move above any function annotated with #[candid_method]! diff --git a/src/internet_identity/src/vc_mvp.rs b/src/internet_identity/src/vc_mvp.rs new file mode 100644 index 0000000000..01dc5c7ef6 --- /dev/null +++ b/src/internet_identity/src/vc_mvp.rs @@ -0,0 +1,239 @@ +use crate::assets::CertifiedAssets; +use crate::delegation::check_frontend_length; +use crate::{delegation, hash, state, update_root_hash, LABEL_SIG, MINUTE_NS}; +use candid::Principal; +use canister_sig_util::CanisterSigPublicKey; +use ic_cdk::api::{data_certificate, time}; +use ic_cdk::trap; +use ic_certified_map::{Hash, HashTree}; +use identity_core::common::{Timestamp, Url}; +use identity_core::convert::FromJson; +use identity_credential::credential::{Credential, CredentialBuilder, Subject}; + +use canister_sig_util::signature_map::SignatureMap; +use internet_identity_interface::internet_identity::types::vc_mvp::{ + GetIdAliasResponse, IdAliasCredentials, PreparedIdAlias, SignedIdAlias, +}; +use internet_identity_interface::internet_identity::types::{ + AnchorNumber, FrontendHostname, IdentityNumber, +}; +use serde::Serialize; +use serde_bytes::ByteBuf; +use serde_json::json; +use vc_util::{ + did_for_principal, vc_jwt_to_jws, vc_signing_input, vc_signing_input_hash, AliasTuple, + II_CREDENTIAL_URL_PREFIX, II_ISSUER_URL, +}; + +// The expiration used for signatures. +#[allow(clippy::identity_op)] +const SIGNATURE_EXPIRATION_PERIOD_NS: u64 = 1 * MINUTE_NS; + +// The expiration of id_alias verfiable credentials. +const ID_ALIAS_VC_EXPIRATION_PERIOD_NS: u64 = 15 * MINUTE_NS; +pub struct InvolvedDapps { + pub(crate) relying_party: FrontendHostname, + pub(crate) issuer: FrontendHostname, +} + +pub async fn prepare_id_alias( + identity_number: IdentityNumber, + dapps: InvolvedDapps, +) -> PreparedIdAlias { + state::ensure_salt_set().await; + check_frontend_length(&dapps.relying_party); + check_frontend_length(&dapps.issuer); + + let seed = calculate_id_alias_seed(identity_number, &dapps); + let canister_sig_pk = CanisterSigPublicKey::new(ic_cdk::id(), seed.to_vec()); + let id_alias_principal = Principal::self_authenticating(canister_sig_pk.to_der()); + + let rp_tuple = AliasTuple { + id_alias: id_alias_principal, + id_dapp: delegation::get_principal(identity_number, dapps.relying_party.clone()), + }; + let issuer_tuple = AliasTuple { + id_alias: id_alias_principal, + id_dapp: delegation::get_principal(identity_number, dapps.issuer.clone()), + }; + + let rp_id_alias_jwt = prepare_id_alias_jwt(&rp_tuple); + let issuer_id_alias_jwt = prepare_id_alias_jwt(&issuer_tuple); + + state::signature_map_mut(|sigs| { + add_signature( + sigs, + vc_jwt_signing_input_hash(&rp_id_alias_jwt, &canister_sig_pk), + seed, + ); + add_signature( + sigs, + vc_jwt_signing_input_hash(&issuer_id_alias_jwt, &canister_sig_pk), + seed, + ); + }); + update_root_hash(); + PreparedIdAlias { + canister_sig_pk_der: ByteBuf::from(canister_sig_pk.to_der()), + rp_id_alias_jwt, + issuer_id_alias_jwt, + } +} + +fn vc_jwt_signing_input_hash(credential_jwt: &str, canister_sig_pk: &CanisterSigPublicKey) -> Hash { + let signing_input = + vc_signing_input(credential_jwt, canister_sig_pk).expect("failed getting signing_input"); + vc_signing_input_hash(&signing_input) +} + +pub fn get_id_alias( + identity_number: IdentityNumber, + dapps: InvolvedDapps, + rp_id_alias_jwt: &str, + issuer_id_alias_jwt: &str, +) -> GetIdAliasResponse { + check_frontend_length(&dapps.relying_party); + check_frontend_length(&dapps.issuer); + + state::assets_and_signatures(|cert_assets, sigs| { + let seed = calculate_id_alias_seed(identity_number, &dapps); + let canister_sig_pk = CanisterSigPublicKey::new(ic_cdk::id(), seed.to_vec()); + let id_alias_principal = Principal::self_authenticating(canister_sig_pk.to_der()); + let id_rp = delegation::get_principal(identity_number, dapps.relying_party.clone()); + let id_issuer = delegation::get_principal(identity_number, dapps.issuer.clone()); + + let signing_input = vc_signing_input(rp_id_alias_jwt, &canister_sig_pk) + .expect("failed getting signing_input"); + let msg_hash = vc_signing_input_hash(&signing_input); + let Some(rp_sig) = get_signature(cert_assets, sigs, seed, msg_hash) else { + return GetIdAliasResponse::NoSuchCredentials("rp_sig not found".to_string()); + }; + let rp_jws = vc_jwt_to_jws(rp_id_alias_jwt, &canister_sig_pk, &rp_sig) + .expect("failed constructing JWS"); + + let signing_input = vc_signing_input(issuer_id_alias_jwt, &canister_sig_pk) + .expect("failed getting signing_input"); + let msg_hash = vc_signing_input_hash(&signing_input); + let Some(issuer_sig) = get_signature(cert_assets, sigs, seed, msg_hash) else { + return GetIdAliasResponse::NoSuchCredentials("issuer_sig not found".to_string()); + }; + let issuer_jws = vc_jwt_to_jws(issuer_id_alias_jwt, &canister_sig_pk, &issuer_sig) + .expect("failed constructing JWS"); + + GetIdAliasResponse::Ok(IdAliasCredentials { + rp_id_alias_credential: SignedIdAlias { + id_alias: id_alias_principal, + id_dapp: id_rp, + credential_jws: rp_jws, + }, + issuer_id_alias_credential: SignedIdAlias { + id_alias: id_alias_principal, + id_dapp: id_issuer, + credential_jws: issuer_jws, + }, + }) + }) +} + +fn get_signature( + cert_assets: &CertifiedAssets, + sigs: &SignatureMap, + seed: Hash, + msg_hash: Hash, +) -> Option> { + let certificate = data_certificate().unwrap_or_else(|| { + trap("data certificate is only available in query calls"); + }); + let witness = sigs.witness(hash::hash_bytes(seed), msg_hash)?; + + let witness_hash = witness.reconstruct(); + let root_hash = sigs.root_hash(); + if witness_hash != root_hash { + trap(&format!( + "internal error: signature map computed an invalid hash tree, witness hash is {}, root hash is {}", + hex::encode(witness_hash), + hex::encode(root_hash) + )); + } + + let tree = ic_certified_map::fork( + HashTree::Pruned(cert_assets.root_hash()), + ic_certified_map::labeled(LABEL_SIG, witness), + ); + + #[derive(Serialize)] + struct Sig<'a> { + certificate: ByteBuf, + tree: HashTree<'a>, + } + + let sig = Sig { + certificate: ByteBuf::from(certificate), + tree, + }; + + let mut cbor = serde_cbor::ser::Serializer::new(Vec::new()); + cbor.self_describe().unwrap(); + sig.serialize(&mut cbor).unwrap(); + Some(cbor.into_inner()) +} + +fn add_signature(sigs: &mut SignatureMap, msg_hash: Hash, seed: Hash) { + let expires_at = time().saturating_add(SIGNATURE_EXPIRATION_PERIOD_NS); + sigs.put(hash::hash_bytes(seed), msg_hash, expires_at); +} + +fn calculate_id_alias_seed(identity_number: AnchorNumber, dapps: &InvolvedDapps) -> Hash { + let salt = state::salt(); + + let mut blob: Vec = vec![]; + blob.push(salt.len() as u8); + blob.extend_from_slice(&salt); + + let identity_number_str = identity_number.to_string(); + let identity_number_blob = identity_number_str.bytes(); + blob.push(identity_number_blob.len() as u8); + blob.extend(identity_number_blob); + + blob.push(dapps.relying_party.bytes().len() as u8); + blob.extend(dapps.relying_party.bytes()); + + blob.push(dapps.issuer.bytes().len() as u8); + blob.extend(dapps.issuer.bytes()); + + hash::hash_bytes(blob) +} + +fn id_alias_credential(alias_tuple: &AliasTuple) -> Credential { + let subject: Subject = Subject::from_json_value(json!({ + "id": did_for_principal(alias_tuple.id_dapp), + "has_id_alias": did_for_principal(alias_tuple.id_alias), + })) + .expect("internal: failed building id_alias subject"); + let exp_timestamp_sec = + Timestamp::from_unix(((time() + ID_ALIAS_VC_EXPIRATION_PERIOD_NS) / 1_000_000_000) as i64) + .expect("internal: failed computing expiration timestamp"); + + let credential: Credential = CredentialBuilder::default() + .id(prepare_credential_id()) + .issuer(Url::parse(II_ISSUER_URL).expect("internal: bad issuer url")) + .type_("InternetIdentityIdAlias") + .subject(subject) + .expiration_date(exp_timestamp_sec) + .build() + .expect("internal: failed building id_alias credential"); + credential +} + +fn prepare_credential_id() -> Url { + let url = Url::parse(II_CREDENTIAL_URL_PREFIX).expect("internal: bad credential id base url"); + url.join(time().to_string()) + .expect("internal: bad credential id extension") +} + +fn prepare_id_alias_jwt(alias_tuple: &AliasTuple) -> String { + let credential = id_alias_credential(alias_tuple); + credential + .serialize_jwt() + .expect("internal: JWT serialization failure") +} diff --git a/src/internet_identity/tests/integration/main.rs b/src/internet_identity/tests/integration/main.rs index 8aac2a65c3..f81b0d852b 100644 --- a/src/internet_identity/tests/integration/main.rs +++ b/src/internet_identity/tests/integration/main.rs @@ -14,3 +14,4 @@ mod rollback; mod stable_memory; mod upgrade; mod v2_api; +mod vc_mvp; diff --git a/src/internet_identity/tests/integration/vc_mvp.rs b/src/internet_identity/tests/integration/vc_mvp.rs new file mode 100644 index 0000000000..837e585d14 --- /dev/null +++ b/src/internet_identity/tests/integration/vc_mvp.rs @@ -0,0 +1,797 @@ +//! Tests related to prepare_id_alias and get_id_alias canister calls. +use canister_sig_util::{extract_raw_root_pk_from_der, CanisterSigPublicKey}; +use canister_tests::api::internet_identity as api; +use canister_tests::framework::*; +use canister_tests::{flows, match_value}; +use ic_test_state_machine_client::CallError; +use identity_jose::jwk::JwkType; +use identity_jose::jws::Decoder; +use identity_jose::jwu::encode_b64; +use internet_identity_interface::internet_identity::types::vc_mvp::{ + GetIdAliasRequest, GetIdAliasResponse, PrepareIdAliasRequest, PrepareIdAliasResponse, +}; +use internet_identity_interface::internet_identity::types::FrontendHostname; +use std::ops::Deref; +use vc_util::verify_credential_jws_with_canister_id; + +fn verify_canister_sig_pk(credential_jws: &str, canister_sig_pk_der: &[u8]) { + let decoder: Decoder = Decoder::new(); + let jws = decoder + .decode_compact_serialization(credential_jws.as_bytes(), None) + .expect("Failure decoding JWS credential"); + let jws_header = jws.protected_header().expect("missing JWS header"); + let jwk = jws_header.deref().jwk().expect("missing JWK in JWS header"); + assert_eq!(jwk.alg(), Some("IcCs")); + assert_eq!(jwk.kty(), JwkType::Oct); + let jwk_params = jwk.try_oct_params().expect("missing JWK oct params"); + assert_eq!(jwk_params.k, encode_b64(canister_sig_pk_der)); +} + +// Verifies that a valid id_alias is created. +#[test] +fn should_get_valid_id_alias() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + let prepare_id_alias_req = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + }; + + let prepare_response = + api::vc_mvp::prepare_id_alias(&env, canister_id, principal_1(), prepare_id_alias_req)? + .expect("Got 'None' from prepare_id_alias"); + + let prepared_id_alias = if let PrepareIdAliasResponse::Ok(prepared) = prepare_response { + prepared + } else { + panic!("prepare id_alias failed") + }; + + let get_id_alias_req = GetIdAliasRequest { + identity_number, + relying_party, + issuer, + rp_id_alias_jwt: prepared_id_alias.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias.issuer_id_alias_jwt, + }; + let id_alias_credentials = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + assert_eq!( + id_alias_credentials.rp_id_alias_credential.id_alias, + id_alias_credentials.issuer_id_alias_credential.id_alias + ); + + // Verify that JWS-credentials contain correct canister signing PK. + verify_canister_sig_pk( + &id_alias_credentials.rp_id_alias_credential.credential_jws, + prepared_id_alias.canister_sig_pk_der.as_ref(), + ); + verify_canister_sig_pk( + &id_alias_credentials + .issuer_id_alias_credential + .credential_jws, + prepared_id_alias.canister_sig_pk_der.as_ref(), + ); + + // Verify the credentials in two ways: via env and via external function. + let canister_sig_pk = + CanisterSigPublicKey::try_from(prepared_id_alias.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"); + let root_pk_raw = + extract_raw_root_pk_from_der(&env.root_key()).expect("Failed decoding IC root key."); + verify_id_alias_credential_via_env( + &env, + prepared_id_alias.canister_sig_pk_der.clone(), + &id_alias_credentials.rp_id_alias_credential, + &env.root_key(), + ); + verify_credential_jws_with_canister_id( + &id_alias_credentials.rp_id_alias_credential.credential_jws, + &canister_sig_pk.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_id_alias_credential_via_env( + &env, + prepared_id_alias.canister_sig_pk_der.clone(), + &id_alias_credentials.issuer_id_alias_credential, + &env.root_key(), + ); + verify_credential_jws_with_canister_id( + &id_alias_credentials + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + Ok(()) +} + +#[test] +fn should_get_different_id_alias_for_different_users() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number_1 = flows::register_anchor(&env, canister_id); + let identity_number_2 = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + let prepare_id_alias_req_1 = PrepareIdAliasRequest { + identity_number: identity_number_1, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + }; + let prepare_id_alias_req_2 = PrepareIdAliasRequest { + identity_number: identity_number_2, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + }; + + let (get_id_alias_req_1, canister_sig_pk_1) = { + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req_1, + )? + .expect("Got 'None' from prepare_id_alias"); + if let PrepareIdAliasResponse::Ok(prepared_id_alias_1) = prepare_response { + ( + GetIdAliasRequest { + identity_number: identity_number_1, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + rp_id_alias_jwt: prepared_id_alias_1.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias_1.issuer_id_alias_jwt, + }, + CanisterSigPublicKey::try_from(prepared_id_alias_1.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"), + ) + } else { + panic!("prepare id_alias failed") + } + }; + + let (get_id_alias_req_2, canister_sig_pk_2) = { + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req_2, + )? + .expect("Got 'None' from prepare_id_alias"); + if let PrepareIdAliasResponse::Ok(prepared_id_alias_2) = prepare_response { + ( + GetIdAliasRequest { + identity_number: identity_number_2, + relying_party, + issuer, + rp_id_alias_jwt: prepared_id_alias_2.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias_2.issuer_id_alias_jwt, + }, + CanisterSigPublicKey::try_from(prepared_id_alias_2.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"), + ) + } else { + panic!("prepare id_alias failed") + } + }; + + let id_alias_credentials_1 = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req_1)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + let id_alias_credentials_2 = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req_2)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + assert_eq!( + id_alias_credentials_1.rp_id_alias_credential.id_alias, + id_alias_credentials_1.issuer_id_alias_credential.id_alias + ); + assert_eq!( + id_alias_credentials_2.rp_id_alias_credential.id_alias, + id_alias_credentials_2.issuer_id_alias_credential.id_alias + ); + assert_ne!( + id_alias_credentials_1.rp_id_alias_credential.id_alias, + id_alias_credentials_2.rp_id_alias_credential.id_alias + ); + assert_ne!( + id_alias_credentials_1.issuer_id_alias_credential.id_alias, + id_alias_credentials_2.issuer_id_alias_credential.id_alias + ); + + let root_pk_raw = + extract_raw_root_pk_from_der(&env.root_key()).expect("Failed decoding IC root key."); + verify_credential_jws_with_canister_id( + &id_alias_credentials_1.rp_id_alias_credential.credential_jws, + &canister_sig_pk_1.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_1 + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk_1.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_2.rp_id_alias_credential.credential_jws, + &canister_sig_pk_2.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_2 + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk_2.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + Ok(()) +} + +#[test] +fn should_get_different_id_alias_for_different_relying_parties() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party_1 = FrontendHostname::from("https://some-dapp-1.com"); + let relying_party_2 = FrontendHostname::from("https://some-dapp-2.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + let prepare_id_alias_req_1 = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party_1.clone(), + issuer: issuer.clone(), + }; + let prepare_id_alias_req_2 = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party_2.clone(), + issuer: issuer.clone(), + }; + + let (get_id_alias_req_1, canister_sig_pk_1) = { + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req_1, + )? + .expect("Got 'None' from prepare_id_alias"); + if let PrepareIdAliasResponse::Ok(prepared_id_alias_1) = prepare_response { + ( + GetIdAliasRequest { + identity_number, + relying_party: relying_party_1, + issuer: issuer.clone(), + rp_id_alias_jwt: prepared_id_alias_1.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias_1.issuer_id_alias_jwt, + }, + CanisterSigPublicKey::try_from(prepared_id_alias_1.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"), + ) + } else { + panic!("prepare id_alias failed") + } + }; + + let (get_id_alias_req_2, canister_sig_pk_2) = { + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req_2, + )? + .expect("Got 'None' from prepare_id_alias"); + if let PrepareIdAliasResponse::Ok(prepared_id_alias_2) = prepare_response { + ( + GetIdAliasRequest { + identity_number, + relying_party: relying_party_2, + issuer, + rp_id_alias_jwt: prepared_id_alias_2.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias_2.issuer_id_alias_jwt, + }, + CanisterSigPublicKey::try_from(prepared_id_alias_2.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"), + ) + } else { + panic!("prepare id_alias failed") + } + }; + + let id_alias_credentials_1 = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req_1)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + let id_alias_credentials_2 = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req_2)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + assert_eq!( + id_alias_credentials_1.rp_id_alias_credential.id_alias, + id_alias_credentials_1.issuer_id_alias_credential.id_alias + ); + + assert_eq!( + id_alias_credentials_2.rp_id_alias_credential.id_alias, + id_alias_credentials_2.issuer_id_alias_credential.id_alias + ); + + assert_ne!( + id_alias_credentials_1.rp_id_alias_credential.id_alias, + id_alias_credentials_2.rp_id_alias_credential.id_alias + ); + + assert_ne!( + id_alias_credentials_1.issuer_id_alias_credential.id_alias, + id_alias_credentials_2.issuer_id_alias_credential.id_alias + ); + + let root_pk_raw = + extract_raw_root_pk_from_der(&env.root_key()).expect("Failed decoding IC root key."); + verify_credential_jws_with_canister_id( + &id_alias_credentials_1.rp_id_alias_credential.credential_jws, + &canister_sig_pk_1.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_1 + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk_1.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_2.rp_id_alias_credential.credential_jws, + &canister_sig_pk_2.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_2 + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk_2.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + + Ok(()) +} + +#[test] +fn should_get_different_id_alias_for_different_issuers() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer_1 = FrontendHostname::from("https://some-issuer-1.com"); + let issuer_2 = FrontendHostname::from("https://some-issuer-2.com"); + let prepare_id_alias_req_1 = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer_1.clone(), + }; + let prepare_id_alias_req_2 = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer_2.clone(), + }; + + let (get_id_alias_req_1, canister_sig_pk_1) = { + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req_1, + )? + .expect("Got 'None' from prepare_id_alias"); + if let PrepareIdAliasResponse::Ok(prepared_id_alias_1) = prepare_response { + ( + GetIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer_1, + rp_id_alias_jwt: prepared_id_alias_1.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias_1.issuer_id_alias_jwt, + }, + CanisterSigPublicKey::try_from(prepared_id_alias_1.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"), + ) + } else { + panic!("prepare id_alias failed") + } + }; + + let (get_id_alias_req_2, canister_sig_pk_2) = { + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req_2, + )? + .expect("Got 'None' from prepare_id_alias"); + if let PrepareIdAliasResponse::Ok(prepared_id_alias_2) = prepare_response { + ( + GetIdAliasRequest { + identity_number, + relying_party, + issuer: issuer_2, + rp_id_alias_jwt: prepared_id_alias_2.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias_2.issuer_id_alias_jwt, + }, + CanisterSigPublicKey::try_from(prepared_id_alias_2.canister_sig_pk_der.as_ref()) + .expect("failed parsing canister sig pk"), + ) + } else { + panic!("prepare id_alias failed") + } + }; + + let id_alias_credentials_1 = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req_1)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + let id_alias_credentials_2 = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req_2)? + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + assert_eq!( + id_alias_credentials_1.rp_id_alias_credential.id_alias, + id_alias_credentials_1.issuer_id_alias_credential.id_alias + ); + + assert_eq!( + id_alias_credentials_2.rp_id_alias_credential.id_alias, + id_alias_credentials_2.issuer_id_alias_credential.id_alias + ); + + assert_ne!( + id_alias_credentials_1.rp_id_alias_credential.id_alias, + id_alias_credentials_2.rp_id_alias_credential.id_alias + ); + + assert_ne!( + id_alias_credentials_1.issuer_id_alias_credential.id_alias, + id_alias_credentials_2.issuer_id_alias_credential.id_alias + ); + + let root_pk_raw = + extract_raw_root_pk_from_der(&env.root_key()).expect("Failed decoding IC root key."); + verify_credential_jws_with_canister_id( + &id_alias_credentials_1.rp_id_alias_credential.credential_jws, + &canister_sig_pk_1.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_1 + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk_1.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_2.rp_id_alias_credential.credential_jws, + &canister_sig_pk_2.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + verify_credential_jws_with_canister_id( + &id_alias_credentials_2 + .issuer_id_alias_credential + .credential_jws, + &canister_sig_pk_2.canister_id, + &root_pk_raw, + ) + .expect("external verification failed"); + + Ok(()) +} + +#[test] +#[should_panic(expected = "could not be authenticated")] +fn should_not_prepare_id_alias_for_different_user() { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + + let _ = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_2(), + PrepareIdAliasRequest { + identity_number, // belongs to principal_1 + relying_party, + issuer, + }, + ) + .expect("Got 'None' from prepare_id_alias"); +} + +#[test] +fn should_not_get_id_alias_for_different_user() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + PrepareIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + }, + )? + .expect("Got 'None' from prepare_id_alias"); + + let _canister_sig_key = if let PrepareIdAliasResponse::Ok(key) = prepare_response { + key + } else { + panic!("prepare id_alias failed") + }; + + let response = api::vc_mvp::get_id_alias( + &env, + canister_id, + principal_2(), + GetIdAliasRequest { + identity_number, // belongs to principal_1 + relying_party, + issuer, + rp_id_alias_jwt: "dummy_jwt".to_string(), + issuer_id_alias_jwt: "another_dummy_jwt".to_string(), + }, + )? + .expect("Got 'None' from get_id_alias"); + + if let GetIdAliasResponse::AuthenticationFailed(_err) = response { + Ok(()) + } else { + panic!("Expected a failed authentication, got {:?}", response); + } +} + +#[test] +fn should_not_get_id_alias_if_not_prepared() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + api::init_salt(&env, canister_id)?; + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + + let response = api::vc_mvp::get_id_alias( + &env, + canister_id, + principal_1(), + GetIdAliasRequest { + identity_number, + relying_party, + issuer, + rp_id_alias_jwt: "dummy jwt".to_string(), + issuer_id_alias_jwt: "another dummy jwt".to_string(), + }, + )? + .expect("Got 'None' from get_id_alias"); + + if let GetIdAliasResponse::NoSuchCredentials(_err) = response { + Ok(()) + } else { + panic!("Expected that credentials not found, got {:?}", response); + } +} + +/// Verifies that there is a graceful failure if II gets upgraded between prepare_id_alias +/// and get_id_alias. +#[test] +fn should_not_get_prepared_id_alias_after_ii_upgrade() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + let prepare_id_alias_req = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + }; + + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req.clone(), + )? + .expect("Got 'None' from prepare_id_alias"); + + let prepared_id_alias = if let PrepareIdAliasResponse::Ok(prepared_id_alias) = prepare_response + { + prepared_id_alias + } else { + panic!("prepare id_alias failed") + }; + + // upgrade, even with the same WASM clears non-stable memory + upgrade_ii_canister(&env, canister_id, II_WASM.clone()); + + let get_id_alias_req = GetIdAliasRequest { + identity_number, + relying_party, + issuer, + rp_id_alias_jwt: prepared_id_alias.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias.issuer_id_alias_jwt, + }; + let response = api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req)? + .expect("Got 'None' from get_id_alias"); + assert!(matches!( + response, + GetIdAliasResponse::NoSuchCredentials(_err) + )); + Ok(()) +} + +#[test] +#[should_panic(expected = "id_alias signature invalid")] +fn should_not_validate_id_alias_with_wrong_canister_key() { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let identity_number = flows::register_anchor(&env, canister_id); + let relying_party = FrontendHostname::from("https://some-dapp.com"); + let issuer = FrontendHostname::from("https://some-issuer.com"); + let prepare_id_alias_req = PrepareIdAliasRequest { + identity_number, + relying_party: relying_party.clone(), + issuer: issuer.clone(), + }; + + let prepare_response = api::vc_mvp::prepare_id_alias( + &env, + canister_id, + principal_1(), + prepare_id_alias_req.clone(), + ) + .expect("Result of prepare_id_alias is not Ok") + .expect("Got 'None' from prepare_id_alias"); + + match_value!( + prepare_response, + PrepareIdAliasResponse::Ok(prepared_id_alias) + ); + + let get_id_alias_req = GetIdAliasRequest { + identity_number, + relying_party, + issuer, + rp_id_alias_jwt: prepared_id_alias.rp_id_alias_jwt, + issuer_id_alias_jwt: prepared_id_alias.issuer_id_alias_jwt, + }; + + let id_alias_credentials = + match api::vc_mvp::get_id_alias(&env, canister_id, principal_1(), get_id_alias_req) + .expect("Result of get_id_alias is not Ok") + .expect("Got 'None' from get_id_alias") + { + GetIdAliasResponse::Ok(credentials) => credentials, + GetIdAliasResponse::NoSuchCredentials(err) => { + panic!("{}", format!("failed to get id_alias credentials: {}", err)) + } + GetIdAliasResponse::AuthenticationFailed(err) => { + panic!("{}", format!("failed authentication: {}", err)) + } + }; + + assert_eq!( + id_alias_credentials.rp_id_alias_credential.id_alias, + id_alias_credentials.issuer_id_alias_credential.id_alias + ); + + verify_id_alias_credential_via_env( + &env, + prepared_id_alias.canister_sig_pk_der.clone(), + &id_alias_credentials.rp_id_alias_credential, + &env.root_key(), + ); + verify_id_alias_credential_via_env( + &env, + prepared_id_alias.canister_sig_pk_der.clone(), + &id_alias_credentials.issuer_id_alias_credential, + &env.root_key(), + ); + + let mut bad_canister_sig_key = prepared_id_alias.canister_sig_pk_der.clone(); + let index = prepared_id_alias.canister_sig_pk_der.as_ref().len() - 1; + let last_byte = bad_canister_sig_key.as_ref()[index]; + bad_canister_sig_key.as_mut()[index] = last_byte + 1; + assert_ne!(prepared_id_alias.canister_sig_pk_der, bad_canister_sig_key); + + verify_id_alias_credential_via_env( + &env, + bad_canister_sig_key, + &id_alias_credentials.rp_id_alias_credential, + &env.root_key(), + ); +} diff --git a/src/internet_identity_interface/src/internet_identity/types.rs b/src/internet_identity_interface/src/internet_identity/types.rs index d5d18f778d..7b88926e6f 100644 --- a/src/internet_identity_interface/src/internet_identity/types.rs +++ b/src/internet_identity_interface/src/internet_identity/types.rs @@ -16,7 +16,7 @@ pub type DeviceVerificationCode = String; pub type FailedAttemptsCounter = u8; mod api_v2; -mod vc_mvp; +pub mod vc_mvp; // re-export v2 types without the ::v2 prefix, so that this crate can be restructured once v1 is removed // without breaking clients