Skip to content

Commit 43f4a85

Browse files
Frederik Rothenbergergithub-actions[bot]
andauthored
Add configuration options for upcoming dynamic captcha feature (#2610)
* 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>
1 parent 19491ae commit 43f4a85

File tree

11 files changed

+143
-29
lines changed

11 files changed

+143
-29
lines changed

src/frontend/generated/internet_identity_idl.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,29 @@ export const idlFactory = ({ IDL }) => {
77
'module_hash' : IDL.Vec(IDL.Nat8),
88
'entries_fetch_limit' : IDL.Nat16,
99
});
10+
const CaptchaConfig = IDL.Record({
11+
'max_unsolved_captchas' : IDL.Nat64,
12+
'captcha_trigger' : IDL.Variant({
13+
'Dynamic' : IDL.Record({
14+
'reference_rate_sampling_interval_s' : IDL.Nat64,
15+
'threshold_pct' : IDL.Nat16,
16+
'current_rate_sampling_interval_s' : IDL.Nat64,
17+
}),
18+
'Static' : IDL.Variant({
19+
'CaptchaDisabled' : IDL.Null,
20+
'CaptchaEnabled' : IDL.Null,
21+
}),
22+
}),
23+
});
1024
const RateLimitConfig = IDL.Record({
1125
'max_tokens' : IDL.Nat64,
1226
'time_per_token_ns' : IDL.Nat64,
1327
});
1428
const InternetIdentityInit = IDL.Record({
1529
'assigned_user_number_range' : IDL.Opt(IDL.Tuple(IDL.Nat64, IDL.Nat64)),
16-
'max_inflight_captchas' : IDL.Opt(IDL.Nat64),
1730
'archive_config' : IDL.Opt(ArchiveConfig),
1831
'canister_creation_cycles_cost' : IDL.Opt(IDL.Nat64),
32+
'captcha_config' : IDL.Opt(CaptchaConfig),
1933
'register_rate_limit' : IDL.Opt(RateLimitConfig),
2034
});
2135
const UserNumber = IDL.Nat64;
@@ -491,15 +505,29 @@ export const init = ({ IDL }) => {
491505
'module_hash' : IDL.Vec(IDL.Nat8),
492506
'entries_fetch_limit' : IDL.Nat16,
493507
});
508+
const CaptchaConfig = IDL.Record({
509+
'max_unsolved_captchas' : IDL.Nat64,
510+
'captcha_trigger' : IDL.Variant({
511+
'Dynamic' : IDL.Record({
512+
'reference_rate_sampling_interval_s' : IDL.Nat64,
513+
'threshold_pct' : IDL.Nat16,
514+
'current_rate_sampling_interval_s' : IDL.Nat64,
515+
}),
516+
'Static' : IDL.Variant({
517+
'CaptchaDisabled' : IDL.Null,
518+
'CaptchaEnabled' : IDL.Null,
519+
}),
520+
}),
521+
});
494522
const RateLimitConfig = IDL.Record({
495523
'max_tokens' : IDL.Nat64,
496524
'time_per_token_ns' : IDL.Nat64,
497525
});
498526
const InternetIdentityInit = IDL.Record({
499527
'assigned_user_number_range' : IDL.Opt(IDL.Tuple(IDL.Nat64, IDL.Nat64)),
500-
'max_inflight_captchas' : IDL.Opt(IDL.Nat64),
501528
'archive_config' : IDL.Opt(ArchiveConfig),
502529
'canister_creation_cycles_cost' : IDL.Opt(IDL.Nat64),
530+
'captcha_config' : IDL.Opt(CaptchaConfig),
503531
'register_rate_limit' : IDL.Opt(RateLimitConfig),
504532
});
505533
return [IDL.Opt(InternetIdentityInit)];

src/frontend/generated/internet_identity_types.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ export interface BufferedArchiveEntry {
7171
'anchor_number' : UserNumber,
7272
'timestamp' : Timestamp,
7373
}
74+
export interface CaptchaConfig {
75+
'max_unsolved_captchas' : bigint,
76+
'captcha_trigger' : {
77+
'Dynamic' : {
78+
'reference_rate_sampling_interval_s' : bigint,
79+
'threshold_pct' : number,
80+
'current_rate_sampling_interval_s' : bigint,
81+
}
82+
} |
83+
{ 'Static' : { 'CaptchaDisabled' : null } | { 'CaptchaEnabled' : null } },
84+
}
7485
export type CaptchaResult = ChallengeResult;
7586
export interface Challenge {
7687
'png_base64' : string,
@@ -178,9 +189,9 @@ export type IdentityRegisterError = { 'BadCaptcha' : null } |
178189
{ 'InvalidMetadata' : string };
179190
export interface InternetIdentityInit {
180191
'assigned_user_number_range' : [] | [[bigint, bigint]],
181-
'max_inflight_captchas' : [] | [bigint],
182192
'archive_config' : [] | [ArchiveConfig],
183193
'canister_creation_cycles_cost' : [] | [bigint],
194+
'captcha_config' : [] | [CaptchaConfig],
184195
'register_rate_limit' : [] | [RateLimitConfig],
185196
}
186197
export interface InternetIdentityStats {

src/internet_identity/internet_identity.did

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,33 @@ type RateLimitConfig = record {
209209
max_tokens: nat64;
210210
};
211211

212+
// Captcha configuration
213+
// Default:
214+
// - max_unsolved_captchas: 500
215+
// - captcha_trigger: Static, CaptchaEnabled
216+
type CaptchaConfig = record {
217+
// Maximum number of unsolved captchas.
218+
max_unsolved_captchas : nat64;
219+
// Configuration for when captcha protection should kick in.
220+
captcha_trigger: variant {
221+
// Based on the rate of registrations compared to some reference time frame and allowing some leeway.
222+
Dynamic: record {
223+
// Percentage of increased registration rate observed in the current rate sampling interval (compared to
224+
// reference rate) at which II will enable captcha for new registrations.
225+
threshold_pct: nat16;
226+
// Length of the interval in seconds used to sample the current rate of registrations.
227+
current_rate_sampling_interval_s: nat64;
228+
// Length of the interval in seconds used to sample the reference rate of registrations.
229+
reference_rate_sampling_interval_s: nat64;
230+
};
231+
// Statically enable / disable captcha
232+
Static: variant {
233+
CaptchaEnabled;
234+
CaptchaDisabled;
235+
}
236+
};
237+
};
238+
212239
// Init arguments of II which can be supplied on install and upgrade.
213240
// Setting a value to null keeps the previous value.
214241
type InternetIdentityInit = record {
@@ -228,9 +255,8 @@ type InternetIdentityInit = record {
228255
canister_creation_cycles_cost : opt nat64;
229256
// Rate limit for the `register` call.
230257
register_rate_limit : opt RateLimitConfig;
231-
// Maximum number of inflight captchas.
232-
// Default: 500
233-
max_inflight_captchas: opt nat64;
258+
// Configuration of the captcha in the registration flow.
259+
captcha_config: opt CaptchaConfig;
234260
};
235261

236262
type ChallengeKey = text;

src/internet_identity/src/anchor_management/registration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub async fn create_challenge() -> Challenge {
2626

2727
// Error out if there are too many inflight challenges
2828
if inflight_challenges.len()
29-
>= state::persistent_state(|s| s.max_inflight_captchas) as usize
29+
>= state::persistent_state(|s| s.captcha_config.max_unsolved_captchas) as usize
3030
{
3131
trap("too many inflight captchas");
3232
}

src/internet_identity/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ fn config() -> InternetIdentityInit {
344344
archive_config,
345345
canister_creation_cycles_cost: Some(persistent_state.canister_creation_cycles_cost),
346346
register_rate_limit: Some(persistent_state.registration_rate_limit.clone()),
347-
max_inflight_captchas: Some(persistent_state.max_inflight_captchas),
347+
captcha_config: Some(persistent_state.captcha_config.clone()),
348348
})
349349
}
350350

@@ -409,9 +409,9 @@ fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
409409
persistent_state.registration_rate_limit = rate_limit;
410410
})
411411
}
412-
if let Some(limit) = arg.max_inflight_captchas {
412+
if let Some(captcha_config) = arg.captcha_config {
413413
state::persistent_state_mut(|persistent_state| {
414-
persistent_state.max_inflight_captchas = limit;
414+
persistent_state.captcha_config = captcha_config;
415415
})
416416
}
417417
}

src/internet_identity/src/state.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ use std::time::Duration;
2121

2222
mod temp_keys;
2323

24-
/// Default value for max number of inflight captchas.
25-
pub const DEFAULT_MAX_INFLIGHT_CAPTCHAS: u64 = 500;
24+
/// Default captcha config
25+
pub const DEFAULT_CAPTCHA_CONFIG: CaptchaConfig = CaptchaConfig {
26+
max_unsolved_captchas: 500,
27+
captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled),
28+
};
2629

2730
/// Default registration rate limit config.
2831
pub const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = RateLimitConfig {
@@ -96,8 +99,8 @@ pub struct PersistentState {
9699
pub domain_active_anchor_stats: ActivityStats<DomainActiveAnchorCounter>,
97100
// Daily and monthly active authentication methods on the II domains.
98101
pub active_authn_method_stats: ActivityStats<AuthnMethodCounter>,
99-
// Maximum number of inflight captchas
100-
pub max_inflight_captchas: u64,
102+
// Configuration of the captcha challenge during registration flow
103+
pub captcha_config: CaptchaConfig,
101104
// Count of entries in the event_data BTreeMap
102105
// event_data is expected to have a lot of entries, thus counting by iterating over it is not
103106
// an option.
@@ -123,7 +126,7 @@ impl Default for PersistentState {
123126
active_anchor_stats: ActivityStats::new(time),
124127
domain_active_anchor_stats: ActivityStats::new(time),
125128
active_authn_method_stats: ActivityStats::new(time),
126-
max_inflight_captchas: DEFAULT_MAX_INFLIGHT_CAPTCHAS,
129+
captcha_config: DEFAULT_CAPTCHA_CONFIG,
127130
event_data_count: 0,
128131
event_aggregations_count: 0,
129132
event_stats_24h_start: None,

src/internet_identity/src/storage/storable_persistent_state.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::archive::ArchiveState;
2-
use crate::state::PersistentState;
2+
use crate::state::{PersistentState, DEFAULT_CAPTCHA_CONFIG};
33
use crate::stats::activity_stats::activity_counter::active_anchor_counter::ActiveAnchorCounter;
44
use crate::stats::activity_stats::activity_counter::authn_method_counter::AuthnMethodCounter;
55
use crate::stats::activity_stats::activity_counter::domain_active_anchor_counter::DomainActiveAnchorCounter;
@@ -9,7 +9,7 @@ use candid::{CandidType, Deserialize};
99
use ic_stable_structures::storable::Bound;
1010
use ic_stable_structures::Storable;
1111
use internet_identity_interface::internet_identity::types::{
12-
FrontendHostname, RateLimitConfig, Timestamp,
12+
CaptchaConfig, FrontendHostname, RateLimitConfig, Timestamp,
1313
};
1414
use std::borrow::Cow;
1515
use std::collections::HashMap;
@@ -26,12 +26,15 @@ pub struct StorablePersistentState {
2626
latest_delegation_origins: HashMap<FrontendHostname, Timestamp>,
2727
// unused, kept for stable memory compatibility
2828
max_num_latest_delegation_origins: u64,
29+
// unused, kept for stable memory compatibility
2930
max_inflight_captchas: u64,
30-
// opt of backwards compatibility
31+
32+
// opt fields because of backwards compatibility
3133
event_data_count: Option<u64>,
32-
// opt of backwards compatibility
3334
event_aggregations_count: Option<u64>,
3435
event_stats_24h_start: Option<EventKey>,
36+
37+
captcha_config: Option<CaptchaConfig>,
3538
}
3639

3740
impl Storable for StorablePersistentState {
@@ -65,10 +68,12 @@ impl From<PersistentState> for StorablePersistentState {
6568
latest_delegation_origins: Default::default(),
6669
// unused, kept for stable memory compatibility
6770
max_num_latest_delegation_origins: 0,
68-
max_inflight_captchas: s.max_inflight_captchas,
71+
// unused, kept for stable memory compatibility
72+
max_inflight_captchas: 0,
6973
event_data_count: Some(s.event_data_count),
7074
event_aggregations_count: Some(s.event_aggregations_count),
7175
event_stats_24h_start: s.event_stats_24h_start,
76+
captcha_config: Some(s.captcha_config),
7277
}
7378
}
7479
}
@@ -82,7 +87,7 @@ impl From<StorablePersistentState> for PersistentState {
8287
active_anchor_stats: s.active_anchor_stats,
8388
domain_active_anchor_stats: s.domain_active_anchor_stats,
8489
active_authn_method_stats: s.active_authn_method_stats,
85-
max_inflight_captchas: s.max_inflight_captchas,
90+
captcha_config: s.captcha_config.unwrap_or(DEFAULT_CAPTCHA_CONFIG),
8691
event_data_count: s.event_data_count.unwrap_or_default(),
8792
event_aggregations_count: s.event_aggregations_count.unwrap_or_default(),
8893
event_stats_24h_start: s.event_stats_24h_start,
@@ -93,7 +98,9 @@ impl From<StorablePersistentState> for PersistentState {
9398
#[cfg(test)]
9499
mod tests {
95100
use super::*;
96-
use crate::state::DEFAULT_MAX_INFLIGHT_CAPTCHAS;
101+
use internet_identity_interface::internet_identity::types::{
102+
CaptchaTrigger, StaticCaptchaTrigger,
103+
};
97104
use std::time::Duration;
98105

99106
#[test]
@@ -121,10 +128,14 @@ mod tests {
121128
active_authn_method_stats: ActivityStats::new(test_time),
122129
latest_delegation_origins: HashMap::new(),
123130
max_num_latest_delegation_origins: 0,
124-
max_inflight_captchas: DEFAULT_MAX_INFLIGHT_CAPTCHAS,
131+
max_inflight_captchas: 0,
125132
event_data_count: Some(0),
126133
event_aggregations_count: Some(0),
127134
event_stats_24h_start: None,
135+
captcha_config: Some(CaptchaConfig {
136+
max_unsolved_captchas: 500,
137+
captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled),
138+
}),
128139
};
129140

130141
assert_eq!(StorablePersistentState::default(), expected_defaults);
@@ -139,7 +150,10 @@ mod tests {
139150
active_anchor_stats: ActivityStats::new(test_time),
140151
domain_active_anchor_stats: ActivityStats::new(test_time),
141152
active_authn_method_stats: ActivityStats::new(test_time),
142-
max_inflight_captchas: DEFAULT_MAX_INFLIGHT_CAPTCHAS,
153+
captcha_config: CaptchaConfig {
154+
max_unsolved_captchas: 500,
155+
captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled),
156+
},
143157
event_data_count: 0,
144158
event_aggregations_count: 0,
145159
event_stats_24h_start: None,

src/internet_identity/tests/integration/anchor_management/registration/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ fn should_not_allow_expired_captcha() -> Result<(), CallError> {
192192
fn should_limit_captcha_creation() -> Result<(), CallError> {
193193
let env = env();
194194
let init_arg = InternetIdentityInit {
195-
max_inflight_captchas: Some(3),
195+
captcha_config: Some(CaptchaConfig {
196+
max_unsolved_captchas: 3,
197+
captcha_trigger: CaptchaTrigger::Static(StaticCaptchaTrigger::CaptchaEnabled),
198+
}),
196199
..Default::default()
197200
};
198201
let canister_id = install_ii_canister_with_arg(&env, II_WASM.clone(), Some(init_arg));

src/internet_identity/tests/integration/conifg.rs renamed to src/internet_identity/tests/integration/config.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use canister_tests::api::internet_identity as api;
22
use canister_tests::framework::{env, install_ii_canister_with_arg, II_WASM};
33
use internet_identity_interface::internet_identity::types::{
4-
ArchiveConfig, InternetIdentityInit, RateLimitConfig,
4+
ArchiveConfig, CaptchaConfig, CaptchaTrigger, InternetIdentityInit, RateLimitConfig,
55
};
66
use pocket_ic::CallError;
77

@@ -21,7 +21,14 @@ fn should_retain_anchor_on_user_range_change() -> Result<(), CallError> {
2121
time_per_token_ns: 99,
2222
max_tokens: 874,
2323
}),
24-
max_inflight_captchas: Some(456),
24+
captcha_config: Some(CaptchaConfig {
25+
max_unsolved_captchas: 788,
26+
captcha_trigger: CaptchaTrigger::Dynamic {
27+
threshold_pct: 12,
28+
current_rate_sampling_interval_s: 456,
29+
reference_rate_sampling_interval_s: 9999,
30+
},
31+
}),
2532
};
2633

2734
let canister_id = install_ii_canister_with_arg(&env, II_WASM.clone(), Some(config.clone()));

src/internet_identity/tests/integration/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod activity_stats;
88
mod aggregation_stats;
99
mod anchor_management;
1010
mod archive_integration;
11-
mod conifg;
11+
mod config;
1212
mod delegation;
1313
mod http;
1414
mod rollback;

0 commit comments

Comments
 (0)