diff --git a/docs/docs/aztec/concepts/accounts/keys.md b/docs/docs/aztec/concepts/accounts/keys.md index 168ff03c07c5..c8d68dc19e49 100644 --- a/docs/docs/aztec/concepts/accounts/keys.md +++ b/docs/docs/aztec/concepts/accounts/keys.md @@ -3,157 +3,125 @@ title: Keys tags: [accounts, keys] --- -The goal of this section is to give app developer a good idea what keys there are used in the system. +In this section, you will learn what keys are used in Aztec, and how the addresses are derived. -In short, there is a **nullifier key** (to spend your notes), an **incoming viewing key** (to view any notes or logs that were sent to you), an **outgoing viewing key** (to view any logs or notes you sent to another entity), a **tagging key** (to quickly find notes relevant to you) and oftentimes a signing key. A signing key is not strictly required by the protocol, but are often used with specific account contracts for authorization purposes. +## Types of keys -Each account in Aztec is backed by 4 key pairs: +Each Aztec account is backed by four key pairs: +- Nullifier keys – used to spend notes. +- Address keys – this is an auxiliary key used for the address derivation; it’s internally utilized by the protocol and does not require any action from developers. +- Incoming viewing keys – used to encrypt a note for the recipient. +- Signing keys – an optional key pair used for account authorization. -- A **nullifier key pair** used for note nullifier computation, comprising the master nullifier secret key (`nsk_m`) and master nullifier public key (`Npk_m`). -- An **incoming viewing key pair** used to encrypt a note for the recipient, consisting of the master incoming viewing secret key (`ivsk_m`) and master incoming viewing public key (`Ivpk_m`). -- An **outgoing viewing key pair** used to encrypt a note for the sender, includes the master outgoing viewing secret key (`ovsk_m`) and master outgoing viewing public key (`Ovpk_m`). -- A **tagging key pair** used to compute tags in a tagging note discovery scheme, comprising the master tagging secret key (`tsk_m`) and master tagging public key (`Tpk_m`). +The first three pairs are embedded into the protocol while the signing key is abstracted up to the account contract developer. -:::info -Key pairs are derived from a secret using a ZCash inspired scheme. -::: - -:::note -Additionally, there is typically a signing key pair which is used for authenticating the owner of the account. -However, since Aztec supports native [account abstraction](../accounts/index.md#what-is-account-abstraction) this is not defined in protocol. -Instead it's up to the account contract developer to implement it. -::: - -## Public keys retrieval - -The keys for our accounts can be retrieved from the [Private eXecution Environment (PXE)](../pxe/index.md) using the following getter in Aztec.nr: +### Nullifier keys -``` -fn get_public_keys(account: AztecAddress) -> PublicKeys; -``` +Nullifier keys are presented as a pair of the master nullifier public key (`Npk_m`) and the master nullifier secret key (`nsk_m`). -It is necessary to first register the user as an account in our PXE, by calling the `registerAccount` PXE endpoint using Aztec.js, providing the account's secret key and partial address. +To spend a note, the user computes a nullifier corresponding to this note. A nullifier is a hash of the note hash and app siloed nullifier secret key, the latter is derived using the nullifier master secret key. To compute the nullifier, the protocol checks that the app siloed key is derived from the master key for this contract and that master nullifier public key is linked to the note owner's address. -During private function execution these keys are obtained via an oracle call from PXE. +### Address keys -## Scoped keys +Address keys are used for account [address derivation](../accounts/index.md). -To minimize damage of potential key leaks the keys are scoped (also called app-siloed) to the contract that requests them. -This means that the keys used for the same user in two different application contracts will be different and potential leak of the scoped keys would only affect 1 application. +Address keys are a pair of keys `AddressPublicKey` and `address_sk` where `address_sk` is a scalar defined as `address_sk = pre_address + ivsk` and `AddressPublicKey` is an elliptic curve point defined as `AddressPublicKey = address_sk * G`. `pre_address` can be thought of as a hash of all account’s key pairs and functions in the account contract: `pre_address := poseidon2(public_keys_hash, partial_address)` where `partial_address := poseidon2(contract_class_id, salted_initialization_hash)` and `public_keys_hash := poseidon2(Npk_m, Ivpk_m, Ovpk_m, Tpk_m)`. -This also allows per-application auditability. -A user may choose to disclose their incoming and outgoing viewing keys for a given application to an auditor or regulator (or for 3rd party interfaces, e.g. giving access to a block explorer to display my activity), as a means to reveal all their activity within that context, while retaining privacy across all other applications in the network. +:::note +Under the current design Aztec protocol does not use `Ovpk` (outgoing viewing key) and `Tpk` (tagging key). However, formally they still exist and can be used by developers for some non-trivial design choices if needed. +::: -In the case of nullifier keys, there is also a security reason involved. -Since the nullifier secret is exposed to the application contract to be used in the nullifier computation, the contract may accidentally or maliciously leak it. -If that happens, only the nullifier secret for that application is compromised (`nsk_app` and not `nsk_m`). +### Incoming viewing keys -Above we mentioned that the notes typically contain `Npk_m`. -It might seem like a mistake given that the notes are nullified with `nsk_app`. -This is intentional and instead of directly trying to derive `Npk_m` from `nsk_app` we instead verify that both of the keys were derived from the same `nsk_m` in our protocol circuits. +The incoming viewing public key (`Ivpk`) is used by the sender to encrypt a note for the recipient. The corresponding incoming viewing secret key (`ivsk`) is used by the recipient to decrypt the note. -## Protocol key types +When it comes to notes encryption and decryption: +- For each note, there is a randomly generated ephemeral key pair (`esk`, `Epk`) where `Epk = esk * G`. +- The `AddressPublicKey` (derived from the `ivsk`) together with `esk` are encrypted as a secret `S`, `S = esk * AddressPublicKey`. +- `symmetric_encryption_key = hash(S)` +- `Ciphertext = aes_encrypt(note, symmetric_encryption_key)` +- The recipient gets a pair (`Epk`, `Ciphertext`) +- The recipient uses the `address_sk` to decrypt the secret: `S = Epk * address_sk`. +- The recipient uses the decrypted secret to decrypt the ciphertext. -All the keys below are Grumpkin keys (public keys derived on the Grumpkin curve). +### Signing keys -## Nullifier keys +Thanks to the native [account abstraction](../accounts#background/index.md), authorization logic can be implemented in an alternative way that is up to the developer (e.g. using Google authorization credentials, vanilla password logic or Face ID mechanism). In all these cases signing keys may not be relevant. -Whenever a note is consumed, a nullifier deterministically derived from it is emitted. -This mechanisms prevents double-spends, since nullifiers are checked by the protocol to be unique. -Now, in order to preserve privacy, a third party should not be able to link a note hash to its nullifier - this link is enforced by the note implementation. -Therefore, calculating the nullifier for a note requires a secret from its owner. +However if one wants to implement authorization logic containing signatures (e.g. ECDSA or Shnorr) they will need signing keys. Usually, an account contract will validate a signature of the incoming payload against a known signing public key. -An application in Aztec.nr can request a secret from the current user for computing the nullifier of a note via the `request_nullifier_secret_key` API: +This is a snippet of our Schnorr Account contract implementation, which uses Schnorr signatures for authentication: -#include_code nullifier /noir-projects/aztec-nr/value-note/src/value_note.nr rust +#include_code is_valid_impl /noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr rust -Typically, `Npk_m` is stored in a note and later on, the note is nullified using the secret app-siloed version (denoted `nsk_app`). -`nsk_app` is derived by hashing `nsk_m` with the app contract address and it is necessary to present it to compute the nullifier. -Validity of `nsk_app` is verified by our protocol kernel circuits. +### Storing signing keys -## Incoming viewing keys +Since signatures are fully abstracted, how the public key is stored in the contract is abstracted as well and left to the developer of the account contract. Among a few common approaches are storing the key in a private note, in an immutable private note, using shared mutable state, reusing other in-protocol keys, or a separate keystore. Below, we elaborate on these approaches. -The public key (denoted `Ivpk`) is used to encrypt a note for a recipient and the corresponding secret key (`ivsk`) is used by the recipient during decryption. +#### Using a private note​ -## Outgoing viewing keys +Storing the signing public key in a private note makes it accessible from the entrypoint function, which is required to be a private function, and allows for rotating the key when needed. However, keep in mind that reading a private note requires nullifying it to ensure it is up-to-date, so each transaction you send will destroy and recreate the public key so the protocol circuits can be sure that the notes are not stale. -App-siloed versions of outgoing viewing keys are denoted `ovsk_app` and `Ovpk_app`. -These keys are used to encrypt a note for a note sender which is necessary for reconstructing transaction history from on-chain data. -For example, during a token transfer, the token contract may dictate that the sender encrypts the note with value with the recipient's `Ivpk`, but also records the transfer with its own `Ovpk_app` for bookkeeping purposes. -If these keys were not used and a new device would be synched there would be no "direct" information available about notes that a user created for other people. +#### Using an immutable private note​ -## Tagging keys +Similar to using a private note, but using an immutable private note removes the need to nullify the note on every read. This generates no nullifiers and commitments per transaction. However, it does not allow the user to rotate their key if they lose it. -Used to compute tags in a tagging note discovery scheme. +#include_code public_key aztec-packages/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr rust :::note -Tagging note discovery scheme won't be present in our testnet so we are intentionally not providing you with much info yet. +When it comes to storing the signing key in a private note, there are several details that rely on the wallets: +- A note with a key is managed similar to any other private note. Wallets are expected to backup all the notes so that they can be restored on another device (e.g. if the user wants to move to another device). +- The note with the key might exist locally only (in PXE) or it can be broadcasted as an encrypted note by the wallet to itself. In the second case, this note will also exist on Aztec. ::: -## Signing keys +#### Using Shared Mutable state -As mentioned above signing keys are not defined in protocol because of [account abstraction](../accounts/index.md#what-is-account-abstraction) and instead the key scheme is defined by the account contract. +:::note +By [Shared Mutable](../shared_state#sharedmutable/index.md) we mean privately readable publicly mutable state. +::: -Usually, an account contract will validate a signature of the incoming payload against a known signing public key. +To make public state accessible privately, there should be a delay window in public state updates. One needs this window to be able to generate proofs client-side. This approach would not generate additional nullifiers and commitments for each transaction while allowing the user to rotate their key. However, this causes every transaction to now have a time-to-live determined by the frequency of the mutable shared state, as well as imposing restrictions on how fast keys can be rotated due to minimum delays. -This is a snippet of our Schnorr Account contract implementation, which uses Schnorr signatures for authentication: - -#include_code is_valid_impl /noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr rust +#### Reusing some of the in-protocol keys -Still, different accounts may use different signing schemes, may require multi-factor authentication, or _may not even use signing keys_ and instead rely on other authentication mechanisms. Read [how to write an account contract](../../../tutorials/codealong/contract_tutorials/write_accounts_contract.md) for a full example of how to manage authentication. +It is possible to use some of the key pairs defined in protocol (e.g. incoming viewing keys) as the signing key. Since this key is part of the address preimage, it can be validated against the account contract address rather than having to store it. However, this approach is not recommended since it reduces the security of the user's account. -Furthermore, and since signatures are fully abstracted, how the key is stored in the contract is abstracted as well and left to the developer of the account contract. -In the following section we describe a few ways how an account contract could be architected to store signing keys. +#### Using a separate keystore -### Storing signing keys +Since there are no restrictions on the actions that an account contract may execute for authenticating a transaction (as long as these are all private function executions), the signing public keys can be stored in a separate keystore contract that is checked on every call. In this case, each user could keep a single contract that acts as a keystore, and have multiple account contracts that check against that keystore for authorization. This will incur a higher proving time for each transaction, but has no additional cost in terms of fees. -#### Using a private note +### Keys generation -Storing the signing public key in a private note makes it accessible from the entrypoint function, which is required to be a private function, and allows for rotating the key when needed. However, keep in mind that reading a private note requires nullifying it to ensure it is up-to-date, so each transaction you send will destroy and recreate the public key. This has the side effect of enforcing a strict ordering across all transactions, since each transaction will refer to the instantiation of the private note from the previous one. +All key pairs (except for the signing keys) are generated in the [Private Execution Environment](../pxe/index.md) (PXE) when a user creates an account. PXE is also responsible for the further key management (oracle access to keys, app siloed keys derivation, etc.) -#### Using an immutable private note +### Keys derivation -Similar to using a private note, but using an immutable private note removes the need to nullify the note on every read. This generates less nullifiers and commitments per transaction, and does not enforce an order across transactions. However, it does not allow the user to rotate their key should they lose it. +All key pairs are derived using elliptic curve public-key cryptography on the [Grumpkin curve](https://github.com/AztecProtocol/aztec-connect/blob/9374aae687ec5ea01adeb651e7b9ab0d69a1b33b/markdown/specs/aztec-connect/src/primitives.md). Where the secret key is represented as a scalar and the public key is represented as an elliptic curve point multiplied by that scalar. -#### Using shared state +The address private key is an exception and derived in a way described above in the section “Address keys”. -A compromise between the two solutions above is to use shared state. This would not generate additional nullifiers and commitments for each transaction while allowing the user to rotate their key. However, this causes every transaction to now have a time-to-live determined by the frequency of the mutable shared state, as well as imposing restrictions on how fast keys can be rotated due to minimum delays. +### The special case of escrow contracts -#### Reusing some of the in-protocol keys +Typically, for account contracts the public keys will be non-zero and for non-account contracts zero. -It is possible to use some of the key pairs defined in protocol (e.g. incoming viewing keys) as the signing key. -Since this key is part of the address preimage (more on this in the privacy master key section), it can be validated against the account contract address rather than having to store it. -However, this approach is not recommended since it reduces the security of the user's account. +An exception (a non-account contract which would have some of the keys non-zero) is an escrow contract. Escrow contract is a type of contract which on its own is an "owner" of a note meaning that it has a` Npk_m` registered and the notes contain this `Npk_m`. -#### Using a separate keystore +Participants in this escrow contract would then somehow get a hold of the escrow's `nsk_m` and nullify the notes based on the logic of the escrow. An example of an escrow contract is a betting contract. In this scenario, both parties involved in the bet would be aware of the escrow's `nsk_m`. The escrow would then release the reward only to the party that provides a "proof of winning". -Since there are no restrictions on the actions that an account contract may execute for authenticating a transaction (as long as these are all private function executions), the signing public keys can be stored in a [separate keystore contract](https://vitalik.ca/general/2023/06/09/three_transitions.html) that is checked on every call. This will incur in a higher proving time for each transaction, but has no additional cost in terms of fees, and allows for easier key management in a centralized contract. +### App siloed keys -### Complete address +All keys on Aztec (except for the signing keys) are app-siloed meaning they are scoped to the contract that requests them. This means that the keys used for the same user in two different application contracts will be different. -When deploying a contract, the contract address is deterministically derived using the following scheme: +App-siloed keys allow to minimize damage of potential key leaks as a leak of the scoped keys would only affect one application. - +App-siloed keys are derived from the corresponding master keys and the contract address. For example, for the app-siloed nullifier secret key: `nsk_app = hash(nsk_m, app_contract_address)`. -``` -partial_address := poseidon2("az_contract_partial_address_v1", contract_class_id, salted_initialization_hash) -public_keys_hash := poseidon2("az_public_keys_hash", Npk_m, Ivpk_m, Ovpk_m, Tpk_m) -address := poseidon2("az_contract_address_v1", public_keys_hash, partial_address) -``` +App-siloed keys [are derived](../storage_slots#implementation/index.md) in PXE every time the user interacts with the application. -Typically, for account contracts the public keys will be non-zero and for non-account contracts zero. -An example of a non-account contract which would have some of the keys non-zero is an escrow contract. -Escrow contract is a type of contract which on its own is an "owner" of a note meaning that it has a `Npk_m` registered and the notes contain this `Npk_m`. -Participants in this escrow contract would then somehow get a hold of the escrow's `nsk_m` and nullify the notes based on the logic of the escrow. -An example of an escrow contract is a betting contract. In this scenario, both parties involved in the bet would be aware of the escrow's `nsk_m`. -The escrow would then release the reward only to the party that provides a "proof of winning". +App-siloed incoming viewing key also allows per-application auditability. A user may choose to disclose this key for a given application to an auditor or regulator (or for 3rd party interfaces, e.g. giving access to a block explorer to display my activity), as a means to reveal all their activity within that context, while retaining privacy across all other applications in the network. -Because of the contract address derivation scheme it is possible to check that a given set of public keys corresponds to a given address just by trying to recompute it. -Since this is commonly needed to be done when sending a note to an account we coined the term **complete address** for the collection of: +### Key rotation -1. all the user's public keys, -2. partial address, -3. contract address. +Key rotation is the process of creating new signing keys to replace existing keys. By rotating encryption keys on a regular schedule or after specific events, you can reduce the potential consequences of the key being compromised. -Once the complete address is shared with the sender, the sender can check that the address was correctly derived from the public keys and partial address and then send the notes to that address. -Because of this it is possible to send a note to an account whose account contract was not yet deployed. +On Aztec, key rotation is impossible for nullifier keys, incoming viewing keys and address keys as all of them are embedded into the address and address is unchangeable. In the meanwhile, signing keys can be rotated. diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index cd8f11643ac0..56c26886c615 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -25,7 +25,9 @@ contract SchnorrAccount { #[storage] struct Storage { + // docs:start:public_key signing_public_key: PrivateImmutable, + // docs:end:public_key } // Constructs the contract