Skip to content
This repository was archived by the owner on Nov 5, 2024. It is now read-only.

Commit 69b25e0

Browse files
authored
Add signUserMessage & verifyUserSignatures utilites (#166)
1 parent e5be81e commit 69b25e0

File tree

10 files changed

+556
-60
lines changed

10 files changed

+556
-60
lines changed

.changeset/quick-carrots-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@onflow/flow-js-testing": minor
3+
---
4+
5+
Add `signUserMessage` utility to sign a message with an arbitrary signer and `verifyUserMessage` to verify signatures. [See more here](/docs/api.md#signusermessagemessage-signer)

dev-test/crypto.test.js

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import {account, config} from "@onflow/fcl"
2+
import {
3+
createAccount,
4+
emulator,
5+
getAccountAddress,
6+
getServiceAddress,
7+
init,
8+
sendTransaction,
9+
shallPass,
10+
signUserMessage,
11+
verifyUserSignatures,
12+
} from "../src"
13+
import {
14+
prependDomainTag,
15+
resolveHashAlgoKey,
16+
resolveSignAlgoKey,
17+
} from "../src/crypto"
18+
19+
beforeEach(async () => {
20+
await init()
21+
await emulator.start()
22+
})
23+
afterEach(async () => {
24+
await emulator.stop()
25+
})
26+
27+
describe("cryptography tests", () => {
28+
test("signUserMessage - sign with address", async () => {
29+
const Alice = await getAccountAddress("Alice")
30+
const msgHex = "a1b2c3"
31+
32+
const signature = await signUserMessage(msgHex, Alice)
33+
34+
expect(Object.keys(signature).length).toBe(3)
35+
expect(signature.addr).toBe(Alice)
36+
expect(signature.keyId).toBe(0)
37+
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
38+
})
39+
40+
test("signUserMessage - sign with KeyObject", async () => {
41+
const hashAlgorithm = "SHA3_256"
42+
const signatureAlgorithm = "ECDSA_P256"
43+
44+
const privateKey = "1234"
45+
const Adam = await createAccount({
46+
name: "Adam",
47+
keys: [
48+
{
49+
privateKey,
50+
hashAlgorithm,
51+
signatureAlgorithm,
52+
weight: 1000,
53+
},
54+
],
55+
})
56+
57+
const signer = {
58+
addr: Adam,
59+
keyId: 0,
60+
privateKey,
61+
hashAlgorithm,
62+
signatureAlgorithm,
63+
}
64+
65+
const msgHex = "a1b2c3"
66+
const signature = await signUserMessage(msgHex, signer)
67+
68+
expect(Object.keys(signature).length).toBe(3)
69+
expect(signature.addr).toBe(Adam)
70+
expect(signature.keyId).toBe(0)
71+
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
72+
})
73+
74+
test("signUserMessage - sign with domain separation tag", async () => {
75+
const Alice = await getAccountAddress("Alice")
76+
const msgHex = "a1b2c3"
77+
78+
const signature = await signUserMessage(msgHex, Alice, "foo")
79+
80+
expect(Object.keys(signature).length).toBe(3)
81+
expect(signature.addr).toBe(Alice)
82+
expect(signature.keyId).toBe(0)
83+
expect(await verifyUserSignatures(msgHex, [signature], "foo")).toBe(true)
84+
})
85+
86+
test("signUserMessage - sign with service key", async () => {
87+
const Alice = await getServiceAddress()
88+
const msgHex = "a1b2c3"
89+
90+
const signature = await signUserMessage(msgHex, Alice)
91+
92+
expect(Object.keys(signature).length).toBe(3)
93+
expect(signature.addr).toBe(Alice)
94+
expect(signature.keyId).toBe(0)
95+
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
96+
})
97+
98+
test("verifyUserSignature & signUserMessage - work with Buffer messageHex", async () => {
99+
const Alice = await getAccountAddress("Alice")
100+
const msgHex = Buffer.from([0xa1, 0xb2, 0xc3])
101+
const signature = await signUserMessage(msgHex, Alice)
102+
103+
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
104+
})
105+
106+
test("verifyUserSignature - fails with bad signature", async () => {
107+
const Alice = await getAccountAddress("Alice")
108+
const msgHex = "a1b2c3"
109+
110+
const badSignature = {
111+
addr: Alice,
112+
keyId: 0,
113+
signature: "a1b2c3",
114+
}
115+
116+
expect(await verifyUserSignatures(msgHex, [badSignature])).toBe(false)
117+
})
118+
119+
test("verifyUserSignature - fails if weight < 1000", async () => {
120+
const Alice = await createAccount({
121+
name: "Alice",
122+
keys: [
123+
{
124+
privateKey: await config().get("PRIVATE_KEY"),
125+
weight: 123,
126+
},
127+
],
128+
})
129+
const msgHex = "a1b2c3"
130+
const signature = await signUserMessage(msgHex, Alice)
131+
132+
expect(await verifyUserSignatures(msgHex, [signature])).toBe(false)
133+
})
134+
135+
test("verifyUserSignatures - throws with null signature object", async () => {
136+
const msgHex = "a1b2c3"
137+
138+
await expect(verifyUserSignatures(msgHex, null)).rejects.toThrow(
139+
"INVARIANT One or mores signatures must be provided"
140+
)
141+
})
142+
143+
test("verifyUserSignatures - throws with no signatures in array", async () => {
144+
const msgHex = "a1b2c3"
145+
146+
await expect(verifyUserSignatures(msgHex, [])).rejects.toThrow(
147+
"INVARIANT One or mores signatures must be provided"
148+
)
149+
})
150+
151+
test("verifyUserSignatures - throws with different account signatures", async () => {
152+
const Alice = await getAccountAddress("Alice")
153+
const Bob = await getAccountAddress("Bob")
154+
const msgHex = "a1b2c3"
155+
156+
const signatureAlice = await signUserMessage(msgHex, Alice)
157+
const signatureBob = await signUserMessage(msgHex, Bob)
158+
159+
await expect(
160+
verifyUserSignatures(msgHex, [signatureAlice, signatureBob])
161+
).rejects.toThrow("INVARIANT Signatures must belong to the same address")
162+
})
163+
164+
test("verifyUserSignatures - throws with invalid signature format", async () => {
165+
const msgHex = "a1b2c3"
166+
const signature = {
167+
foo: "bar",
168+
}
169+
170+
await expect(verifyUserSignatures(msgHex, [signature])).rejects.toThrow(
171+
"INVARIANT One or more signature is invalid. Valid signatures have the following keys: addr, keyId, siganture"
172+
)
173+
})
174+
175+
test("verifyUserSignatures - throws with non-existant key", async () => {
176+
const Alice = await getAccountAddress("Alice")
177+
const msgHex = "a1b2c3"
178+
179+
const signature = await signUserMessage(msgHex, Alice)
180+
signature.keyId = 42
181+
182+
await expect(verifyUserSignatures(msgHex, [signature])).rejects.toThrow(
183+
`INVARIANT Key index ${signature.keyId} does not exist on account ${Alice}`
184+
)
185+
})
186+
187+
test("prependDomainTag prepends a domain tag to a given msgHex", () => {
188+
const msgHex = "a1b2c3"
189+
const domainTag = "hello world"
190+
const paddedDomainTagHex =
191+
"00000000000000000000000000000000000000000068656c6c6f20776f726c64"
192+
193+
const result = prependDomainTag(msgHex, domainTag)
194+
const expected = paddedDomainTagHex + msgHex
195+
196+
expect(result).toEqual(expected)
197+
})
198+
199+
test("resolveHashAlgoKey - unsupported hash algorithm", () => {
200+
const hashAlgorithm = "ABC123"
201+
expect(() => resolveHashAlgoKey(hashAlgorithm)).toThrow(
202+
`Provided hash algorithm "${hashAlgorithm}" is not currently supported`
203+
)
204+
})
205+
206+
test("resolveHashAlgoKey - unsupported signature algorithm", () => {
207+
const signatureAlgorithm = "ABC123"
208+
expect(() => resolveSignAlgoKey(signatureAlgorithm)).toThrow(
209+
`Provided signature algorithm "${signatureAlgorithm}" is not currently supported`
210+
)
211+
})
212+
})

dev-test/util/validate-key-pair.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {ec as EC} from "elliptic"
2-
import {
3-
resolveSignAlgoKey,
4-
SignAlgoECMap,
5-
SignatureAlgorithm,
6-
} from "../../src/crypto"
1+
import {resolveSignAlgoKey, ec, SignatureAlgorithm} from "../../src/crypto"
72
import {isString} from "../../src/utils"
83

94
export function validateKeyPair(
@@ -12,7 +7,6 @@ export function validateKeyPair(
127
signatureAlgorithm = SignatureAlgorithm.P256
138
) {
149
const signAlgoKey = resolveSignAlgoKey(signatureAlgorithm)
15-
const curve = SignAlgoECMap[signAlgoKey]
1610

1711
const prepareKey = key => {
1812
if (isString(key)) key = Buffer.from(key, "hex")
@@ -23,8 +17,7 @@ export function validateKeyPair(
2317
publicKey = prepareKey(publicKey)
2418
privateKey = prepareKey(privateKey)
2519

26-
const ec = new EC(curve)
27-
const pair = ec.keyPair({
20+
const pair = ec[signAlgoKey].keyPair({
2821
pub: publicKey,
2922
priv: privateKey,
3023
})

docs/api.md

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,96 @@ The `pubFlowKey` method exported by Flow JS Testing Library will generate an RLP
244244

245245
If `keyObject` is not provided, Flow JS Testing will default to the [universal private key](./accounts.md#universal-private-key).
246246

247+
#### Returns
248+
249+
| Type | Description |
250+
| ------ | ---------------------- |
251+
| Buffer | RLP-encoded public key |
252+
253+
#### Usage
254+
255+
```javascript
256+
import {pubFlowKey}
257+
258+
const key = {
259+
privateKey: "a1b2c3" // private key as hex string
260+
hashAlgorithm: HashAlgorithm.SHA3_256
261+
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
262+
weight: 1000
263+
}
264+
265+
const pubKey = await pubFlowKey(key) // public key generated from keyObject provided
266+
const genericPubKey = await pubFlowKey() // public key generated from universal private key/service key
267+
```
268+
269+
### `signUserMessage(msgHex, signer, domainTag)`
270+
271+
The `signUserMessage` method will produce a user signature of some arbitrary data using a particular signer.
272+
273+
#### Arguments
274+
275+
| Name | Type | Optional | Description |
276+
| ----------- | -------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
277+
| `msgHex` | string or Buffer | | a hex-encoded string or Buffer which will be used to generate the signature |
278+
| `signer` | [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject) || [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject) object representing user to generate this signature for (default: [universal private key](./accounts.md#universal-private-key)) |
279+
| `domainTag` | string || Domain separation tag provided as a utf-8 encoded string (default: no domain separation tag). See more about [domain tags here](https://docs.onflow.org/cadence/language/crypto/#hashing-with-a-domain-tag). |
280+
281+
#### Returns
282+
283+
| Type | Description |
284+
| ------------------------------------------- | -------------------------------------------------------------------------------------------------- |
285+
| [SignatureObject](./api.md#signatureobject) | An object representing the signature for the message & account/keyId which signed for this message |
286+
287+
#### Usage
288+
289+
```javascript
290+
import {signUserMessage, getAccountAddress} from "@onflow/flow-js-testing"
291+
292+
const Alice = await getAccountAddress("Alice")
293+
const msgHex = "a1b2c3"
294+
295+
const signature = await generateUserSignature(msgHex, Alice)
296+
```
297+
298+
## `verifyUserSigntatures(msgHex, signatures, domainTag)`
299+
300+
Used to verify signatures generated by [`signUserMessage`](./api.md#signusermessagemessage-signer). This function takes an array of signatures and verifies that the total key weight sums to >= 1000 and that these signatures are valid.
301+
302+
#### Arguments
303+
304+
| Name | Type | Optional | Description |
305+
| ------------ | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
306+
| `msgHex` | string | | the message which the provided signatures correspond to provided as a hex-encoded string or Buffer |
307+
| `signatures` | [[SignatureObject](./api.md#signatureobject)] | | An array of [SignatureObjects](./api.md#signatureobject) which will be verified against this message |
308+
| `domainTag` | string || Domain separation tag provided as a utf-8 encoded string (default: no domain separation tag). See more about [domain tags here](https://docs.onflow.org/cadence/language/crypto/#hashing-with-a-domain-tag). |
309+
310+
#### Returns
311+
312+
This method returns an object with the following keys:
313+
| Type | Description |
314+
| ---- | ----------- |
315+
| boolean | Returns true if signatures are valid and total weight >= 1000 |
316+
317+
#### Usage
318+
319+
```javascript
320+
import {
321+
signUserMessage,
322+
verifyUserSignatures,
323+
getAccountAddress,
324+
} from "@onflow/flow-js-testing"
325+
326+
const Alice = await getAccountAddress("Alice")
327+
const msgHex = "a1b2c3"
328+
329+
const signature = await generateUserSignature(msgHex, Alice)
330+
331+
console.log(await verifyUserSignatures(msgHex, Alice)) // true
332+
333+
const Bob = await getAccountAddress("Bob")
334+
console.log(await verifyUserSignatures(msgHex, Bob)) // false
335+
```
336+
247337
## Emulator
248338

249339
Flow Javascript Testing Framework exposes `emulator` singleton allowing you to run and stop emulator instance
@@ -264,7 +354,7 @@ Starts emulator on a specified port. Returns Promise.
264354
| Key | Type | Optional | Description |
265355
| ----------- | ------- | -------- | --------------------------------------------------------------------------------- |
266356
| `logging` | boolean || whether log messages from emulator shall be added to the output (default: false) |
267-
| `flags` | string || custom command-line flags to supply to the emulator (default: "") |
357+
| `flags` | string || custom command-line flags to supply to the emulator (default: no flags) |
268358
| `adminPort` | number || override the port which the emulator will run the admin server on (default: auto) |
269359
| `restPort` | number || override the port which the emulator will run the REST server on (default: auto) |
270360
| `grpcPort` | number || override the port which the emulator will run the GRPC server on (default: auto) |
@@ -1392,6 +1482,16 @@ const pubKey = await pubFlowKey({
13921482
})
13931483
```
13941484

1485+
### SignatureObject
1486+
1487+
Signature objects are used to represent a signature for a particular message as well as the account and keyId which signed for this message.
1488+
1489+
| Key | Value Type | Description |
1490+
| ----------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1491+
| `addr` | [Address](https://docs.onflow.org/fcl/reference/api/#address) | the address of the account which this signature has been generated for |
1492+
| `keyId` | number | [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject) object representing user to generate this signature for |
1493+
| `signature` | string | a hexidecimal-encoded string representation of the generated signature |
1494+
13951495
### SignerInfoObject
13961496

13971497
Signer Info objects are used to specify information about which signer and which key from this signer shall be used to [sign a transaction](./send-transactions.md).

0 commit comments

Comments
 (0)