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

Commit

Permalink
Add signUserMessage & verifyUserSignatures utilites (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink authored Aug 26, 2022
1 parent e5be81e commit 69b25e0
Show file tree
Hide file tree
Showing 10 changed files with 556 additions and 60 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-carrots-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@onflow/flow-js-testing": minor
---

Add `signUserMessage` utility to sign a message with an arbitrary signer and `verifyUserMessage` to verify signatures. [See more here](/docs/api.md#signusermessagemessage-signer)
212 changes: 212 additions & 0 deletions dev-test/crypto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {account, config} from "@onflow/fcl"
import {
createAccount,
emulator,
getAccountAddress,
getServiceAddress,
init,
sendTransaction,
shallPass,
signUserMessage,
verifyUserSignatures,
} from "../src"
import {
prependDomainTag,
resolveHashAlgoKey,
resolveSignAlgoKey,
} from "../src/crypto"

beforeEach(async () => {
await init()
await emulator.start()
})
afterEach(async () => {
await emulator.stop()
})

describe("cryptography tests", () => {
test("signUserMessage - sign with address", async () => {
const Alice = await getAccountAddress("Alice")
const msgHex = "a1b2c3"

const signature = await signUserMessage(msgHex, Alice)

expect(Object.keys(signature).length).toBe(3)
expect(signature.addr).toBe(Alice)
expect(signature.keyId).toBe(0)
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
})

test("signUserMessage - sign with KeyObject", async () => {
const hashAlgorithm = "SHA3_256"
const signatureAlgorithm = "ECDSA_P256"

const privateKey = "1234"
const Adam = await createAccount({
name: "Adam",
keys: [
{
privateKey,
hashAlgorithm,
signatureAlgorithm,
weight: 1000,
},
],
})

const signer = {
addr: Adam,
keyId: 0,
privateKey,
hashAlgorithm,
signatureAlgorithm,
}

const msgHex = "a1b2c3"
const signature = await signUserMessage(msgHex, signer)

expect(Object.keys(signature).length).toBe(3)
expect(signature.addr).toBe(Adam)
expect(signature.keyId).toBe(0)
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
})

test("signUserMessage - sign with domain separation tag", async () => {
const Alice = await getAccountAddress("Alice")
const msgHex = "a1b2c3"

const signature = await signUserMessage(msgHex, Alice, "foo")

expect(Object.keys(signature).length).toBe(3)
expect(signature.addr).toBe(Alice)
expect(signature.keyId).toBe(0)
expect(await verifyUserSignatures(msgHex, [signature], "foo")).toBe(true)
})

test("signUserMessage - sign with service key", async () => {
const Alice = await getServiceAddress()
const msgHex = "a1b2c3"

const signature = await signUserMessage(msgHex, Alice)

expect(Object.keys(signature).length).toBe(3)
expect(signature.addr).toBe(Alice)
expect(signature.keyId).toBe(0)
expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
})

test("verifyUserSignature & signUserMessage - work with Buffer messageHex", async () => {
const Alice = await getAccountAddress("Alice")
const msgHex = Buffer.from([0xa1, 0xb2, 0xc3])
const signature = await signUserMessage(msgHex, Alice)

expect(await verifyUserSignatures(msgHex, [signature])).toBe(true)
})

test("verifyUserSignature - fails with bad signature", async () => {
const Alice = await getAccountAddress("Alice")
const msgHex = "a1b2c3"

const badSignature = {
addr: Alice,
keyId: 0,
signature: "a1b2c3",
}

expect(await verifyUserSignatures(msgHex, [badSignature])).toBe(false)
})

test("verifyUserSignature - fails if weight < 1000", async () => {
const Alice = await createAccount({
name: "Alice",
keys: [
{
privateKey: await config().get("PRIVATE_KEY"),
weight: 123,
},
],
})
const msgHex = "a1b2c3"
const signature = await signUserMessage(msgHex, Alice)

expect(await verifyUserSignatures(msgHex, [signature])).toBe(false)
})

test("verifyUserSignatures - throws with null signature object", async () => {
const msgHex = "a1b2c3"

await expect(verifyUserSignatures(msgHex, null)).rejects.toThrow(
"INVARIANT One or mores signatures must be provided"
)
})

test("verifyUserSignatures - throws with no signatures in array", async () => {
const msgHex = "a1b2c3"

await expect(verifyUserSignatures(msgHex, [])).rejects.toThrow(
"INVARIANT One or mores signatures must be provided"
)
})

test("verifyUserSignatures - throws with different account signatures", async () => {
const Alice = await getAccountAddress("Alice")
const Bob = await getAccountAddress("Bob")
const msgHex = "a1b2c3"

const signatureAlice = await signUserMessage(msgHex, Alice)
const signatureBob = await signUserMessage(msgHex, Bob)

await expect(
verifyUserSignatures(msgHex, [signatureAlice, signatureBob])
).rejects.toThrow("INVARIANT Signatures must belong to the same address")
})

test("verifyUserSignatures - throws with invalid signature format", async () => {
const msgHex = "a1b2c3"
const signature = {
foo: "bar",
}

await expect(verifyUserSignatures(msgHex, [signature])).rejects.toThrow(
"INVARIANT One or more signature is invalid. Valid signatures have the following keys: addr, keyId, siganture"
)
})

test("verifyUserSignatures - throws with non-existant key", async () => {
const Alice = await getAccountAddress("Alice")
const msgHex = "a1b2c3"

const signature = await signUserMessage(msgHex, Alice)
signature.keyId = 42

await expect(verifyUserSignatures(msgHex, [signature])).rejects.toThrow(
`INVARIANT Key index ${signature.keyId} does not exist on account ${Alice}`
)
})

test("prependDomainTag prepends a domain tag to a given msgHex", () => {
const msgHex = "a1b2c3"
const domainTag = "hello world"
const paddedDomainTagHex =
"00000000000000000000000000000000000000000068656c6c6f20776f726c64"

const result = prependDomainTag(msgHex, domainTag)
const expected = paddedDomainTagHex + msgHex

expect(result).toEqual(expected)
})

test("resolveHashAlgoKey - unsupported hash algorithm", () => {
const hashAlgorithm = "ABC123"
expect(() => resolveHashAlgoKey(hashAlgorithm)).toThrow(
`Provided hash algorithm "${hashAlgorithm}" is not currently supported`
)
})

test("resolveHashAlgoKey - unsupported signature algorithm", () => {
const signatureAlgorithm = "ABC123"
expect(() => resolveSignAlgoKey(signatureAlgorithm)).toThrow(
`Provided signature algorithm "${signatureAlgorithm}" is not currently supported`
)
})
})
11 changes: 2 additions & 9 deletions dev-test/util/validate-key-pair.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {ec as EC} from "elliptic"
import {
resolveSignAlgoKey,
SignAlgoECMap,
SignatureAlgorithm,
} from "../../src/crypto"
import {resolveSignAlgoKey, ec, SignatureAlgorithm} from "../../src/crypto"
import {isString} from "../../src/utils"

export function validateKeyPair(
Expand All @@ -12,7 +7,6 @@ export function validateKeyPair(
signatureAlgorithm = SignatureAlgorithm.P256
) {
const signAlgoKey = resolveSignAlgoKey(signatureAlgorithm)
const curve = SignAlgoECMap[signAlgoKey]

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

const ec = new EC(curve)
const pair = ec.keyPair({
const pair = ec[signAlgoKey].keyPair({
pub: publicKey,
priv: privateKey,
})
Expand Down
102 changes: 101 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,96 @@ The `pubFlowKey` method exported by Flow JS Testing Library will generate an RLP

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

#### Returns

| Type | Description |
| ------ | ---------------------- |
| Buffer | RLP-encoded public key |

#### Usage

```javascript
import {pubFlowKey}

const key = {
privateKey: "a1b2c3" // private key as hex string
hashAlgorithm: HashAlgorithm.SHA3_256
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
weight: 1000
}

const pubKey = await pubFlowKey(key) // public key generated from keyObject provided
const genericPubKey = await pubFlowKey() // public key generated from universal private key/service key
```

### `signUserMessage(msgHex, signer, domainTag)`

The `signUserMessage` method will produce a user signature of some arbitrary data using a particular signer.

#### Arguments

| Name | Type | Optional | Description |
| ----------- | -------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `msgHex` | string or Buffer | | a hex-encoded string or Buffer which will be used to generate the signature |
| `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)) |
| `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). |

#### Returns

| Type | Description |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [SignatureObject](./api.md#signatureobject) | An object representing the signature for the message & account/keyId which signed for this message |

#### Usage

```javascript
import {signUserMessage, getAccountAddress} from "@onflow/flow-js-testing"

const Alice = await getAccountAddress("Alice")
const msgHex = "a1b2c3"

const signature = await generateUserSignature(msgHex, Alice)
```

## `verifyUserSigntatures(msgHex, signatures, domainTag)`

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.

#### Arguments

| Name | Type | Optional | Description |
| ------------ | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `msgHex` | string | | the message which the provided signatures correspond to provided as a hex-encoded string or Buffer |
| `signatures` | [[SignatureObject](./api.md#signatureobject)] | | An array of [SignatureObjects](./api.md#signatureobject) which will be verified against this message |
| `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). |

#### Returns

This method returns an object with the following keys:
| Type | Description |
| ---- | ----------- |
| boolean | Returns true if signatures are valid and total weight >= 1000 |

#### Usage

```javascript
import {
signUserMessage,
verifyUserSignatures,
getAccountAddress,
} from "@onflow/flow-js-testing"

const Alice = await getAccountAddress("Alice")
const msgHex = "a1b2c3"

const signature = await generateUserSignature(msgHex, Alice)

console.log(await verifyUserSignatures(msgHex, Alice)) // true

const Bob = await getAccountAddress("Bob")
console.log(await verifyUserSignatures(msgHex, Bob)) // false
```

## Emulator

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

### SignatureObject

Signature objects are used to represent a signature for a particular message as well as the account and keyId which signed for this message.

| Key | Value Type | Description |
| ----------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `addr` | [Address](https://docs.onflow.org/fcl/reference/api/#address) | the address of the account which this signature has been generated for |
| `keyId` | number | [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject) object representing user to generate this signature for |
| `signature` | string | a hexidecimal-encoded string representation of the generated signature |

### SignerInfoObject

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).
Expand Down
Loading

0 comments on commit 69b25e0

Please sign in to comment.