diff --git a/crates/sui-e2e-tests/tests/passkey_e2e_tests.rs b/crates/sui-e2e-tests/tests/passkey_e2e_tests.rs index 5792fae74e306f..3007a659c29794 100644 --- a/crates/sui-e2e-tests/tests/passkey_e2e_tests.rs +++ b/crates/sui-e2e-tests/tests/passkey_e2e_tests.rs @@ -34,48 +34,8 @@ use sui_types::{ use test_cluster::TestCluster; use test_cluster::TestClusterBuilder; use url::Url; - -struct MyUserValidationMethod {} -#[async_trait::async_trait] -impl UserValidationMethod for MyUserValidationMethod { - async fn check_user_presence(&self) -> bool { - true - } - - async fn check_user_verification(&self) -> bool { - true - } - - fn is_verification_enabled(&self) -> Option { - Some(true) - } - - fn is_presence_enabled(&self) -> bool { - true - } -} - -/// A helper struct for passkey response and transaction construction. -pub struct PasskeyResponse { - user_sig_bytes: Vec, - authenticator_data: Vec, - client_data_json: String, - intent_msg: IntentMessage, -} - -/// Submits a transaction to the test cluster and returns the result. -async pub fn execute_tx(tx: Transaction, test_cluster: &TestCluster) -> SuiResult { - test_cluster - .authority_aggregator() - .authority_clients - .values() - .next() - .unwrap() - .authority_client() - .handle_transaction(tx, Some(SocketAddr::new([127, 0, 0, 1].into(), 0))) - .await - .map(|_| ()) -} +pub mod passkey_util; +use passkey_util::{execute_tx, MyUserValidationMethod, PasskeyResponse}; /// Register a new passkey, derive its address, fund it with gas and create a test /// transaction, then get a response from the passkey from signing. @@ -202,6 +162,7 @@ async fn create_credential_and_sign_test_tx( let client_data_json = authenticated_cred.response.client_data_json.as_slice(); PasskeyResponse { + sender, user_sig_bytes, authenticator_data: authenticator_data.to_vec(), client_data_json: String::from_utf8_lossy(client_data_json).to_string(), diff --git a/crates/sui-e2e-tests/tests/passkey_session_e2e_test.rs b/crates/sui-e2e-tests/tests/passkey_session_e2e_test.rs deleted file mode 100644 index 2778cd1ba33a7c..00000000000000 --- a/crates/sui-e2e-tests/tests/passkey_session_e2e_test.rs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 -use fastcrypto::{hash::HashFunction, traits::ToFromBytes}; -use p256::pkcs8::DecodePublicKey; -use passkey_authenticator::{Authenticator, UserValidationMethod}; -use passkey_client::Client; -use passkey_types::{ - ctap2::Aaguid, - rand::random_vec, - webauthn::{ - AttestationConveyancePreference, CredentialCreationOptions, CredentialRequestOptions, - PublicKeyCredentialCreationOptions, PublicKeyCredentialParameters, - PublicKeyCredentialRequestOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, UserVerificationRequirement, - }, - Bytes, Passkey, -}; -use shared_crypto::intent::{Intent, IntentMessage, INTENT_PREFIX_LENGTH}; -use std::net::SocketAddr; -use sui_core::authority_client::AuthorityAPI; -use sui_macros::sim_test; -use sui_test_transaction_builder::TestTransactionBuilder; -use sui_types::error::UserInputError; -use sui_types::error::{SuiError, SuiResult}; -use sui_types::signature::GenericSignature; -use sui_types::transaction::Transaction; -use sui_types::{ - base_types::SuiAddress, - crypto::{PublicKey, SignatureScheme}, - passkey_authenticator::{to_signing_message, PasskeyAuthenticator}, - transaction::TransactionData, -}; -use sui_types::{ - crypto::{DefaultHash, Signature}, -}; -use test_cluster::TestCluster; -use test_cluster::TestClusterBuilder; -use url::Url; - -#[sim_test] -async fn test_passkey_session_feature_deny() { - // use sui_protocol_config::ProtocolConfig; - // let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { - // config.set_passkey_auth_for_testing(false); - // config - // }); - // let test_cluster = TestClusterBuilder::new().build().await; - // let response = create_credential_and_sign_test_tx(&test_cluster, None, false, false).await; - // let tx = make_good_passkey_tx(response); - // let err = execute_tx(tx, &test_cluster).await.unwrap_err(); - // assert!(matches!( - // err, - // SuiError::UserInputError { - // error: UserInputError::Unsupported(..) - // } - // )); -} - -#[sim_test] -async fn test_passkey_authenticator_scenarios() { - // let test_cluster = TestClusterBuilder::new().build().await; - // let response = create_credential_and_sign_test_tx(&test_cluster, None, false, false).await; - // let tx = make_good_passkey_tx(response); - // let res = execute_tx(tx, &test_cluster).await; - // case 1: assert!(res.is_ok()); - // case 2: sign a different tx with df passkey ephemeral sig, but with same register sig passes - // case 3: epoch passes, the ephermal sig expires, fails to verify - // case 4: max_epoch bound delta = 1, but max epoch - current epoch > 1, too large, fails - // case 5: use a different ephemeral key than the one committed to passkey, fails to verify - -} - -#[sim_test] -async fn test_passkey_fails_mismatched_challenge() { - // let test_cluster = TestClusterBuilder::new().build().await; - - // // Tweak intent in challenge that is sent to passkey. - // let response = create_credential_and_sign_test_tx(&test_cluster, None, true, false).await; - // let sig = GenericSignature::PasskeyAuthenticator( - // PasskeyAuthenticator::new_for_testing( - // response.authenticator_data, - // response.client_data_json, - // Signature::from_bytes(&response.user_sig_bytes).unwrap(), - // ) - // .unwrap(), - // ); - // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); - // let res = execute_tx(tx, &test_cluster).await; - // let err = res.unwrap_err(); - // assert_eq!( - // err, - // SuiError::InvalidSignature { - // error: "Invalid challenge".to_string() - // } - // ); - - // // Tweak tx_digest bytes in challenge that is sent to passkey. - // let response = create_credential_and_sign_test_tx(&test_cluster, None, false, true).await; - // let sig = GenericSignature::PasskeyAuthenticator( - // PasskeyAuthenticator::new_for_testing( - // response.authenticator_data, - // response.client_data_json, - // Signature::from_bytes(&response.user_sig_bytes).unwrap(), - // ) - // .unwrap(), - // ); - // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); - // let res = execute_tx(tx, &test_cluster).await; - // let err = res.unwrap_err(); - // assert_eq!( - // err, - // SuiError::InvalidSignature { - // error: "Invalid challenge".to_string() - // } - // ); -} - -#[sim_test] -async fn test_passkey_fails_to_verify_register_sig() { - // let test_cluster = TestClusterBuilder::new().build().await; - // let response = create_credential_and_sign_test_tx(&test_cluster, None, false, false).await; - // let mut modified_sig = response.user_sig_bytes.clone(); - // modified_sig[1] = 0x00; - // let sig = GenericSignature::PasskeyAuthenticator( - // PasskeyAuthenticator::new_for_testing( - // response.authenticator_data, - // response.client_data_json, - // Signature::from_bytes(&modified_sig).unwrap(), - // ) - // .unwrap(), - // ); - // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); - // let res = execute_tx(tx, &test_cluster).await; - // let err = res.unwrap_err(); - // assert_eq!( - // err, - // SuiError::InvalidSignature { - // error: "Fails to verify".to_string() - // } - // ); -} -#[sim_test] -async fn test_passkey_fails_to_verify_ephemeral_sig() { - // ephemeral sig is on an unrelated personal message -} - -#[sim_test] -async fn test_passkey_fails_to_verify_mismatch_max_epoch() { - // ephemeral sig is on an unrelated personal message -} - -#[sim_test] -async fn test_passkey_fails_wrong_author() { - // let test_cluster = TestClusterBuilder::new().build().await; - // // Modify sender that receives gas and construct test txn. - // let response = - // create_credential_and_sign_test_tx(&test_cluster, Some(SuiAddress::ZERO), false, false) - // .await; - // let sig = GenericSignature::PasskeyAuthenticator( - // PasskeyAuthenticator::new_for_testing( - // response.authenticator_data, - // response.client_data_json, - // Signature::from_bytes(&response.user_sig_bytes).unwrap(), - // ) - // .unwrap(), - // ); - // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); - // let res = execute_tx(tx, &test_cluster).await; - // let err = res.unwrap_err(); - // assert!(matches!(err, SuiError::SignerSignatureAbsent { .. })); -} diff --git a/crates/sui-e2e-tests/tests/passkey_session_e2e_tests.rs b/crates/sui-e2e-tests/tests/passkey_session_e2e_tests.rs new file mode 100644 index 00000000000000..34110a5f64d65d --- /dev/null +++ b/crates/sui-e2e-tests/tests/passkey_session_e2e_tests.rs @@ -0,0 +1,426 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use fastcrypto::{ + ed25519::Ed25519KeyPair, + secp256r1::Secp256r1KeyPair, + traits::{KeyPair, ToFromBytes}, +}; +use p256::pkcs8::DecodePublicKey; +use passkey_authenticator::{Authenticator, UserValidationMethod}; +use passkey_client::Client; +use passkey_types::{ + ctap2::Aaguid, + rand::random_vec, + webauthn::{ + AttestationConveyancePreference, CredentialCreationOptions, CredentialRequestOptions, + PublicKeyCredentialCreationOptions, PublicKeyCredentialParameters, + PublicKeyCredentialRequestOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, UserVerificationRequirement, + }, + Bytes, Passkey, +}; +use rand::{rngs::StdRng, SeedableRng}; +use shared_crypto::intent::{Intent, IntentMessage, PersonalMessage}; +use sui_macros::sim_test; +use sui_test_transaction_builder::TestTransactionBuilder; +use sui_types::error::UserInputError; +use sui_types::error::{SuiError, SuiResult}; +use sui_types::signature::GenericSignature; +use sui_types::transaction::Transaction; +use sui_types::{ + base_types::SuiAddress, + crypto::{PublicKey, SignatureScheme}, + transaction::TransactionData, +}; +use sui_types::{ + crypto::{get_key_pair, Signature}, + passkey_session_authenticator::{PasskeySessionAuthenticator, RawPasskeySessionAuthenticator}, +}; +use test_cluster::TestCluster; +use test_cluster::TestClusterBuilder; +use url::Url; + +pub mod passkey_util; +use passkey_util::{execute_tx, MyUserValidationMethod, PasskeyResponse}; + +async fn make_passkey_session_tx( + test_cluster: &TestCluster, + eph_kp: Ed25519KeyPair, + max_epoch: u64, + register_msg: Option>, + wrong_register_sig: bool, + wrong_ephemeral_sig: bool, +) -> Transaction { + let register_msg = match register_msg { + Some(msg) => msg, + None => { + let mut register_msg = vec![SignatureScheme::ED25519.flag()]; + register_msg.extend_from_slice(eph_kp.public().as_bytes()); + register_msg.extend_from_slice(&max_epoch.to_be_bytes()); + register_msg + } + }; + + let response = + create_credential_and_commit_ephemeral_pk(test_cluster, None, register_msg, false, false) + .await; + let ephemeral_signature = Signature::new_secure(&response.intent_msg, &eph_kp); + + let passkey_session_authenticator = RawPasskeySessionAuthenticator { + authenticator_data: response.authenticator_data, + client_data_json: response.client_data_json, + register_signature: if wrong_register_sig { + let mut fake_signature = response.user_sig_bytes.clone(); + fake_signature[2] = fake_signature[2] + 1; + Signature::from_bytes(&fake_signature).unwrap() + } else { + Signature::from_bytes(&response.user_sig_bytes).unwrap() + }, + max_epoch, + ephemeral_signature: if wrong_ephemeral_sig { + let fake_kp = Secp256r1KeyPair::generate(&mut StdRng::from_seed([0u8; 32])); + let intent_msg = IntentMessage::new( + Intent::personal_message(), + PersonalMessage { + message: "random".as_bytes().to_vec(), + }, + ); + Signature::new_secure(&intent_msg, &fake_kp) + } else { + ephemeral_signature + }, + } + .try_into() + .unwrap(); + let sig = GenericSignature::PasskeySessionAuthenticator(passkey_session_authenticator); + Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]) +} + +/// Register a new passkey, derive its address, fund it with gas and create a test +/// transaction, then get a response from the passkey from signing. +async fn create_credential_and_commit_ephemeral_pk( + test_cluster: &TestCluster, + sender: Option, + passkey_challenge: Vec, + change_intent: bool, + change_tx: bool, +) -> PasskeyResponse { + // set up authenticator and client + let my_aaguid = Aaguid::new_empty(); + let user_validation_method = MyUserValidationMethod {}; + let store: Option = None; + let my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); + let mut my_client = Client::new(my_authenticator); + let origin = Url::parse("https://www.sui.io").unwrap(); + + // Create credential. + let challenge_bytes_from_rp: Bytes = random_vec(32).into(); + let user_entity = PublicKeyCredentialUserEntity { + id: random_vec(32).into(), + display_name: "Johnny Passkey".into(), + name: "jpasskey@example.org".into(), + }; + let request = CredentialCreationOptions { + public_key: PublicKeyCredentialCreationOptions { + rp: PublicKeyCredentialRpEntity { + id: None, // Leaving the ID as None means use the effective domain + name: origin.domain().unwrap().into(), + }, + user: user_entity, + challenge: challenge_bytes_from_rp, + pub_key_cred_params: vec![PublicKeyCredentialParameters { + ty: PublicKeyCredentialType::PublicKey, + alg: coset::iana::Algorithm::ES256, + }], + timeout: None, + exclude_credentials: None, + authenticator_selection: None, + hints: None, + attestation: AttestationConveyancePreference::None, + attestation_formats: None, + extensions: None, + }, + }; + let my_webauthn_credential = my_client.register(&origin, request, None).await.unwrap(); + let verifying_key = p256::ecdsa::VerifyingKey::from_public_key_der( + my_webauthn_credential + .response + .public_key + .unwrap() + .as_slice(), + ) + .unwrap(); + + // Derive compact pubkey from DER format. + let encoded_point = verifying_key.to_encoded_point(false); + let x = encoded_point.x(); + let y = encoded_point.y(); + let prefix = if y.unwrap()[31] % 2 == 0 { 0x02 } else { 0x03 }; + let mut pk_bytes = vec![prefix]; + pk_bytes.extend_from_slice(x.unwrap()); + let pk = PublicKey::try_from_bytes(SignatureScheme::PasskeyAuthenticator, &pk_bytes).unwrap(); + + // Request a signature from passkey with challenge set to passkey_digest. + let credential_request = CredentialRequestOptions { + public_key: PublicKeyCredentialRequestOptions { + challenge: Bytes::from(passkey_challenge), + timeout: None, + rp_id: Some(String::from(origin.domain().unwrap())), + allow_credentials: None, + user_verification: UserVerificationRequirement::default(), + attestation: Default::default(), + attestation_formats: None, + extensions: None, + hints: None, + }, + }; + + let authenticated_cred = my_client + .authenticate(&origin, credential_request, None) + .await + .unwrap(); + + // Parse signature from der format in response and normalize it to lower s. + let sig_bytes_der = authenticated_cred.response.signature.as_slice(); + let sig = p256::ecdsa::Signature::from_der(sig_bytes_der).unwrap(); + let sig_bytes = sig.normalize_s().unwrap_or(sig).to_bytes(); + + let mut user_sig_bytes = vec![SignatureScheme::Secp256r1.flag()]; + user_sig_bytes.extend_from_slice(&sig_bytes); + user_sig_bytes.extend_from_slice(&pk_bytes); + + // Parse authenticator_data and client_data_json from response. + let authenticator_data = authenticated_cred.response.authenticator_data.as_slice(); + let client_data_json = authenticated_cred.response.client_data_json.as_slice(); + + // Compute sui address as sender, fund gas and make a test transaction. + let sender = match sender { + Some(s) => s, + None => SuiAddress::from(&pk), + }; + let rgp = test_cluster.get_reference_gas_price().await; + let gas = test_cluster + .fund_address_and_return_gas(rgp, Some(20000000000), sender) + .await; + let tx_data = TestTransactionBuilder::new(sender, gas, rgp) + .transfer_sui(None, SuiAddress::ZERO) + .build(); + let intent_msg = IntentMessage::new(Intent::sui_transaction(), tx_data); + + PasskeyResponse { + sender, + user_sig_bytes, + authenticator_data: authenticator_data.to_vec(), + client_data_json: String::from_utf8_lossy(client_data_json).to_string(), + intent_msg, + } +} + +#[sim_test] +async fn test_passkey_session_feature_deny() { + use sui_protocol_config::ProtocolConfig; + let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { + config.set_passkey_session_auth_for_testing(false); + config + }); + let test_cluster = TestClusterBuilder::new().build().await; + + let kp: Ed25519KeyPair = get_key_pair().1; + let max_epoch = 2 as u64; + let tx = make_passkey_session_tx(&test_cluster, kp, max_epoch, None, false, false).await; + let err = execute_tx(tx, &test_cluster).await.unwrap_err(); + assert!(matches!( + err, + SuiError::UserInputError { + error: UserInputError::Unsupported(..) + } + )); +} + +#[sim_test] +async fn test_passkey_authenticator_scenarios() { + use sui_protocol_config::ProtocolConfig; + let test_cluster = TestClusterBuilder::new() + .with_epoch_duration_ms(15000) + .with_default_jwks() + .build() + .await; + + let kp = Ed25519KeyPair::generate(&mut StdRng::from_seed([0u8; 32])); + let max_epoch = 1 as u64; + + // case 1: sign a tx with passkey ephemeral sig and register sig, passes + let tx = make_passkey_session_tx(&test_cluster, kp.copy(), max_epoch, None, false, false).await; + let res = execute_tx(tx, &test_cluster).await; + assert!(res.is_ok()); + + // case 2: use a mismatched ephemeral key with what the register signature commits to, fails to verify. + let kp2: Ed25519KeyPair = get_key_pair().1; + let mut register_msg = vec![SignatureScheme::ED25519.flag()]; + register_msg.extend_from_slice(kp2.public().as_bytes()); + register_msg.extend_from_slice(&max_epoch.to_be_bytes()); + let tx = make_passkey_session_tx( + &test_cluster, + kp.copy(), + max_epoch, + Some(register_msg), + false, + false, + ) + .await; + let res = execute_tx(tx, &test_cluster).await; + assert!(matches!( + res, + Err(SuiError::InvalidSignature { error }) if error == "Invalid parsed challenge" + )); + + // case 3: use a mismatched max_epoch with what the register signature commits to, fails to verify. + let mut register_msg = vec![SignatureScheme::ED25519.flag()]; + register_msg.extend_from_slice(kp.public().as_bytes()); + register_msg.extend_from_slice(&(max_epoch + 1).to_be_bytes()); + let tx = make_passkey_session_tx( + &test_cluster, + kp.copy(), + max_epoch, + Some(register_msg), + false, + false, + ) + .await; + let res = execute_tx(tx, &test_cluster).await; + assert!(matches!( + res, + Err(SuiError::InvalidSignature { error }) if error == "Invalid parsed challenge" + )); + + // case 4: invalid register signature fails to verify. + let tx = make_passkey_session_tx(&test_cluster, kp.copy(), max_epoch, None, true, false).await; + let res = execute_tx(tx, &test_cluster).await; + assert!(matches!( + res, + Err(SuiError::InvalidSignature { error }) if error == "Fails to verify register sig" + )); + // case 5: invalid ephemeral signature fails to verify. + let tx = make_passkey_session_tx(&test_cluster, kp.copy(), max_epoch, None, false, true).await; + let res = execute_tx(tx, &test_cluster).await; + println!("res: {:?}", res); + assert!(matches!( + res, + Err(SuiError::InvalidSignature { error }) if error == "Fails to verify ephemeral sig" + )); + // // case 6: advance 2 epochs, the ephermal sig expires, fails to verify + // test_cluster.trigger_reconfiguration().await; + // test_cluster.trigger_reconfiguration().await; + + // let tx = make_passkey_session_tx(&test_cluster, kp.copy(), max_epoch, None, false, false).await; + // let res = execute_tx(tx, &test_cluster).await; + // assert!(matches!( + // res, + // Err(SuiError::InvalidSignature { error }) if error == "Passkey session max epoch too large 2, current epoch 2, max accepted: 1" + // )); + // // case 4: max_epoch bound delta = 1, but max epoch - current epoch > 1, too large, fails + // let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { + // config.set_passkey_session_max_epoch_upper_bound_delta_for_testing(Some(1)); + // config + // }); +} + +#[sim_test] +async fn test_passkey_fails_mismatched_challenge() { + // let test_cluster = TestClusterBuilder::new().build().await; + + // // Tweak intent in challenge that is sent to passkey. + // let response = create_credential_and_sign_test_tx(&test_cluster, None, true, false).await; + // let sig = GenericSignature::PasskeyAuthenticator( + // PasskeyAuthenticator::new_for_testing( + // response.authenticator_data, + // response.client_data_json, + // Signature::from_bytes(&response.user_sig_bytes).unwrap(), + // ) + // .unwrap(), + // ); + // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); + // let res = execute_tx(tx, &test_cluster).await; + // let err = res.unwrap_err(); + // assert_eq!( + // err, + // SuiError::InvalidSignature { + // error: "Invalid challenge".to_string() + // } + // ); + + // // Tweak tx_digest bytes in challenge that is sent to passkey. + // let response = create_credential_and_sign_test_tx(&test_cluster, None, false, true).await; + // let sig = GenericSignature::PasskeyAuthenticator( + // PasskeyAuthenticator::new_for_testing( + // response.authenticator_data, + // response.client_data_json, + // Signature::from_bytes(&response.user_sig_bytes).unwrap(), + // ) + // .unwrap(), + // ); + // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); + // let res = execute_tx(tx, &test_cluster).await; + // let err = res.unwrap_err(); + // assert_eq!( + // err, + // SuiError::InvalidSignature { + // error: "Invalid challenge".to_string() + // } + // ); +} + +#[sim_test] +async fn test_passkey_fails_to_verify_register_sig() { + // let test_cluster = TestClusterBuilder::new().build().await; + // let response = create_credential_and_sign_test_tx(&test_cluster, None, false, false).await; + // let mut modified_sig = response.user_sig_bytes.clone(); + // modified_sig[1] = 0x00; + // let sig = GenericSignature::PasskeyAuthenticator( + // PasskeyAuthenticator::new_for_testing( + // response.authenticator_data, + // response.client_data_json, + // Signature::from_bytes(&modified_sig).unwrap(), + // ) + // .unwrap(), + // ); + // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); + // let res = execute_tx(tx, &test_cluster).await; + // let err = res.unwrap_err(); + // assert_eq!( + // err, + // SuiError::InvalidSignature { + // error: "Fails to verify".to_string() + // } + // ); +} +#[sim_test] +async fn test_passkey_fails_to_verify_ephemeral_sig() { + // ephemeral sig is on an unrelated personal message +} + +#[sim_test] +async fn test_passkey_fails_to_verify_mismatch_max_epoch() { + // ephemeral sig is on an unrelated personal message +} + +#[sim_test] +async fn test_passkey_fails_wrong_author() { + // let test_cluster = TestClusterBuilder::new().build().await; + // // Modify sender that receives gas and construct test txn. + // let response = + // create_credential_and_sign_test_tx(&test_cluster, Some(SuiAddress::ZERO), false, false) + // .await; + // let sig = GenericSignature::PasskeyAuthenticator( + // PasskeyAuthenticator::new_for_testing( + // response.authenticator_data, + // response.client_data_json, + // Signature::from_bytes(&response.user_sig_bytes).unwrap(), + // ) + // .unwrap(), + // ); + // let tx = Transaction::from_generic_sig_data(response.intent_msg.value, vec![sig]); + // let res = execute_tx(tx, &test_cluster).await; + // let err = res.unwrap_err(); + // assert!(matches!(err, SuiError::SignerSignatureAbsent { .. })); +} diff --git a/crates/sui-e2e-tests/tests/passkey_util.rs b/crates/sui-e2e-tests/tests/passkey_util.rs new file mode 100644 index 00000000000000..2403103e9f042d --- /dev/null +++ b/crates/sui-e2e-tests/tests/passkey_util.rs @@ -0,0 +1,81 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use fastcrypto::traits::ToFromBytes; +use p256::pkcs8::DecodePublicKey; +use passkey_authenticator::{Authenticator, UserValidationMethod}; +use passkey_client::Client; +use passkey_types::{ + ctap2::Aaguid, + rand::random_vec, + webauthn::{ + AttestationConveyancePreference, CredentialCreationOptions, CredentialRequestOptions, + PublicKeyCredentialCreationOptions, PublicKeyCredentialParameters, + PublicKeyCredentialRequestOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, UserVerificationRequirement, + }, + Bytes, Passkey, +}; +use shared_crypto::intent::{Intent, IntentMessage}; +use std::net::SocketAddr; +use sui_core::authority_client::AuthorityAPI; +use sui_macros::sim_test; +use sui_test_transaction_builder::TestTransactionBuilder; +use sui_types::crypto::Signature; +use sui_types::error::UserInputError; +use sui_types::error::{SuiError, SuiResult}; +use sui_types::signature::GenericSignature; +use sui_types::transaction::Transaction; +use sui_types::{ + base_types::SuiAddress, + crypto::{PublicKey, SignatureScheme}, + passkey_authenticator::{to_signing_message, PasskeyAuthenticator}, + transaction::TransactionData, +}; +use test_cluster::TestCluster; +use test_cluster::TestClusterBuilder; +use url::Url; + +/// Helper struct to initialize passkey client. +pub struct MyUserValidationMethod {} +#[async_trait::async_trait] +impl UserValidationMethod for MyUserValidationMethod { + async fn check_user_presence(&self) -> bool { + true + } + + async fn check_user_verification(&self) -> bool { + true + } + + fn is_verification_enabled(&self) -> Option { + Some(true) + } + + fn is_presence_enabled(&self) -> bool { + true + } +} + +/// Response with fields from passkey authentication. +#[derive(Debug)] +pub struct PasskeyResponse { + pub user_sig_bytes: Vec, + pub authenticator_data: Vec, + pub client_data_json: String, + pub intent_msg: IntentMessage, + pub sender: SuiAddress, +} + +/// Submits a transaction to the test cluster and returns the result. +pub async fn execute_tx(tx: Transaction, test_cluster: &TestCluster) -> SuiResult { + test_cluster + .authority_aggregator() + .authority_clients + .values() + .next() + .unwrap() + .authority_client() + .handle_transaction(tx, Some(SocketAddr::new([127, 0, 0, 1].into(), 0))) + .await + .map(|_| ()) +} diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index 758e8320217930..6b7ca55750bc90 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -537,6 +537,10 @@ struct FeatureFlags { #[serde(skip_serializing_if = "is_false")] passkey_session_auth: bool, + // Set the upper bound allowed for max_epoch in passkey session signature. + #[serde(skip_serializing_if = "Option::is_none")] + passkey_session_max_epoch_upper_bound_delta: Option, + // Use AuthorityCapabilitiesV2 #[serde(skip_serializing_if = "is_false")] authority_capabilities_v2: bool, @@ -2976,6 +2980,10 @@ impl ProtocolConfig { if chain != Chain::Mainnet { cfg.feature_flags.uncompressed_g1_group_elements = true; } + + if chain != Chain::Testnet && chain != Chain::Mainnet { + cfg.feature_flags.passkey_session_auth = true; + } } // Use this template when making changes: // @@ -3130,6 +3138,18 @@ impl ProtocolConfig { self.feature_flags.passkey_auth = val } + pub fn set_passkey_session_auth_for_testing(&mut self, val: bool) { + self.feature_flags.passkey_session_auth = val + } + + pub fn set_passkey_session_max_epoch_upper_bound_delta_for_testing( + &mut self, + val: Option, + ) { + self.feature_flags + .passkey_session_max_epoch_upper_bound_delta = val + } + pub fn set_consensus_distributed_vote_scoring_strategy_for_testing(&mut self, val: bool) { self.feature_flags .consensus_distributed_vote_scoring_strategy = val; diff --git a/crates/sui-types/src/passkey_session_authenticator.rs b/crates/sui-types/src/passkey_session_authenticator.rs index d8c7a0cca5ec1e..d8852e4dbc9633 100644 --- a/crates/sui-types/src/passkey_session_authenticator.rs +++ b/crates/sui-types/src/passkey_session_authenticator.rs @@ -44,17 +44,17 @@ pub struct PasskeySessionAuthenticator { /// during the authentication request (see [CollectedClientData](https://www.w3.org/TR/webauthn-2/#dictdef-collectedclientdata)) client_data_json: String, - /// Normalized r1 signature returned by passkey. This signature commits to ephemral public key and max epoch. + /// Normalized r1 signature returned by passkey. This signature commits to ephemral public key and max epoch. /// Initialized from `register_signature` in `RawPasskeySessionAuthenticator`. #[serde(skip)] register_signature: Secp256r1Signature, - /// Compact r1 public key of the passkey. + /// Compact r1 public key of the passkey. /// Initialized from `register_signature` in `RawPasskeySessionAuthenticator`. #[serde(skip)] passkey_pk: Secp256r1PublicKey, - /// Ephemeral signature that commits to intent message of tx_data. + /// Ephemeral signature that commits to intent message of tx_data. ephemeral_signature: Signature, /// challenge field parsed from clientDataJSON. This should be `eph_flag || eph_pk || max_epoch`. @@ -109,8 +109,8 @@ impl TryFrom for PasskeySessionAuthenticator { let passkey_pk = Secp256r1PublicKey::from_bytes(raw.register_signature.public_key_bytes()) .map_err(|_| SuiError::InvalidSignature { - error: "Invalid r1 pk".to_string(), - })?; + error: "Invalid r1 pk".to_string(), + })?; let register_signature = Secp256r1Signature::from_bytes( raw.register_signature.signature_bytes(), @@ -169,25 +169,6 @@ impl Serialize for PasskeySessionAuthenticator { } } impl PasskeySessionAuthenticator { - /// A constructor for [struct PasskeySessionAuthenticator] with custom - /// defined fields. Used for testing. - pub fn new_for_testing( - authenticator_data: Vec, - client_data_json: String, - register_signature: Signature, - ephemeral_signature: Signature, - max_epoch: EpochId, - ) -> Result { - let raw = RawPasskeySessionAuthenticator { - authenticator_data, - client_data_json, - register_signature, - ephemeral_signature, - max_epoch, - }; - raw.try_into() - } - /// Returns the public key of the passkey authenticator. pub fn get_pk(&self) -> SuiResult { Ok(PublicKey::Passkey((&self.passkey_pk).into())) @@ -220,9 +201,8 @@ impl AuthenticatorTrait for PasskeySessionAuthenticator { // the checks here ensure that `current_epoch + passkey_session_max_epoch_upper_bound_delta >= self.max_epoch >= current_epoch`. // 1. if the config for upper bound is set, ensure that the max epoch in signature is not larger than epoch + upper_bound. if let Some(delta) = max_epoch_upper_bound_delta { - let max_epoch_upper_bound = epoch - .checked_add(delta) - .ok_or(SuiError::InvalidSignature { + let max_epoch_upper_bound = + epoch.checked_add(delta).ok_or(SuiError::InvalidSignature { error: "Max epoch upper bound delta overflow".to_string(), })?; if self.max_epoch > max_epoch_upper_bound { @@ -298,7 +278,7 @@ impl AuthenticatorTrait for PasskeySessionAuthenticator { self.passkey_pk .verify(&message, &self.register_signature) .map_err(|_| SuiError::InvalidSignature { - error: "Fails to verify passkey sig".to_string(), + error: "Fails to verify register sig".to_string(), }) } } @@ -330,4 +310,4 @@ impl AsRef<[u8]> for PasskeySessionAuthenticator { }) .expect("OnceCell invariant violated") } -} \ No newline at end of file +} diff --git a/crates/sui-types/src/unit_tests/passkey_session_authenticator_test.rs b/crates/sui-types/src/unit_tests/passkey_session_authenticator_test.rs index 9a6f3c4079da4f..53b177423e962e 100644 --- a/crates/sui-types/src/unit_tests/passkey_session_authenticator_test.rs +++ b/crates/sui-types/src/unit_tests/passkey_session_authenticator_test.rs @@ -1,10 +1,9 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - use crate::committee::EpochId; use crate::crypto::get_key_pair; +use crate::passkey_authenticator::passkey_authenticator_test::MyUserValidationMethod; use crate::passkey_session_authenticator::{ PasskeySessionAuthenticator, RawPasskeySessionAuthenticator, }; @@ -18,12 +17,13 @@ use crate::{ transaction::{TransactionData, TEST_ONLY_GAS_UNIT_FOR_TRANSFER}, }; use fastcrypto::ed25519::Ed25519KeyPair; +use std::sync::Arc; use fastcrypto::traits::KeyPair; use fastcrypto::traits::ToFromBytes; use p256::pkcs8::DecodePublicKey; -use crate::passkey_authenticator::passkey_authenticator_test::{make_credential_creation_option, MyUserValidationMethod}; +use crate::passkey_authenticator::passkey_authenticator_test::make_credential_creation_option; use passkey_authenticator::{Authenticator, UserValidationMethod}; use passkey_client::Client; use passkey_types::{ @@ -42,7 +42,6 @@ use rand::SeedableRng; use shared_crypto::intent::PersonalMessage; use shared_crypto::intent::{Intent, IntentMessage}; use url::Url; - /// Response with fields from passkey authentication. #[derive(Debug)] pub struct PasskeySessionResponse { @@ -378,4 +377,4 @@ async fn test_passkey_fails_wrong_client_data_type() { // error: "Fails to verify ephemeral sig".to_string() // } // ); -// } \ No newline at end of file +// }