diff --git a/crates/sui-crypto/Cargo.toml b/crates/sui-crypto/Cargo.toml index e5c9974b2..c5cec4e40 100644 --- a/crates/sui-crypto/Cargo.toml +++ b/crates/sui-crypto/Cargo.toml @@ -25,6 +25,7 @@ rustdoc-args = [ default = [] ed25519 = ["dep:ed25519-dalek", "dep:rand_core"] secp256r1 = ["dep:p256", "dep:rand_core"] +passkey = ["secp256r1", "dep:sha2"] secp256k1 = ["dep:k256", "dep:rand_core", "signature/std"] zklogin = [ "dep:ark-bn254", @@ -64,6 +65,9 @@ ed25519-dalek = { version = "2.1.1", optional = true } # secp256r1 support p256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "std"], optional = true } +# passkey verification support +sha2 = { version = "0.10.8", optional = true } + # secp256k1 support k256 = { version = "0.13.4", default-features = false, features = ["ecdsa"], optional = true } diff --git a/crates/sui-crypto/src/lib.rs b/crates/sui-crypto/src/lib.rs index 2be7cd122..fd77be2ad 100644 --- a/crates/sui-crypto/src/lib.rs +++ b/crates/sui-crypto/src/lib.rs @@ -23,6 +23,10 @@ pub mod secp256k1; #[cfg_attr(doc_cfg, doc(cfg(feature = "secp256r1")))] pub mod secp256r1; +#[cfg(feature = "passkey")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "passkey")))] +pub mod passkey; + #[cfg(feature = "zklogin")] #[cfg_attr(doc_cfg, doc(cfg(feature = "zklogin")))] pub mod zklogin; diff --git a/crates/sui-crypto/src/multisig.rs b/crates/sui-crypto/src/multisig.rs index 5a9dbd079..cb03a140f 100644 --- a/crates/sui-crypto/src/multisig.rs +++ b/crates/sui-crypto/src/multisig.rs @@ -244,10 +244,14 @@ impl Verifier for UserSignatureVerifier { zklogin_verifier.verify(message, zklogin_authenticator.as_ref()) } - + #[cfg(not(feature = "passkey"))] UserSignature::Passkey(_) => Err(SignatureError::from_source( - "unsupported user signature scheme", + "support for passkey is not enabled", )), + #[cfg(feature = "passkey")] + UserSignature::Passkey(passkey_authenticator) => { + crate::passkey::PasskeyVerifier::default().verify(message, passkey_authenticator) + } } } } diff --git a/crates/sui-crypto/src/passkey.rs b/crates/sui-crypto/src/passkey.rs new file mode 100644 index 000000000..dde00bd05 --- /dev/null +++ b/crates/sui-crypto/src/passkey.rs @@ -0,0 +1,90 @@ +use crate::secp256r1::Secp256r1VerifyingKey; +use crate::SignatureError; +use signature::Verifier; +use sui_sdk_types::types::PasskeyAuthenticator; +use sui_sdk_types::types::SimpleSignature; +use sui_sdk_types::types::UserSignature; + +#[derive(Default, Clone, Debug)] +pub struct PasskeyVerifier {} + +impl PasskeyVerifier { + pub fn new() -> Self { + Self {} + } +} + +impl Verifier for PasskeyVerifier { + fn verify( + &self, + message: &[u8], + authenticator: &PasskeyAuthenticator, + ) -> Result<(), SignatureError> { + let SimpleSignature::Secp256r1 { + signature, + public_key, + } = authenticator.signature() + else { + return Err(SignatureError::from_source("not a secp256r1 signature")); + }; + + if message != authenticator.challenge() { + return Err(SignatureError::from_source( + "passkey challenge does not match expected message", + )); + } + + // Construct passkey signing message = authenticator_data || sha256(client_data_json). + let mut message = authenticator.authenticator_data().to_owned(); + let client_data_hash = { + use sha2::Digest; + + let mut hasher = sha2::Sha256::new(); + hasher.update(authenticator.client_data_json().as_bytes()); + hasher.finalize() + }; + message.extend_from_slice(&client_data_hash); + + let verifying_key = Secp256r1VerifyingKey::new(&public_key)?; + + verifying_key.verify(&message, &signature) + } +} + +impl Verifier for PasskeyVerifier { + fn verify(&self, message: &[u8], signature: &UserSignature) -> Result<(), SignatureError> { + let UserSignature::Passkey(authenticator) = signature else { + return Err(SignatureError::from_source("not a passkey authenticator")); + }; + + >::verify(self, message, authenticator) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SuiVerifier; + use sui_sdk_types::types::Transaction; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test as test; + + #[test] + fn transaction_signing_fixture() { + let transaction = "AAAAACdZawPnpJRjmVcwDu6xrIumtq5NLO+6GHbs0iGdCoD7AQ0T0TolicYERdSvyCRjSSduDZLbSpBsZBoib+lF48EBcgAAAAAAAAAgpQr/Mudl9BdzyBdkbqTlqBw4/aJ21kAD/jpJKa05im4nWWsD56SUY5lXMA7usayLprauTSzvuhh27NIhnQqA++gDAAAAAAAAgIQeAAAAAAAA"; + let signature = "BiVJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XYx0AAAAAhgF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCJjaGFsbGVuZ2UiOiJXellBZmVvbHcweU15bEFheDRvbzNjVC1rdEVaM0xmenZXcURqakxKZVRvIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo1MTczIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfWICfOgpQ38QYao9Gj0/bqmWYNkuxvbuN3lz4uzFcXeVMEVivX41eC9H+tk+UnvUvKzThtf+uMLFzerU0zZLi8le4QJJsAUcyjsP/1UPAesax8UOC14M62FjAqtqaR46wR7jCg=="; + + let transaction: Transaction = { + use base64ct::Encoding; + let bytes = base64ct::Base64::decode_vec(transaction).unwrap(); + bcs::from_bytes(&bytes).unwrap() + }; + let signature = UserSignature::from_base64(signature).unwrap(); + + let verifier = PasskeyVerifier::default(); + verifier + .verify_transaction(&transaction, &signature) + .unwrap(); + } +} diff --git a/crates/sui-sdk-types/src/types/crypto/passkey.rs b/crates/sui-sdk-types/src/types/crypto/passkey.rs index 323e1aa03..779ecf5fb 100644 --- a/crates/sui-sdk-types/src/types/crypto/passkey.rs +++ b/crates/sui-sdk-types/src/types/crypto/passkey.rs @@ -39,6 +39,10 @@ impl PasskeyAuthenticator { &self.client_data_json } + pub fn challenge(&self) -> &[u8] { + &self.challenge + } + pub fn signature(&self) -> SimpleSignature { SimpleSignature::Secp256r1 { signature: self.signature,