From 1f3cd02090070c0b3693521dc2b8335b6bc4ba36 Mon Sep 17 00:00:00 2001 From: thunkar Date: Thu, 1 Aug 2024 15:23:20 +0200 Subject: [PATCH] initial approach --- noir-projects/noir-contracts/Nargo.toml | 3 +- .../Nargo.toml | 3 +- .../src/main.nr | 6 +- .../ecdsa_public_key_note/Nargo.toml | 8 + .../src/lib.nr} | 0 .../ecdsa_r_account_contract/Nargo.toml | 10 ++ .../ecdsa_r_account_contract/src/main.nr | 73 +++++++++ yarn-project/accounts/package.json | 3 +- .../accounts/scripts/copy-contracts.sh | 2 +- yarn-project/accounts/src/ecdsa/artifact.ts | 5 - .../ecdsa/{ => ecdsa_k}/account_contract.ts | 12 +- .../accounts/src/ecdsa/ecdsa_k/artifact.ts | 5 + .../accounts/src/ecdsa/ecdsa_k/index.ts | 37 +++++ yarn-project/accounts/src/ecdsa/index.ts | 39 +---- .../src/ecdsa/ssh_ecdsa_r/account_contract.ts | 39 +++++ .../src/ecdsa/ssh_ecdsa_r/artifact.ts | 5 + .../accounts/src/ecdsa/ssh_ecdsa_r/index.ts | 37 +++++ yarn-project/accounts/src/utils/index.ts | 1 + yarn-project/accounts/src/utils/ssh_agent.ts | 154 ++++++++++++++++++ .../cli-wallet/src/cmds/create_account.ts | 19 ++- yarn-project/cli-wallet/src/cmds/deploy.ts | 7 +- yarn-project/cli-wallet/src/cmds/index.ts | 28 ++-- yarn-project/cli-wallet/src/cmds/send.ts | 7 +- .../cli-wallet/src/storage/wallet_db.ts | 32 +++- yarn-project/cli-wallet/src/utils/accounts.ts | 82 ++++++++-- yarn-project/cli-wallet/src/utils/ecdsa.ts | 15 ++ yarn-project/cli/src/utils/commands.ts | 18 +- .../src/e2e_account_contracts.test.ts | 4 +- 28 files changed, 533 insertions(+), 121 deletions(-) rename noir-projects/noir-contracts/contracts/{ecdsa_account_contract => ecdsa_k_account_contract}/Nargo.toml (65%) rename noir-projects/noir-contracts/contracts/{ecdsa_account_contract => ecdsa_k_account_contract}/src/main.nr (96%) create mode 100644 noir-projects/noir-contracts/contracts/ecdsa_public_key_note/Nargo.toml rename noir-projects/noir-contracts/contracts/{ecdsa_account_contract/src/ecdsa_public_key_note.nr => ecdsa_public_key_note/src/lib.nr} (100%) create mode 100644 noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/src/main.nr delete mode 100644 yarn-project/accounts/src/ecdsa/artifact.ts rename yarn-project/accounts/src/ecdsa/{ => ecdsa_k}/account_contract.ts (75%) create mode 100644 yarn-project/accounts/src/ecdsa/ecdsa_k/artifact.ts create mode 100644 yarn-project/accounts/src/ecdsa/ecdsa_k/index.ts create mode 100644 yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/account_contract.ts create mode 100644 yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/artifact.ts create mode 100644 yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/index.ts create mode 100644 yarn-project/accounts/src/utils/index.ts create mode 100644 yarn-project/accounts/src/utils/ssh_agent.ts create mode 100644 yarn-project/cli-wallet/src/utils/ecdsa.ts diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 13e3d4d0613..ad2c511db1c 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -20,7 +20,8 @@ members = [ "contracts/docs_example_contract", "contracts/easy_private_token_contract", "contracts/easy_private_voting_contract", - "contracts/ecdsa_account_contract", + "contracts/ecdsa_k_account_contract", + "contracts/ecdsa_r_account_contract", "contracts/escrow_contract", "contracts/gas_token_contract", "contracts/import_test_contract", diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/ecdsa_k_account_contract/Nargo.toml similarity index 65% rename from noir-projects/noir-contracts/contracts/ecdsa_account_contract/Nargo.toml rename to noir-projects/noir-contracts/contracts/ecdsa_k_account_contract/Nargo.toml index 3be9963cdae..2a7dd2645db 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/ecdsa_k_account_contract/Nargo.toml @@ -1,5 +1,5 @@ [package] -name = "ecdsa_account_contract" +name = "ecdsa_k_account_contract" authors = [""] compiler_version = ">=0.25.0" type = "contract" @@ -7,3 +7,4 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } authwit = { path = "../../../aztec-nr/authwit" } +ecdsa_public_key_note = { path = "../ecdsa_public_key_note" } diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/ecdsa_k_account_contract/src/main.nr similarity index 96% rename from noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr rename to noir-projects/noir-contracts/contracts/ecdsa_k_account_contract/src/main.nr index f4495e48217..cbb29eb3779 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/ecdsa_k_account_contract/src/main.nr @@ -1,8 +1,6 @@ -mod ecdsa_public_key_note; - // Account contract that uses ECDSA signatures for authentication on the same curve as Ethereum. // The signing key is stored in an immutable private note and should be different from the signing key. -contract EcdsaAccount { +contract EcdsaKAccount { use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PrivateContext, PrivateImmutable}; use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note; @@ -14,7 +12,7 @@ contract EcdsaAccount { auth_witness::get_auth_witness }; - use crate::ecdsa_public_key_note::EcdsaPublicKeyNote; + use dep::ecdsa_public_key_note::EcdsaPublicKeyNote; #[aztec(storage)] struct Storage { diff --git a/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/Nargo.toml b/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/Nargo.toml new file mode 100644 index 00000000000..6399145f965 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ecdsa_public_key_note" +authors = ["aztec-labs"] +compiler_version = ">=0.25.0" +type = "lib" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/ecdsa_public_key_note.nr b/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/src/lib.nr similarity index 100% rename from noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/ecdsa_public_key_note.nr rename to noir-projects/noir-contracts/contracts/ecdsa_public_key_note/src/lib.nr diff --git a/noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/Nargo.toml new file mode 100644 index 00000000000..2c51fa00b77 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ecdsa_r_account_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +authwit = { path = "../../../aztec-nr/authwit" } +ecdsa_public_key_note = { path = "../ecdsa_public_key_note" } diff --git a/noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/src/main.nr new file mode 100644 index 00000000000..b5f76de410a --- /dev/null +++ b/noir-projects/noir-contracts/contracts/ecdsa_r_account_contract/src/main.nr @@ -0,0 +1,73 @@ +// Account contract that uses ECDSA signatures for authentication on random version of the p256 curve (to use with touchID). +contract EcdsaRAccount { + use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PrivateContext, PrivateImmutable}; + use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note; + + use dep::aztec::protocol_types::abis::call_context::CallContext; + use dep::std; + + use dep::authwit::{ + entrypoint::{app::AppPayload, fee::FeePayload}, account::AccountActions, + auth_witness::get_auth_witness + }; + + use dep::ecdsa_public_key_note::EcdsaPublicKeyNote; + + #[aztec(storage)] + struct Storage { + public_key: PrivateImmutable, + } + + // Creates a new account out of an ECDSA public key to use for signature verification + #[aztec(private)] + #[aztec(initializer)] + fn constructor(signing_pub_key_x: [u8; 32], signing_pub_key_y: [u8; 32]) { + let this = context.this_address(); + let header = context.get_header(); + let this_npk_m_hash = header.get_npk_m_hash(&mut context, this); + // Not emitting outgoing for msg_sender here to not have to register keys for the contract through which we + // deploy this (typically MultiCallEntrypoint). I think it's ok here as I feel the outgoing here is not that + // important. + + let mut pub_key_note = EcdsaPublicKeyNote::new(signing_pub_key_x, signing_pub_key_y, this_npk_m_hash); + storage.public_key.initialize(&mut pub_key_note).emit(encode_and_encrypt_note(&mut context, this, this)); + } + + // Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts + #[aztec(private)] + fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) { + let actions = AccountActions::init(&mut context, is_valid_impl); + actions.entrypoint(app_payload, fee_payload); + } + + #[aztec(private)] + #[aztec(noinitcheck)] + #[aztec(view)] + fn verify_private_authwit(inner_hash: Field) -> Field { + let actions = AccountActions::init(&mut context, is_valid_impl); + actions.verify_private_authwit(inner_hash) + } + + #[contract_library_method] + fn is_valid_impl(context: &mut PrivateContext, outer_hash: Field) -> bool { + // Load public key from storage + let storage = Storage::init(context); + let public_key = storage.public_key.get_note(); + + // Load auth witness + let witness: [Field; 64] = get_auth_witness(outer_hash); + let mut signature: [u8; 64] = [0; 64]; + for i in 0..64 { + signature[i] = witness[i] as u8; + } + + // Verify payload signature using Ethereum's signing scheme + // Note that noir expects the hash of the message/challenge as input to the ECDSA verification. + let outer_hash_bytes: [u8; 32] = outer_hash.to_be_bytes(32).as_array(); + let hashed_message: [u8; 32] = std::hash::sha256(outer_hash_bytes); + let verification = std::ecdsa_secp256r1::verify_signature(public_key.x, public_key.y, signature, hashed_message); + assert(verification == true); + + true + } +} diff --git a/yarn-project/accounts/package.json b/yarn-project/accounts/package.json index b1209964ae4..d4d3d8ab916 100644 --- a/yarn-project/accounts/package.json +++ b/yarn-project/accounts/package.json @@ -9,7 +9,8 @@ "./ecdsa": "./dest/ecdsa/index.js", "./schnorr": "./dest/schnorr/index.js", "./single_key": "./dest/single_key/index.js", - "./testing": "./dest/testing/index.js" + "./testing": "./dest/testing/index.js", + "./utils": "./dest/utils/index.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/accounts/scripts/copy-contracts.sh b/yarn-project/accounts/scripts/copy-contracts.sh index 5984357006a..d7f19e13c10 100755 --- a/yarn-project/accounts/scripts/copy-contracts.sh +++ b/yarn-project/accounts/scripts/copy-contracts.sh @@ -2,7 +2,7 @@ set -euo pipefail mkdir -p ./artifacts -contracts=(schnorr_account_contract-SchnorrAccount ecdsa_account_contract-EcdsaAccount schnorr_single_key_account_contract-SchnorrSingleKeyAccount) +contracts=(schnorr_account_contract-SchnorrAccount ecdsa_k_account_contract-EcdsaKAccount ecdsa_r_account_contract-EcdsaRAccount schnorr_single_key_account_contract-SchnorrSingleKeyAccount) decl=$(cat < { diff --git a/yarn-project/accounts/src/ecdsa/ecdsa_k/artifact.ts b/yarn-project/accounts/src/ecdsa/ecdsa_k/artifact.ts new file mode 100644 index 00000000000..eac072ed08e --- /dev/null +++ b/yarn-project/accounts/src/ecdsa/ecdsa_k/artifact.ts @@ -0,0 +1,5 @@ +import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js'; + +import EcdsaKAccountContractJson from '../../../artifacts/EcdsaKAccount.json' assert { type: 'json' }; + +export const EcdsaKAccountContractArtifact = loadContractArtifact(EcdsaKAccountContractJson as NoirCompiledContract); diff --git a/yarn-project/accounts/src/ecdsa/ecdsa_k/index.ts b/yarn-project/accounts/src/ecdsa/ecdsa_k/index.ts new file mode 100644 index 00000000000..660cfe33913 --- /dev/null +++ b/yarn-project/accounts/src/ecdsa/ecdsa_k/index.ts @@ -0,0 +1,37 @@ +/** + * The `@aztec/accounts/ecdsa` export provides an ECDSA account contract implementation, that uses an ECDSA private key for authentication, and a Grumpkin key for encryption. + * Consider using this account type when working with integrations with Ethereum wallets. + * + * @packageDocumentation + */ +import { AccountManager, type Salt } from '@aztec/aztec.js/account'; +import { type AccountWallet, getWallet } from '@aztec/aztec.js/wallet'; +import { type PXE } from '@aztec/circuit-types'; +import { type AztecAddress, type Fr } from '@aztec/circuits.js'; + +import { EcdsaKAccountContract } from './account_contract.js'; + +export { EcdsaKAccountContractArtifact } from './artifact.js'; +export { EcdsaKAccountContract }; + +/** + * Creates an Account that relies on an ECDSA signing key for authentication. + * @param pxe - An PXE server instance. + * @param secretKey - Secret key used to derive all the keystore keys. + * @param signingPrivateKey - Secp256k1 key used for signing transactions. + * @param salt - Deployment salt. + */ +export function getEcdsaKAccount(pxe: PXE, secretKey: Fr, signingPrivateKey: Buffer, salt?: Salt): AccountManager { + return new AccountManager(pxe, secretKey, new EcdsaKAccountContract(signingPrivateKey), salt); +} + +/** + * Gets a wallet for an already registered account using ECDSA signatures. + * @param pxe - An PXE server instance. + * @param address - Address for the account. + * @param signingPrivateKey - ECDSA key used for signing transactions. + * @returns A wallet for this account that can be used to interact with a contract instance. + */ +export function getEcdsaKWallet(pxe: PXE, address: AztecAddress, signingPrivateKey: Buffer): Promise { + return getWallet(pxe, address, new EcdsaKAccountContract(signingPrivateKey)); +} diff --git a/yarn-project/accounts/src/ecdsa/index.ts b/yarn-project/accounts/src/ecdsa/index.ts index b27891f23dc..a380d7b954d 100644 --- a/yarn-project/accounts/src/ecdsa/index.ts +++ b/yarn-project/accounts/src/ecdsa/index.ts @@ -1,37 +1,2 @@ -/** - * The `@aztec/accounts/ecdsa` export provides an ECDSA account contract implementation, that uses an ECDSA private key for authentication, and a Grumpkin key for encryption. - * Consider using this account type when working with integrations with Ethereum wallets. - * - * @packageDocumentation - */ -import { AccountManager, type Salt } from '@aztec/aztec.js/account'; -import { type AccountWallet, getWallet } from '@aztec/aztec.js/wallet'; -import { type PXE } from '@aztec/circuit-types'; -import { type AztecAddress, type Fr } from '@aztec/circuits.js'; - -import { EcdsaAccountContract } from './account_contract.js'; - -export { EcdsaAccountContractArtifact } from './artifact.js'; -export { EcdsaAccountContract }; - -/** - * Creates an Account that relies on an ECDSA signing key for authentication. - * @param pxe - An PXE server instance. - * @param secretKey - Secret key used to derive all the keystore keys. - * @param signingPrivateKey - Secp256k1 key used for signing transactions. - * @param salt - Deployment salt. - */ -export function getEcdsaAccount(pxe: PXE, secretKey: Fr, signingPrivateKey: Buffer, salt?: Salt): AccountManager { - return new AccountManager(pxe, secretKey, new EcdsaAccountContract(signingPrivateKey), salt); -} - -/** - * Gets a wallet for an already registered account using ECDSA signatures. - * @param pxe - An PXE server instance. - * @param address - Address for the account. - * @param signingPrivateKey - ECDSA key used for signing transactions. - * @returns A wallet for this account that can be used to interact with a contract instance. - */ -export function getEcdsaWallet(pxe: PXE, address: AztecAddress, signingPrivateKey: Buffer): Promise { - return getWallet(pxe, address, new EcdsaAccountContract(signingPrivateKey)); -} +export * from './ecdsa_k/index.js'; +export * from './ssh_ecdsa_r/index.js'; diff --git a/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/account_contract.ts b/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/account_contract.ts new file mode 100644 index 00000000000..3554ba0c702 --- /dev/null +++ b/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/account_contract.ts @@ -0,0 +1,39 @@ +import { type AuthWitnessProvider } from '@aztec/aztec.js/account'; +import { AuthWitness, type CompleteAddress } from '@aztec/circuit-types'; +import { type ContractArtifact } from '@aztec/foundation/abi'; +import { type Fr } from '@aztec/foundation/fields'; + +import { DefaultAccountContract } from '../../defaults/account_contract.js'; +import { signWithAgent } from '../../utils/ssh_agent.js'; +import { EcdsaRAccountContractArtifact } from './artifact.js'; + +/** + * Account contract that authenticates transactions using ECDSA signatures + * verified against a secp256r1 public key stored in an immutable encrypted note. + */ +export class EcdsaRSSHAccountContract extends DefaultAccountContract { + constructor(private signingPublicKey: Buffer) { + super(EcdsaRAccountContractArtifact as ContractArtifact); + } + + getDeploymentArgs() { + return [this.signingPublicKey.subarray(0, 32), this.signingPublicKey.subarray(32, 64)]; + } + + getAuthWitnessProvider(_address: CompleteAddress): AuthWitnessProvider { + return new SSHEcdsaAuthWitnessProvider(this.signingPublicKey); + } +} + +/** Creates auth witnesses using ECDSA signatures. */ +class SSHEcdsaAuthWitnessProvider implements AuthWitnessProvider { + constructor(private signingPublicKey: Buffer) {} + + async createAuthWit(messageHash: Fr): Promise { + // Key type and curve name + const keyType = Buffer.from('ecdsa-sha2-nistp256'); + const curveName = Buffer.from('nistp256'); + const signature = await signWithAgent(keyType, curveName, this.signingPublicKey, messageHash.toBuffer()); + return Promise.resolve(new AuthWitness(messageHash, [...signature.r, ...signature.s])); + } +} diff --git a/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/artifact.ts b/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/artifact.ts new file mode 100644 index 00000000000..ea8382b6137 --- /dev/null +++ b/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/artifact.ts @@ -0,0 +1,5 @@ +import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js'; + +import EcdsaRAccountContractJson from '../../../artifacts/EcdsaRAccount.json' assert { type: 'json' }; + +export const EcdsaRAccountContractArtifact = loadContractArtifact(EcdsaRAccountContractJson as NoirCompiledContract); diff --git a/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/index.ts b/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/index.ts new file mode 100644 index 00000000000..fb368a3c7a9 --- /dev/null +++ b/yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/index.ts @@ -0,0 +1,37 @@ +/** + * The `@aztec/accounts/ecdsa` export provides an ECDSA account contract implementation, that uses an ECDSA private key for authentication, and a Grumpkin key for encryption. + * Consider using this account type when working with integrations with Ethereum wallets. + * + * @packageDocumentation + */ +import { AccountManager, type Salt } from '@aztec/aztec.js/account'; +import { type AccountWallet, getWallet } from '@aztec/aztec.js/wallet'; +import { type PXE } from '@aztec/circuit-types'; +import { type AztecAddress, type Fr } from '@aztec/circuits.js'; + +import { EcdsaRSSHAccountContract } from './account_contract.js'; + +export { EcdsaRAccountContractArtifact } from './artifact.js'; +export { EcdsaRSSHAccountContract }; + +/** + * Creates an Account that relies on an ECDSA signing key for authentication. + * @param pxe - An PXE server instance. + * @param secretKey - Secret key used to derive all the keystore keys. + * @param signingPublicKey - Secp2561 key used to identify its corresponding private key in the SSH Agent. + * @param salt - Deployment salt. + */ +export function getEcdsaRSSHAccount(pxe: PXE, secretKey: Fr, signingPublicKey: Buffer, salt?: Salt): AccountManager { + return new AccountManager(pxe, secretKey, new EcdsaRSSHAccountContract(signingPublicKey), salt); +} + +/** + * Gets a wallet for an already registered account using ECDSA signatures. + * @param pxe - An PXE server instance. + * @param address - Address for the account. + * @param signingPrivateKey - ECDSA key used for signing transactions. + * @returns A wallet for this account that can be used to interact with a contract instance. + */ +export function getEcdsaRSSHWallet(pxe: PXE, address: AztecAddress, signingPublicKey: Buffer): Promise { + return getWallet(pxe, address, new EcdsaRSSHAccountContract(signingPublicKey)); +} diff --git a/yarn-project/accounts/src/utils/index.ts b/yarn-project/accounts/src/utils/index.ts new file mode 100644 index 00000000000..ba7c543a064 --- /dev/null +++ b/yarn-project/accounts/src/utils/index.ts @@ -0,0 +1 @@ +export * from './ssh_agent.js'; diff --git a/yarn-project/accounts/src/utils/ssh_agent.ts b/yarn-project/accounts/src/utils/ssh_agent.ts new file mode 100644 index 00000000000..469f6e4835e --- /dev/null +++ b/yarn-project/accounts/src/utils/ssh_agent.ts @@ -0,0 +1,154 @@ +import { EcdsaSignature } from '@aztec/circuits.js/barretenberg'; + +import { Buffer } from 'buffer'; +import net from 'net'; + +const SSH_AGENT_IDENTITIES_REQUEST = 11; +const SSH_AGENT_IDENTITIES_RESPONSE = 12; +const SSH_AGENT_SIGN_REQUEST = 13; +const SSH_AGENT_SIGN_RESPONSE = 14; + +export function connectToAgent() { + const socketPath = process.env.SSH_AUTH_SOCK; + if (!socketPath) { + throw new Error('SSH_AUTH_SOCK is not set'); + } + return net.connect(socketPath); +} + +type StoredKey = { + type: string; + publicKey: string; + comment: string; +}; + +export async function getIdentities(): Promise { + return new Promise((resolve, reject) => { + const stream = connectToAgent(); + stream.on('connect', () => { + const request = Buffer.concat([ + Buffer.from([0, 0, 0, 5 + 4]), // length + Buffer.from([SSH_AGENT_IDENTITIES_REQUEST]), + Buffer.from([0, 0, 0, 0]), // flags + ]); + + // console.log(request.toString('hex')); + stream.write(request); + }); + + stream.on('data', data => { + const responseType = data[4]; + if (responseType === SSH_AGENT_IDENTITIES_RESPONSE) { + let offset = 5; + const numKeys = data.readUInt32BE(offset); + + var keys = []; + for (let i = 0; i < numKeys; i++) { + offset += 4; + const keyLength = data.readUInt32BE(offset); + offset += 4; + const key = data.slice(offset, offset + keyLength); + offset += keyLength; + const commentLength = data.readUInt32BE(offset); + offset += 4; + var comment = data.slice(offset, offset + commentLength); + offset += commentLength; + + let keyOffset = 0; + const typeLen = key.readUInt32BE(keyOffset); + keyOffset += 4; + const type = key.slice(keyOffset, keyOffset + typeLen); + + keys.push({ + type: type.toString('ascii'), + publicKey: key.toString('base64'), + comment: comment.toString('utf8'), + }); + } + stream.end(); + resolve(keys); + } else { + stream.end(); + reject(`Unexpected response type: ${responseType}`); + } + }); + }); +} + +export async function signWithAgent(keyType: Buffer, curveName: Buffer, publicKey: Buffer, data: Buffer) { + return new Promise((resolve, reject) => { + if (publicKey.length !== 64) { + reject('Public key must be 64 bytes'); + return; + } + const stream = connectToAgent(); + stream.on('connect', () => { + // Construct the key blob + const keyBlob = Buffer.concat([ + Buffer.from([0, 0, 0, keyType.length]), + keyType, + Buffer.from([0, 0, 0, curveName.length]), + curveName, + Buffer.from([0, 0, 0, publicKey.length + 1, 4]), + publicKey, + ]); + const request = Buffer.concat([ + Buffer.from([0, 0, 0, 5 + keyBlob.length + 4 + data.length + 4]), // length + Buffer.from([SSH_AGENT_SIGN_REQUEST]), + Buffer.from([0, 0, 0, keyBlob.length]), // key blob length + keyBlob, + Buffer.from([0, 0, 0, data.length]), // data length + data, + Buffer.from([0, 0, 0, 0]), // flags + ]); + + // console.log(request.toString('hex')); + stream.write(request); + }); + + stream.on('data', data => { + const type = data[4]; + + if (type === SSH_AGENT_SIGN_RESPONSE) { + const signatureLength = data.readUInt32BE(5); + const signature = data.slice(9, 9 + signatureLength); + + // Extract ECDSA signature components + let offset = 0; + const sigTypeLen = signature.readUInt32BE(offset); + offset += 4; + const sigType = signature.slice(offset, offset + sigTypeLen).toString(); + offset += sigTypeLen; + + if (sigType !== 'ecdsa-sha2-nistp256') { + stream.end(); + reject(`Unexpected signature type: ${sigType}`); + } + + offset += 4; + const rLen = signature.readUInt32BE(offset); + offset += 4; + let r = signature.slice(offset, offset + rLen); + offset += rLen; + + const sLen = signature.readUInt32BE(offset); + offset += 4; + let s = signature.slice(offset, offset + sLen); + + if (r.length > 32) { + r = r.slice(1); + } + + if (s.length > 32) { + s = s.slice(1); + } + + stream.end(); + resolve(new EcdsaSignature(r, s, Buffer.from([0]))); + } else { + stream.end(); + reject(`Unexpected response type: ${type}`); + } + }); + }); +} diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index c09282c6b8d..00599dc502a 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -1,4 +1,6 @@ +import { getEcdsaRSSHAccount } from '@aztec/accounts/ecdsa'; import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { getIdentities } from '@aztec/accounts/utils'; import { type DeployAccountOptions, createCompatibleClient } from '@aztec/aztec.js'; import { deriveSigningKey } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; @@ -6,10 +8,14 @@ import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { type IFeeOpts, printGasEstimates } from '../fees.js'; import { WalletDB } from '../storage/wallet_db.js'; +import { AccountType, createAndStoreAccount } from '../utils/accounts.js'; +import { extractECDSAPublicKeyFromBase64String } from '../utils/ecdsa.js'; export async function createAccount( rpcUrl: string, - privateKey: Fr | undefined, + accountType: AccountType, + secretKey: Fr | undefined, + publicKey: string | undefined, alias: string | undefined, registerOnly: boolean, publicDeploy: boolean, @@ -20,21 +26,18 @@ export async function createAccount( log: LogFn, ) { const client = await createCompatibleClient(rpcUrl, debugLogger); - const printPK = typeof privateKey === 'undefined'; - privateKey ??= Fr.random(); const salt = Fr.ZERO; + secretKey ??= Fr.random(); - const account = getSchnorrAccount(client, privateKey, deriveSigningKey(privateKey), salt); + const account = await createAndStoreAccount(client, accountType, secretKey, publicKey, salt, alias); const { address, publicKeys, partialAddress } = account.getCompleteAddress(); - await WalletDB.getInstance().storeAccount(address, { alias, privateKey, salt }); - log(`\nNew account:\n`); log(`Address: ${address.toString()}`); log(`Public key: 0x${publicKeys.toString()}`); - if (printPK) { - log(`Private key: ${privateKey.toString()}`); + if (secretKey) { + log(`Secret key: ${secretKey.toString()}`); } log(`Partial address: ${partialAddress.toString()}`); log(`Salt: ${salt.toString()}`); diff --git a/yarn-project/cli-wallet/src/cmds/deploy.ts b/yarn-project/cli-wallet/src/cmds/deploy.ts index b6932215de6..728b0f019cf 100644 --- a/yarn-project/cli-wallet/src/cmds/deploy.ts +++ b/yarn-project/cli-wallet/src/cmds/deploy.ts @@ -5,7 +5,7 @@ import { getInitializer } from '@aztec/foundation/abi'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { type IFeeOpts, printGasEstimates } from '../fees.js'; -import { AccountType, createOrRetrieveWallet } from '../utils/accounts.js'; +import { retrieveWallet } from '../utils/accounts.js'; export async function deploy( artifactPath: string, @@ -14,8 +14,7 @@ export async function deploy( publicKeys: PublicKeys | undefined, rawArgs: any[], salt: Fr | undefined, - privateKey: Fr | undefined, - aliasOrAddress: string | undefined, + aliasOrAddress: string, initializer: string | undefined, skipPublicDeployment: boolean, skipClassRegistration: boolean, @@ -40,7 +39,7 @@ export async function deploy( ); } - const wallet = await createOrRetrieveWallet(AccountType.SCHNORR, client, privateKey, aliasOrAddress); + const wallet = await retrieveWallet(client, aliasOrAddress); const deployer = new ContractDeployer(contractArtifact, wallet, publicKeys?.hash() ?? Fr.ZERO, initializer); diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index b84a3b029aa..f80349907b1 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -1,7 +1,7 @@ import { PublicKeys } from '@aztec/circuits.js'; import { addOptions, - createPrivateKeyOption, + createSecretKeyOption, logJson, parseAztecAddress, parseFieldFromHexString, @@ -24,8 +24,13 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL '--skip-initialization', 'Skip initializing the account contract. Useful for publicly deploying an existing account.', ) + .option('-t, --type ', 'Type of account to create. Default is schnorr.', 'schnorr') .option('--public-deploy', 'Publicly deploys the account and registers the class if needed.') - .addOption(createPrivateKeyOption('Private key for account. Uses random by default.', false)) + .addOption(createSecretKeyOption('Secret key for account. Uses random by default.', false).conflicts('public-key')) + .option( + '-p, --public-key ', + 'Public key that identifies a private signing key stored outside of the wallet. Used for ECDSA SSH accounts over the secp256r1 curve.', + ) .option('-a, --alias ', 'Alias for the account. Used for easy reference in the PXE.'); addOptions(createAccountCommand, FeeOpts.getOptions()) @@ -39,10 +44,13 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .action(async (_options, command) => { const { createAccount } = await import('../cmds/create_account.js'); const options = command.optsWithGlobals(); - const { privateKey, wait, registerOnly, skipInitialization, publicDeploy, rpcUrl, alias } = options; + const { secretKey, publicKey, wait, registerOnly, skipInitialization, publicDeploy, rpcUrl, alias, type } = + options; await createAccount( rpcUrl, - privateKey, + type, + secretKey, + publicKey, alias, registerOnly, skipInitialization, @@ -75,15 +83,10 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL parseFieldFromHexString, ) .option('--universal', 'Do not mix the sender address into the deployment.') - .option( + .requiredOption( '-a, --alias-or-address ', 'Alias or address of the account to deploy from. Incompatible with --private-key.', ) - .addOption( - createPrivateKeyOption("The sender's private key. Incompatible with --alias-or-address", false).conflicts( - 'alias', - ), - ) .option('--json', 'Emit output as json') // `options.wait` is default true. Passing `--no-wait` will set it to false. // https://github.com/tj/commander.js#other-option-types-negatable-boolean-and-booleanvalue @@ -115,7 +118,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL publicKey ? PublicKeys.fromString(publicKey) : undefined, rawArgs, salt, - privateKey, aliasOrAddress, typeof init === 'string' ? init : undefined, !publicDeployment, @@ -137,11 +139,10 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .option('--args [functionArgs...]', 'Function arguments', []) .requiredOption('-c, --contract-artifact ', "A compiled Aztec.nr contract's ABI in JSON format") .requiredOption('-ca, --contract-address
', 'Aztec address of the contract.', parseAztecAddress) - .option( + .requiredOption( '-a, --alias-or-address ', 'Alias or address of the account to deploy from. Incompatible with --private-key.', ) - .addOption(createPrivateKeyOption("The sender's private key.", false).conflicts('alias')) .option('--no-wait', 'Print transaction hash without waiting for it to be mined'); addOptions(sendCommand, FeeOpts.getOptions()).action(async (functionName, _options, command) => { @@ -153,7 +154,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL args, contractArtifact, contractAddress, - privateKey, aliasOrAddress, rpcUrl, !noWait, diff --git a/yarn-project/cli-wallet/src/cmds/send.ts b/yarn-project/cli-wallet/src/cmds/send.ts index 1b862d142b5..8c9820a6412 100644 --- a/yarn-project/cli-wallet/src/cmds/send.ts +++ b/yarn-project/cli-wallet/src/cmds/send.ts @@ -4,15 +4,14 @@ import { prepTx } from '@aztec/cli/utils'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { type IFeeOpts, printGasEstimates } from '../fees.js'; -import { AccountType, createOrRetrieveWallet } from '../utils/accounts.js'; +import { retrieveWallet } from '../utils/accounts.js'; export async function send( functionName: string, functionArgsIn: any[], contractArtifactPath: string, contractAddress: AztecAddress, - privateKey: Fr | undefined, - aliasOrAddress: string | undefined, + aliasOrAddress: string, rpcUrl: string, wait: boolean, feeOpts: IFeeOpts, @@ -23,7 +22,7 @@ export async function send( const client = await createCompatibleClient(rpcUrl, debugLogger); - const wallet = await createOrRetrieveWallet(AccountType.SCHNORR, client, privateKey, aliasOrAddress); + const wallet = await retrieveWallet(client, aliasOrAddress); const contract = await Contract.at(contractAddress, contractArtifact, wallet); const call = contract.methods[functionName](...functionArgs); diff --git a/yarn-project/cli-wallet/src/storage/wallet_db.ts b/yarn-project/cli-wallet/src/storage/wallet_db.ts index 356a0e20b61..4580f2aa7be 100644 --- a/yarn-project/cli-wallet/src/storage/wallet_db.ts +++ b/yarn-project/cli-wallet/src/storage/wallet_db.ts @@ -1,6 +1,8 @@ import { AztecAddress, Fr } from '@aztec/circuits.js'; import { type AztecKVStore, type AztecMap } from '@aztec/kv-store'; +import { AccountType } from '../utils/accounts.js'; + export class WalletDB { #accounts!: AztecMap; @@ -17,28 +19,44 @@ export class WalletDB { async storeAccount( address: AztecAddress, - { privateKey, salt, alias }: { privateKey: Fr; salt: Fr; alias: string | undefined }, + { type, secretKey, salt, alias }: { type: AccountType; secretKey: Fr; salt: Fr; alias: string | undefined }, ) { if (alias) { await this.#accounts.set(`${alias}`, address.toBuffer()); } - await this.#accounts.set(`${address.toString()}-pk`, privateKey.toBuffer()); + await this.#accounts.set(`${address.toString()}-type`, Buffer.from(type)); + await this.#accounts.set(`${address.toString()}-sk`, secretKey.toBuffer()); await this.#accounts.set(`${address.toString()}-salt`, salt.toBuffer()); } + async storeAccountMetadata(aliasOrAddress: AztecAddress | string, metadataKey: string, metadata: Buffer) { + const { address } = this.retrieveAccount(aliasOrAddress); + await this.#accounts.set(`${address.toString()}-${metadataKey}`, metadata); + } + + retrieveAccountMetadata(aliasOrAddress: AztecAddress | string, metadataKey: string) { + const { address } = this.retrieveAccount(aliasOrAddress); + const result = this.#accounts.get(`${address.toString()}-${metadataKey}`); + if (!result) { + throw new Error(`Could not find metadata with key ${metadataKey} for account ${aliasOrAddress}`); + } + return result; + } + retrieveAccount(aliasOrAddress: AztecAddress | string) { const address = typeof aliasOrAddress === 'object' ? aliasOrAddress : AztecAddress.fromBuffer(this.#accounts.get(aliasOrAddress)!); - const privateKeyBuffer = this.#accounts.get(`${address.toString()}-pk`); - if (!privateKeyBuffer) { + const secretKeyBuffer = this.#accounts.get(`${address.toString()}-sk`); + if (!secretKeyBuffer) { throw new Error( - `Could not find ${address}-pk. Account "${aliasOrAddress.toString}" does not exist on this wallet.`, + `Could not find ${address}-sk. Account "${aliasOrAddress.toString}" does not exist on this wallet.`, ); } - const privateKey = Fr.fromBuffer(privateKeyBuffer); + const secretKey = Fr.fromBuffer(secretKeyBuffer); const salt = Fr.fromBuffer(this.#accounts.get(`${address.toString()}-salt`)!); - return { privateKey, salt }; + const type = this.#accounts.get(`${address.toString()}-type`)!.toString('utf8') as AccountType; + return { address, secretKey, salt, type }; } } diff --git a/yarn-project/cli-wallet/src/utils/accounts.ts b/yarn-project/cli-wallet/src/utils/accounts.ts index a53ec8203df..1265be83fdb 100644 --- a/yarn-project/cli-wallet/src/utils/accounts.ts +++ b/yarn-project/cli-wallet/src/utils/accounts.ts @@ -1,29 +1,77 @@ +import { getEcdsaRSSHAccount } from '@aztec/accounts/ecdsa'; import { getSchnorrAccount } from '@aztec/accounts/schnorr'; -import { Fr, deriveSigningKey } from '@aztec/circuits.js'; +import { getIdentities } from '@aztec/accounts/utils'; +import { AztecAddress, Fr, deriveSigningKey } from '@aztec/circuits.js'; import { type PXE } from '../../../circuit-types/src/interfaces/pxe.js'; import { WalletDB } from '../storage/wallet_db.js'; +import { extractECDSAPublicKeyFromBase64String } from './ecdsa.js'; -export enum AccountType { - SCHNORR = 'schnorr', - ECDSASECP256R1 = 'ecdsasecp256r1', - ECDSASECP256K1 = 'ecdsasecp256k1', -} +export type AccountType = 'schnorr' | 'ecdsasecp256r1ssh' | 'ecdsasecp256k1'; -export async function createOrRetrieveWallet( +export async function createAndStoreAccount( + client: PXE, type: AccountType, - pxe: PXE, - privateKey: Fr | undefined, - aliasOrAddress: string | undefined, + secretKey: Fr, + publicKey: string | undefined, + salt: Fr, + alias: string | undefined, ) { + let account; + const db = WalletDB.getInstance(); + switch (type) { + case 'schnorr': { + account = getSchnorrAccount(client, secretKey, deriveSigningKey(secretKey), salt); + const { address } = account.getCompleteAddress(); + await db.storeAccount(address, { type, alias, secretKey, salt }); + break; + } + case 'ecdsasecp256r1ssh': { + if (!publicKey) { + throw new Error('Public key stored in the SSH agent must be provided for ECDSA SSH account'); + } + const identities = await getIdentities(); + const foundIdentity = identities.find( + identity => identity.type === 'ecdsa-sha2-nistp256' && identity.publicKey === publicKey, + ); + if (!foundIdentity) { + throw new Error(`Identity for public key ${publicKey} not found in the SSH agent`); + } + + const publicSigningKey = extractECDSAPublicKeyFromBase64String(foundIdentity.publicKey); + console.log(publicSigningKey.toString('hex')); + account = getEcdsaRSSHAccount(client, secretKey, publicSigningKey, salt); + const { address } = account.getCompleteAddress(); + await db.storeAccount(address, { type, alias, secretKey, salt }); + await db.storeAccountMetadata(address, 'publicSigningKey', publicSigningKey); + break; + } + default: { + throw new Error(`Unsupported account type: ${type}`); + } + } + + return account; +} + +export async function retrieveWallet(pxe: PXE, aliasOrAddress: string | AztecAddress) { let wallet; - if (aliasOrAddress) { - const { salt, privateKey } = WalletDB.getInstance().retrieveAccount(aliasOrAddress); - wallet = await getSchnorrAccount(pxe, privateKey, deriveSigningKey(privateKey), salt).getWallet(); - } else if (privateKey) { - wallet = await getSchnorrAccount(pxe, privateKey, deriveSigningKey(privateKey), Fr.ZERO).getWallet(); - } else { - throw new Error('Either a private key or an account address/alias must be provided'); + const { type, salt, secretKey } = WalletDB.getInstance().retrieveAccount(aliasOrAddress); + + switch (type) { + case 'schnorr': { + wallet = await getSchnorrAccount(pxe, secretKey, deriveSigningKey(secretKey), salt).getWallet(); + break; + } + case 'ecdsasecp256r1ssh': { + const publicSigningKey = WalletDB.getInstance().retrieveAccountMetadata(aliasOrAddress, 'publicSigningKey'); + wallet = await getEcdsaRSSHAccount(pxe, secretKey, publicSigningKey, salt).getWallet(); + break; + } + default: { + throw new Error(`Unsupported account type: ${type}`); + } } + return wallet; } diff --git a/yarn-project/cli-wallet/src/utils/ecdsa.ts b/yarn-project/cli-wallet/src/utils/ecdsa.ts new file mode 100644 index 00000000000..07edef1e32a --- /dev/null +++ b/yarn-project/cli-wallet/src/utils/ecdsa.ts @@ -0,0 +1,15 @@ +export function extractECDSAPublicKeyFromBase64String(publicKey: string): Buffer { + const publicKeyBuffer = Buffer.from(publicKey, 'base64'); + let keyOffset = 0; + const typeLen = publicKeyBuffer.readUInt32BE(keyOffset); + keyOffset += 4; + keyOffset += typeLen; + + const curveLen = publicKeyBuffer.readUInt32BE(keyOffset); + keyOffset += 4; + keyOffset += curveLen; + + const keyLen = publicKeyBuffer.readUInt32BE(keyOffset); + keyOffset += 5; // 4+1 to extract the prefix + return publicKeyBuffer.slice(keyOffset, keyOffset + keyLen - 1); +} diff --git a/yarn-project/cli/src/utils/commands.ts b/yarn-project/cli/src/utils/commands.ts index b68916a755d..2adb6e1701f 100644 --- a/yarn-project/cli/src/utils/commands.ts +++ b/yarn-project/cli/src/utils/commands.ts @@ -45,10 +45,10 @@ export const l1ChainIdOption = new Option('-c, --l1-chain-id ', 'Chain I return parsedValue; }); -export const createPrivateKeyOption = (description: string, mandatory: boolean) => - new Option('-pk, --private-key ', description) - .env('PRIVATE_KEY') - .argParser(parsePrivateKey) +export const createSecretKeyOption = (description: string, mandatory: boolean) => + new Option('-sk, --secret-key ', description) + .env('SECRET_KEY') + .argParser(parseSecretKey) .makeOptionMandatory(mandatory); export const logJson = (log: LogFn) => (obj: object) => log(JSON.stringify(obj, null, 2)); @@ -299,16 +299,16 @@ export function parsePartialAddress(address: string): Fr { } /** - * Parses a private key from a string. + * Parses a secret key from a string. * @param privateKey - A string - * @returns A private key + * @returns A secret key * @throws InvalidArgumentError if the input string is not valid. */ -export function parsePrivateKey(privateKey: string): Fr { +export function parseSecretKey(secretKey: string): Fr { try { - return Fr.fromString(privateKey); + return Fr.fromString(secretKey); } catch (err) { - throw new InvalidArgumentError(`Invalid encryption private key: ${privateKey}`); + throw new InvalidArgumentError(`Invalid encryption secret key: ${secretKey}`); } } diff --git a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts index b1492648f86..454be00bf21 100644 --- a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts +++ b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts @@ -1,4 +1,4 @@ -import { EcdsaAccountContract } from '@aztec/accounts/ecdsa'; +import { EcdsaKAccountContract } from '@aztec/accounts/ecdsa'; import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; import { SingleKeyAccountContract } from '@aztec/accounts/single_key'; import { @@ -93,6 +93,6 @@ describe('e2e_account_contracts', () => { }); describe('ecdsa stored-key account', () => { - itShouldBehaveLikeAnAccountContract(() => new EcdsaAccountContract(randomBytes(32)), walletSetup, walletAt); + itShouldBehaveLikeAnAccountContract(() => new EcdsaKAccountContract(randomBytes(32)), walletSetup, walletAt); }); });