-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implemented verifier and tests. updated benchmarks.
- Loading branch information
Showing
5 changed files
with
188 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,63 +1,26 @@ | ||
import { bench, group, run } from "mitata"; | ||
import { signMessage, verify } from "./signMessage.js"; | ||
import { Signer } from "./signer.js"; | ||
import { Verifier } from "./verifier.js"; | ||
|
||
const secretKey = | ||
"3faae992336ea6599fbee55bb2605f1a1297c7288b860725cdfc8794413559dba3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const publicKey = "a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const body = JSON.stringify({ | ||
cursor: | ||
"gBCLb0z81lU8vbvZVzJkEaWwLpc_DFhqVQ3jLxVJgYH2pSTFicymUzd9bx2GlKH51RboGgmo19eZRX588ZED7YW8y7FhuSM6EHh4wNzo87Dne6KjPQlIIOhjC-iJMNncUT7SYgz9f7UI5N_nb6XZMxMyMZEuK2blizdZqoZXIfAVsHthkjz6cJ6Bga_A-YtEq-AnEuf1xn6lDzF1Lx4LOc_RNqGe6z4nN3Rq", | ||
clock: { | ||
timestamp: "2023-06-15T04:21:58.000Z", | ||
number: 250665484, | ||
id: "0ef0da0cf870f489833ac498da073acadf895d22f3dce68483aa43cac1d27b17", | ||
}, | ||
manifest: { | ||
chain: "wax", | ||
moduleName: "map_transfers", | ||
moduleHash: "6aa24e6aa34db4a4faf55c69c6f612aeb06053c2", | ||
}, | ||
data: { | ||
items: [ | ||
{ | ||
trxId: "dd93c64db8ff91cfac74e731fd518548aa831be3d833e6a1fefeac69d2ddd138", | ||
actionOrdinal: 2, | ||
contract: "eosio.token", | ||
action: "transfer", | ||
symcode: "WAX", | ||
from: "banxawallet1", | ||
to: "atomicmarket", | ||
quantity: "1340.00000000 WAX", | ||
memo: "deposit", | ||
precision: 8, | ||
amount: "134000000000", | ||
value: 1340, | ||
}, | ||
{ | ||
trxId: "dd93c64db8ff91cfac74e731fd518548aa831be3d833e6a1fefeac69d2ddd138", | ||
actionOrdinal: 7, | ||
contract: "eosio.token", | ||
action: "transfer", | ||
symcode: "WAX", | ||
from: "atomicmarket", | ||
to: "jft4m.c.wam", | ||
quantity: "1206.00000000 WAX", | ||
memo: "AtomicMarket Sale Payout - ID #129675349", | ||
precision: 8, | ||
amount: "120600000000", | ||
value: 1206, | ||
}, | ||
], | ||
}, | ||
const signature = | ||
"0mKZAisTwl5IiRkc229KuPowpSS8pEsXQr7e6rsUXKmXkLoJRn8TZfhwruEjbshoLNw2kO2kyCZs_1EkR9cnC3siZXhwIjoyNTI0NjA4MDAwMDAwLCJpZCI6ImEzY2I3MzY2ZWU4Y2E3NzIyNWI0ZDQxNzcyZTI3MGU0ZTgzMWQxNzFkMWRlNzFkOTE3MDdjNDJlN2JhODJjYzkifQ"; | ||
|
||
const signer = new Signer(secretKey, 60); | ||
const verifier = new Verifier([publicKey]); | ||
|
||
group("sign", () => { | ||
// @ts-expect-error | ||
bench("sign - no cache", () => signer.refreshSignature()); | ||
bench("sign - with cache", () => signer.signature); | ||
}); | ||
const timestamp = 1686802918; | ||
const sig = | ||
"d7b6b6b76ffb3ad58337d3082bcbeef39de1c2c4cd19f9d24955974358bb85e4bbdde31d055f60b1035750b4ca07e4e4c1398924106352577509b077ddd85802"; | ||
const msg = Buffer.from(timestamp + body); | ||
|
||
group("signMessage", () => { | ||
bench("signMessage", () => signMessage(timestamp, body, secretKey)); | ||
bench("verify", () => verify(msg, sig, publicKey)); | ||
group("verify", () => { | ||
// @ts-expect-error | ||
bench("verify - no cache", () => verifier.verifyMessage(signature)); | ||
bench("verify - with cache", () => verifier.verify(signature)); | ||
}); | ||
|
||
await run(); | ||
await run({ avg: true, json: false, colors: true, min_max: true, collect: false, percentiles: false }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,102 @@ | ||
import { expect, test } from "bun:test"; | ||
import { makeSignature, verify } from "./signMessage.js"; | ||
import { beforeEach, describe, expect, setSystemTime, spyOn, test } from "bun:test"; | ||
import { Signer } from "./signer.js"; | ||
import { Verifier } from "./verifier.js"; | ||
|
||
test("signMessage", () => { | ||
const secretKey = | ||
"3faae992336ea6599fbee55bb2605f1a1297c7288b860725cdfc8794413559dba3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const publicKey = "a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const expectedSignature = | ||
"0mKZAisTwl5IiRkc229KuPowpSS8pEsXQr7e6rsUXKmXkLoJRn8TZfhwruEjbshoLNw2kO2kyCZs_1EkR9cnC3siZXhwIjoyNTI0NjA4MDAwMDAwLCJpZCI6ImEzY2I3MzY2ZWU4Y2E3NzIyNWI0ZDQxNzcyZTI3MGU0ZTgzMWQxNzFkMWRlNzFkOTE3MDdjNDJlN2JhODJjYzkifQ"; | ||
const secretKey = | ||
"3faae992336ea6599fbee55bb2605f1a1297c7288b860725cdfc8794413559dba3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const publicKey = "a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
|
||
const expiry = new Date(2050, 0, 1).getTime(); | ||
const signature = makeSignature(expiry, secretKey); | ||
describe("signatures", () => { | ||
beforeEach(() => { | ||
// Reset clock | ||
setSystemTime(); | ||
}); | ||
|
||
expect(signature).toBe(expectedSignature); | ||
expect(verify(Buffer.from(signature, "base64url"), publicKey)).toBeTrue(); | ||
}); | ||
// This test will be invalid from January 1, 2050 | ||
test("signMessage", () => { | ||
// Make the token expire in 2050 by modifying the current time | ||
setSystemTime(new Date(2050, 0, 1)); | ||
const signer = new Signer(secretKey, 0); | ||
// Reset time to regular clock | ||
setSystemTime(); | ||
const verifier = new Verifier([publicKey]); | ||
|
||
const expectedSignature = | ||
"0mKZAisTwl5IiRkc229KuPowpSS8pEsXQr7e6rsUXKmXkLoJRn8TZfhwruEjbshoLNw2kO2kyCZs_1EkR9cnC3siZXhwIjoyNTI0NjA4MDAwMDAwLCJpZCI6ImEzY2I3MzY2ZWU4Y2E3NzIyNWI0ZDQxNzcyZTI3MGU0ZTgzMWQxNzFkMWRlNzFkOTE3MDdjNDJlN2JhODJjYzkifQ"; | ||
|
||
expect(signer.signature).toBe(expectedSignature); | ||
expect(verifier.verify(signer.signature)).toBeTrue(); | ||
}); | ||
|
||
test("signMessage cache", () => { | ||
setSystemTime(new Date("2000-01-01T00:00:00.000Z")); | ||
const signer = new Signer(secretKey, 60); | ||
const refreshSignatureSpy = spyOn(signer as any, "refreshSignature"); | ||
|
||
// A signature is automatically generated when the Signer object is created | ||
signer.signature; | ||
expect(refreshSignatureSpy).toHaveBeenCalledTimes(0); | ||
|
||
// Requesting the signature in the first 60% of the time window will not regenerate it (0.6*60s = 36s) | ||
setSystemTime(new Date("2000-01-01T00:00:35.000Z")); | ||
signer.signature; | ||
expect(refreshSignatureSpy).toHaveBeenCalledTimes(0); | ||
|
||
// Requesting the signature after the first 60% of the time window will regenerate it | ||
setSystemTime(new Date("2000-01-01T00:00:36.000Z")); | ||
signer.signature; | ||
expect(refreshSignatureSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
// This test will be invalid from January 1, 2050 | ||
test("verify", () => { | ||
const invalidPublicKey = "36657c7498f2ff2e9a520dcfbdad4e7c1e5354a75623165e28f6577a45a9eec3"; | ||
|
||
const expiry = new Date(2050, 0, 1); | ||
const expired = new Date(2000, 0, 1); | ||
|
||
const tests = [ | ||
{ key: publicKey, expiry: expiry, expected: true }, | ||
{ key: publicKey, expiry: expired, expected: false }, | ||
{ key: invalidPublicKey, expiry: expired, expected: false }, | ||
{ key: invalidPublicKey, expiry: expired, expected: false }, | ||
]; | ||
|
||
for (const test of tests) { | ||
setSystemTime(test.expiry); | ||
const signer = new Signer(secretKey, 0); | ||
setSystemTime(); | ||
const verifier = new Verifier([test.key]); | ||
|
||
expect(verifier.verify(signer.signature)).toBe(test.expected); | ||
} | ||
}); | ||
|
||
test("verify cache", () => { | ||
setSystemTime(new Date("2000-01-01T00:00:00.000Z")); | ||
const signer = new Signer(secretKey, 60); | ||
const { signature } = signer; | ||
|
||
const verifier = new Verifier([publicKey]); | ||
const verifyMessageSpy = spyOn(verifier as any, "verifyMessage"); | ||
|
||
expect(verifier.verify(signature)).toBeTrue(); | ||
expect(verifyMessageSpy).toHaveBeenCalledTimes(1); | ||
|
||
// This signature is already known, we do not need to revalidate it | ||
expect(verifier.verify(signature)).toBeTrue(); | ||
expect(verifyMessageSpy).toHaveBeenCalledTimes(1); | ||
|
||
// This signature expires in 1s, but it is still valid. We do not need to revalide it. | ||
setSystemTime(new Date("2000-01-01T00:00:59.000Z")); | ||
expect(verifier.verify(signature)).toBeTrue(); | ||
expect(verifyMessageSpy).toHaveBeenCalledTimes(1); | ||
|
||
test("verify", () => { | ||
const secretKey = | ||
"3faae992336ea6599fbee55bb2605f1a1297c7288b860725cdfc8794413559dba3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const publicKey = "a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9"; | ||
const invalidPublicKey = "36657c7498f2ff2e9a520dcfbdad4e7c1e5354a75623165e28f6577a45a9eec3"; | ||
|
||
const expiry = new Date(2050, 0, 1).getTime(); | ||
const expired = new Date(2000, 0, 1).getTime(); | ||
|
||
const tests = [ | ||
{ key: publicKey, expiry: expiry, expected: true }, | ||
{ key: publicKey, expiry: expired, expected: false }, | ||
{ key: invalidPublicKey, expiry: expired, expected: false }, | ||
{ key: invalidPublicKey, expiry: expired, expected: false }, | ||
]; | ||
|
||
for (const test of tests) { | ||
const signature = makeSignature(test.expiry, secretKey); | ||
expect(verify(Buffer.from(signature, "base64url"), test.key)).toBe(test.expected); | ||
} | ||
// This signature is expired, it should be removed from the cache. | ||
setSystemTime(new Date("2000-01-01T00:01:00.000Z")); | ||
expect(verifier.verify(signature)).toBeFalse(); | ||
// @ts-expect-error | ||
expect(verifier.expirationTimes).toEqual({}); | ||
expect(verifyMessageSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { logger } from "substreams-sink"; | ||
import nacl from "tweetnacl"; | ||
|
||
export class Verifier { | ||
private expirationTimes: { [message: string]: number } = {}; | ||
|
||
constructor(public knownPublicKeys: string[]) {} | ||
|
||
public verify(message: string): boolean { | ||
if (!this.expirationTimes[message]) { | ||
this.removedExpired(); | ||
return this.verifyMessage(message); | ||
} | ||
|
||
if (this.now() >= this.expirationTimes[message]) { | ||
this.removedExpired(); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private verifyMessage(message: string) { | ||
try { | ||
const msg = Buffer.from(message, "base64url"); | ||
const signature = msg.subarray(0, nacl.sign.signatureLength); | ||
const payloadBuffer = msg.subarray(nacl.sign.signatureLength); | ||
const payload = JSON.parse(payloadBuffer.toString("utf-8")); | ||
|
||
if (this.now() >= payload.exp) { | ||
throw new Error("signature has expired"); | ||
} | ||
|
||
const publicKey = this.knownPublicKeys.find((key) => key === payload.id); | ||
if (!publicKey) { | ||
throw new Error("unknown public key"); | ||
} | ||
|
||
if (!nacl.sign.detached.verify(payloadBuffer, signature, Buffer.from(publicKey, "hex"))) { | ||
throw new Error("invalid signature"); | ||
} | ||
|
||
this.expirationTimes[message] = payload.exp; | ||
return true; | ||
} catch (err) { | ||
logger.error(err); | ||
return false; | ||
} | ||
} | ||
|
||
private removedExpired() { | ||
for (const key of Object.keys(this.expirationTimes)) { | ||
if (this.now() >= this.expirationTimes[key]) { | ||
delete this.expirationTimes[key]; | ||
} | ||
} | ||
} | ||
|
||
private now() { | ||
return new Date().getTime(); | ||
} | ||
} |