@openzeppelin/compact-contracts 0.4.0-alpha.6 → 0.5.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/access/AccessControl.compact +1 -1
- package/access/Ownable.compact +1 -1
- package/access/ShieldedAccessControl.compact +30 -26
- package/access/ZOwnablePK.compact +1 -1
- package/crypto/Ecdh.compact +153 -0
- package/crypto/EcdhMask.compact +103 -70
- package/crypto/Ecdsa.compact +1 -1
- package/crypto/Eip712.compact +1 -1
- package/crypto/ElGamal.compact +1 -1
- package/multisig/ForwarderPrivate.compact +1 -1
- package/multisig/ForwarderShielded.compact +1 -1
- package/multisig/ForwarderUnshielded.compact +1 -1
- package/multisig/ProposalManager.compact +1 -1
- package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
- package/multisig/ShieldedTreasury.compact +1 -1
- package/multisig/ShieldedTreasuryStateless.compact +1 -1
- package/multisig/Signer.compact +1 -1
- package/multisig/UnshieldedTreasury.compact +1 -1
- package/multisig/examples/ForwarderPrivateExample.compact +1 -1
- package/multisig/examples/ForwarderShieldedExample.compact +1 -1
- package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
- package/multisig/examples/ShieldedMultiSigV2Example.compact +1 -1
- package/multisig/presets/NativeShieldedTokenIssuer.compact +1 -1
- package/multisig/presets/ShieldedMultiSigV2.compact +1 -1
- package/package.json +1 -1
- package/security/Allowlist.compact +1 -1
- package/security/Blocklist.compact +1 -1
- package/security/Initializable.compact +1 -1
- package/security/Pausable.compact +1 -1
- package/token/ConfidentialFungibleToken.compact +1 -1
- package/token/ConfidentialNoteFungibleToken.compact +741 -0
- package/token/FungibleToken.compact +1 -1
- package/token/MultiToken.compact +1 -1
- package/token/NativeShieldedToken.compact +1 -1
- package/token/NativeShieldedTokenCore.compact +1 -1
- package/token/NativeShieldedTokenFamily.compact +1 -1
- package/token/NonFungibleToken.compact +1 -1
- package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
- package/utils/EvmAbi.compact +1 -1
- package/utils/Utils.compact +1 -1
package/access/Ownable.compact
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.5.0-alpha.1 (access/ShieldedAccessControl.compact)
|
|
3
3
|
|
|
4
4
|
pragma language_version >= 0.26.0;
|
|
5
5
|
|
|
@@ -108,8 +108,8 @@ pragma language_version >= 0.26.0;
|
|
|
108
108
|
* depends on the number of plausible `(role, accountId)` pairings. Since role identifiers
|
|
109
109
|
* are public, the anonymity set is effectively the number of plausible account IDs per role.
|
|
110
110
|
* - Outside observers will know when roles are granted and revoked.
|
|
111
|
-
* - Outside observers can infer the total number of role
|
|
112
|
-
*
|
|
111
|
+
* - Outside observers can infer the total number of distinct `(role, accountId)` pairings granted
|
|
112
|
+
* across all roles, not per-role counts.
|
|
113
113
|
* - Outside observers can link calls made by the same role instance across time.
|
|
114
114
|
* - Users can be retroactively deanonymized if their secret key is exposed.
|
|
115
115
|
* - Outside observers will NOT be able to identify the holder of any role
|
|
@@ -133,12 +133,8 @@ pragma language_version >= 0.26.0;
|
|
|
133
133
|
* - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`.
|
|
134
134
|
* Failure to do so may weaken privacy guarantees.
|
|
135
135
|
* - The `_instanceSalt` is immutable and used to differentiate deployments.
|
|
136
|
-
* - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
|
|
137
|
-
*
|
|
138
|
-
* capacity through repeated grants of the same active (role, accountId) pairing.
|
|
139
|
-
* Duplicate grants are benign from a security perspective. They produce identical
|
|
140
|
-
* commitments and a single nullifier invalidates all copies; however, they waste tree capacity.
|
|
141
|
-
* Implementing contracts are responsible for mitigating tree exhaustion risk.
|
|
136
|
+
* - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. Each distinct
|
|
137
|
+
* `(role, accountId)` pairing consumes one slot; re-granting an active pairing is a no-op.
|
|
142
138
|
* - A single `secretKey` compromise exposes all roles held by that user within the contract
|
|
143
139
|
* instance, since all roles share the same `accountId`. Users requiring compartmentalization
|
|
144
140
|
* between roles should use separate keys per role and accept the additional key management overhead.
|
|
@@ -155,7 +151,8 @@ pragma language_version >= 0.26.0;
|
|
|
155
151
|
* - The Merkle tree capacity of 2^20 leaf slots is a permanent, irrecoverable resource.
|
|
156
152
|
* There is no mechanism to reclaim slots after revocation, no upgrade path, and no emergency
|
|
157
153
|
* recovery. Once exhausted, the contract is permanently unable to grant new roles. Deployers
|
|
158
|
-
* must monitor slot consumption off-chain and plan capacity accordingly.
|
|
154
|
+
* must monitor slot consumption off-chain and plan capacity accordingly. The exported tree
|
|
155
|
+
* reports occupancy via `firstFree()` (TypeScript) and `isFull()` (circuit).
|
|
159
156
|
*
|
|
160
157
|
* @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we
|
|
161
158
|
* plan on migrating to a ZK-friendly hashing function when an implementation is available.
|
|
@@ -213,6 +210,13 @@ module ShieldedAccessControl {
|
|
|
213
210
|
*/
|
|
214
211
|
export ledger _roleCommitmentNullifiers: Set<RoleNullifier>;
|
|
215
212
|
|
|
213
|
+
/**
|
|
214
|
+
* @description Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so
|
|
215
|
+
* one pairing can never consume more than one `_operatorRoles` slot.
|
|
216
|
+
* @type {Set<RoleNullifier>} _grantedRoleNullifiers
|
|
217
|
+
*/
|
|
218
|
+
export ledger _grantedRoleNullifiers: Set<RoleNullifier>;
|
|
219
|
+
|
|
216
220
|
/**
|
|
217
221
|
* @sealed @ledger _instanceSalt
|
|
218
222
|
* @description A per-instance value provided at initialization used to namespace
|
|
@@ -395,12 +399,11 @@ module ShieldedAccessControl {
|
|
|
395
399
|
|
|
396
400
|
/**
|
|
397
401
|
* @description Grants `role` to `accountId` by inserting a role commitment unique to the
|
|
398
|
-
* `(role, accountId)` pairing into the `_operatorRoles` Merkle tree.
|
|
399
|
-
*
|
|
400
|
-
* limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
|
|
402
|
+
* `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active
|
|
403
|
+
* pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new
|
|
401
404
|
* `accountId` must be generated to be re-authorized for a revoked `role`.
|
|
402
405
|
*
|
|
403
|
-
* @constraints k=15, rows=
|
|
406
|
+
* @constraints k=15, rows=29071
|
|
404
407
|
*
|
|
405
408
|
* Requirements:
|
|
406
409
|
*
|
|
@@ -429,9 +432,8 @@ module ShieldedAccessControl {
|
|
|
429
432
|
|
|
430
433
|
/**
|
|
431
434
|
* @description Grants `role` to `accountId` by inserting a role commitment unique to the
|
|
432
|
-
* `(role, accountId)` pairing into the `_operatorRoles` Merkle tree.
|
|
433
|
-
*
|
|
434
|
-
* limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
|
|
435
|
+
* `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active
|
|
436
|
+
* pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new
|
|
435
437
|
* `accountId` must be generated to be re-authorized for a revoked `role`.
|
|
436
438
|
*
|
|
437
439
|
* Internal circuit without access restriction.
|
|
@@ -439,12 +441,10 @@ module ShieldedAccessControl {
|
|
|
439
441
|
* @warning Exposing this circuit directly in an implementing contract would allow anyone to grant
|
|
440
442
|
* roles without authorization. It must be wrapped with appropriate access control.
|
|
441
443
|
*
|
|
442
|
-
* @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots
|
|
443
|
-
* Deployers should monitor slot consumption off-chain.
|
|
444
|
-
* but are otherwise benign. A single nullifier invalidates all duplicate commitments.
|
|
445
|
-
* Implementing contracts are responsible for mitigating tree exhaustion risk.
|
|
444
|
+
* @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per
|
|
445
|
+
* distinct pairing. Deployers should monitor slot consumption off-chain.
|
|
446
446
|
*
|
|
447
|
-
* @constraints k=14, rows=
|
|
447
|
+
* @constraints k=14, rows=12326
|
|
448
448
|
*
|
|
449
449
|
* Requirements:
|
|
450
450
|
*
|
|
@@ -573,13 +573,14 @@ module ShieldedAccessControl {
|
|
|
573
573
|
/**
|
|
574
574
|
* @description Core business logic for the grant/revoke role circuits. Asserts that the
|
|
575
575
|
* `(role, accountId)` pairing has not already been revoked. On success, dispatches on
|
|
576
|
-
* `updateType`: a `Grant` inserts the role commitment into `_operatorRoles
|
|
577
|
-
* a `Revoke` inserts the nullifier
|
|
576
|
+
* `updateType`: a `Grant` inserts the role commitment into `_operatorRoles` unless the
|
|
577
|
+
* nullifier is already in `_grantedRoleNullifiers`, and a `Revoke` inserts the nullifier
|
|
578
|
+
* into `_roleCommitmentNullifiers`.
|
|
578
579
|
*
|
|
579
580
|
* Disclosures:
|
|
580
581
|
*
|
|
581
582
|
* - A nullifier for the respective role commitment.
|
|
582
|
-
* - A role commitment (on Grant only).
|
|
583
|
+
* - A role commitment (on first Grant only).
|
|
583
584
|
*
|
|
584
585
|
* @notice The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call,
|
|
585
586
|
* regardless of the update type. Since `Set.member` publicly reveals its argument, the
|
|
@@ -603,7 +604,10 @@ module ShieldedAccessControl {
|
|
|
603
604
|
assert(!isRevoked, "ShieldedAccessControl: role is already revoked");
|
|
604
605
|
|
|
605
606
|
if (updateType == UpdateType.Grant) {
|
|
606
|
-
|
|
607
|
+
if (!_grantedRoleNullifiers.member(disclose(roleNullifier))) {
|
|
608
|
+
_grantedRoleNullifiers.insert(disclose(roleNullifier));
|
|
609
|
+
_operatorRoles.insert(disclose(roleCommitment));
|
|
610
|
+
}
|
|
607
611
|
} else {
|
|
608
612
|
_roleCommitmentNullifiers.insert(disclose(roleNullifier));
|
|
609
613
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/Ecdh.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.26.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module Ecdh
|
|
8
|
+
* @description Stateless Diffie-Hellman key agreement over Jubjub. Splits into
|
|
9
|
+
* the two halves of one exchange: `deriveShared` runs the sender side and
|
|
10
|
+
* returns the ephemeral public key to publish alongside the shared secret, and
|
|
11
|
+
* `recoverShared` runs the recipient side from that published point. The module
|
|
12
|
+
* agrees on a point and nothing more: deriving keys, masks or nonces from it
|
|
13
|
+
* belongs to the consumer (`crypto/EcdhMask` for hybrid encryption).
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.secg.org/sec1-v2.pdf#page=33 SEC 1 v2.0 §3.3.1, Elliptic Curve Diffie-Hellman Primitive
|
|
16
|
+
* @see https://www.secg.org/sec1-v2.pdf#page=30 SEC 1 v2.0 §3.2.2.1, Elliptic Curve Public Key Validation Primitive
|
|
17
|
+
* @see https://www.secg.org/sec1-v2.pdf#page=29 SEC 1 v2.0 §3.2.1, Elliptic Curve Key Pair Generation Primitive
|
|
18
|
+
*
|
|
19
|
+
* @dev Construction and notation. SEC 1's symbol follows in brackets.
|
|
20
|
+
* - `g`: the Jubjub prime-order subgroup generator, of order `ℓ` [`G`, `n`].
|
|
21
|
+
* - `ek`: the recipient's secret scalar in `[1, ℓ-1]` [`d`, §3.2.1].
|
|
22
|
+
* - `e`: the sender's ephemeral scalar in `[1, ℓ-1]`, fresh per call [`d`, §3.2.1].
|
|
23
|
+
* - `pk = g^ek`: the recipient's public key [`Q`, §3.2.1].
|
|
24
|
+
* - `E = g^e`: the ephemeral public key, published [`Q`, §3.2.1].
|
|
25
|
+
* - `S = pk^e = g^{ek*e} = E^ek`: the shared secret point, never published
|
|
26
|
+
* [`P = d * Q`, §3.3.1]. §3.3.1 outputs `z = x_P`; this module returns `P`
|
|
27
|
+
* itself and leaves the KDF to the consumer.
|
|
28
|
+
* - `identity`: the point at infinity, `ecMulGenerator(0)` [`O`]. §3.2.2.1
|
|
29
|
+
* action 1 rejects it as a key; §3.3.1 action 2 rejects it as `P`.
|
|
30
|
+
* `deriveShared` does the first; the second cannot fire in a prime-order
|
|
31
|
+
* group once the first holds and `e ≢ 0 (mod ℓ)`.
|
|
32
|
+
* - Jubjub only. A second curve gets its own module with the same circuit names.
|
|
33
|
+
*
|
|
34
|
+
* @dev Future APIs.
|
|
35
|
+
* - `deriveEphemeral(seed: Bytes<32>, nonce: Field): JubjubScalar` — a canonical
|
|
36
|
+
* `e` for consumers, making the uniqueness half of the freshness rule true by
|
|
37
|
+
* construction rather than a precondition.
|
|
38
|
+
* - `assertValidPk(pk)` / `isIdentity(pk): Boolean` — reject an identity key
|
|
39
|
+
* where it is registered. The `deriveShared` guard fires on the sender's
|
|
40
|
+
* transaction, so a bad key costs the sender, not the registrant.
|
|
41
|
+
* - Jubjub key-pair derivation lives in `crypto/ElGamal` (`derivePk`,
|
|
42
|
+
* `secretToScalar`); a shared key module is its natural home.
|
|
43
|
+
*
|
|
44
|
+
* @dev Freshness and secrecy precondition (CRITICAL). `e` MUST be both unique per
|
|
45
|
+
* call to the same recipient AND secret (unpredictable to observers), even across
|
|
46
|
+
* circuit invocations that reuse a randomness seed.
|
|
47
|
+
*
|
|
48
|
+
* Uniqueness: a repeated `e` to one recipient repeats `S`, so every key, mask or
|
|
49
|
+
* nonce a consumer derives from `S` repeats too. Where that derivation is a
|
|
50
|
+
* one-time pad (`crypto/EcdhMask`) the reuse is catastrophic: subtracting two
|
|
51
|
+
* ciphertexts reveals the plaintext difference, and either plaintext once the
|
|
52
|
+
* other is known or constrained.
|
|
53
|
+
*
|
|
54
|
+
* Secrecy: `recipientPk` is public, so anyone who learns `e` recomputes
|
|
55
|
+
* `S = recipientPk^e` and everything derived from it. A unique but PREDICTABLE
|
|
56
|
+
* `e` (a plain counter, a public nonce) therefore breaks confidentiality
|
|
57
|
+
* completely. `e` must be high-entropy and secret, exactly like an ElGamal
|
|
58
|
+
* encryption randomness (see `crypto/ElGamal`), not merely distinct.
|
|
59
|
+
*
|
|
60
|
+
* The caller supplies `e` and owns both properties, e.g. by expanding a secret
|
|
61
|
+
* per-invocation seed bound to a per-recipient-unique nonce (a monotonic index).
|
|
62
|
+
* This is stronger than exponential ElGamal's uniqueness requirement (there,
|
|
63
|
+
* reuse merely leaks plaintext equality); do not treat `e` as "any randomness."
|
|
64
|
+
*
|
|
65
|
+
* @dev Weak inputs. Two degenerate inputs collapse `S` to the identity point,
|
|
66
|
+
* which every observer can compute: `recipientPk = identity` gives `S = identity`
|
|
67
|
+
* for any `e`, and `e = 0` gives `S = identity` for any key. Anything a consumer
|
|
68
|
+
* derives from such an `S` is public. The identity is a valid subgroup point, so
|
|
69
|
+
* the runtime subgroup guarantee does NOT catch it; `deriveShared` therefore
|
|
70
|
+
* asserts `recipientPk != identity` and `ephemeralPk (= g^e) != identity`, and
|
|
71
|
+
* every sender path reaches both through it. Guarding the ephemeral POINT rather
|
|
72
|
+
* than the scalar subsumes `e != 0` and stays correct regardless of how the
|
|
73
|
+
* runtime treats an out-of-range scalar: `g^e = identity` captures both `e = 0`
|
|
74
|
+
* and any `e ≡ 0 (mod ℓ)`. (Guarding `e != 0` directly would instead lean on the
|
|
75
|
+
* runtime faulting `ecMul` for scalars `≥ ℓ`; the point guard removes that
|
|
76
|
+
* dependency.) These are the only weak inputs (for a subgroup key,
|
|
77
|
+
* `S = identity` iff the ephemeral point is the identity).
|
|
78
|
+
*
|
|
79
|
+
* @dev Subgroup membership. Secrecy of `S` relies on the runtime constraining
|
|
80
|
+
* every `JubjubPoint` into the prime-order subgroup (argued in `crypto/ElGamal`),
|
|
81
|
+
* so a caller cannot hand in a low-order point that leaks `ekScalar`.
|
|
82
|
+
*
|
|
83
|
+
* @dev No ledger state, no witnesses. Both circuits are pure and take their keys
|
|
84
|
+
* and randomness as explicit arguments. `recoverShared` is exposed for
|
|
85
|
+
* wallet/off-chain use (recovering a received value needs no proof); the on-chain
|
|
86
|
+
* path only ever calls `deriveShared`.
|
|
87
|
+
*/
|
|
88
|
+
module Ecdh {
|
|
89
|
+
import CompactStandardLibrary;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @description One sender-side ECDH step: the ephemeral public key to publish
|
|
93
|
+
* and the shared secret point to derive from.
|
|
94
|
+
*
|
|
95
|
+
* @notice `sShared` is secret. Keep it a circuit local; disclosing it opens
|
|
96
|
+
* everything derived from it.
|
|
97
|
+
*/
|
|
98
|
+
export struct SharedSecret {
|
|
99
|
+
/** `E = g^e`, public. The recipient recovers `S = E^ek` from it. */
|
|
100
|
+
ephemeralPk: JubjubPoint;
|
|
101
|
+
|
|
102
|
+
/** `S = pk^e`, secret. Never disclose; derive from it in-circuit. */
|
|
103
|
+
sShared: JubjubPoint;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @description Runs the sender's ECDH step: the ephemeral public key `g^e` to
|
|
108
|
+
* publish and the shared secret `pk^e` to derive from. The single site of the
|
|
109
|
+
* weak-input guards.
|
|
110
|
+
*
|
|
111
|
+
* @notice `e` MUST be fresh per call and secret (see the module freshness
|
|
112
|
+
* precondition).
|
|
113
|
+
*
|
|
114
|
+
* @constraints k=11, rows=1385
|
|
115
|
+
*
|
|
116
|
+
* Requirements:
|
|
117
|
+
*
|
|
118
|
+
* - `recipientPk` is not the identity point.
|
|
119
|
+
* - `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call.
|
|
120
|
+
*
|
|
121
|
+
* @param recipientPk - The recipient's Jubjub public key (`g^ek`).
|
|
122
|
+
* @param e - A fresh ephemeral scalar.
|
|
123
|
+
* @return `{ ephemeralPk = g^e, sShared = recipientPk^e }`.
|
|
124
|
+
*/
|
|
125
|
+
export pure circuit deriveShared(recipientPk: JubjubPoint, e: JubjubScalar): SharedSecret {
|
|
126
|
+
const identity = ecMulGenerator(0 as JubjubScalar);
|
|
127
|
+
// An identity key gives S = identity for every `e`.
|
|
128
|
+
assert(recipientPk != identity, "Ecdh: identity pk");
|
|
129
|
+
const ephemeralPk = ecMulGenerator(e);
|
|
130
|
+
// Guarding the POINT subsumes `e == 0` and any `e ≡ 0 (mod ℓ)` (see the
|
|
131
|
+
// weak-inputs note).
|
|
132
|
+
assert(ephemeralPk != identity, "Ecdh: zero ephemeral");
|
|
133
|
+
return SharedSecret { ephemeralPk: ephemeralPk, sShared: ecMul(recipientPk, e) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @description The recipient's half of `deriveShared`: recomputes `S = E^ek`
|
|
138
|
+
* from a published ephemeral public key.
|
|
139
|
+
*
|
|
140
|
+
* @dev Never asserts. A wrong `ekScalar` yields an unrelated point, so a wallet
|
|
141
|
+
* scanning exchanges not addressed to it learns nothing from this circuit and
|
|
142
|
+
* the module exposes no key-correctness oracle.
|
|
143
|
+
*
|
|
144
|
+
* @constraints k=9, rows=440
|
|
145
|
+
*
|
|
146
|
+
* @param ephemeralPk - The sender's published `g^e`.
|
|
147
|
+
* @param ekScalar - The recipient's secret scalar.
|
|
148
|
+
* @return The ECDH shared secret point.
|
|
149
|
+
*/
|
|
150
|
+
export pure circuit recoverShared(ephemeralPk: JubjubPoint, ekScalar: JubjubScalar): JubjubPoint {
|
|
151
|
+
return ecMul(ephemeralPk, ekScalar);
|
|
152
|
+
}
|
|
153
|
+
}
|
package/crypto/EcdhMask.compact
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/EcdhMask.compact)
|
|
3
3
|
|
|
4
4
|
pragma language_version >= 0.26.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module EcdhMask
|
|
8
8
|
* @description Stateless ECDH hybrid encryption of a single field element to a
|
|
9
|
-
* Jubjub public key
|
|
10
|
-
*
|
|
11
|
-
* discrete-log search and the value is
|
|
12
|
-
* direct-decrypt counterpart to the
|
|
13
|
-
*
|
|
9
|
+
* Jubjub public key, built on the key agreement in `crypto/Ecdh`. Delivers a
|
|
10
|
+
* value to whoever holds the secret behind a Jubjub public key WITHOUT encoding
|
|
11
|
+
* it in the exponent, so recovery needs no discrete-log search and the value is
|
|
12
|
+
* unbounded within `Uint<128>`. This is the direct-decrypt counterpart to the
|
|
13
|
+
* homomorphic `crypto/ElGamal`: use ElGamal to accumulate ciphertexts, use this
|
|
14
|
+
* to hand a recipient the plaintext.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* @see https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_6.pdf#page=461 Boneh and Shoup, A Graduate Course in Applied Cryptography v0.6, §11.5 ElGamal encryption (E_EG), Remark 11.1 (E'_EG), Theorem 11.4
|
|
17
|
+
* @see https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_6.pdf#page=495 Boneh and Shoup, §12.4 CCA-secure ElGamal encryption, Theorem 12.4
|
|
18
|
+
* @see https://www.shoup.net/papers/twindh.pdf Cash, Kiltz and Shoup, The Twin Diffie-Hellman Problem and Applications, §1.1 Hashed ElGamal Encryption
|
|
19
|
+
* @see https://cseweb.ucsd.edu/~mihir/papers/dhies.pdf Abdalla, Bellare and Rogaway, DHIES, §8 ODH and SDH
|
|
20
|
+
* @see https://www.secg.org/sec1-v2.pdf#page=58 SEC 1 v2.0 §5.1.3, Encryption Operation
|
|
21
|
+
* @see https://www.secg.org/sec1-v2.pdf#page=59 SEC 1 v2.0 §5.1.4, Decryption Operation, action 3
|
|
22
|
+
*
|
|
23
|
+
* @dev Construction and notation. Boneh and Shoup's E'_EG symbol follows in
|
|
24
|
+
* brackets (§11.5, Remark 11.1).
|
|
25
|
+
* - `pk = g^ek`: the recipient's public key [`u = g^α`].
|
|
26
|
+
* - `e`: the ephemeral scalar, fresh per call [`β`].
|
|
27
|
+
* - `E = g^e`: the ephemeral public key [`v = g^β`].
|
|
28
|
+
* - `S = pk^e = g^{ek*e}`: the shared point, from `crypto/Ecdh` [`w = u^β`].
|
|
29
|
+
* - `mask = KDF(S, domain)`: the domain-separated field mask, see `kdf`
|
|
30
|
+
* [`k = H(w)`]. E_EG hashes `(v, w)`; E'_EG hashes `w` alone, as here.
|
|
31
|
+
* - `ct = value + mask`: the field one-time pad [`c = E_s(k, m)`].
|
|
32
|
+
* - `Ciphertext { ephemeralPk: E, ct }`: the ciphertext [`(v, c)`].
|
|
33
|
+
* - Recipient recovers `S = E^ek`, `mask = KDF(S, domain)`, `value = ct - mask`.
|
|
34
|
+
*
|
|
35
|
+
* @dev Key agreement. `deriveShared`, `recoverShared` and the `SharedSecret`
|
|
36
|
+
* struct live in `crypto/Ecdh` and are not re-exported here; a consumer that
|
|
37
|
+
* needs them imports that module directly. It owns the weak-input guards every
|
|
38
|
+
* sender path here inherits.
|
|
21
39
|
*
|
|
22
40
|
* @dev Freshness and secrecy precondition (CRITICAL). `e` MUST be both unique per
|
|
23
41
|
* call to the same recipient AND secret (unpredictable to observers), even across
|
|
@@ -25,7 +43,8 @@ pragma language_version >= 0.26.0;
|
|
|
25
43
|
*
|
|
26
44
|
* Uniqueness: the mask is a one-time pad, so a repeated `e` repeats `S` and
|
|
27
45
|
* `mask`, and reusing a pad is catastrophic (subtracting two ciphertexts recovers
|
|
28
|
-
* both values).
|
|
46
|
+
* both values). Padding two values under the same `(S, domain)` reuses the pad
|
|
47
|
+
* the same way (see `kdf`).
|
|
29
48
|
*
|
|
30
49
|
* Secrecy: `recipientPk` is public, so anyone who learns `e` recomputes
|
|
31
50
|
* `S = recipientPk^e`, hence the mask, and opens the ciphertext. A unique but
|
|
@@ -38,19 +57,11 @@ pragma language_version >= 0.26.0;
|
|
|
38
57
|
* This is stronger than exponential ElGamal's uniqueness requirement (there,
|
|
39
58
|
* reuse merely leaks plaintext equality); do not treat `e` as "any randomness."
|
|
40
59
|
*
|
|
41
|
-
* @dev Weak inputs.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* catch it; `encrypt` therefore asserts `recipientPk != identity` and
|
|
47
|
-
* `ephemeralPk (= g^e) != identity`. Guarding the ephemeral POINT rather than the
|
|
48
|
-
* scalar subsumes `e != 0` and stays correct regardless of how the runtime treats
|
|
49
|
-
* an out-of-range scalar: `g^e = identity` captures both `e = 0` and any
|
|
50
|
-
* `e ≡ 0 (mod ℓ)`. (Guarding `e != 0` directly would instead lean on the runtime
|
|
51
|
-
* faulting `ecMul` for scalars `≥ ℓ`; the point guard removes that dependency.)
|
|
52
|
-
* These are the only weak inputs (for a subgroup key, `S = identity` iff the
|
|
53
|
-
* ephemeral point is the identity).
|
|
60
|
+
* @dev Weak inputs. An identity `recipientPk` or a zero `e` collapses `S` to the
|
|
61
|
+
* identity point, giving `mask = KDF(identity, domain)`, a value any observer can
|
|
62
|
+
* recompute, so `ct = value + mask` opens trivially. `crypto/Ecdh.deriveShared`
|
|
63
|
+
* rejects both, and `encrypt` reaches them only through it; that module's
|
|
64
|
+
* weak-inputs note carries the full argument.
|
|
54
65
|
*
|
|
55
66
|
* @dev Integrity. The pad is malleable and NO authentication is added here. This
|
|
56
67
|
* is safe in the intended on-chain use, where the ciphertext is computed
|
|
@@ -60,10 +71,13 @@ pragma language_version >= 0.26.0;
|
|
|
60
71
|
* enable theft. Consumers with a different threat model (mutable storage,
|
|
61
72
|
* in-transit tampering) MUST add their own authentication.
|
|
62
73
|
*
|
|
63
|
-
* @dev Security. This is hashed ElGamal
|
|
64
|
-
*
|
|
65
|
-
* the
|
|
66
|
-
*
|
|
74
|
+
* @dev Security. This is hashed ElGamal over Jubjub: Boneh and Shoup, *A
|
|
75
|
+
* Graduate Course in Applied Cryptography* v0.6, §11.5, in the Remark 11.1
|
|
76
|
+
* form that keys the KDF on the shared point alone. It inherits that scheme's
|
|
77
|
+
* guarantee: IND-CPA under CDH in the random-oracle model (Theorem 11.4, the
|
|
78
|
+
* `persistentHash` KDF keyed by the secret point `S`), and so also under the
|
|
79
|
+
* stronger Oracle Diffie-Hellman assumption. It is NOT IND-CCA: Theorem 12.4
|
|
80
|
+
* needs a MAC, and the additive pad is malleable (see Integrity), which
|
|
67
81
|
* is acceptable here only because the on-chain ciphertext is bound in-circuit to
|
|
68
82
|
* the value it commits to elsewhere. Hiding carries a large margin: `kdf` returns
|
|
69
83
|
* a `degradeToTransient` output in `[0, 2^248)` while `value < 2^128`, so even
|
|
@@ -72,9 +86,10 @@ pragma language_version >= 0.26.0;
|
|
|
72
86
|
* is PRF-like. Confidentiality relies on the runtime constraining every
|
|
73
87
|
* `JubjubPoint` into the prime-order subgroup (see `crypto/ElGamal`), so `S`
|
|
74
88
|
* cannot be a low-order point that leaks `ek`. Binding the ephemeral `E` into the
|
|
75
|
-
* KDF is deliberately omitted: for a fixed `recipientPk`, `E = g^e`
|
|
76
|
-
* `S = pk^e` are both determined by `e`, so `KDF(S)` already varies with `E
|
|
77
|
-
* folding it in
|
|
89
|
+
* KDF is deliberately omitted (Remark 11.1): for a fixed `recipientPk`, `E = g^e`
|
|
90
|
+
* and `S = pk^e` are both determined by `e`, so `KDF(S)` already varies with `E`;
|
|
91
|
+
* folding it in only tightens the CCA reduction (Remark 12.2)
|
|
92
|
+
* which this module does not claim.
|
|
78
93
|
*
|
|
79
94
|
* @dev Shared key with `crypto/ElGamal` (in scope). A consumer MAY use the same
|
|
80
95
|
* recipient Jubjub key for both these memos (hashed ElGamal) and exponential-
|
|
@@ -82,6 +97,9 @@ pragma language_version >= 0.26.0;
|
|
|
82
97
|
* safe: the two schemes share only the public key, this KDF is domain-separated
|
|
83
98
|
* and keyed by the secret DH point, and neither exposes a decryption oracle, so
|
|
84
99
|
* under CPA / ODH a ciphertext of one scheme does not help attack the other.
|
|
100
|
+
* The per-encryption scalars MUST be independent (e.g. distinct
|
|
101
|
+
* `expandRandomness` tags over one seed): a shared scalar shares `S`, so the
|
|
102
|
+
* memo becomes a plaintext-check oracle for the ElGamal ciphertext.
|
|
85
103
|
*
|
|
86
104
|
* @dev No ledger state, no witnesses. Every circuit is pure and takes its keys,
|
|
87
105
|
* value, and randomness as explicit arguments. `decrypt` is exposed as a pure
|
|
@@ -90,37 +108,22 @@ pragma language_version >= 0.26.0;
|
|
|
90
108
|
*/
|
|
91
109
|
module EcdhMask {
|
|
92
110
|
import CompactStandardLibrary;
|
|
111
|
+
import "./Ecdh" prefix Ecdh_;
|
|
93
112
|
|
|
94
113
|
/**
|
|
95
|
-
* @description An ECDH one-time-pad ciphertext
|
|
96
|
-
*
|
|
114
|
+
* @description An ECDH one-time-pad ciphertext, `(v, c)` in Boneh and Shoup.
|
|
115
|
+
* Public once written; neither field is secret on its own.
|
|
116
|
+
*
|
|
117
|
+
* @notice `ephemeralPk` is never the identity for a ciphertext `encrypt`
|
|
118
|
+
* produced, and `decrypt` rejects one that is. A consumer that hand-builds a
|
|
119
|
+
* placeholder with an identity `ephemeralPk` must document it as a sentinel.
|
|
97
120
|
*/
|
|
98
121
|
export struct Ciphertext {
|
|
122
|
+
/** `E = g^e`, the sender's ephemeral public key. Never the identity. */
|
|
99
123
|
ephemeralPk: JubjubPoint;
|
|
100
|
-
ct: Field;
|
|
101
|
-
}
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* domain-separated by a caller-supplied `domain` tag. Hashes the point to
|
|
106
|
-
* `Bytes<32>`, then re-hashes it with the domain tag and truncates into the
|
|
107
|
-
* field via `degradeToTransient`. The consumer chooses `domain` (so this
|
|
108
|
-
* module is not tied to any one protocol) and MUST use the same value on
|
|
109
|
-
* encrypt and decrypt.
|
|
110
|
-
*
|
|
111
|
-
* @dev Uses `persistentHash` deliberately: the recipient reproduces this mask
|
|
112
|
-
* to decrypt, possibly across a platform upgrade, so it must be upgrade-stable.
|
|
113
|
-
*
|
|
114
|
-
* @param sShared - The ECDH shared secret point (`pk^e` for the sender,
|
|
115
|
-
* `E^ek` for the recipient; equal by construction).
|
|
116
|
-
* @param domain - The consumer's domain-separation tag.
|
|
117
|
-
* @return The field mask.
|
|
118
|
-
*/
|
|
119
|
-
export pure circuit kdf(sShared: JubjubPoint, domain: Bytes<32>): Field {
|
|
120
|
-
const pointHash = persistentHash<JubjubPoint>(sShared);
|
|
121
|
-
return degradeToTransient(
|
|
122
|
-
persistentHash<Vector<2, Bytes<32>>>([pointHash, domain])
|
|
123
|
-
);
|
|
125
|
+
/** `value + KDF(S, domain)`, the field one-time pad over the value. */
|
|
126
|
+
ct: Field;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
@@ -128,6 +131,8 @@ module EcdhMask {
|
|
|
128
131
|
*
|
|
129
132
|
* @notice `e` MUST be fresh per call (see the module freshness precondition).
|
|
130
133
|
*
|
|
134
|
+
* @constraints k=14, rows=9269
|
|
135
|
+
*
|
|
131
136
|
* Requirements:
|
|
132
137
|
*
|
|
133
138
|
* - `recipientPk` is not the identity point.
|
|
@@ -145,23 +150,47 @@ module EcdhMask {
|
|
|
145
150
|
e: JubjubScalar,
|
|
146
151
|
domain: Bytes<32>
|
|
147
152
|
): Ciphertext {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
// The weak-input guards come from `deriveShared`, not repeated here.
|
|
154
|
+
const shared = Ecdh_deriveShared(recipientPk, e);
|
|
155
|
+
const mask = kdf(shared.sShared, domain);
|
|
156
|
+
return Ciphertext { ephemeralPk: shared.ephemeralPk, ct: (value as Field) + mask };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @description Derives the field mask from the ECDH shared secret point and
|
|
161
|
+
* a caller-supplied `domain` tag: hashes the point to `Bytes<32>`, re-hashes
|
|
162
|
+
* it with the tag, and truncates into the field via `degradeToTransient`.
|
|
163
|
+
* The mask is a pure function of `(sShared, domain)`, so the pair MUST match
|
|
164
|
+
* on encrypt and decrypt, and MUST be distinct for every value it pads. Two
|
|
165
|
+
* values padded under one pair share the pad and leak their difference.
|
|
166
|
+
*
|
|
167
|
+
* @dev Uses `persistentHash` deliberately: the recipient reproduces this mask
|
|
168
|
+
* to decrypt, possibly across a platform upgrade, so it must be upgrade-stable.
|
|
169
|
+
*
|
|
170
|
+
* @constraints k=13, rows=7917
|
|
171
|
+
*
|
|
172
|
+
* @param sShared - The ECDH shared secret point (`pk^e` for the sender,
|
|
173
|
+
* `E^ek` for the recipient; equal by construction).
|
|
174
|
+
* @param domain - The consumer's domain-separation tag.
|
|
175
|
+
* @return The field mask.
|
|
176
|
+
*/
|
|
177
|
+
export pure circuit kdf(sShared: JubjubPoint, domain: Bytes<32>): Field {
|
|
178
|
+
const pointHash = persistentHash<JubjubPoint>(sShared);
|
|
179
|
+
return degradeToTransient(
|
|
180
|
+
persistentHash<Vector<2, Bytes<32>>>([pointHash, domain])
|
|
181
|
+
);
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
/**
|
|
162
185
|
* @description Recovers the value from a ciphertext using the recipient's
|
|
163
186
|
* secret scalar. Pure and off-chain (no proof needed to read a received value).
|
|
164
187
|
*
|
|
188
|
+
* @notice Rejects a ciphertext whose `ephemeralPk` is the identity point
|
|
189
|
+
* (SEC 1 §5.1.4, action 3). `encrypt` cannot produce one, so a wallet that
|
|
190
|
+
* meets one holds a placeholder, not an encryption.
|
|
191
|
+
*
|
|
192
|
+
* @constraints k=14, rows=8976
|
|
193
|
+
*
|
|
165
194
|
* @param ciphertext - The ciphertext to decrypt.
|
|
166
195
|
* @param ekScalar - The recipient's secret scalar (`crypto/ElGamal`'s
|
|
167
196
|
* `secretToScalar(EK)`).
|
|
@@ -170,7 +199,11 @@ module EcdhMask {
|
|
|
170
199
|
* which is `< 2^128`).
|
|
171
200
|
*/
|
|
172
201
|
export pure circuit decrypt(ciphertext: Ciphertext, ekScalar: JubjubScalar, domain: Bytes<32>): Field {
|
|
173
|
-
|
|
202
|
+
// `encrypt` never emits an identity ephemeral; one here is a malformed
|
|
203
|
+
// ciphertext whose mask is a public constant, so refuse rather than decrypt.
|
|
204
|
+
assert(ciphertext.ephemeralPk != ecMulGenerator(0 as JubjubScalar),
|
|
205
|
+
"EcdhMask: identity ephemeral");
|
|
206
|
+
const sShared = Ecdh_recoverShared(ciphertext.ephemeralPk, ekScalar);
|
|
174
207
|
const mask = kdf(sShared, domain);
|
|
175
208
|
return ciphertext.ct - mask;
|
|
176
209
|
}
|
package/crypto/Ecdsa.compact
CHANGED
package/crypto/Eip712.compact
CHANGED
package/crypto/ElGamal.compact
CHANGED