Skip to content

Commit

Permalink
Encapsulate anchor in its own module (#1100)
Browse files Browse the repository at this point in the history
* Encapsulate anchor in its own module

This makes the anchor and device constraints unit testable
and separates the logic of validating data from the other
tasks such as authentication or housekeeping.

* Implement review input
  • Loading branch information
Frederik Rothenberger authored Dec 21, 2022
1 parent 21039a7 commit c5829f6
Show file tree
Hide file tree
Showing 10 changed files with 926 additions and 378 deletions.
215 changes: 47 additions & 168 deletions src/internet_identity/src/anchor_management.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::archive::{archive_operation, device_diff};
use crate::state::RegistrationState::DeviceTentativelyAdded;
use crate::state::{Anchor, Device, TentativeDeviceRegistration};
use crate::state::TentativeDeviceRegistration;
use crate::storage::anchor::{Anchor, Device};
use crate::{delegation, state, trap_if_not_authenticated};
use candid::Principal;
use ic_cdk::api::time;
use ic_cdk::{caller, trap};
use internet_identity_interface::archive::{DeviceDataWithoutAlias, Operation};
Expand All @@ -15,7 +15,11 @@ pub fn get_anchor_info(anchor_number: AnchorNumber) -> IdentityAnchorInfo {
let anchor = state::anchor(anchor_number);
trap_if_not_authenticated(&anchor);

let devices = anchor.devices.into_iter().map(DeviceData::from).collect();
let devices = anchor
.into_devices()
.into_iter()
.map(DeviceData::from)
.collect();
let now = time();

state::tentative_device_registrations(|tentative_device_registrations| {
Expand Down Expand Up @@ -51,118 +55,79 @@ pub fn get_anchor_info(anchor_number: AnchorNumber) -> IdentityAnchorInfo {
}

pub fn add(anchor_number: AnchorNumber, device_data: DeviceData) {
const MAX_ENTRIES_PER_ANCHOR: usize = 10;

let mut anchor = state::anchor(anchor_number);
// must be called before the first await because it requires caller()
trap_if_not_authenticated(&anchor);

let new_device = Device::from(device_data);
check_device(&new_device, &anchor.devices);

if anchor
.devices
.iter()
.find(|e| e.pubkey == new_device.pubkey)
.is_some()
{
trap("Device already added.");
}

if anchor.devices.len() >= MAX_ENTRIES_PER_ANCHOR {
anchor.add_device(new_device.clone()).unwrap_or_else(|err| {
trap(&format!(
"at most {} authentication information entries are allowed per anchor",
MAX_ENTRIES_PER_ANCHOR,
));
}

anchor.devices.push(new_device.clone());
"failed to add device to anchor {}: {}",
anchor_number, err
))
});
write_anchor(anchor_number, anchor);

delegation::prune_expired_signatures();

archive_operation(
anchor_number,
caller(),
Operation::AddDevice {
device: DeviceDataWithoutAlias::from(new_device),
},
);
delegation::prune_expired_signatures();
}

/// Replace or remove an existing device.
///
/// NOTE: all mutable operations should call this function because it handles device protection
fn mutate_device_or_trap(
anchor: &mut Anchor,
device_key: DeviceKey,
new_value: Option<Device>,
) -> Operation {
let index = match anchor.devices.iter().position(|e| e.pubkey == device_key) {
None => trap("Could not find device to mutate, check device key"),
Some(index) => index,
};

let existing_device = anchor.devices.get_mut(index).unwrap();
pub fn update(user_number: AnchorNumber, device_key: DeviceKey, device_data: DeviceData) {
let mut anchor = state::anchor(user_number);
trap_if_not_authenticated(&anchor);

// Run appropriate checks for protected devices
match existing_device.protection {
DeviceProtection::Unprotected => (),
DeviceProtection::Protected => {
// If the call is not authenticated with the device to mutate, abort
if caller() != Principal::self_authenticating(&existing_device.pubkey) {
trap("Device is protected. Must be authenticated with this device to mutate");
}
}
let Some(existing_device) = anchor.device(&device_key) else {
trap("Could not find device to update, check device key")
};

match new_value {
Some(new_device) => {
let diff = device_diff(existing_device, &new_device);
*existing_device = new_device;
Operation::UpdateDevice {
device: device_key,
new_values: diff,
}
}
None => {
// NOTE: we void the more efficient remove_swap to ensure device ordering
// is not changed
anchor.devices.remove(index);
Operation::RemoveDevice { device: device_key }
}
}
}

pub fn update(anchor_number: AnchorNumber, device_key: DeviceKey, device_data: DeviceData) {
if device_key != device_data.pubkey {
trap("device key may not be updated");
}
let mut anchor = state::anchor(anchor_number);

trap_if_not_authenticated(&anchor);
let new_device = Device::from(device_data);
check_device(&new_device, &anchor.devices);

let operation = mutate_device_or_trap(&mut anchor, device_key, Some(new_device));
let diff = device_diff(existing_device, &new_device);

write_anchor(anchor_number, anchor);
anchor
.modify_device(&device_key, new_device.clone())
.unwrap_or_else(|err| {
trap(&format!(
"failed to modify device of anchor {}: {}",
user_number, err
))
});
write_anchor(user_number, anchor);

delegation::prune_expired_signatures();

archive_operation(anchor_number, caller(), operation);
archive_operation(
user_number,
caller(),
Operation::UpdateDevice {
device: device_key,
new_values: diff,
},
);
}

pub fn remove(anchor_number: AnchorNumber, device_key: DeviceKey) {
let mut anchor = state::anchor(anchor_number);
// must be called before the first await because it requires caller()
trap_if_not_authenticated(&anchor);

let operation = mutate_device_or_trap(&mut anchor, device_key, None);
anchor.remove_device(&device_key).unwrap_or_else(|err| {
trap(&format!(
"failed to remove device of anchor {}: {}",
anchor_number, err
))
});
write_anchor(anchor_number, anchor);

archive_operation(
anchor_number,
caller(),
Operation::RemoveDevice { device: device_key },
);
delegation::prune_expired_signatures();
archive_operation(anchor_number, caller(), operation);
}

/// Writes the supplied entries to stable memory and updates the anchor operation metric.
Expand All @@ -180,89 +145,3 @@ fn write_anchor(anchor_number: AnchorNumber, anchor: Anchor) {
metrics.anchor_operation_counter += 1;
});
}

/// This checks some device invariants, in particular:
/// * Sizes of various fields do not exceed limits
/// * Sum of sizes of all variable length fields does not exceed limit
/// * Only recovery phrases can be protected
/// * There can only be one recovery phrase
///
/// Otherwise, trap.
///
/// NOTE: while in the future we may lift this restriction, for now we do ensure that
/// protected devices are limited to recovery phrases, which the webapp expects.
fn check_device(device: &Device, existing_devices: &[Device]) {
/// Single devices can use up to 564 bytes for the variable length fields alone.
/// In order to not give away all the anchor space to the device vector, we limit the sum of the
/// size of all variable fields of all devices. This ensures that we have the flexibility to expand
/// or change anchors in the future.
/// The value 2048 was chosen because it is the max anchor size before the stable memory migration.
/// This means that all pre-existing anchors are below this limit. And after the migration, the
/// candid encoded `vec devices` will stay far below 4KB in size (testing showed anchors of up to
/// 2259 bytes).
const VARIABLE_FIELDS_LIMIT: usize = 2048;

check_entry_limits(device);

if device.protection == DeviceProtection::Protected && device.key_type != KeyType::SeedPhrase {
trap(&format!(
"Only recovery phrases can be protected but key type is {:?}",
device.key_type
));
}

// if the device is a recovery phrase, check if a different recovery phrase already exists
if device.key_type == KeyType::SeedPhrase
&& existing_devices.iter().any(|existing_device| {
existing_device.pubkey != device.pubkey
&& existing_device.key_type == KeyType::SeedPhrase
})
{
trap("There is already a recovery phrase and only one is allowed.");
}

let existing_variable_size: usize = existing_devices
.iter()
// filter out the device being checked to not count it twice in case of update operations
.filter(|elem| elem.pubkey != device.pubkey)
.map(|device| device.variable_fields_len())
.sum();

if existing_variable_size + device.variable_fields_len() > VARIABLE_FIELDS_LIMIT {
trap("Devices exceed allowed storage limit. Either use shorter aliases or remove an existing device.")
}
}

fn check_entry_limits(device: &Device) {
const ALIAS_LEN_LIMIT: usize = 64;
const PK_LEN_LIMIT: usize = 300;
const CREDENTIAL_ID_LEN_LIMIT: usize = 200;

let n = device.alias.len();
if n > ALIAS_LEN_LIMIT {
trap(&format!(
"alias length {} exceeds the limit of {} bytes",
n, ALIAS_LEN_LIMIT,
));
}

let n = device.pubkey.len();
if n > PK_LEN_LIMIT {
trap(&format!(
"public key length {} exceeds the limit of {} bytes",
n, PK_LEN_LIMIT,
));
}

let n = device
.credential_id
.as_ref()
.map(|bytes| bytes.len())
.unwrap_or_default();
if n > CREDENTIAL_ID_LEN_LIMIT {
trap(&format!(
"credential id length {} exceeds the limit of {} bytes",
n, CREDENTIAL_ID_LEN_LIMIT,
));
}
}
23 changes: 12 additions & 11 deletions src/internet_identity/src/anchor_management/registration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::anchor_management::{check_device, write_anchor};
use crate::anchor_management::write_anchor;
use crate::archive::archive_operation;
use crate::state::{Anchor, ChallengeInfo, Device};
use crate::state::ChallengeInfo;
use crate::storage::anchor::Device;
use crate::storage::Salt;
use crate::{delegation, secs_to_nanos, state};
use candid::Principal;
Expand Down Expand Up @@ -161,7 +162,6 @@ pub fn register(device_data: DeviceData, challenge_result: ChallengeAttempt) ->
}

let device = Device::from(device_data);
check_device(&device, &vec![]);

if caller() != Principal::self_authenticating(&device.pubkey) {
trap(&format!(
Expand All @@ -171,15 +171,16 @@ pub fn register(device_data: DeviceData, challenge_result: ChallengeAttempt) ->
));
}

let allocation = state::storage_mut(|storage| storage.allocate_anchor_number());
let allocation = state::storage_mut(|storage| storage.allocate_anchor());
match allocation {
Some(anchor_number) => {
write_anchor(
anchor_number,
Anchor {
devices: vec![device.clone()],
},
);
Some((anchor_number, mut anchor)) => {
anchor.add_device(device.clone()).unwrap_or_else(|err| {
trap(&format!(
"failed to register anchor {}: {}",
anchor_number, err
))
});
write_anchor(anchor_number, anchor);
archive_operation(
anchor_number,
caller(),
Expand Down
2 changes: 1 addition & 1 deletion src/internet_identity/src/archive.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::state;
use crate::state::Device;
use crate::storage::anchor::Device;
use candid::{CandidType, Deserialize, Principal};
use ic_cdk::api::call::{call_with_payment, CallResult};
use ic_cdk::api::management_canister::main::{
Expand Down
6 changes: 3 additions & 3 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::anchor_management::tentative_device_registration;
use crate::archive::ArchiveState;
use crate::assets::init_assets;
use crate::state::Anchor;
use crate::storage::anchor::Anchor;
use candid::Principal;
use ic_cdk::api::{caller, set_certified_data, trap};
use ic_cdk_macros::{init, post_upgrade, pre_upgrade, query, update};
Expand Down Expand Up @@ -90,7 +90,7 @@ fn lookup(anchor_number: AnchorNumber) -> Vec<DeviceData> {
storage
.read(anchor_number)
.unwrap_or_default()
.devices
.into_devices()
.into_iter()
.map(DeviceData::from)
.collect()
Expand Down Expand Up @@ -259,7 +259,7 @@ fn update_root_hash() {

/// Checks if the caller is authenticated against the anchor provided and traps if not.
fn trap_if_not_authenticated(anchor: &Anchor) {
for device in &anchor.devices {
for device in anchor.devices() {
if caller() == Principal::self_authenticating(&device.pubkey) {
return;
}
Expand Down
Loading

0 comments on commit c5829f6

Please sign in to comment.