Skip to content

Commit 1897b97

Browse files
authored
fix: No more dangling identity anchors and make data migration work with non-existent anchors (#3502)
* first * un-delete FE file * second * first * regression test * Unit tests for allocate_anchor_safe * clippy * Factor out OpenID validation to prior phase in apply_identity_data * clippy * Never forget to flush!
1 parent 606b484 commit 1897b97

File tree

8 files changed

+468
-91
lines changed

8 files changed

+468
-91
lines changed

src/internet_identity/src/anchor_management.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use crate::archive::{archive_operation, device_diff};
22
use crate::openid::{OpenIdCredential, OpenIdCredentialKey};
33
use crate::storage::anchor::{Anchor, AnchorError, Device};
4+
use crate::storage::Storage;
45
use crate::{state, stats::activity_stats};
56
use candid::Principal;
67
use ic_cdk::api::time;
78
use ic_cdk::{caller, trap};
9+
use ic_stable_structures::Memory;
810
use internet_identity_interface::archive::types::{DeviceDataWithoutAlias, Operation};
911
use internet_identity_interface::internet_identity::types::openid::OpenIdCredentialData;
1012
use internet_identity_interface::internet_identity::types::{
@@ -179,21 +181,46 @@ pub fn identity_properties_replace(
179181
set_name(anchor, properties.name)
180182
}
181183

182-
/// Adds an `OpenIdCredential` to the given anchor and returns the operation to be archived.
183-
/// Returns an error if the `OpenIdCredential` already exists in this or another anchor.
184-
pub fn add_openid_credential(
184+
pub fn check_openid_credential_is_unique<M: Memory + Clone>(
185+
storage: &Storage<M>,
186+
openid_credential_key: &OpenIdCredentialKey,
187+
) -> Result<(), AnchorError> {
188+
if storage
189+
.lookup_anchor_with_openid_credential(openid_credential_key)
190+
.is_some()
191+
{
192+
return Err(AnchorError::OpenIdCredentialAlreadyRegistered);
193+
}
194+
195+
Ok(())
196+
}
197+
198+
/// Similar to `add_openid_credential` but is only safe to call after establishing the
199+
/// `check_openid_credential_is_unique` precondition.
200+
pub fn add_openid_credential_skip_checks(
185201
anchor: &mut Anchor,
186202
openid_credential: OpenIdCredential,
187203
) -> Result<Operation, AnchorError> {
188-
if lookup_anchor_with_openid_credential(&openid_credential.key()).is_some() {
189-
return Err(AnchorError::OpenIdCredentialAlreadyRegistered);
190-
}
191204
anchor.add_openid_credential(openid_credential.clone())?;
205+
192206
Ok(Operation::AddOpenIdCredential {
193207
iss: openid_credential.iss,
194208
})
195209
}
196210

211+
/// Adds an `OpenIdCredential` to the given anchor and returns the operation to be archived.
212+
/// Returns an error if the `OpenIdCredential` already exists in this or another anchor.
213+
pub fn add_openid_credential(
214+
anchor: &mut Anchor,
215+
openid_credential: OpenIdCredential,
216+
) -> Result<Operation, AnchorError> {
217+
storage_borrow(|storage| check_openid_credential_is_unique(storage, &openid_credential.key()))?;
218+
219+
let operation = add_openid_credential_skip_checks(anchor, openid_credential)?;
220+
221+
Ok(operation)
222+
}
223+
197224
/// Removes an `OpenIdCredential` of the given anchor and returns the operation to be archived.
198225
/// Return an error if the `OpenIdCredential` to be removed does not exist.
199226
pub fn remove_openid_credential(
@@ -214,11 +241,6 @@ pub fn update_openid_credential(
214241
anchor.update_openid_credential(openid_credential)
215242
}
216243

217-
/// Lookup `AnchorNumber` for the given `OpenIdCredentialKey`.
218-
pub fn lookup_anchor_with_openid_credential(key: &OpenIdCredentialKey) -> Option<AnchorNumber> {
219-
storage_borrow(|storage| storage.lookup_anchor_with_openid_credential(key))
220-
}
221-
222244
/// Lookup `DeviceKeyWithAnchor` for the given `CredentialId`.
223245
pub fn lookup_device_key_with_credential_id(
224246
credential_id: &CredentialId,

src/internet_identity/src/anchor_management/registration/registration_flow_v2.rs

Lines changed: 176 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,24 @@ use crate::anchor_management::registration::captcha::{
44
use crate::anchor_management::registration::rate_limit::process_rate_limit;
55
use crate::anchor_management::registration::Base64;
66
use crate::anchor_management::{
7-
activity_bookkeeping, add_openid_credential, post_operation_bookkeeping, set_name,
7+
activity_bookkeeping, add_openid_credential_skip_checks, check_openid_credential_is_unique,
8+
post_operation_bookkeeping, set_name,
89
};
910
use crate::state::flow_states::RegistrationFlowState;
10-
use crate::storage::anchor::Device;
11+
use crate::storage::anchor::{Anchor, Device};
12+
use crate::storage::Storage;
1113
use crate::{openid, state};
1214
use candid::Principal;
1315
use ic_cdk::api::time;
1416
use ic_cdk::caller;
17+
use ic_stable_structures::Memory;
1518
use internet_identity_interface::archive::types::{DeviceDataWithoutAlias, Operation};
1619
use internet_identity_interface::internet_identity::types::IdRegFinishError::IdentityLimitReached;
1720
use internet_identity_interface::internet_identity::types::{
1821
AuthorizationKey, CaptchaTrigger, CheckCaptchaArg, CheckCaptchaError, CreateIdentityData,
19-
DeviceData, DeviceWithUsage, IdRegFinishError, IdRegFinishResult, IdRegNextStepResult,
20-
IdRegStartError, IdentityNumber, OpenIDRegFinishArg, RegistrationFlowNextStep,
21-
StaticCaptchaTrigger,
22+
DeviceData, DeviceWithUsage, IdRegFinishArg, IdRegFinishError, IdRegFinishResult,
23+
IdRegNextStepResult, IdRegStartError, IdentityNumber, OpenIDRegFinishArg,
24+
RegistrationFlowNextStep, StaticCaptchaTrigger,
2225
};
2326

2427
impl RegistrationFlowState {
@@ -166,7 +169,9 @@ pub fn identity_registration_finish(
166169
});
167170
};
168171

169-
let identity_number = create_identity(&arg)?;
172+
let now = time();
173+
174+
let identity_number = create_identity(&arg, now)?;
170175

171176
// flow completed --> remove flow state
172177
state::with_flow_states_mut(|flow_states| flow_states.remove_registration_flow(&caller));
@@ -192,14 +197,65 @@ pub fn identity_registration_finish(
192197
Ok(IdRegFinishResult { identity_number })
193198
}
194199

195-
fn create_identity(arg: &CreateIdentityData) -> Result<IdentityNumber, IdRegFinishError> {
196-
let now = time();
197-
let Some(mut identity) = state::storage_borrow_mut(|s| s.allocate_anchor(now)) else {
198-
return Err(IdentityLimitReached);
199-
};
200+
fn create_openid_credential_and_config(
201+
openid_registration_data: &OpenIDRegFinishArg,
202+
) -> Result<(openid::OpenIdCredential, String), IdRegFinishError> {
203+
let OpenIDRegFinishArg { jwt, salt, name: _ } = openid_registration_data;
200204

201-
let operation = match &arg {
202-
CreateIdentityData::PubkeyAuthn(id_reg_finish_arg) => {
205+
let (openid_credential, openid_config_iss) = openid::with_provider(jwt, |provider| {
206+
Ok((provider.verify(jwt, salt)?, provider.issuer()))
207+
})?;
208+
209+
Ok((openid_credential, openid_config_iss))
210+
}
211+
212+
#[derive(Clone, Debug, Eq, PartialEq)]
213+
enum ValidatedCreateIdentityData {
214+
PubkeyAuthn(IdRegFinishArg),
215+
OpenID(ValidatedOpenIDRegFinishArg),
216+
}
217+
218+
#[derive(Clone, Debug, Eq, PartialEq)]
219+
pub struct ValidatedOpenIDRegFinishArg {
220+
pub name: String,
221+
pub credential: openid::OpenIdCredential,
222+
pub openid_config_iss: String,
223+
}
224+
225+
fn validate_identity_data<M: Memory + Clone>(
226+
storage: &Storage<M>,
227+
arg: &CreateIdentityData,
228+
) -> Result<ValidatedCreateIdentityData, IdRegFinishError> {
229+
match &arg {
230+
CreateIdentityData::PubkeyAuthn(arg) => {
231+
Ok(ValidatedCreateIdentityData::PubkeyAuthn(arg.clone()))
232+
}
233+
CreateIdentityData::OpenID(openid_registration_data) => {
234+
let (credential, openid_config_iss) =
235+
create_openid_credential_and_config(openid_registration_data)?;
236+
237+
check_openid_credential_is_unique(storage, &credential.key())
238+
.map_err(|err| IdRegFinishError::InvalidAuthnMethod(err.to_string()))?;
239+
240+
let name = openid_registration_data.name.clone();
241+
242+
Ok(ValidatedCreateIdentityData::OpenID(
243+
ValidatedOpenIDRegFinishArg {
244+
name,
245+
credential,
246+
openid_config_iss,
247+
},
248+
))
249+
}
250+
}
251+
}
252+
253+
fn apply_identity_data(
254+
identity: &mut Anchor,
255+
arg: ValidatedCreateIdentityData,
256+
) -> Result<Operation, IdRegFinishError> {
257+
match arg {
258+
ValidatedCreateIdentityData::PubkeyAuthn(id_reg_finish_arg) => {
203259
let name = id_reg_finish_arg.name.clone();
204260
let device = DeviceWithUsage::try_from(id_reg_finish_arg.authn_method.clone())
205261
.map(|device| Device::from(DeviceData::from(device)))
@@ -208,49 +264,130 @@ fn create_identity(arg: &CreateIdentityData) -> Result<IdentityNumber, IdRegFini
208264
identity
209265
.add_device(device.clone())
210266
.map_err(|err| IdRegFinishError::InvalidAuthnMethod(err.to_string()))?;
211-
set_name(&mut identity, name)
267+
set_name(identity, name)
212268
.map_err(|err| IdRegFinishError::StorageError(err.to_string()))?;
213269
activity_bookkeeping(
214-
&mut identity,
270+
identity,
215271
&AuthorizationKey::DeviceKey(device.pubkey.clone()),
216272
);
217273

218-
Operation::RegisterAnchor {
274+
Ok(Operation::RegisterAnchor {
219275
device: DeviceDataWithoutAlias::from(device),
220-
}
276+
})
221277
}
222-
CreateIdentityData::OpenID(openid_registration_data) => {
223-
let OpenIDRegFinishArg { jwt, salt, name } = openid_registration_data;
224-
let (openid_credential, openid_config_iss) = openid::with_provider(jwt, |provider| {
225-
Ok((provider.verify(jwt, salt)?, provider.issuer()))
226-
})?;
227-
add_openid_credential(&mut identity, openid_credential.clone())
278+
ValidatedCreateIdentityData::OpenID(ValidatedOpenIDRegFinishArg {
279+
name,
280+
credential,
281+
openid_config_iss,
282+
}) => {
283+
let key = credential.key();
284+
let iss = openid_config_iss.clone();
285+
286+
add_openid_credential_skip_checks(identity, credential)
228287
.map_err(|err| IdRegFinishError::InvalidAuthnMethod(err.to_string()))?;
229-
set_name(&mut identity, Some(name.clone()))
288+
289+
set_name(identity, Some(name))
230290
.map_err(|err| IdRegFinishError::StorageError(err.to_string()))?;
291+
231292
activity_bookkeeping(
232-
&mut identity,
233-
&AuthorizationKey::OpenIdCredentialKey((
234-
openid_credential.key(),
235-
Some(openid_config_iss),
236-
)),
293+
identity,
294+
&AuthorizationKey::OpenIdCredentialKey((key, Some(openid_config_iss))),
237295
);
238296

239-
Operation::RegisterAnchorWithOpenIdCredential {
240-
iss: openid_credential.iss,
241-
}
297+
Ok(Operation::RegisterAnchorWithOpenIdCredential { iss })
242298
}
243-
};
299+
}
300+
}
244301

245-
let identity_number = identity.anchor_number();
302+
fn create_identity(arg: &CreateIdentityData, now: u64) -> Result<IdentityNumber, IdRegFinishError> {
303+
let (operation, identity_number): (Operation, u64) = state::storage_borrow_mut(|storage| {
304+
let arg = validate_identity_data(storage, arg)?;
246305

247-
state::storage_borrow_mut(|s| {
248-
s.registration_rates.new_registration();
249-
s.create(identity)
250-
})
251-
.map_err(|err| IdRegFinishError::StorageError(err.to_string()))?;
306+
let (operation, identity) =
307+
storage.allocate_anchor_safe(now, |identity: Option<Anchor>| {
308+
let Some(mut identity) = identity else {
309+
return Err(IdentityLimitReached);
310+
};
311+
312+
let operation = apply_identity_data(&mut identity, arg)?;
313+
314+
Ok((operation, identity))
315+
})?;
316+
317+
let identity_number = identity.anchor_number();
318+
319+
storage.registration_rates.new_registration();
320+
storage
321+
.create(identity)
322+
.map_err(|err| IdRegFinishError::StorageError(err.to_string()))?;
323+
324+
Ok::<_, IdRegFinishError>((operation, identity_number))
325+
})?;
252326

327+
// TODO: propagate the `now` timestamp from above to here to avoid `time()` call.
253328
post_operation_bookkeeping(identity_number, operation);
254329

255330
Ok(identity_number)
256331
}
332+
333+
#[cfg(test)]
334+
mod create_identity_tests {
335+
use super::*;
336+
use crate::state::{storage_borrow, storage_replace};
337+
use crate::storage::Storage;
338+
use ic_stable_structures::VectorMemory;
339+
use internet_identity_interface::internet_identity::types::{
340+
AuthnMethodData, DeviceData, DeviceProtection, KeyType, Purpose,
341+
};
342+
343+
#[test]
344+
fn create_identity_failure_does_not_leave_dangling_anchor_allocation() {
345+
storage_replace(Storage::new((0, 10000), VectorMemory::default()));
346+
347+
// Record initial anchor count
348+
let initial_anchor_count = storage_borrow(|storage| storage.anchor_count());
349+
350+
// Create identity data with an extremely long name that will cause failure
351+
let very_long_name = "a".repeat(10_000); // Assuming this exceeds the name length limit
352+
let create_identity_data = CreateIdentityData::PubkeyAuthn(
353+
internet_identity_interface::internet_identity::types::IdRegFinishArg {
354+
authn_method: AuthnMethodData::from(DeviceData {
355+
pubkey: vec![1, 2, 3].into(),
356+
alias: "test_device".to_string(),
357+
credential_id: Some(vec![4, 5, 6].into()),
358+
purpose: Purpose::Authentication,
359+
key_type: KeyType::Unknown,
360+
protection: DeviceProtection::Unprotected,
361+
origin: None,
362+
metadata: None,
363+
}),
364+
name: Some(very_long_name),
365+
},
366+
);
367+
368+
// Attempt to create identity - this should fail
369+
let result = create_identity(&create_identity_data, 111);
370+
371+
// Verify that the creation failed
372+
assert!(result.is_err());
373+
374+
// Verify that the anchor count hasn't changed - no dangling allocation
375+
let final_anchor_count = storage_borrow(|storage| storage.anchor_count());
376+
assert_eq!(
377+
initial_anchor_count, final_anchor_count,
378+
"Anchor count should remain unchanged after failed identity creation"
379+
);
380+
381+
// Verify that no anchor was actually created by trying to read the next anchor number
382+
let expected_next_anchor = storage_borrow(|storage| {
383+
let (id_range_lo, _) = storage.assigned_anchor_number_range();
384+
id_range_lo + final_anchor_count as u64
385+
});
386+
387+
let read_result = storage_borrow(|storage| storage.read(expected_next_anchor));
388+
assert!(
389+
read_result.is_err(),
390+
"No anchor should exist at the next anchor number after failed creation"
391+
);
392+
}
393+
}

0 commit comments

Comments
 (0)