From 43f4a856d43484e8a1053eddac7bbe9f904383e4 Mon Sep 17 00:00:00 2001 From: Frederik Rothenberger Date: Fri, 13 Sep 2024 17:06:55 +0200 Subject: [PATCH] Add configuration options for upcoming dynamic captcha feature (#2610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add configuration options for upcoming dynamic captcha feature This PR adds the required configuration options to make the captcha (on registration) dynamic: once implemented, if the dynamic config is enabled, the captcha will only be required on unusually high rate of new registrations. * 🤖 npm run generate auto-update * Clarify default --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../generated/internet_identity_idl.js | 32 +++++++++++++++++-- .../generated/internet_identity_types.d.ts | 13 +++++++- src/internet_identity/internet_identity.did | 32 +++++++++++++++++-- .../src/anchor_management/registration.rs | 2 +- src/internet_identity/src/main.rs | 6 ++-- src/internet_identity/src/state.rs | 13 +++++--- .../src/storage/storable_persistent_state.rs | 32 +++++++++++++------ .../anchor_management/registration/mod.rs | 5 ++- .../integration/{conifg.rs => config.rs} | 11 +++++-- .../tests/integration/main.rs | 2 +- .../src/internet_identity/types.rs | 24 +++++++++++++- 11 files changed, 143 insertions(+), 29 deletions(-) rename src/internet_identity/tests/integration/{conifg.rs => config.rs} (70%) diff --git a/src/frontend/generated/internet_identity_idl.js b/src/frontend/generated/internet_identity_idl.js index 3a1cf65544..baa1177d0f 100644 --- a/src/frontend/generated/internet_identity_idl.js +++ b/src/frontend/generated/internet_identity_idl.js @@ -7,15 +7,29 @@ export const idlFactory = ({ IDL }) => { 'module_hash' : IDL.Vec(IDL.Nat8), 'entries_fetch_limit' : IDL.Nat16, }); + const CaptchaConfig = IDL.Record({ + 'max_unsolved_captchas' : IDL.Nat64, + 'captcha_trigger' : IDL.Variant({ + 'Dynamic' : IDL.Record({ + 'reference_rate_sampling_interval_s' : IDL.Nat64, + 'threshold_pct' : IDL.Nat16, + 'current_rate_sampling_interval_s' : IDL.Nat64, + }), + 'Static' : IDL.Variant({ + 'CaptchaDisabled' : IDL.Null, + 'CaptchaEnabled' : IDL.Null, + }), + }), + }); const RateLimitConfig = IDL.Record({ 'max_tokens' : IDL.Nat64, 'time_per_token_ns' : IDL.Nat64, }); const InternetIdentityInit = IDL.Record({ 'assigned_user_number_range' : IDL.Opt(IDL.Tuple(IDL.Nat64, IDL.Nat64)), - 'max_inflight_captchas' : IDL.Opt(IDL.Nat64), 'archive_config' : IDL.Opt(ArchiveConfig), 'canister_creation_cycles_cost' : IDL.Opt(IDL.Nat64), + 'captcha_config' : IDL.Opt(CaptchaConfig), 'register_rate_limit' : IDL.Opt(RateLimitConfig), }); const UserNumber = IDL.Nat64; @@ -491,15 +505,29 @@ export const init = ({ IDL }) => { 'module_hash' : IDL.Vec(IDL.Nat8), 'entries_fetch_limit' : IDL.Nat16, }); + const CaptchaConfig = IDL.Record({ + 'max_unsolved_captchas' : IDL.Nat64, + 'captcha_trigger' : IDL.Variant({ + 'Dynamic' : IDL.Record({ + 'reference_rate_sampling_interval_s' : IDL.Nat64, + 'threshold_pct' : IDL.Nat16, + 'current_rate_sampling_interval_s' : IDL.Nat64, + }), + 'Static' : IDL.Variant({ + 'CaptchaDisabled' : IDL.Null, + 'CaptchaEnabled' : IDL.Null, + }), + }), + }); const RateLimitConfig = IDL.Record({ 'max_tokens' : IDL.Nat64, 'time_per_token_ns' : IDL.Nat64, }); const InternetIdentityInit = IDL.Record({ 'assigned_user_number_range' : IDL.Opt(IDL.Tuple(IDL.Nat64, IDL.Nat64)), - 'max_inflight_captchas' : IDL.Opt(IDL.Nat64), 'archive_config' : IDL.Opt(ArchiveConfig), 'canister_creation_cycles_cost' : IDL.Opt(IDL.Nat64), + 'captcha_config' : IDL.Opt(CaptchaConfig), 'register_rate_limit' : IDL.Opt(RateLimitConfig), }); return [IDL.Opt(InternetIdentityInit)]; diff --git a/src/frontend/generated/internet_identity_types.d.ts b/src/frontend/generated/internet_identity_types.d.ts index e4d481c7aa..1dc8ea53d6 100644 --- a/src/frontend/generated/internet_identity_types.d.ts +++ b/src/frontend/generated/internet_identity_types.d.ts @@ -71,6 +71,17 @@ export interface BufferedArchiveEntry { 'anchor_number' : UserNumber, 'timestamp' : Timestamp, } +export interface CaptchaConfig { + 'max_unsolved_captchas' : bigint, + 'captcha_trigger' : { + 'Dynamic' : { + 'reference_rate_sampling_interval_s' : bigint, + 'threshold_pct' : number, + 'current_rate_sampling_interval_s' : bigint, + } + } | + { 'Static' : { 'CaptchaDisabled' : null } | { 'CaptchaEnabled' : null } }, +} export type CaptchaResult = ChallengeResult; export interface Challenge { 'png_base64' : string, @@ -178,9 +189,9 @@ export type IdentityRegisterError = { 'BadCaptcha' : null } | { 'InvalidMetadata' : string }; export interface InternetIdentityInit { 'assigned_user_number_range' : [] | [[bigint, bigint]], - 'max_inflight_captchas' : [] | [bigint], 'archive_config' : [] | [ArchiveConfig], 'canister_creation_cycles_cost' : [] | [bigint], + 'captcha_config' : [] | [CaptchaConfig], 'register_rate_limit' : [] | [RateLimitConfig], } export interface InternetIdentityStats { diff --git a/src/internet_identity/internet_identity.did b/src/internet_identity/internet_identity.did index 79d71b2e31..43bcf2caf0 100644 --- a/src/internet_identity/internet_identity.did +++ b/src/internet_identity/internet_identity.did @@ -209,6 +209,33 @@ type RateLimitConfig = record { max_tokens: nat64; }; +// Captcha configuration +// Default: +// - max_unsolved_captchas: 500 +// - captcha_trigger: Static, CaptchaEnabled +type CaptchaConfig = record { + // Maximum number of unsolved captchas. + max_unsolved_captchas : nat64; + // Configuration for when captcha protection should kick in. + captcha_trigger: variant { + // Based on the rate of registrations compared to some reference time frame and allowing some leeway. + Dynamic: record { + // Percentage of increased registration rate observed in the current rate sampling interval (compared to + // reference rate) at which II will enable captcha for new registrations. + threshold_pct: nat16; + // Length of the interval in seconds used to sample the current rate of registrations. + current_rate_sampling_interval_s: nat64; + // Length of the interval in seconds used to sample the reference rate of registrations. + reference_rate_sampling_interval_s: nat64; + }; + // Statically enable / disable captcha + Static: variant { + CaptchaEnabled; + CaptchaDisabled; + } + }; +}; + // Init arguments of II which can be supplied on install and upgrade. // Setting a value to null keeps the previous value. type InternetIdentityInit = record { @@ -228,9 +255,8 @@ type InternetIdentityInit = record { canister_creation_cycles_cost : opt nat64; // Rate limit for the `register` call. register_rate_limit : opt RateLimitConfig; - // Maximum number of inflight captchas. - // Default: 500 - max_inflight_captchas: opt nat64; + // Configuration of the captcha in the registration flow. + captcha_config: opt CaptchaConfig; }; type ChallengeKey = text; diff --git a/src/internet_identity/src/anchor_management/registration.rs b/src/internet_identity/src/anchor_management/registration.rs index e49c6f85db..b8cbfe6118 100644 --- a/src/internet_identity/src/anchor_management/registration.rs +++ b/src/internet_identity/src/anchor_management/registration.rs @@ -26,7 +26,7 @@ pub async fn create_challenge() -> Challenge { // Error out if there are too many inflight challenges if inflight_challenges.len() - >= state::persistent_state(|s| s.max_inflight_captchas) as usize + >= state::persistent_state(|s| s.captcha_config.max_unsolved_captchas) as usize { trap("too many inflight captchas"); } diff --git a/src/internet_identity/src/main.rs b/src/internet_identity/src/main.rs index 3ff07a05a0..d32e5d9d56 100644 --- a/src/internet_identity/src/main.rs +++ b/src/internet_identity/src/main.rs @@ -344,7 +344,7 @@ fn config() -> InternetIdentityInit { archive_config, canister_creation_cycles_cost: Some(persistent_state.canister_creation_cycles_cost), register_rate_limit: Some(persistent_state.registration_rate_limit.clone()), - max_inflight_captchas: Some(persistent_state.max_inflight_captchas), + captcha_config: Some(persistent_state.captcha_config.clone()), }) } @@ -409,9 +409,9 @@ fn apply_install_arg(maybe_arg: Option) { persistent_state.registration_rate_limit = rate_limit; }) } - if let Some(limit) = arg.max_inflight_captchas { + if let Some(captcha_config) = arg.captcha_config { state::persistent_state_mut(|persistent_state| { - persistent_state.max_inflight_captchas = limit; + persistent_state.captcha_config = captcha_config; }) } } diff --git a/src/internet_identity/src/state.rs b/src/internet_identity/src/state.rs index b254682795..d803f04286 100644 --- a/src/internet_identity/src/state.rs +++ b/src/internet_identity/src/state.rs @@ -21,8 +21,11 @@ use std::time::Duration; mod temp_keys; -/// Default value for max number of inflight captchas. -pub const DEFAULT_MAX_INFLIGHT_CAPTCHAS: u64 = 500; +/// Default captcha config +pub const DEFAULT_CAPTCHA_CONFIG: CaptchaConfig = CaptchaConfig { + max_unsolved_captchas: 500, + captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled), +}; /// Default registration rate limit config. pub const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = RateLimitConfig { @@ -96,8 +99,8 @@ pub struct PersistentState { pub domain_active_anchor_stats: ActivityStats, // Daily and monthly active authentication methods on the II domains. pub active_authn_method_stats: ActivityStats, - // Maximum number of inflight captchas - pub max_inflight_captchas: u64, + // Configuration of the captcha challenge during registration flow + pub captcha_config: CaptchaConfig, // Count of entries in the event_data BTreeMap // event_data is expected to have a lot of entries, thus counting by iterating over it is not // an option. @@ -123,7 +126,7 @@ impl Default for PersistentState { active_anchor_stats: ActivityStats::new(time), domain_active_anchor_stats: ActivityStats::new(time), active_authn_method_stats: ActivityStats::new(time), - max_inflight_captchas: DEFAULT_MAX_INFLIGHT_CAPTCHAS, + captcha_config: DEFAULT_CAPTCHA_CONFIG, event_data_count: 0, event_aggregations_count: 0, event_stats_24h_start: None, diff --git a/src/internet_identity/src/storage/storable_persistent_state.rs b/src/internet_identity/src/storage/storable_persistent_state.rs index b186729819..93e8bca03b 100644 --- a/src/internet_identity/src/storage/storable_persistent_state.rs +++ b/src/internet_identity/src/storage/storable_persistent_state.rs @@ -1,5 +1,5 @@ use crate::archive::ArchiveState; -use crate::state::PersistentState; +use crate::state::{PersistentState, DEFAULT_CAPTCHA_CONFIG}; use crate::stats::activity_stats::activity_counter::active_anchor_counter::ActiveAnchorCounter; use crate::stats::activity_stats::activity_counter::authn_method_counter::AuthnMethodCounter; use crate::stats::activity_stats::activity_counter::domain_active_anchor_counter::DomainActiveAnchorCounter; @@ -9,7 +9,7 @@ use candid::{CandidType, Deserialize}; use ic_stable_structures::storable::Bound; use ic_stable_structures::Storable; use internet_identity_interface::internet_identity::types::{ - FrontendHostname, RateLimitConfig, Timestamp, + CaptchaConfig, FrontendHostname, RateLimitConfig, Timestamp, }; use std::borrow::Cow; use std::collections::HashMap; @@ -26,12 +26,15 @@ pub struct StorablePersistentState { latest_delegation_origins: HashMap, // unused, kept for stable memory compatibility max_num_latest_delegation_origins: u64, + // unused, kept for stable memory compatibility max_inflight_captchas: u64, - // opt of backwards compatibility + + // opt fields because of backwards compatibility event_data_count: Option, - // opt of backwards compatibility event_aggregations_count: Option, event_stats_24h_start: Option, + + captcha_config: Option, } impl Storable for StorablePersistentState { @@ -65,10 +68,12 @@ impl From for StorablePersistentState { latest_delegation_origins: Default::default(), // unused, kept for stable memory compatibility max_num_latest_delegation_origins: 0, - max_inflight_captchas: s.max_inflight_captchas, + // unused, kept for stable memory compatibility + max_inflight_captchas: 0, event_data_count: Some(s.event_data_count), event_aggregations_count: Some(s.event_aggregations_count), event_stats_24h_start: s.event_stats_24h_start, + captcha_config: Some(s.captcha_config), } } } @@ -82,7 +87,7 @@ impl From for PersistentState { active_anchor_stats: s.active_anchor_stats, domain_active_anchor_stats: s.domain_active_anchor_stats, active_authn_method_stats: s.active_authn_method_stats, - max_inflight_captchas: s.max_inflight_captchas, + captcha_config: s.captcha_config.unwrap_or(DEFAULT_CAPTCHA_CONFIG), event_data_count: s.event_data_count.unwrap_or_default(), event_aggregations_count: s.event_aggregations_count.unwrap_or_default(), event_stats_24h_start: s.event_stats_24h_start, @@ -93,7 +98,9 @@ impl From for PersistentState { #[cfg(test)] mod tests { use super::*; - use crate::state::DEFAULT_MAX_INFLIGHT_CAPTCHAS; + use internet_identity_interface::internet_identity::types::{ + CaptchaTrigger, StaticCaptchaTrigger, + }; use std::time::Duration; #[test] @@ -121,10 +128,14 @@ mod tests { active_authn_method_stats: ActivityStats::new(test_time), latest_delegation_origins: HashMap::new(), max_num_latest_delegation_origins: 0, - max_inflight_captchas: DEFAULT_MAX_INFLIGHT_CAPTCHAS, + max_inflight_captchas: 0, event_data_count: Some(0), event_aggregations_count: Some(0), event_stats_24h_start: None, + captcha_config: Some(CaptchaConfig { + max_unsolved_captchas: 500, + captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled), + }), }; assert_eq!(StorablePersistentState::default(), expected_defaults); @@ -139,7 +150,10 @@ mod tests { active_anchor_stats: ActivityStats::new(test_time), domain_active_anchor_stats: ActivityStats::new(test_time), active_authn_method_stats: ActivityStats::new(test_time), - max_inflight_captchas: DEFAULT_MAX_INFLIGHT_CAPTCHAS, + captcha_config: CaptchaConfig { + max_unsolved_captchas: 500, + captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled), + }, event_data_count: 0, event_aggregations_count: 0, event_stats_24h_start: None, diff --git a/src/internet_identity/tests/integration/anchor_management/registration/mod.rs b/src/internet_identity/tests/integration/anchor_management/registration/mod.rs index 6d0c7281a3..b939e356d7 100644 --- a/src/internet_identity/tests/integration/anchor_management/registration/mod.rs +++ b/src/internet_identity/tests/integration/anchor_management/registration/mod.rs @@ -192,7 +192,10 @@ fn should_not_allow_expired_captcha() -> Result<(), CallError> { fn should_limit_captcha_creation() -> Result<(), CallError> { let env = env(); let init_arg = InternetIdentityInit { - max_inflight_captchas: Some(3), + captcha_config: Some(CaptchaConfig { + max_unsolved_captchas: 3, + captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled), + }), ..Default::default() }; let canister_id = install_ii_canister_with_arg(&env, II_WASM.clone(), Some(init_arg)); diff --git a/src/internet_identity/tests/integration/conifg.rs b/src/internet_identity/tests/integration/config.rs similarity index 70% rename from src/internet_identity/tests/integration/conifg.rs rename to src/internet_identity/tests/integration/config.rs index 7c91245851..4e79933818 100644 --- a/src/internet_identity/tests/integration/conifg.rs +++ b/src/internet_identity/tests/integration/config.rs @@ -1,7 +1,7 @@ use canister_tests::api::internet_identity as api; use canister_tests::framework::{env, install_ii_canister_with_arg, II_WASM}; use internet_identity_interface::internet_identity::types::{ - ArchiveConfig, InternetIdentityInit, RateLimitConfig, + ArchiveConfig, CaptchaConfig, CaptchaTrigger, InternetIdentityInit, RateLimitConfig, }; use pocket_ic::CallError; @@ -21,7 +21,14 @@ fn should_retain_anchor_on_user_range_change() -> Result<(), CallError> { time_per_token_ns: 99, max_tokens: 874, }), - max_inflight_captchas: Some(456), + captcha_config: Some(CaptchaConfig { + max_unsolved_captchas: 788, + captcha_trigger: CaptchaTrigger::Dynamic { + threshold_pct: 12, + current_rate_sampling_interval_s: 456, + reference_rate_sampling_interval_s: 9999, + }, + }), }; let canister_id = install_ii_canister_with_arg(&env, II_WASM.clone(), Some(config.clone())); diff --git a/src/internet_identity/tests/integration/main.rs b/src/internet_identity/tests/integration/main.rs index 4be2f079a6..894b847b31 100644 --- a/src/internet_identity/tests/integration/main.rs +++ b/src/internet_identity/tests/integration/main.rs @@ -8,7 +8,7 @@ mod activity_stats; mod aggregation_stats; mod anchor_management; mod archive_integration; -mod conifg; +mod config; mod delegation; mod http; mod rollback; diff --git a/src/internet_identity_interface/src/internet_identity/types.rs b/src/internet_identity_interface/src/internet_identity/types.rs index 53463816ea..97fec64c80 100644 --- a/src/internet_identity_interface/src/internet_identity/types.rs +++ b/src/internet_identity_interface/src/internet_identity/types.rs @@ -196,7 +196,7 @@ pub struct InternetIdentityInit { pub archive_config: Option, pub canister_creation_cycles_cost: Option, pub register_rate_limit: Option, - pub max_inflight_captchas: Option, + pub captcha_config: Option, } #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] @@ -229,6 +229,28 @@ pub struct RateLimitConfig { pub max_tokens: u64, } +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +pub struct CaptchaConfig { + pub max_unsolved_captchas: u64, + pub captcha_trigger: CaptchaTrigger, +} + +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +pub enum CaptchaTrigger { + Dynamic { + threshold_pct: u16, + current_rate_sampling_interval_s: u64, + reference_rate_sampling_interval_s: u64, + }, + Static(StaticCaptchaTrigger), +} + +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +pub enum StaticCaptchaTrigger { + CaptchaEnabled, + CaptchaDisabled, +} + /// Configuration parameters of the archive to be used on the next deployment. #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub struct ArchiveConfig {