From fc1f85005cd3c878014900446243c9bd4b368502 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 26 Aug 2022 14:19:42 -0700 Subject: [PATCH] Add signUserMessage & verifyUserSignatures utilites --- .changeset/quick-carrots-type.md | 5 + dev-test/crypto.test.js | 212 +++++++++++++++++++++++ dev-test/util/validate-key-pair.js | 11 +- docs/api.md | 102 ++++++++++- src/crypto.js | 260 ++++++++++++++++++++++++----- src/deploy-code.js | 2 +- src/index.js | 11 +- src/manager.js | 6 +- src/utils.js | 5 + 9 files changed, 555 insertions(+), 59 deletions(-) create mode 100644 .changeset/quick-carrots-type.md create mode 100644 dev-test/crypto.test.js diff --git a/.changeset/quick-carrots-type.md b/.changeset/quick-carrots-type.md new file mode 100644 index 00000000..70201017 --- /dev/null +++ b/.changeset/quick-carrots-type.md @@ -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) diff --git a/dev-test/crypto.test.js b/dev-test/crypto.test.js new file mode 100644 index 00000000..ff787336 --- /dev/null +++ b/dev-test/crypto.test.js @@ -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` + ) + }) +}) diff --git a/dev-test/util/validate-key-pair.js b/dev-test/util/validate-key-pair.js index 06e32a35..1bbb4538 100644 --- a/dev-test/util/validate-key-pair.js +++ b/dev-test/util/validate-key-pair.js @@ -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( @@ -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") @@ -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, }) diff --git a/docs/api.md b/docs/api.md index 2fcf27a2..e2d8b932 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 @@ -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) | @@ -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). diff --git a/src/crypto.js b/src/crypto.js index f89deb1f..db70d408 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -17,31 +17,79 @@ */ import * as rlp from "rlp" +import {config, account} from "@onflow/fcl" import {ec as EC} from "elliptic" -import {config} from "@onflow/fcl" -import {isObject, isString} from "./utils" +import {getServiceAddress, isObject, isString} from "./utils" +import {invariant} from "./invariant" import {sha3_256} from "js-sha3" import {sha256 as sha2_256} from "js-sha256" +/** + * Represents a signature for an arbitrary message generated using a particular key + * @typedef {Object} SignatureObject + * @property {string} addr address of account whose key was used to sign the message + * @property {number} keyId key index on the account of they key used to sign the message + * @property {string} signature signature corresponding to the signed message hash as hex-encoded string + */ + +/** + * Represents a private key object which may be used to generate a public key + * @typedef {Object} KeyObject + * @property {string | Buffer} privateKey private key for this key object + * @property {SignatureAlgorithm} [signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used with this key + * @property {HashAlgorithm} [hashAlgorithm=HashAlgorithm.SHA3_256] hash algorithm used with this key + * @property {weight} [weight=1000] desired weight of this key (default full weight) + */ + +/** + * Represents a signer of a message or transaction + * @typedef {Object} SignerInfoObject + * @property {string} addr address of the signer + * @property {HashAlgorithm} [hashAlgorithm=HashAlgorithm.SHA3_256] hash algorithm used to hash the message before signing + * @property {SignatureAlgorithm} [signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used to generate the signature + * @property {number} [keyId=0] index of the key on the signers account to use + * @property {string | Buffer} [privateKey=SERVICE_KEY] private key of the signer (defaults to universal private key/service key from config) + */ + +/** + * Enum for signing algorithms + * @readonly + * @enum {number} + */ export const SignatureAlgorithm = { ECDSA_P256: 2, ECDSA_secp256k1: 3, } +/** + * Enum for hasing algorithms + * @readonly + * @enum {number} + */ export const HashAlgorithm = { SHA2_256: 1, SHA3_256: 3, } -export const HashAlgoFnMap = { +/** + * Enum for mapping hash algorithm name to hashing function + * @readonly + * @enum {function} + */ +export const HashFunction = { SHA2_256: sha2_256, SHA3_256: sha3_256, } -export const SignAlgoECMap = { - ECDSA_P256: "p256", - ECDSA_secp256k1: "secp256k1", +/** + * Enum for mapping signature algorithm to elliptic instance + * @readonly + * @enum {EC} + */ +export const ec = { + ECDSA_P256: new EC("p256"), + ECDSA_secp256k1: new EC("secp256k1"), } export const resolveHashAlgoKey = hashAlgorithm => { @@ -74,7 +122,7 @@ export const resolveSignAlgoKey = signatureAlgorithm => { const hashMsgHex = (msgHex, hashAlgorithm = HashAlgorithm.SHA3_256) => { const hashAlgorithmKey = resolveHashAlgoKey(hashAlgorithm) - const hashFn = HashAlgoFnMap[hashAlgorithmKey] + const hashFn = HashFunction[hashAlgorithmKey] const hash = hashFn.create() hash.update(Buffer.from(msgHex, "hex")) @@ -87,11 +135,9 @@ export const signWithKey = ( hashAlgorithm = HashAlgorithm.SHA3_256, signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 ) => { - const signatureAlgorithmKey = resolveSignAlgoKey(signatureAlgorithm) - const curve = SignAlgoECMap[signatureAlgorithmKey] + const signAlgo = resolveSignAlgoKey(signatureAlgorithm) - const ec = new EC(curve) - const key = ec.keyFromPrivate(Buffer.from(privateKey, "hex")) + const key = ec[signAlgo].keyFromPrivate(Buffer.from(privateKey, "hex")) const sig = key.sign(hashMsgHex(msgHex, hashAlgorithm)) const n = 32 // half of signature length? const r = sig.r.toArrayLike(Buffer, "be", n) @@ -99,28 +145,39 @@ export const signWithKey = ( return Buffer.concat([r, s]).toString("hex") } +export const resolveSignerKey = async signer => { + let addr = await getServiceAddress(), + keyId = 0, + privateKey = await config().get("PRIVATE_KEY"), + hashAlgorithm = HashAlgorithm.SHA3_256, + signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 + + if (isObject(signer)) { + ;({ + addr = addr, + keyId = keyId, + privateKey = privateKey, + hashAlgorithm = hashAlgorithm, + signatureAlgorithm = signatureAlgorithm, + } = signer) + } else { + addr = signer || addr + } + + return { + addr, + keyId, + privateKey, + hashAlgorithm, + signatureAlgorithm, + } +} + export const authorization = signer => async (account = {}) => { - const serviceAddress = await config().get("SERVICE_ADDRESS") - - let addr = serviceAddress, - keyId = 0, - privateKey = await config().get("PRIVATE_KEY"), - hashAlgorithm = HashAlgorithm.SHA3_256, - signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 - - if (isObject(signer)) { - ;({ - addr = addr, - keyId = keyId, - privateKey = privateKey, - hashAlgorithm = hashAlgorithm, - signatureAlgorithm = signatureAlgorithm, - } = signer) - } else { - addr = signer || addr - } + const {addr, keyId, privateKey, hashAlgorithm, signatureAlgorithm} = + await resolveSignerKey(signer) const signingFunction = async data => ({ keyId, @@ -141,22 +198,30 @@ export const authorization = } } -export const pubFlowKey = async ({ - privateKey, - hashAlgorithm = HashAlgorithm.SHA3_256, - signatureAlgorithm = SignatureAlgorithm.ECDSA_P256, - weight = 1000, // give key full weight -}) => { +/** + * Returns an RLP-encoded public key for a particular private key as a hex-encoded string + * @param {KeyObject} keyObject + * @param {string | Buffer} keyObject.privateKey private key as hex-encoded string or Buffer + * @param {HashAlgorithm | string} [keyObject.hashAlgorithm=HashAlgorithm.SHA3_256] hasing algorithnm used to hash messages using this key + * @param {SignatureAlgorithm | string} [keyObject.signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used to generate signatures using this key + * @param {number} [keyObject.weight=1000] weight of the key + * @returns {string} + */ +export const pubFlowKey = async (keyObject = {}) => { + let { + privateKey = await config().get("PRIVATE_KEY"), + hashAlgorithm = HashAlgorithm.SHA3_256, + signatureAlgorithm = SignatureAlgorithm.ECDSA_P256, + weight = 1000, // give key full weight + } = keyObject + // Converty hex string private key to buffer if not buffer already if (!Buffer.isBuffer(privateKey)) privateKey = Buffer.from(privateKey, "hex") const hashAlgoName = resolveHashAlgoKey(hashAlgorithm) const sigAlgoName = resolveSignAlgoKey(signatureAlgorithm) - const curve = SignAlgoECMap[sigAlgoName] - - const ec = new EC(curve) - const keys = ec.keyFromPrivate(privateKey) + const keys = ec[sigAlgoName].keyFromPrivate(privateKey) const publicKey = keys.getPublic("hex").replace(/^04/, "") return rlp .encode([ @@ -167,3 +232,118 @@ export const pubFlowKey = async ({ ]) .toString("hex") } + +export const prependDomainTag = (msgHex, domainTag) => { + const rightPaddedBuffer = buffer => + Buffer.concat([Buffer.alloc(32 - buffer.length, 0), buffer]) + let domainTagBuffer = rightPaddedBuffer(Buffer.from(domainTag, "utf-8")) + return domainTagBuffer.toString("hex") + msgHex +} + +/** + * Signs a user message for a given signer + * @param {string | Buffer} msgHex hex-encoded string or Buffer of the message to sign + * @param {string | SignerInfoObject} signer signer address provided as string and JS Testing signs with universal private key/service key or signer info provided manually via SignerInfoObject + * @param {string} domainTag utf-8 domain tag to use when hashing message + * @returns {SignatureObject} signature object which can be validated using verifyUserSignatures + */ +export const signUserMessage = async (msgHex, signer, domainTag) => { + if (Buffer.isBuffer(msgHex)) msgHex.toString("hex") + + const {addr, keyId, privateKey, hashAlgorithm, signatureAlgorithm} = + await resolveSignerKey(signer, true) + + if (domainTag) { + msgHex = prependDomainTag(msgHex, domainTag) + } + + return { + keyId, + addr: addr, + signature: signWithKey( + privateKey, + msgHex, + hashAlgorithm, + signatureAlgorithm + ), + } +} + +/** + * Verifies whether user signatures were valid for a particular message hex + * @param {string | Buffer} msgHex hex-encoded string or buffer of message to verify + * @param {[SignatureObject]} signatures array of signatures to verify against msgHex + * @param {string} [domainTag=""] utf-8 domain tag to use when hashing message + * @returns {boolean} true if signatures are valid and total weight >= 1000 + */ +export const verifyUserSignatures = async ( + msgHex, + signatures, + domainTag = "" +) => { + if (Buffer.isBuffer(msgHex)) msgHex = msgHex.toString("hex") + + invariant(signatures, "One or mores signatures must be provided") + + // convert to array + signatures = [].concat(signatures) + + invariant(signatures.length > 0, "One or mores signatures must be provided") + + invariant( + signatures.reduce( + (valid, sig) => + valid && sig.signature != null && sig.keyId != null && sig.addr != null, + true + ), + "One or more signature is invalid. Valid signatures have the following keys: addr, keyId, siganture" + ) + + const address = signatures[0].addr + invariant( + signatures.reduce((same, sig) => same && sig.addr === address, true), + "Signatures must belong to the same address" + ) + + const keys = (await account(address)).keys + + const largestKeyId = Math.max(...signatures.map(sig => sig.keyId)) + invariant( + largestKeyId < keys.length, + `Key index ${largestKeyId} does not exist on account ${address}` + ) + + // Apply domain tag if needed + if (domainTag) { + msgHex = prependDomainTag(msgHex, domainTag) + } + + let totalWeight = 0 + for (let i in signatures) { + const {signature, keyId} = signatures[i] + const { + hashAlgoString: hashAlgo, + signAlgoString: signAlgo, + weight, + publicKey, + revoked, + } = keys[keyId] + + const key = ec[signAlgo].keyFromPublic(Buffer.from("04" + publicKey, "hex")) + + console.log(revoked) + if (revoked) return false + + const msgHash = hashMsgHex(msgHex, hashAlgo) + const sigBuffer = Buffer.from(signature, "hex") + const signatureInput = { + r: sigBuffer.slice(0, 32), + s: sigBuffer.slice(-32), + } + + if (!key.verify(msgHash, signatureInput)) return false + + totalWeight += weight + } + return totalWeight >= 1000 +} diff --git a/src/deploy-code.js b/src/deploy-code.js index f695e381..280b6449 100644 --- a/src/deploy-code.js +++ b/src/deploy-code.js @@ -17,7 +17,7 @@ */ import {sendTransaction} from "./interaction" -import {getServiceAddress} from "./manager" +import {getServiceAddress} from "./utils" import {defaultsByName, getContractCode} from "./file" import txRegistry from "./generated/transactions" diff --git a/src/index.js b/src/index.js index 75d63552..5bf0fb65 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,6 @@ export {getFlowBalance, mintFlow} from "./flow-token" export {deployContract, deployContractByName} from "./deploy-code" export {createAccount, getAccountAddress} from "./account" export { - getServiceAddress, getBlockOffset, setBlockOffset, getTimestampOffset, @@ -44,8 +43,14 @@ export { shallRevert, shallThrow, } from "./jest-asserts" -export {HashAlgorithm, SignatureAlgorithm, pubFlowKey} from "./crypto" -export {isAddress} from "./utils" export {builtInMethods} from "./transformers" +export { + HashAlgorithm, + SignatureAlgorithm, + pubFlowKey, + signUserMessage, + verifyUserSignatures, +} from "./crypto" +export {isAddress, getServiceAddress} from "./utils" export {default as emulator} from "./emulator" diff --git a/src/manager.js b/src/manager.js index a5e630df..6189339c 100644 --- a/src/manager.js +++ b/src/manager.js @@ -17,8 +17,8 @@ */ import {executeScript, sendTransaction} from "./interaction" -import {withPrefix, config} from "@onflow/fcl" import registry from "./generated" +import {getServiceAddress} from "./utils" import {authorization} from "./crypto" export const initManager = async () => { @@ -27,10 +27,6 @@ export const initManager = async () => { }) } -export const getServiceAddress = async () => { - return withPrefix(await config().get("SERVICE_ADDRESS")) -} - export const getManagerAddress = async () => { const serviceAddress = await getServiceAddress() diff --git a/src/utils.js b/src/utils.js index ab8fb8ef..4cbe1915 100644 --- a/src/utils.js +++ b/src/utils.js @@ -16,6 +16,7 @@ * limitations under the License. */ +import {config, withPrefix} from "@onflow/fcl" import {createServer} from "net" export const isObject = arg => typeof arg === "object" && arg !== null @@ -35,3 +36,7 @@ export function getAvailablePorts(count = 1) { }) }) } + +export const getServiceAddress = async () => { + return withPrefix(await config().get("SERVICE_ADDRESS")) +}