-
Notifications
You must be signed in to change notification settings - Fork 12
Shielded AccessControl #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts
Outdated
Show resolved
Hide resolved
| * - A nullifier for the role commitment produced by SHA256(roleId | account | nonce) | ||
| * must not exist in the `_roleCommitmentNullifiers` set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sooo we need to improve the nullifier construction. The purpose of the nullifier set is to track which commitments have been used or spent without revealing which specific commitment was used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| export circuit hasRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): Role { | ||
| assert(!Utils_isContractAddress(account), "ShieldedAccessControl: contract address roles are not yet supported"); | ||
|
|
||
| const nonce = wit_secretNonce(roleId); | ||
| const index = wit_getRoleIndex(roleId, account); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the purpose of this circuit. The name hasRole implies that anyone can check if account has the roleId role, but the caller would need to have the secret nonce associated with account to calculate the commitment. Only the role grantee would be able to call this on themself then, right?
If the idea is that we're checking if the caller has the role, I'd change the circuit name to callerHasRole and remove account and just use ownPublicKey. If this is meant to check any account has any role, I don't see how this works with the secret nonce witness. Let me know if I'm missing something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely think a callerHasRole circuit is a great addition, but hasRole, in my conceptualization of this system, would be useful for admin users and I think still deserves a place in the contract. The name could probably be improved to make it clear the caller has privileged access to user info.
| type FailingCircuits = [ | ||
| method: keyof ShieldedAccessControlSimulator, | ||
| isValidNonce: boolean, | ||
| isValidIndex: boolean, | ||
| isValidPath: boolean, | ||
| args: unknown[], | ||
| ]; | ||
| const checkedCircuits: FailingCircuits[] = [ | ||
| ['assertOnlyRole', false, true, true, [DEFAULT_ADMIN_ROLE]], | ||
| ['assertOnlyRole', true, false, true, [DEFAULT_ADMIN_ROLE]], | ||
| ['assertOnlyRole', true, true, false, [DEFAULT_ADMIN_ROLE]], | ||
| ['assertOnlyRole', false, false, true, [DEFAULT_ADMIN_ROLE]], | ||
| ['assertOnlyRole', true, false, false, [DEFAULT_ADMIN_ROLE]], | ||
| ['assertOnlyRole', false, true, false, [DEFAULT_ADMIN_ROLE]], | ||
| ['assertOnlyRole', false, false, false, [DEFAULT_ADMIN_ROLE]], | ||
| ['grantRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['grantRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['grantRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['grantRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['grantRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['grantRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['grantRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', false, true, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', true, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', true, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', false, false, true, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', true, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', false, true, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ['revokeRole', false, false, false, [DEFAULT_ADMIN_ROLE, Z_ADMIN]], | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is doing too much IMO. It might be worth keeping describe blocks focused on one circuit at a time. It's a preference, but IMO it's more organized and easier to follow...or maybe have helper assertion functions if there's a lot of repetition
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This def works though if you want to keep it. Let's get everything working first and then we can laser in on the testing deets
Co-authored-by: Andrew Fleming <[email protected]> Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <[email protected]>
…elin/midnight-contracts into shielded-access-control
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @emnul, that’s a partial review — I left some comments.
- (From my experince in the client-side) Midnight’s key management has two main keys:
ZswapCoinPublicKeyandEncPublicKey. Since inside Compact we only deal withZswapCoinPublicKey, I think it’s best to explicitly call itzcpkorzpkinstead of justpk. This should help prevent any confusion for client-side developers who might otherwise mistake it forEncPublicKey. - If that makes sense, then to avoid further confusion, I’d also suggest unifying the terminology where
OwnerIdandAccountIdare currently used interchangeably. We could instead consistently usezcpkfor the raw key andshieldedZcpkfor the derived identifier (SHA256(zcpk, nonce)).
| * disclosing information about role holder. Role commitments are created with the following | ||
| * hashing scheme SHA256(roleId | account | nonce | merkleTreeIndex). | ||
| * | ||
| * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @notice Using the SHA256 hashing function comes at a significant performace cost. In the future, we | |
| * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we |
| * in the top-level contract and be unique. One way to achieve this is by | ||
| * using `export sealed ledger` hash digests that are initialized in the top-level contract: | ||
| * | ||
| * ```typescript |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * ```typescript | |
| * ```compact |
it won't highlight here anyways.
| * | ||
| * To restrict access to a circuit, use {assertOnlyRole}: | ||
| * | ||
| * ```typescript |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * ```typescript | |
| * ```compact |
| * @type {MerkleTree<10, roleCommitment>} | ||
| * @type {MerkleTree<10, Bytes<32>>} _operatorRoles | ||
| */ | ||
| export ledger _operatorRoles: MerkleTree<10, Bytes<32>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have 10 as a fixed depth for the tree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was an arbitrarily large number I thought would be sufficient for most use cases. It's not possible to dynamically determine the tree size nor would that be desirable considering the additional initialization overhead every circuit would need.
| export ledger _currentMerkleTreeIndex: Counter; | ||
|
|
||
| export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to include docs here.
| * The `id` is expected to be computed off-chain as: | ||
| * `id = SHA256(pk, nonce)` | ||
| * | ||
| * - `pk`: The owner's public key. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * - `pk`: The owner's public key. | |
| * - `zcpk`: The owner's ZswapCoinPublickey. |
| * - The type data of `account` - a ZswapCoinPublicKey or ContractAddress. | ||
| * | ||
| * @param {Bytes<32>} roleId - The role identifier. | ||
| * @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. | |
| * @param {Either<ZswapCoinPublicKey, ContractAddress>} accountId - A ZswapCoinPublicKey or ContractAddress. |
| * | ||
| * @param {Bytes<32>} roleId - The role identifier. | ||
| * @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. | ||
| * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @param {Bytes<32>} nonce - A nonce created using SHA256(SK | "role-nonce" | role | PK) |
| } | ||
|
|
||
| /** | ||
| * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. | |
| * @description Attempts to grant `roleId` to `accountId` and returns a boolean indicating if `roleId` was granted. |
| export circuit _revokeRole(roleId: Bytes<32>, accountId: Bytes<32>): Boolean { | ||
| const role = getRole(roleId, accountId); | ||
| if (!role.isApproved) { | ||
| return false; | ||
| } | ||
|
|
||
| _roleCommitmentNullifiers.insert(disclose(role.commitmentNullifier)); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curcious are we talking care of cleaning the _operatorRoles MT after this revoke to be storage efficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had thought about doing this initially, but I could not come up with a Merkle tree management solution that wasn't overly complex or did not add significant ledger storage / circuit size overhead.
Types of changes
Closes #88
This pull request introduces a Shielded AccessControl module. Roles are stored as MerkleTree commitments to avoid disclosing information regarding the roles an account may have. Role commitments are created with using the following hashing scheme SHA256 ( SHA256(PublicKey | roleIdentifier | nonce) | index ) where index is the index of the role commitment in the MerkleTree. We track this using a mapping from a role commitment to an index in the MerkleTree. A Counter tracks the next index available in the MerkleTree. The index also enforces uniqueness for all role commitments in the MerkleTree. A
Bytes<32>salt value is included in the ledger state that would allow devs to use a public pseudorandom salt value instantiated using theInitializablemodule to strengthen the underlying HKDF function. A salt value equal to length of the output of the chosen hashing function (SHA256 in our case) is recommended.The nonce is created locally using HKDF-SHA256(SK, info="role-nonce" || roleId || PK || ContractAddress, length=32) where HKDF refers to HMAC-based Extract-and-Expand Key Derivation Function. Length is set to 32 to allow us to hash the nonce in-circuit with the PublicKey and role identifier (all values must be of type Bytes<32>). Additionally, it's recommended that length of HKDF be set It's important to note that since HKDF is not verifiable on-chain we cannot enforce its use. Users may generate
noncehowever they want, but we should strongly recommend using the method described above as well as provide an implementation to ensure an attacker cannot break privacy.PR Checklist
Notes on Contract Design
Why use assertions instead of if statements in
_checkMerkleTree?We're able to reduce circuit size by about 280 rows for each circuit using assertions instead of if statements
Why use SHA256( SHA256(PublicKey | roleIdentifier | nonce) | index) as the hashing scheme for role commitments
We're able to significantly reduce circuit size by about 4000 rows for each circuit using this scheme.
Why not use an Anonymous Role-Based Access Control implementation?
One of the goals of this Shielded Access Control implementation is to keep the API consistent with the Unshielded Access Control module. An Anonymous Role-Based Access Control implementation would require an entirely different design. Please let us know if you'd like to see an Anonymous Role-Based Access Control implementation in the Github Discussions
Why does compilation take so long?
Unfortunately, Compact does not have a ZK friendly hashing function that can be used to derive state at this time, so the SHA256 hash function must be used. The usage of SHA256 incurs a large circuit size penalty on the contract which in turn slows compilation times.
Why include contract address in nonce creation scheme?
Adding the contract address improves domain separation