From 0d29ccd5e1fd3ba1c704f331effac69db2018a33 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Sun, 18 Feb 2024 19:05:59 -0800 Subject: [PATCH 01/39] Fix `iss`-related bug in Groth16 path & refactor (#12017) Co-authored-by: Oliver --- Cargo.lock | 43 +- Cargo.toml | 1 + aptos-move/aptos-vm/src/zkid_validation.rs | 30 +- .../framework/aptos-framework/doc/zkid.md | 49 +- .../aptos-framework/sources/zkid.move | 23 +- aptos-move/vm-genesis/src/lib.rs | 7 +- .../generate-format/tests/staged/api.yaml | 11 +- .../generate-format/tests/staged/aptos.yaml | 11 +- .../tests/staged/consensus.yaml | 11 +- testsuite/smoke-test/src/zkid.rs | 622 +++-------- types/Cargo.toml | 2 + types/src/bn254_circom.rs | 456 --------- types/src/lib.rs | 1 - types/src/transaction/authenticator.rs | 229 ++--- types/src/zkid.rs | 967 ------------------ types/src/zkid/bn254_circom.rs | 296 ++++++ types/src/zkid/circuit_constants.rs | 92 ++ types/src/zkid/circuit_testcases.rs | 174 ++++ types/src/zkid/configuration.rs | 90 ++ types/src/zkid/groth16_sig.rs | 104 ++ types/src/zkid/groth16_vk.rs | 115 +++ types/src/zkid/mod.rs | 306 ++++++ types/src/zkid/openid_sig.rs | 206 ++++ types/src/zkid/test_utils.rs | 138 +++ types/src/zkid/tests.rs | 136 +++ 25 files changed, 2023 insertions(+), 2097 deletions(-) delete mode 100644 types/src/bn254_circom.rs delete mode 100644 types/src/zkid.rs create mode 100644 types/src/zkid/bn254_circom.rs create mode 100644 types/src/zkid/circuit_constants.rs create mode 100644 types/src/zkid/circuit_testcases.rs create mode 100644 types/src/zkid/configuration.rs create mode 100644 types/src/zkid/groth16_sig.rs create mode 100644 types/src/zkid/groth16_vk.rs create mode 100644 types/src/zkid/mod.rs create mode 100644 types/src/zkid/openid_sig.rs create mode 100644 types/src/zkid/test_utils.rs create mode 100644 types/src/zkid/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 813ebc314d4a4..f46ddcca9e138 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4168,6 +4168,8 @@ dependencies = [ "rand 0.7.3", "rayon", "regex", + "ring 0.16.20", + "rsa 0.9.6", "serde", "serde-big-array", "serde_bytes", @@ -7437,7 +7439,7 @@ dependencies = [ "fixed-hash 0.8.0", "impl-codec 0.6.0", "impl-rlp", - "scale-info 1.0.0", + "scale-info 2.10.0", "tiny-keccak", ] @@ -7518,7 +7520,7 @@ dependencies = [ "impl-codec 0.6.0", "impl-rlp", "primitive-types 0.12.2", - "scale-info 1.0.0", + "scale-info 2.10.0", "uint", ] @@ -8524,7 +8526,7 @@ dependencies = [ "regex", "reqwest", "ring 0.16.20", - "rsa", + "rsa 0.6.1", "serde", "serde_json", "sha2 0.10.8", @@ -12434,6 +12436,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.8", + "pkcs8 0.10.2", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.8.0" @@ -12883,7 +12896,7 @@ dependencies = [ "fixed-hash 0.8.0", "impl-codec 0.6.0", "impl-rlp", - "scale-info 1.0.0", + "scale-info 2.10.0", "uint", ] @@ -13853,7 +13866,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "pkcs1", + "pkcs1 0.3.3", "pkcs8 0.8.0", "rand_core 0.6.4", "smallvec", @@ -13861,6 +13874,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1 0.7.5", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rstack" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index 06a1b56b63447..8ddc2fc5106fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -640,6 +640,7 @@ reqwest-retry = "0.2.1" ring = { version = "0.16.20", features = ["std"] } ripemd = "0.1.1" rocksdb = { version = "0.21.0", features = ["lz4"] } +rsa = { version = "0.9.6" } rstack-self = { version = "0.3.0", features = ["dw"], default_features = false } rstest = "0.15.0" rusty-fork = "0.3.0" diff --git a/aptos-move/aptos-vm/src/zkid_validation.rs b/aptos-move/aptos-vm/src/zkid_validation.rs index b483eb84c83e1..a532fa0e4a424 100644 --- a/aptos-move/aptos-vm/src/zkid_validation.rs +++ b/aptos-move/aptos-vm/src/zkid_validation.rs @@ -5,14 +5,15 @@ use crate::move_vm_ext::AptosMoveResolver; use aptos_crypto::ed25519::Ed25519PublicKey; use aptos_types::{ - bn254_circom::{get_public_inputs_hash, Groth16VerificationKey}, invalid_signature, jwks::{jwk::JWK, PatchedJWKs}, on_chain_config::{CurrentTimeMicroseconds, OnChainConfig}, transaction::authenticator::EphemeralPublicKey, vm_status::{StatusCode, VMStatus}, - zkid, - zkid::{Configuration, ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig}, + zkid::{ + get_public_inputs_hash, Configuration, Groth16VerificationKey, ZkIdPublicKey, + ZkIdSignature, ZkpOrOpenIdSig, + }, }; use move_binary_format::errors::Location; use move_core_types::{language_storage::CORE_CODE_ADDRESS, move_resource::MoveStructType}; @@ -86,11 +87,11 @@ fn get_jwk_for_zkid_authenticator( .parse_jwt_header() .map_err(|_| invalid_signature!("Failed to parse JWT header"))?; let jwk_move_struct = jwks - .get_jwk(&zkid_pub_key.iss, &jwt_header.kid) + .get_jwk(&zkid_pub_key.iss_val, &jwt_header.kid) .map_err(|_| { invalid_signature!(format!( "JWK for {} with KID {} was not found", - zkid_pub_key.iss, jwt_header.kid + zkid_pub_key.iss_val, jwt_header.kid )) })?; @@ -148,10 +149,7 @@ pub fn validate_zkid_authenticators( // 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() { - zkid::is_allowed_override_aud( - config, - proof.override_aud_val.as_ref().unwrap(), - )?; + config.is_allowed_override_aud(proof.override_aud_val.as_ref().unwrap())?; } // The training wheels signature is only checked if a training wheels PK is set on chain @@ -163,14 +161,10 @@ pub fn validate_zkid_authenticators( })?; } - let public_inputs_hash = get_public_inputs_hash( - zkid_sig, - zkid_pub_key, - &rsa_jwk, - proof.exp_horizon_secs, - config, - ) - .map_err(|_| invalid_signature!("Could not compute public inputs hash"))?; + let public_inputs_hash = + get_public_inputs_hash(zkid_sig, zkid_pub_key, &rsa_jwk, config).map_err( + |_| invalid_signature!("Could not compute public inputs hash"), + )?; proof .verify_proof(public_inputs_hash, pvk) .map_err(|_| invalid_signature!("Proof verification failed"))?; @@ -201,7 +195,7 @@ pub fn validate_zkid_authenticators( // // We are now ready to verify the RSA signature openid_sig - .verify_jwt_signature(rsa_jwk, &zkid_sig.jwt_header) + .verify_jwt_signature(&rsa_jwk, &zkid_sig.jwt_header_b64) .map_err(|_| { invalid_signature!( "RSA signature verification failed for OpenIdSig" diff --git a/aptos-move/framework/aptos-framework/doc/zkid.md b/aptos-move/framework/aptos-framework/doc/zkid.md index 3ebc307caac96..11af85a6f12d2 100644 --- a/aptos-move/framework/aptos-framework/doc/zkid.md +++ b/aptos-move/framework/aptos-framework/doc/zkid.md @@ -14,6 +14,7 @@ - [Function `update_groth16_verification_key`](#0x1_zkid_update_groth16_verification_key) - [Function `update_configuration`](#0x1_zkid_update_configuration) - [Function `update_training_wheels`](#0x1_zkid_update_training_wheels) +- [Function `update_max_exp_horizon`](#0x1_zkid_update_max_exp_horizon) - [Function `remove_all_override_auds`](#0x1_zkid_remove_all_override_auds) - [Function `add_override_aud`](#0x1_zkid_add_override_aud) @@ -153,22 +154,16 @@ The 288-byte Groth16 verification key (VK) for the zkID relation. The training wheels PK, if training wheels are on
-nonce_commitment_num_bytes: u16 -
-
- The size of the "nonce commitment (to the EPK and expiration date)" stored in the JWT's nonce field. -
-
max_commited_epk_bytes: u16
The max length of an ephemeral public key supported in our circuit (93 bytes)
-max_iss_field_bytes: u16 +max_iss_val_bytes: u16
- The max length of the field name and value of the JWT's iss field supported in our circuit (e.g., "iss":"aptos.com") + The max length of the value of the JWT's iss field supported in our circuit (e.g., "https://accounts.google.com")
max_extra_field_bytes: u16 @@ -243,7 +238,7 @@ The training wheels PK needs to be 32 bytes long. -
public fun new_configuration(override_aud_val: vector<string::String>, max_zkid_signatures_per_txn: u16, max_exp_horizon_secs: u64, training_wheels_pubkey: option::Option<vector<u8>>, nonce_commitment_num_bytes: u16, max_commited_epk_bytes: u16, max_iss_field_bytes: u16, max_extra_field_bytes: u16, max_jwt_header_b64_bytes: u32): zkid::Configuration
+
public fun new_configuration(override_aud_val: vector<string::String>, max_zkid_signatures_per_txn: u16, max_exp_horizon_secs: u64, training_wheels_pubkey: option::Option<vector<u8>>, max_commited_epk_bytes: u16, max_iss_val_bytes: u16, max_extra_field_bytes: u16, max_jwt_header_b64_bytes: u32): zkid::Configuration
 
@@ -257,9 +252,8 @@ The training wheels PK needs to be 32 bytes long. max_zkid_signatures_per_txn: u16, max_exp_horizon_secs: u64, training_wheels_pubkey: Option<vector<u8>>, - nonce_commitment_num_bytes: u16, max_commited_epk_bytes: u16, - max_iss_field_bytes: u16, + max_iss_val_bytes: u16, max_extra_field_bytes: u16, max_jwt_header_b64_bytes: u32 ): Configuration { @@ -268,9 +262,8 @@ The training wheels PK needs to be 32 bytes long. max_zkid_signatures_per_txn, max_exp_horizon_secs, training_wheels_pubkey, - nonce_commitment_num_bytes, max_commited_epk_bytes, - max_iss_field_bytes, + max_iss_val_bytes, max_extra_field_bytes, max_jwt_header_b64_bytes, } @@ -341,9 +334,8 @@ The training wheels PK needs to be 32 bytes long. max_zkid_signatures_per_txn: _, max_exp_horizon_secs: _, training_wheels_pubkey: _, - nonce_commitment_num_bytes: _, max_commited_epk_bytes: _, - max_iss_field_bytes: _, + max_iss_val_bytes: _, max_extra_field_bytes: _, max_jwt_header_b64_bytes: _, } = move_from<Configuration>(signer::address_of(fx)); @@ -385,6 +377,33 @@ The training wheels PK needs to be 32 bytes long. + + + + +## Function `update_max_exp_horizon` + + + +
public fun update_max_exp_horizon(fx: &signer, max_exp_horizon_secs: u64)
+
+ + + +
+Implementation + + +
public fun update_max_exp_horizon(fx: &signer, max_exp_horizon_secs: u64) acquires Configuration {
+    system_addresses::assert_aptos_framework(fx);
+
+    let config = borrow_global_mut<Configuration>(signer::address_of(fx));
+    config.max_exp_horizon_secs = max_exp_horizon_secs;
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/zkid.move b/aptos-move/framework/aptos-framework/sources/zkid.move index 169ccde3a5158..96a73d06a5d66 100644 --- a/aptos-move/framework/aptos-framework/sources/zkid.move +++ b/aptos-move/framework/aptos-framework/sources/zkid.move @@ -41,12 +41,10 @@ module aptos_framework::zkid { max_exp_horizon_secs: u64, /// The training wheels PK, if training wheels are on training_wheels_pubkey: Option>, - /// The size of the "nonce commitment (to the EPK and expiration date)" stored in the JWT's `nonce` field. - nonce_commitment_num_bytes: u16, /// The max length of an ephemeral public key supported in our circuit (93 bytes) max_commited_epk_bytes: u16, - /// The max length of the field name and value of the JWT's `iss` field supported in our circuit (e.g., `"iss":"aptos.com"`) - max_iss_field_bytes: u16, + /// The max length of the value of the JWT's `iss` field supported in our circuit (e.g., `"https://accounts.google.com"`) + max_iss_val_bytes: u16, /// The max length of the JWT field name and value (e.g., `"max_age":"18"`) supported in our circuit max_extra_field_bytes: u16, /// The max length of the base64url-encoded JWT header in bytes supported in our circuit @@ -81,9 +79,8 @@ module aptos_framework::zkid { max_zkid_signatures_per_txn: u16, max_exp_horizon_secs: u64, training_wheels_pubkey: Option>, - nonce_commitment_num_bytes: u16, max_commited_epk_bytes: u16, - max_iss_field_bytes: u16, + max_iss_val_bytes: u16, max_extra_field_bytes: u16, max_jwt_header_b64_bytes: u32 ): Configuration { @@ -92,9 +89,8 @@ module aptos_framework::zkid { max_zkid_signatures_per_txn, max_exp_horizon_secs, training_wheels_pubkey, - nonce_commitment_num_bytes, max_commited_epk_bytes, - max_iss_field_bytes, + max_iss_val_bytes, max_extra_field_bytes, max_jwt_header_b64_bytes, } @@ -129,9 +125,8 @@ module aptos_framework::zkid { max_zkid_signatures_per_txn: _, max_exp_horizon_secs: _, training_wheels_pubkey: _, - nonce_commitment_num_bytes: _, max_commited_epk_bytes: _, - max_iss_field_bytes: _, + max_iss_val_bytes: _, max_extra_field_bytes: _, max_jwt_header_b64_bytes: _, } = move_from(signer::address_of(fx)); @@ -152,6 +147,14 @@ module aptos_framework::zkid { config.training_wheels_pubkey = pk; } + // Convenience method to set the zkID max expiration horizon, only callable via governance proposal. + public fun update_max_exp_horizon(fx: &signer, max_exp_horizon_secs: u64) acquires Configuration { + system_addresses::assert_aptos_framework(fx); + + let config = borrow_global_mut(signer::address_of(fx)); + config.max_exp_horizon_secs = max_exp_horizon_secs; + } + // Convenience method to append to clear the set of zkID override `aud`'s, only callable via governance proposal. // WARNING: When no override `aud` is set, recovery of zkID accounts associated with applications that disappeared // is no longer possible. diff --git a/aptos-move/vm-genesis/src/lib.rs b/aptos-move/vm-genesis/src/lib.rs index a8661920af65d..8d738e20f007d 100644 --- a/aptos-move/vm-genesis/src/lib.rs +++ b/aptos-move/vm-genesis/src/lib.rs @@ -19,8 +19,6 @@ use aptos_gas_schedule::{ }; use aptos_types::{ account_config::{self, aptos_test_root_address, events::NewEpochEvent, CORE_CODE_ADDRESS}, - bn254_circom, - bn254_circom::Groth16VerificationKey, chain_id::ChainId, contract_event::{ContractEvent, ContractEventV1}, move_utils::as_move_value::AsMoveValue, @@ -31,6 +29,7 @@ use aptos_types::{ transaction::{authenticator::AuthenticationKey, ChangeSet, Transaction, WriteSetPayload}, write_set::TransactionWrite, zkid, + zkid::{Groth16VerificationKey, DEVNET_VERIFICATION_KEY}, }; use aptos_vm::{ data_cache::AsMoveResolver, @@ -537,7 +536,7 @@ fn initialize_on_chain_governance(session: &mut SessionExt, genesis_config: &Gen } fn initialize_zkid(session: &mut SessionExt, chain_id: ChainId) { - let config = zkid::Configuration::new_for_devnet_and_testing(); + let config = zkid::Configuration::new_for_devnet(); exec_function( session, ZKID_MODULE_NAME, @@ -549,7 +548,7 @@ fn initialize_zkid(session: &mut SessionExt, chain_id: ChainId) { ]), ); if !chain_id.is_mainnet() { - let vk = Groth16VerificationKey::from(bn254_circom::DEVNET_VERIFYING_KEY.clone()); + let vk = Groth16VerificationKey::from(DEVNET_VERIFICATION_KEY.clone()); exec_function( session, ZKID_MODULE_NAME, diff --git a/testsuite/generate-format/tests/staged/api.yaml b/testsuite/generate-format/tests/staged/api.yaml index 3d9845c2dc130..3313a11a4ac25 100644 --- a/testsuite/generate-format/tests/staged/api.yaml +++ b/testsuite/generate-format/tests/staged/api.yaml @@ -342,8 +342,8 @@ MultisigTransactionPayload: TYPENAME: EntryFunction OpenIdSig: STRUCT: - - jwt_sig: STR - - jwt_payload: STR + - jwt_sig_b64: STR + - jwt_payload_b64: STR - uid_key: STR - epk_blinder: BYTES - pepper: @@ -450,7 +450,8 @@ SignedGroth16Zkp: - non_malleability_signature: TYPENAME: EphemeralSignature - exp_horizon_secs: U64 - - extra_field: STR + - extra_field: + OPTION: STR - override_aud_val: OPTION: STR - training_wheels_signature: @@ -781,14 +782,14 @@ WriteSetV0: TYPENAME: WriteSetMut ZkIdPublicKey: STRUCT: - - iss: STR + - iss_val: STR - idc: TYPENAME: IdCommitment ZkIdSignature: STRUCT: - sig: TYPENAME: ZkpOrOpenIdSig - - jwt_header: STR + - jwt_header_b64: STR - exp_timestamp_secs: U64 - ephemeral_pubkey: TYPENAME: EphemeralPublicKey diff --git a/testsuite/generate-format/tests/staged/aptos.yaml b/testsuite/generate-format/tests/staged/aptos.yaml index b578f684fef83..18771655135e2 100644 --- a/testsuite/generate-format/tests/staged/aptos.yaml +++ b/testsuite/generate-format/tests/staged/aptos.yaml @@ -288,8 +288,8 @@ MultisigTransactionPayload: TYPENAME: EntryFunction OpenIdSig: STRUCT: - - jwt_sig: STR - - jwt_payload: STR + - jwt_sig_b64: STR + - jwt_payload_b64: STR - uid_key: STR - epk_blinder: BYTES - pepper: @@ -382,7 +382,8 @@ SignedGroth16Zkp: - non_malleability_signature: TYPENAME: EphemeralSignature - exp_horizon_secs: U64 - - extra_field: STR + - extra_field: + OPTION: STR - override_aud_val: OPTION: STR - training_wheels_signature: @@ -663,14 +664,14 @@ WriteSetV0: TYPENAME: WriteSetMut ZkIdPublicKey: STRUCT: - - iss: STR + - iss_val: STR - idc: TYPENAME: IdCommitment ZkIdSignature: STRUCT: - sig: TYPENAME: ZkpOrOpenIdSig - - jwt_header: STR + - jwt_header_b64: STR - exp_timestamp_secs: U64 - ephemeral_pubkey: TYPENAME: EphemeralPublicKey diff --git a/testsuite/generate-format/tests/staged/consensus.yaml b/testsuite/generate-format/tests/staged/consensus.yaml index 81fbf1c5776db..57a155d591d4f 100644 --- a/testsuite/generate-format/tests/staged/consensus.yaml +++ b/testsuite/generate-format/tests/staged/consensus.yaml @@ -566,8 +566,8 @@ MultisigTransactionPayload: TYPENAME: EntryFunction OpenIdSig: STRUCT: - - jwt_sig: STR - - jwt_payload: STR + - jwt_sig_b64: STR + - jwt_payload_b64: STR - uid_key: STR - epk_blinder: BYTES - pepper: @@ -741,7 +741,8 @@ SignedGroth16Zkp: - non_malleability_signature: TYPENAME: EphemeralSignature - exp_horizon_secs: U64 - - extra_field: STR + - extra_field: + OPTION: STR - override_aud_val: OPTION: STR - training_wheels_signature: @@ -1085,14 +1086,14 @@ WriteSetV0: TYPENAME: WriteSetMut ZkIdPublicKey: STRUCT: - - iss: STR + - iss_val: STR - idc: TYPENAME: IdCommitment ZkIdSignature: STRUCT: - sig: TYPENAME: ZkpOrOpenIdSig - - jwt_header: STR + - jwt_header_b64: STR - exp_timestamp_secs: U64 - ephemeral_pubkey: TYPENAME: EphemeralPublicKey diff --git a/testsuite/smoke-test/src/zkid.rs b/testsuite/smoke-test/src/zkid.rs index c735ad7a0268d..23a0784dd9522 100644 --- a/testsuite/smoke-test/src/zkid.rs +++ b/testsuite/smoke-test/src/zkid.rs @@ -5,452 +5,176 @@ use aptos::test::CliTestFramework; use aptos_cached_packages::aptos_stdlib; use aptos_crypto::{ ed25519::{Ed25519PrivateKey, Ed25519PublicKey}, - encoding_type::EncodingType, SigningKey, Uniform, }; -use aptos_forge::{LocalSwarm, NodeExt, Swarm, SwarmExt}; +use aptos_forge::{AptosPublicInfo, LocalSwarm, NodeExt, Swarm, SwarmExt}; use aptos_logger::{debug, info}; use aptos_rest_client::Client; -use aptos_sdk::types::{AccountKey, LocalAccount}; use aptos_types::{ - bn254_circom::{G1Bytes, G2Bytes, Groth16VerificationKey}, jwks::{ jwk::{JWKMoveStruct, JWK}, - rsa::RSA_JWK, AllProvidersJWKs, PatchedJWKs, ProviderJWKs, }, transaction::{ - authenticator::{AnyPublicKey, EphemeralPublicKey, EphemeralSignature}, + authenticator::{AnyPublicKey, EphemeralSignature}, SignedTransaction, }, zkid::{ - Configuration, Groth16Zkp, IdCommitment, OpenIdSig, Pepper, SignedGroth16Zkp, - ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig, + test_utils::{ + get_sample_esk, get_sample_iss, get_sample_jwk, get_sample_zkid_groth16_sig_and_pk, + get_sample_zkid_openid_sig_and_pk, + }, + Configuration, Groth16VerificationKey, ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig, }, }; use move_core_types::account_address::AccountAddress; use rand::thread_rng; use std::time::Duration; -// TODO(zkid): test the override aud_val path -// TODO(zkid): These tests are not modular and they lack instructions for how to regenerate the proofs. +// TODO(zkid): Test the override aud_val path #[tokio::test] -async fn test_zkid_oidc_signature_transaction_submission() { - let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) - .with_aptos() - .build_with_cli(0) - .await; - let _ = test_setup(&mut swarm, &mut cli).await; - - let mut info = swarm.aptos_public_info(); - - let pepper = Pepper::new([0u8; 31]); - let idc = - IdCommitment::new_from_preimage(&pepper, "test_client_id", "sub", "test_account").unwrap(); - let sender_zkid_public_key = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc, - }; - let sender_any_public_key = AnyPublicKey::zkid(sender_zkid_public_key.clone()); - let account_address = info - .create_user_account_with_any_key(&sender_any_public_key) - .await - .unwrap(); - info.mint(account_address, 10_000_000_000).await.unwrap(); - - let ephemeral_private_key: Ed25519PrivateKey = EncodingType::Hex - .decode_key( - "zkid test ephemeral private key", - "0x1111111111111111111111111111111111111111111111111111111111111111" - .as_bytes() - .to_vec(), - ) - .unwrap(); - let ephemeral_account: aptos_sdk::types::LocalAccount = LocalAccount::new( - account_address, - AccountKey::from_private_key(ephemeral_private_key), - 0, - ); - let ephemeral_public_key = EphemeralPublicKey::ed25519(ephemeral_account.public_key().clone()); - - let recipient = info - .create_and_fund_user_account(20_000_000_000) - .await - .unwrap(); +async fn test_zkid_oidc_txn_verifies() { + let (_, _, mut swarm, signed_txn) = + get_zkid_transaction(get_sample_zkid_openid_sig_and_pk).await; - let raw_txn = info - .transaction_factory() - .payload(aptos_stdlib::aptos_coin_transfer(recipient.address(), 100)) - .sender(account_address) - .sequence_number(1) - .build(); - - let sender_sig = ephemeral_account.private_key().sign(&raw_txn).unwrap(); - let ephemeral_signature = EphemeralSignature::ed25519(sender_sig); - - let epk_blinder = vec![0u8; 31]; - let jwt_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_string(); - let jwt_payload = "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiJ0ZXN0X2NsaWVudF9pZCIsInN1YiI6InRlc3RfYWNjb3VudCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibm9uY2UiOiIxMzIwMTc1NTc0Njg5NjI2Mjk1MjE1NjI0NDQ5OTc3ODc4Njk5NzE5Njc3NzE0MzIzOTg5Njk3NzczODY0NTIzOTkwMzIyNzI4MjE2IiwibmJmIjoxNzAyODA4OTM2LCJpYXQiOjE3MDQ5MDkyMzYsImV4cCI6MTcyNzgxMjgzNiwianRpIjoiZjEwYWZiZjBlN2JiOTcyZWI4ZmE2M2YwMjQ5YjBhMzRhMjMxZmM0MCJ9".to_string(); - let jwt_sig = "W4-yUKHhM7HYYhELuP9vfRH1D2IgcSSxz397SMz4u04WfLW3mBrmsaZ0QBgUwy33I7ZA6UoffnuUN8M8koXjfFMv0AfTgkQNJCg0X7cPCIn0WplONF6i4ACWUZjX_fSg36y5cRLDBv4pMOOMEI_eGyMt2tOoNZ2Fik1k-AXsyVNV-mqBtzblhdiGpy0bBgvcrMvJiBfe-AJazv-W3Ik5M0OeZB12YbQDHQSMTjhPEnADn6gmgsERBKbaGO8ieKW0v2Ukb3yqIy7PtdM44wJ0E_u2_tyqffmm6VoH6zaiFHgvEqfT7IM1w8_8k7nk2M9rT__o2A0cGWsYzhw3Mxs1Xw".to_string(); - - let openid_signature = OpenIdSig { - jwt_sig, - jwt_payload, - uid_key: "sub".to_string(), - epk_blinder, - pepper, - idc_aud_val: None, - }; - - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::OpenIdSig(openid_signature), - jwt_header, - exp_timestamp_secs: 1727812836, - ephemeral_pubkey: ephemeral_public_key, - ephemeral_signature, - }; - - let signed_txn = SignedTransaction::new_zkid(raw_txn, sender_zkid_public_key, zk_sig); - - info!("Submit openid transaction"); - info.client() + info!("Submit OpenID transaction"); + let result = swarm + .aptos_public_info() + .client() .submit_without_serializing_response(&signed_txn) - .await - .unwrap(); -} - -#[tokio::test] -async fn test_zkid_oidc_signature_transaction_submission_fails_jwt_verification() { - let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) - .with_aptos() - .build_with_cli(0) .await; - let _ = test_setup(&mut swarm, &mut cli).await; - let mut info = swarm.aptos_public_info(); - let pepper = Pepper::new([0u8; 31]); - let idc = - IdCommitment::new_from_preimage(&pepper, "test_client_id", "sub", "test_account").unwrap(); - let sender_zkid_public_key = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc, - }; - let sender_any_public_key = AnyPublicKey::zkid(sender_zkid_public_key.clone()); - let account_address = info - .create_user_account_with_any_key(&sender_any_public_key) - .await - .unwrap(); - info.mint(account_address, 10_000_000_000).await.unwrap(); - - let ephemeral_private_key: Ed25519PrivateKey = EncodingType::Hex - .decode_key( - "zkid test ephemeral private key", - "0x1111111111111111111111111111111111111111111111111111111111111111" - .as_bytes() - .to_vec(), - ) - .unwrap(); - let ephemeral_account: aptos_sdk::types::LocalAccount = LocalAccount::new( - account_address, - AccountKey::from_private_key(ephemeral_private_key), - 0, - ); - let ephemeral_public_key = EphemeralPublicKey::ed25519(ephemeral_account.public_key().clone()); - - let recipient = info - .create_and_fund_user_account(20_000_000_000) - .await - .unwrap(); - - let raw_txn = info - .transaction_factory() - .payload(aptos_stdlib::aptos_coin_transfer(recipient.address(), 100)) - .sender(account_address) - .sequence_number(1) - .build(); - - let sender_sig = ephemeral_account.private_key().sign(&raw_txn).unwrap(); - let ephemeral_signature = EphemeralSignature::ed25519(sender_sig); - - let epk_blinder = vec![0u8; 31]; - let jwt_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_string(); - let jwt_payload = "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiJ0ZXN0X2NsaWVudF9pZCIsInN1YiI6InRlc3RfYWNjb3VudCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibm9uY2UiOiIxMzIwMTc1NTc0Njg5NjI2Mjk1MjE1NjI0NDQ5OTc3ODc4Njk5NzE5Njc3NzE0MzIzOTg5Njk3NzczODY0NTIzOTkwMzIyNzI4MjE2IiwibmJmIjoxNzAyODA4OTM2LCJpYXQiOjE3MDQ5MDkyMzYsImV4cCI6MTcyNzgxMjgzNiwianRpIjoiZjEwYWZiZjBlN2JiOTcyZWI4ZmE2M2YwMjQ5YjBhMzRhMjMxZmM0MCJ9".to_string(); - let jwt_sig = "bad_signature".to_string(); - - let openid_signature = OpenIdSig { - jwt_sig, - jwt_payload, - uid_key: "sub".to_string(), - epk_blinder, - pepper, - idc_aud_val: None, - }; + if let Err(e) = result { + panic!("Error with OpenID TXN verification: {:?}", e) + } +} - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::OpenIdSig(openid_signature), - jwt_header, - exp_timestamp_secs: 1727812836, - ephemeral_pubkey: ephemeral_public_key, - ephemeral_signature, - }; +#[tokio::test] +async fn test_zkid_oidc_txn_with_bad_jwt_sig() { + let (tw_sk, mut swarm) = setup_local_net().await; + let (mut zkid_sig, zkid_pk) = get_sample_zkid_openid_sig_and_pk(); + + match &mut zkid_sig.sig { + ZkpOrOpenIdSig::Groth16Zkp(_) => panic!("Internal inconsistency"), + ZkpOrOpenIdSig::OpenIdSig(openid_sig) => { + openid_sig.jwt_sig_b64 = "bad signature".to_string() // Mauling the signature + }, + } - let signed_txn = SignedTransaction::new_zkid(raw_txn, sender_zkid_public_key, zk_sig); + let mut info = swarm.aptos_public_info(); + let signed_txn = sign_zkid_transaction(&mut info, zkid_sig, zkid_pk, &tw_sk).await; - info!("Submit openid transaction"); - let _err = info + info!("Submit OpenID transaction with bad JWT signature"); + let result = info .client() .submit_without_serializing_response(&signed_txn) - .await - .unwrap_err(); + .await; + + if result.is_ok() { + panic!("OpenID TXN with bad JWT signature should have failed verification") + } } #[tokio::test] -async fn test_zkid_oidc_signature_transaction_submission_epk_expired() { - let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) - .with_aptos() - .build_with_cli(0) - .await; - let _ = test_setup(&mut swarm, &mut cli).await; - let mut info = swarm.aptos_public_info(); +async fn test_zkid_oidc_txn_with_expired_epk() { + let (tw_sk, mut swarm) = setup_local_net().await; + let (mut zkid_sig, zkid_pk) = get_sample_zkid_openid_sig_and_pk(); - let pepper = Pepper::new([0u8; 31]); - let idc = - IdCommitment::new_from_preimage(&pepper, "test_client_id", "sub", "test_account").unwrap(); - let sender_zkid_public_key = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc, - }; - let sender_any_public_key = AnyPublicKey::zkid(sender_zkid_public_key.clone()); - let account_address = info - .create_user_account_with_any_key(&sender_any_public_key) - .await - .unwrap(); - info.mint(account_address, 10_000_000_000).await.unwrap(); - - let ephemeral_private_key: Ed25519PrivateKey = EncodingType::Hex - .decode_key( - "zkid test ephemeral private key", - "0x1111111111111111111111111111111111111111111111111111111111111111" - .as_bytes() - .to_vec(), - ) - .unwrap(); - let ephemeral_account: aptos_sdk::types::LocalAccount = LocalAccount::new( - account_address, - AccountKey::from_private_key(ephemeral_private_key), - 0, - ); - let ephemeral_public_key = EphemeralPublicKey::ed25519(ephemeral_account.public_key().clone()); - - let recipient = info - .create_and_fund_user_account(20_000_000_000) - .await - .unwrap(); + zkid_sig.exp_timestamp_secs = 1; // This should fail the verification since the expiration date is way in the past - let raw_txn = info - .transaction_factory() - .payload(aptos_stdlib::aptos_coin_transfer(recipient.address(), 100)) - .sender(account_address) - .sequence_number(1) - .build(); + let mut info = swarm.aptos_public_info(); + let signed_txn = sign_zkid_transaction(&mut info, zkid_sig, zkid_pk, &tw_sk).await; - let sender_sig = ephemeral_account.private_key().sign(&raw_txn).unwrap(); - let ephemeral_signature = EphemeralSignature::ed25519(sender_sig); - - let epk_blinder = vec![0u8; 31]; - let jwt_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_string(); - let jwt_payload = "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiJ0ZXN0X2NsaWVudF9pZCIsInN1YiI6InRlc3RfYWNjb3VudCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibm9uY2UiOiIxMzIwMTc1NTc0Njg5NjI2Mjk1MjE1NjI0NDQ5OTc3ODc4Njk5NzE5Njc3NzE0MzIzOTg5Njk3NzczODY0NTIzOTkwMzIyNzI4MjE2IiwibmJmIjoxNzAyODA4OTM2LCJpYXQiOjE3MDQ5MDkyMzYsImV4cCI6MTcyNzgxMjgzNiwianRpIjoiZjEwYWZiZjBlN2JiOTcyZWI4ZmE2M2YwMjQ5YjBhMzRhMjMxZmM0MCJ9".to_string(); - let jwt_sig = "W4-yUKHhM7HYYhELuP9vfRH1D2IgcSSxz397SMz4u04WfLW3mBrmsaZ0QBgUwy33I7ZA6UoffnuUN8M8koXjfFMv0AfTgkQNJCg0X7cPCIn0WplONF6i4ACWUZjX_fSg36y5cRLDBv4pMOOMEI_eGyMt2tOoNZ2Fik1k-AXsyVNV-mqBtzblhdiGpy0bBgvcrMvJiBfe-AJazv-W3Ik5M0OeZB12YbQDHQSMTjhPEnADn6gmgsERBKbaGO8ieKW0v2Ukb3yqIy7PtdM44wJ0E_u2_tyqffmm6VoH6zaiFHgvEqfT7IM1w8_8k7nk2M9rT__o2A0cGWsYzhw3Mxs1Xw".to_string(); - - let openid_signature = OpenIdSig { - jwt_sig, - jwt_payload, - uid_key: "sub".to_string(), - epk_blinder, - pepper, - idc_aud_val: None, - }; + info!("Submit OpenID transaction with expired EPK"); + let result = info + .client() + .submit_without_serializing_response(&signed_txn) + .await; - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::OpenIdSig(openid_signature), - jwt_header, - exp_timestamp_secs: 1, // Expired timestamp - ephemeral_pubkey: ephemeral_public_key, - ephemeral_signature, - }; + if result.is_ok() { + panic!("OpenID TXN with expired EPK should have failed verification") + } +} - let signed_txn = SignedTransaction::new_zkid(raw_txn, sender_zkid_public_key, zk_sig); +#[tokio::test] +async fn test_zkid_groth16_verifies() { + let (_, _, mut swarm, signed_txn) = + get_zkid_transaction(get_sample_zkid_groth16_sig_and_pk).await; - info!("Submit openid transaction"); - let _err = info + info!("Submit zkID Groth16 transaction"); + let result = swarm + .aptos_public_info() .client() .submit_without_serializing_response(&signed_txn) - .await - .unwrap_err(); + .await; + + if let Err(e) = result { + panic!("Error with zkID Groth16 TXN verification: {:?}", e) + } } #[tokio::test] -async fn test_zkid_groth16_verifies() { - let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) - .with_aptos() - .build_with_cli(0) - .await; - let tw_sk = test_setup(&mut swarm, &mut cli).await; +async fn test_zkid_groth16_with_mauled_proof() { + let (tw_sk, mut swarm) = setup_local_net().await; + let (mut zkid_sig, zkid_pk) = get_sample_zkid_groth16_sig_and_pk(); + + match &mut zkid_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 mut info = swarm.aptos_public_info(); + let signed_txn = sign_zkid_transaction(&mut info, zkid_sig, zkid_pk, &tw_sk).await; - let pepper = Pepper::from_number(76); - let idc = IdCommitment::new_from_preimage( - &pepper, - "407408718192.apps.googleusercontent.com", - "sub", - "113990307082899718775", - ) - .unwrap(); - let sender_zkid_public_key = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc, - }; - let sender_any_public_key = AnyPublicKey::zkid(sender_zkid_public_key.clone()); - let account_address = info - .create_user_account_with_any_key(&sender_any_public_key) - .await - .unwrap(); - info.mint(account_address, 10_000_000_000).await.unwrap(); - - let ephemeral_private_key: Ed25519PrivateKey = EncodingType::Hex - .decode_key( - "zkid test ephemeral private key", - "0x76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" - .as_bytes() - .to_vec(), - ) - .unwrap(); - let ephemeral_account: aptos_sdk::types::LocalAccount = LocalAccount::new( - account_address, - AccountKey::from_private_key(ephemeral_private_key), - 0, - ); - let ephemeral_public_key = EphemeralPublicKey::ed25519(ephemeral_account.public_key().clone()); + info!("Submit zkID Groth16 transaction"); + let result = info + .client() + .submit_without_serializing_response(&signed_txn) + .await; - let recipient = info - .create_and_fund_user_account(20_000_000_000) - .await - .unwrap(); + if result.is_ok() { + panic!("zkID Groth16 TXN with mauled proof should have failed verification") + } +} - let raw_txn = info - .transaction_factory() - .payload(aptos_stdlib::aptos_coin_transfer(recipient.address(), 100)) - .sender(account_address) - .sequence_number(1) - .build(); +#[tokio::test] +async fn test_zkid_groth16_with_bad_tw_signature() { + let (_tw_sk, mut swarm) = setup_local_net().await; + let (zkid_sig, zkid_pk) = get_sample_zkid_groth16_sig_and_pk(); - let sender_sig = ephemeral_account.private_key().sign(&raw_txn).unwrap(); - let ephemeral_signature = EphemeralSignature::ed25519(sender_sig); - - let a = G1Bytes::new_unchecked( - "20534193224874816823038374805971256353897254359389549519579800571198905682623", - "3128047629776327625062258700337193014005673411952335683536865294076478098678", - ) - .unwrap(); - let b = G2Bytes::new_unchecked( - [ - "11831059544281359959902363827760224027191828999098259913907764686593049260801", - "14933419822301565783764657928814181728459886670248956535955133596731082875810", - ], - [ - "16616167200367085072660100259194052934821478809307596510515652443339946625933", - "1103855954970567341442645156173756328940907403537523212700521414512165362008", - ], - ) - .unwrap(); - let c = G1Bytes::new_unchecked( - "296457556259014920933232985275282694032456344171046224944953719399946325676", - "10314488872240559867545387237625153841351761679810222583912967187658678987385", - ) - .unwrap(); - let proof = Groth16Zkp::new(a, b, c); - - let jwt_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_string(); - - let proof_sig = ephemeral_account.private_key().sign(&proof).unwrap(); - let ephem_proof_sig = EphemeralSignature::ed25519(proof_sig); - - // TODO(zkid): Refactor tests to be modular and add test for bad training wheels signature (commented out below). - //let bad_sk = Ed25519PrivateKey::generate(&mut thread_rng()); - let config = Configuration::new_for_devnet_and_testing(); - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::Groth16Zkp(SignedGroth16Zkp { - proof: proof.clone(), - non_malleability_signature: ephem_proof_sig, - extra_field: "\"family_name\":\"Straka\",".to_string(), - exp_horizon_secs: config.max_exp_horizon_secs, - override_aud_val: None, - training_wheels_signature: Some(EphemeralSignature::ed25519( - tw_sk.sign(&proof).unwrap(), - )), - }), - jwt_header, - exp_timestamp_secs: 1900255944, - ephemeral_pubkey: ephemeral_public_key, - ephemeral_signature, - }; + let mut info = swarm.aptos_public_info(); - let signed_txn = SignedTransaction::new_zkid(raw_txn, sender_zkid_public_key, zk_sig); + // using the sample ESK rather than the TW SK to get a bad training wheels signature + let signed_txn = sign_zkid_transaction(&mut info, zkid_sig, zkid_pk, &get_sample_esk()).await; - info!("Submit zero knowledge transaction"); + info!("Submit zkID Groth16 transaction"); let result = info .client() .submit_without_serializing_response(&signed_txn) .await; - if let Err(e) = result { - panic!("Error with Groth16 TXN verification: {:?}", e) + if result.is_ok() { + panic!( + "zkID Groth16 TXN with bad training wheels signature should have failed verification" + ) } } -#[tokio::test] -async fn test_zkid_groth16_signature_transaction_submission_proof_signature_check_fails() { - let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) - .with_aptos() - .build_with_cli(0) - .await; - let tw_sk = test_setup(&mut swarm, &mut cli).await; - let mut info = swarm.aptos_public_info(); - - let pepper = Pepper::from_number(76); - let idc = IdCommitment::new_from_preimage( - &pepper, - "407408718192.apps.googleusercontent.com", - "sub", - "113990307082899718775", - ) - .unwrap(); - let sender_zkid_public_key = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc, - }; - let sender_any_public_key = AnyPublicKey::zkid(sender_zkid_public_key.clone()); - let account_address = info - .create_user_account_with_any_key(&sender_any_public_key) +async fn sign_zkid_transaction<'a>( + info: &mut AptosPublicInfo<'a>, + mut zkid_sig: ZkIdSignature, + zkid_pk: ZkIdPublicKey, + tw_sk: &Ed25519PrivateKey, +) -> SignedTransaction { + let zkid_addr = info + .create_user_account_with_any_key(&AnyPublicKey::zkid(zkid_pk.clone())) .await .unwrap(); - info.mint(account_address, 10_000_000_000).await.unwrap(); - - let ephemeral_private_key: Ed25519PrivateKey = EncodingType::Hex - .decode_key( - "zkid test ephemeral private key", - "0x76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" - .as_bytes() - .to_vec(), - ) - .unwrap(); - let ephemeral_account: aptos_sdk::types::LocalAccount = LocalAccount::new( - account_address, - AccountKey::from_private_key(ephemeral_private_key), - 0, - ); - let ephemeral_public_key = EphemeralPublicKey::ed25519(ephemeral_account.public_key().clone()); + info.mint(zkid_addr, 10_000_000_000).await.unwrap(); let recipient = info .create_and_fund_user_account(20_000_000_000) @@ -460,66 +184,54 @@ async fn test_zkid_groth16_signature_transaction_submission_proof_signature_chec let raw_txn = info .transaction_factory() .payload(aptos_stdlib::aptos_coin_transfer(recipient.address(), 100)) - .sender(account_address) + .sender(zkid_addr) .sequence_number(1) .build(); - let sender_sig = ephemeral_account.private_key().sign(&raw_txn).unwrap(); - let ephemeral_signature = EphemeralSignature::ed25519(sender_sig); - - let a = G1Bytes::new_unchecked( - "20534193224874816823038374805971256353897254359389549519579800571198905682623", - "3128047629776327625062258700337193014005673411952335683536865294076478098678", - ) - .unwrap(); - let b = G2Bytes::new_unchecked( - [ - "11831059544281359959902363827760224027191828999098259913907764686593049260801", - "14933419822301565783764657928814181728459886670248956535955133596731082875810", - ], - [ - "16616167200367085072660100259194052934821478809307596510515652443339946625933", - "1103855954970567341442645156173756328940907403537523212700521414512165362008", - ], - ) - .unwrap(); - let c = G1Bytes::new_unchecked( - "296457556259014920933232985275282694032456344171046224944953719399946325676", - "10314488872240559867545387237625153841351761679810222583912967187658678987385", - ) - .unwrap(); - let proof = Groth16Zkp::new(a, b, c); - - let jwt_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_string(); - - let config = Configuration::new_for_devnet_and_testing(); - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::Groth16Zkp(SignedGroth16Zkp { - proof: proof.clone(), - non_malleability_signature: ephemeral_signature.clone(), // Wrong signature - extra_field: "\"family_name\":\"Straka\",".to_string(), - exp_horizon_secs: config.max_exp_horizon_secs, - override_aud_val: None, - training_wheels_signature: Some(EphemeralSignature::ed25519( - tw_sk.sign(&proof).unwrap(), - )), - }), - jwt_header, - exp_timestamp_secs: 1900255944, - ephemeral_pubkey: ephemeral_public_key, - ephemeral_signature, - }; + let esk = get_sample_esk(); + zkid_sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); + + // Compute the training wheels signature if not present + match &mut zkid_sig.sig { + ZkpOrOpenIdSig::Groth16Zkp(proof) => { + proof.training_wheels_signature = Some(EphemeralSignature::ed25519( + tw_sk.sign(&proof.proof).unwrap(), + )); + }, + ZkpOrOpenIdSig::OpenIdSig(_) => {}, + } + + SignedTransaction::new_zkid(raw_txn, zkid_pk, zkid_sig) +} - let signed_txn = SignedTransaction::new_zkid(raw_txn, sender_zkid_public_key, zk_sig); +async fn get_zkid_transaction( + get_pk_and_sig_func: fn() -> (ZkIdSignature, ZkIdPublicKey), +) -> (ZkIdSignature, ZkIdPublicKey, LocalSwarm, SignedTransaction) { + let (tw_sk, mut swarm) = setup_local_net().await; - info!("Submit zero knowledge transaction"); - info.client() - .submit_without_serializing_response(&signed_txn) - .await - .unwrap_err(); + let (zkid_sig, zkid_pk) = get_pk_and_sig_func(); + + let mut info = swarm.aptos_public_info(); + let signed_txn = + sign_zkid_transaction(&mut info, zkid_sig.clone(), zkid_pk.clone(), &tw_sk).await; + + (zkid_sig, zkid_pk, swarm, signed_txn) } -async fn test_setup(swarm: &mut LocalSwarm, cli: &mut CliTestFramework) -> Ed25519PrivateKey { +async fn setup_local_net() -> (Ed25519PrivateKey, LocalSwarm) { + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(1) + .with_aptos() + .build_with_cli(0) + .await; + + let tw_sk = spawn_network_and_execute_gov_proposals(&mut swarm, &mut cli).await; + (tw_sk, swarm) +} + +async fn spawn_network_and_execute_gov_proposals( + swarm: &mut LocalSwarm, + cli: &mut CliTestFramework, +) -> Ed25519PrivateKey { let client = swarm.validators().next().unwrap().rest_client(); let root_idx = cli.add_account_with_address_to_cli( swarm.root_key(), @@ -543,16 +255,10 @@ async fn test_setup(swarm: &mut LocalSwarm, cli: &mut CliTestFramework) -> Ed255 .get_account_resource_bcs::(AccountAddress::ONE, "0x1::zkid::Configuration") .await; let config = maybe_response.unwrap().into_inner(); - println!("zkID configuration: {:?}", config); - - let iss = "https://accounts.google.com"; - let jwk = RSA_JWK { - kid:"test_jwk".to_owned(), - kty:"RSA".to_owned(), - alg:"RS256".to_owned(), - e:"AQAB".to_owned(), - n:"6S7asUuzq5Q_3U9rbs-PkDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb_XqZaKgSYaC_h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONYW5Zu3PwyvAWk5D6ueIUhLtYzpcB-etoNdL3Ir2746KIy_VUsDwAM7dhrqSK8U2xFCGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAKctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcajtw".to_owned(), - }; + println!("zkID configuration before: {:?}", config); + + let iss = get_sample_iss(); + let jwk = get_sample_jwk(); let training_wheels_sk = Ed25519PrivateKey::generate(&mut thread_rng()); let training_wheels_pk = Ed25519PublicKey::from(&training_wheels_sk); @@ -568,25 +274,29 @@ use std::string::utf8; use std::option; fun main(core_resources: &signer) {{ let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); - let google_jwk_0 = jwks::new_rsa_jwk( + let jwk_0 = jwks::new_rsa_jwk( + utf8(b"{}"), + utf8(b"{}"), utf8(b"{}"), - utf8(b"RS256"), - utf8(b"AQAB"), utf8(b"{}") ); let patches = vector[ jwks::new_patch_remove_all(), - jwks::new_patch_upsert_jwk(b"{}", google_jwk_0), + jwks::new_patch_upsert_jwk(b"{}", jwk_0), ]; jwks::set_patches(&framework_signer, patches); + zkid::update_max_exp_horizon(&framework_signer, {}); zkid::update_training_wheels(&framework_signer, option::some(x"{}")); }} }} "#, jwk.kid, + jwk.alg, + jwk.e, jwk.n, iss, + Configuration::new_for_testing().max_exp_horizon_secs, hex::encode(training_wheels_pk.to_bytes()) ); @@ -599,13 +309,19 @@ fun main(core_resources: &signer) {{ let expected_providers_jwks = AllProvidersJWKs { entries: vec![ProviderJWKs { - issuer: b"https://accounts.google.com".to_vec(), + issuer: iss.into_bytes(), version: 0, jwks: vec![JWKMoveStruct::from(JWK::RSA(jwk))], }], }; assert_eq!(expected_providers_jwks, patched_jwks.jwks); + let maybe_response = client + .get_account_resource_bcs::(AccountAddress::ONE, "0x1::zkid::Configuration") + .await; + let config = maybe_response.unwrap().into_inner(); + println!("zkID configuration after: {:?}", config); + let mut info = swarm.aptos_public_info(); // Increment sequence number since we patched a JWK diff --git a/types/Cargo.toml b/types/Cargo.toml index 20b73cf3b09b2..2c2a0ae267150 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -44,6 +44,8 @@ proptest = { workspace = true, optional = true } proptest-derive = { workspace = true, optional = true } rand = { workspace = true } rayon = { workspace = true } +ring = { workspace = true } +rsa = { workspace = true } serde = { workspace = true } serde-big-array = { workspace = true } serde_bytes = { workspace = true } diff --git a/types/src/bn254_circom.rs b/types/src/bn254_circom.rs deleted file mode 100644 index 3e6098ba3d468..0000000000000 --- a/types/src/bn254_circom.rs +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright © Aptos Foundation - -use crate::{ - jwks::rsa::RSA_JWK, - move_utils::as_move_value::AsMoveValue, - zkid::{Configuration, IdCommitment, ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig}, -}; -use anyhow::bail; -use aptos_crypto::{poseidon_bn254, CryptoMaterialError}; -use ark_bn254::{Bn254, Fq, Fq2, Fr, G1Affine, G1Projective, G2Affine, G2Projective}; -use ark_ff::PrimeField; -use ark_groth16::{PreparedVerifyingKey, VerifyingKey}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use move_core_types::{ - ident_str, - identifier::IdentStr, - move_resource::MoveStructType, - value::{MoveStruct, MoveValue}, -}; -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; -use serde_big_array::BigArray; -use std::fmt::{Display, Formatter}; - -// TODO(zkid): Some of this stuff, if not all, belongs to the aptos-crypto crate - -pub const G1_PROJECTIVE_COMPRESSED_NUM_BYTES: usize = 32; -pub const G2_PROJECTIVE_COMPRESSED_NUM_BYTES: usize = 64; - -/// Useful macro for arkworks serialization! -macro_rules! serialize { - ($obj:expr) => {{ - let mut buf = vec![]; - $obj.serialize_compressed(&mut buf).unwrap(); - buf - }}; -} - -/// Reflection of aptos_framework::zkid::Groth16PreparedVerificationKey -#[derive(Serialize, Deserialize, Debug)] -pub struct Groth16VerificationKey { - pub alpha_g1: Vec, - pub beta_g2: Vec, - pub gamma_g2: Vec, - pub delta_g2: Vec, - pub gamma_abc_g1: Vec>, -} - -impl AsMoveValue for Groth16VerificationKey { - fn as_move_value(&self) -> MoveValue { - MoveValue::Struct(MoveStruct::Runtime(vec![ - self.alpha_g1.as_move_value(), - self.beta_g2.as_move_value(), - self.gamma_g2.as_move_value(), - self.delta_g2.as_move_value(), - self.gamma_abc_g1.as_move_value(), - ])) - } -} - -/// WARNING: This struct uses resource groups on the Move side. Do NOT implement OnChainConfig -/// for it, since `OnChainConfig::fetch_config` does not work with resource groups (yet). -impl MoveStructType for Groth16VerificationKey { - const MODULE_NAME: &'static IdentStr = ident_str!("zkid"); - const STRUCT_NAME: &'static IdentStr = ident_str!("Groth16VerificationKey"); -} - -impl TryFrom for PreparedVerifyingKey { - type Error = CryptoMaterialError; - - fn try_from(vk: Groth16VerificationKey) -> Result { - if vk.gamma_abc_g1.len() != 2 { - return Err(CryptoMaterialError::DeserializationError); - } - - Ok(Self::from(VerifyingKey { - alpha_g1: G1Affine::deserialize_compressed(vk.alpha_g1.as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError)?, - beta_g2: G2Affine::deserialize_compressed(vk.beta_g2.as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError)?, - gamma_g2: G2Affine::deserialize_compressed(vk.gamma_g2.as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError)?, - delta_g2: G2Affine::deserialize_compressed(vk.delta_g2.as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError)?, - gamma_abc_g1: vec![ - G1Affine::deserialize_compressed(vk.gamma_abc_g1[0].as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError)?, - G1Affine::deserialize_compressed(vk.gamma_abc_g1[1].as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError)?, - ], - })) - } -} - -impl From> for Groth16VerificationKey { - fn from(pvk: PreparedVerifyingKey) -> Self { - let PreparedVerifyingKey { - vk: - VerifyingKey { - alpha_g1, - beta_g2, - gamma_g2, - delta_g2, - gamma_abc_g1, - }, - alpha_g1_beta_g2: _alpha_g1_beta_g2, // unnecessary for Move - gamma_g2_neg_pc: _gamma_g2_neg_pc, // unnecessary for Move - delta_g2_neg_pc: _delta_g2_neg_pc, // unnecessary for Move - } = pvk; - - let mut gamma_abc_g1_bytes = Vec::with_capacity(gamma_abc_g1.len()); - for e in gamma_abc_g1.iter() { - gamma_abc_g1_bytes.push(serialize!(e)); - } - - Groth16VerificationKey { - alpha_g1: serialize!(alpha_g1), - beta_g2: serialize!(beta_g2), - gamma_g2: serialize!(gamma_g2), - delta_g2: serialize!(delta_g2), - gamma_abc_g1: gamma_abc_g1_bytes, - } - } -} - -impl Display for Groth16VerificationKey { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "alpha_g1: {}", hex::encode(&self.alpha_g1))?; - write!(f, "beta_g2: {}", hex::encode(&self.beta_g2))?; - write!(f, "gamma_g2: {}", hex::encode(&self.gamma_g2))?; - write!(f, "delta_g2: {}", hex::encode(&self.delta_g2))?; - for (i, e) in self.gamma_abc_g1.iter().enumerate() { - write!(f, "gamma_abc_g1[{i}]: {}", hex::encode(serialize!(e)))?; - } - Ok(()) - } -} - -pub static DEVNET_VERIFYING_KEY: Lazy> = Lazy::new(devnet_pvk); - -/// This will do the proper subgroup membership checks. -fn g1_projective_str_to_affine(x: &str, y: &str) -> anyhow::Result { - let g1_affine = G1Bytes::new_unchecked(x, y)?.deserialize_into_affine()?; - Ok(g1_affine) -} - -/// This will do the proper subgroup membership checks. -fn g2_projective_str_to_affine(x: [&str; 2], y: [&str; 2]) -> anyhow::Result { - let g2_affine = G2Bytes::new_unchecked(x, y)?.to_affine()?; - Ok(g2_affine) -} - -/// This function uses the decimal uncompressed point serialization which is outputted by circom. -fn devnet_pvk() -> PreparedVerifyingKey { - // Convert the projective points to affine. - let alpha_g1 = g1_projective_str_to_affine( - "16672231080302629756836614130913173861541009360974119524782950408048375831661", - "1076145001163048025135533382088266750240489485046298539187659509488738517245", - ) - .unwrap(); - - let beta_g2 = g2_projective_str_to_affine( - [ - "1125365732643211423779651913319958385653115422366520671538751860820509133538", - "10055196097002324305342942912758079446356594743098794928675544207400347950287", - ], - [ - "10879716754714953827605171295191459580695363989155343984818520267224463075503", - "440220374146936557739765173414663598678359360031905981547938788314460390904", - ], - ) - .unwrap(); - - let gamma_g2 = g2_projective_str_to_affine( - [ - "10857046999023057135944570762232829481370756359578518086990519993285655852781", - "11559732032986387107991004021392285783925812861821192530917403151452391805634", - ], - [ - "8495653923123431417604973247489272438418190587263600148770280649306958101930", - "4082367875863433681332203403145435568316851327593401208105741076214120093531", - ], - ) - .unwrap(); - - let delta_g2 = g2_projective_str_to_affine( - [ - "10857046999023057135944570762232829481370756359578518086990519993285655852781", - "11559732032986387107991004021392285783925812861821192530917403151452391805634", - ], - [ - "8495653923123431417604973247489272438418190587263600148770280649306958101930", - "4082367875863433681332203403145435568316851327593401208105741076214120093531", - ], - ) - .unwrap(); - - let mut gamma_abc_g1 = Vec::new(); - for points in [ - g1_projective_str_to_affine( - "709845293616032000883655261014820428774807602111296273992483611119383326362", - "645961711055726048875381920095150798755926517220714963239815637182963128467", - ) - .unwrap(), - g1_projective_str_to_affine( - "9703775855460452449287141941638080366156266996878046656846622159120386001635", - "1903615495723998350630869740881559921229604803173196414121492346747062004184", - ) - .unwrap(), - ] { - gamma_abc_g1.push(points); - } - - let vk = VerifyingKey { - alpha_g1, - beta_g2, - gamma_g2, - delta_g2, - gamma_abc_g1, - }; - - PreparedVerifyingKey::from(vk) -} - -fn parse_field_element(s: &str) -> Result { - s.parse::() - .map_err(|_e| CryptoMaterialError::DeserializationError) -} - -macro_rules! serialize { - ($obj:expr, $method:ident) => {{ - let mut buf = vec![]; - $obj.$method(&mut buf)?; - buf - }}; -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub struct G1Bytes(pub(crate) [u8; G1_PROJECTIVE_COMPRESSED_NUM_BYTES]); - -impl G1Bytes { - pub fn new_unchecked(x: &str, y: &str) -> anyhow::Result { - let g1 = G1Projective::new_unchecked( - parse_field_element(x)?, - parse_field_element(y)?, - parse_field_element("1")?, - ); - - let bytes: Vec = serialize!(g1, serialize_compressed); - Self::new_from_vec(bytes) - } - - /// Used internall or for testing. - pub fn new_from_vec(vec: Vec) -> anyhow::Result { - if vec.len() == G1_PROJECTIVE_COMPRESSED_NUM_BYTES { - let mut bytes = [0; G1_PROJECTIVE_COMPRESSED_NUM_BYTES]; - bytes.copy_from_slice(&vec); - Ok(Self(bytes)) - } else { - bail!( - "Serialized BN254 G1 must have exactly {} bytes", - G1_PROJECTIVE_COMPRESSED_NUM_BYTES - ) - } - } - - pub fn deserialize_into_affine(&self) -> Result { - self.try_into() - } -} - -impl TryInto for &G1Bytes { - type Error = CryptoMaterialError; - - fn try_into(self) -> Result { - G1Projective::deserialize_compressed(self.0.as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError) - } -} - -impl TryInto for &G1Bytes { - type Error = CryptoMaterialError; - - fn try_into(self) -> Result { - let g1_projective: G1Projective = self.try_into()?; - Ok(g1_projective.into()) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub struct G2Bytes(#[serde(with = "BigArray")] pub(crate) [u8; G2_PROJECTIVE_COMPRESSED_NUM_BYTES]); - -impl G2Bytes { - pub fn new_unchecked(x: [&str; 2], y: [&str; 2]) -> anyhow::Result { - let g2 = G2Projective::new_unchecked( - Fq2::new(parse_field_element(x[0])?, parse_field_element(x[1])?), - Fq2::new(parse_field_element(y[0])?, parse_field_element(y[1])?), - Fq2::new(parse_field_element("1")?, parse_field_element("0")?), - ); - - let bytes: Vec = serialize!(g2, serialize_compressed); - Self::new_from_vec(bytes) - } - - pub fn new_from_vec(vec: Vec) -> anyhow::Result { - if vec.len() == G2_PROJECTIVE_COMPRESSED_NUM_BYTES { - let mut bytes = [0; G2_PROJECTIVE_COMPRESSED_NUM_BYTES]; - bytes.copy_from_slice(&vec); - Ok(Self(bytes)) - } else { - bail!( - "Serialized BN254 G2 must have exactly {} bytes", - G2_PROJECTIVE_COMPRESSED_NUM_BYTES - ) - } - } - - pub fn to_affine(&self) -> Result { - self.try_into() - } -} - -impl TryInto for &G2Bytes { - type Error = CryptoMaterialError; - - fn try_into(self) -> Result { - G2Projective::deserialize_compressed(self.0.as_slice()) - .map_err(|_| CryptoMaterialError::DeserializationError) - } -} - -impl TryInto for &G2Bytes { - type Error = CryptoMaterialError; - - fn try_into(self) -> Result { - let g2_projective: G2Projective = self.try_into()?; - Ok(g2_projective.into()) - } -} - -pub fn get_public_inputs_hash( - sig: &ZkIdSignature, - pk: &ZkIdPublicKey, - jwk: &RSA_JWK, - exp_horizon_secs: u64, - config: &Configuration, -) -> anyhow::Result { - let extra_field_hashed; - let override_aud_val_hashed; - let use_override_aud; - if let ZkpOrOpenIdSig::Groth16Zkp(proof) = &sig.sig { - extra_field_hashed = poseidon_bn254::pad_and_hash_string( - &proof.extra_field, - config.max_extra_field_bytes as usize, - )?; - if let Some(override_aud_val) = &proof.override_aud_val { - use_override_aud = ark_bn254::Fr::from(1); - override_aud_val_hashed = poseidon_bn254::pad_and_hash_string( - override_aud_val, - IdCommitment::MAX_AUD_VAL_BYTES, - )?; - } else { - use_override_aud = ark_bn254::Fr::from(0); - override_aud_val_hashed = - poseidon_bn254::pad_and_hash_string("", IdCommitment::MAX_AUD_VAL_BYTES)?; - } - } else { - bail!("Cannot get_public_inputs_hash for ZkIdSignature") - } - - // Add the epk as padded and packed scalars - let mut frs = poseidon_bn254::pad_and_pack_bytes_to_scalars_with_len( - sig.ephemeral_pubkey.to_bytes().as_slice(), - config.max_commited_epk_bytes as usize, - )?; - - // Add the id_commitment as a scalar - frs.push(Fr::from_le_bytes_mod_order(&pk.idc.0)); - - // Add the exp_timestamp_secs as a scalar - frs.push(Fr::from(sig.exp_timestamp_secs)); - - // Add the epk lifespan as a scalar - frs.push(Fr::from(exp_horizon_secs)); - - // Add the hash of the iss (formatted key-value pair string). - let formatted_iss = format!("\"iss\":\"{}\",", pk.iss); - frs.push(poseidon_bn254::pad_and_hash_string( - &formatted_iss, - config.max_iss_field_bytes as usize, - )?); - - frs.push(extra_field_hashed); - - // Add the hash of the jwt_header with the "." separator appended - let jwt_header_with_separator = format!("{}.", sig.jwt_header); - frs.push(poseidon_bn254::pad_and_hash_string( - &jwt_header_with_separator, - config.max_jwt_header_b64_bytes as usize, - )?); - - frs.push(jwk.to_poseidon_scalar()?); - - frs.push(override_aud_val_hashed); - - frs.push(use_override_aud); - - poseidon_bn254::hash_scalars(frs) -} - -#[cfg(test)] -mod test { - use crate::bn254_circom::{ - devnet_pvk, G1Bytes, G2Bytes, Groth16VerificationKey, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, - G2_PROJECTIVE_COMPRESSED_NUM_BYTES, - }; - use ark_bn254::Bn254; - use ark_groth16::PreparedVerifyingKey; - - #[test] - pub fn test_bn254_serialized_sizes() { - let g1 = G1Bytes::new_unchecked( - "16672231080302629756836614130913173861541009360974119524782950408048375831661", - "1076145001163048025135533382088266750240489485046298539187659509488738517245", - ) - .unwrap(); - - let g2 = G2Bytes::new_unchecked( - [ - "1125365732643211423779651913319958385653115422366520671538751860820509133538", - "10055196097002324305342942912758079446356594743098794928675544207400347950287", - ], - [ - "10879716754714953827605171295191459580695363989155343984818520267224463075503", - "440220374146936557739765173414663598678359360031905981547938788314460390904", - ], - ) - .unwrap(); - - let g1_bytes = bcs::to_bytes(&g1).unwrap(); - assert_eq!(g1_bytes.len(), G1_PROJECTIVE_COMPRESSED_NUM_BYTES); - - let g2_bytes = bcs::to_bytes(&g2).unwrap(); - assert_eq!(g2_bytes.len(), G2_PROJECTIVE_COMPRESSED_NUM_BYTES); - } - - #[test] - // Tests conversion between the devnet ark_groth16::PreparedVerificationKey and our Move - // representation of it. - fn print_groth16_pvk() { - let groth16_vk: Groth16VerificationKey = devnet_pvk().into(); - let same_pvk: PreparedVerifyingKey = groth16_vk.try_into().unwrap(); - - assert_eq!(same_pvk, devnet_pvk()); - } -} diff --git a/types/src/lib.rs b/types/src/lib.rs index a63b6fc523959..4b8212a730a89 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -61,7 +61,6 @@ pub use utility_coin::*; pub mod account_view; pub mod aggregate_signature; pub mod block_executor; -pub mod bn254_circom; pub mod bytes; pub mod delayed_fields; pub mod state_store; diff --git a/types/src/transaction/authenticator.rs b/types/src/transaction/authenticator.rs index 21a9320ae563e..abdf423193c33 100644 --- a/types/src/transaction/authenticator.rs +++ b/types/src/transaction/authenticator.rs @@ -1005,9 +1005,10 @@ impl AnySignature { }, (Self::WebAuthn { signature }, _) => signature.verify(message, public_key), (Self::ZkId { signature }, AnyPublicKey::ZkId { public_key: _ }) => { + // TODO(zkid): Batch-verify these two signatures match &signature.sig { ZkpOrOpenIdSig::Groth16Zkp(proof) => { - proof.verify_non_malleability_sig(&signature.ephemeral_pubkey)? + proof.verify_non_malleability_sig(&signature.ephemeral_pubkey)?; }, ZkpOrOpenIdSig::OpenIdSig(_) => {}, } @@ -1124,9 +1125,10 @@ impl TryFrom<&[u8]> for EphemeralPublicKey { mod tests { use super::*; use crate::{ - bn254_circom::{G1Bytes, G2Bytes}, transaction::{webauthn::AssertionSignature, SignedTransaction}, - zkid::{Configuration, Groth16Zkp, IdCommitment, OpenIdSig, Pepper, SignedGroth16Zkp}, + zkid::test_utils::{ + get_sample_esk, get_sample_zkid_groth16_sig_and_pk, get_sample_zkid_openid_sig_and_pk, + }, }; use aptos_crypto::{ ed25519::Ed25519PrivateKey, @@ -1664,176 +1666,97 @@ mod tests { } #[test] - fn test_zkid_oidc_sig_fails_with_bad_ephemeral_signature() { - let pepper = Pepper::from_number(76); - let idc = - IdCommitment::new_from_preimage(&pepper, "s6BhdRkqt3", "sub", "248289761001").unwrap(); - let sender_zkid_public_key = ZkIdPublicKey { - iss: "https://server.example.com".to_owned(), - idc, - }; - let sender_any_public_key = AnyPublicKey::zkid(sender_zkid_public_key); - let sender_auth_key = AuthenticationKey::any_key(sender_any_public_key.clone()); - let sender_addr = sender_auth_key.account_address(); - - let esk = Ed25519PrivateKey::generate_for_testing(); - let epk = EphemeralPublicKey::ed25519(esk.public_key()); - - let raw_txn = crate::test_helpers::transaction_test_helpers::get_test_signed_transaction( + fn test_zkid_openid_txn() { + let esk = get_sample_esk(); + let (mut zkid_sig, zkid_pk) = get_sample_zkid_openid_sig_and_pk(); + let sender_addr = + AuthenticationKey::any_key(AnyPublicKey::zkid(zkid_pk.clone())).account_address(); + let mut raw_txn = crate::test_helpers::transaction_test_helpers::get_test_raw_transaction( sender_addr, 0, - &esk, - esk.public_key(), None, - 0, - 0, None, - ) - .into_raw_transaction(); - let ephemeral_sig = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); - - let bad_esk = Ed25519PrivateKey::generate(&mut thread_rng()); // Wrong private key! - let bad_ephemeral_sig = EphemeralSignature::ed25519(bad_esk.sign(&raw_txn).unwrap()); - - let openid_signature = OpenIdSig { - jwt_sig: "jwt_sig is verified in the prologue".to_string(), - jwt_payload: "JWT payload is now verified in prologue too".to_string(), - uid_key: "sub".to_string(), - epk_blinder: vec![0u8; OpenIdSig::EPK_BLINDER_NUM_BYTES], - pepper, - idc_aud_val: None, - }; - - let signed_txn = build_signature( - &sender_any_public_key, - &raw_txn, - &openid_signature, - &epk, - &ephemeral_sig, - ); - let badly_signed_txn = build_signature( - &sender_any_public_key, - &raw_txn, - &openid_signature, - &epk, - &bad_ephemeral_sig, + None, + None, ); + zkid_sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); - assert!(signed_txn.verify_signature().is_ok()); - assert!(badly_signed_txn.verify_signature().is_err()); - } - - fn build_signature( - sender_any_public_key: &AnyPublicKey, - raw_txn: &RawTransaction, - oidc_sig: &OpenIdSig, - epk: &EphemeralPublicKey, - eph_sig: &EphemeralSignature, - ) -> SignedTransaction { - let exp_timestamp_secs = 100000000000; // does not matter - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::OpenIdSig(oidc_sig.clone()), - // {"alg":"RS256","typ":"JWT"} - jwt_header: "JWT header is verified during prologue too".to_owned(), - exp_timestamp_secs, - ephemeral_pubkey: epk.clone(), - ephemeral_signature: eph_sig.clone(), - }; + let single_key_auth = + SingleKeyAuthenticator::new(AnyPublicKey::zkid(zkid_pk), AnySignature::zkid(zkid_sig)); + let account_auth = AccountAuthenticator::single_key(single_key_auth); + let signed_txn = + SignedTransaction::new_single_sender(raw_txn.clone(), account_auth.clone()); + signed_txn.verify_signature().unwrap(); - let account_auth = AccountAuthenticator::single_key(SingleKeyAuthenticator::new( - sender_any_public_key.clone(), - AnySignature::zkid(zk_sig), - )); - SignedTransaction::new_single_sender(raw_txn.clone(), account_auth) + // Badly-signed TXN + raw_txn.expiration_timestamp_secs += 1; + let signed_txn = SignedTransaction::new_single_sender(raw_txn, account_auth); + assert!(signed_txn.verify_signature().is_err()); } /// TODO(zkid): Redundancy; a similar test case is in types/src/zkid.rs #[test] - fn test_zkid_groth16_proof_verification() { - let a = G1Bytes::new_unchecked( - "20534193224874816823038374805971256353897254359389549519579800571198905682623", - "3128047629776327625062258700337193014005673411952335683536865294076478098678", - ) - .unwrap(); - let b = G2Bytes::new_unchecked( - [ - "11831059544281359959902363827760224027191828999098259913907764686593049260801", - "14933419822301565783764657928814181728459886670248956535955133596731082875810", - ], - [ - "16616167200367085072660100259194052934821478809307596510515652443339946625933", - "1103855954970567341442645156173756328940907403537523212700521414512165362008", - ], - ) - .unwrap(); - let c = G1Bytes::new_unchecked( - "296457556259014920933232985275282694032456344171046224944953719399946325676", - "10314488872240559867545387237625153841351761679810222583912967187658678987385", - ) - .unwrap(); - let proof = Groth16Zkp::new(a, b, c); - - let sender = Ed25519PrivateKey::generate_for_testing(); - let sender_pub = sender.public_key(); - let sender_auth_key = AuthenticationKey::ed25519(&sender_pub); - let sender_addr = sender_auth_key.account_address(); - let raw_txn = crate::test_helpers::transaction_test_helpers::get_test_signed_transaction( + fn test_zkid_groth16_txn() { + let esk = get_sample_esk(); + let (mut zkid_sig, zkid_pk) = get_sample_zkid_groth16_sig_and_pk(); + let sender_addr = + AuthenticationKey::any_key(AnyPublicKey::zkid(zkid_pk.clone())).account_address(); + let mut raw_txn = crate::test_helpers::transaction_test_helpers::get_test_raw_transaction( sender_addr, 0, - &sender, - sender.public_key(), None, - 0, - 0, None, - ) - .into_raw_transaction(); + None, + None, + ); + zkid_sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); - let sender_sig = sender.sign(&raw_txn).unwrap(); + let single_key_auth = + SingleKeyAuthenticator::new(AnyPublicKey::zkid(zkid_pk), AnySignature::zkid(zkid_sig)); + let account_auth = AccountAuthenticator::single_key(single_key_auth); + let signed_txn = + SignedTransaction::new_single_sender(raw_txn.clone(), account_auth.clone()); - let epk = EphemeralPublicKey::ed25519(sender.public_key()); - let es = EphemeralSignature::ed25519(sender_sig); - - let proof_sig = sender.sign(&proof).unwrap(); - let ephem_proof_sig = EphemeralSignature::ed25519(proof_sig); - ephem_proof_sig.verify(&proof, &epk).unwrap(); - let config = Configuration::new_for_devnet_and_testing(); - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::Groth16Zkp(SignedGroth16Zkp { - proof: proof.clone(), - non_malleability_signature: ephem_proof_sig, - extra_field: "\"family_name\":\"Straka\",".to_string(), - exp_horizon_secs: config.max_exp_horizon_secs, - override_aud_val: None, - training_wheels_signature: Some(EphemeralSignature::ed25519( - Ed25519Signature::dummy_signature(), - )), - }), - jwt_header: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_owned(), - exp_timestamp_secs: 1900255944, - ephemeral_pubkey: epk, - ephemeral_signature: es, - }; + signed_txn.verify_signature().unwrap(); - let pepper = Pepper::from_number(76); - let addr_seed = IdCommitment::new_from_preimage( - &pepper, - "407408718192.apps.googleusercontent.com", - "sub", - "113990307082899718775", - ) - .unwrap(); + // Badly-signed TXN + raw_txn.expiration_timestamp_secs += 1; + let signed_txn = SignedTransaction::new_single_sender(raw_txn, account_auth); + assert!(signed_txn.verify_signature().is_err()); + } - let zk_pk = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc: addr_seed, - }; + #[test] + fn test_zkid_groth16_txn_fails_non_malleability_check() { + let esk = get_sample_esk(); + let (mut zkid_sig, zkid_pk) = get_sample_zkid_groth16_sig_and_pk(); + let sender_addr = + AuthenticationKey::any_key(AnyPublicKey::zkid(zkid_pk.clone())).account_address(); + let raw_txn = crate::test_helpers::transaction_test_helpers::get_test_raw_transaction( + sender_addr, + 0, + None, + None, + None, + None, + ); + zkid_sig.ephemeral_signature = EphemeralSignature::ed25519(esk.sign(&raw_txn).unwrap()); + + let tw_sk = Ed25519PrivateKey::generate(&mut thread_rng()); + // Bad non-malleability signature + match &mut zkid_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 sk_auth = - SingleKeyAuthenticator::new(AnyPublicKey::zkid(zk_pk), AnySignature::zkid(zk_sig)); - let account_auth = AccountAuthenticator::single_key(sk_auth); + let single_key_auth = + SingleKeyAuthenticator::new(AnyPublicKey::zkid(zkid_pk), AnySignature::zkid(zkid_sig)); + let account_auth = AccountAuthenticator::single_key(single_key_auth); let signed_txn = SignedTransaction::new_single_sender(raw_txn, account_auth); - let verification_result = signed_txn.verify_signature(); - verification_result.unwrap(); + + assert!(signed_txn.verify_signature().is_err()); } } diff --git a/types/src/zkid.rs b/types/src/zkid.rs deleted file mode 100644 index a76bade434b90..0000000000000 --- a/types/src/zkid.rs +++ /dev/null @@ -1,967 +0,0 @@ -// Copyright © Aptos Foundation - -use crate::{ - bn254_circom::{G1Bytes, G2Bytes}, - jwks::rsa::RSA_JWK, - move_utils::as_move_value::AsMoveValue, - on_chain_config::CurrentTimeMicroseconds, - transaction::{ - authenticator::{ - AnyPublicKey, AnySignature, EphemeralPublicKey, EphemeralSignature, MAX_NUM_OF_SIGS, - }, - SignedTransaction, - }, -}; -use anyhow::{bail, ensure, Context, Result}; -use aptos_crypto::{poseidon_bn254, CryptoMaterialError, ValidCryptoMaterial}; -use aptos_crypto_derive::{BCSCryptoHash, CryptoHasher}; -use ark_bn254::{self, Bn254, Fr}; -use ark_groth16::{Groth16, PreparedVerifyingKey, Proof}; -use ark_serialize::CanonicalSerialize; -use base64::URL_SAFE_NO_PAD; -use move_core_types::{ - ident_str, - identifier::IdentStr, - move_resource::MoveStructType, - value::{MoveStruct, MoveValue}, - vm_status::{StatusCode, VMStatus}, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use serde_with::skip_serializing_none; -use std::{ - collections::BTreeMap, - str, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -#[macro_export] -macro_rules! invalid_signature { - ($message:expr) => { - VMStatus::error(StatusCode::INVALID_SIGNATURE, Some($message.to_owned())) - }; -} - -/// The size of the pepper used to create a _hiding_ identity commitment (IDC) when deriving a zkID -/// address. This value should **NOT* be changed since on-chain addresses are based on it (e.g., -/// hashing with a larger pepper would lead to a different address). -pub const PEPPER_NUM_BYTES: usize = poseidon_bn254::BYTES_PACKED_PER_SCALAR; - -/// Reflection of aptos_framework::zkid::Configs -#[derive(Serialize, Deserialize, Debug)] -pub struct Configuration { - pub override_aud_vals: Vec, - pub max_zkid_signatures_per_txn: u16, - pub max_exp_horizon_secs: u64, - pub training_wheels_pubkey: Option>, - pub nonce_commitment_num_bytes: u16, - pub max_commited_epk_bytes: u16, - pub max_iss_field_bytes: u16, - pub max_extra_field_bytes: u16, - pub max_jwt_header_b64_bytes: u32, -} - -impl AsMoveValue for Configuration { - fn as_move_value(&self) -> MoveValue { - MoveValue::Struct(MoveStruct::Runtime(vec![ - self.override_aud_vals.as_move_value(), - self.max_zkid_signatures_per_txn.as_move_value(), - self.max_exp_horizon_secs.as_move_value(), - self.training_wheels_pubkey.as_move_value(), - self.nonce_commitment_num_bytes.as_move_value(), - self.max_commited_epk_bytes.as_move_value(), - self.max_iss_field_bytes.as_move_value(), - self.max_extra_field_bytes.as_move_value(), - self.max_jwt_header_b64_bytes.as_move_value(), - ])) - } -} - -/// WARNING: This struct uses resource groups on the Move side. Do NOT implement OnChainConfig -/// for it, since `OnChainConfig::fetch_config` does not work with resource groups (yet). -impl MoveStructType for Configuration { - const MODULE_NAME: &'static IdentStr = ident_str!("zkid"); - const STRUCT_NAME: &'static IdentStr = ident_str!("Configuration"); -} - -impl Configuration { - /// Should only be used for testing. - pub const OVERRIDE_AUD_FOR_TESTING: &'static str = "some_override_aud"; - - pub fn new_for_devnet_and_testing() -> Configuration { - const POSEIDON_BYTES_PACKED_PER_SCALAR: u16 = 31; - - Configuration { - override_aud_vals: vec![Self::OVERRIDE_AUD_FOR_TESTING.to_owned()], - max_zkid_signatures_per_txn: 3, - max_exp_horizon_secs: 100_255_944, - training_wheels_pubkey: None, - nonce_commitment_num_bytes: 32, - max_commited_epk_bytes: 3 * POSEIDON_BYTES_PACKED_PER_SCALAR, - max_iss_field_bytes: 126, - max_extra_field_bytes: 350, - max_jwt_header_b64_bytes: 300, - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub struct JwkId { - /// The OIDC provider associated with this JWK - pub iss: String, - /// The Key ID associated with this JWK (https://datatracker.ietf.org/doc/html/rfc7517#section-4.5) - pub kid: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub struct OpenIdSig { - /// The base64url encoded JWS signature of the OIDC JWT (https://datatracker.ietf.org/doc/html/rfc7515#section-3) - pub jwt_sig: String, - /// The base64url encoded JSON payload of the OIDC JWT (https://datatracker.ietf.org/doc/html/rfc7519#section-3) - pub jwt_payload: String, - /// The name of the key in the claim that maps to the user identifier; e.g., "sub" or "email" - pub uid_key: String, - /// The random value used to obfuscate the EPK from OIDC providers in the nonce field - #[serde(with = "serde_bytes")] - pub epk_blinder: Vec, - /// The privacy-preserving value used to calculate the identity commitment. It is typically uniquely derived from `(iss, client_id, uid_key, uid_val)`. - pub pepper: Pepper, - /// When an override aud_val is used, the signature needs to contain the aud_val committed in the - /// IDC, since the JWT will contain the override. - pub idc_aud_val: Option, -} - -impl OpenIdSig { - /// The size of the blinding factor used to compute the nonce commitment to the EPK and expiration - /// date. This can be upgraded, if the OAuth nonce reconstruction is upgraded carefully. - pub const EPK_BLINDER_NUM_BYTES: usize = poseidon_bn254::BYTES_PACKED_PER_SCALAR; - - /// Verifies an `OpenIdSig` by doing the following checks: - /// 1. Check that the ephemeral public key lifespan is under MAX_EXPIRY_HORIZON_SECS - /// 2. Check that the iss claim in the ZkIdPublicKey matches the one in the jwt_payload - /// 3. Check that the identity commitment in the ZkIdPublicKey matches the one constructed from the jwt_payload - /// 4. Check that the nonce constructed from the ephemeral public key, blinder, and exp_timestamp_secs matches the one in the jwt_payload - // TODO(zkid): Refactor to return a `Result<(), VMStatus>` because (1) this is now called in the - // VM and (2) is_override_aud_allowed does. - pub fn verify_jwt_claims( - &self, - exp_timestamp_secs: u64, - epk: &EphemeralPublicKey, - pk: &ZkIdPublicKey, - config: &Configuration, - ) -> Result<()> { - let jwt_payload_json = base64url_decode_as_str(&self.jwt_payload)?; - let claims: Claims = serde_json::from_str(&jwt_payload_json)?; - - let max_expiration_date = - seconds_from_epoch(claims.oidc_claims.iat + config.max_exp_horizon_secs); - let expiration_date: SystemTime = seconds_from_epoch(exp_timestamp_secs); - - ensure!( - expiration_date < max_expiration_date, - "The ephemeral public key's expiration date is too far into the future" - ); - - ensure!( - claims.oidc_claims.iss.eq(&pk.iss), - "'iss' claim was supposed to match \"{}\"", - pk.iss - ); - - // When an aud_val override is set, the IDC-committed `aud` is included next to the - // OpenID signature. - let idc_aud_val = match self.idc_aud_val.as_ref() { - None => &claims.oidc_claims.aud, - Some(idc_aud_val) => { - // If there's an override, check that the override `aud` from the JWT, is allow-listed - ensure!( - is_allowed_override_aud(config, &claims.oidc_claims.aud).is_ok(), - "{} is not an allow-listed override aud", - &claims.oidc_claims.aud - ); - idc_aud_val - }, - }; - let uid_val = claims.get_uid_val(&self.uid_key)?; - ensure!( - IdCommitment::new_from_preimage(&self.pepper, idc_aud_val, &self.uid_key, &uid_val)? - .eq(&pk.idc), - "Address IDC verification failed" - ); - - ensure!( - self.reconstruct_oauth_nonce(exp_timestamp_secs, epk, config)? - .eq(&claims.oidc_claims.nonce), - "'nonce' claim did not contain the expected EPK and expiration date commitment" - ); - - Ok(()) - } - - pub fn verify_jwt_signature(&self, rsa_jwk: RSA_JWK, jwt_header: &String) -> Result<()> { - let jwt_payload = &self.jwt_payload; - let jwt_sig = &self.jwt_sig; - let jwt_token = format!("{}.{}.{}", jwt_header, jwt_payload, jwt_sig); - rsa_jwk.verify_signature(&jwt_token)?; - Ok(()) - } - - pub fn reconstruct_oauth_nonce( - &self, - exp_timestamp_secs: u64, - epk: &EphemeralPublicKey, - config: &Configuration, - ) -> Result { - let mut frs = poseidon_bn254::pad_and_pack_bytes_to_scalars_with_len( - epk.to_bytes().as_slice(), - config.max_commited_epk_bytes as usize, - )?; - - frs.push(Fr::from(exp_timestamp_secs)); - frs.push(poseidon_bn254::pack_bytes_to_one_scalar( - &self.epk_blinder[..], - )?); - - let nonce_fr = poseidon_bn254::hash_scalars(frs)?; - Ok(nonce_fr.to_string()) - } -} - -impl TryFrom<&[u8]> for OpenIdSig { - type Error = CryptoMaterialError; - - fn try_from(bytes: &[u8]) -> Result { - bcs::from_bytes::(bytes).map_err(|_e| CryptoMaterialError::DeserializationError) - } -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] -pub struct OidcClaims { - iss: String, - aud: String, - sub: String, - nonce: String, - iat: u64, - email: Option, - email_verified: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Claims { - #[serde(flatten)] - oidc_claims: OidcClaims, - #[serde(default)] - additional_claims: BTreeMap, -} - -impl Claims { - fn get_uid_val(&self, uid_key: &String) -> Result { - match uid_key.as_str() { - "email" => { - let email_verified = self - .oidc_claims - .email_verified - .clone() - .context("'email_verified' claim is missing")?; - // the 'email_verified' claim may be a boolean or a boolean-as-a-string. - let email_verified_as_bool = email_verified.as_bool().unwrap_or(false); - let email_verified_as_str = email_verified.as_str().unwrap_or("false"); - ensure!( - email_verified_as_bool || email_verified_as_str.eq("true"), - "'email_verified' claim was not \"true\"" - ); - self.oidc_claims - .email - .clone() - .context("email claim missing on jwt") - }, - "sub" => Ok(self.oidc_claims.sub.clone()), - _ => { - let uid_val = self - .additional_claims - .get(uid_key) - .context(format!("{} claim missing on jwt", uid_key))? - .as_str() - .context(format!("{} value is not a string", uid_key))?; - Ok(uid_val.to_string()) - }, - } - } -} - -#[derive( - Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, CryptoHasher, BCSCryptoHash, -)] -pub struct Groth16Zkp { - 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, - /// 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, - /// An extra field (e.g., `"":"") that will be matched publicly in the JWT - pub extra_field: String, - /// Will be set to the override `aud` value that the circuit should match, instead of the `aud` in the IDC. - /// This will allow users to recover their zkID accounts derived by an application that is no longer online. - pub override_aud_val: Option, - /// A signature on the proof (via the training wheels SK) to mitigate against flaws in our circuit - pub training_wheels_signature: Option, -} - -impl SignedGroth16Zkp { - pub fn verify_non_malleability_sig(&self, pub_key: &EphemeralPublicKey) -> Result<()> { - self.non_malleability_signature.verify(&self.proof, pub_key) - } - - pub fn verify_training_wheels_sig(&self, pub_key: &EphemeralPublicKey) -> Result<()> { - if let Some(training_wheels_signature) = &self.training_wheels_signature { - training_wheels_signature.verify(&self.proof, pub_key) - } else { - bail!("No training_wheels_signature found") - } - } - - pub fn verify_proof( - &self, - public_inputs_hash: Fr, - pvk: &PreparedVerifyingKey, - ) -> Result<()> { - self.proof.verify_proof(public_inputs_hash, pvk) - } -} - -impl TryFrom<&[u8]> for Groth16Zkp { - type Error = CryptoMaterialError; - - fn try_from(bytes: &[u8]) -> Result { - bcs::from_bytes::(bytes).map_err(|_e| CryptoMaterialError::DeserializationError) - } -} - -impl Groth16Zkp { - pub fn new(a: G1Bytes, b: G2Bytes, c: G1Bytes) -> Self { - Groth16Zkp { a, b, c } - } - - pub fn verify_proof( - &self, - public_inputs_hash: Fr, - pvk: &PreparedVerifyingKey, - ) -> Result<()> { - let proof: Proof = Proof { - a: self.a.deserialize_into_affine()?, - b: self.b.to_affine()?, - c: self.c.deserialize_into_affine()?, - }; - let result = Groth16::::verify_proof(pvk, &proof, &[public_inputs_hash])?; - if !result { - bail!("groth16 proof verification failed") - } - Ok(()) - } -} - -/// 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. -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub enum ZkpOrOpenIdSig { - Groth16Zkp(SignedGroth16Zkp), - OpenIdSig(OpenIdSig), -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub struct ZkIdSignature { - /// 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, - - /// The base64url-encoded header, which contains two relevant fields: - /// 1. `kid`, which indicates which of the OIDC provider's JWKs should be used to verify the - /// \[ZKPoK of an\] OpenID signature., - /// 2. `alg`, which indicates which type of signature scheme was used to sign the JWT - pub jwt_header: String, - - /// The expiry time of the `ephemeral_pubkey` represented as a UNIX epoch timestamp in seconds. - pub exp_timestamp_secs: u64, - - /// 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`. - pub ephemeral_signature: EphemeralSignature, -} - -impl TryFrom<&[u8]> for ZkIdSignature { - type Error = CryptoMaterialError; - - fn try_from(bytes: &[u8]) -> Result { - bcs::from_bytes::(bytes) - .map_err(|_e| CryptoMaterialError::DeserializationError) - } -} - -impl ValidCryptoMaterial for ZkIdSignature { - fn to_bytes(&self) -> Vec { - bcs::to_bytes(&self).expect("Only unhandleable errors happen here.") - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct JWTHeader { - pub kid: String, - pub alg: String, -} - -impl ZkIdSignature { - /// A reasonable upper bound for the number of bytes we expect in a zkID public key. This is - /// enforced by our full nodes when they receive zkID TXNs. - pub const MAX_LEN: usize = 4000; - - pub fn parse_jwt_header(&self) -> Result { - let jwt_header_json = base64url_decode_as_str(&self.jwt_header)?; - let header: JWTHeader = serde_json::from_str(&jwt_header_json)?; - Ok(header) - } - - pub fn verify_expiry(&self, current_time: &CurrentTimeMicroseconds) -> Result<()> { - let block_time = UNIX_EPOCH + Duration::from_micros(current_time.microseconds); - let expiry_time = seconds_from_epoch(self.exp_timestamp_secs); - - if block_time > expiry_time { - bail!("zkID Signature is expired"); - } else { - Ok(()) - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct Pepper(pub(crate) [u8; PEPPER_NUM_BYTES]); - -impl Pepper { - pub fn new(bytes: [u8; PEPPER_NUM_BYTES]) -> Self { - Self(bytes) - } - - pub fn to_bytes(&self) -> &[u8; PEPPER_NUM_BYTES] { - &self.0 - } - - // Used for testing. #[cfg(test)] doesn't seem to allow for use in smoke tests. - pub fn from_number(num: u128) -> Self { - let big_int = num_bigint::BigUint::from(num); - let bytes: Vec = big_int.to_bytes_le(); - let mut extended_bytes = [0u8; PEPPER_NUM_BYTES]; - extended_bytes[..bytes.len()].copy_from_slice(&bytes); - Self(extended_bytes) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct IdCommitment(#[serde(with = "serde_bytes")] pub(crate) Vec); - -impl IdCommitment { - /// The size of the identity commitment (IDC) used to derive a zkID address. This value should **NOT* - /// be changed since on-chain addresses are based on it (e.g., hashing a larger-sized IDC would lead - /// to a different address). - pub const NUM_BYTES: usize = 32; -} - -impl IdCommitment { - /// The max length of the value of the JWT's `aud` field supported in our circuit. zkID address - /// derivation depends on this, so it should not be changed. - pub const MAX_AUD_VAL_BYTES: usize = 115; - // 4 * poseidon_bn254::BYTES_PACKED_PER_SCALAR; - /// The max length of the JWT field name that stores the user's ID (e.g., `sub`, `email`) which is - /// supported in our circuit. zkID address derivation depends on this, so it should not be changed. - pub const MAX_UID_KEY_BYTES: usize = 30; - // 2 * poseidon_bn254::BYTES_PACKED_PER_SCALAR; - /// The max length of the value of the JWT's UID field (`sub`, `email`) that stores the user's ID - /// which is supported in our circuit. zkID address derivation depends on this, so it should not - /// be changed. - pub const MAX_UID_VAL_BYTES: usize = 330; - - // 4 * poseidon_bn254::BYTES_PACKED_PER_SCALAR; - - pub fn new_from_preimage( - pepper: &Pepper, - aud: &str, - uid_key: &str, - uid_val: &str, - ) -> Result { - let aud_val_hash = poseidon_bn254::pad_and_hash_string(aud, Self::MAX_AUD_VAL_BYTES)?; - let uid_key_hash = poseidon_bn254::pad_and_hash_string(uid_key, Self::MAX_UID_KEY_BYTES)?; - let uid_val_hash = poseidon_bn254::pad_and_hash_string(uid_val, Self::MAX_UID_VAL_BYTES)?; - let pepper_scalar = poseidon_bn254::pack_bytes_to_one_scalar(pepper.0.as_slice())?; - - let fr = poseidon_bn254::hash_scalars(vec![ - pepper_scalar, - aud_val_hash, - uid_val_hash, - uid_key_hash, - ])?; - - let mut idc_bytes = vec![0u8; IdCommitment::NUM_BYTES]; - fr.serialize_uncompressed(&mut idc_bytes[..])?; - Ok(IdCommitment(idc_bytes)) - } - - pub fn to_bytes(&self) -> Vec { - bcs::to_bytes(&self).expect("Only unhandleable errors happen here.") - } -} - -impl TryFrom<&[u8]> for IdCommitment { - type Error = CryptoMaterialError; - - fn try_from(_value: &[u8]) -> Result { - bcs::from_bytes::(_value) - .map_err(|_e| CryptoMaterialError::DeserializationError) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ZkIdPublicKey { - /// The OIDC provider. - pub iss: String, - - /// SNARK-friendly commitment to: - /// 1. The application's ID; i.e., the `aud` field in the signed OIDC JWT representing the OAuth client ID. - /// 2. The OIDC provider's internal identifier for the user; e.g., the `sub` field in the signed OIDC JWT - /// which is Google's internal user identifier for bob@gmail.com, or the `email` field. - /// - /// e.g., H(aud || uid_key || uid_val || pepper), where `pepper` is the commitment's randomness used to hide - /// `aud` and `sub`. - pub idc: IdCommitment, -} - -impl ZkIdPublicKey { - /// A reasonable upper bound for the number of bytes we expect in a zkID public key. This is - /// enforced by our full nodes when they receive zkID TXNs. - pub const MAX_LEN: usize = 200 + IdCommitment::NUM_BYTES; - - pub fn to_bytes(&self) -> Vec { - bcs::to_bytes(&self).expect("Only unhandleable errors happen here.") - } -} - -impl TryFrom<&[u8]> for ZkIdPublicKey { - type Error = CryptoMaterialError; - - fn try_from(_value: &[u8]) -> Result { - bcs::from_bytes::(_value) - .map_err(|_e| CryptoMaterialError::DeserializationError) - } -} - -pub fn get_zkid_authenticators( - transaction: &SignedTransaction, -) -> Result> { - // Check all the signers in the TXN - let single_key_authenticators = transaction - .authenticator_ref() - .to_single_key_authenticators()?; - let mut authenticators = Vec::with_capacity(MAX_NUM_OF_SIGS); - for authenticator in single_key_authenticators { - if let (AnyPublicKey::ZkId { public_key }, AnySignature::ZkId { signature }) = - (authenticator.public_key(), authenticator.signature()) - { - authenticators.push((public_key.clone(), signature.clone())) - } - } - Ok(authenticators) -} - -pub fn base64url_encode_str(data: &str) -> String { - base64::encode_config(data.as_bytes(), URL_SAFE_NO_PAD) -} - -pub fn base64url_decode_as_str(b64: &str) -> Result { - let decoded_bytes = base64::decode_config(b64, URL_SAFE_NO_PAD)?; - // Convert the decoded bytes to a UTF-8 string - let str = String::from_utf8(decoded_bytes)?; - Ok(str) -} - -fn seconds_from_epoch(secs: u64) -> SystemTime { - UNIX_EPOCH + Duration::from_secs(secs) -} - -pub fn is_allowed_override_aud( - config: &Configuration, - override_aud_val: &String, -) -> Result<(), VMStatus> { - let matches = config - .override_aud_vals - .iter() - .filter(|&e| e.eq(override_aud_val)) - .count(); - - if matches == 0 { - Err(invalid_signature!( - "override aud is not allow-listed in 0x1::zkid" - )) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::{ - bn254_circom::{get_public_inputs_hash, DEVNET_VERIFYING_KEY}, - jwks::rsa::RSA_JWK, - transaction::authenticator::{AuthenticationKey, EphemeralPublicKey, EphemeralSignature}, - zkid::{ - base64url_encode_str, Configuration, G1Bytes, G2Bytes, Groth16Zkp, IdCommitment, - OpenIdSig, Pepper, SignedGroth16Zkp, ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig, - }, - }; - use aptos_crypto::{ - ed25519::{Ed25519PrivateKey, Ed25519Signature}, - PrivateKey, SigningKey, Uniform, - }; - use std::ops::Deref; - - // TODO(zkid): This test case must be rewritten to be more modular and updatable. - // Right now, there are no instructions on how to produce this test case. - #[test] - fn test_zkid_groth16_proof_verification() { - let a = G1Bytes::new_unchecked( - "20534193224874816823038374805971256353897254359389549519579800571198905682623", - "3128047629776327625062258700337193014005673411952335683536865294076478098678", - ) - .unwrap(); - let b = G2Bytes::new_unchecked( - [ - "11831059544281359959902363827760224027191828999098259913907764686593049260801", - "14933419822301565783764657928814181728459886670248956535955133596731082875810", - ], - [ - "16616167200367085072660100259194052934821478809307596510515652443339946625933", - "1103855954970567341442645156173756328940907403537523212700521414512165362008", - ], - ) - .unwrap(); - let c = G1Bytes::new_unchecked( - "296457556259014920933232985275282694032456344171046224944953719399946325676", - "10314488872240559867545387237625153841351761679810222583912967187658678987385", - ) - .unwrap(); - let proof = Groth16Zkp::new(a, b, c); - - let sender = Ed25519PrivateKey::generate_for_testing(); - let sender_pub = sender.public_key(); - let sender_auth_key = AuthenticationKey::ed25519(&sender_pub); - let sender_addr = sender_auth_key.account_address(); - let raw_txn = crate::test_helpers::transaction_test_helpers::get_test_signed_transaction( - sender_addr, - 0, - &sender, - sender.public_key(), - None, - 0, - 0, - None, - ) - .into_raw_transaction(); - - let sender_sig = sender.sign(&raw_txn).unwrap(); - - let epk = EphemeralPublicKey::ed25519(sender.public_key()); - let es = EphemeralSignature::ed25519(sender_sig); - - let proof_sig = sender.sign(&proof).unwrap(); - let ephem_proof_sig = EphemeralSignature::ed25519(proof_sig); - let config = Configuration::new_for_devnet_and_testing(); - let zk_sig = ZkIdSignature { - sig: ZkpOrOpenIdSig::Groth16Zkp(SignedGroth16Zkp { - proof: proof.clone(), - non_malleability_signature: ephem_proof_sig, - extra_field: "\"family_name\":\"Straka\",".to_string(), - exp_horizon_secs: config.max_exp_horizon_secs, - override_aud_val: None, - training_wheels_signature: Some(EphemeralSignature::ed25519( - Ed25519Signature::dummy_signature(), - )), - }), - jwt_header: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3RfandrIiwidHlwIjoiSldUIn0".to_owned(), - exp_timestamp_secs: 1900255944, - ephemeral_pubkey: epk, - ephemeral_signature: es, - }; - - let pepper = Pepper::from_number(76); - let addr_seed = IdCommitment::new_from_preimage( - &pepper, - "407408718192.apps.googleusercontent.com", - "sub", - "113990307082899718775", - ) - .unwrap(); - - let zk_pk = ZkIdPublicKey { - iss: "https://accounts.google.com".to_owned(), - idc: addr_seed, - }; - let jwk = RSA_JWK { - kid:"1".to_owned(), - kty:"RSA".to_owned(), - alg:"RS256".to_owned(), - e:"AQAB".to_owned(), - n:"6S7asUuzq5Q_3U9rbs-PkDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb_XqZaKgSYaC_h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONYW5Zu3PwyvAWk5D6ueIUhLtYzpcB-etoNdL3Ir2746KIy_VUsDwAM7dhrqSK8U2xFCGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAKctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcajtw".to_owned(), - }; - - let public_inputs_hash = - get_public_inputs_hash(&zk_sig, &zk_pk, &jwk, config.max_exp_horizon_secs, &config) - .unwrap(); - - proof - .verify_proof(public_inputs_hash, DEVNET_VERIFYING_KEY.deref()) - .unwrap(); - } - - /// Returns frequently-used JSON in our test cases - fn get_jwt_payload_json( - iss: &str, - uid_key: &str, - uid_val: &str, - aud: &str, - nonce: Option, - ) -> String { - let nonce_str = match &nonce { - None => "15142559071815587978635947836206288328330533396937069427032377153167520963771", - Some(s) => s.as_str(), - }; - - format!( - r#"{{ - "iss": "{}", - "{}": "{}", - "aud": "{}", - "nonce": "{}", - "exp": 1311281970, - "iat": 1311280970, - "name": "Jane Doe", - "given_name": "Jane", - "family_name": "Doe", - "gender": "female", - "birthdate": "0000-10-31", - "email": "janedoe@example.com", - "picture": "http://example.com/janedoe/me.jpg" - }}"#, - iss, uid_key, uid_val, aud, nonce_str - ) - } - - fn get_jwt_default_values() -> ( - &'static str, - &'static str, - &'static str, - &'static str, - u64, - Configuration, - EphemeralPublicKey, - u128, - ZkIdPublicKey, - ) { - let iss = "https://server.example.com"; - let aud = "s6BhdRkqt3"; - let uid_key = "sub"; - let uid_val = "248289761001"; - let exp_timestamp_secs = 1311281970; - let config = Configuration::new_for_devnet_and_testing(); - let pepper = 76; - - let zkid_pk = ZkIdPublicKey { - iss: iss.to_owned(), - idc: IdCommitment::new_from_preimage( - &Pepper::from_number(pepper), - aud, - uid_key, - uid_val, - ) - .unwrap(), - }; - - let epk = - EphemeralPublicKey::ed25519(Ed25519PrivateKey::generate_for_testing().public_key()); - - ( - iss, - aud, - uid_key, - uid_val, - exp_timestamp_secs, - config, - epk, - pepper, - zkid_pk, - ) - } - - #[test] - fn test_zkid_oidc_sig_verifies() { - let (iss, aud, uid_key, uid_val, exp_timestamp_secs, config, epk, pepper, zkid_pk) = - get_jwt_default_values(); - - let oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - assert!(oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_ok()); - } - - #[test] - fn test_zkid_oidc_sig_fails_with_different_pepper() { - let (iss, aud, uid_key, uid_val, exp_timestamp_secs, config, epk, pepper, zkid_pk) = - get_jwt_default_values(); - let bad_pepper = pepper + 1; - - let oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - - assert!(oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_ok()); - - let bad_oidc_sig = zkid_simulate_oidc_signature( - uid_key, - bad_pepper, // Pepper does not match - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - - assert!(bad_oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_err()); - } - - #[test] - fn test_zkid_oidc_sig_fails_with_expiry_past_horizon() { - let (iss, aud, uid_key, uid_val, exp_timestamp_secs, config, epk, pepper, zkid_pk) = - get_jwt_default_values(); - let oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - - assert!(oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_ok()); - - let bad_exp_timestamp_secs = 1000000000000000000; - assert!(oidc_sig - .verify_jwt_claims(bad_exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_err()); - } - - #[test] - fn test_zkid_oidc_sig_fails_with_different_uid_val() { - let (iss, aud, uid_key, uid_val, exp_timestamp_secs, config, epk, pepper, zkid_pk) = - get_jwt_default_values(); - let oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - - assert!(oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_ok()); - - let bad_uid_val = format!("{}+1", uid_val); - let bad_oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, bad_uid_val.as_str(), aud, None), - ); - - assert!(bad_oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_err()); - } - - #[test] - fn test_zkid_oidc_sig_fails_with_bad_nonce() { - let (iss, aud, uid_key, uid_val, exp_timestamp_secs, config, epk, pepper, zkid_pk) = - get_jwt_default_values(); - let oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - - assert!(oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_ok()); - - let bad_nonce = "bad nonce".to_string(); - let bad_oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, Some(bad_nonce)), - ); - - assert!(bad_oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_err()); - } - - #[test] - fn test_zkid_oidc_sig_with_different_iss() { - let (iss, aud, uid_key, uid_val, exp_timestamp_secs, config, epk, pepper, zkid_pk) = - get_jwt_default_values(); - let oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(iss, uid_key, uid_val, aud, None), - ); - - assert!(oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_ok()); - - let bad_iss = format!("{}+1", iss); - let bad_oidc_sig = zkid_simulate_oidc_signature( - uid_key, - pepper, - &get_jwt_payload_json(bad_iss.as_str(), uid_key, uid_val, aud, None), - ); - - assert!(bad_oidc_sig - .verify_jwt_claims(exp_timestamp_secs, &epk, &zkid_pk, &config) - .is_err()); - } - - fn zkid_simulate_oidc_signature( - uid_key: &str, - pepper: u128, - jwt_payload_unencoded: &str, - ) -> OpenIdSig { - let jwt_payload = base64url_encode_str(jwt_payload_unencoded); - - OpenIdSig { - jwt_sig: "jwt_sig is verified in the prologue".to_string(), - jwt_payload, - uid_key: uid_key.to_owned(), - epk_blinder: vec![0u8; OpenIdSig::EPK_BLINDER_NUM_BYTES], - pepper: Pepper::from_number(pepper), - idc_aud_val: None, - } - } -} diff --git a/types/src/zkid/bn254_circom.rs b/types/src/zkid/bn254_circom.rs new file mode 100644 index 0000000000000..a0806cb0eca42 --- /dev/null +++ b/types/src/zkid/bn254_circom.rs @@ -0,0 +1,296 @@ +// Copyright © Aptos Foundation + +use crate::{ + jwks::rsa::RSA_JWK, + serialize, + zkid::{Configuration, IdCommitment, ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig}, +}; +use anyhow::bail; +use aptos_crypto::{poseidon_bn254, CryptoMaterialError}; +use ark_bn254::{Fq, Fq2, Fr, G1Affine, G1Projective, G2Affine, G2Projective}; +use ark_ff::PrimeField; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use num_traits::{One, Zero}; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +// TODO(zkid): Some of this stuff, if not all, belongs to the aptos-crypto crate + +pub const G1_PROJECTIVE_COMPRESSED_NUM_BYTES: usize = 32; +pub const G2_PROJECTIVE_COMPRESSED_NUM_BYTES: usize = 64; + +/// This will do the proper subgroup membership checks. +pub(crate) fn g1_projective_str_to_affine(x: &str, y: &str) -> anyhow::Result { + let g1_affine = G1Bytes::new_unchecked(x, y)?.deserialize_into_affine()?; + Ok(g1_affine) +} + +/// This will do the proper subgroup membership checks. +pub(crate) fn g2_projective_str_to_affine(x: [&str; 2], y: [&str; 2]) -> anyhow::Result { + let g2_affine = G2Bytes::new_unchecked(x, y)?.as_affine()?; + Ok(g2_affine) +} + +/// Converts a decimal string to an Fq +fn parse_fq_element(s: &str) -> Result { + s.parse::() + .map_err(|_e| CryptoMaterialError::DeserializationError) +} + +#[allow(unused)] +/// Converts a decimal string to an Fr +pub fn parse_fr_element(s: &str) -> Result { + s.parse::() + .map_err(|_e| CryptoMaterialError::DeserializationError) +} + +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] +pub struct G1Bytes(pub(crate) [u8; G1_PROJECTIVE_COMPRESSED_NUM_BYTES]); + +impl G1Bytes { + pub fn new_unchecked(x: &str, y: &str) -> anyhow::Result { + let g1 = G1Projective::new_unchecked( + parse_fq_element(x)?, + parse_fq_element(y)?, + parse_fq_element("1")?, + ); + + let bytes: Vec = serialize!(g1); + Self::new_from_vec(bytes) + } + + /// Used internally or for testing. + pub fn new_from_vec(vec: Vec) -> anyhow::Result { + if vec.len() == G1_PROJECTIVE_COMPRESSED_NUM_BYTES { + let mut bytes = [0; G1_PROJECTIVE_COMPRESSED_NUM_BYTES]; + bytes.copy_from_slice(&vec); + Ok(Self(bytes)) + } else { + bail!( + "Serialized BN254 G1 must have exactly {} bytes", + G1_PROJECTIVE_COMPRESSED_NUM_BYTES + ) + } + } + + pub fn deserialize_into_affine(&self) -> Result { + self.try_into() + } +} + +impl TryInto for &G1Bytes { + type Error = CryptoMaterialError; + + fn try_into(self) -> Result { + G1Projective::deserialize_compressed(self.0.as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError) + } +} + +impl TryInto for &G1Bytes { + type Error = CryptoMaterialError; + + fn try_into(self) -> Result { + let g1_projective: G1Projective = self.try_into()?; + Ok(g1_projective.into()) + } +} + +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] +pub struct G2Bytes(#[serde(with = "BigArray")] pub(crate) [u8; G2_PROJECTIVE_COMPRESSED_NUM_BYTES]); + +impl G2Bytes { + pub fn new_unchecked(x: [&str; 2], y: [&str; 2]) -> anyhow::Result { + let g2 = G2Projective::new_unchecked( + Fq2::new(parse_fq_element(x[0])?, parse_fq_element(x[1])?), + Fq2::new(parse_fq_element(y[0])?, parse_fq_element(y[1])?), + Fq2::new(parse_fq_element("1")?, parse_fq_element("0")?), + ); + + let bytes: Vec = serialize!(g2); + Self::new_from_vec(bytes) + } + + pub fn new_from_vec(vec: Vec) -> anyhow::Result { + if vec.len() == G2_PROJECTIVE_COMPRESSED_NUM_BYTES { + let mut bytes = [0; G2_PROJECTIVE_COMPRESSED_NUM_BYTES]; + bytes.copy_from_slice(&vec); + Ok(Self(bytes)) + } else { + bail!( + "Serialized BN254 G2 must have exactly {} bytes", + G2_PROJECTIVE_COMPRESSED_NUM_BYTES + ) + } + } + + pub fn as_affine(&self) -> Result { + self.try_into() + } +} + +impl TryInto for &G2Bytes { + type Error = CryptoMaterialError; + + fn try_into(self) -> Result { + G2Projective::deserialize_compressed(self.0.as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError) + } +} + +impl TryInto for &G2Bytes { + type Error = CryptoMaterialError; + + fn try_into(self) -> Result { + let g2_projective: G2Projective = self.try_into()?; + Ok(g2_projective.into()) + } +} + +pub fn get_public_inputs_hash( + sig: &ZkIdSignature, + pk: &ZkIdPublicKey, + jwk: &RSA_JWK, + config: &Configuration, +) -> anyhow::Result { + if let ZkpOrOpenIdSig::Groth16Zkp(proof) = &sig.sig { + let (has_extra_field, extra_field_hash) = match &proof.extra_field { + None => (Fr::zero(), Fr::zero()), + Some(extra_field) => ( + Fr::one(), + poseidon_bn254::pad_and_hash_string( + extra_field, + config.max_extra_field_bytes as usize, + )?, + ), + }; + + let (override_aud_val_hash, use_override_aud) = match &proof.override_aud_val { + Some(override_aud_val) => ( + poseidon_bn254::pad_and_hash_string( + override_aud_val, + IdCommitment::MAX_AUD_VAL_BYTES, + )?, + ark_bn254::Fr::from(1), + ), + None => ( + poseidon_bn254::pad_and_hash_string("", IdCommitment::MAX_AUD_VAL_BYTES)?, + ark_bn254::Fr::from(0), + ), + }; + + // Add the hash of the jwt_header with the "." separator appended + let jwt_header_with_separator = format!("{}.", sig.jwt_header_b64); + let jwt_header_hash = poseidon_bn254::pad_and_hash_string( + &jwt_header_with_separator, + config.max_jwt_header_b64_bytes as usize, + )?; + + let jwk_hash = jwk.to_poseidon_scalar()?; + + // Add the hash of the value of the `iss` field + let iss_field_hash = poseidon_bn254::pad_and_hash_string( + pk.iss_val.as_str(), + config.max_iss_val_bytes as usize, + )?; + + // Add the id_commitment as a scalar + let idc = Fr::from_le_bytes_mod_order(&pk.idc.0); + + // Add the exp_timestamp_secs as a scalar + let exp_timestamp_secs = Fr::from(sig.exp_timestamp_secs); + + // Add the epk lifespan as a scalar + let exp_horizon_secs = Fr::from(proof.exp_horizon_secs); + + // Add the epk as padded and packed scalars + let mut epk_frs = poseidon_bn254::pad_and_pack_bytes_to_scalars_with_len( + sig.ephemeral_pubkey.to_bytes().as_slice(), + config.max_commited_epk_bytes as usize, + )?; + + // println!("Num EPK scalars: {}", epk_frs.len()); + // for (i, e) in epk_frs.iter().enumerate() { + // println!("EPK Fr[{}]: {}", i, e.to_string()) + // } + // println!("IDC: {}", idc); + // println!("exp_timestamp_secs: {}", exp_timestamp_secs); + // println!("exp_horizon_secs: {}", exp_horizon_secs); + // println!("iss field hash: {}", pk.iss_val); + // println!("Has extra field: {}", has_extra_field); + // println!("Extra field val: {:?}", proof.extra_field); + // println!("Extra field hash: {}", extra_field_hash); + // println!("JWT header val: {}", jwt_header_with_separator); + // println!("JWT header hash: {}", jwt_header_hash); + // println!("JWK hash: {}", jwk_hash); + // println!("Override aud hash: {}", override_aud_val_hash); + // println!("Use override aud: {}", use_override_aud.to_string()); + + let mut frs = vec![]; + frs.append(&mut epk_frs); + frs.push(idc); + frs.push(exp_timestamp_secs); + frs.push(exp_horizon_secs); + frs.push(iss_field_hash); + frs.push(has_extra_field); + frs.push(extra_field_hash); + frs.push(jwt_header_hash); + frs.push(jwk_hash); + frs.push(override_aud_val_hash); + frs.push(use_override_aud); + poseidon_bn254::hash_scalars(frs) + } else { + bail!("Cannot get_public_inputs_hash for ZkIdSignature") + } +} + +#[cfg(test)] +mod test { + use crate::zkid::{ + bn254_circom::{ + G1Bytes, G2Bytes, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, + G2_PROJECTIVE_COMPRESSED_NUM_BYTES, + }, + circuit_constants::devnet_prepared_vk, + Groth16VerificationKey, + }; + use ark_bn254::Bn254; + use ark_groth16::PreparedVerifyingKey; + + #[test] + pub fn test_bn254_serialized_sizes() { + let g1 = G1Bytes::new_unchecked( + "16672231080302629756836614130913173861541009360974119524782950408048375831661", + "1076145001163048025135533382088266750240489485046298539187659509488738517245", + ) + .unwrap(); + + let g2 = G2Bytes::new_unchecked( + [ + "1125365732643211423779651913319958385653115422366520671538751860820509133538", + "10055196097002324305342942912758079446356594743098794928675544207400347950287", + ], + [ + "10879716754714953827605171295191459580695363989155343984818520267224463075503", + "440220374146936557739765173414663598678359360031905981547938788314460390904", + ], + ) + .unwrap(); + + let g1_bytes = bcs::to_bytes(&g1).unwrap(); + assert_eq!(g1_bytes.len(), G1_PROJECTIVE_COMPRESSED_NUM_BYTES); + + let g2_bytes = bcs::to_bytes(&g2).unwrap(); + assert_eq!(g2_bytes.len(), G2_PROJECTIVE_COMPRESSED_NUM_BYTES); + } + + #[test] + // Tests conversion between the devnet ark_groth16::PreparedVerificationKey and our Move + // representation of it. + fn print_groth16_pvk() { + let groth16_vk: Groth16VerificationKey = devnet_prepared_vk().into(); + let same_pvk: PreparedVerifyingKey = groth16_vk.try_into().unwrap(); + + assert_eq!(same_pvk, devnet_prepared_vk()); + } +} diff --git a/types/src/zkid/circuit_constants.rs b/types/src/zkid/circuit_constants.rs new file mode 100644 index 0000000000000..4fd18fde5f735 --- /dev/null +++ b/types/src/zkid/circuit_constants.rs @@ -0,0 +1,92 @@ +// Copyright © Aptos Foundation + +//! These constants are from commit 125522b4b226f8ece3e3162cecfefe915d13bc30 of zkid-circuit. + +use crate::zkid::bn254_circom::{g1_projective_str_to_affine, g2_projective_str_to_affine}; +use aptos_crypto::poseidon_bn254; +use ark_bn254::Bn254; +use ark_groth16::{PreparedVerifyingKey, VerifyingKey}; + +pub(crate) const MAX_AUD_VAL_BYTES: usize = 120; +pub(crate) const MAX_UID_KEY_BYTES: usize = 30; +pub(crate) const MAX_UID_VAL_BYTES: usize = 330; +pub(crate) const MAX_ISS_VAL_BYTES: u16 = 120; +pub(crate) const MAX_EXTRA_FIELD_BYTES: u16 = 350; +pub(crate) const MAX_JWT_HEADER_B64_BYTES: u32 = 300; + +/// This constant is not explicitly defined in the circom template, but only implicitly in the way +/// we hash the EPK. +pub(crate) const MAX_COMMITED_EPK_BYTES: u16 = 3 * poseidon_bn254::BYTES_PACKED_PER_SCALAR as u16; + +/// This function uses the decimal uncompressed point serialization which is outputted by circom. +/// https://github.com/aptos-labs/devnet-groth16-keys/commit/02e5675f46ce97f8b61a4638e7a0aaeaa4351f76 +pub fn devnet_prepared_vk() -> PreparedVerifyingKey { + // Convert the projective points to affine. + let alpha_g1 = g1_projective_str_to_affine( + "16672231080302629756836614130913173861541009360974119524782950408048375831661", + "1076145001163048025135533382088266750240489485046298539187659509488738517245", + ) + .unwrap(); + + let beta_g2 = g2_projective_str_to_affine( + [ + "1125365732643211423779651913319958385653115422366520671538751860820509133538", + "10055196097002324305342942912758079446356594743098794928675544207400347950287", + ], + [ + "10879716754714953827605171295191459580695363989155343984818520267224463075503", + "440220374146936557739765173414663598678359360031905981547938788314460390904", + ], + ) + .unwrap(); + + let gamma_g2 = g2_projective_str_to_affine( + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634", + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531", + ], + ) + .unwrap(); + + let delta_g2 = g2_projective_str_to_affine( + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634", + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531", + ], + ) + .unwrap(); + + let mut gamma_abc_g1 = Vec::new(); + for points in [ + g1_projective_str_to_affine( + "333957087685714773491410343905674131693317845924221586503521553512853800005", + "16794842110397433586916934076838854067112427849394773076676106408631114267154", + ) + .unwrap(), + g1_projective_str_to_affine( + "14679941092573826838949544937315479399329040741655244517938404383938168565228", + "19977040285201397592140173066949293223501504328707794673737757867503037033174", + ) + .unwrap(), + ] { + gamma_abc_g1.push(points); + } + + let vk = VerifyingKey { + alpha_g1, + beta_g2, + gamma_g2, + delta_g2, + gamma_abc_g1, + }; + + PreparedVerifyingKey::from(vk) +} diff --git a/types/src/zkid/circuit_testcases.rs b/types/src/zkid/circuit_testcases.rs new file mode 100644 index 0000000000000..00ffde64fcaa6 --- /dev/null +++ b/types/src/zkid/circuit_testcases.rs @@ -0,0 +1,174 @@ +// Copyright © Aptos Foundation + +//^ This file stores the details associated with a sample zkID proof. The constants are outputted by +//^ `input_gen.py` in the `zkid-circuit` repo (or can be derived implicitly from that code). + +use crate::{ + jwks::rsa::RSA_JWK, + transaction::authenticator::EphemeralPublicKey, + zkid::{ + base64url_encode_str, + bn254_circom::{G1Bytes, G2Bytes}, + Claims, Configuration, Groth16Zkp, IdCommitment, OpenIdSig, Pepper, ZkIdPublicKey, + }, +}; +use aptos_crypto::{ed25519::Ed25519PrivateKey, PrivateKey, Uniform}; +use once_cell::sync::Lazy; +use ring::signature::RsaKeyPair; +use rsa::{pkcs1::EncodeRsaPrivateKey, pkcs8::DecodePrivateKey}; + +/// The JWT header, decoded +pub(crate) static SAMPLE_JWT_HEADER_DECODED: Lazy = Lazy::new(|| { + format!( + r#"{{"alg":"{}","kid":"{}","typ":"JWT"}}"#, + SAMPLE_JWK.alg.as_str(), + SAMPLE_JWK.kid.as_str() + ) +}); + +/// The JWT header, base64url-encoded +pub(crate) static SAMPLE_JWT_HEADER_B64: Lazy = + Lazy::new(|| base64url_encode_str(SAMPLE_JWT_HEADER_DECODED.as_str())); + +/// The JWT payload, decoded + +static SAMPLE_NONCE: Lazy = Lazy::new(|| { + let config = Configuration::new_for_testing(); + OpenIdSig::reconstruct_oauth_nonce( + SAMPLE_EPK_BLINDER.as_slice(), + SAMPLE_EXP_DATE, + &SAMPLE_EPK, + &config, + ) + .unwrap() +}); + +/// TODO(zkid): Use a multiline format here, for diff-friendliness +pub(crate) static SAMPLE_JWT_PAYLOAD_DECODED: Lazy = Lazy::new(|| { + format!( + r#"{{"iss":"https://accounts.google.com","azp":"407408718192.apps.googleusercontent.com","aud":"407408718192.apps.googleusercontent.com","sub":"113990307082899718775","hd":"aptoslabs.com","email":"michael@aptoslabs.com","email_verified":true,"at_hash":"bxIESuI59IoZb5alCASqBg","name":"Michael Straka","picture":"https://lh3.googleusercontent.com/a/ACg8ocJvY4kVUBRtLxe1IqKWL5i7tBDJzFp9YuWVXMzwPpbs=s96-c","given_name":"Michael","family_name":"Straka","locale":"en","iat":1700255944,"exp":2700259544,"nonce":"{}"}}"#, + SAMPLE_NONCE.as_str() + ) +}); + +/// Consistent with what is in `SAMPLE_JWT_PAYLOAD_DECODED` +pub(crate) const SAMPLE_JWT_EXTRA_FIELD: &str = r#""family_name":"Straka","#; + +/// The JWT parsed as a struct +pub(crate) static SAMPLE_JWT_PARSED: Lazy = + Lazy::new(|| serde_json::from_str(SAMPLE_JWT_PAYLOAD_DECODED.as_str()).unwrap()); + +/// The JWK under which the JWT is signed, taken from https://token.dev +pub(crate) static SAMPLE_JWK: Lazy = Lazy::new(|| { + RSA_JWK { + kid: "test_jwk".to_owned(), + kty: "RSA".to_owned(), + alg: "RS256".to_owned(), + e: "AQAB".to_owned(), + n: "6S7asUuzq5Q_3U9rbs-PkDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb_XqZaKgSYaC_h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONYW5Zu3PwyvAWk5D6ueIUhLtYzpcB-etoNdL3Ir2746KIy_VUsDwAM7dhrqSK8U2xFCGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAKctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcajtw".to_owned(), +} +}); + +/// This is the SK from https://token.dev/. +/// To convert it into a JSON, you can use https://irrte.ch/jwt-js-decode/pem2jwk.html +pub(crate) static SAMPLE_JWK_SK: Lazy = Lazy::new(|| { + let sk = r#"-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDpLtqxS7OrlD/d +T2tuz4+QNUh2OCa2Bat4bmpY+wL3FdkqIxXUCJX0tfKpCwBikKoQMzddt+ZmoZvj +zIuFv9eploqBJhoL+HYOMzuWCshACn33TZGvx9SYs3aK+vm2cvFRQ6cw5zZJC2v1 +2DNM41hblm7c/DK8BaTkPq54hSEu1jOlwH562g10vcivbvjoojL9VSwPAAzt2Gup +IrxTbEUIaVq7iKQ5O2/MOjCcAwcyt8TurUHpZlAMBCUGbFFCzIqWfkMiwq/rFq42 +wdGAEApy1TFkbwzhAkjHdLoC6CF3dFkLgJrkB7193wvyaU1gEKtCE5nt1LR/hq3h +quUtxqO3AgMBAAECggEBANX6C+7EA/TADrbcCT7fMuNnMb5iGovPuiDCWc6bUIZC +Q0yac45l7o1nZWzfzpOkIprJFNZoSgIF7NJmQeYTPCjAHwsSVraDYnn3Y4d1D3tM +5XjJcpX2bs1NactxMTLOWUl0JnkGwtbWp1Qq+DBnMw6ghc09lKTbHQvhxSKNL/0U +C+YmCYT5ODmxzLBwkzN5RhxQZNqol/4LYVdji9bS7N/UITw5E6LGDOo/hZHWqJsE +fgrJTPsuCyrYlwrNkgmV2KpRrGz5MpcRM7XHgnqVym+HyD/r9E7MEFdTLEaiiHcm +Ish1usJDEJMFIWkF+rnEoJkQHbqiKlQBcoqSbCmoMWECgYEA/4379mMPF0JJ/EER +4VH7/ZYxjdyphenx2VYCWY/uzT0KbCWQF8KXckuoFrHAIP3EuFn6JNoIbja0NbhI +HGrU29BZkATG8h/xjFy/zPBauxTQmM+yS2T37XtMoXNZNS/ubz2lJXMOapQQiXVR +l/tzzpyWaCe9j0NT7DAU0ZFmDbECgYEA6ZbjkcOs2jwHsOwwfamFm4VpUFxYtED7 +9vKzq5d7+Ii1kPKHj5fDnYkZd+mNwNZ02O6OGxh40EDML+i6nOABPg/FmXeVCya9 +Vump2Yqr2fAK3xm6QY5KxAjWWq2kVqmdRmICSL2Z9rBzpXmD5o06y9viOwd2bhBo +0wB02416GecCgYEA+S/ZoEa3UFazDeXlKXBn5r2tVEb2hj24NdRINkzC7h23K/z0 +pDZ6tlhPbtGkJodMavZRk92GmvF8h2VJ62vAYxamPmhqFW5Qei12WL+FuSZywI7F +q/6oQkkYT9XKBrLWLGJPxlSKmiIGfgKHrUrjgXPutWEK1ccw7f10T2UXvgECgYEA +nXqLa58G7o4gBUgGnQFnwOSdjn7jkoppFCClvp4/BtxrxA+uEsGXMKLYV75OQd6T +IhkaFuxVrtiwj/APt2lRjRym9ALpqX3xkiGvz6ismR46xhQbPM0IXMc0dCeyrnZl +QKkcrxucK/Lj1IBqy0kVhZB1IaSzVBqeAPrCza3AzqsCgYEAvSiEjDvGLIlqoSvK +MHEVe8PBGOZYLcAdq4YiOIBgddoYyRsq5bzHtTQFgYQVK99Cnxo+PQAvzGb+dpjN +/LIEAS2LuuWHGtOrZlwef8ZpCQgrtmp/phXfVi6llcZx4mMm7zYmGhh2AsA9yEQc +acgc4kgDThAjD7VlXad9UHpNMO8= +-----END PRIVATE KEY-----"#; + + // TODO(zkid): Hacking around the difficulty of parsing PKCS#8-encoded PEM files with the `pem` crate + let der = rsa::RsaPrivateKey::from_pkcs8_pem(sk) + .unwrap() + .to_pkcs1_der() + .unwrap(); + RsaKeyPair::from_der(der.as_bytes()).unwrap() +}); + +pub(crate) const SAMPLE_UID_KEY: &str = "sub"; + +/// The nonce-committed expiration date (not the JWT `exp`), 12/21/5490 +pub(crate) const SAMPLE_EXP_DATE: u64 = 111_111_111_111; + +/// ~31,710 years +pub(crate) const SAMPLE_EXP_HORIZON_SECS: u64 = 999_999_999_999; + +pub(crate) static SAMPLE_PEPPER: Lazy = Lazy::new(|| Pepper::from_number(76)); + +pub(crate) static SAMPLE_ESK: Lazy = + Lazy::new(Ed25519PrivateKey::generate_for_testing); + +pub(crate) static SAMPLE_EPK: Lazy = + Lazy::new(|| EphemeralPublicKey::ed25519(SAMPLE_ESK.public_key())); + +pub(crate) static SAMPLE_EPK_BLINDER: Lazy> = Lazy::new(|| vec![42u8]); + +pub(crate) static SAMPLE_ZKID_PK: Lazy = Lazy::new(|| { + assert_eq!(SAMPLE_UID_KEY, "sub"); + + ZkIdPublicKey { + iss_val: SAMPLE_JWT_PARSED.oidc_claims.iss.to_owned(), + idc: IdCommitment::new_from_preimage( + &SAMPLE_PEPPER, + SAMPLE_JWT_PARSED.oidc_claims.aud.as_str(), + SAMPLE_UID_KEY, + SAMPLE_JWT_PARSED.oidc_claims.sub.as_str(), + ) + .unwrap(), + } +}); + +/// A valid Groth16 proof for the JWT under `SAMPLE_JWK`, where the public inputs have: +/// - uid_key set to `sub` +/// - 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( + G1Bytes::new_unchecked( + "12231709561876342858591497461541533679382707548832581865026884128195038623819", + "19550065013334671766459652895464943208897555190003385241537366958524038549651", + ) + .unwrap(), + G2Bytes::new_unchecked( + [ + "17760114700472440073566664035341233176332867365948052821768844085204638465257", + "2074118366711830630562352153651013053077229376039883853182809642185973784582", + ], + [ + "21474168538255367719812229486236305962320711305273777702403534410487962424082", + "17404352079167923594003522667505828016450036154572779269542685309363067054790", + ], + ) + .unwrap(), + G1Bytes::new_unchecked( + "9194799847136645728085689496796085217935413772780751043375835048405276952071", + "17704024912475005725846633700069393676807658122056968962396516331631047675983", + ) + .unwrap(), + ) +}); diff --git a/types/src/zkid/configuration.rs b/types/src/zkid/configuration.rs new file mode 100644 index 0000000000000..af5651ad659c9 --- /dev/null +++ b/types/src/zkid/configuration.rs @@ -0,0 +1,90 @@ +// Copyright © Aptos Foundation + +use crate::{ + invalid_signature, + move_utils::as_move_value::AsMoveValue, + zkid::{circuit_constants, circuit_testcases::SAMPLE_EXP_HORIZON_SECS}, +}; +use move_core_types::{ + ident_str, + identifier::IdentStr, + move_resource::MoveStructType, + value::{MoveStruct, MoveValue}, + vm_status::{StatusCode, VMStatus}, +}; +use serde::{Deserialize, Serialize}; + +/// Reflection of aptos_framework::zkid::Configs +#[derive(Serialize, Deserialize, Debug)] +pub struct Configuration { + pub override_aud_vals: Vec, + pub max_zkid_signatures_per_txn: u16, + pub max_exp_horizon_secs: u64, + pub training_wheels_pubkey: Option>, + pub max_commited_epk_bytes: u16, + pub max_iss_val_bytes: u16, + pub max_extra_field_bytes: u16, + pub max_jwt_header_b64_bytes: u32, +} + +impl AsMoveValue for Configuration { + fn as_move_value(&self) -> MoveValue { + MoveValue::Struct(MoveStruct::Runtime(vec![ + self.override_aud_vals.as_move_value(), + self.max_zkid_signatures_per_txn.as_move_value(), + self.max_exp_horizon_secs.as_move_value(), + self.training_wheels_pubkey.as_move_value(), + self.max_commited_epk_bytes.as_move_value(), + self.max_iss_val_bytes.as_move_value(), + self.max_extra_field_bytes.as_move_value(), + self.max_jwt_header_b64_bytes.as_move_value(), + ])) + } +} + +/// WARNING: This struct uses resource groups on the Move side. Do NOT implement OnChainConfig +/// for it, since `OnChainConfig::fetch_config` does not work with resource groups (yet). +impl MoveStructType for Configuration { + const MODULE_NAME: &'static IdentStr = ident_str!("zkid"); + const STRUCT_NAME: &'static IdentStr = ident_str!("Configuration"); +} + +impl Configuration { + /// Should only be used for testing. + pub const OVERRIDE_AUD_FOR_TESTING: &'static str = "some_override_aud"; + + pub fn new_for_devnet() -> Configuration { + Configuration { + override_aud_vals: vec![Self::OVERRIDE_AUD_FOR_TESTING.to_owned()], + max_zkid_signatures_per_txn: 3, + max_exp_horizon_secs: 10_000_000, // ~115.74 days + training_wheels_pubkey: None, + max_commited_epk_bytes: circuit_constants::MAX_COMMITED_EPK_BYTES, + max_iss_val_bytes: circuit_constants::MAX_ISS_VAL_BYTES, + max_extra_field_bytes: circuit_constants::MAX_EXTRA_FIELD_BYTES, + max_jwt_header_b64_bytes: circuit_constants::MAX_JWT_HEADER_B64_BYTES, + } + } + + pub fn new_for_testing() -> Configuration { + let mut config = Self::new_for_devnet(); + config.max_exp_horizon_secs = SAMPLE_EXP_HORIZON_SECS + 1; // ~31,689 years + config + } + + pub fn is_allowed_override_aud(&self, override_aud_val: &String) -> Result<(), VMStatus> { + let matches = self + .override_aud_vals + .iter() + .filter(|&e| e.eq(override_aud_val)) + .count(); + + if matches == 0 { + Err(invalid_signature!( + "override aud is not allow-listed in 0x1::zkid" + )) + } else { + Ok(()) + } + } +} diff --git a/types/src/zkid/groth16_sig.rs b/types/src/zkid/groth16_sig.rs new file mode 100644 index 0000000000000..9b018e2f7068c --- /dev/null +++ b/types/src/zkid/groth16_sig.rs @@ -0,0 +1,104 @@ +// Copyright © Aptos Foundation + +#[cfg(test)] +use crate::zkid::bn254_circom::{ + G1_PROJECTIVE_COMPRESSED_NUM_BYTES, G2_PROJECTIVE_COMPRESSED_NUM_BYTES, +}; +use crate::{ + transaction::authenticator::{EphemeralPublicKey, EphemeralSignature}, + zkid::bn254_circom::{G1Bytes, G2Bytes}, +}; +use anyhow::bail; +use aptos_crypto::CryptoMaterialError; +use aptos_crypto_derive::{BCSCryptoHash, CryptoHasher}; +use ark_bn254::{Bn254, Fr}; +use ark_groth16::{Groth16, PreparedVerifyingKey, Proof}; +use serde::{Deserialize, Serialize}; + +#[derive( + Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, CryptoHasher, BCSCryptoHash, +)] +pub struct Groth16Zkp { + 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, + /// 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, + /// An optional extra field (e.g., `"":"") that will be matched publicly in the JWT + pub extra_field: Option, + /// Will be set to the override `aud` value that the circuit should match, instead of the `aud` in the IDC. + /// This will allow users to recover their zkID accounts derived by an application that is no longer online. + pub override_aud_val: Option, + /// A signature on the proof (via the training wheels SK) to mitigate against flaws in our circuit + pub training_wheels_signature: Option, +} + +impl SignedGroth16Zkp { + pub fn verify_non_malleability_sig(&self, pub_key: &EphemeralPublicKey) -> anyhow::Result<()> { + self.non_malleability_signature.verify(&self.proof, pub_key) + } + + pub fn verify_training_wheels_sig(&self, pub_key: &EphemeralPublicKey) -> anyhow::Result<()> { + if let Some(training_wheels_signature) = &self.training_wheels_signature { + training_wheels_signature.verify(&self.proof, pub_key) + } else { + bail!("No training_wheels_signature found") + } + } + + pub fn verify_proof( + &self, + public_inputs_hash: Fr, + pvk: &PreparedVerifyingKey, + ) -> anyhow::Result<()> { + self.proof.verify_proof(public_inputs_hash, pvk) + } +} + +impl TryFrom<&[u8]> for Groth16Zkp { + type Error = CryptoMaterialError; + + fn try_from(bytes: &[u8]) -> Result { + bcs::from_bytes::(bytes).map_err(|_e| CryptoMaterialError::DeserializationError) + } +} + +impl Groth16Zkp { + pub fn new(a: G1Bytes, b: G2Bytes, c: G1Bytes) -> Self { + Groth16Zkp { a, b, c } + } + + #[cfg(test)] + pub fn dummy_proof() -> Self { + Groth16Zkp { + 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(), + } + } + + pub fn verify_proof( + &self, + public_inputs_hash: Fr, + pvk: &PreparedVerifyingKey, + ) -> anyhow::Result<()> { + let proof: Proof = Proof { + a: self.a.deserialize_into_affine()?, + b: self.b.as_affine()?, + c: self.c.deserialize_into_affine()?, + }; + let result = Groth16::::verify_proof(pvk, &proof, &[public_inputs_hash])?; + if !result { + bail!("groth16 proof verification failed") + } + Ok(()) + } +} diff --git a/types/src/zkid/groth16_vk.rs b/types/src/zkid/groth16_vk.rs new file mode 100644 index 0000000000000..6d1e485f5477a --- /dev/null +++ b/types/src/zkid/groth16_vk.rs @@ -0,0 +1,115 @@ +// Copyright © Aptos Foundation + +use crate::{move_utils::as_move_value::AsMoveValue, serialize}; +use aptos_crypto::CryptoMaterialError; +use ark_bn254::{Bn254, G1Affine, G2Affine}; +use ark_groth16::{PreparedVerifyingKey, VerifyingKey}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use move_core_types::{ + ident_str, + identifier::IdentStr, + move_resource::MoveStructType, + value::{MoveStruct, MoveValue}, +}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +/// Reflection of aptos_framework::zkid::Groth16PreparedVerificationKey +#[derive(Serialize, Deserialize, Debug)] +pub struct Groth16VerificationKey { + pub alpha_g1: Vec, + pub beta_g2: Vec, + pub gamma_g2: Vec, + pub delta_g2: Vec, + pub gamma_abc_g1: Vec>, +} + +impl AsMoveValue for Groth16VerificationKey { + fn as_move_value(&self) -> MoveValue { + MoveValue::Struct(MoveStruct::Runtime(vec![ + self.alpha_g1.as_move_value(), + self.beta_g2.as_move_value(), + self.gamma_g2.as_move_value(), + self.delta_g2.as_move_value(), + self.gamma_abc_g1.as_move_value(), + ])) + } +} + +/// WARNING: This struct uses resource groups on the Move side. Do NOT implement OnChainConfig +/// for it, since `OnChainConfig::fetch_config` does not work with resource groups (yet). +impl MoveStructType for Groth16VerificationKey { + const MODULE_NAME: &'static IdentStr = ident_str!("zkid"); + const STRUCT_NAME: &'static IdentStr = ident_str!("Groth16VerificationKey"); +} + +impl TryFrom for PreparedVerifyingKey { + type Error = CryptoMaterialError; + + fn try_from(vk: Groth16VerificationKey) -> Result { + if vk.gamma_abc_g1.len() != 2 { + return Err(CryptoMaterialError::DeserializationError); + } + + Ok(Self::from(VerifyingKey { + alpha_g1: G1Affine::deserialize_compressed(vk.alpha_g1.as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError)?, + beta_g2: G2Affine::deserialize_compressed(vk.beta_g2.as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError)?, + gamma_g2: G2Affine::deserialize_compressed(vk.gamma_g2.as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError)?, + delta_g2: G2Affine::deserialize_compressed(vk.delta_g2.as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError)?, + gamma_abc_g1: vec![ + G1Affine::deserialize_compressed(vk.gamma_abc_g1[0].as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError)?, + G1Affine::deserialize_compressed(vk.gamma_abc_g1[1].as_slice()) + .map_err(|_| CryptoMaterialError::DeserializationError)?, + ], + })) + } +} + +impl From> for Groth16VerificationKey { + fn from(pvk: PreparedVerifyingKey) -> Self { + let PreparedVerifyingKey { + vk: + VerifyingKey { + alpha_g1, + beta_g2, + gamma_g2, + delta_g2, + gamma_abc_g1, + }, + alpha_g1_beta_g2: _alpha_g1_beta_g2, // unnecessary for Move + gamma_g2_neg_pc: _gamma_g2_neg_pc, // unnecessary for Move + delta_g2_neg_pc: _delta_g2_neg_pc, // unnecessary for Move + } = pvk; + + let mut gamma_abc_g1_bytes = Vec::with_capacity(gamma_abc_g1.len()); + for e in gamma_abc_g1.iter() { + gamma_abc_g1_bytes.push(serialize!(e)); + } + + Groth16VerificationKey { + alpha_g1: serialize!(alpha_g1), + beta_g2: serialize!(beta_g2), + gamma_g2: serialize!(gamma_g2), + delta_g2: serialize!(delta_g2), + gamma_abc_g1: gamma_abc_g1_bytes, + } + } +} + +impl Display for Groth16VerificationKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "alpha_g1: {}", hex::encode(&self.alpha_g1))?; + write!(f, "beta_g2: {}", hex::encode(&self.beta_g2))?; + write!(f, "gamma_g2: {}", hex::encode(&self.gamma_g2))?; + write!(f, "delta_g2: {}", hex::encode(&self.delta_g2))?; + for (i, e) in self.gamma_abc_g1.iter().enumerate() { + write!(f, "gamma_abc_g1[{i}]: {}", hex::encode(serialize!(e)))?; + } + Ok(()) + } +} diff --git a/types/src/zkid/mod.rs b/types/src/zkid/mod.rs new file mode 100644 index 0000000000000..d0e5cd54fcf65 --- /dev/null +++ b/types/src/zkid/mod.rs @@ -0,0 +1,306 @@ +// Copyright © Aptos Foundation + +use crate::{ + on_chain_config::CurrentTimeMicroseconds, + transaction::{ + authenticator::{ + AnyPublicKey, AnySignature, EphemeralPublicKey, EphemeralSignature, MAX_NUM_OF_SIGS, + }, + SignedTransaction, + }, +}; +use anyhow::bail; +use aptos_crypto::{poseidon_bn254, CryptoMaterialError, ValidCryptoMaterial}; +use ark_bn254::Bn254; +use ark_groth16::PreparedVerifyingKey; +use ark_serialize::CanonicalSerialize; +use base64::URL_SAFE_NO_PAD; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::{ + str, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +mod bn254_circom; +mod circuit_constants; +mod circuit_testcases; +mod configuration; +mod groth16_sig; +mod groth16_vk; +mod openid_sig; +pub mod test_utils; + +use crate::zkid::circuit_constants::devnet_prepared_vk; +pub use bn254_circom::get_public_inputs_hash; +pub use configuration::Configuration; +pub use groth16_sig::{Groth16Zkp, SignedGroth16Zkp}; +pub use groth16_vk::Groth16VerificationKey; +pub use openid_sig::{Claims, OpenIdSig}; + +/// The devnet VK that is initialized during genesis. +pub static DEVNET_VERIFICATION_KEY: Lazy> = + Lazy::new(devnet_prepared_vk); + +#[macro_export] +macro_rules! invalid_signature { + ($message:expr) => { + VMStatus::error(StatusCode::INVALID_SIGNATURE, Some($message.to_owned())) + }; +} + +/// Useful macro for arkworks serialization! +#[macro_export] +macro_rules! serialize { + ($obj:expr) => {{ + let mut buf = vec![]; + $obj.serialize_compressed(&mut buf).unwrap(); + buf + }}; +} + +/// 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. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] +pub enum ZkpOrOpenIdSig { + Groth16Zkp(SignedGroth16Zkp), + OpenIdSig(OpenIdSig), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] +pub struct ZkIdSignature { + /// 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, + + /// The base64url-encoded header (no dot at the end), which contains two relevant fields: + /// 1. `kid`, which indicates which of the OIDC provider's JWKs should be used to verify the + /// \[ZKPoK of an\] OpenID signature., + /// 2. `alg`, which indicates which type of signature scheme was used to sign the JWT + pub jwt_header_b64: String, + + /// The expiry time of the `ephemeral_pubkey` represented as a UNIX epoch timestamp in seconds. + pub exp_timestamp_secs: u64, + + /// 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`. + pub ephemeral_signature: EphemeralSignature, +} + +impl TryFrom<&[u8]> for ZkIdSignature { + type Error = CryptoMaterialError; + + fn try_from(bytes: &[u8]) -> Result { + bcs::from_bytes::(bytes) + .map_err(|_e| CryptoMaterialError::DeserializationError) + } +} + +impl ValidCryptoMaterial for ZkIdSignature { + fn to_bytes(&self) -> Vec { + bcs::to_bytes(&self).expect("Only unhandleable errors happen here.") + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JWTHeader { + pub kid: String, + pub alg: String, +} + +impl ZkIdSignature { + /// A reasonable upper bound for the number of bytes we expect in a zkID public key. This is + /// enforced by our full nodes when they receive zkID TXNs. + pub const MAX_LEN: usize = 4000; + + pub fn parse_jwt_header(&self) -> anyhow::Result { + let jwt_header_json = base64url_decode_as_str(&self.jwt_header_b64)?; + let header: JWTHeader = serde_json::from_str(&jwt_header_json)?; + Ok(header) + } + + pub fn verify_expiry(&self, current_time: &CurrentTimeMicroseconds) -> anyhow::Result<()> { + let block_time = UNIX_EPOCH + Duration::from_micros(current_time.microseconds); + let expiry_time = seconds_from_epoch(self.exp_timestamp_secs); + + if block_time > expiry_time { + bail!("zkID Signature is expired"); + } else { + Ok(()) + } + } +} + +/// The pepper is used to create a _hiding_ identity commitment (IDC) when deriving a zkID address. +/// We fix its size at `poseidon_bn254::BYTES_PACKED_PER_SCALAR` to avoid extra hashing work when +/// computing the public inputs hash. +/// +/// This value should **NOT* be changed since on-chain addresses are based on it (e.g., +/// hashing with a larger pepper would lead to a different address). +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct Pepper(pub(crate) [u8; poseidon_bn254::BYTES_PACKED_PER_SCALAR]); + +impl Pepper { + pub const NUM_BYTES: usize = poseidon_bn254::BYTES_PACKED_PER_SCALAR; + + pub fn new(bytes: [u8; Self::NUM_BYTES]) -> Self { + Self(bytes) + } + + pub fn to_bytes(&self) -> &[u8; Self::NUM_BYTES] { + &self.0 + } + + pub fn from_hex(hex: &str) -> Self { + let bytes = hex::decode(hex).unwrap(); + let mut extended_bytes = [0u8; Self::NUM_BYTES]; + extended_bytes.copy_from_slice(&bytes); + Self(extended_bytes) + } + + // Used for testing. #[cfg(test)] doesn't seem to allow for use in smoke tests. + pub fn from_number(num: u128) -> Self { + let big_int = num_bigint::BigUint::from(num); + let bytes: Vec = big_int.to_bytes_le(); + let mut extended_bytes = [0u8; Self::NUM_BYTES]; + extended_bytes[..bytes.len()].copy_from_slice(&bytes); + Self(extended_bytes) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct IdCommitment(#[serde(with = "serde_bytes")] pub(crate) Vec); + +impl IdCommitment { + /// The max length of the value of the JWT's `aud` field supported in our circuit. zkID address + /// derivation depends on this, so it should not be changed. + pub const MAX_AUD_VAL_BYTES: usize = circuit_constants::MAX_AUD_VAL_BYTES; + /// The max length of the JWT field name that stores the user's ID (e.g., `sub`, `email`) which is + /// supported in our circuit. zkID address derivation depends on this, so it should not be changed. + pub const MAX_UID_KEY_BYTES: usize = circuit_constants::MAX_UID_KEY_BYTES; + /// The max length of the value of the JWT's UID field (`sub`, `email`) that stores the user's ID + /// which is supported in our circuit. zkID address derivation depends on this, so it should not + /// be changed. + pub const MAX_UID_VAL_BYTES: usize = circuit_constants::MAX_UID_VAL_BYTES; + /// The size of the identity commitment (IDC) used to derive a zkID address. This value should **NOT* + /// be changed since on-chain addresses are based on it (e.g., hashing a larger-sized IDC would lead + /// to a different address). + pub const NUM_BYTES: usize = 32; + + pub fn new_from_preimage( + pepper: &Pepper, + aud: &str, + uid_key: &str, + uid_val: &str, + ) -> anyhow::Result { + let aud_val_hash = poseidon_bn254::pad_and_hash_string(aud, Self::MAX_AUD_VAL_BYTES)?; + // println!("aud_val_hash: {}", aud_val_hash); + let uid_key_hash = poseidon_bn254::pad_and_hash_string(uid_key, Self::MAX_UID_KEY_BYTES)?; + // println!("uid_key_hash: {}", uid_key_hash); + let uid_val_hash = poseidon_bn254::pad_and_hash_string(uid_val, Self::MAX_UID_VAL_BYTES)?; + // println!("uid_val_hash: {}", uid_val_hash); + let pepper_scalar = poseidon_bn254::pack_bytes_to_one_scalar(pepper.0.as_slice())?; + // println!("Pepper Fr: {}", pepper_scalar); + + let fr = poseidon_bn254::hash_scalars(vec![ + pepper_scalar, + aud_val_hash, + uid_val_hash, + uid_key_hash, + ])?; + + let mut idc_bytes = vec![0u8; IdCommitment::NUM_BYTES]; + fr.serialize_uncompressed(&mut idc_bytes[..])?; + Ok(IdCommitment(idc_bytes)) + } + + pub fn to_bytes(&self) -> Vec { + bcs::to_bytes(&self).expect("Only unhandleable errors happen here.") + } +} + +impl TryFrom<&[u8]> for IdCommitment { + type Error = CryptoMaterialError; + + fn try_from(_value: &[u8]) -> Result { + bcs::from_bytes::(_value) + .map_err(|_e| CryptoMaterialError::DeserializationError) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ZkIdPublicKey { + /// The value of the `iss` field from the JWT, indicating the OIDC provider. + /// e.g., https://accounts.google.com + pub iss_val: String, + + /// SNARK-friendly commitment to: + /// 1. The application's ID; i.e., the `aud` field in the signed OIDC JWT representing the OAuth client ID. + /// 2. The OIDC provider's internal identifier for the user; e.g., the `sub` field in the signed OIDC JWT + /// which is Google's internal user identifier for bob@gmail.com, or the `email` field. + /// + /// e.g., H(aud || uid_key || uid_val || pepper), where `pepper` is the commitment's randomness used to hide + /// `aud` and `sub`. + pub idc: IdCommitment, +} + +impl ZkIdPublicKey { + /// A reasonable upper bound for the number of bytes we expect in a zkID public key. This is + /// enforced by our full nodes when they receive zkID TXNs. + pub const MAX_LEN: usize = 200 + IdCommitment::NUM_BYTES; + + pub fn to_bytes(&self) -> Vec { + bcs::to_bytes(&self).expect("Only unhandleable errors happen here.") + } +} + +impl TryFrom<&[u8]> for ZkIdPublicKey { + type Error = CryptoMaterialError; + + fn try_from(_value: &[u8]) -> Result { + bcs::from_bytes::(_value) + .map_err(|_e| CryptoMaterialError::DeserializationError) + } +} + +pub fn get_zkid_authenticators( + transaction: &SignedTransaction, +) -> anyhow::Result> { + // Check all the signers in the TXN + let single_key_authenticators = transaction + .authenticator_ref() + .to_single_key_authenticators()?; + let mut authenticators = Vec::with_capacity(MAX_NUM_OF_SIGS); + for authenticator in single_key_authenticators { + if let (AnyPublicKey::ZkId { public_key }, AnySignature::ZkId { signature }) = + (authenticator.public_key(), authenticator.signature()) + { + authenticators.push((public_key.clone(), signature.clone())) + } + } + Ok(authenticators) +} + +pub(crate) fn base64url_encode_str(data: &str) -> String { + base64::encode_config(data.as_bytes(), URL_SAFE_NO_PAD) +} + +pub(crate) fn base64url_encode_bytes(data: &[u8]) -> String { + base64::encode_config(data, URL_SAFE_NO_PAD) +} + +fn base64url_decode_as_str(b64: &str) -> anyhow::Result { + let decoded_bytes = base64::decode_config(b64, URL_SAFE_NO_PAD)?; + // Convert the decoded bytes to a UTF-8 string + let str = String::from_utf8(decoded_bytes)?; + Ok(str) +} + +fn seconds_from_epoch(secs: u64) -> SystemTime { + UNIX_EPOCH + Duration::from_secs(secs) +} + +#[cfg(test)] +mod tests; diff --git a/types/src/zkid/openid_sig.rs b/types/src/zkid/openid_sig.rs new file mode 100644 index 0000000000000..60f2ab1c3d2cc --- /dev/null +++ b/types/src/zkid/openid_sig.rs @@ -0,0 +1,206 @@ +// Copyright © Aptos Foundation + +use crate::{ + jwks::rsa::RSA_JWK, + transaction::authenticator::EphemeralPublicKey, + zkid::{ + base64url_decode_as_str, seconds_from_epoch, Configuration, IdCommitment, Pepper, + ZkIdPublicKey, + }, +}; +use anyhow::{ensure, Context}; +use aptos_crypto::{poseidon_bn254, CryptoMaterialError}; +use ark_bn254::Fr; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_with::skip_serializing_none; +use std::collections::BTreeMap; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] +pub struct OpenIdSig { + /// The base64url encoded JWS signature of the OIDC JWT (https://datatracker.ietf.org/doc/html/rfc7515#section-3) + pub jwt_sig_b64: String, + /// The base64url encoded JSON payload of the OIDC JWT (https://datatracker.ietf.org/doc/html/rfc7519#section-3) + pub jwt_payload_b64: String, + /// The name of the key in the claim that maps to the user identifier; e.g., "sub" or "email" + pub uid_key: String, + /// The random value used to obfuscate the EPK from OIDC providers in the nonce field + #[serde(with = "serde_bytes")] + pub epk_blinder: Vec, + /// The privacy-preserving value used to calculate the identity commitment. It is typically uniquely derived from `(iss, client_id, uid_key, uid_val)`. + pub pepper: Pepper, + /// When an override aud_val is used, the signature needs to contain the aud_val committed in the + /// IDC, since the JWT will contain the override. + pub idc_aud_val: Option, +} + +impl OpenIdSig { + /// The size of the blinding factor used to compute the nonce commitment to the EPK and expiration + /// date. This can be upgraded, if the OAuth nonce reconstruction is upgraded carefully. + pub const EPK_BLINDER_NUM_BYTES: usize = poseidon_bn254::BYTES_PACKED_PER_SCALAR; + + /// Verifies an `OpenIdSig` by doing the following checks: + /// 1. Check that the ephemeral public key lifespan is under MAX_EXPIRY_HORIZON_SECS + /// 2. Check that the iss claim in the ZkIdPublicKey matches the one in the jwt_payload + /// 3. Check that the identity commitment in the ZkIdPublicKey matches the one constructed from the jwt_payload + /// 4. Check that the nonce constructed from the ephemeral public key, blinder, and exp_timestamp_secs matches the one in the jwt_payload + // TODO(zkid): Refactor to return a `Result<(), VMStatus>` because (1) this is now called in the + // VM and (2) is_override_aud_allowed does. + pub fn verify_jwt_claims( + &self, + exp_timestamp_secs: u64, + epk: &EphemeralPublicKey, + pk: &ZkIdPublicKey, + config: &Configuration, + ) -> anyhow::Result<()> { + let jwt_payload_json = base64url_decode_as_str(&self.jwt_payload_b64)?; + let claims: Claims = serde_json::from_str(&jwt_payload_json)?; + + let max_expiration_date = + seconds_from_epoch(claims.oidc_claims.iat + config.max_exp_horizon_secs); + let expiration_date = seconds_from_epoch(exp_timestamp_secs); + + ensure!( + expiration_date < max_expiration_date, + "The ephemeral public key's expiration date is too far into the future" + ); + + ensure!( + claims.oidc_claims.iss.eq(&pk.iss_val), + "'iss' claim was supposed to match \"{}\"", + pk.iss_val + ); + + // When an aud_val override is set, the IDC-committed `aud` is included next to the + // OpenID signature. + let idc_aud_val = match self.idc_aud_val.as_ref() { + None => &claims.oidc_claims.aud, + Some(idc_aud_val) => { + // If there's an override, check that the override `aud` from the JWT, is allow-listed + ensure!( + config + .is_allowed_override_aud(&claims.oidc_claims.aud) + .is_ok(), + "{} is not an allow-listed override aud", + &claims.oidc_claims.aud + ); + idc_aud_val + }, + }; + let uid_val = claims.get_uid_val(&self.uid_key)?; + ensure!( + IdCommitment::new_from_preimage(&self.pepper, idc_aud_val, &self.uid_key, &uid_val)? + .eq(&pk.idc), + "Address IDC verification failed" + ); + + let actual_nonce = OpenIdSig::reconstruct_oauth_nonce( + &self.epk_blinder[..], + exp_timestamp_secs, + epk, + config, + )?; + ensure!( + actual_nonce.eq(&claims.oidc_claims.nonce), + "'nonce' claim did not match: JWT contained {} but recomputed {}", + claims.oidc_claims.nonce, + actual_nonce + ); + + Ok(()) + } + + pub fn verify_jwt_signature( + &self, + rsa_jwk: &RSA_JWK, + jwt_header_b64: &String, + ) -> anyhow::Result<()> { + let jwt_payload_b64 = &self.jwt_payload_b64; + let jwt_sig_b64 = &self.jwt_sig_b64; + let jwt_token = format!("{}.{}.{}", jwt_header_b64, jwt_payload_b64, jwt_sig_b64); + rsa_jwk.verify_signature(&jwt_token)?; + Ok(()) + } + + pub fn reconstruct_oauth_nonce( + epk_blinder: &[u8], + exp_timestamp_secs: u64, + epk: &EphemeralPublicKey, + config: &Configuration, + ) -> anyhow::Result { + let mut frs = poseidon_bn254::pad_and_pack_bytes_to_scalars_with_len( + epk.to_bytes().as_slice(), + config.max_commited_epk_bytes as usize, + )?; + + frs.push(Fr::from(exp_timestamp_secs)); + frs.push(poseidon_bn254::pack_bytes_to_one_scalar(epk_blinder)?); + + let nonce_fr = poseidon_bn254::hash_scalars(frs)?; + Ok(nonce_fr.to_string()) + } +} + +impl TryFrom<&[u8]> for OpenIdSig { + type Error = CryptoMaterialError; + + fn try_from(bytes: &[u8]) -> Result { + bcs::from_bytes::(bytes).map_err(|_e| CryptoMaterialError::DeserializationError) + } +} + +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OidcClaims { + pub iss: String, + pub aud: String, + pub sub: String, + pub nonce: String, + pub iat: u64, + pub exp: u64, + pub email: Option, + pub email_verified: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Claims { + #[serde(flatten)] + pub oidc_claims: OidcClaims, + #[serde(default)] + pub additional_claims: BTreeMap, +} + +impl Claims { + fn get_uid_val(&self, uid_key: &String) -> anyhow::Result { + match uid_key.as_str() { + "email" => { + let email_verified = self + .oidc_claims + .email_verified + .clone() + .context("'email_verified' claim is missing")?; + // the 'email_verified' claim may be a boolean or a boolean-as-a-string. + let email_verified_as_bool = email_verified.as_bool().unwrap_or(false); + let email_verified_as_str = email_verified.as_str().unwrap_or("false"); + ensure!( + email_verified_as_bool || email_verified_as_str.eq("true"), + "'email_verified' claim was not \"true\"" + ); + self.oidc_claims + .email + .clone() + .context("email claim missing on jwt") + }, + "sub" => Ok(self.oidc_claims.sub.clone()), + _ => { + let uid_val = self + .additional_claims + .get(uid_key) + .context(format!("{} claim missing on jwt", uid_key))? + .as_str() + .context(format!("{} value is not a string", uid_key))?; + Ok(uid_val.to_string()) + }, + } + } +} diff --git a/types/src/zkid/test_utils.rs b/types/src/zkid/test_utils.rs new file mode 100644 index 0000000000000..7201dd12d6e51 --- /dev/null +++ b/types/src/zkid/test_utils.rs @@ -0,0 +1,138 @@ +// Copyright © Aptos Foundation + +use crate::{ + jwks::rsa::RSA_JWK, + transaction::authenticator::EphemeralSignature, + zkid::{ + base64url_encode_bytes, base64url_encode_str, + circuit_testcases::{ + SAMPLE_EPK, SAMPLE_EPK_BLINDER, SAMPLE_ESK, SAMPLE_EXP_DATE, SAMPLE_EXP_HORIZON_SECS, + SAMPLE_JWK, SAMPLE_JWK_SK, SAMPLE_JWT_EXTRA_FIELD, SAMPLE_JWT_HEADER_B64, + SAMPLE_JWT_PARSED, SAMPLE_PEPPER, SAMPLE_PROOF, SAMPLE_UID_KEY, SAMPLE_ZKID_PK, + }, + Groth16Zkp, OpenIdSig, SignedGroth16Zkp, ZkIdPublicKey, ZkIdSignature, ZkpOrOpenIdSig, + }, +}; +use aptos_crypto::{ed25519::Ed25519PrivateKey, SigningKey, Uniform}; +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()) +}); + +pub fn get_sample_esk() -> Ed25519PrivateKey { + // Cloning is disabled outside #[cfg(test)] + let serialized: &[u8] = &(SAMPLE_ESK.to_bytes()); + Ed25519PrivateKey::try_from(serialized).unwrap() +} + +pub fn get_sample_iss() -> String { + SAMPLE_JWT_PARSED.oidc_claims.iss.clone() +} + +pub fn get_sample_jwk() -> RSA_JWK { + SAMPLE_JWK.clone() +} + +/// Note: Does not have a valid ephemeral signature. Use the SAMPLE_ESK to compute one over the +/// desired TXN. +pub fn get_sample_zkid_groth16_sig_and_pk() -> (ZkIdSignature, ZkIdPublicKey) { + let proof = *SAMPLE_PROOF; + + let groth16zkp = SignedGroth16Zkp { + proof, + non_malleability_signature: EphemeralSignature::ed25519(SAMPLE_ESK.sign(&proof).unwrap()), + 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 = ZkIdSignature { + sig: ZkpOrOpenIdSig::Groth16Zkp(groth16zkp.clone()), + jwt_header_b64: SAMPLE_JWT_HEADER_B64.to_string(), + exp_timestamp_secs: SAMPLE_EXP_DATE, + ephemeral_pubkey: SAMPLE_EPK.clone(), + ephemeral_signature: DUMMY_EPHEMERAL_SIGNATURE.clone(), + }; + + (zk_sig, SAMPLE_ZKID_PK.clone()) +} + +/// Note: Does not have a valid ephemeral signature. Use the SAMPLE_ESK to compute one over the +/// desired TXN. +pub fn get_sample_zkid_openid_sig_and_pk() -> (ZkIdSignature, ZkIdPublicKey) { + let jwt_payload_b64 = + base64url_encode_str(serde_json::to_string(&*SAMPLE_JWT_PARSED).unwrap().as_str()); + + let jwt_header_b64 = SAMPLE_JWT_HEADER_B64.to_string(); + let msg = jwt_header_b64.clone() + "." + jwt_payload_b64.as_str(); + let rng = ring::rand::SystemRandom::new(); + let sk = &*SAMPLE_JWK_SK; + let mut jwt_sig = vec![0u8; sk.public_modulus_len()]; + + sk.sign( + &signature::RSA_PKCS1_SHA256, + &rng, + msg.as_bytes(), + jwt_sig.as_mut_slice(), + ) + .unwrap(); + + let openid_sig = OpenIdSig { + jwt_sig_b64: base64url_encode_bytes(jwt_sig.as_slice()), + jwt_payload_b64, + uid_key: SAMPLE_UID_KEY.to_owned(), + epk_blinder: SAMPLE_EPK_BLINDER.clone(), + pepper: SAMPLE_PEPPER.clone(), + idc_aud_val: None, + }; + + let zk_sig = ZkIdSignature { + sig: ZkpOrOpenIdSig::OpenIdSig(openid_sig.clone()), + jwt_header_b64, + exp_timestamp_secs: SAMPLE_EXP_DATE, + ephemeral_pubkey: SAMPLE_EPK.clone(), + ephemeral_signature: DUMMY_EPHEMERAL_SIGNATURE.clone(), + }; + + (zk_sig, SAMPLE_ZKID_PK.clone()) +} + +#[cfg(test)] +mod test { + use crate::zkid::{ + circuit_testcases::{SAMPLE_EPK, SAMPLE_EPK_BLINDER, SAMPLE_EXP_DATE, SAMPLE_JWK}, + get_public_inputs_hash, + test_utils::get_sample_zkid_groth16_sig_and_pk, + Configuration, OpenIdSig, + }; + + /// Since our proof generation toolkit is incomplete; currently doing it here. + #[test] + fn zkid_print_nonce_commitment_and_public_inputs_hash() { + let config = Configuration::new_for_testing(); + let nonce = OpenIdSig::reconstruct_oauth_nonce( + SAMPLE_EPK_BLINDER.as_slice(), + SAMPLE_EXP_DATE, + &SAMPLE_EPK, + &config, + ) + .unwrap(); + println!( + "Nonce computed from exp_date {} and EPK blinder {}: {}", + SAMPLE_EXP_DATE, + hex::encode(SAMPLE_EPK_BLINDER.as_slice()), + nonce + ); + + let (zkid_sig, zkid_pk) = get_sample_zkid_groth16_sig_and_pk(); + let public_inputs_hash = + get_public_inputs_hash(&zkid_sig, &zkid_pk, &SAMPLE_JWK, &config).unwrap(); + + println!("Public inputs hash: {}", public_inputs_hash); + } +} diff --git a/types/src/zkid/tests.rs b/types/src/zkid/tests.rs new file mode 100644 index 0000000000000..6392d7141f569 --- /dev/null +++ b/types/src/zkid/tests.rs @@ -0,0 +1,136 @@ +// Copyright © Aptos Foundation + +use crate::zkid::{ + base64url_encode_str, + bn254_circom::get_public_inputs_hash, + circuit_testcases::*, + test_utils::{get_sample_zkid_groth16_sig_and_pk, get_sample_zkid_openid_sig_and_pk}, + Configuration, ZkpOrOpenIdSig, DEVNET_VERIFICATION_KEY, +}; +use std::ops::{AddAssign, Deref}; + +// TODO(zkid): Add instructions on how to produce this test case. +#[test] +fn test_zkid_groth16_proof_verification() { + let config = Configuration::new_for_devnet(); + + let (zk_sig, zk_pk) = get_sample_zkid_groth16_sig_and_pk(); + let proof = match &zk_sig.sig { + ZkpOrOpenIdSig::Groth16Zkp(proof) => proof.clone(), + ZkpOrOpenIdSig::OpenIdSig(_) => panic!("Internal inconsistency"), + }; + + let public_inputs_hash = get_public_inputs_hash(&zk_sig, &zk_pk, &SAMPLE_JWK, &config).unwrap(); + + println!( + "zkID Groth16 test public inputs hash: {}", + public_inputs_hash + ); + + proof + .verify_proof(public_inputs_hash, DEVNET_VERIFICATION_KEY.deref()) + .unwrap(); +} + +#[test] +fn test_zkid_oidc_sig_verifies() { + // Verification should succeed + let config = Configuration::new_for_testing(); + let (zkid_sig, zkid_pk) = get_sample_zkid_openid_sig_and_pk(); + + let oidc_sig = match &zkid_sig.sig { + ZkpOrOpenIdSig::Groth16Zkp(_) => panic!("Internal inconsistency"), + ZkpOrOpenIdSig::OpenIdSig(oidc_sig) => oidc_sig.clone(), + }; + + oidc_sig + .verify_jwt_claims( + zkid_sig.exp_timestamp_secs, + &zkid_sig.ephemeral_pubkey, + &zkid_pk, + &config, + ) + .unwrap(); + + oidc_sig + .verify_jwt_signature(&SAMPLE_JWK, &zkid_sig.jwt_header_b64) + .unwrap(); + + // Maul the pepper; verification should fail + let mut bad_oidc_sig = oidc_sig.clone(); + bad_oidc_sig.pepper.0[0].add_assign(1); + assert_ne!(bad_oidc_sig.pepper, oidc_sig.pepper); + + let e = bad_oidc_sig + .verify_jwt_claims( + zkid_sig.exp_timestamp_secs, + &zkid_sig.ephemeral_pubkey, + &zkid_pk, + &config, + ) + .unwrap_err(); + assert!(e.to_string().contains("IDC verification failed")); + + // Expiration date is past the expiration horizon; verification should fail + let bad_oidc_sig = oidc_sig.clone(); + let e = bad_oidc_sig + .verify_jwt_claims( + SAMPLE_JWT_PARSED.oidc_claims.iat + config.max_exp_horizon_secs, + &zkid_sig.ephemeral_pubkey, + &zkid_pk, + &config, + ) + .unwrap_err(); + assert!(e.to_string().contains("expiration date is too far")); + + // `sub` field does not match IDC; verification should fail + let mut bad_oidc_sig = oidc_sig.clone(); + let mut jwt = SAMPLE_JWT_PARSED.clone(); + jwt.oidc_claims.sub = format!("{}+1", SAMPLE_JWT_PARSED.oidc_claims.sub); + bad_oidc_sig.jwt_payload_b64 = + base64url_encode_str(serde_json::to_string(&jwt).unwrap().as_str()); + + let e = bad_oidc_sig + .verify_jwt_claims( + zkid_sig.exp_timestamp_secs, + &zkid_sig.ephemeral_pubkey, + &zkid_pk, + &config, + ) + .unwrap_err(); + assert!(e.to_string().contains("IDC verification failed")); + + // `nonce` field is wrong; verification should fail + let mut bad_oidc_sig = oidc_sig.clone(); + let mut jwt = SAMPLE_JWT_PARSED.clone(); + jwt.oidc_claims.nonce = "bad nonce".to_string(); + bad_oidc_sig.jwt_payload_b64 = + base64url_encode_str(serde_json::to_string(&jwt).unwrap().as_str()); + + let e = bad_oidc_sig + .verify_jwt_claims( + zkid_sig.exp_timestamp_secs, + &zkid_sig.ephemeral_pubkey, + &zkid_pk, + &config, + ) + .unwrap_err(); + assert!(e.to_string().contains("'nonce' claim")); + + // `iss` field is wrong; verification should fail + let mut bad_oidc_sig = oidc_sig.clone(); + let mut jwt = SAMPLE_JWT_PARSED.clone(); + jwt.oidc_claims.iss = "bad iss".to_string(); + bad_oidc_sig.jwt_payload_b64 = + base64url_encode_str(serde_json::to_string(&jwt).unwrap().as_str()); + + let e = bad_oidc_sig + .verify_jwt_claims( + zkid_sig.exp_timestamp_secs, + &zkid_sig.ephemeral_pubkey, + &zkid_pk, + &config, + ) + .unwrap_err(); + assert!(e.to_string().contains("'iss' claim ")); +} From f92a19735c2a3983e4c27317c7f597ca1bc98d8b Mon Sep 17 00:00:00 2001 From: George Mitenkov Date: Tue, 20 Feb 2024 02:06:10 +0100 Subject: [PATCH 02/39] [aptosvm] Simplify VM flows (#11888) * Duplicated logic for creating the gas meter for view functions has been removed. * Duplicated logic for calculating gas used for view functions has been removed. * There was unreachable code in failure transaction cleanup, where the discarded status has been returned immediately, but then re-checked again. The first check is shifted inside. * No more default transaction metadata. * Scripts are now validated consistently. * Simplifies transaction execution function signature to avoid `Option`. * Removes duplicated features from `AptosVM` and keeps them in `MoveVMExt`. * Fixes a bug when script hash was not computed for `RunOnAbort`. Related tests are moved to `move-e2e-tests`. --- aptos-move/aptos-vm/src/aptos_vm.rs | 416 +++++++----------- .../aptos-vm/src/block_executor/vm_wrapper.rs | 21 +- .../aptos-vm/src/move_vm_ext/session.rs | 43 +- aptos-move/aptos-vm/src/move_vm_ext/vm.rs | 40 +- aptos-move/aptos-vm/src/testing.rs | 39 ++ .../aptos-vm/src/transaction_metadata.rs | 31 +- aptos-move/aptos-vm/src/zkid_validation.rs | 18 +- .../e2e-move-tests/src/tests/account.rs | 25 ++ aptos-move/e2e-move-tests/src/tests/mod.rs | 2 + aptos-move/e2e-move-tests/src/tests/vm.rs | 58 +++ .../src/tests/failed_transaction_tests.rs | 121 ----- .../src/tests/invariant_violation.rs | 7 +- aptos-move/e2e-testsuite/src/tests/mod.rs | 1 - .../execution/ptx-executor/src/runner.rs | 2 +- 14 files changed, 353 insertions(+), 471 deletions(-) create mode 100644 aptos-move/e2e-move-tests/src/tests/account.rs create mode 100644 aptos-move/e2e-move-tests/src/tests/vm.rs delete mode 100644 aptos-move/e2e-testsuite/src/tests/failed_transaction_tests.rs diff --git a/aptos-move/aptos-vm/src/aptos_vm.rs b/aptos-move/aptos-vm/src/aptos_vm.rs index 0b4207c3d5f58..ebadab6d2487d 100644 --- a/aptos-move/aptos-vm/src/aptos_vm.rs +++ b/aptos-move/aptos-vm/src/aptos_vm.rs @@ -9,15 +9,15 @@ use crate::{ errors::{discarded_output, expect_only_successful_execution}, gas::{check_gas, get_gas_parameters}, move_vm_ext::{ - get_max_binary_format_version, AptosMoveResolver, MoveVmExt, RespawnedSession, SessionExt, - SessionId, + get_max_binary_format_version, get_max_identifier_size, AptosMoveResolver, MoveVmExt, + RespawnedSession, SessionExt, SessionId, }, sharded_block_executor::{executor_client::ExecutorClient, ShardedBlockExecutor}, system_module_names::*, transaction_metadata::TransactionMetadata, transaction_validation, verifier, zkid_validation, VMExecutor, VMValidator, }; -use anyhow::{anyhow, Result}; +use anyhow::anyhow; use aptos_block_executor::txn_commit_hook::NoOpTransactionCommitHook; use aptos_crypto::HashValue; use aptos_framework::{natives::code::PublishRequest, RuntimeModuleMetadataV1}; @@ -44,16 +44,15 @@ use aptos_types::{ new_epoch_event_key, ConfigurationResource, FeatureFlag, Features, OnChainConfig, TimedFeatureOverride, TimedFeatures, TimedFeaturesBuilder, }, - state_store::StateView, + state_store::{StateView, TStateView}, transaction::{ authenticator::AnySignature, signature_verified_transaction::SignatureVerifiedTransaction, BlockOutput, EntryFunction, ExecutionError, ExecutionStatus, ModuleBundle, Multisig, - MultisigTransactionPayload, SignatureCheckedTransaction, SignedTransaction, Transaction, - TransactionOutput, TransactionPayload, TransactionStatus, VMValidatorResult, + MultisigTransactionPayload, Script, SignatureCheckedTransaction, SignedTransaction, + Transaction, TransactionOutput, TransactionPayload, TransactionStatus, VMValidatorResult, ViewFunctionOutput, WriteSetPayload, }, vm_status::{AbortLocation, StatusCode, VMStatus}, - zkid::ZkpOrOpenIdSig, }; use aptos_utils::{aptos_try, return_on_failure}; use aptos_vm_logging::{log_schema::AdapterLogSchema, speculative_error, speculative_log}; @@ -71,7 +70,6 @@ use move_binary_format::{ compatibility::Compatibility, deserializer::DeserializerConfig, errors::{Location, PartialVMError, PartialVMResult, VMError, VMResult}, - file_format_common::{IDENTIFIER_SIZE_MAX, LEGACY_IDENTIFIER_SIZE_MAX}, CompiledModule, }; use move_core_types::{ @@ -166,7 +164,6 @@ pub struct AptosVM { gas_feature_version: u64, gas_params: Result, pub(crate) storage_gas_params: Result, - features: Features, timed_features: TimedFeatures, } @@ -212,7 +209,7 @@ impl AptosVM { misc_gas_params, gas_feature_version, chain_id.id(), - features.clone(), + features, timed_features.clone(), resolver, aggregator_v2_type_tagging, @@ -225,7 +222,6 @@ impl AptosVM { gas_feature_version, gas_params, storage_gas_params, - features, timed_features, } } @@ -238,6 +234,11 @@ impl AptosVM { self.move_vm.new_session(resolver, session_id) } + #[inline(always)] + fn features(&self) -> &Features { + self.move_vm.features() + } + /// Sets execution concurrency level when invoked the first time. pub fn set_concurrency_level_once(mut concurrency_level: usize) { concurrency_level = min(concurrency_level, num_cpus::get()); @@ -344,28 +345,6 @@ impl AptosVM { get_or_vm_startup_failure(&self.gas_params, &log_context) } - /// Generates a transaction output for a transaction that encountered errors during the - /// execution process. This is public for now only for tests. - pub fn failed_transaction_cleanup( - &self, - error_code: VMStatus, - gas_meter: &mut impl AptosGasMeter, - txn_data: &TransactionMetadata, - resolver: &impl AptosMoveResolver, - log_context: &AdapterLogSchema, - change_set_configs: &ChangeSetConfigs, - ) -> VMOutput { - self.failed_transaction_cleanup_and_keep_vm_status( - error_code, - gas_meter, - txn_data, - resolver, - log_context, - change_set_configs, - ) - .1 - } - pub fn as_move_resolver<'r, R: ExecutorView>( &self, executor_view: &'r R, @@ -373,7 +352,7 @@ impl AptosVM { StorageAdapter::new_with_config( executor_view, self.gas_feature_version, - &self.features, + self.features(), None, ) } @@ -385,7 +364,7 @@ impl AptosVM { StorageAdapter::new_with_config( executor_view, self.gas_feature_version, - &self.features, + self.features(), Some(executor_view), ) } @@ -395,12 +374,9 @@ impl AptosVM { gas_meter: &impl AptosGasMeter, storage_fee_refund: u64, ) -> FeeStatement { - let gas_used = txn_data - .max_gas_amount() - .checked_sub(gas_meter.balance()) - .expect("Balance should always be less than or equal to max gas amount"); + let gas_used = Self::gas_used(txn_data.max_gas_amount(), gas_meter); FeeStatement::new( - gas_used.into(), + gas_used, u64::from(gas_meter.execution_gas_used()), u64::from(gas_meter.io_gas_used()), u64::from(gas_meter.storage_fee_used()), @@ -408,9 +384,9 @@ impl AptosVM { ) } - fn failed_transaction_cleanup_and_keep_vm_status( + pub(crate) fn failed_transaction_cleanup( &self, - error_code: VMStatus, + error_vm_status: VMStatus, gas_meter: &mut impl AptosGasMeter, txn_data: &TransactionMetadata, resolver: &impl AptosMoveResolver, @@ -439,11 +415,13 @@ impl AptosVM { } } - match TransactionStatus::from_vm_status( - error_code.clone(), - self.features + let txn_status = TransactionStatus::from_vm_status( + error_vm_status.clone(), + self.features() .is_enabled(FeatureFlag::CHARGE_INVARIANT_VIOLATION), - ) { + ); + + match txn_status { TransactionStatus::Keep(status) => { // The transaction should be kept. Run the appropriate post transaction workflows // including epilogue. This runs a new session that ignores any side effects that @@ -464,12 +442,12 @@ impl AptosVM { }, Err(err) => discarded_output(err.status_code()), }; - (error_code, txn_output) + (error_vm_status, txn_output) + }, + TransactionStatus::Discard(status_code) => { + let discarded_output = discarded_output(status_code); + (error_vm_status, discarded_output) }, - TransactionStatus::Discard(status_code) => ( - VMStatus::error(status_code, None), - discarded_output(status_code), - ), TransactionStatus::Retry => unreachable!(), } } @@ -507,7 +485,7 @@ impl AptosVM { const ZERO_STORAGE_REFUND: u64 = 0; let is_account_init_for_sponsored_transaction = - is_account_init_for_sponsored_transaction(txn_data, &self.features, resolver)?; + is_account_init_for_sponsored_transaction(txn_data, self.features(), resolver)?; if is_account_init_for_sponsored_transaction { let mut session = self.new_session(resolver, SessionId::run_on_abort(txn_data)); @@ -537,7 +515,7 @@ impl AptosVM { { info!( *log_context, - "Failed during charge_change_set: {:?}. Most likely exceded gas limited.", err, + "Failed during charge_change_set: {:?}. Most likely exceeded gas limited.", err, ); }; @@ -585,7 +563,7 @@ impl AptosVM { session, gas_meter.balance(), fee_statement, - &self.features, + self.features(), txn_data, log_context, ) @@ -603,7 +581,7 @@ impl AptosVM { &mut session, gas_meter.balance(), fee_statement, - &self.features, + self.features(), txn_data, log_context, )?; @@ -647,7 +625,7 @@ impl AptosVM { session, gas_meter.balance(), fee_statement, - &self.features, + self.features(), txn_data, log_context, ) @@ -662,30 +640,53 @@ impl AptosVM { Ok((VMStatus::Executed, output)) } + fn validate_and_execute_script( + &self, + session: &mut SessionExt, + // Note: cannot use AptosGasMeter because it is not implemented for + // UnmeteredGasMeter. + gas_meter: &mut impl GasMeter, + senders: Vec, + script: &Script, + ) -> Result { + let loaded_func = session.load_script(script.code(), script.ty_args().to_vec())?; + // TODO(Gerardo): consolidate the extended validation to verifier. + verifier::event_validation::verify_no_event_emission_in_script( + script.code(), + &session.get_vm_config().deserializer_config, + )?; + + let args = verifier::transaction_arg_validation::validate_combine_signer_and_txn_args( + session, + senders, + convert_txn_args(script.args()), + &loaded_func, + self.features().is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS), + )?; + + Ok(session.execute_script(script.code(), script.ty_args().to_vec(), args, gas_meter)?) + } + fn validate_and_execute_entry_function( &self, session: &mut SessionExt, gas_meter: &mut impl AptosGasMeter, senders: Vec, - script_fn: &EntryFunction, + entry_fn: &EntryFunction, ) -> Result { - let function = session.load_function( - script_fn.module(), - script_fn.function(), - script_fn.ty_args(), - )?; - let struct_constructors = self.features.is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS); + let function = + session.load_function(entry_fn.module(), entry_fn.function(), entry_fn.ty_args())?; let args = verifier::transaction_arg_validation::validate_combine_signer_and_txn_args( session, senders, - script_fn.args().to_vec(), + entry_fn.args().to_vec(), &function, - struct_constructors, + self.features().is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS), )?; Ok(session.execute_entry_function( - script_fn.module(), - script_fn.function(), - script_fn.ty_args().to_vec(), + entry_fn.module(), + entry_fn.function(), + entry_fn.ty_args().to_vec(), args, gas_meter, )?) @@ -702,7 +703,7 @@ impl AptosVM { new_published_modules_loaded: &mut bool, change_set_configs: &ChangeSetConfigs, ) -> Result<(VMStatus, VMOutput), VMStatus> { - fail_point!("move_adapter::execute_script_or_entry_function", |_| { + fail_point!("aptos_vm::execute_script_or_entry_function", |_| { Err(VMStatus::Error { status_code: StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, sub_status: Some(move_core_types::vm_status::sub_status::unknown_invariant_violation::EPARANOID_FAILURE), @@ -710,73 +711,48 @@ impl AptosVM { }) }); - // Run the execution logic - { - gas_meter.charge_intrinsic_gas_for_transaction(txn_data.transaction_size())?; - - match payload { - TransactionPayload::Script(script) => { - let loaded_func = - session.load_script(script.code(), script.ty_args().to_vec())?; - // Gerardo: consolidate the extended validation to verifier. - verifier::event_validation::verify_no_event_emission_in_script( - script.code(), - &session.get_vm_config().deserializer_config, - )?; + gas_meter.charge_intrinsic_gas_for_transaction(txn_data.transaction_size())?; - let args = - verifier::transaction_arg_validation::validate_combine_signer_and_txn_args( - &mut session, - txn_data.senders(), - convert_txn_args(script.args()), - &loaded_func, - self.features.is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS), - )?; - session.execute_script( - script.code(), - script.ty_args().to_vec(), - args, - gas_meter, - )?; - }, - TransactionPayload::EntryFunction(script_fn) => { - self.validate_and_execute_entry_function( - &mut session, - gas_meter, - txn_data.senders(), - script_fn, - )?; - }, + match payload { + TransactionPayload::Script(script) => { + self.validate_and_execute_script( + &mut session, + gas_meter, + txn_data.senders(), + script, + )?; + }, + TransactionPayload::EntryFunction(entry_fn) => { + self.validate_and_execute_entry_function( + &mut session, + gas_meter, + txn_data.senders(), + entry_fn, + )?; + }, - // Not reachable as this function should only be invoked for entry or script - // transaction payload. - _ => { - return Err(VMStatus::error(StatusCode::UNREACHABLE, None)); - }, - }; + // Not reachable as this function should only be invoked for entry or script + // transaction payload. + _ => unreachable!("Only scripts or entry functions are executed"), + }; - self.resolve_pending_code_publish( - &mut session, - gas_meter, - new_published_modules_loaded, - )?; + self.resolve_pending_code_publish(&mut session, gas_meter, new_published_modules_loaded)?; - let respawned_session = self.charge_change_set_and_respawn_session( - session, - resolver, - gas_meter, - change_set_configs, - txn_data, - )?; + let respawned_session = self.charge_change_set_and_respawn_session( + session, + resolver, + gas_meter, + change_set_configs, + txn_data, + )?; - self.success_transaction_cleanup( - respawned_session, - gas_meter, - txn_data, - log_context, - change_set_configs, - ) - } + self.success_transaction_cleanup( + respawned_session, + gas_meter, + txn_data, + log_context, + change_set_configs, + ) } fn charge_change_set( @@ -796,7 +772,7 @@ impl AptosVM { txn_data.gas_unit_price, resolver.as_executor_view(), )?; - if !self.features.is_storage_deletion_refund_enabled() { + if !self.features().is_storage_deletion_refund_enabled() { storage_refund = 0.into(); } @@ -1148,15 +1124,8 @@ impl AptosVM { /// Deserialize a module bundle. fn deserialize_module_bundle(&self, modules: &ModuleBundle) -> VMResult> { - let max_version = get_max_binary_format_version(&self.features, None); - let max_identifier_size = if self - .features - .is_enabled(FeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH) - { - IDENTIFIER_SIZE_MAX - } else { - LEGACY_IDENTIFIER_SIZE_MAX - }; + let max_version = get_max_binary_format_version(self.features(), None); + let max_identifier_size = get_max_identifier_size(self.features()); let config = DeserializerConfig::new(max_version, max_identifier_size); let mut result = vec![]; for module_blob in modules.iter() { @@ -1217,7 +1186,7 @@ impl AptosVM { true, true, !self - .features + .features() .is_enabled(FeatureFlag::TREAT_FRIEND_AS_PRIVATE), ), )); @@ -1266,13 +1235,14 @@ impl AptosVM { } } } - aptos_framework::verify_module_metadata(m, &self.features, &self.timed_features) + aptos_framework::verify_module_metadata(m, self.features(), &self.timed_features) .map_err(|err| Self::metadata_validation_error(&err.to_string()))?; } verifier::resource_groups::validate_resource_groups( session, modules, - self.features.is_enabled(FeatureFlag::SAFER_RESOURCE_GROUPS), + self.features() + .is_enabled(FeatureFlag::SAFER_RESOURCE_GROUPS), )?; verifier::event_validation::validate_module_events(session, modules)?; @@ -1290,7 +1260,7 @@ impl AptosVM { .finish(Location::Undefined) } - fn make_standard_gas_meter( + pub(crate) fn make_standard_gas_meter( &self, balance: Gas, log_context: &AdapterLogSchema, @@ -1323,29 +1293,9 @@ impl AptosVM { )); } - // zkID feature gating - let authenticators = aptos_types::zkid::get_zkid_authenticators(transaction); - match &authenticators { - Ok(authenticators) => { - for (_, sig) in authenticators { - if !self.features.is_zkid_enabled() - && matches!(sig.sig, ZkpOrOpenIdSig::Groth16Zkp { .. }) - { - return Err(VMStatus::error(StatusCode::FEATURE_UNDER_GATING, None)); - } - if (!self.features.is_zkid_enabled() || !self.features.is_zkid_zkless_enabled()) - && matches!(sig.sig, ZkpOrOpenIdSig::OpenIdSig { .. }) - { - return Err(VMStatus::error(StatusCode::FEATURE_UNDER_GATING, None)); - } - } - }, - Err(_) => { - return Err(VMStatus::error(StatusCode::INVALID_SIGNATURE, None)); - }, - } - - zkid_validation::validate_zkid_authenticators(&authenticators.unwrap(), resolver)?; + let authenticators = aptos_types::zkid::get_zkid_authenticators(transaction) + .map_err(|_| VMStatus::error(StatusCode::INVALID_SIGNATURE, None))?; + zkid_validation::validate_zkid_authenticators(&authenticators, self.features(), resolver)?; // The prologue MUST be run AFTER any validation. Otherwise you may run prologue and hit // SEQUENCE_NUMBER_TOO_NEW if there is more than one transaction from the same sender and @@ -1380,24 +1330,14 @@ impl AptosVM { self.move_vm.mark_loader_cache_as_invalid(); }; - let txn_status = TransactionStatus::from_vm_status( - err.clone(), - self.features - .is_enabled(FeatureFlag::CHARGE_INVARIANT_VIOLATION), - ); - if txn_status.is_discarded() { - let discarded_output = discarded_output(err.status_code()); - (err, discarded_output) - } else { - self.failed_transaction_cleanup_and_keep_vm_status( - err, - gas_meter, - txn_data, - resolver, - log_context, - &storage_gas_params.change_set_configs, - ) - } + self.failed_transaction_cleanup( + err, + gas_meter, + txn_data, + resolver, + log_context, + &storage_gas_params.change_set_configs, + ) } fn execute_user_transaction_impl( @@ -1430,7 +1370,7 @@ impl AptosVM { } let is_account_init_for_sponsored_transaction = - match is_account_init_for_sponsored_transaction(&txn_data, &self.features, resolver) { + match is_account_init_for_sponsored_transaction(&txn_data, self.features(), resolver) { Ok(result) => result, Err(err) => { let vm_status = err.into_vm_status(); @@ -1572,7 +1512,6 @@ impl AptosVM { txn_sender: Option, session_id: SessionId, ) -> Result { - let mut gas_meter = UnmeteredGasMeter; let change_set_configs = ChangeSetConfigs::unlimited_at_gas_feature_version(self.gas_feature_version); @@ -1607,23 +1546,12 @@ impl AptosVM { Some(sender) => vec![sender, *execute_as], }; - let loaded_func = - tmp_session.load_script(script.code(), script.ty_args().to_vec())?; - let args = - verifier::transaction_arg_validation::validate_combine_signer_and_txn_args( - &mut tmp_session, - senders, - convert_txn_args(script.args()), - &loaded_func, - self.features.is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS), - )?; - - return_on_failure!(tmp_session.execute_script( - script.code(), - script.ty_args().to_vec(), - args, - &mut gas_meter, - )); + self.validate_and_execute_script( + &mut tmp_session, + &mut UnmeteredGasMeter, + senders, + script, + )?; Ok(tmp_session.finish(&change_set_configs)?) }, } @@ -1713,7 +1641,7 @@ impl AptosVM { Ok((VMStatus::Executed, output)) } - pub(crate) fn process_block_prologue( + fn process_block_prologue( &self, resolver: &impl AptosMoveResolver, block_metadata: BlockMetadata, @@ -1755,7 +1683,7 @@ impl AptosVM { Ok((VMStatus::Executed, output)) } - pub(crate) fn process_block_prologue_ext( + fn process_block_prologue_ext( &self, resolver: &impl AptosMoveResolver, block_metadata_ext: BlockMetadataExt, @@ -1797,7 +1725,7 @@ impl AptosVM { } fn extract_module_metadata(&self, module: &ModuleId) -> Option> { - if self.features.is_enabled(FeatureFlag::VM_BINARY_FORMAT_V6) { + if self.features().is_enabled(FeatureFlag::VM_BINARY_FORMAT_V6) { aptos_framework::get_vm_metadata(&self.move_vm, module) } else { aptos_framework::get_vm_metadata_v0(&self.move_vm, module) @@ -1810,7 +1738,7 @@ impl AptosVM { func_name: Identifier, type_args: Vec, arguments: Vec>, - gas_budget: u64, + max_gas_amount: u64, ) -> ViewFunctionOutput { let resolver = state_view.as_move_resolver(); let vm = AptosVM::new( @@ -1818,13 +1746,13 @@ impl AptosVM { /*override_is_delayed_field_optimization_capable=*/ Some(false), ); let log_context = AdapterLogSchema::new(state_view.id(), 0); - let mut gas_meter = match Self::memory_tracked_gas_meter(&vm, &log_context, gas_budget) { + let mut gas_meter = match vm.make_standard_gas_meter(max_gas_amount.into(), &log_context) { Ok(gas_meter) => gas_meter, - Err(e) => return ViewFunctionOutput::new(Err(e), 0), + Err(e) => return ViewFunctionOutput::new(Err(anyhow::Error::msg(format!("{}", e))), 0), }; let mut session = vm.new_session(&resolver, SessionId::Void); - match Self::execute_view_function_in_vm( + let execution_result = Self::execute_view_function_in_vm( &mut session, &vm, module_id, @@ -1832,35 +1760,16 @@ impl AptosVM { type_args, arguments, &mut gas_meter, - ) { - Ok(result) => { - ViewFunctionOutput::new(Ok(result), Self::gas_used(gas_budget, &gas_meter)) - }, - Err(e) => ViewFunctionOutput::new(Err(e), Self::gas_used(gas_budget, &gas_meter)), + ); + let gas_used = Self::gas_used(max_gas_amount.into(), &gas_meter); + match execution_result { + Ok(result) => ViewFunctionOutput::new(Ok(result), gas_used), + Err(e) => ViewFunctionOutput::new(Err(e), gas_used), } } - fn memory_tracked_gas_meter( - vm: &AptosVM, - log_context: &AdapterLogSchema, - gas_budget: u64, - ) -> Result>> { - let gas_meter = MemoryTrackedGasMeter::new(StandardGasMeter::new(StandardGasAlgebra::new( - vm.gas_feature_version, - get_or_vm_startup_failure(&vm.gas_params, log_context)? - .vm - .clone(), - get_or_vm_startup_failure(&vm.storage_gas_params, log_context)?.clone(), - gas_budget, - ))); - Ok(gas_meter) - } - - fn gas_used( - gas_budget: u64, - gas_meter: &MemoryTrackedGasMeter>, - ) -> u64 { - GasQuantity::new(gas_budget) + fn gas_used(max_gas_amount: Gas, gas_meter: &impl AptosGasMeter) -> u64 { + max_gas_amount .checked_sub(gas_meter.balance()) .expect("Balance should always be less than or equal to max gas amount") .into() @@ -1873,8 +1782,8 @@ impl AptosVM { func_name: Identifier, type_args: Vec, arguments: Vec>, - gas_meter: &mut MemoryTrackedGasMeter>, - ) -> Result>> { + gas_meter: &mut impl AptosGasMeter, + ) -> anyhow::Result>> { let func_inst = session.load_function(&module_id, &func_name, &type_args)?; let metadata = vm.extract_module_metadata(&module_id); let arguments = verifier::view_function::validate_view_function( @@ -1883,7 +1792,7 @@ impl AptosVM { func_name.as_ident_str(), &func_inst, metadata.as_ref().map(Arc::as_ref), - vm.features.is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS), + vm.features().is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS), )?; Ok(session @@ -1914,7 +1823,7 @@ impl AptosVM { self.gas_feature_version, resolver, txn_data, - &self.features, + self.features(), log_context, )?; @@ -1963,13 +1872,13 @@ impl AptosVM { txn: &SignatureVerifiedTransaction, resolver: &impl AptosMoveResolver, log_context: &AdapterLogSchema, - ) -> Result<(VMStatus, VMOutput, Option), VMStatus> { + ) -> Result<(VMStatus, VMOutput), VMStatus> { assert!(!self.is_simulation, "VM has to be created for execution"); if let SignatureVerifiedTransaction::Invalid(_) = txn { let vm_status = VMStatus::error(StatusCode::INVALID_SIGNATURE, None); let discarded_output = discarded_output(vm_status.status_code()); - return Ok((vm_status, discarded_output, None)); + return Ok((vm_status, discarded_output)); } Ok(match txn.expect_valid() { @@ -1977,7 +1886,7 @@ impl AptosVM { fail_point!("aptos_vm::execution::block_metadata"); let (vm_status, output) = self.process_block_prologue(resolver, block_metadata.clone(), log_context)?; - (vm_status, output, Some("block_prologue".to_string())) + (vm_status, output) }, Transaction::BlockMetadataExt(block_metadata_ext) => { fail_point!("aptos_vm::execution::block_metadata_ext"); @@ -1986,7 +1895,7 @@ impl AptosVM { block_metadata_ext.clone(), log_context, )?; - (vm_status, output, Some("block_prologue_ext".to_string())) + (vm_status, output) }, Transaction::GenesisTransaction(write_set_payload) => { let (vm_status, output) = self.process_waypoint_change_set( @@ -1994,11 +1903,10 @@ impl AptosVM { write_set_payload.clone(), log_context, )?; - (vm_status, output, Some("waypoint_write_set".to_string())) + (vm_status, output) }, Transaction::UserTransaction(txn) => { fail_point!("aptos_vm::execution::user_transaction"); - let sender = txn.sender().to_hex(); let _timer = TXN_TOTAL_SECONDS.start_timer(); let (vm_status, output) = self.execute_user_transaction(resolver, txn, log_context); @@ -2069,17 +1977,17 @@ impl AptosVM { if let Some(label) = counter_label { USER_TRANSACTIONS_EXECUTED.with_label_values(&[label]).inc(); } - (vm_status, output, Some(sender)) + (vm_status, output) }, Transaction::StateCheckpoint(_) => { let status = TransactionStatus::Keep(ExecutionStatus::Success); let output = VMOutput::empty_with_status(status); - (VMStatus::Executed, output, Some("state_checkpoint".into())) + (VMStatus::Executed, output) }, Transaction::ValidatorTransaction(txn) => { let (vm_status, output) = self.process_validator_transaction(resolver, txn.clone(), log_context)?; - (vm_status, output, Some("validator_transaction".to_string())) + (vm_status, output) }, }) } @@ -2185,7 +2093,7 @@ impl VMValidator for AptosVM { let log_context = AdapterLogSchema::new(state_view.id(), 0); if !self - .features + .features() .is_enabled(FeatureFlag::SINGLE_SENDER_AUTHENTICATOR) { if let aptos_types::transaction::authenticator::TransactionAuthenticator::SingleSender{ .. } = transaction.authenticator_ref() { @@ -2193,7 +2101,7 @@ impl VMValidator for AptosVM { } } - if !self.features.is_enabled(FeatureFlag::WEBAUTHN_SIGNATURE) { + if !self.features().is_enabled(FeatureFlag::WEBAUTHN_SIGNATURE) { if let Ok(sk_authenticators) = transaction .authenticator_ref() .to_single_key_authenticators() @@ -2330,8 +2238,6 @@ fn vm_thread_safe() { fn assert_send() {} fn assert_sync() {} - use crate::AptosVM; - assert_send::(); assert_sync::(); assert_send::(); diff --git a/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs b/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs index 10c0cb6197a53..3561599e0cd18 100644 --- a/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs +++ b/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs @@ -64,23 +64,12 @@ impl<'a, S: 'a + StateView + Sync> ExecutorTask for AptosExecutorTask<'a, S> { .vm .execute_single_transaction(txn, &resolver, &log_context) { - Ok((vm_status, vm_output, sender)) => { + Ok((vm_status, vm_output)) => { if vm_output.status().is_discarded() { - match sender { - Some(s) => speculative_trace!( - &log_context, - format!( - "Transaction discarded, sender: {}, error: {:?}", - s, vm_status - ), - ), - None => { - speculative_trace!( - &log_context, - format!("Transaction malformed, error: {:?}", vm_status), - ) - }, - }; + speculative_trace!( + &log_context, + format!("Transaction discarded, status: {:?}", vm_status), + ); } if vm_status.status_code() == StatusCode::SPECULATIVE_EXECUTION_ABORT_ERROR { ExecutionStatus::SpeculativeExecutionAbortError( diff --git a/aptos-move/aptos-vm/src/move_vm_ext/session.rs b/aptos-move/aptos-vm/src/move_vm_ext/session.rs index 6ce6e0911cb1c..f98be6aabcfe2 100644 --- a/aptos-move/aptos-vm/src/move_vm_ext/session.rs +++ b/aptos-move/aptos-vm/src/move_vm_ext/session.rs @@ -16,7 +16,7 @@ use aptos_framework::natives::{ use aptos_table_natives::{NativeTableContext, TableChangeSet}; use aptos_types::{ access_path::AccessPath, block_metadata::BlockMetadata, block_metadata_ext::BlockMetadataExt, - contract_event::ContractEvent, on_chain_config::Features, state_store::state_key::StateKey, + contract_event::ContractEvent, state_store::state_key::StateKey, validator_txn::ValidatorTransaction, }; use aptos_vm_types::{change_set::VMChangeSet, storage::change_set_configs::ChangeSetConfigs}; @@ -151,24 +151,54 @@ impl SessionId { pub fn as_uuid(&self) -> HashValue { self.hash() } + + pub(crate) fn into_script_hash(self) -> Vec { + match self { + Self::Txn { + sender: _, + sequence_number: _, + script_hash, + } + | Self::Prologue { + sender: _, + sequence_number: _, + script_hash, + } + | Self::Epilogue { + sender: _, + sequence_number: _, + script_hash, + } + | Self::RunOnAbort { + sender: _, + sequence_number: _, + script_hash, + } + | Self::ValidatorTxn { script_hash } => script_hash, + Self::BlockMeta { id: _ } + | Self::Genesis { id: _ } + | Self::Void + | Self::BlockMetaExt { id: _ } => vec![], + } + } } pub struct SessionExt<'r, 'l> { inner: Session<'r, 'l>, remote: &'r dyn AptosMoveResolver, - features: Arc, + is_storage_slot_metadata_enabled: bool, } impl<'r, 'l> SessionExt<'r, 'l> { pub fn new( inner: Session<'r, 'l>, remote: &'r dyn AptosMoveResolver, - features: Arc, + is_storage_slot_metadata_enabled: bool, ) -> Self { Self { inner, remote, - features, + is_storage_slot_metadata_enabled, } } @@ -219,10 +249,7 @@ impl<'r, 'l> SessionExt<'r, 'l> { let event_context: NativeEventContext = extensions.remove(); let events = event_context.into_events(); - let woc = WriteOpConverter::new( - self.remote, - self.features.is_storage_slot_metadata_enabled(), - ); + let woc = WriteOpConverter::new(self.remote, self.is_storage_slot_metadata_enabled); let change_set = Self::convert_change_set( &woc, diff --git a/aptos-move/aptos-vm/src/move_vm_ext/vm.rs b/aptos-move/aptos-vm/src/move_vm_ext/vm.rs index ac185b314972a..20a7f7accdacd 100644 --- a/aptos-move/aptos-vm/src/move_vm_ext/vm.rs +++ b/aptos-move/aptos-vm/src/move_vm_ext/vm.rs @@ -14,10 +14,7 @@ use aptos_gas_algebra::DynamicExpression; use aptos_gas_schedule::{MiscGasParameters, NativeGasParameters}; use aptos_native_interface::SafeNativeBuilder; use aptos_table_natives::NativeTableContext; -use aptos_types::{ - chain_id::ChainId, - on_chain_config::{FeatureFlag, Features, TimedFeatureFlag, TimedFeatures}, -}; +use aptos_types::on_chain_config::{FeatureFlag, Features, TimedFeatureFlag, TimedFeatures}; use move_binary_format::{ deserializer::DeserializerConfig, errors::VMResult, @@ -28,12 +25,12 @@ use move_bytecode_verifier::VerifierConfig; use move_vm_runtime::{ config::VMConfig, move_vm::MoveVM, native_extensions::NativeContextExtensions, }; -use std::{ops::Deref, sync::Arc}; +use std::ops::Deref; pub struct MoveVmExt { inner: MoveVM, chain_id: u8, - features: Arc, + features: Features, } pub fn get_max_binary_format_version( @@ -134,7 +131,7 @@ impl MoveVmExt { resolver, )?, chain_id, - features: Arc::new(features), + features, }) } @@ -204,30 +201,9 @@ impl MoveVmExt { extensions.add(NativeRistrettoPointContext::new()); extensions.add(AlgebraContext::new()); extensions.add(NativeAggregatorContext::new(txn_hash, resolver, resolver)); - - let script_hash = match session_id { - SessionId::Txn { - sender: _, - sequence_number: _, - script_hash, - } - | SessionId::Prologue { - sender: _, - sequence_number: _, - script_hash, - } - | SessionId::Epilogue { - sender: _, - sequence_number: _, - script_hash, - } => script_hash, - SessionId::ValidatorTxn { script_hash } => script_hash, - _ => vec![], - }; - extensions.add(NativeTransactionContext::new( txn_hash.to_vec(), - script_hash, + session_id.into_script_hash(), self.chain_id, )); extensions.add(NativeCodeContext::default()); @@ -241,12 +217,12 @@ impl MoveVmExt { SessionExt::new( self.inner.new_session_with_extensions(resolver, extensions), resolver, - self.features.clone(), + self.features.is_storage_slot_metadata_enabled(), ) } - pub fn get_chain_id(&self) -> ChainId { - ChainId::new(self.chain_id) + pub(crate) fn features(&self) -> &Features { + &self.features } } diff --git a/aptos-move/aptos-vm/src/testing.rs b/aptos-move/aptos-vm/src/testing.rs index 37d3b71e2df99..baae2c3f98705 100644 --- a/aptos-move/aptos-vm/src/testing.rs +++ b/aptos-move/aptos-vm/src/testing.rs @@ -1,7 +1,15 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 +use crate::{ + aptos_vm::get_or_vm_startup_failure, data_cache::AsMoveResolver, + transaction_metadata::TransactionMetadata, AptosVM, +}; +use aptos_types::{state_store::StateView, transaction::SignedTransaction}; +use aptos_vm_logging::log_schema::AdapterLogSchema; +use aptos_vm_types::output::VMOutput; use move_binary_format::errors::VMResult; +use move_core_types::vm_status::VMStatus; #[derive(Debug, Eq, Hash, PartialEq)] pub enum InjectedError { @@ -47,3 +55,34 @@ pub mod testing_only { }) } } + +impl AptosVM { + #[cfg(any(test, feature = "testing"))] + pub fn test_failed_transaction_cleanup( + &self, + error_vm_status: VMStatus, + txn: &SignedTransaction, + state_view: &impl StateView, + gas_meter_balance: u64, + ) -> (VMStatus, VMOutput) { + let txn_data = TransactionMetadata::new(txn); + let log_context = AdapterLogSchema::new(state_view.id(), 0); + + let mut gas_meter = self + .make_standard_gas_meter(gas_meter_balance.into(), &log_context) + .expect("Should be able to create a gas meter for tests"); + let change_set_configs = &get_or_vm_startup_failure(&self.storage_gas_params, &log_context) + .expect("Storage gas parameters should exist for tests") + .change_set_configs; + + let resolver = state_view.as_move_resolver(); + self.failed_transaction_cleanup( + error_vm_status, + &mut gas_meter, + &txn_data, + &resolver, + &log_context, + change_set_configs, + ) + } +} diff --git a/aptos-move/aptos-vm/src/transaction_metadata.rs b/aptos-move/aptos-vm/src/transaction_metadata.rs index bd39cc629c822..cdc83dba3f648 100644 --- a/aptos-move/aptos-vm/src/transaction_metadata.rs +++ b/aptos-move/aptos-vm/src/transaction_metadata.rs @@ -2,14 +2,13 @@ // Parts of the project are originally copyright © Meta Platforms, Inc. // SPDX-License-Identifier: Apache-2.0 -use aptos_crypto::{ed25519::Ed25519PrivateKey, HashValue, PrivateKey}; +use aptos_crypto::HashValue; use aptos_gas_algebra::{FeePerGasUnit, Gas, NumBytes}; use aptos_types::{ account_address::AccountAddress, chain_id::ChainId, - transaction::{authenticator::AuthenticationKey, SignedTransaction, TransactionPayload}, + transaction::{SignedTransaction, TransactionPayload}, }; -use std::convert::TryFrom; pub struct TransactionMetadata { pub sender: AccountAddress, @@ -118,30 +117,6 @@ impl TransactionMetadata { } pub fn is_multi_agent(&self) -> bool { - !(self.secondary_signers.is_empty() && self.fee_payer.is_none()) - } -} - -impl Default for TransactionMetadata { - fn default() -> Self { - let mut buf = [0u8; Ed25519PrivateKey::LENGTH]; - buf[Ed25519PrivateKey::LENGTH - 1] = 1; - let public_key = Ed25519PrivateKey::try_from(&buf[..]).unwrap().public_key(); - TransactionMetadata { - sender: AccountAddress::ZERO, - authentication_key: AuthenticationKey::ed25519(&public_key).to_vec(), - secondary_signers: vec![], - secondary_authentication_keys: vec![], - sequence_number: 0, - fee_payer: None, - fee_payer_authentication_key: None, - max_gas_amount: 100_000_000.into(), - gas_unit_price: 0.into(), - transaction_size: 0.into(), - expiration_timestamp_secs: 0, - chain_id: ChainId::test(), - script_hash: vec![], - script_size: NumBytes::zero(), - } + !self.secondary_signers.is_empty() || self.fee_payer.is_some() } } diff --git a/aptos-move/aptos-vm/src/zkid_validation.rs b/aptos-move/aptos-vm/src/zkid_validation.rs index a532fa0e4a424..34351ace2c341 100644 --- a/aptos-move/aptos-vm/src/zkid_validation.rs +++ b/aptos-move/aptos-vm/src/zkid_validation.rs @@ -7,7 +7,7 @@ use aptos_crypto::ed25519::Ed25519PublicKey; use aptos_types::{ invalid_signature, jwks::{jwk::JWK, PatchedJWKs}, - on_chain_config::{CurrentTimeMicroseconds, OnChainConfig}, + on_chain_config::{CurrentTimeMicroseconds, Features, OnChainConfig}, transaction::authenticator::EphemeralPublicKey, vm_status::{StatusCode, VMStatus}, zkid::{ @@ -100,16 +100,28 @@ fn get_jwk_for_zkid_authenticator( Ok(jwk) } -pub fn validate_zkid_authenticators( +pub(crate) fn validate_zkid_authenticators( authenticators: &Vec<(ZkIdPublicKey, ZkIdSignature)>, + features: &Features, resolver: &impl AptosMoveResolver, ) -> Result<(), VMStatus> { + // zkID feature gating. + for (_, sig) in authenticators { + if !features.is_zkid_enabled() && matches!(sig.sig, ZkpOrOpenIdSig::Groth16Zkp { .. }) { + return Err(VMStatus::error(StatusCode::FEATURE_UNDER_GATING, None)); + } + if (!features.is_zkid_enabled() || !features.is_zkid_zkless_enabled()) + && matches!(sig.sig, ZkpOrOpenIdSig::OpenIdSig { .. }) + { + return Err(VMStatus::error(StatusCode::FEATURE_UNDER_GATING, None)); + } + } + if authenticators.is_empty() { return Ok(()); } let config = &get_configs_onchain(resolver)?; - if authenticators.len() > config.max_zkid_signatures_per_txn as usize { return Err(invalid_signature!("Too many zkID authenticators")); } diff --git a/aptos-move/e2e-move-tests/src/tests/account.rs b/aptos-move/e2e-move-tests/src/tests/account.rs new file mode 100644 index 0000000000000..fddf1683fded8 --- /dev/null +++ b/aptos-move/e2e-move-tests/src/tests/account.rs @@ -0,0 +1,25 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::MoveHarness; +use aptos_cached_packages::aptos_stdlib::aptos_account_transfer; +use aptos_language_e2e_tests::account::Account; +use claims::assert_err_eq; +use move_core_types::vm_status::StatusCode; + +#[test] +fn non_existent_sender() { + let mut h = MoveHarness::new(); + + let sender = Account::new(); + let receiver = h.new_account_with_balance_and_sequence_number(100_000, 0); + + let txn = sender + .transaction() + .payload(aptos_account_transfer(*receiver.address(), 10)) + .sequence_number(0) + .sign(); + + let status = h.run(txn); + assert_err_eq!(status.status(), StatusCode::SENDING_ACCOUNT_DOES_NOT_EXIST); +} diff --git a/aptos-move/e2e-move-tests/src/tests/mod.rs b/aptos-move/e2e-move-tests/src/tests/mod.rs index c05f09cc54743..a8f9e1283792e 100644 --- a/aptos-move/e2e-move-tests/src/tests/mod.rs +++ b/aptos-move/e2e-move-tests/src/tests/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 mod access_path_test; +mod account; mod aggregator; mod aggregator_v2; mod aggregator_v2_events; @@ -46,4 +47,5 @@ mod token_objects; mod transaction_fee; mod type_too_large; mod vector_numeric_address; +mod vm; mod vote; diff --git a/aptos-move/e2e-move-tests/src/tests/vm.rs b/aptos-move/e2e-move-tests/src/tests/vm.rs new file mode 100644 index 0000000000000..c4a30284a8f32 --- /dev/null +++ b/aptos-move/e2e-move-tests/src/tests/vm.rs @@ -0,0 +1,58 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::MoveHarness; +use aptos_cached_packages::aptos_stdlib::aptos_account_transfer; +use aptos_types::{ + state_store::state_key::StateKey, transaction::ExecutionStatus, write_set::WriteOp, +}; +use aptos_vm::{data_cache::AsMoveResolver, AptosVM}; +use claims::{assert_ok_eq, assert_some}; +use move_core_types::vm_status::{StatusCode, VMStatus}; +use test_case::test_case; + +// Make sure verification and invariant violation errors are kept. +#[test_case(StatusCode::TYPE_MISMATCH)] +#[test_case(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)] +fn failed_transaction_cleanup_charges_gas(status_code: StatusCode) { + let mut h = MoveHarness::new(); + let sender = h.new_account_with_balance_and_sequence_number(1_000_000, 10); + let receiver = h.new_account_with_balance_and_sequence_number(1_000_000, 10); + + let max_gas_amount = 100_000; + let txn = sender + .transaction() + .sequence_number(10) + .max_gas_amount(max_gas_amount) + .payload(aptos_account_transfer(*receiver.address(), 1)) + .sign(); + + let state_view = h.executor.get_state_view(); + let vm = AptosVM::new( + &state_view.as_move_resolver(), + /*override_is_delayed_field_optimization_capable=*/ Some(false), + ); + + let balance = 10_000; + let output = vm + .test_failed_transaction_cleanup( + VMStatus::error(status_code, None), + &txn, + state_view, + balance, + ) + .1; + + let write_set: Vec<(&StateKey, &WriteOp)> = output + .change_set() + .concrete_write_set_iter() + .map(|(k, v)| (k, assert_some!(v))) + .collect(); + assert!(!write_set.is_empty()); + assert_eq!(output.gas_used(), max_gas_amount - balance); + assert!(!output.status().is_discarded()); + assert_ok_eq!( + output.status().as_kept_status(), + ExecutionStatus::MiscellaneousError(Some(status_code)) + ); +} diff --git a/aptos-move/e2e-testsuite/src/tests/failed_transaction_tests.rs b/aptos-move/e2e-testsuite/src/tests/failed_transaction_tests.rs deleted file mode 100644 index 11446d98d514b..0000000000000 --- a/aptos-move/e2e-testsuite/src/tests/failed_transaction_tests.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright © Aptos Foundation -// Parts of the project are originally copyright © Meta Platforms, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use aptos_gas_meter::{StandardGasAlgebra, StandardGasMeter}; -use aptos_gas_schedule::{AptosGasParameters, LATEST_GAS_FEATURE_VERSION}; -use aptos_language_e2e_tests::{common_transactions::peer_to_peer_txn, executor::FakeExecutor}; -use aptos_memory_usage_tracker::MemoryTrackedGasMeter; -use aptos_types::{ - state_store::{state_key::StateKey, TStateView}, - transaction::ExecutionStatus, - vm_status::{StatusCode, VMStatus}, - write_set::WriteOp, -}; -use aptos_vm::{data_cache::AsMoveResolver, transaction_metadata::TransactionMetadata, AptosVM}; -use aptos_vm_logging::log_schema::AdapterLogSchema; -use aptos_vm_types::storage::StorageGasParameters; -use claims::assert_some; -use move_core_types::vm_status::StatusCode::TYPE_MISMATCH; - -#[test] -fn failed_transaction_cleanup_test() { - let mut executor = FakeExecutor::from_head_genesis(); - // TODO(Gas): double check this - let sender = executor.create_raw_account_data(1_000_000, 10); - executor.add_account_data(&sender); - - let log_context = AdapterLogSchema::new(executor.get_state_view().id(), 0); - let aptos_vm = AptosVM::new( - &executor.get_state_view().as_move_resolver(), - /*override_is_delayed_field_optimization_capable=*/ None, - ); - let data_cache = executor.get_state_view().as_move_resolver(); - - let txn_data = TransactionMetadata { - sender: *sender.address(), - max_gas_amount: 100_000.into(), - gas_unit_price: 0.into(), - sequence_number: 10, - ..Default::default() - }; - - let gas_params = AptosGasParameters::zeros(); - let storage_gas_params = - StorageGasParameters::unlimited(gas_params.vm.txn.legacy_free_write_bytes_quota); - - let change_set_configs = storage_gas_params.change_set_configs.clone(); - - let mut gas_meter = MemoryTrackedGasMeter::new(StandardGasMeter::new(StandardGasAlgebra::new( - LATEST_GAS_FEATURE_VERSION, - gas_params.vm, - storage_gas_params, - 10_000, - ))); - - // TYPE_MISMATCH should be kept and charged. - let out1 = aptos_vm.failed_transaction_cleanup( - VMStatus::error(StatusCode::TYPE_MISMATCH, None), - &mut gas_meter, - &txn_data, - &data_cache, - &log_context, - &change_set_configs, - ); - - let write_set: Vec<(&StateKey, &WriteOp)> = out1 - .change_set() - .concrete_write_set_iter() - .map(|(k, v)| (k, assert_some!(v))) - .collect(); - assert!(!write_set.is_empty()); - assert_eq!(out1.gas_used(), 90_000); - assert!(!out1.status().is_discarded()); - assert_eq!( - out1.status().status(), - // StatusCode::TYPE_MISMATCH - Ok(ExecutionStatus::MiscellaneousError(Some(TYPE_MISMATCH))) - ); - - // Invariant violations should be charged. - let out2 = aptos_vm.failed_transaction_cleanup( - VMStatus::error(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, None), - &mut gas_meter, - &txn_data, - &data_cache, - &log_context, - &change_set_configs, - ); - assert!(out2.gas_used() != 0); - assert!(!out2.status().is_discarded()); - assert_eq!( - out2.status().status(), - Ok(ExecutionStatus::MiscellaneousError(Some( - StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR - ))) - ); -} - -#[test] -fn non_existent_sender() { - let mut executor = FakeExecutor::from_head_genesis(); - let sequence_number = 0; - let sender = executor.create_raw_account(); - let receiver = executor.create_raw_account_data(100_000, sequence_number); - executor.add_account_data(&receiver); - - let transfer_amount = 10; - let txn = peer_to_peer_txn( - &sender, - receiver.account(), - sequence_number, - transfer_amount, - 0, - ); - - let output = &executor.execute_transaction(txn); - assert_eq!( - output.status().status(), - Err(StatusCode::SENDING_ACCOUNT_DOES_NOT_EXIST), - ); -} diff --git a/aptos-move/e2e-testsuite/src/tests/invariant_violation.rs b/aptos-move/e2e-testsuite/src/tests/invariant_violation.rs index 2a7d94f972757..9cef3a8dcb6b5 100644 --- a/aptos-move/e2e-testsuite/src/tests/invariant_violation.rs +++ b/aptos-move/e2e-testsuite/src/tests/invariant_violation.rs @@ -13,17 +13,12 @@ use move_core_types::{value::MoveValue, vm_status::StatusCode}; #[test] fn invariant_violation_error() { let _scenario = fail::FailScenario::setup(); - fail::cfg( - "move_adapter::execute_script_or_entry_function", - "100%return", - ) - .unwrap(); + fail::cfg("aptos_vm::execute_script_or_entry_function", "100%return").unwrap(); ::aptos_logger::Logger::init_for_testing(); let mut executor = FakeExecutor::from_head_genesis(); - // create and publish a sender with 1_000_000 coins and a receiver with 100_000 coins let sender = executor.create_raw_account_data(1_000_000, 10); let receiver = executor.create_raw_account_data(100_000, 10); executor.add_account_data(&sender); diff --git a/aptos-move/e2e-testsuite/src/tests/mod.rs b/aptos-move/e2e-testsuite/src/tests/mod.rs index 03f411f1bd17c..e4515b78e7cf5 100644 --- a/aptos-move/e2e-testsuite/src/tests/mod.rs +++ b/aptos-move/e2e-testsuite/src/tests/mod.rs @@ -16,7 +16,6 @@ mod account_universe; mod create_account; mod data_store; mod execution_strategies; -mod failed_transaction_tests; mod genesis; mod genesis_initializations; mod invariant_violation; diff --git a/experimental/execution/ptx-executor/src/runner.rs b/experimental/execution/ptx-executor/src/runner.rs index ee3fc58d4b9b6..b214906c459c2 100644 --- a/experimental/execution/ptx-executor/src/runner.rs +++ b/experimental/execution/ptx-executor/src/runner.rs @@ -273,7 +273,7 @@ impl<'scope, 'view: 'scope, BaseView: StateView + Sync> Worker<'view, BaseView> }; let _post = PER_WORKER_TIMER.timer_with(&[&idx, "run_txn_post_vm"]); // TODO(ptx): error handling - let (_vm_status, vm_output, _msg) = vm_output.expect("VM execution failed."); + let (_vm_status, vm_output) = vm_output.expect("VM execution failed."); // inform output state values to the manager // TODO use try_into_storage_change_set() instead, and ChangeSet it returns, instead of VMOutput. From d4fdb8f08929903044673d03e79c9f118a6c714a Mon Sep 17 00:00:00 2001 From: Zekun Wang <41706692+fEst1ck@users.noreply.github.com> Date: Tue, 20 Feb 2024 04:35:48 -0500 Subject: [PATCH 03/39] [Compiler V2] Critical edge elimination (#11894) Implement a pass to eliminate critical edges by splitting them with empty blocks --- .../move/move-compiler-v2/src/experiments.rs | 3 + third_party/move/move-compiler-v2/src/lib.rs | 4 + .../move/move-compiler-v2/src/pipeline/mod.rs | 1 + .../split_critical_edges_processor.rs | 375 ++++++++++++++++++ .../bytecode/src/stackless_bytecode.rs | 19 +- 5 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 third_party/move/move-compiler-v2/src/pipeline/split_critical_edges_processor.rs diff --git a/third_party/move/move-compiler-v2/src/experiments.rs b/third_party/move/move-compiler-v2/src/experiments.rs index 37971120f4aea..de90d247e2165 100644 --- a/third_party/move/move-compiler-v2/src/experiments.rs +++ b/third_party/move/move-compiler-v2/src/experiments.rs @@ -26,4 +26,7 @@ impl Experiment { /// A flag which allows to turn off safety checks, like reference safety. /// Retention: permanent. pub const NO_SAFETY: &'static str = "no-safety"; + /// A flag which allows to turn on the critical edge splitting pass. + /// Retention: temporary. This should be removed after the pass can be tested. + pub const SPLIT_CRITICAL_EDGES: &'static str = "split-critical-edges"; } diff --git a/third_party/move/move-compiler-v2/src/lib.rs b/third_party/move/move-compiler-v2/src/lib.rs index ad5bd791e6aec..075685aafa50c 100644 --- a/third_party/move/move-compiler-v2/src/lib.rs +++ b/third_party/move/move-compiler-v2/src/lib.rs @@ -18,6 +18,7 @@ use crate::pipeline::{ exit_state_analysis::ExitStateAnalysisProcessor, explicit_drop::ExplicitDrop, livevar_analysis_processor::LiveVarAnalysisProcessor, reference_safety_processor::ReferenceSafetyProcessor, + split_critical_edges_processor::SplitCriticalEdgesProcessor, uninitialized_use_checker::UninitializedUseChecker, unreachable_code_analysis::UnreachableCodeProcessor, unreachable_code_remover::UnreachableCodeRemover, @@ -207,6 +208,9 @@ pub fn bytecode_pipeline(env: &GlobalEnv) -> FunctionTargetPipeline { let options = env.get_extension::().expect("options"); let safety_on = !options.experiment_on(Experiment::NO_SAFETY); let mut pipeline = FunctionTargetPipeline::default(); + if options.experiment_on(Experiment::SPLIT_CRITICAL_EDGES) { + pipeline.add_processor(Box::new(SplitCriticalEdgesProcessor {})); + } if safety_on { pipeline.add_processor(Box::new(UninitializedUseChecker {})); } diff --git a/third_party/move/move-compiler-v2/src/pipeline/mod.rs b/third_party/move/move-compiler-v2/src/pipeline/mod.rs index 30b5c402338ba..1eb996661cf7a 100644 --- a/third_party/move/move-compiler-v2/src/pipeline/mod.rs +++ b/third_party/move/move-compiler-v2/src/pipeline/mod.rs @@ -20,6 +20,7 @@ pub mod exit_state_analysis; pub mod explicit_drop; pub mod livevar_analysis_processor; pub mod reference_safety_processor; +pub mod split_critical_edges_processor; pub mod uninitialized_use_checker; pub mod unreachable_code_analysis; pub mod unreachable_code_remover; diff --git a/third_party/move/move-compiler-v2/src/pipeline/split_critical_edges_processor.rs b/third_party/move/move-compiler-v2/src/pipeline/split_critical_edges_processor.rs new file mode 100644 index 0000000000000..ed7d11dbd5f9e --- /dev/null +++ b/third_party/move/move-compiler-v2/src/pipeline/split_critical_edges_processor.rs @@ -0,0 +1,375 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +//! This pass splits critical edges with empty blocks. +//! A critical edge is an edge where the source node has multiple successors, +//! and the target node has multiple predecessors. +//! +//! Side effects: clear existing annotations. +//! +//! Prerequisites: no call instructions have abort actions. +//! +//! Postconditions: no critical edges in the control flow graph. + +use log::{log_enabled, Level}; +use move_model::{ast::TempIndex, model::FunctionEnv}; +use move_stackless_bytecode::{ + function_target::FunctionData, + function_target_pipeline::{FunctionTargetProcessor, FunctionTargetsHolder}, + stackless_bytecode::{AttrId, Bytecode, Label}, + stackless_control_flow_graph::{BlockId, StacklessControlFlowGraph}, +}; +use std::collections::{BTreeMap, BTreeSet}; + +pub struct SplitCriticalEdgesProcessor {} + +impl FunctionTargetProcessor for SplitCriticalEdgesProcessor { + fn process( + &self, + _targets: &mut FunctionTargetsHolder, + fun_env: &FunctionEnv, + mut data: FunctionData, + _scc_opt: Option<&[FunctionEnv]>, + ) -> FunctionData { + if cfg!(debug_assertions) || log_enabled!(Level::Debug) { + Self::check_precondition(&data); + } + if fun_env.is_native() { + return data; + } + let mut transformer = SplitCriticalEdgesTransformation::new(std::mem::take(&mut data.code)); + transformer.transform(); + data.code = transformer.code; + data.annotations.clear(); + if cfg!(debug_assertions) || log_enabled!(Level::Debug) { + Self::check_postcondition(&data.code); + } + data + } + + fn name(&self) -> String { + "SplitCriticalEdgesProcessor".to_owned() + } +} + +impl SplitCriticalEdgesProcessor { + /// Checks the precondition of the transformaiton; cf. module documentation. + fn check_precondition(data: &FunctionData) { + for instr in &data.code { + if matches!(instr, Bytecode::Call(_, _, _, _, Some(_))) { + panic!("precondition violated: found call instruction with abort action") + } + } + } + + /// Checks the postcondition of the transformation; cf. module documentation. + fn check_postcondition(code: &[Bytecode]) { + let cfg = StacklessControlFlowGraph::new_forward(code); + let blocks = cfg.blocks(); + let mut pred_count: BTreeMap = + blocks.iter().map(|block_id| (*block_id, 0)).collect(); + for block in &blocks { + // don't count the edge from the dummy start to a block as an incoming edge + if *block == cfg.entry_block() { + continue; + } + for suc_block in cfg.successors(*block) { + *pred_count + .get_mut(suc_block) + .unwrap_or_else(|| panic!("block {}", suc_block)) += 1; + } + } + for block in blocks { + let successors = cfg.successors(block); + if successors.len() > 1 { + for suc_block in successors { + assert!( + *pred_count.get(suc_block).expect("pred count") <= 1, + "{} has > 1 predecessors", + suc_block + ) + } + } + } + } +} + +struct SplitCriticalEdgesTransformation { + /// Function data of the function being transformed + code: Vec, + /// Labels used in the original code and in the generated code + labels: BTreeSet