Skip to content

Commit facb255

Browse files
committed
feat(ERTP): enforce input validation in ERTP/AmountMath
BREAKING CHANGE: NatValues now only accept bigints, lower-case amountMath is removed, and AmountMath methods always follow the order of: brand, value
1 parent 4a68ee0 commit facb255

File tree

144 files changed

+3041
-2499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+3041
-2499
lines changed
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Input Validation for ERTP - A Walkthrough
2+
3+
## Requirements
4+
5+
We want ERTP to be robust against malicious code in the same vat, so the ERTP must be robust without the kind of checks that we get for free in intervat communication going through `@agoric/marshal` and `@agoric/swingset-vat`. In other words, the only tools at our disposal are being in a Hardened JS environment, and helpers that we import and call directly, such as `passStyleOf` from `@agoric/marshal`.
6+
7+
## Malicious Behavior
8+
9+
We identify two personas: the creator of an issuerKit, and a user of the asset type. A user is someone who holds a purse or payment of that brand, or describes something in terms of an amount of that brand. A creator might be vulnerable to an attack by users and vice versa. Users might be vulnerable to other users.
10+
11+
The malicious behavior we would like to prevent is:
12+
13+
1. Users stealing funds from other users
14+
2. Users minting who do not have access to the mint
15+
3. The creator of an issuerKit revoking assets from the holders of purses and payments
16+
17+
## Entry points in ERTP
18+
19+
There are three entry points in ERTP:
20+
1. packages/ERTP/src/amountMath.js
21+
2. packages/ERTP/src/issuerKit.js
22+
3. packages/ERTP/src/typeGuards.js
23+
24+
This document will only analyze `AmountMath.coerce`, but the
25+
methodology for validating inputs remains the same for the ERTP functions.
26+
27+
`AmountMath.coerce` takes a `brand` and an `amount` and returns a new `amount`, but throws if the amount does not match the brand, or if the amount is not a valid amount generally.
28+
29+
For `coerce` to work according to spec, if the user passes in an invalid brand or an invalid amount, we must throw.
30+
31+
## Valid amounts
32+
33+
A valid amount is an object record with two properties: `brand` and
34+
`value`. `brand` is a Remotable with a few methods, such as
35+
`isMyIssuer`, but for the purposes of a valid amount, we allow any
36+
Remotable and do not call any of these methods (we would have to call
37+
them asynchronously in order to check them, since the `brand` might be
38+
a remote object, so we do not check them.)
39+
40+
The value is either a bigint Nat in the case of AssetKind.NAT (we formerly allowed numbers for backwards compatibility, but now only bigints are allowed) or an array of Structures in the case of AssetKind.SET. The array of Structures might include any combination of other Structures, “undefined”, “null”, booleans, symbols, strings, pass-by-copy records, pass-by-copy arrays, numbers, bigints, Remotables, and errors. Importantly, a Structure cannot contain promises.
41+
42+
## Invalid Amounts
43+
44+
There are a number of ways in which amounts can be invalid:
45+
46+
### Not an object
47+
48+
* **The danger**: we’ll get unexpected failures later on in our use of the amount rather than failing fast at the start as intended.
49+
50+
### Not a CopyRecord
51+
52+
* **The danger**: an object that isn’t a `copyRecord` can have `getters` that return different values at different times, so checking the value is no guarantee that the value will be the same when accessed later. For example:
53+
54+
```js
55+
const object1 = {};
56+
let checked = false;
57+
Object.defineProperty(object1, 'value', {
58+
get() {
59+
if (checked) {
60+
return 1000000n;
61+
} else {
62+
checked = true;
63+
return 1n;
64+
}
65+
},
66+
});
67+
```
68+
```sh
69+
> object1.value
70+
1n
71+
> object1.value
72+
1000000n
73+
```
74+
75+
`Object.freeze(object1)` is not helpful here, as the `getter` is not changing, the value that is being returned is. `harden` also does not prevent `value` from changing. ​​When harden does encounter an accessor property during its traversal, it does not "read" the property, and thereby does not call the getter. Rather, it proceeds to recursively harden the getter and setter themselves. That's because, for an accessor property, its current value isn't, for this purpose, considered part of its API surface, but rather part of the behavior of that API. Thus, we need an additional way to ensure that the value of `checked` cannot change.
76+
77+
`isPassStyleOf` throws on objects with accessor properties like the object defined above.
78+
79+
### Is a CopyRecord and is a proxy
80+
81+
* **The dangers**:
82+
1) A proxy can throw on any property access, but only the first
83+
time. Any time it does *not* throw, it *must* return the same
84+
value as before.
85+
2) A proxy can mount a reentrancy attack where a proxy handler is running while code is executing, and can reenter the code while the original is still executing.
86+
87+
Commit points are a good mitigation for #1 - do all of your checks upfront so that any throwing causes you to fail fast, before any state is changed.
88+
89+
Some mitigation for #2 are:
90+
1. Create a new object with a spread operator so that the proxy isn't
91+
used further.
92+
2. Destructure and then never use the original object again.
93+
3. Use `pureCopy`. `pureCopy` ensures the object is not a proxy, but `pureCopy` only works for copyRecords that are pure data, meaning no remotables and no promises.
94+
95+
We aim to address proxy-based reentrancy by other, lower-level means, thus making `passStyleOf(amount) === ‘copyRecord’` sufficient protection against proxy-based reentrancy as well, but we should note that proxy-based reentrancy is not a threat across a vat boundary because it requires synchronous access.
96+
97+
## Invalid brands
98+
99+
A brand can be "wrong" if:
100+
1. It's not a Remotable
101+
2. Its methods don’t adhere to the expected Brand API
102+
3. It misbehaves (i.e. answering isMyIssuer with different responses)
103+
104+
In determining whether a brand is "valid", we only check that the brand is a remotable. This means that a brand
105+
that passes our input validation could still have the wrong API or misbehave.
106+
107+
## Who hardens?
108+
It is the responsibility of the sender/client (the creator of an
109+
issuerKit or user of ERTP) to harden. AmountMath does not harden
110+
inputs.
111+
112+
## The implementation
113+
114+
In `AmountMath.coerce(brand, allegedAmount)`, we do the following:
115+
1. assert that the `brand` is a remotable
116+
2. assert that the `allegedAmount` is a `copyRecord`
117+
3. destructure the `allegedAmount` into `allegedBrand` and
118+
`allegedValue`
119+
4. Assert that the `brand` is identical to the `allegedBrand`
120+
5. Call `AmountMath.make(brand, allegedValue)`, which:
121+
* Asserts that the `brand` is a remotable, again.
122+
* Asserts that the `allegedValue` is a `copyArray` or a `bigint`
123+
* Gets the appropriate MathHelpers
124+
* Calls `helpers.doCoerce(allegedValue)`, which either asserts
125+
that the value is a `Nat bigint` or that the value is a
126+
`copyArray` `structure` with no duplicate elements
127+
11. Return a new `amount`
128+
129+
Thus, we ensure that the `brand` is valid by checking that it is a
130+
remotable. We ensure that the `allegedAmount` is a copyRecord with
131+
valid `brand` and `value` properties.
132+
133+
If `allegedAmount` were a proxy, we would either throw in step 3 (the
134+
destructuring), or we successfully get both the `allegedBrand` and
135+
`allegedValue` and never touch the proxy again. Currently, we do not
136+
attempt to prevent proxy-based reentrancy, so this is the full extent
137+
of our defense.
138+
139+
140+
141+

0 commit comments

Comments
 (0)