AZIP-11: Authorization #27
paperclip-minim
started this conversation in
AZIP Proposals
Replies: 2 comments
-
|
See #28 |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
Updated the discussion: the static call is now an explicit design choice |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
AZIP: AZIP-11
We're standardizing the current de-facto mechanism for authorization, with a few changes yet to be implemented.
Context and motivation
The current authorization flow, as outlined in aztec::authwit::auth is a de-facto standard because it's the first and only authorization mechanism so far, and there's not enough development power for divergences to have arisen yet.
Its standardization through an AZIP would enshrine it as a consensuated common interface between protocols, as opposed to just a helper in the standard libraries.
Furthermore, it's beneficial to introduce some changes, whose rationales are explained in depth in the proposed changes section below. There are also a standardization choices and an open questions section with decisions and observations.
The specification itself can be found in the PR link commented below in this discussion.
Proposed changes
Rename “authwit” to “authorization”
This is useful for separating domains of knowledge. Noir developers needn't know what a ZK witness is.
Furthermore, authorization doesn't necessarily rely on the explicit use of a witness, as the current accounts do. A multi-auth threshold account may rely on the existance of unspent notes, rather than receiving a specific kind of signature from each participant.
Remove the return type from the verifying functions
The current function signature for private and public authorizations are
This would lead a developer to expect some use to the returned value. However, the default implementation follows either of these flows:
IS_VALID_SELECTOR.I.e., there's currently two mutually-exclusive mechanisms used inconsistently. The functions should either return a
Fieldthat represents whether the authorization succeeds and not revert (unless catastrophe), or not return anything and revert when authorization is not granted.We chose the later, as it's more conservative with respect to security.
Include the approver's app-siloed nullifying key in the replay protection
The current design computes the nullifier as
If the function call (or the message) doesn't have a nonce, and depending on the nature of the consumer contract, these nullifiers may be brute-forceable. The inclusion of the consumer-siloed nullifying key in the computation removes that responsability from the consumer.
Standardization choices
De-facto entrypoint is not standardized
There's currently a default implementation for account contracts' entrypoint. While its use easens development, it does not pertain inter-contract behaviour, and thus we chose to omit it.
No clear signing
Since it doesn't pertain inter-contract behaviour, this is also not standardized here. However, it'd be optimal for wallets to have clear singing.
Design choices
Authorize when the caller is the approver
It’s assumed that having the approver as the caller suffices to consider the call authorized. Otherwise, wallets would either have to:
Asking that consumers SHOULD assume authorization when the approver is the sender allows both good UX and preserving the authorization↔user presence parity, while letting compliant contracts decide to force an authorization check.
Revert when not authorizing
The functions
verify_private_authorizationandconsumeboth signal authorization by not reverting, and no return value is provided. The alternative would be not to revert, and instead signal authorization via a return value.This design choice follows the principle of failing fast. As a tradeoff, applications must take care not to brick when an approver is not approving. The authorization registry has functions that perform this check explicitly (
is_consumableandis_revert_all), whereas the check must be implemented by the app explicitly in private.Perform a static call
This prevents reentrancy. Without it, a consumer may expect a
verify_private_authorizationcall to just perform authorization, but the approver may perform an external call to the consumer, so that by the timeverify_private_authorizationreturns, the consumer's state has been modified.The downside, however, is that approvers cannot use side effects as part of their access control, which is needed for basic operations such as reading a private mutable variable.
Robust reentrancy protection for non-static calls most likely requires protocol-level changes outside the scope of this AZIP. Developers could mitigate the attack surface by using the checks-efects-interactions pattern, but it requires active attention on their behalf, and opens up a door to a lot of incidents.
Hence, we decided to go for the safe option for the time being.
Open questions
Should we use
inner_hashin all external methods?Authorization setters and
is_consumablein theAuthRegistrytake themessage_hashas input, whereasconsumetakes aninner_hashas input. If there are no use-cases where a contract may want to check the status for an authorization meant to another contract, it'd appear clearer to useinner_hashin all of them, and assume the caller is the consumer. This further removes the responsibility of computing the message hash from the consumer.AuthRegistry as private intermediary?
Conceptually, the responsibility of protecting against replay seems better suited for the approver, rather than the consumer. If a consumer forgets to incorporate replay protection (in which case it wouldn't be compliant with this standard), all accounts are vulnerable in this protocol. If accounts were responsible for replay protection, DeFi protocol developers wouldn't hold such crucial responsibility, and it would be solved once per account contract, not once per protocol.
However, the call to
verify_private_authorizationmust be static to prevent reentrancy.A possible solution to this would be to have
AuthRegistryas an intermediary between the account contract and the app contract. It’d have a privateverify_private_authorizationfunction itself that would statically callverify_private_authorizationand emit a nullifier for replay protection.However, since authorizations are, for the most part, not meant to be shared around, it doesn't seem to be worth the increase in cost (one more external call).
Should call authorization and/or clear signing go into another AZIP?
This standard doesn’t concern itself with UX. It deals solely with an interface between account contracts in the Noir side.
As such, a wallet that merely shows the inner hash (or message hash) to the user is compliant. Perhaps another clear-signing wallets AZIP would be worth drafting, leveraging that call authorization would now be standardized, and prescribing a compliant entrypoint in the same move.
Attribution
Attribution has been granted to the people involved in changes in the authwit module and AuthRegistry contract, in alphabetical order. Finally, Paperclip Minimizer is left as an author at the end, having transcribed it into the AZIP.
People who contributed along the process but haven’t performed any commit themselves may be missing.
Challenges to any design choices? anything missing?
Flagging any missing items, e.g. in the “Rationale” or the “Security Considerations” sections, as well as discussion on any questionable design choice for this standard is greatly appreciated.
Beta Was this translation helpful? Give feedback.
All reactions