Skip to content

Commit

Permalink
Introduce API v2 authn_method_security_settings_replace (#2162)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Frederik Rothenberger and github-actions[bot] authored Dec 22, 2023
1 parent dd90b0f commit a7832cc
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/canister_tests/src/api/internet_identity/api_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result<(), AuthnMethodSecuritySettingsReplaceError>, 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,
Expand Down
13 changes: 13 additions & 0 deletions src/frontend/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 })],
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -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>,
Expand Down
10 changes: 10 additions & 0 deletions src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;});
Expand Down
22 changes: 22 additions & 0 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions src/internet_identity/tests/integration/v2_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit a7832cc

Please sign in to comment.