Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/archive/archive.did
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ type Operation = variant {
add_name;
update_name;
remove_name;
// Registering the browser a session was created from. Once per browser per
// anchor; the self-reported name is redacted like an account name.
register_session_device : record {
name : Private;
};
create_account : record {
name : Private;
};
Expand Down
3 changes: 2 additions & 1 deletion src/canister_tests/src/api/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ pub mod compat {
| Operation::AddEmailRecovery
| Operation::RemoveEmailRecovery
| Operation::AddVerifiedEmail
| Operation::RemoveVerifiedEmail => {
| Operation::RemoveVerifiedEmail
| Operation::RegisterSessionDevice { .. } => {
panic!("not available in compat type")
}
Operation::CreateAccount { name } => CompatOperation::CreateAccount { name },
Expand Down
104 changes: 104 additions & 0 deletions src/canister_tests/src/api/internet_identity/api_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,3 +762,107 @@ pub fn account_principal_index_backfill_status(
(),
)
}

pub fn prepare_account_session(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
request: PrepareAccountSessionRequest,
) -> Result<Result<PrepareAccountSessionResponse, AccountSessionError>, RejectResponse> {
call_candid_as(
env,
canister_id,
RawEffectivePrincipal::None,
sender,
"prepare_account_session",
(request,),
)
.map(|(x,)| x)
}

pub fn get_account_session(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
request: GetAccountSessionRequest,
) -> Result<Result<GetAccountSessionResponse, AccountSessionError>, RejectResponse> {
query_candid_as(env, canister_id, sender, "get_account_session", (request,)).map(|(x,)| x)
}

/// Like the other app-facing helpers, but attaches the session locator bundle as
/// `sender_info`. PocketIC does not verify the `sender_info` canister signature, so the
/// signer, the bundle expiry and the seed match against the caller are what this
/// exercises.
pub fn app_prepare_delegation(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
request: AppPrepareDelegationRequest,
) -> Result<Result<AppPrepareDelegationResponse, AppSessionError>, RejectResponse> {
call_candid_as(
env,
canister_id,
RawEffectivePrincipal::None,
sender,
"app_prepare_delegation",
(request,),
)
.map(|(x,)| x)
}

pub fn app_get_delegation(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
request: AppGetDelegationRequest,
) -> Result<Result<SignedDelegation, AppSessionError>, RejectResponse> {
query_candid_as(env, canister_id, sender, "app_get_delegation", (request,)).map(|(x,)| x)
}

pub fn app_revoke_session(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
) -> Result<(), RejectResponse> {
env.update_call(
canister_id,
sender,
"app_revoke_session",
candid::encode_args(()).expect("encode app_revoke_session args"),
)
.map(|_| ())
}

pub fn revoke_account_session(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
request: RevokeAccountSessionRequest,
) -> Result<Result<(), SessionRevokeError>, RejectResponse> {
call_candid_as(
env,
canister_id,
RawEffectivePrincipal::None,
sender,
"revoke_account_session",
(request,),
)
.map(|(x,)| x)
}

pub fn revoke_device_sessions(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
request: RevokeDeviceSessionsRequest,
) -> Result<Result<(), SessionRevokeError>, RejectResponse> {
call_candid_as(
env,
canister_id,
RawEffectivePrincipal::None,
sender,
"revoke_device_sessions",
(request,),
)
.map(|(x,)| x)
}
64 changes: 64 additions & 0 deletions src/canister_tests/src/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,70 @@ pub fn restore_compressed_stable_memory(env: &PocketIc, canister_id: CanisterId,
env.set_stable_memory(canister_id, buffer, BlobCompression::Gzip);
}

/// A browser key of the kind `prepare_account_session` demands a proof from.
///
/// The DER encoding and the domain prefix have to match what the canister verifies,
/// so both are spelled out here rather than derived.
pub struct BrowserKey {
signing_key: p256::ecdsa::SigningKey,
}

/// The SPKI header WebCrypto emits for an `ECDSA` P-256 public key, ahead of the 65-byte
/// uncompressed point.
const P256_SPKI_HEADER: [u8; 26] = [
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a,
0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00,
];

const DEVICE_KEY_SIGNATURE_DOMAIN: &[u8] = b"ii-session-device-key";
const SUCCESSOR_KEY_SIGNATURE_DOMAIN: &[u8] = b"ii-session-device-successor";

impl BrowserKey {
pub fn new(seed: u8) -> Self {
Self {
signing_key: p256::ecdsa::SigningKey::from_bytes(&[seed; 32].into())
.expect("failed to build a browser key"),
}
}

/// The key a browser rotates to after `self`, so a test can walk the chain.
pub fn successor(&self) -> Self {
let mut seed = [0u8; 32];
seed.copy_from_slice(&self.signing_key.to_bytes());
seed[0] = seed[0].wrapping_add(1);
Self {
signing_key: p256::ecdsa::SigningKey::from_bytes(&seed.into())
.expect("failed to build a browser key"),
}
}

pub fn public_key(&self) -> PublicKey {
let point = p256::ecdsa::VerifyingKey::from(&self.signing_key).to_encoded_point(false);
let mut der = P256_SPKI_HEADER.to_vec();
der.extend_from_slice(point.as_bytes());
ByteBuf::from(der)
}

pub fn sign(&self, session_key: &SessionKey, next_device_key: &PublicKey) -> ByteBuf {
self.sign_with(DEVICE_KEY_SIGNATURE_DOMAIN, session_key, next_device_key)
}

/// The successor's own signature, proving the browser holds the key it announces.
pub fn sign_as_successor(&self, session_key: &SessionKey, device_key: &PublicKey) -> ByteBuf {
self.sign_with(SUCCESSOR_KEY_SIGNATURE_DOMAIN, session_key, device_key)
}

fn sign_with(&self, domain: &[u8], session_key: &SessionKey, other: &PublicKey) -> ByteBuf {
use p256::ecdsa::signature::Signer;

let mut message = domain.to_vec();
message.extend_from_slice(session_key);
message.extend_from_slice(other);
let signature: p256::ecdsa::Signature = self.signing_key.sign(&message);
ByteBuf::from(signature.to_bytes().to_vec())
}
}

pub const PUBKEY_1: &str = "test";
pub const PUBKEY_2: &str = "some other key";
pub const RECOVERY_PUBKEY_1: &str = "recovery 1";
Expand Down
Loading
Loading