-
Notifications
You must be signed in to change notification settings - Fork 195
feat(be): verify a browser's key and its announced successor #4264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sea-snake
merged 63 commits into
feat/session-devices-registry
from
feat/session-device-key-proof
Sep 9, 2026
+384
−2
Merged
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 f0f73bb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 05fc09f
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake a517b53
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake ec611d0
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 627da44
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 774bcf4
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 8a7913e
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 9e2d9ff
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 4b29aa2
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 61c0514
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake a8c95f0
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake fbb9ff7
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 4dc7999
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 3f860ff
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 6f5aa8e
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake cff60da
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 6852c3a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 4251b9d
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 2ea420c
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 9265a88
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 7565274
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 93af7a3
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 6e66b69
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 24da671
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 72609cb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 16db4d8
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake abd76cd
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 936a75a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 903d6cb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 1e63ac9
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake dc355cb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake f52441c
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake d95b0f1
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake fc0b4ee
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake e480f9d
refactor(be): a browser, not a session device
sea-snake f24c62d
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake a7b0d9e
refactor(be): the key a browser proves with says browser
sea-snake 4629e83
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake aea1bc2
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake b3eebe9
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake d1aa655
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 9a5c56f
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 23bbbcb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 14f8b87
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake e60b657
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 410099b
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake a190628
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 1a55ddf
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 896741a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 4a81fcd
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake e39f89d
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake d1f36ff
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 2bc8aa1
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake ff07585
chore: merge feat/session-devices-registry
sea-snake b5afb48
chore: merge feat/session-devices-registry
sea-snake d97094f
chore: merge feat/session-devices-registry
sea-snake f72c880
chore: merge feat/session-devices-registry
sea-snake cdc36fb
chore: merge feat/session-devices-registry
sea-snake 1000beb
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 1d5952a
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 3c050de
Merge branch 'feat/session-devices-registry' into feat/session-device…
sea-snake 6ef8323
feat(browser-key): verifying yields evidence, and one encoding per key
sea-snake File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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( | ||
|
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( | ||
| ¤t.public, | ||
| ¤t.current(session, &next.public), | ||
| &next.public, | ||
| &next.successor(session, ¤t.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( | ||
| ¤t.public, | ||
| ¤t.current(&session, &announced.public), | ||
| &announced.public, | ||
| ¤t.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( | ||
| ¤t.public, | ||
| ¤t.successor(&session, &next.public), | ||
| &next.public, | ||
| &next.successor(&session, ¤t.public), | ||
| &session | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_signature_over_another_session_key_is_refused() { | ||
| let current = key(1); | ||
| let next = key(2); | ||
|
|
||
| assert!(!verify_browser_keys( | ||
| ¤t.public, | ||
| ¤t.current(&session_key(7), &next.public), | ||
| &next.public, | ||
| &next.successor(&session_key(7), ¤t.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( | ||
| ¤t.public, | ||
| ¤t.current(&session, &announced.public), | ||
| &substituted.public, | ||
| &substituted.successor(&session, ¤t.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( | ||
| ¤t.public, | ||
| &other.current(&session, &next.public), | ||
| &next.public, | ||
| &next.successor(&session, ¤t.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( | ||
| ¤t.public, | ||
| &bare.to_bytes(), | ||
| &next.public, | ||
| &next.successor(&session, ¤t.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]), | ||
| ¤t.current(&session, &next.public), | ||
| &next.public, | ||
| &next.successor(&session, ¤t.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( | ||
| ¤t.public, | ||
| &signature, | ||
| &next.public, | ||
| &next.successor(&session, ¤t.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( | ||
| ¤t.public, | ||
| &[], | ||
| &next.public, | ||
| &next.successor(&session, ¤t.public), | ||
| &session | ||
| )); | ||
| assert!(!verify_browser_keys( | ||
| ¤t.public, | ||
| ¤t.current(&session, &next.public), | ||
| &next.public, | ||
| &[], | ||
| &session | ||
| )); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.