-
Notifications
You must be signed in to change notification settings - Fork 18
add passkey implementation #3748
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
base: dev
Are you sure you want to change the base?
Conversation
BillyWooo
commented
Oct 5, 2025
- Passkey Challenge System: New challenge generation and verification mechanism for secure passkey operations: executor-storage/src/passkey_challenge.rs
- Passkey Storage & Management: Storage layer for passkey credentials with CRUD operations: executor-storage/src/passkey.rs
- WebAuthn Crypto Support: Full passkey cryptographic operations including signature verification: executor-crypto/src/passkey.rs
- RPC Endpoints: New API methods for passkey lifecycle management
-
- omni_requestPasskeyChallenge - Generate authentication challenges
-
- omni_attachPasskey - Register new passkeys to accounts
-
- omni_removePasskey - Remove passkeys from accounts
|
Claude finished @BillyWooo's task —— View job PR Review: Passkey ImplementationI've completed a comprehensive review of the passkey implementation. Overall, this is a well-structured implementation with good security practices. Here's my analysis: Todo List:
SummaryThis PR implements a comprehensive passkey system with three main components:
Code Quality & Best Practices ✅Strengths:
Areas for improvement:
Security Analysis 🔒Strong security measures:
Potential concerns:
Performance Considerations ⚡Efficient design:
Potential optimizations:
Test Coverage 🧪Excellent test coverage:
Test quality is very high - comprehensive scenarios including concurrent sessions and error conditions. Potential Bugs & Issues 🐛
Recommendations 📋High Priority:
Medium Priority:
Low Priority:
Dependencies & Security 🔐New dependencies look appropriate and are well-maintained:
All dependencies are industry-standard choices for WebAuthn implementation. Overall Assessment ⭐This is a high-quality implementation that follows WebAuthn standards correctly and implements proper security measures. The code is well-tested, properly structured, and handles edge cases appropriately. Recommendation: Approve with minor suggestions - The implementation is solid and secure, with only minor improvements suggested for production readiness. |
|
Claude finished @BillyWooo's task —— View job PR Review: Passkey ImplementationThis is a comprehensive and well-implemented passkey system that follows WebAuthn standards with proper security measures. Here's my detailed analysis: Todo List:
SummaryThis PR implements a complete passkey system with three main components:
Code Quality & Best Practices ✅Strengths:
Areas for improvement:
Security Analysis 🔒Strong security measures:
Potential concerns:
Performance Considerations ⚡Efficient design:
Potential optimizations:
Test Coverage 🧪Excellent test coverage:
Test quality is very high - comprehensive scenarios including concurrent sessions and error conditions. Potential Bugs & Issues 🐛
Recommendations 📋High Priority:
Medium Priority:
Low Priority:
Dependencies & Security 🔐New dependencies look appropriate and are well-maintained:
All dependencies are industry-standard choices for WebAuthn implementation. Overall Assessment ⭐This is a high-quality implementation that follows WebAuthn standards correctly and implements proper security measures. The code is well-tested, properly structured, and handles edge cases appropriately. Recommendation: Approve with minor suggestions - The implementation is solid and secure, with only minor improvements suggested for production readiness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you - the overall logic looks fine, I didn't go very detailed into e.g. passkey verification, attach/removal.
I mainly have questions regarding db read/write
| Self { db } | ||
| } | ||
|
|
||
| fn generate_key(omni_account: &AccountId, credential_id: &str) -> [u8; 32] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How long will the credential_id be? It makes sense to pre-hash it if it's very long - otherwise I'd rather keep it simple as use (AccountId, &'static str) as key, see e.g. HeimaJwtStorageKey.
Prehashing also makes it impossible to enumerate all entries if e.g. I want to "list all passkeys for a given user" (as what's implemented in the passkey_challenge) - correct me if I'm wrong @silva-fj
| expires_at: current_time + timeout_seconds, | ||
| }; | ||
|
|
||
| let challenge_key = challenge.as_bytes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would directly use &str or String as key - converting it to bytes first and then insert it would SCALE encode it again which could cause unexpected result
cc @silva-fj on this one too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also does the key need to involve omni-account?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would directly use
&strorStringas key - converting it to bytes first and then insert it would SCALE encode it again which could cause unexpected resultcc @silva-fj on this one too
Yes, we should use types for keys and values that implement Codec, our Storage trait takes care of the encoding
| ErrorCode::ParseError | ||
| })?; | ||
|
|
||
| let identity = Identity::try_from(params.user_id.clone()).map_err(|_| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's "attaching" passkey, shall we check the user_id can't be of passkey type?
| match e { | ||
| PasskeyChallengeError::ChallengeNotFound => { | ||
| error!("Challenge not found"); | ||
| }, | ||
| PasskeyChallengeError::ChallengeExpired => { | ||
| error!("Challenge expired"); | ||
| }, | ||
| PasskeyChallengeError::InvalidChallenge => { | ||
| error!("Invalid challenge"); | ||
| }, | ||
| _ => { | ||
| error!("Challenge verification failed: {:?}", e); | ||
| }, | ||
| } | ||
| executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed | ||
| }) | ||
| }, | ||
| ) | ||
| .map_err(|e| { | ||
| error!("Client data verification failed: {:?}", e); | ||
| match e { | ||
| executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully we could simpify this a bit after #3780 is merged
| pub client_auth: Option<ClientAuth>, | ||
| pub action_type: HyperliquidActionType, | ||
| pub chain_id: ChainId, | ||
| pub attach_passkey: Option<AttachPasskeyData>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for my understanding:
which rpc to call in reality, omni_attachPasskey or this rpc, or both?
| } | ||
|
|
||
| self.remove(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; | ||
| self.cleanup_expired24h_challenges()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should call this on this method, we can spawn a task for this
| } | ||
|
|
||
| /// Check if a challenge exists and is valid (without consuming it) | ||
| pub fn is_challenge_valid( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this only used in tests?
| ErrorCode::ServerError(-32011) // Challenge mismatch | ||
| }, | ||
| executor_crypto::passkey::PasskeyError::OriginVerificationFailed => { | ||
| ErrorCode::ServerError(-32012) // Origin mismatch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should double check that these codes are not used in error_code.rs and it would probably be better to create the new constants and use them here
| use executor_storage::PasskeyStorage; | ||
|
|
||
| let passkey_identity = | ||
| Identity::from_web2_account(&passkey_data.user_id, Web2IdentityType::Passkey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What identity is this? Identity::Passkey ?
| expires_at: current_time + timeout_seconds, | ||
| }; | ||
|
|
||
| let challenge_key = challenge.as_bytes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would directly use
&strorStringas key - converting it to bytes first and then insert it would SCALE encode it again which could cause unexpected resultcc @silva-fj on this one too
Yes, we should use types for keys and values that implement Codec, our Storage trait takes care of the encoding