diff --git a/noir-projects/noir-contracts/contracts/key_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/key_registry_contract/src/main.nr index aef32ca6133..95d6360563e 100644 --- a/noir-projects/noir-contracts/contracts/key_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/key_registry_contract/src/main.nr @@ -41,6 +41,20 @@ contract KeyRegistry { npk_m_y_registry.schedule_value_change(new_npk_m.y); } + #[aztec(public)] + fn rotate_ivpk_m(address: AztecAddress, new_ivpk_m: GrumpkinPoint, nonce: Field) { + if (!address.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, address); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let ivpk_m_x_registry = storage.ivpk_m_x_registry.at(address); + let ivpk_m_y_registry = storage.ivpk_m_y_registry.at(address); + ivpk_m_x_registry.schedule_value_change(new_ivpk_m.x); + ivpk_m_y_registry.schedule_value_change(new_ivpk_m.y); + } + #[aztec(public)] fn register(address: AztecAddress, partial_address: PartialAddress, keys: PublicKeys) { let computed_address = AztecAddress::compute(keys.hash(), partial_address); diff --git a/yarn-project/aztec.js/src/wallet/account_wallet.ts b/yarn-project/aztec.js/src/wallet/account_wallet.ts index 93c4114a598..1aba1709df8 100644 --- a/yarn-project/aztec.js/src/wallet/account_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/account_wallet.ts @@ -1,5 +1,12 @@ import { type AuthWitness, type FunctionCall, type PXE, type TxExecutionRequest } from '@aztec/circuit-types'; -import { AztecAddress, CANONICAL_KEY_REGISTRY_ADDRESS, Fq, Fr, derivePublicKeyFromSecretKey } from '@aztec/circuits.js'; +import { + AztecAddress, + CANONICAL_KEY_REGISTRY_ADDRESS, + Fq, + Fr, + type GrumpkinPrivateKey, + derivePublicKeyFromSecretKey, +} from '@aztec/circuits.js'; import { type ABIParameterVisibility, type FunctionAbi, FunctionType } from '@aztec/foundation/abi'; import { type AccountInterface } from '../account/interface.js'; @@ -174,7 +181,7 @@ export class AccountWallet extends BaseWallet { * * This does not hinder our ability to spend notes tied to a previous master nullifier public key, provided we have the master nullifier secret key for it. */ - public async rotateNullifierKeys(newNskM: Fq = Fq.random()): Promise { + public async rotateNullifierKeys(newNskM: GrumpkinPrivateKey = Fq.random()): Promise { // We rotate our secret key in the keystore first, because if the subsequent interaction fails, there are no bad side-effects. // If vice versa (the key registry is called first), but the call to the PXE fails, we will end up in a situation with unspendable notes, as we have not committed our // nullifier secret key to our wallet. @@ -189,6 +196,27 @@ export class AccountWallet extends BaseWallet { await interaction.send().wait(); } + /** + * Rotates the account incoming viewing key pair. + * @param newIvskM - The new incoming viewing key secret key we want to use. + * @remarks - This function also calls the canonical key registry with the account's new derived incoming viewing public key. + * We are doing it this way to avoid user error, in the case that a user rotates their keys in the key registry, + * but fails to do so in the key store. This leads to unspendable notes. + * + * This does not hinder our ability to spend notes tied to a previous incoming viewing public key, provided we have the incoming viewing secret key for it. + */ + public async rotateIncomingViewingKeys(newIvskM: GrumpkinPrivateKey = Fq.random()): Promise { + await this.pxe.rotateIvskM(this.getAddress(), newIvskM); + const interaction = new ContractFunctionInteraction( + this, + AztecAddress.fromBigInt(CANONICAL_KEY_REGISTRY_ADDRESS), + this.getRotateIvpkMAbi(), + [this.getAddress(), derivePublicKeyFromSecretKey(newIvskM), Fr.ZERO], + ); + + await interaction.send().wait(); + } + /** * Returns a function interaction to cancel a message hash as authorized in this account. * @param messageHashOrIntent - The message or the caller and action to authorize/revoke @@ -327,4 +355,39 @@ export class AccountWallet extends BaseWallet { returnTypes: [], }; } + + private getRotateIvpkMAbi(): FunctionAbi { + return { + name: 'rotate_ivpk_m', + isInitializer: false, + functionType: FunctionType.PUBLIC, + isInternal: false, + isStatic: false, + parameters: [ + { + name: 'address', + type: { + fields: [{ name: 'inner', type: { kind: 'field' } }], + kind: 'struct', + path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress', + }, + visibility: 'private' as ABIParameterVisibility, + }, + { + name: 'new_ivpk_m', + type: { + fields: [ + { name: 'x', type: { kind: 'field' } }, + { name: 'y', type: { kind: 'field' } }, + ], + kind: 'struct', + path: 'authwit::aztec::protocol_types::grumpkin_point::GrumpkinPoint', + }, + visibility: 'private' as ABIParameterVisibility, + }, + { name: 'nonce', type: { kind: 'field' }, visibility: 'private' as ABIParameterVisibility }, + ], + returnTypes: [], + }; + } } diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index 9ef5b80b253..3efa3239338 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -76,6 +76,9 @@ export abstract class BaseWallet implements Wallet { rotateNskM(address: AztecAddress, secretKey: Fq) { return this.pxe.rotateNskM(address, secretKey); } + rotateIvskM(address: AztecAddress, secretKey: Fq) { + return this.pxe.rotateIvskM(address, secretKey); + } registerRecipient(account: CompleteAddress): Promise { return this.pxe.registerRecipient(account); } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index 944601d1fb0..6f828bf29a0 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -1,4 +1,10 @@ -import { type AztecAddress, type CompleteAddress, type Fq, type Fr, type PartialAddress } from '@aztec/circuits.js'; +import { + type AztecAddress, + type CompleteAddress, + type Fr, + type GrumpkinPrivateKey, + type PartialAddress, +} from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { type ContractClassWithId, @@ -114,13 +120,19 @@ export interface PXE { * Rotates master nullifier keys. * @param address - The address of the account we want to rotate our key for. * @param newNskM - The new master nullifier secret key we want to use. - * @remarks - One should not use this function directly without also calling the canonical key registry to rotate - * the new master nullifier secret key's derived master nullifier public key. - * Therefore, it is preferred to use rotateNullifierKeys on AccountWallet, as that handles the call to the Key Registry as well. - * - * This does not hinder our ability to spend notes tied to a previous master nullifier public key, provided we have the master nullifier secret key for it. + * @remarks - Has to be called along with rotate_npk_m on key registry contract. Instead of this function call + * rotateNullifierKeys on AccountWallet to handle the call to the Key Registry as well. + */ + rotateNskM(address: AztecAddress, newNskM: GrumpkinPrivateKey): Promise; + + /** + * Rotates incoming viewing keys. + * @param address - The address of the account we want to rotate our key for. + * @param newIvskM - The new incoming viewing key secret key we want to use. + * @remarks - Has to be called along with rotate_ivpk_m on key registry contract. Instead of this function call + * rotateIncomingViewingKeys on AccountWallet to handle the call to the Key Registry as well. */ - rotateNskM(address: AztecAddress, newNskM: Fq): Promise; + rotateIvskM(address: AztecAddress, newIvskM: GrumpkinPrivateKey): Promise; /** * Registers a contract class in the PXE without registering any associated contract instance with it. diff --git a/yarn-project/end-to-end/src/e2e_key_rotation.test.ts b/yarn-project/end-to-end/src/e2e_key_rotation.test.ts index e49c523bc87..e9d3253662d 100644 --- a/yarn-project/end-to-end/src/e2e_key_rotation.test.ts +++ b/yarn-project/end-to-end/src/e2e_key_rotation.test.ts @@ -149,7 +149,7 @@ describe('e2e_key_rotation', () => { await contract.methods.redeem_shield(recipient, balance, secret).send().wait(); }; - it(`Rotates keys and uses them`, async () => { + it(`Rotates nullifier keys and uses them`, async () => { // 1. We check that setup set initial balances as expected await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance); await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n); @@ -233,4 +233,6 @@ describe('e2e_key_rotation', () => { await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n); } }, 600_000); + + it(`Rotates incoming viewing keys`, async () => {}); }); diff --git a/yarn-project/key-store/src/__snapshots__/key_store.test.ts.snap b/yarn-project/key-store/src/__snapshots__/key_store.test.ts.snap new file mode 100644 index 00000000000..265c4447bc1 --- /dev/null +++ b/yarn-project/key-store/src/__snapshots__/key_store.test.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KeyStore key rotation tests 1`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`; + +exports[`KeyStore key rotation tests 2`] = `"0x296e42f1039b62290372d608fcab55b00a3f96c1c8aa347b2a830639c5a12757"`; + +exports[`KeyStore key rotation tests 3`] = `"0x019f2a705b68683f1d86da639a543411fa779af41896c3920d0c2d5226c686dd"`; + +exports[`KeyStore key rotation tests 4`] = `"0x117445c8819c06b9a0889e5cce1f550e32ec6993c23f57bc9fc5cda05df520ae"`; + +exports[`KeyStore key rotation tests 5`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`; + +exports[`KeyStore key rotation tests 6`] = `"0x21e3ca4bc7ae2b5e9fe343f4eec5c0aa7391857333821a4b0a1c7d4cb0055bf0"`; + +exports[`KeyStore key rotation tests 7`] = `"0x0900aea4825d057e5bc916063a535520a7c6283740eaf218cd6961b10cba46fd"`; + +exports[`KeyStore key rotation tests 8`] = `"0x27ccbe41ff5f33fa78348533da9d4a79e8fea8805771e61748ea42be4202f168"`; + +exports[`KeyStore key rotation tests 9`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`; + +exports[`KeyStore key rotation tests 10`] = `"0x1895c1aa4f807cc8429c4d73a7bcb366653fea18f2b35f12589c599398733e7b"`; + +exports[`KeyStore key rotation tests 11`] = `"0x28e0f5e42af72adfa2aa1f7c64c34dc9589104e4a3bc0ac0d63f21a6780d8250"`; + +exports[`KeyStore key rotation tests 12`] = `"0x0f00f1ac34f2e4ba49d25cc57b4d0ed45a1cc2b55b292e1fb09b157887153f8e"`; + +exports[`KeyStore key rotation tests 13`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`; + +exports[`KeyStore key rotation tests 14`] = `"0x105baf9ca58f8a5bff283642c423f3f1d46107975f05a751abc7e8b51728277f"`; + +exports[`KeyStore key rotation tests 15`] = `"0x2fb411ed0ba7bf7b44963b1c36a1de17546da2ebee4564d9cc62486c3325149e"`; + +exports[`KeyStore key rotation tests 16`] = `"0x1c0fb3a8709e6ea1a9c67b3b499b5e6d0471cb38a6c824319805aeaca0e201df"`; diff --git a/yarn-project/key-store/src/key_store.test.ts b/yarn-project/key-store/src/key_store.test.ts index 5c13479d406..d5150055b36 100644 --- a/yarn-project/key-store/src/key_store.test.ts +++ b/yarn-project/key-store/src/key_store.test.ts @@ -2,9 +2,11 @@ import { AztecAddress, Fq, Fr, - computeAppNullifierSecretKey, + type KeyPrefix, + computeAppSecretKey, deriveKeys, derivePublicKeyFromSecretKey, + KEY_PREFIXES, } from '@aztec/circuits.js'; import { openTmpStore } from '@aztec/kv-store/utils'; @@ -89,7 +91,7 @@ describe('KeyStore', () => { ); }); - it('nullifier key rotation tests', async () => { + it.each(KEY_PREFIXES)('key rotation tests', async keyPrefix => { const keyStore = new KeyStore(openTmpStore()); // Arbitrary fixed values @@ -97,74 +99,66 @@ describe('KeyStore', () => { const partialAddress = new Fr(243523n); const { address: accountAddress } = await keyStore.addAccount(sk, partialAddress); - expect(accountAddress.toString()).toMatchInlineSnapshot( - `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`, - ); + expect(accountAddress.toString()).toMatchSnapshot(); // Arbitrary fixed values - const newMasterNullifierSecretKeys = [new Fq(420n), new Fq(69n), new Fq(42069n)]; - const newDerivedMasterNullifierPublicKeys = [ - derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[0]), - derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[1]), - derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[2]), + const newMasterSecretKeys = [new Fq(420n), new Fq(69n), new Fq(42069n)]; + const newDerivedMasterPublicKeys = [ + derivePublicKeyFromSecretKey(newMasterSecretKeys[0]), + derivePublicKeyFromSecretKey(newMasterSecretKeys[1]), + derivePublicKeyFromSecretKey(newMasterSecretKeys[2]), ]; - const newComputedMasterNullifierPublicKeyHashes = [ - newDerivedMasterNullifierPublicKeys[0].hash(), - newDerivedMasterNullifierPublicKeys[1].hash(), - newDerivedMasterNullifierPublicKeys[2].hash(), + const newComputedMasterPublicKeyHashes = [ + newDerivedMasterPublicKeys[0].hash(), + newDerivedMasterPublicKeys[1].hash(), + newDerivedMasterPublicKeys[2].hash(), ]; - // We rotate our nullifier key - await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[0]); - await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[1]); - await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[2]); + // We rotate our key + await keyStore.rotateMasterKey(accountAddress, keyPrefix, newMasterSecretKeys[0]); + await keyStore.rotateMasterKey(accountAddress, keyPrefix, newMasterSecretKeys[1]); + await keyStore.rotateMasterKey(accountAddress, keyPrefix, newMasterSecretKeys[2]); - // We make sure we can get master nullifier public keys with master nullifier public key hashes - const { pkM: masterNullifierPublicKey2 } = await keyStore.getKeyValidationRequest( - newComputedMasterNullifierPublicKeyHashes[2], + // We make sure we can get master public keys with master public key hashes + const { pkM: masterPublicKey2 } = await keyStore.getKeyValidationRequest( + newComputedMasterPublicKeyHashes[2], AztecAddress.random(), // Address is random because we are not interested in the app secret key here ); - expect(masterNullifierPublicKey2).toEqual(newDerivedMasterNullifierPublicKeys[2]); - const { pkM: masterNullifierPublicKey1 } = await keyStore.getKeyValidationRequest( - newComputedMasterNullifierPublicKeyHashes[1], + expect(masterPublicKey2).toEqual(newDerivedMasterPublicKeys[2]); + const { pkM: masterPublicKey1 } = await keyStore.getKeyValidationRequest( + newComputedMasterPublicKeyHashes[1], AztecAddress.random(), // Address is random because we are not interested in the app secret key here ); - expect(masterNullifierPublicKey1).toEqual(newDerivedMasterNullifierPublicKeys[1]); - const { pkM: masterNullifierPublicKey0 } = await keyStore.getKeyValidationRequest( - newComputedMasterNullifierPublicKeyHashes[0], + expect(masterPublicKey1).toEqual(newDerivedMasterPublicKeys[1]); + const { pkM: masterPublicKey0 } = await keyStore.getKeyValidationRequest( + newComputedMasterPublicKeyHashes[0], AztecAddress.random(), // Address is random because we are not interested in the app secret key here ); - expect(masterNullifierPublicKey0).toEqual(newDerivedMasterNullifierPublicKeys[0]); + expect(masterPublicKey0).toEqual(newDerivedMasterPublicKeys[0]); // Arbitrary app contract address const appAddress = AztecAddress.fromBigInt(624n); - // We make sure we can get app nullifier secret keys with master nullifier public key hashes - const { skApp: appNullifierSecretKey0 } = await keyStore.getKeyValidationRequest( - newComputedMasterNullifierPublicKeyHashes[0], + // We make sure we can get app secret keys with master public key hashes + const { skApp: appSecretKey0 } = await keyStore.getKeyValidationRequest( + newComputedMasterPublicKeyHashes[0], appAddress, ); - expect(appNullifierSecretKey0.toString()).toMatchInlineSnapshot( - `"0x296e42f1039b62290372d608fcab55b00a3f96c1c8aa347b2a830639c5a12757"`, - ); - const { skApp: appNullifierSecretKey1 } = await keyStore.getKeyValidationRequest( - newComputedMasterNullifierPublicKeyHashes[1], + expect(appSecretKey0.toString()).toMatchSnapshot(); + const { skApp: appSecretKey1 } = await keyStore.getKeyValidationRequest( + newComputedMasterPublicKeyHashes[1], appAddress, ); - expect(appNullifierSecretKey1.toString()).toMatchInlineSnapshot( - `"0x019f2a705b68683f1d86da639a543411fa779af41896c3920d0c2d5226c686dd"`, - ); - const { skApp: appNullifierSecretKey2 } = await keyStore.getKeyValidationRequest( - newComputedMasterNullifierPublicKeyHashes[2], + expect(appSecretKey1.toString()).toMatchSnapshot(); + const { skApp: appSecretKey2 } = await keyStore.getKeyValidationRequest( + newComputedMasterPublicKeyHashes[2], appAddress, ); - expect(appNullifierSecretKey2.toString()).toMatchInlineSnapshot( - `"0x117445c8819c06b9a0889e5cce1f550e32ec6993c23f57bc9fc5cda05df520ae"`, - ); + expect(appSecretKey2.toString()).toMatchSnapshot(); - expect(appNullifierSecretKey0).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[0], appAddress)); - expect(appNullifierSecretKey1).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[1], appAddress)); - expect(appNullifierSecretKey2).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[2], appAddress)); + expect(appSecretKey0).toEqual(computeAppSecretKey(newMasterSecretKeys[0], appAddress, keyPrefix)); + expect(appSecretKey1).toEqual(computeAppSecretKey(newMasterSecretKeys[1], appAddress, keyPrefix)); + expect(appSecretKey2).toEqual(computeAppSecretKey(newMasterSecretKeys[2], appAddress, keyPrefix)); }); }); diff --git a/yarn-project/key-store/src/key_store.ts b/yarn-project/key-store/src/key_store.ts index f3705a92425..bab3089d01d 100644 --- a/yarn-project/key-store/src/key_store.ts +++ b/yarn-project/key-store/src/key_store.ts @@ -2,7 +2,7 @@ import { type PublicKey } from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress, - Fq, + type Fq, Fr, GeneratorIndex, type GrumpkinPrivateKey, @@ -307,28 +307,24 @@ export class KeyStore { } /** - * Rotates the master nullifier key for the specified account. - * - * @dev This function updates the secret and public keys associated with the account. - * It appends a new secret key to the existing secret keys, derives the - * corresponding public key, and updates the stored keys accordingly. - * - * @param account - The account address for which the master nullifier key is being rotated. - * @param newSecretKey - (Optional) A new secret key of type Fq. If not provided, a random key is generated. - * @throws If the account does not have existing nullifier secret keys or public keys. + * Rotates the master key defined by key prefix for the specified account. + * @param account - The account address for which the master key is being rotated. + * @param keyPrefix - The key prefix of the master key to rotate. + * @param newSecretKey - A new secret key to rotate to. + * @throws If the account does not have existing secret keys or public keys. * @returns A Promise that resolves when the key rotation is complete. */ - public async rotateMasterNullifierKey(account: AztecAddress, newSecretKey: Fq = Fq.random()) { + public async rotateMasterKey(account: AztecAddress, keyPrefix: KeyPrefix, newSecretKey: GrumpkinPrivateKey) { // We append the secret key to the array of secret keys - await this.#appendValue(`${account.toString()}-nsk_m`, newSecretKey); + await this.#appendValue(`${account.toString()}-${keyPrefix}sk_m`, newSecretKey); // Now we derive the public key from the new secret key and append it to the buffer of original public keys const newPublicKey = derivePublicKeyFromSecretKey(newSecretKey); - await this.#appendValue(`${account.toString()}-npk_m`, newPublicKey); + await this.#appendValue(`${account.toString()}-${keyPrefix}pk_m`, newPublicKey); - // At last we store npk_m_hash under `account-npk_m_hash` key to be able to obtain address and key prefix + // At last we store pk_m_hash under `account-pk_m_hash` key to be able to obtain address and key prefix // using the #getKeyPrefixAndAccount function later on - await this.#appendValue(`${account.toString()}-npk_m_hash`, newPublicKey.hash()); + await this.#appendValue(`${account.toString()}-${keyPrefix}pk_m_hash`, newPublicKey.hash()); } /** diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index f7d6b8b02c5..743fe6f0fd3 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -157,7 +157,13 @@ export class PXEService implements PXE { } async rotateNskM(account: AztecAddress, secretKey: Fq): Promise { - await this.keyStore.rotateMasterNullifierKey(account, secretKey); + // 'n' prefix stands for nullifier key + await this.keyStore.rotateMasterKey(account, 'n', secretKey); + } + + async rotateIvskM(account: AztecAddress, secretKey: Fq): Promise { + // 'iv' prefix stands for incoming viewing key + await this.keyStore.rotateMasterKey(account, 'iv', secretKey); } public addCapsule(capsule: Fr[]) {