From a7832ccc9d756152eb47bb375ec4bed1cd85112c Mon Sep 17 00:00:00 2001 From: Frederik Rothenberger Date: Fri, 22 Dec 2023 16:24:43 +0100 Subject: [PATCH] Introduce API v2 authn_method_security_settings_replace (#2162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce API v2 authn_method_security_settings_replace This PR adds an API v2 method to update security relevant settings of an authentication method. By having separated `authn_method_security_settings_replace` from `authn_method_metadata_replace`, we can in the future apply different policies based on the risk of the action. * 🤖 npm run generate auto-update --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../src/api/internet_identity/api_v2.rs | 18 +++ .../generated/internet_identity_idl.js | 13 ++ .../generated/internet_identity_types.d.ts | 8 + src/internet_identity/internet_identity.did | 10 ++ src/internet_identity/src/main.rs | 22 +++ .../v2_api/authn_method_security_settings.rs | 148 ++++++++++++++++++ .../tests/integration/v2_api/mod.rs | 1 + .../src/internet_identity/types/api_v2.rs | 5 + 8 files changed, 225 insertions(+) create mode 100644 src/internet_identity/tests/integration/v2_api/authn_method_security_settings.rs diff --git a/src/canister_tests/src/api/internet_identity/api_v2.rs b/src/canister_tests/src/api/internet_identity/api_v2.rs index 19f4f4963b..e566f1155e 100644 --- a/src/canister_tests/src/api/internet_identity/api_v2.rs +++ b/src/canister_tests/src/api/internet_identity/api_v2.rs @@ -125,6 +125,24 @@ pub fn authn_method_metadata_replace( .map(|(x,)| x) } +pub fn authn_method_security_settings_replace( + env: &StateMachine, + canister_id: CanisterId, + sender: Principal, + identity_number: IdentityNumber, + public_key: &PublicKey, + security_settings: &AuthnMethodSecuritySettings, +) -> Result, CallError> { + call_candid_as( + env, + canister_id, + sender, + "authn_method_security_settings_replace", + (identity_number, public_key, security_settings), + ) + .map(|(x,)| x) +} + pub fn authn_method_remove( env: &StateMachine, canister_id: CanisterId, diff --git a/src/frontend/generated/internet_identity_idl.js b/src/frontend/generated/internet_identity_idl.js index 077d1b18c7..a0c38c6846 100644 --- a/src/frontend/generated/internet_identity_idl.js +++ b/src/frontend/generated/internet_identity_idl.js @@ -118,6 +118,9 @@ export const idlFactory = ({ IDL }) => { 'AuthnMethodNotFound' : IDL.Null, 'InvalidMetadata' : IDL.Text, }); + const AuthnMethodSecuritySettingsReplaceError = IDL.Variant({ + 'AuthnMethodNotFound' : IDL.Null, + }); const ChallengeKey = IDL.Text; const Challenge = IDL.Record({ 'png_base64' : IDL.Text, @@ -333,6 +336,16 @@ export const idlFactory = ({ IDL }) => { [IDL.Variant({ 'Ok' : IDL.Null, 'Err' : AuthnMethodReplaceError })], [], ), + 'authn_method_security_settings_replace' : IDL.Func( + [IdentityNumber, PublicKey, AuthnMethodSecuritySettings], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : AuthnMethodSecuritySettingsReplaceError, + }), + ], + [], + ), 'captcha_create' : IDL.Func( [], [IDL.Variant({ 'Ok' : Challenge, 'Err' : IDL.Null })], diff --git a/src/frontend/generated/internet_identity_types.d.ts b/src/frontend/generated/internet_identity_types.d.ts index 700ca61457..76626f2223 100644 --- a/src/frontend/generated/internet_identity_types.d.ts +++ b/src/frontend/generated/internet_identity_types.d.ts @@ -52,6 +52,9 @@ export interface AuthnMethodSecuritySettings { 'protection' : AuthnMethodProtection, 'purpose' : AuthnMethodPurpose, } +export type AuthnMethodSecuritySettingsReplaceError = { + 'AuthnMethodNotFound' : null + }; export interface BufferedArchiveEntry { 'sequence_number' : bigint, 'entry' : Uint8Array | number[], @@ -284,6 +287,11 @@ export interface _SERVICE { { 'Ok' : null } | { 'Err' : AuthnMethodReplaceError } >, + 'authn_method_security_settings_replace' : ActorMethod< + [IdentityNumber, PublicKey, AuthnMethodSecuritySettings], + { 'Ok' : null } | + { 'Err' : AuthnMethodSecuritySettingsReplaceError } + >, 'captcha_create' : ActorMethod<[], { 'Ok' : Challenge } | { 'Err' : null }>, 'create_challenge' : ActorMethod<[], Challenge>, 'deploy_archive' : ActorMethod<[Uint8Array | number[]], DeployArchiveResult>, diff --git a/src/internet_identity/internet_identity.did b/src/internet_identity/internet_identity.did index b3a2e44b5a..b8978ceba5 100644 --- a/src/internet_identity/internet_identity.did +++ b/src/internet_identity/internet_identity.did @@ -429,6 +429,11 @@ type AuthnMethodMetadataReplaceError = variant { AuthnMethodNotFound; }; +type AuthnMethodSecuritySettingsReplaceError = variant { + /// No authentication method found with the given public key. + AuthnMethodNotFound; +}; + type PrepareIdAliasRequest = record { /// Origin of the issuer in the attribute sharing flow. issuer : FrontendHostname; @@ -556,6 +561,11 @@ service : (opt InternetIdentityInit) -> { // Requires authentication. authn_method_metadata_replace: (IdentityNumber, PublicKey, MetadataMapV2) -> (variant {Ok; Err: AuthnMethodMetadataReplaceError;}); + // Replaces the authentication method security settings. + // The existing security settings will be overwritten. + // Requires authentication. + authn_method_security_settings_replace: (IdentityNumber, PublicKey, AuthnMethodSecuritySettings) -> (variant {Ok; Err: AuthnMethodSecuritySettingsReplaceError;}); + // Removes the authentication method associated with the public key from the identity. // Requires authentication. authn_method_remove: (IdentityNumber, PublicKey) -> (variant {Ok; Err;}); diff --git a/src/internet_identity/src/main.rs b/src/internet_identity/src/main.rs index 1c32753a79..4f78aecefa 100644 --- a/src/internet_identity/src/main.rs +++ b/src/internet_identity/src/main.rs @@ -672,6 +672,28 @@ mod v2_api { Ok(()) } + #[update] + #[candid_method] + fn authn_method_security_settings_replace( + identity_number: IdentityNumber, + authn_method_pk: PublicKey, + new_security_settings: AuthnMethodSecuritySettings, + ) -> Result<(), AuthnMethodSecuritySettingsReplaceError> { + let anchor_info = get_anchor_info(identity_number); + let Some(mut device) = anchor_info + .into_device_data() + .into_iter() + .find(|d| d.pubkey == authn_method_pk) + else { + return Err(AuthnMethodSecuritySettingsReplaceError::AuthnMethodNotFound); + }; + + device.protection = DeviceProtection::from(new_security_settings.protection); + device.purpose = Purpose::from(new_security_settings.purpose); + update(identity_number, authn_method_pk, device); + Ok(()) + } + #[update] #[candid_method] fn identity_metadata_replace( diff --git a/src/internet_identity/tests/integration/v2_api/authn_method_security_settings.rs b/src/internet_identity/tests/integration/v2_api/authn_method_security_settings.rs new file mode 100644 index 0000000000..51f45de44a --- /dev/null +++ b/src/internet_identity/tests/integration/v2_api/authn_method_security_settings.rs @@ -0,0 +1,148 @@ +use crate::v2_api::authn_method_test_helpers::{ + create_identity_with_authn_method, test_authn_method, +}; +use candid::Principal; +use canister_tests::api::internet_identity::api_v2; +use canister_tests::framework::{ + env, expect_user_error_with_message, install_ii_canister, II_WASM, +}; +use ic_test_state_machine_client::CallError; +use ic_test_state_machine_client::ErrorCode::CanisterCalledTrap; +use internet_identity_interface::internet_identity::types::{ + AuthnMethodData, AuthnMethodProtection, AuthnMethodPurpose, AuthnMethodSecuritySettings, + AuthnMethodSecuritySettingsReplaceError, MetadataEntryV2, +}; +use regex::Regex; +use serde_bytes::ByteBuf; +use std::collections::HashMap; + +#[test] +fn should_replace_authn_method_security_settings() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let authn_method = recovery_phrase_authn_method(); + + let identity_number = create_identity_with_authn_method(&env, canister_id, &authn_method); + + let identity_info = + api_v2::identity_info(&env, canister_id, authn_method.principal(), identity_number)? + .expect("identity info failed"); + let actual_security_settings = &identity_info + .authn_methods + .first() + .expect("expect authn_methods not to be empty") + .security_settings; + assert_eq!(actual_security_settings, &authn_method.security_settings); + + let new_security_settings = AuthnMethodSecuritySettings { + protection: AuthnMethodProtection::Protected, + purpose: AuthnMethodPurpose::Recovery, + }; + api_v2::authn_method_security_settings_replace( + &env, + canister_id, + authn_method.principal(), + identity_number, + &authn_method.public_key(), + &new_security_settings, + )? + .expect("security settings replace failed"); + + let identity_info = + api_v2::identity_info(&env, canister_id, authn_method.principal(), identity_number)? + .expect("identity info failed"); + let actual_security_settings = &identity_info + .authn_methods + .first() + .expect("expect authn_methods not to be empty") + .security_settings; + assert_eq!(actual_security_settings, &new_security_settings); + Ok(()) +} + +#[test] +fn should_require_authentication_to_replace_security_settings() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let authn_method = recovery_phrase_authn_method(); + + let identity_number = create_identity_with_authn_method(&env, canister_id, &authn_method); + + let identity_info = + api_v2::identity_info(&env, canister_id, authn_method.principal(), identity_number)? + .expect("identity info failed"); + let actual_security_settings = &identity_info + .authn_methods + .first() + .expect("expect authn_methods not to be empty") + .security_settings; + assert_eq!(actual_security_settings, &authn_method.security_settings); + + let new_security_settings = AuthnMethodSecuritySettings { + protection: AuthnMethodProtection::Protected, + purpose: AuthnMethodPurpose::Recovery, + }; + let result = api_v2::authn_method_security_settings_replace( + &env, + canister_id, + Principal::anonymous(), + identity_number, + &authn_method.public_key(), + &new_security_settings, + ); + + expect_user_error_with_message( + result, + CanisterCalledTrap, + Regex::new("[a-z\\d-]+ could not be authenticated.").unwrap(), + ); + Ok(()) +} + +#[test] +fn should_check_authn_method_exists() -> Result<(), CallError> { + let env = env(); + let canister_id = install_ii_canister(&env, II_WASM.clone()); + let authn_method = recovery_phrase_authn_method(); + + let identity_number = create_identity_with_authn_method(&env, canister_id, &authn_method); + + let identity_info = + api_v2::identity_info(&env, canister_id, authn_method.principal(), identity_number)? + .expect("identity info failed"); + let actual_security_settings = &identity_info + .authn_methods + .first() + .expect("expect authn_methods not to be empty") + .security_settings; + assert_eq!(actual_security_settings, &authn_method.security_settings); + + let new_security_settings = AuthnMethodSecuritySettings { + protection: AuthnMethodProtection::Protected, + purpose: AuthnMethodPurpose::Recovery, + }; + let result = api_v2::authn_method_security_settings_replace( + &env, + canister_id, + authn_method.principal(), + identity_number, + &ByteBuf::from(vec![1, 2, 3, 4]), + &new_security_settings, + )?; + + assert!(matches!( + result, + Err(AuthnMethodSecuritySettingsReplaceError::AuthnMethodNotFound) + )); + Ok(()) +} + +fn recovery_phrase_authn_method() -> AuthnMethodData { + AuthnMethodData { + metadata: HashMap::from([( + "usage".to_string(), + MetadataEntryV2::String("recovery_phrase".to_string()), + )]), + ..test_authn_method() + } +} diff --git a/src/internet_identity/tests/integration/v2_api/mod.rs b/src/internet_identity/tests/integration/v2_api/mod.rs index 18cc05fc9c..2893e90083 100644 --- a/src/internet_identity/tests/integration/v2_api/mod.rs +++ b/src/internet_identity/tests/integration/v2_api/mod.rs @@ -2,6 +2,7 @@ mod authn_method_add; mod authn_method_metadata; mod authn_method_remove; mod authn_method_replace; +mod authn_method_security_settings; pub mod authn_method_test_helpers; mod identity_authn_info; mod identity_info; diff --git a/src/internet_identity_interface/src/internet_identity/types/api_v2.rs b/src/internet_identity_interface/src/internet_identity/types/api_v2.rs index 824d0cfda5..70d7e3484b 100644 --- a/src/internet_identity_interface/src/internet_identity/types/api_v2.rs +++ b/src/internet_identity_interface/src/internet_identity/types/api_v2.rs @@ -104,6 +104,11 @@ pub enum AuthnMethodMetadataReplaceError { AuthnMethodNotFound, } +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +pub enum AuthnMethodSecuritySettingsReplaceError { + AuthnMethodNotFound, +} + #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub struct RegistrationModeInfo { pub expiration: Timestamp,