diff --git a/Cargo.lock b/Cargo.lock index ff36772b951b8..024ea7cc12ddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15102,7 +15102,6 @@ dependencies = [ "aptos-vault-client", "aptos-vm", "aptos-vm-genesis", - "ark-ff", "async-trait", "base64 0.13.1", "bcs 0.1.4", diff --git a/aptos-move/aptos-release-builder/src/components/feature_flags.rs b/aptos-move/aptos-release-builder/src/components/feature_flags.rs index 7b68c67a4282d..efc8c03201f6f 100644 --- a/aptos-move/aptos-release-builder/src/components/feature_flags.rs +++ b/aptos-move/aptos-release-builder/src/components/feature_flags.rs @@ -104,6 +104,7 @@ pub enum FeatureFlag { RefundableBytes, ObjectCodeDeployment, MaxObjectNestingCheck, + KeylessAccountsWithPasskeys, } fn generate_features_blob(writer: &CodeWriter, data: &[u64]) { @@ -266,6 +267,9 @@ impl From for AptosFeatureFlag { FeatureFlag::RefundableBytes => AptosFeatureFlag::REFUNDABLE_BYTES, FeatureFlag::ObjectCodeDeployment => AptosFeatureFlag::OBJECT_CODE_DEPLOYMENT, FeatureFlag::MaxObjectNestingCheck => AptosFeatureFlag::MAX_OBJECT_NESTING_CHECK, + FeatureFlag::KeylessAccountsWithPasskeys => { + AptosFeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS + }, } } } @@ -351,6 +355,9 @@ impl From for FeatureFlag { AptosFeatureFlag::REFUNDABLE_BYTES => FeatureFlag::RefundableBytes, AptosFeatureFlag::OBJECT_CODE_DEPLOYMENT => FeatureFlag::ObjectCodeDeployment, AptosFeatureFlag::MAX_OBJECT_NESTING_CHECK => FeatureFlag::MaxObjectNestingCheck, + AptosFeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS => { + FeatureFlag::KeylessAccountsWithPasskeys + }, } } } diff --git a/aptos-move/aptos-vm/src/keyless_validation.rs b/aptos-move/aptos-vm/src/keyless_validation.rs index 8432e106223a2..81f8349e0c841 100644 --- a/aptos-move/aptos-vm/src/keyless_validation.rs +++ b/aptos-move/aptos-vm/src/keyless_validation.rs @@ -8,11 +8,11 @@ use aptos_types::{ invalid_signature, jwks::{jwk::JWK, PatchedJWKs}, keyless::{ - get_public_inputs_hash, Configuration, Groth16VerificationKey, KeylessPublicKey, - KeylessSignature, ZkpOrOpenIdSig, + get_public_inputs_hash, Configuration, EphemeralCertificate, Groth16VerificationKey, + KeylessPublicKey, KeylessSignature, ZKP, }, on_chain_config::{CurrentTimeMicroseconds, Features, OnChainConfig}, - transaction::authenticator::EphemeralPublicKey, + transaction::authenticator::{EphemeralPublicKey, EphemeralSignature}, vm_status::{StatusCode, VMStatus}, }; use move_binary_format::errors::Location; @@ -107,11 +107,16 @@ pub(crate) fn validate_authenticators( // Feature-gating for keyless-but-zkless TXNs: If keyless TXNs *are* enabled, and (1) this // is a ZKless transaction but (2) ZKless TXNs are not yet enabled, discard the TXN from // being put on-chain. - if matches!(sig.sig, ZkpOrOpenIdSig::OpenIdSig { .. }) + if matches!(sig.cert, EphemeralCertificate::OpenIdSig { .. }) && !features.is_keyless_zkless_enabled() { return Err(VMStatus::error(StatusCode::FEATURE_UNDER_GATING, None)); } + if matches!(sig.ephemeral_signature, EphemeralSignature::WebAuthn { .. }) + && !features.is_keyless_with_passkeys_enabled() + { + return Err(VMStatus::error(StatusCode::FEATURE_UNDER_GATING, None)); + } } let config = &get_configs_onchain(resolver)?; @@ -143,41 +148,49 @@ pub(crate) fn validate_authenticators( for (pk, sig) in authenticators { let jwk = get_jwk_for_authenticator(&patched_jwks, pk, sig)?; - match &sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => match jwk { + match &sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(zksig) => match jwk { JWK::RSA(rsa_jwk) => { - if proof.exp_horizon_secs > config.max_exp_horizon_secs { + if zksig.exp_horizon_secs > config.max_exp_horizon_secs { return Err(invalid_signature!("The expiration horizon is too long")); } // If an `aud` override was set for account recovery purposes, check that it is // in the allow-list on-chain. - if proof.override_aud_val.is_some() { - config.is_allowed_override_aud(proof.override_aud_val.as_ref().unwrap())?; + if zksig.override_aud_val.is_some() { + config.is_allowed_override_aud(zksig.override_aud_val.as_ref().unwrap())?; } - let public_inputs_hash = get_public_inputs_hash(sig, pk, &rsa_jwk, config) - .map_err(|_| invalid_signature!("Could not compute public inputs hash"))?; - - // The training wheels signature is only checked if a training wheels PK is set on chain - if training_wheels_pk.is_some() { - proof - .verify_training_wheels_sig( - training_wheels_pk.as_ref().unwrap(), - &public_inputs_hash, - ) - .map_err(|_| { - invalid_signature!("Could not verify training wheels signature") - })?; + match zksig.proof { + ZKP::Groth16(_) => { + let public_inputs_hash = + get_public_inputs_hash(sig, pk, &rsa_jwk, config).map_err( + |_| invalid_signature!("Could not compute public inputs hash"), + )?; + + // The training wheels signature is only checked if a training wheels PK is set on chain + if training_wheels_pk.is_some() { + zksig + .verify_training_wheels_sig( + training_wheels_pk.as_ref().unwrap(), + &public_inputs_hash, + ) + .map_err(|_| { + invalid_signature!( + "Could not verify training wheels signature" + ) + })?; + } + + zksig + .verify_groth16_proof(public_inputs_hash, pvk) + .map_err(|_| invalid_signature!("Proof verification failed"))?; + }, } - - proof - .verify_proof(public_inputs_hash, pvk) - .map_err(|_| invalid_signature!("Proof verification failed"))?; }, JWK::Unsupported(_) => return Err(invalid_signature!("JWK is not supported")), }, - ZkpOrOpenIdSig::OpenIdSig(openid_sig) => { + EphemeralCertificate::OpenIdSig(openid_sig) => { match jwk { JWK::RSA(rsa_jwk) => { openid_sig diff --git a/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs b/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs index 0aea703c64ede..cd83e53decb2c 100644 --- a/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs +++ b/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs @@ -10,7 +10,8 @@ use aptos_types::{ get_sample_esk, get_sample_groth16_sig_and_pk, get_sample_iss, get_sample_jwk, get_sample_openid_sig_and_pk, }, - Configuration, KeylessPublicKey, KeylessSignature, ZkpOrOpenIdSig, + Configuration, EphemeralCertificate, KeylessPublicKey, KeylessSignature, + TransactionAndProof, }, on_chain_config::FeatureFlag, transaction::{ @@ -129,17 +130,22 @@ fn get_keyless_txn( println!("RawTxn sender: {:?}", raw_txn.sender()); + let mut txn_and_zkp = TransactionAndProof { + message: raw_txn.clone(), + proof: None, + }; let esk = get_sample_esk(); - sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); // Compute the training wheels signature if not present - match &mut sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => { + match &mut sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(proof) => { // Training wheels should be disabled. - proof.training_wheels_signature = None + proof.training_wheels_signature = None; + txn_and_zkp.proof = Some(proof.proof); }, - ZkpOrOpenIdSig::OpenIdSig(_) => {}, + EphemeralCertificate::OpenIdSig(_) => {}, } + sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&txn_and_zkp).unwrap()); let transaction = SignedTransaction::new_keyless(raw_txn, pk, sig); println!( diff --git a/aptos-move/framework/move-stdlib/doc/features.md b/aptos-move/framework/move-stdlib/doc/features.md index cc4939af38ca8..c9373cc71ccc4 100644 --- a/aptos-move/framework/move-stdlib/doc/features.md +++ b/aptos-move/framework/move-stdlib/doc/features.md @@ -100,6 +100,8 @@ return true. - [Function `is_object_code_deployment_enabled`](#0x1_features_is_object_code_deployment_enabled) - [Function `get_max_object_nesting_check_feature`](#0x1_features_get_max_object_nesting_check_feature) - [Function `max_object_nesting_check_enabled`](#0x1_features_max_object_nesting_check_enabled) +- [Function `get_keyless_accounts_with_passkeys_feature`](#0x1_features_get_keyless_accounts_with_passkeys_feature) +- [Function `keyless_accounts_with_passkeys_feature_enabled`](#0x1_features_keyless_accounts_with_passkeys_feature_enabled) - [Function `change_feature_flags`](#0x1_features_change_feature_flags) - [Function `change_feature_flags_for_next_epoch`](#0x1_features_change_feature_flags_for_next_epoch) - [Function `on_new_epoch`](#0x1_features_on_new_epoch) @@ -464,6 +466,18 @@ Lifetime: transient + + +Whether keyless accounts support passkey-based ephemeral signatures. + +Lifetime: transient + + +
const KEYLESS_ACCOUNTS_WITH_PASSKEYS: u64 = 54;
+
+ + + Whether the ZK-less mode of the keyless accounts feature is enabled. @@ -2296,6 +2310,52 @@ Lifetime: transient + + + + +## Function `get_keyless_accounts_with_passkeys_feature` + + + +
public fun get_keyless_accounts_with_passkeys_feature(): u64
+
+ + + +
+Implementation + + +
public fun get_keyless_accounts_with_passkeys_feature(): u64 { KEYLESS_ACCOUNTS_WITH_PASSKEYS }
+
+ + + +
+ + + +## Function `keyless_accounts_with_passkeys_feature_enabled` + + + +
public fun keyless_accounts_with_passkeys_feature_enabled(): bool
+
+ + + +
+Implementation + + +
public fun keyless_accounts_with_passkeys_feature_enabled(): bool acquires Features {
+    is_enabled(KEYLESS_ACCOUNTS_WITH_PASSKEYS)
+}
+
+ + +
diff --git a/aptos-move/framework/move-stdlib/sources/configs/features.move b/aptos-move/framework/move-stdlib/sources/configs/features.move index 4aa5af637774f..dacd668ca3342 100644 --- a/aptos-move/framework/move-stdlib/sources/configs/features.move +++ b/aptos-move/framework/move-stdlib/sources/configs/features.move @@ -417,6 +417,17 @@ module std::features { is_enabled(MAX_OBJECT_NESTING_CHECK) } + /// Whether keyless accounts support passkey-based ephemeral signatures. + /// + /// Lifetime: transient + const KEYLESS_ACCOUNTS_WITH_PASSKEYS: u64 = 54; + + public fun get_keyless_accounts_with_passkeys_feature(): u64 { KEYLESS_ACCOUNTS_WITH_PASSKEYS } + + public fun keyless_accounts_with_passkeys_feature_enabled(): bool acquires Features { + is_enabled(KEYLESS_ACCOUNTS_WITH_PASSKEYS) + } + // ============================================================================================ // Feature Flag Implementation diff --git a/crates/aptos-crypto/src/poseidon_bn254.rs b/crates/aptos-crypto/src/poseidon_bn254.rs index e8c900c50a9d4..2524b36453ef7 100644 --- a/crates/aptos-crypto/src/poseidon_bn254.rs +++ b/crates/aptos-crypto/src/poseidon_bn254.rs @@ -3,7 +3,7 @@ //! Implements the Poseidon hash function for BN-254, which hashes $\le$ 16 field elements and //! produces a single field element as output. use anyhow::bail; -use ark_ff::PrimeField; +use ark_ff::{BigInteger, PrimeField}; use once_cell::sync::Lazy; // TODO(keyless): Figure out the right library for Poseidon. use poseidon_ark::Poseidon; @@ -206,6 +206,14 @@ pub fn pack_bytes_to_one_scalar(chunk: &[u8]) -> anyhow::Result { Ok(fr) } +/// Utility method to convert an Fr to a 32-byte slice. +pub fn fr_to_bytes_le(fr: &ark_bn254::Fr) -> [u8; 32] { + fr.into_bigint() + .to_bytes_le() + .try_into() + .expect("expected 32-byte public inputs hash") +} + #[cfg(test)] mod test { use crate::{ diff --git a/protos/typescript/src/aptos/remote_executor/v1/network_msg.ts b/protos/typescript/src/aptos/remote_executor/v1/network_msg.ts index 513962a346e1a..7a04f56e2e30e 100644 --- a/protos/typescript/src/aptos/remote_executor/v1/network_msg.ts +++ b/protos/typescript/src/aptos/remote_executor/v1/network_msg.ts @@ -244,7 +244,7 @@ export const NetworkMessageServiceClient = makeGenericClientConstructor( }; function bytesFromBase64(b64: string): Uint8Array { - if (globalThis.Buffer) { + if ((globalThis as any).Buffer) { return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); } else { const bin = globalThis.atob(b64); @@ -257,7 +257,7 @@ function bytesFromBase64(b64: string): Uint8Array { } function base64FromBytes(arr: Uint8Array): string { - if (globalThis.Buffer) { + if ((globalThis as any).Buffer) { return globalThis.Buffer.from(arr).toString("base64"); } else { const bin: string[] = []; diff --git a/protos/typescript/src/aptos/transaction/v1/transaction.ts b/protos/typescript/src/aptos/transaction/v1/transaction.ts index 06a477568d3ee..1666ba1f79ba7 100644 --- a/protos/typescript/src/aptos/transaction/v1/transaction.ts +++ b/protos/typescript/src/aptos/transaction/v1/transaction.ts @@ -9237,7 +9237,7 @@ export const WriteOpSizeInfo = { }; function bytesFromBase64(b64: string): Uint8Array { - if (globalThis.Buffer) { + if ((globalThis as any).Buffer) { return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); } else { const bin = globalThis.atob(b64); @@ -9250,7 +9250,7 @@ function bytesFromBase64(b64: string): Uint8Array { } function base64FromBytes(arr: Uint8Array): string { - if (globalThis.Buffer) { + if ((globalThis as any).Buffer) { return globalThis.Buffer.from(arr).toString("base64"); } else { const bin: string[] = []; diff --git a/testsuite/generate-format/src/api.rs b/testsuite/generate-format/src/api.rs index bff2b61b72505..6d22991341573 100644 --- a/testsuite/generate-format/src/api.rs +++ b/testsuite/generate-format/src/api.rs @@ -116,7 +116,7 @@ pub fn get_registry() -> Result { tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; - tracer.trace_type::(&samples)?; + tracer.trace_type::(&samples)?; // events tracer.trace_type::(&samples)?; diff --git a/testsuite/generate-format/src/aptos.rs b/testsuite/generate-format/src/aptos.rs index 07d553721b5e8..3d0d4800cca2a 100644 --- a/testsuite/generate-format/src/aptos.rs +++ b/testsuite/generate-format/src/aptos.rs @@ -110,7 +110,7 @@ pub fn get_registry() -> Result { tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; - tracer.trace_type::(&samples)?; + tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; // aliases within StructTag diff --git a/testsuite/generate-format/src/consensus.rs b/testsuite/generate-format/src/consensus.rs index 05bb6d73a23b6..0a36c8fc9f099 100644 --- a/testsuite/generate-format/src/consensus.rs +++ b/testsuite/generate-format/src/consensus.rs @@ -106,7 +106,7 @@ pub fn get_registry() -> Result { tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; - tracer.trace_type::(&samples)?; + tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; tracer.trace_type::(&samples)?; diff --git a/testsuite/generate-format/src/lib.rs b/testsuite/generate-format/src/lib.rs index 96d72f5e9406c..a2e2add7c3faa 100644 --- a/testsuite/generate-format/src/lib.rs +++ b/testsuite/generate-format/src/lib.rs @@ -8,7 +8,7 @@ use aptos_crypto::ed25519::{Ed25519PublicKey, Ed25519Signature}; use aptos_types::{ keyless, - keyless::{Groth16Zkp, IdCommitment, Pepper, SignedGroth16Zkp, ZkpOrOpenIdSig}, + keyless::{EphemeralCertificate, Groth16Proof, IdCommitment, Pepper, ZeroKnowledgeSig}, transaction::authenticator::{EphemeralPublicKey, EphemeralSignature}, }; use clap::{Parser, ValueEnum}; @@ -93,11 +93,8 @@ pub(crate) fn trace_keyless_structs( idc: IdCommitment::new_from_preimage(&Pepper::from_number(2), "", "", "").unwrap(), }; let keyless_signature = keyless::KeylessSignature { - sig: ZkpOrOpenIdSig::Groth16Zkp(SignedGroth16Zkp { - proof: Groth16Zkp::dummy_proof(), - non_malleability_signature: EphemeralSignature::Ed25519 { - signature: signature.clone(), - }, + cert: EphemeralCertificate::ZeroKnowledgeSig(ZeroKnowledgeSig { + proof: Groth16Proof::dummy_proof().into(), exp_horizon_secs: 0, extra_field: None, override_aud_val: None, diff --git a/testsuite/generate-format/tests/staged/api.yaml b/testsuite/generate-format/tests/staged/api.yaml index 707d26a9114ad..224ed4788f26f 100644 --- a/testsuite/generate-format/tests/staged/api.yaml +++ b/testsuite/generate-format/tests/staged/api.yaml @@ -220,6 +220,16 @@ EntryFunction: TYPENAME: TypeTag - args: SEQ: BYTES +EphemeralCertificate: + ENUM: + 0: + ZeroKnowledgeSig: + NEWTYPE: + TYPENAME: ZeroKnowledgeSig + 1: + OpenIdSig: + NEWTYPE: + TYPENAME: OpenIdSig EphemeralPublicKey: ENUM: 0: @@ -234,6 +244,11 @@ EphemeralSignature: STRUCT: - signature: TYPENAME: Ed25519Signature + 1: + WebAuthn: + STRUCT: + - signature: + TYPENAME: PartialAuthenticatorAssertionResponse EventHandle: STRUCT: - count: U64 @@ -280,7 +295,7 @@ G2Bytes: TUPLEARRAY: CONTENT: U8 SIZE: 64 -Groth16Zkp: +Groth16Proof: STRUCT: - a: TYPENAME: G1Bytes @@ -309,8 +324,8 @@ KeylessPublicKey: TYPENAME: IdCommitment KeylessSignature: STRUCT: - - sig: - TYPENAME: ZkpOrOpenIdSig + - cert: + TYPENAME: EphemeralCertificate - jwt_header_json: STR - exp_date_secs: U64 - ephemeral_pubkey: @@ -458,20 +473,6 @@ Secp256r1EcdsaSignature: NEWTYPESTRUCT: BYTES Signature: NEWTYPESTRUCT: BYTES -SignedGroth16Zkp: - STRUCT: - - proof: - TYPENAME: Groth16Zkp - - non_malleability_signature: - TYPENAME: EphemeralSignature - - exp_horizon_secs: U64 - - extra_field: - OPTION: STR - - override_aud_val: - OPTION: STR - - training_wheels_signature: - OPTION: - TYPENAME: EphemeralSignature SignedTransaction: STRUCT: - raw_txn: @@ -795,13 +796,21 @@ WriteSetPayload: WriteSetV0: NEWTYPESTRUCT: TYPENAME: WriteSetMut -ZkpOrOpenIdSig: +ZKP: ENUM: 0: - Groth16Zkp: + Groth16: NEWTYPE: - TYPENAME: SignedGroth16Zkp - 1: - OpenIdSig: - NEWTYPE: - TYPENAME: OpenIdSig + TYPENAME: Groth16Proof +ZeroKnowledgeSig: + STRUCT: + - proof: + TYPENAME: ZKP + - exp_horizon_secs: U64 + - extra_field: + OPTION: STR + - override_aud_val: + OPTION: STR + - training_wheels_signature: + OPTION: + TYPENAME: EphemeralSignature diff --git a/testsuite/generate-format/tests/staged/aptos.yaml b/testsuite/generate-format/tests/staged/aptos.yaml index b5ae9ee40f026..209dfd54dfbec 100644 --- a/testsuite/generate-format/tests/staged/aptos.yaml +++ b/testsuite/generate-format/tests/staged/aptos.yaml @@ -197,6 +197,16 @@ EntryFunction: TYPENAME: TypeTag - args: SEQ: BYTES +EphemeralCertificate: + ENUM: + 0: + ZeroKnowledgeSig: + NEWTYPE: + TYPENAME: ZeroKnowledgeSig + 1: + OpenIdSig: + NEWTYPE: + TYPENAME: OpenIdSig EphemeralPublicKey: ENUM: 0: @@ -211,6 +221,11 @@ EphemeralSignature: STRUCT: - signature: TYPENAME: Ed25519Signature + 1: + WebAuthn: + STRUCT: + - signature: + TYPENAME: PartialAuthenticatorAssertionResponse EventKey: STRUCT: - creation_number: U64 @@ -226,7 +241,7 @@ G2Bytes: TUPLEARRAY: CONTENT: U8 SIZE: 64 -Groth16Zkp: +Groth16Proof: STRUCT: - a: TYPENAME: G1Bytes @@ -255,8 +270,8 @@ KeylessPublicKey: TYPENAME: IdCommitment KeylessSignature: STRUCT: - - sig: - TYPENAME: ZkpOrOpenIdSig + - cert: + TYPENAME: EphemeralCertificate - jwt_header_json: STR - exp_date_secs: U64 - ephemeral_pubkey: @@ -390,20 +405,6 @@ Secp256r1EcdsaSignature: NEWTYPESTRUCT: BYTES Signature: NEWTYPESTRUCT: BYTES -SignedGroth16Zkp: - STRUCT: - - proof: - TYPENAME: Groth16Zkp - - non_malleability_signature: - TYPENAME: EphemeralSignature - - exp_horizon_secs: U64 - - extra_field: - OPTION: STR - - override_aud_val: - OPTION: STR - - training_wheels_signature: - OPTION: - TYPENAME: EphemeralSignature SignedTransaction: STRUCT: - raw_txn: @@ -677,13 +678,21 @@ WriteSetPayload: WriteSetV0: NEWTYPESTRUCT: TYPENAME: WriteSetMut -ZkpOrOpenIdSig: +ZKP: ENUM: 0: - Groth16Zkp: + Groth16: NEWTYPE: - TYPENAME: SignedGroth16Zkp - 1: - OpenIdSig: - NEWTYPE: - TYPENAME: OpenIdSig + TYPENAME: Groth16Proof +ZeroKnowledgeSig: + STRUCT: + - proof: + TYPENAME: ZKP + - exp_horizon_secs: U64 + - extra_field: + OPTION: STR + - override_aud_val: + OPTION: STR + - training_wheels_signature: + OPTION: + TYPENAME: EphemeralSignature diff --git a/testsuite/generate-format/tests/staged/consensus.yaml b/testsuite/generate-format/tests/staged/consensus.yaml index 1645582e14e8e..1817ad8b686f4 100644 --- a/testsuite/generate-format/tests/staged/consensus.yaml +++ b/testsuite/generate-format/tests/staged/consensus.yaml @@ -442,6 +442,16 @@ EntryFunction: TYPENAME: TypeTag - args: SEQ: BYTES +EphemeralCertificate: + ENUM: + 0: + ZeroKnowledgeSig: + NEWTYPE: + TYPENAME: ZeroKnowledgeSig + 1: + OpenIdSig: + NEWTYPE: + TYPENAME: OpenIdSig EphemeralPublicKey: ENUM: 0: @@ -456,6 +466,11 @@ EphemeralSignature: STRUCT: - signature: TYPENAME: Ed25519Signature + 1: + WebAuthn: + STRUCT: + - signature: + TYPENAME: PartialAuthenticatorAssertionResponse EpochChangeProof: STRUCT: - ledger_info_with_sigs: @@ -486,7 +501,7 @@ G2Bytes: TUPLEARRAY: CONTENT: U8 SIZE: 64 -Groth16Zkp: +Groth16Proof: STRUCT: - a: TYPENAME: G1Bytes @@ -515,8 +530,8 @@ KeylessPublicKey: TYPENAME: IdCommitment KeylessSignature: STRUCT: - - sig: - TYPENAME: ZkpOrOpenIdSig + - cert: + TYPENAME: EphemeralCertificate - jwt_header_json: STR - exp_date_secs: U64 - ephemeral_pubkey: @@ -749,20 +764,6 @@ SignedBatchInfoMsg: - signed_infos: SEQ: TYPENAME: SignedBatchInfo -SignedGroth16Zkp: - STRUCT: - - proof: - TYPENAME: Groth16Zkp - - non_malleability_signature: - TYPENAME: EphemeralSignature - - exp_horizon_secs: U64 - - extra_field: - OPTION: STR - - override_aud_val: - OPTION: STR - - training_wheels_signature: - OPTION: - TYPENAME: EphemeralSignature SignedTransaction: STRUCT: - raw_txn: @@ -1099,13 +1100,21 @@ WriteSetPayload: WriteSetV0: NEWTYPESTRUCT: TYPENAME: WriteSetMut -ZkpOrOpenIdSig: +ZKP: ENUM: 0: - Groth16Zkp: + Groth16: NEWTYPE: - TYPENAME: SignedGroth16Zkp - 1: - OpenIdSig: - NEWTYPE: - TYPENAME: OpenIdSig + TYPENAME: Groth16Proof +ZeroKnowledgeSig: + STRUCT: + - proof: + TYPENAME: ZKP + - exp_horizon_secs: U64 + - extra_field: + OPTION: STR + - override_aud_val: + OPTION: STR + - training_wheels_signature: + OPTION: + TYPENAME: EphemeralSignature diff --git a/testsuite/smoke-test/Cargo.toml b/testsuite/smoke-test/Cargo.toml index dacc1e882037c..501e639fa553b 100644 --- a/testsuite/smoke-test/Cargo.toml +++ b/testsuite/smoke-test/Cargo.toml @@ -42,7 +42,6 @@ aptos-temppath = { workspace = true } aptos-types = { workspace = true } aptos-vm = { workspace = true } aptos-vm-genesis = { workspace = true } -ark-ff = { workspace = true } async-trait = { workspace = true } bcs = { workspace = true } diesel = { workspace = true, features = [ diff --git a/testsuite/smoke-test/src/keyless.rs b/testsuite/smoke-test/src/keyless.rs index 2a482216685bd..84513df225af6 100644 --- a/testsuite/smoke-test/src/keyless.rs +++ b/testsuite/smoke-test/src/keyless.rs @@ -5,6 +5,7 @@ use aptos::test::CliTestFramework; use aptos_cached_packages::aptos_stdlib; use aptos_crypto::{ ed25519::{Ed25519PrivateKey, Ed25519PublicKey}, + poseidon_bn254::fr_to_bytes_le, SigningKey, Uniform, }; use aptos_forge::{AptosPublicInfo, LocalSwarm, NodeExt, Swarm, SwarmExt}; @@ -17,24 +18,25 @@ use aptos_types::{ AllProvidersJWKs, PatchedJWKs, ProviderJWKs, }, keyless::{ - get_public_inputs_hash, + get_public_inputs_hash, test_utils, test_utils::{ get_sample_esk, get_sample_groth16_sig_and_pk, get_sample_iss, get_sample_jwk, get_sample_openid_sig_and_pk, }, - Configuration, Groth16VerificationKey, Groth16ZkpAndStatement, KeylessPublicKey, - KeylessSignature, ZkpOrOpenIdSig, KEYLESS_ACCOUNT_MODULE_NAME, + Configuration, EphemeralCertificate, Groth16ProofAndStatement, Groth16VerificationKey, + KeylessPublicKey, KeylessSignature, TransactionAndProof, KEYLESS_ACCOUNT_MODULE_NAME, }, transaction::{ - authenticator::{AnyPublicKey, EphemeralSignature}, + authenticator::{ + AccountAuthenticator, AnyPublicKey, AnySignature, EphemeralSignature, + TransactionAuthenticator, + }, SignedTransaction, }, }; -use ark_ff::{BigInteger, PrimeField}; use move_core_types::account_address::AccountAddress; use rand::thread_rng; use std::time::Duration; - // TODO(keyless): Test the override aud_val path #[tokio::test] @@ -58,9 +60,9 @@ async fn test_keyless_oidc_txn_with_bad_jwt_sig() { let (tw_sk, config, jwk, mut swarm) = setup_local_net().await; let (mut sig, pk) = get_sample_openid_sig_and_pk(); - match &mut sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(_) => panic!("Internal inconsistency"), - ZkpOrOpenIdSig::OpenIdSig(openid_sig) => { + match &mut sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(_) => panic!("Internal inconsistency"), + EphemeralCertificate::OpenIdSig(openid_sig) => { openid_sig.jwt_sig = vec![0u8; 16] // Mauling the signature }, } @@ -119,18 +121,11 @@ async fn test_keyless_groth16_verifies() { #[tokio::test] async fn test_keyless_groth16_with_mauled_proof() { let (tw_sk, config, jwk, mut swarm) = setup_local_net().await; - let (mut sig, pk) = get_sample_groth16_sig_and_pk(); - - match &mut sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => { - proof.non_malleability_signature = - EphemeralSignature::ed25519(tw_sk.sign(&proof.proof).unwrap()); // bad signature using the TW SK rather than the ESK - }, - ZkpOrOpenIdSig::OpenIdSig(_) => panic!("Internal inconsistency"), - } + let (sig, pk) = get_sample_groth16_sig_and_pk(); let mut info = swarm.aptos_public_info(); let signed_txn = sign_transaction(&mut info, sig, pk, &jwk, &config, &tw_sk).await; + let signed_txn = maul_groth16_zkp_signature(signed_txn); info!("Submit keyless Groth16 transaction"); let result = info @@ -193,40 +188,64 @@ async fn sign_transaction<'a>( .build(); let esk = get_sample_esk(); - sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); - - let public_inputs_hash: Option<[u8; 32]> = if let ZkpOrOpenIdSig::Groth16Zkp(_) = &sig.sig { - // This will only calculate the hash if it's needed, avoiding unnecessary computation. - Some( - get_public_inputs_hash(&sig, &pk, jwk, config) - .unwrap() - .into_bigint() - .to_bytes_le() - .try_into() - .expect("expected 32-byte public inputs hash"), - ) - } else { - None + + let public_inputs_hash: Option<[u8; 32]> = + if let EphemeralCertificate::ZeroKnowledgeSig(_) = &sig.cert { + // This will only calculate the hash if it's needed, avoiding unnecessary computation. + Some(fr_to_bytes_le( + &get_public_inputs_hash(&sig, &pk, jwk, config).unwrap(), + )) + } else { + None + }; + + let mut txn_and_zkp = TransactionAndProof { + message: raw_txn.clone(), + proof: None, }; // Compute the training wheels signature if not present - match &mut sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => { - let proof_and_statement = Groth16ZkpAndStatement { - proof: proof.proof, + match &mut sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(proof) => { + let proof_and_statement = Groth16ProofAndStatement { + proof: proof.proof.into(), public_inputs_hash: public_inputs_hash.unwrap(), }; proof.training_wheels_signature = Some(EphemeralSignature::ed25519( tw_sk.sign(&proof_and_statement).unwrap(), )); + + txn_and_zkp.proof = Some(proof.proof); }, - ZkpOrOpenIdSig::OpenIdSig(_) => {}, + EphemeralCertificate::OpenIdSig(_) => {}, } + sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&txn_and_zkp).unwrap()); + SignedTransaction::new_keyless(raw_txn, pk, sig) } +fn maul_groth16_zkp_signature(txn: SignedTransaction) -> SignedTransaction { + // extract the keyless PK and signature + let (pk, sig) = match txn.authenticator() { + TransactionAuthenticator::SingleSender { + sender: AccountAuthenticator::SingleKey { authenticator }, + } => match (authenticator.public_key(), authenticator.signature()) { + (AnyPublicKey::Keyless { public_key }, AnySignature::Keyless { signature }) => { + (public_key.clone(), signature.clone()) + }, + _ => panic!("Expected keyless authenticator"), + }, + _ => panic!("Expected keyless authenticator"), + }; + + // disassemble the txn + let raw_txn = txn.into_raw_transaction(); + + test_utils::maul_raw_groth16_txn(pk, sig, raw_txn) +} + async fn get_transaction( get_pk_and_sig_func: fn() -> (KeylessSignature, KeylessPublicKey), ) -> ( diff --git a/types/src/keyless/bn254_circom.rs b/types/src/keyless/bn254_circom.rs index 33c2b81666f91..f780ab2ba6c5d 100644 --- a/types/src/keyless/bn254_circom.rs +++ b/types/src/keyless/bn254_circom.rs @@ -3,8 +3,8 @@ use crate::{ jwks::rsa::RSA_JWK, keyless::{ - base64url_encode_str, Configuration, IdCommitment, KeylessPublicKey, KeylessSignature, - ZkpOrOpenIdSig, + base64url_encode_str, Configuration, EphemeralCertificate, IdCommitment, KeylessPublicKey, + KeylessSignature, }, serialize, }; @@ -238,7 +238,7 @@ pub fn get_public_inputs_hash( jwk: &RSA_JWK, config: &Configuration, ) -> anyhow::Result { - if let ZkpOrOpenIdSig::Groth16Zkp(proof) = &sig.sig { + if let EphemeralCertificate::ZeroKnowledgeSig(proof) = &sig.cert { let (has_extra_field, extra_field_hash) = match &proof.extra_field { None => (Fr::zero(), Fr::zero()), Some(extra_field) => ( diff --git a/types/src/keyless/circuit_testcases.rs b/types/src/keyless/circuit_testcases.rs index a3f62b43d1a11..220ffd34df237 100644 --- a/types/src/keyless/circuit_testcases.rs +++ b/types/src/keyless/circuit_testcases.rs @@ -8,7 +8,7 @@ use crate::{ keyless::{ base64url_encode_str, bn254_circom::{G1Bytes, G2Bytes}, - Claims, Configuration, Groth16Zkp, IdCommitment, KeylessPublicKey, OpenIdSig, Pepper, + Claims, Configuration, Groth16Proof, IdCommitment, KeylessPublicKey, OpenIdSig, Pepper, }, transaction::authenticator::EphemeralPublicKey, }; @@ -147,8 +147,8 @@ pub(crate) static SAMPLE_PK: Lazy = Lazy::new(|| { /// - no override aud /// - the extra field enabled /// https://github.com/aptos-labs/devnet-groth16-keys/commit/02e5675f46ce97f8b61a4638e7a0aaeaa4351f76 -pub(crate) static SAMPLE_PROOF: Lazy = Lazy::new(|| { - Groth16Zkp::new( +pub(crate) static SAMPLE_PROOF: Lazy = Lazy::new(|| { + Groth16Proof::new( G1Bytes::new_unchecked( "4470668953498815291118813694625852066171551105654596174374858885226578750734", "14788589714058859505243017007182544407755183524390103786436121684254044340756", diff --git a/types/src/keyless/groth16_sig.rs b/types/src/keyless/groth16_sig.rs index 26171e0a5180a..e26721662ea8a 100644 --- a/types/src/keyless/groth16_sig.rs +++ b/types/src/keyless/groth16_sig.rs @@ -1,8 +1,12 @@ // Copyright © Aptos Foundation use crate::{ - keyless::bn254_circom::{ - G1Bytes, G2Bytes, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, G2_PROJECTIVE_COMPRESSED_NUM_BYTES, + keyless::{ + bn254_circom::{ + G1Bytes, G2Bytes, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, + G2_PROJECTIVE_COMPRESSED_NUM_BYTES, + }, + zkp_sig::ZKP, }, transaction::authenticator::{EphemeralPublicKey, EphemeralSignature}, }; @@ -19,17 +23,15 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[derive( Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, CryptoHasher, BCSCryptoHash, )] -pub struct Groth16Zkp { +pub struct Groth16Proof { a: G1Bytes, b: G2Bytes, c: G1Bytes, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub struct SignedGroth16Zkp { - pub proof: Groth16Zkp, - /// A signature on the proof (via the ephemeral SK) to prevent malleability attacks. - pub non_malleability_signature: EphemeralSignature, +pub struct ZeroKnowledgeSig { + pub proof: ZKP, /// The expiration horizon that the circuit should enforce on the expiration date committed in /// the nonce. This must be <= `Configuration::max_expiration_horizon_secs`. pub exp_horizon_secs: u64, @@ -48,13 +50,13 @@ pub struct SignedGroth16Zkp { /// prover service can sign them together. It is only used during signature verification & never /// sent over the network. #[derive(Debug, CryptoHasher, BCSCryptoHash, PartialEq, Eq)] -pub struct Groth16ZkpAndStatement { - pub proof: Groth16Zkp, +pub struct Groth16ProofAndStatement { + pub proof: Groth16Proof, // TODO(keyless): implement Serialize/Deserialize for Fr and use Fr here directly pub public_inputs_hash: [u8; 32], } -impl<'de> Deserialize<'de> for Groth16ZkpAndStatement { +impl<'de> Deserialize<'de> for Groth16ProofAndStatement { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, @@ -67,15 +69,15 @@ impl<'de> Deserialize<'de> for Groth16ZkpAndStatement { // In this case, we use the serde(with = "hex") macro, which causes public_inputs_hash // to deserialize from a hex string. #[derive(::serde::Deserialize)] - #[serde(rename = "Groth16ZkpAndStatement")] + #[serde(rename = "Groth16ProofAndStatement")] struct Value { - pub proof: Groth16Zkp, + pub proof: Groth16Proof, #[serde(with = "hex")] pub public_inputs_hash: [u8; 32], } let value = Value::deserialize(deserializer)?; - Ok(Groth16ZkpAndStatement { + Ok(Groth16ProofAndStatement { proof: value.proof, public_inputs_hash: value.public_inputs_hash, }) @@ -83,14 +85,14 @@ impl<'de> Deserialize<'de> for Groth16ZkpAndStatement { // Same as above, except this time we don't use the serde(with = "hex") macro, so that // serde uses default behavior for serialization. #[derive(::serde::Deserialize)] - #[serde(rename = "Groth16ZkpAndStatement")] + #[serde(rename = "Groth16ProofAndStatement")] struct Value { - pub proof: Groth16Zkp, + pub proof: Groth16Proof, pub public_inputs_hash: [u8; 32], } let value = Value::deserialize(deserializer)?; - Ok(Groth16ZkpAndStatement { + Ok(Groth16ProofAndStatement { proof: value.proof, public_inputs_hash: value.public_inputs_hash, }) @@ -98,16 +100,16 @@ impl<'de> Deserialize<'de> for Groth16ZkpAndStatement { } } -impl Serialize for Groth16ZkpAndStatement { +impl Serialize for Groth16ProofAndStatement { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { if serializer.is_human_readable() { #[derive(::serde::Serialize)] - #[serde(rename = "Groth16ZkpAndStatement")] + #[serde(rename = "Groth16ProofAndStatement")] struct Value { - pub proof: Groth16Zkp, + pub proof: Groth16Proof, #[serde(with = "hex")] pub public_inputs_hash: [u8; 32], } @@ -120,9 +122,9 @@ impl Serialize for Groth16ZkpAndStatement { value.serialize(serializer) } else { #[derive(::serde::Serialize)] - #[serde(rename = "Groth16ZkpAndStatement")] + #[serde(rename = "Groth16ProofAndStatement")] struct Value { - pub proof: Groth16Zkp, + pub proof: Groth16Proof, pub public_inputs_hash: [u8; 32], } @@ -136,11 +138,7 @@ impl Serialize for Groth16ZkpAndStatement { } } -impl SignedGroth16Zkp { - pub fn verify_non_malleability_sig(&self, pub_key: &EphemeralPublicKey) -> anyhow::Result<()> { - self.non_malleability_signature.verify(&self.proof, pub_key) - } - +impl ZeroKnowledgeSig { pub fn verify_training_wheels_sig( &self, pub_key: &EphemeralPublicKey, @@ -154,8 +152,10 @@ impl SignedGroth16Zkp { .map_err(|_| anyhow!("expected 32-byte public inputs hash"))?; // TODO(keyless): unnecessary cloning here; requires refactoring of our CryptoHasher trait which requires Deserialize to be implemented - let proof_and_statement = Groth16ZkpAndStatement { - proof: self.proof, + let proof_and_statement = Groth16ProofAndStatement { + proof: match self.proof { + ZKP::Groth16(proof) => proof, + }, public_inputs_hash, }; @@ -165,43 +165,46 @@ impl SignedGroth16Zkp { } } - pub fn verify_proof( + pub fn verify_groth16_proof( &self, public_inputs_hash: Fr, pvk: &PreparedVerifyingKey, ) -> anyhow::Result<()> { - self.proof.verify_proof(public_inputs_hash, pvk) + match self.proof { + ZKP::Groth16(proof) => proof.verify_proof(public_inputs_hash, pvk), + } } } -impl TryFrom<&[u8]> for Groth16Zkp { +impl TryFrom<&[u8]> for Groth16Proof { type Error = CryptoMaterialError; fn try_from(bytes: &[u8]) -> Result { - bcs::from_bytes::(bytes).map_err(|_e| CryptoMaterialError::DeserializationError) + bcs::from_bytes::(bytes) + .map_err(|_e| CryptoMaterialError::DeserializationError) } } -impl Groth16Zkp { +impl Groth16Proof { pub fn new(a: G1Bytes, b: G2Bytes, c: G1Bytes) -> Self { - Groth16Zkp { a, b, c } + Groth16Proof { a, b, c } } - pub fn get_a(&self) -> G1Bytes { - self.a + pub fn get_a(&self) -> &G1Bytes { + &self.a } - pub fn get_b(&self) -> G2Bytes { - self.b + pub fn get_b(&self) -> &G2Bytes { + &self.b } - pub fn get_c(&self) -> G1Bytes { - self.c + pub fn get_c(&self) -> &G1Bytes { + &self.c } /// NOTE: For testing only. (And used in `testsuite/generate-format`.) pub fn dummy_proof() -> Self { - Groth16Zkp { + Groth16Proof { a: G1Bytes::new_from_vec(vec![0u8; G1_PROJECTIVE_COMPRESSED_NUM_BYTES]).unwrap(), b: G2Bytes::new_from_vec(vec![1u8; G2_PROJECTIVE_COMPRESSED_NUM_BYTES]).unwrap(), c: G1Bytes::new_from_vec(vec![2u8; G1_PROJECTIVE_COMPRESSED_NUM_BYTES]).unwrap(), diff --git a/types/src/keyless/mod.rs b/types/src/keyless/mod.rs index ca61f052b198c..0a3ed51c32d52 100644 --- a/types/src/keyless/mod.rs +++ b/types/src/keyless/mod.rs @@ -11,6 +11,7 @@ use crate::{ }; use anyhow::bail; use aptos_crypto::{poseidon_bn254, CryptoMaterialError, ValidCryptoMaterial}; +use aptos_crypto_derive::{BCSCryptoHash, CryptoHasher}; use ark_bn254::Bn254; use ark_groth16::PreparedVerifyingKey; use ark_serialize::CanonicalSerialize; @@ -30,6 +31,7 @@ mod groth16_sig; mod groth16_vk; mod openid_sig; pub mod test_utils; +mod zkp_sig; use crate::keyless::circuit_constants::devnet_prepared_vk; pub use bn254_circom::{ @@ -37,9 +39,10 @@ pub use bn254_circom::{ G2Bytes, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, G2_PROJECTIVE_COMPRESSED_NUM_BYTES, }; pub use configuration::Configuration; -pub use groth16_sig::{Groth16Zkp, Groth16ZkpAndStatement, SignedGroth16Zkp}; +pub use groth16_sig::{Groth16Proof, Groth16ProofAndStatement, ZeroKnowledgeSig}; pub use groth16_vk::Groth16VerificationKey; pub use openid_sig::{Claims, OpenIdSig}; +pub use zkp_sig::ZKP; /// The name of the Move module for keyless accounts deployed at 0x1. pub const KEYLESS_ACCOUNT_MODULE_NAME: &str = "keyless_account"; @@ -65,21 +68,23 @@ macro_rules! serialize { }}; } -/// Allows us to support direct verification of OpenID signatures, in the rare case that we would -/// need to turn off ZK proofs due to a bug in the circuit. +/// A signature from the OIDC provider over the user ID, the application ID and the EPK, which serves +/// as a "certificate" binding the EPK to the keyless account associated with that user and application. +/// +/// This is a \[ZKPoK of an\] OpenID signature over a JWT containing several relevant fields +/// (e.g., `aud`, `sub`, `iss`, `nonce`) where `nonce` is a commitment to the `ephemeral_pubkey` and +/// the expiration time +/// `exp_timestamp_secs`. #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub enum ZkpOrOpenIdSig { - Groth16Zkp(SignedGroth16Zkp), +pub enum EphemeralCertificate { + ZeroKnowledgeSig(ZeroKnowledgeSig), OpenIdSig(OpenIdSig), } /// NOTE: See `KeylessPublicKey` comments for why this cannot be named `Signature`. #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] pub struct KeylessSignature { - /// A \[ZKPoK of an\] OpenID signature over several relevant fields (e.g., `aud`, `sub`, `iss`, - /// `nonce`) where `nonce` contains a commitment to `ephemeral_pubkey` and an expiration time - /// `exp_timestamp_secs`. - pub sig: ZkpOrOpenIdSig, + pub cert: EphemeralCertificate, /// The decoded/plaintext JWT header (i.e., *not* base64url-encoded), with two relevant fields: /// 1. `kid`, which indicates which of the OIDC provider's JWKs should be used to verify the @@ -92,10 +97,19 @@ pub struct KeylessSignature { /// A short lived public key used to verify the `ephemeral_signature`. pub ephemeral_pubkey: EphemeralPublicKey, - /// The signature of the transaction signed by the private key of the `ephemeral_pubkey`. + + /// A signature ove the transaction and, if present, the ZKP, under `ephemeral_pubkey`. + /// The ZKP is included in this signature to prevent malleability attacks. pub ephemeral_signature: EphemeralSignature, } +/// This struct wraps the transaction and optional ZKP that is signed with the ephemeral secret key. +#[derive(Serialize, Deserialize, CryptoHasher, BCSCryptoHash)] +pub struct TransactionAndProof { + pub message: T, + pub proof: Option, +} + impl TryFrom<&[u8]> for KeylessSignature { type Error = CryptoMaterialError; diff --git a/types/src/keyless/test_utils.rs b/types/src/keyless/test_utils.rs index 040d84c9e3d8c..54847456748aa 100644 --- a/types/src/keyless/test_utils.rs +++ b/types/src/keyless/test_utils.rs @@ -1,6 +1,6 @@ // Copyright © Aptos Foundation -use super::{Groth16ZkpAndStatement, Pepper}; +use super::{Groth16ProofAndStatement, Pepper, TransactionAndProof}; use crate::{ jwks::rsa::RSA_JWK, keyless::{ @@ -11,20 +11,23 @@ use crate::{ SAMPLE_JWT_HEADER_JSON, SAMPLE_JWT_PARSED, SAMPLE_JWT_PAYLOAD_JSON, SAMPLE_PEPPER, SAMPLE_PK, SAMPLE_PROOF, SAMPLE_UID_KEY, }, - get_public_inputs_hash, Configuration, Groth16Zkp, KeylessPublicKey, KeylessSignature, - OpenIdSig, SignedGroth16Zkp, ZkpOrOpenIdSig, + get_public_inputs_hash, + zkp_sig::ZKP, + Configuration, EphemeralCertificate, Groth16Proof, KeylessPublicKey, KeylessSignature, + OpenIdSig, ZeroKnowledgeSig, }, - transaction::authenticator::EphemeralSignature, + transaction::{authenticator::EphemeralSignature, RawTransaction, SignedTransaction}, +}; +use aptos_crypto::{ + ed25519::Ed25519PrivateKey, poseidon_bn254::fr_to_bytes_le, SigningKey, Uniform, }; -use aptos_crypto::{ed25519::Ed25519PrivateKey, SigningKey, Uniform}; -use ark_ff::{BigInteger, PrimeField}; use once_cell::sync::Lazy; use ring::signature; static DUMMY_EPHEMERAL_SIGNATURE: Lazy = Lazy::new(|| { let sk = Ed25519PrivateKey::generate_for_testing(); - // Signing the sample proof, for lack of any other dummy thing to sign. - EphemeralSignature::ed25519(sk.sign::(&SAMPLE_PROOF).unwrap()) + // Signing the sample proof, for lack of any other dummy struct to sign. + EphemeralSignature::ed25519(sk.sign::(&SAMPLE_PROOF).unwrap()) }); pub fn get_sample_esk() -> Ed25519PrivateKey { @@ -45,20 +48,15 @@ pub fn get_sample_pepper() -> Pepper { SAMPLE_PEPPER.clone() } -pub fn get_sample_groth16_zkp_and_statement() -> Groth16ZkpAndStatement { +pub fn get_sample_groth16_zkp_and_statement() -> Groth16ProofAndStatement { let config = Configuration::new_for_testing(); let (sig, pk) = get_sample_groth16_sig_and_pk(); - let public_inputs_hash = get_public_inputs_hash(&sig, &pk, &SAMPLE_JWK, &config) - .unwrap() - .into_bigint() - .to_bytes_le() - .try_into() - .expect("expected 32-bytes public inputs hash"); - - let proof = match sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(SignedGroth16Zkp { + let public_inputs_hash = + fr_to_bytes_le(&get_public_inputs_hash(&sig, &pk, &SAMPLE_JWK, &config).unwrap()); + + let proof = match sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(ZeroKnowledgeSig { proof, - non_malleability_signature: _, exp_horizon_secs: _, extra_field: _, override_aud_val: _, @@ -67,8 +65,10 @@ pub fn get_sample_groth16_zkp_and_statement() -> Groth16ZkpAndStatement { _ => unreachable!(), }; - Groth16ZkpAndStatement { - proof, + Groth16ProofAndStatement { + proof: match proof { + ZKP::Groth16(proof) => proof, + }, public_inputs_hash, } } @@ -78,24 +78,23 @@ pub fn get_sample_groth16_zkp_and_statement() -> Groth16ZkpAndStatement { pub fn get_sample_groth16_sig_and_pk() -> (KeylessSignature, KeylessPublicKey) { let proof = *SAMPLE_PROOF; - let groth16zkp = SignedGroth16Zkp { - proof, - non_malleability_signature: EphemeralSignature::ed25519(SAMPLE_ESK.sign(&proof).unwrap()), + let zks = ZeroKnowledgeSig { + proof: proof.into(), extra_field: Some(SAMPLE_JWT_EXTRA_FIELD.to_string()), exp_horizon_secs: SAMPLE_EXP_HORIZON_SECS, override_aud_val: None, training_wheels_signature: None, }; - let zk_sig = KeylessSignature { - sig: ZkpOrOpenIdSig::Groth16Zkp(groth16zkp.clone()), + let sig = KeylessSignature { + cert: EphemeralCertificate::ZeroKnowledgeSig(zks.clone()), jwt_header_json: SAMPLE_JWT_HEADER_JSON.to_string(), exp_date_secs: SAMPLE_EXP_DATE, ephemeral_pubkey: SAMPLE_EPK.clone(), ephemeral_signature: DUMMY_EPHEMERAL_SIGNATURE.clone(), }; - (zk_sig, SAMPLE_PK.clone()) + (sig, SAMPLE_PK.clone()) } /// Note: Does not have a valid ephemeral signature. Use the SAMPLE_ESK to compute one over the @@ -126,7 +125,7 @@ pub fn get_sample_openid_sig_and_pk() -> (KeylessSignature, KeylessPublicKey) { }; let zk_sig = KeylessSignature { - sig: ZkpOrOpenIdSig::OpenIdSig(openid_sig.clone()), + cert: EphemeralCertificate::OpenIdSig(openid_sig.clone()), jwt_header_json: SAMPLE_JWT_HEADER_JSON.to_string(), exp_date_secs: SAMPLE_EXP_DATE, ephemeral_pubkey: SAMPLE_EPK.clone(), @@ -136,6 +135,36 @@ pub fn get_sample_openid_sig_and_pk() -> (KeylessSignature, KeylessPublicKey) { (zk_sig, SAMPLE_PK.clone()) } +pub fn maul_raw_groth16_txn( + pk: KeylessPublicKey, + mut sig: KeylessSignature, + raw_txn: RawTransaction, +) -> SignedTransaction { + let mut txn_and_zkp = TransactionAndProof { + message: raw_txn.clone(), + proof: None, + }; + + // maul ephemeral signature to be over a different proof: (a, b, a) instead of (a, b, c) + match &mut sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(proof) => { + let ZKP::Groth16(old_proof) = proof.proof; + + txn_and_zkp.proof = Some( + Groth16Proof::new(*old_proof.get_a(), *old_proof.get_b(), *old_proof.get_a()) + .into(), + ); + }, + EphemeralCertificate::OpenIdSig(_) => {}, + }; + + let esk = get_sample_esk(); + sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&txn_and_zkp).unwrap()); + + // reassemble TXN + SignedTransaction::new_keyless(raw_txn, pk, sig) +} + #[cfg(test)] mod test { use crate::keyless::{ diff --git a/types/src/keyless/tests.rs b/types/src/keyless/tests.rs index 5b77347237213..ec915b7fcca9f 100644 --- a/types/src/keyless/tests.rs +++ b/types/src/keyless/tests.rs @@ -4,7 +4,7 @@ use crate::keyless::{ bn254_circom::get_public_inputs_hash, circuit_testcases::*, test_utils::{get_sample_groth16_sig_and_pk, get_sample_openid_sig_and_pk}, - Configuration, ZkpOrOpenIdSig, DEVNET_VERIFICATION_KEY, + Configuration, EphemeralCertificate, DEVNET_VERIFICATION_KEY, }; use std::ops::{AddAssign, Deref}; @@ -14,9 +14,9 @@ fn test_keyless_groth16_proof_verification() { let config = Configuration::new_for_devnet(); let (zk_sig, zk_pk) = get_sample_groth16_sig_and_pk(); - let proof = match &zk_sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => proof.clone(), - ZkpOrOpenIdSig::OpenIdSig(_) => panic!("Internal inconsistency"), + let proof = match &zk_sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(proof) => proof.clone(), + EphemeralCertificate::OpenIdSig(_) => panic!("Internal inconsistency"), }; let public_inputs_hash = get_public_inputs_hash(&zk_sig, &zk_pk, &SAMPLE_JWK, &config).unwrap(); @@ -27,7 +27,7 @@ fn test_keyless_groth16_proof_verification() { ); proof - .verify_proof(public_inputs_hash, DEVNET_VERIFICATION_KEY.deref()) + .verify_groth16_proof(public_inputs_hash, DEVNET_VERIFICATION_KEY.deref()) .unwrap(); } @@ -37,9 +37,9 @@ fn test_keyless_oidc_sig_verifies() { let config = Configuration::new_for_testing(); let (sig, pk) = get_sample_openid_sig_and_pk(); - let oidc_sig = match &sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(_) => panic!("Internal inconsistency"), - ZkpOrOpenIdSig::OpenIdSig(oidc_sig) => oidc_sig.clone(), + let oidc_sig = match &sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(_) => panic!("Internal inconsistency"), + EphemeralCertificate::OpenIdSig(oidc_sig) => oidc_sig.clone(), }; oidc_sig diff --git a/types/src/keyless/zkp_sig.rs b/types/src/keyless/zkp_sig.rs new file mode 100644 index 0000000000000..f7a1d34a4ad71 --- /dev/null +++ b/types/src/keyless/zkp_sig.rs @@ -0,0 +1,26 @@ +// Copyright © Aptos Foundation + +use crate::keyless::Groth16Proof; +use aptos_crypto_derive::{BCSCryptoHash, CryptoHasher}; +use serde::{Deserialize, Serialize}; + +#[derive( + Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, CryptoHasher, BCSCryptoHash, +)] +pub enum ZKP { + Groth16(Groth16Proof), +} + +impl From for ZKP { + fn from(proof: Groth16Proof) -> Self { + ZKP::Groth16(proof) + } +} + +impl From for Groth16Proof { + fn from(zkp: ZKP) -> Self { + match zkp { + ZKP::Groth16(proof) => proof, + } + } +} diff --git a/types/src/on_chain_config/aptos_features.rs b/types/src/on_chain_config/aptos_features.rs index fee23f0bc38a6..1e5919b82fb26 100644 --- a/types/src/on_chain_config/aptos_features.rs +++ b/types/src/on_chain_config/aptos_features.rs @@ -65,6 +65,7 @@ pub enum FeatureFlag { REFUNDABLE_BYTES = 51, OBJECT_CODE_DEPLOYMENT = 52, MAX_OBJECT_NESTING_CHECK = 53, + KEYLESS_ACCOUNTS_WITH_PASSKEYS = 54, } impl FeatureFlag { @@ -116,6 +117,7 @@ impl FeatureFlag { FeatureFlag::REFUNDABLE_BYTES, FeatureFlag::OBJECT_CODE_DEPLOYMENT, FeatureFlag::MAX_OBJECT_NESTING_CHECK, + FeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS, ] } } @@ -242,6 +244,10 @@ impl Features { self.is_enabled(FeatureFlag::KEYLESS_BUT_ZKLESS_ACCOUNTS) } + pub fn is_keyless_with_passkeys_enabled(&self) -> bool { + self.is_enabled(FeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS) + } + pub fn is_reconfigure_with_dkg_enabled(&self) -> bool { self.is_enabled(FeatureFlag::RECONFIGURE_WITH_DKG) } diff --git a/types/src/transaction/authenticator.rs b/types/src/transaction/authenticator.rs index f0cec81be61af..67efa5370c035 100644 --- a/types/src/transaction/authenticator.rs +++ b/types/src/transaction/authenticator.rs @@ -4,7 +4,7 @@ use crate::{ account_address::AccountAddress, - keyless::{KeylessPublicKey, KeylessSignature, ZkpOrOpenIdSig}, + keyless::{EphemeralCertificate, KeylessPublicKey, KeylessSignature, TransactionAndProof}, transaction::{ webauthn::PartialAuthenticatorAssertionResponse, RawTransaction, RawTransactionWithData, }, @@ -1005,23 +1005,32 @@ impl AnySignature { }, (Self::WebAuthn { signature }, _) => signature.verify(message, public_key), (Self::Keyless { signature }, AnyPublicKey::Keyless { public_key: _ }) => { - // TODO(keyless): Batch-verify these two signatures - match &signature.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => { - proof.verify_non_malleability_sig(&signature.ephemeral_pubkey)?; - }, - ZkpOrOpenIdSig::OpenIdSig(_) => {}, - } - // Verify the ephemeral signature on the TXN. The rest of the verification, - // i.e., [ZKPoK of] OpenID signature verification will be done in `AptosVM::run_prologue`. - // This is due to the dependency on the JWK, which must be fetched from the chain, - // since JWKs are updated automatically via consensus. + // Verifies the ephemeral signature on the TXN and, if present, the ZKP. The rest of + // the verification, i.e., [ZKPoK of] OpenID signature verification is done in + // `AptosVM::run_prologue`. + // + // This is because the JWK, under which the [ZKPoK of an] OpenID signature verifies, + // can only be fetched from on chain inside the `AptosVM`. // // This deferred verification is what actually ensures the `signature.ephemeral_pubkey` // used below is the right pubkey signed by the OIDC provider. + + let mut txn_and_zkp = TransactionAndProof { + message, + proof: None, + }; + + // Add the ZK proof into the `txn_and_zkp` struct, if we are in the ZK path + match &signature.cert { + EphemeralCertificate::ZeroKnowledgeSig(proof) => { + txn_and_zkp.proof = Some(proof.proof) + }, + EphemeralCertificate::OpenIdSig(_) => {}, + } + signature .ephemeral_signature - .verify(message, &signature.ephemeral_pubkey) + .verify(&txn_and_zkp, &signature.ephemeral_pubkey) }, _ => bail!("Invalid key, signature pairing"), } @@ -1067,7 +1076,12 @@ impl AnyPublicKey { } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum EphemeralSignature { - Ed25519 { signature: Ed25519Signature }, + Ed25519 { + signature: Ed25519Signature, + }, + WebAuthn { + signature: PartialAuthenticatorAssertionResponse, + }, } impl EphemeralSignature { @@ -1084,6 +1098,12 @@ impl EphemeralSignature { (Self::Ed25519 { signature }, EphemeralPublicKey::Ed25519 { public_key }) => { signature.verify(message, public_key) }, + (Self::WebAuthn { signature }, EphemeralPublicKey::Secp256r1Ecdsa { public_key }) => { + signature.verify(message, &AnyPublicKey::secp256r1_ecdsa(public_key.clone())) + }, + _ => { + bail!("Unsupported ephemeral signature and public key combination"); + }, } } } @@ -1099,7 +1119,12 @@ impl TryFrom<&[u8]> for EphemeralSignature { #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum EphemeralPublicKey { - Ed25519 { public_key: Ed25519PublicKey }, + Ed25519 { + public_key: Ed25519PublicKey, + }, + Secp256r1Ecdsa { + public_key: secp256r1_ecdsa::PublicKey, + }, } impl EphemeralPublicKey { @@ -1162,13 +1187,21 @@ impl Serialize for EphemeralPublicKey { #[derive(::serde::Serialize)] #[serde(rename = "EphemeralPublicKey")] enum Value { - Ed25519 { public_key: Ed25519PublicKey }, + Ed25519 { + public_key: Ed25519PublicKey, + }, + Secp256r1Ecdsa { + public_key: secp256r1_ecdsa::PublicKey, + }, } let value = match self { EphemeralPublicKey::Ed25519 { public_key } => Value::Ed25519 { public_key: public_key.clone(), }, + EphemeralPublicKey::Secp256r1Ecdsa { public_key } => Value::Secp256r1Ecdsa { + public_key: public_key.clone(), + }, }; value.serialize(serializer) @@ -1182,6 +1215,7 @@ mod tests { use crate::{ keyless::test_utils::{ get_sample_esk, get_sample_groth16_sig_and_pk, get_sample_openid_sig_and_pk, + maul_raw_groth16_txn, }, transaction::{webauthn::AssertionSignature, SignedTransaction}, }; @@ -1192,7 +1226,6 @@ mod tests { PrivateKey, SigningKey, Uniform, }; use hex::FromHex; - use rand::thread_rng; #[test] fn test_from_str_should_not_panic_by_given_empty_string() { @@ -1734,7 +1767,13 @@ mod tests { None, None, ); - sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); + sig.ephemeral_signature = EphemeralSignature::ed25519( + esk.sign(&TransactionAndProof { + message: raw_txn.clone(), + proof: None, + }) + .unwrap(), + ); let single_key_auth = SingleKeyAuthenticator::new(AnyPublicKey::keyless(pk), AnySignature::keyless(sig)); @@ -1763,7 +1802,17 @@ mod tests { None, None, ); - sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); + let mut txn_and_zkp = TransactionAndProof { + message: raw_txn.clone(), + proof: None, + }; + match &mut sig.cert { + EphemeralCertificate::ZeroKnowledgeSig(proof) => { + txn_and_zkp.proof = Some(proof.proof); + }, + EphemeralCertificate::OpenIdSig(_) => panic!("Internal inconsistency"), + } + sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&txn_and_zkp).unwrap()); let single_key_auth = SingleKeyAuthenticator::new(AnyPublicKey::keyless(pk), AnySignature::keyless(sig)); @@ -1781,8 +1830,7 @@ mod tests { #[test] fn test_groth16_txn_fails_non_malleability_check() { - let esk = get_sample_esk(); - let (mut sig, pk) = get_sample_groth16_sig_and_pk(); + let (sig, pk) = get_sample_groth16_sig_and_pk(); let sender_addr = AuthenticationKey::any_key(AnyPublicKey::keyless(pk.clone())).account_address(); let raw_txn = crate::test_helpers::transaction_test_helpers::get_test_raw_transaction( @@ -1793,23 +1841,7 @@ mod tests { None, None, ); - sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); - - let tw_sk = Ed25519PrivateKey::generate(&mut thread_rng()); - // Bad non-malleability signature - match &mut sig.sig { - ZkpOrOpenIdSig::Groth16Zkp(proof) => { - // bad signature using the TW SK rather than the ESK - proof.non_malleability_signature = - EphemeralSignature::ed25519(tw_sk.sign(&proof.proof).unwrap()); - }, - ZkpOrOpenIdSig::OpenIdSig(_) => panic!("Internal inconsistency"), - } - - let single_key_auth = - SingleKeyAuthenticator::new(AnyPublicKey::keyless(pk), AnySignature::keyless(sig)); - let account_auth = AccountAuthenticator::single_key(single_key_auth); - let signed_txn = SignedTransaction::new_single_sender(raw_txn, account_auth); + let signed_txn = maul_raw_groth16_txn(pk, sig, raw_txn); assert!(signed_txn.verify_signature().is_err()); } diff --git a/types/src/unit_tests/keyless_serialization_test.rs b/types/src/unit_tests/keyless_serialization_test.rs index a37d7138b37de..da71017e2c893 100644 --- a/types/src/unit_tests/keyless_serialization_test.rs +++ b/types/src/unit_tests/keyless_serialization_test.rs @@ -2,7 +2,7 @@ use crate::{ keyless::{ - test_utils, G1Bytes, G2Bytes, Groth16ZkpAndStatement, Pepper, + test_utils, G1Bytes, G2Bytes, Groth16ProofAndStatement, Pepper, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, G2_PROJECTIVE_COMPRESSED_NUM_BYTES, }, transaction::authenticator::EphemeralPublicKey, @@ -99,14 +99,14 @@ fn test_groth16_zkp_and_statement_serialization() { let groth16_zkp_and_statement = test_utils::get_sample_groth16_zkp_and_statement(); assert_eq!( - serde_json::from_str::( + serde_json::from_str::( &serde_json::to_string(&groth16_zkp_and_statement).unwrap() ) .unwrap(), groth16_zkp_and_statement ); assert_eq!( - bcs::from_bytes::( + bcs::from_bytes::( &bcs::to_bytes(&groth16_zkp_and_statement).unwrap() ) .unwrap(), @@ -138,7 +138,8 @@ fn test_groth16_zkp_and_statement_serialization() { assert_eq!( serde_json::to_string( - &serde_json::from_str::(groth16_zkp_and_statement_str).unwrap() + &serde_json::from_str::(groth16_zkp_and_statement_str) + .unwrap() ) .unwrap() .as_str(), @@ -146,7 +147,7 @@ fn test_groth16_zkp_and_statement_serialization() { ); assert_eq!( bcs::to_bytes( - &bcs::from_bytes::(&groth16_zkp_and_statement_bytes).unwrap() + &bcs::from_bytes::(&groth16_zkp_and_statement_bytes).unwrap() ) .unwrap(), groth16_zkp_and_statement_bytes @@ -155,17 +156,17 @@ fn test_groth16_zkp_and_statement_serialization() { #[test] fn test_g1_bytes_serialization() { - let groth16_zkp_and_statement = test_utils::get_sample_groth16_zkp_and_statement(); + let zkp_and_stmt = test_utils::get_sample_groth16_zkp_and_statement(); - let g1_bytes = groth16_zkp_and_statement.proof.get_a(); + let g1_bytes = zkp_and_stmt.proof.get_a(); assert_eq!( - serde_json::from_str::(&serde_json::to_string(&g1_bytes).unwrap()).unwrap(), - g1_bytes + serde_json::from_str::(&serde_json::to_string(g1_bytes).unwrap()).unwrap(), + *g1_bytes ); assert_eq!( - bcs::from_bytes::(&bcs::to_bytes(&g1_bytes).unwrap()).unwrap(), - g1_bytes + bcs::from_bytes::(&bcs::to_bytes(g1_bytes).unwrap()).unwrap(), + *g1_bytes ); // these values were generated as follows: @@ -198,17 +199,17 @@ fn test_g1_bytes_serialization() { #[test] fn test_g2_bytes_serialization() { - let groth16_zkp_and_statement = test_utils::get_sample_groth16_zkp_and_statement(); + let zkp_and_stmt = test_utils::get_sample_groth16_zkp_and_statement(); - let g2_bytes = groth16_zkp_and_statement.proof.get_b(); + let g2_bytes = zkp_and_stmt.proof.get_b(); assert_eq!( - serde_json::from_str::(&serde_json::to_string(&g2_bytes).unwrap()).unwrap(), - g2_bytes + serde_json::from_str::(&serde_json::to_string(g2_bytes).unwrap()).unwrap(), + *g2_bytes ); assert_eq!( - bcs::from_bytes::(&bcs::to_bytes(&g2_bytes).unwrap()).unwrap(), - g2_bytes + bcs::from_bytes::(&bcs::to_bytes(g2_bytes).unwrap()).unwrap(), + *g2_bytes ); // these values were generated as follows: