Skip to content

Commit

Permalink
Add configuration options for upcoming dynamic captcha feature (#2610)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
frederikrothenberger and github-actions[bot] committed Sep 13, 2024
1 parent 19491ae commit 43f4a85
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 29 deletions.
32 changes: 30 additions & 2 deletions src/frontend/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)];
Expand Down
13 changes: 12 additions & 1 deletion src/frontend/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 29 additions & 3 deletions src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
6 changes: 3 additions & 3 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
})
}

Expand Down Expand Up @@ -409,9 +409,9 @@ fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
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;
})
}
}
Expand Down
13 changes: 8 additions & 5 deletions src/internet_identity/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -96,8 +99,8 @@ pub struct PersistentState {
pub domain_active_anchor_stats: ActivityStats<DomainActiveAnchorCounter>,
// Daily and monthly active authentication methods on the II domains.
pub active_authn_method_stats: ActivityStats<AuthnMethodCounter>,
// 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.
Expand All @@ -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,
Expand Down
32 changes: 23 additions & 9 deletions src/internet_identity/src/storage/storable_persistent_state.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -26,12 +26,15 @@ pub struct StorablePersistentState {
latest_delegation_origins: HashMap<FrontendHostname, Timestamp>,
// 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<u64>,
// opt of backwards compatibility
event_aggregations_count: Option<u64>,
event_stats_24h_start: Option<EventKey>,

captcha_config: Option<CaptchaConfig>,
}

impl Storable for StorablePersistentState {
Expand Down Expand Up @@ -65,10 +68,12 @@ impl From<PersistentState> 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),
}
}
}
Expand All @@ -82,7 +87,7 @@ impl From<StorablePersistentState> 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,
Expand All @@ -93,7 +98,9 @@ impl From<StorablePersistentState> 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]
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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()));
Expand Down
2 changes: 1 addition & 1 deletion src/internet_identity/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 23 additions & 1 deletion src/internet_identity_interface/src/internet_identity/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub struct InternetIdentityInit {
pub archive_config: Option<ArchiveConfig>,
pub canister_creation_cycles_cost: Option<u64>,
pub register_rate_limit: Option<RateLimitConfig>,
pub max_inflight_captchas: Option<u64>,
pub captcha_config: Option<CaptchaConfig>,
}

#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)]
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 43f4a85

Please sign in to comment.