Skip to content
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
960f5be
feat(be): verify a browser's key and its announced successor
sea-snake Aug 22, 2026
f0f73bb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Aug 25, 2026
05fc09f
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Aug 25, 2026
a517b53
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Aug 25, 2026
ec611d0
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Aug 31, 2026
627da44
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Aug 31, 2026
774bcf4
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Aug 31, 2026
8a7913e
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 1, 2026
9e2d9ff
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 1, 2026
4b29aa2
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 1, 2026
61c0514
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
a8c95f0
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
fbb9ff7
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
4dc7999
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
3f860ff
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
6f5aa8e
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
cff60da
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 4, 2026
6852c3a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
4251b9d
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
2ea420c
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
9265a88
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
7565274
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
93af7a3
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
6e66b69
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
24da671
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
72609cb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
16db4d8
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
abd76cd
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
936a75a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
903d6cb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
1e63ac9
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 5, 2026
dc355cb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 6, 2026
f52441c
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 6, 2026
d95b0f1
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 6, 2026
fc0b4ee
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 6, 2026
e480f9d
refactor(be): a browser, not a session device
sea-snake Sep 6, 2026
f24c62d
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 6, 2026
a7b0d9e
refactor(be): the key a browser proves with says browser
sea-snake Sep 6, 2026
4629e83
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
aea1bc2
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
b3eebe9
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
d1aa655
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
9a5c56f
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
23bbbcb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
14f8b87
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
e60b657
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
410099b
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
a190628
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
1a55ddf
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
896741a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 7, 2026
4a81fcd
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 8, 2026
e39f89d
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 8, 2026
d1f36ff
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 8, 2026
2bc8aa1
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 8, 2026
ff07585
chore: merge feat/session-devices-registry
sea-snake Sep 8, 2026
b5afb48
chore: merge feat/session-devices-registry
sea-snake Sep 8, 2026
d97094f
chore: merge feat/session-devices-registry
sea-snake Sep 8, 2026
f72c880
chore: merge feat/session-devices-registry
sea-snake Sep 8, 2026
cdc36fb
chore: merge feat/session-devices-registry
sea-snake Sep 9, 2026
1000beb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 9, 2026
1d5952a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 9, 2026
3c050de
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake Sep 9, 2026
6ef8323
feat(browser-key): verifying yields evidence, and one encoding per key
sea-snake Sep 9, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ rsa = "0.9.10"
minicbor = "1.0.0"

# DNSSEC verifier deps (PR 1b on docs/ongoing/email-recovery.md §7)
Comment thread
sea-snake marked this conversation as resolved.
Outdated
p256 = { version = "0.13", default-features = false, features = ["ecdsa", "sha256"] }
p256 = { version = "0.13", default-features = false, features = ["ecdsa", "sha256", "pkcs8"] }
ed25519-dalek = { version = "2.2", default-features = false }

# Certification
Expand Down
2 changes: 2 additions & 0 deletions src/canister_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ base64.workspace = true
flate2 = "1.0"
hex.workspace = true
lazy_static.workspace = true
# Signs the browser-key proof `prepare_account_session` requires.
p256.workspace = true
regex.workspace = true
serde.workspace = true
serde_cbor.workspace = true
Expand Down
1 change: 1 addition & 0 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ mod mcp_registration;

mod openid;
mod session_delegation;
mod sessions;
mod single_flight_cache;
mod state;
mod stats;
Expand Down
5 changes: 5 additions & 0 deletions src/internet_identity/src/sessions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// The sign-in ceremony that creates a session is added on top of this; for now the module
// holds only the verifier its request will be checked against.
#![allow(dead_code)]

pub mod browser_key;
283 changes: 283 additions & 0 deletions src/internet_identity/src/sessions/browser_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
//! Verifies that a sign-in request comes from a browser holding the key it names.

use internet_identity_interface::internet_identity::types::{PublicKey, SessionKey};
use p256::ecdsa::signature::Verifier;
use p256::ecdsa::{Signature, VerifyingKey};
use p256::pkcs8::DecodePublicKey;

/// Prefixed to the signed message so the browser key cannot be made to sign for another
/// purpose by presenting a message from one.
const BROWSER_KEY_SIGNATURE_DOMAIN: &[u8] = b"ii-session-browser-key";

/// A different prefix for the successor's own signature, so neither signature can be
/// replayed in the other's role.
const SUCCESSOR_KEY_SIGNATURE_DOMAIN: &[u8] = b"ii-session-browser-successor";

/// A browser key is P-256, and the signature the raw `r || s` pair WebCrypto produces.
const BROWSER_KEY_SIGNATURE_BYTES: usize = 64;

/// Both keys sign: the current one over the session key and its successor, and the successor
/// over the session key and the key it replaces.
///
/// The successor's own signature is what stops a key being announced by someone who does not
/// hold it — without it, keys read off the wire could be planted as another browser's
/// successor and claimed when that browser next presented one.
pub fn verify_browser_keys(
current_browser_key: &PublicKey,
current_browser_key_signature: &[u8],
next_browser_key: &PublicKey,
next_browser_key_signature: &[u8],
session_key: &SessionKey,
) -> bool {
verify(
Comment thread
sea-snake marked this conversation as resolved.
Outdated
current_browser_key,
current_browser_key_signature,
&signed_message(BROWSER_KEY_SIGNATURE_DOMAIN, session_key, next_browser_key),
) && verify(
next_browser_key,
next_browser_key_signature,
&signed_message(
SUCCESSOR_KEY_SIGNATURE_DOMAIN,
session_key,
current_browser_key,
),
)
}

fn verify(key: &PublicKey, signature: &[u8], message: &[u8]) -> bool {
if signature.len() != BROWSER_KEY_SIGNATURE_BYTES {
return false;
}
let Ok(key) = VerifyingKey::from_public_key_der(key) else {
return false;
};
let Ok(signature) = Signature::from_slice(signature) else {
return false;
};
key.verify(message, &signature).is_ok()
}

/// Covers the other key as well as the session key: keys are visible on the wire, so a
/// signature that bound only the session key could be paired with one a caller chose.
fn signed_message(domain: &[u8], session_key: &SessionKey, other_key: &PublicKey) -> Vec<u8> {
let mut message = Vec::with_capacity(domain.len() + session_key.len() + other_key.len());
message.extend_from_slice(domain);
message.extend_from_slice(session_key);
message.extend_from_slice(other_key);
message
}

#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::signature::Signer;
use p256::ecdsa::SigningKey;
use serde_bytes::ByteBuf;

/// 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,
];

struct Key {
signing: SigningKey,
public: PublicKey,
}

fn key(seed: u8) -> Key {
let signing = SigningKey::from_bytes(&[seed; 32].into()).unwrap();
let point = VerifyingKey::from(&signing).to_encoded_point(false);
let mut der = P256_SPKI_HEADER.to_vec();
der.extend_from_slice(point.as_bytes());
Key {
signing,
public: ByteBuf::from(der),
}
}

impl Key {
fn sign(&self, domain: &[u8], session_key: &SessionKey, other: &PublicKey) -> Vec<u8> {
let signature: Signature =
self.signing
.sign(&signed_message(domain, session_key, other));
signature.to_bytes().to_vec()
}

fn current(&self, session_key: &SessionKey, next: &PublicKey) -> Vec<u8> {
self.sign(BROWSER_KEY_SIGNATURE_DOMAIN, session_key, next)
}

fn successor(&self, session_key: &SessionKey, current: &PublicKey) -> Vec<u8> {
self.sign(SUCCESSOR_KEY_SIGNATURE_DOMAIN, session_key, current)
}
}

fn session_key(seed: u8) -> SessionKey {
ByteBuf::from(vec![seed; 62])
}

/// A rotation as an honest browser performs it: it holds both keys and signs with both.
fn rotation(current: &Key, next: &Key, session: &SessionKey) -> bool {
verify_browser_keys(
&current.public,
&current.current(session, &next.public),
&next.public,
&next.successor(session, &current.public),
session,
)
}

#[test]
fn a_browser_holding_both_keys_is_accepted() {
assert!(rotation(&key(1), &key(2), &session_key(7)));
}

#[test]
fn a_successor_the_caller_does_not_hold_is_refused() {
let current = key(1);
let announced = key(2);
let session = session_key(7);

// Everything the wire carries, but signed only by the key the caller holds.
assert!(!verify_browser_keys(
&current.public,
&current.current(&session, &announced.public),
&announced.public,
&current.current(&session, &announced.public),
&session
));
}

#[test]
fn a_successor_signature_replayed_as_the_current_one_is_refused() {
let current = key(1);
let next = key(2);
let session = session_key(7);

assert!(!verify_browser_keys(
&current.public,
&current.successor(&session, &next.public),
&next.public,
&next.successor(&session, &current.public),
&session
));
}

#[test]
fn a_signature_over_another_session_key_is_refused() {
let current = key(1);
let next = key(2);

assert!(!verify_browser_keys(
&current.public,
&current.current(&session_key(7), &next.public),
&next.public,
&next.successor(&session_key(7), &current.public),
&session_key(8)
));
}

#[test]
fn a_signature_paired_with_another_successor_is_refused() {
let current = key(1);
let announced = key(2);
let substituted = key(3);
let session = session_key(7);

assert!(!verify_browser_keys(
&current.public,
&current.current(&session, &announced.public),
&substituted.public,
&substituted.successor(&session, &current.public),
&session
));
}

#[test]
fn another_browsers_signature_is_refused() {
let current = key(1);
let other = key(9);
let next = key(2);
let session = session_key(7);

assert!(!verify_browser_keys(
&current.public,
&other.current(&session, &next.public),
&next.public,
&next.successor(&session, &current.public),
&session
));
}

#[test]
fn a_signature_over_the_bare_session_key_is_refused() {
let current = key(1);
let next = key(2);
let session = session_key(7);
let bare: Signature = current.signing.sign(&session);

assert!(!verify_browser_keys(
&current.public,
&bare.to_bytes(),
&next.public,
&next.successor(&session, &current.public),
&session
));
}

#[test]
fn a_key_that_is_not_a_p256_public_key_is_refused() {
let current = key(1);
let next = key(2);
let session = session_key(7);

assert!(!verify_browser_keys(
&ByteBuf::from(vec![0u8; 91]),
&current.current(&session, &next.public),
&next.public,
&next.successor(&session, &current.public),
&session
));
}

#[test]
fn a_signature_of_the_wrong_length_is_refused() {
let current = key(1);
let next = key(2);
let session = session_key(7);
let mut signature = current.current(&session, &next.public);
signature.push(0);

assert!(!verify_browser_keys(
&current.public,
&signature,
&next.public,
&next.successor(&session, &current.public),
&session
));
}

#[test]
fn an_empty_signature_is_refused() {
let current = key(1);
let next = key(2);
let session = session_key(7);

assert!(!verify_browser_keys(
&current.public,
&[],
&next.public,
&next.successor(&session, &current.public),
&session
));
assert!(!verify_browser_keys(
&current.public,
&current.current(&session, &next.public),
&next.public,
&[],
&session
));
}
}
Loading