Skip to content

Commit

Permalink
implemented verifier and tests. updated benchmarks.
Browse files Browse the repository at this point in the history
  • Loading branch information
JulienR1 committed Nov 24, 2023
1 parent 7a6105f commit a5ee1ec
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 120 deletions.
71 changes: 17 additions & 54 deletions src/signMessage.bench.ts
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 });
130 changes: 97 additions & 33 deletions src/signMessage.spec.ts
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);
});
});
29 changes: 0 additions & 29 deletions src/signMessage.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
import { logger } from "substreams-sink";
import nacl from "tweetnacl";

export function makeSignature(expirationTime: number, secretKey: string) {
const id = secretKey.substring(nacl.sign.secretKeyLength);
const payload = JSON.stringify({ exp: expirationTime, id });
const signed = nacl.sign(Buffer.from(payload), Buffer.from(secretKey, "hex"));
return Buffer.from(signed).toString("base64url");
}

export function verify(msg: Buffer, publicKey: string) {
try {
const signature = msg.subarray(0, nacl.sign.signatureLength);
const payloadBuffer = msg.subarray(nacl.sign.signatureLength);
const payload = JSON.parse(payloadBuffer.toString("utf-8"));

if (new Date().getTime() >= payload.exp) {
throw new Error("signature has expired");
}

if (publicKey !== payload.id) {
throw new Error("invalid public key");
}

return nacl.sign.detached.verify(payloadBuffer, signature, Buffer.from(publicKey, "hex"));
} catch (err) {
logger.error(err);
return false;
}
}

export function keyPair() {
const { secretKey, publicKey } = nacl.sign.keyPair();
return {
Expand Down
16 changes: 12 additions & 4 deletions src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { makeSignature } from "./signMessage.js";
import nacl from "tweetnacl";

export class Signer {
// After how much time (in % of expiration time) should a token be regenerated.
private readonly GRACE_PERIOD = 0.6;

private publicKey: string;
private latestSignature!: { signature: string; expiryTime: number };

constructor(public secretKey: string, public expirationTime: number) {
constructor(private secretKey: string, private expirationTime: number) {
this.publicKey = this.secretKey.substring(nacl.sign.secretKeyLength);
this.refreshSignature();
}

public get signature() {
if (this.latestSignature.expiryTime - this.now() < 0.6 * this.expirationTime * 1000) {
if (this.latestSignature.expiryTime - this.now() <= (1 - this.GRACE_PERIOD) * this.expirationTime * 1000) {
this.refreshSignature();
}

Expand All @@ -17,7 +22,10 @@ export class Signer {

private refreshSignature() {
const expiryTime = this.nextExpiredTime();
const signature = makeSignature(expiryTime, this.secretKey);
const payload = JSON.stringify({ exp: expiryTime, id: this.publicKey });
const signedBuffer = nacl.sign(Buffer.from(payload), Buffer.from(this.secretKey, "hex"));

const signature = Buffer.from(signedBuffer).toString("base64url");
this.latestSignature = { expiryTime, signature };
}

Expand Down
62 changes: 62 additions & 0 deletions src/verifier.ts
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();
}
}

0 comments on commit a5ee1ec

Please sign in to comment.