Skip to content

Commit

Permalink
Merge pull request #17 from pinax-network/feature/updated-signing-pro…
Browse files Browse the repository at this point in the history
…cess

Updated signing process
  • Loading branch information
DenisCarriere authored Nov 29, 2023
2 parents 3e0c829 + b122bb4 commit 413b4b3
Show file tree
Hide file tree
Showing 22 changed files with 311 additions and 236 deletions.
27 changes: 11 additions & 16 deletions bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@ import { Option } from "commander";
import { commander, logger } from "substreams-sink";
import { action } from "../index.js";
import pkg from "../package.json" assert { type: "json" };
import { keyPair } from "../src/auth/ed25519.js";
import { ping } from "../src/ping.js";
import { keyPair } from "../src/signMessage.js";

export interface WebhookRunOptions extends commander.RunOptions {
webhookUrl: string;
secretKey: string;
disablePing: boolean;
expiryTime: number;
}

const expirationOption = new Option("--expiry-time <number>", "Time before a transmission becomes invalid (in seconds)").env("EXPIRY_TIME").default(40)

// Run Webhook Sink
const program = commander.program(pkg);
const command = commander.run(program, pkg);
command.addOption(
new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL"),
);
command.addOption(
new Option("--secret-key <string>", "TweetNaCl Secret-key to sign POST data payload")
.makeOptionMandatory()
.env("SECRET_KEY"),
);
command.addOption(new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL"));
command.addOption(new Option("--secret-key <string>", "TweetNaCl Secret-key to sign POST data payload").makeOptionMandatory().env("SECRET_KEY"));
command.addOption(new Option("--disable-ping", "Disable ping on init").env("DISABLE_PING").default(false));
command.addOption(expirationOption);
command.action(action);

program
Expand All @@ -40,14 +38,11 @@ program
.command("ping")
.description("Ping Webhook URL")
.addOption(new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL"))
.addOption(
new Option("--secret-key <string>", "TweetNaCl Secret-key to sign POST data payload")
.makeOptionMandatory()
.env("SECRET_KEY"),
)
.action(async (options) => {
.addOption(new Option("--secret-key <string>", "TweetNaCl Secret-key to sign POST data payload").makeOptionMandatory().env("SECRET_KEY"))
.addOption(expirationOption)
.action(async (options: WebhookRunOptions) => {
logger.settings.type = "hidden";
const response = await ping(options.webhookUrl, options.secretKey);
const response = await ping(options.webhookUrl, options.secretKey, options.expiryTime);
if (response) console.log("✅ OK");
else console.log("⁉️ ERROR");
});
Expand Down
23 changes: 11 additions & 12 deletions examples/bun/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,26 @@ export default {
port: PORT,
async fetch(request) {
// get headers and body from POST request
const timestamp = request.headers.get("x-signature-timestamp");
const signature = request.headers.get("x-signature-ed25519");
const expiry = request.headers.get("x-signature-ed25519-expiry");
const publicKey = request.headers.get("x-signature-ed25519-public-key");

const body = await request.text();

if (!timestamp)
return new Response("missing required timestamp in headers", {
status: 400,
});
if (!signature)
return new Response("missing required signature in headers", {
status: 400,
});
if (!signature) return new Response("missing required signature in headers", { status: 400 });
if (!expiry) return new Response("missing required expiry in headers", { status: 400 });
if (!publicKey) return new Response("missing required public key in headers", { status: 400 });
if (!body) return new Response("missing body", { status: 400 });

if (publicKey !== PUBLIC_KEY) return new Response("unknown public key", { status: 401 });

// validate signature using public key
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(body),
Buffer.from(signature, "hex"),
Buffer.from(PUBLIC_KEY, "hex"),
Buffer.from(publicKey, "hex")
);
console.log({ isVerified, timestamp, signature });
console.log({ isVerified, signature });
console.log(body);

if (!isVerified) {
Expand Down
27 changes: 11 additions & 16 deletions examples/deno/http.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
import nacl from "npm:tweetnacl";
import "https://deno.land/[email protected]/dotenv/load.ts";
import { encode } from "https://deno.land/[email protected]/encoding/hex.ts";
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import nacl from "npm:tweetnacl";

const PORT = Deno.env.get("PORT");
const PUBLIC_KEY = Deno.env.get("PUBLIC_KEY");

const handler = async (request: Request) => {
// get headers and body from POST request
const timestamp = request.headers.get("x-signature-timestamp");
const signature = request.headers.get("x-signature-ed25519");
const expiry = request.headers.get("x-signature-ed25519-expiry");
const publicKey = request.headers.get("x-signature-ed25519-public-key");

const body = await request.text();

if (!timestamp)
return new Response("missing required timestamp in headers", {
status: 400,
});
if (!signature)
return new Response("missing required signature in headers", {
status: 400,
});
if (!signature) return new Response("missing required signature in headers", { status: 400 });
if (!expiry) return new Response("missing required expiry in headers", { status: 400 });
if (!publicKey) return new Response("missing required public key in headers", { status: 400 });
if (!body) return new Response("missing body", { status: 400 });

if (publicKey !== PUBLIC_KEY) return new Response("unknown public key", { status: 401 });

// TO-DO: 🚨 FIX CODE BELOW 🚨
// validate signature using public key
const isVerified = nacl.sign.detached.verify(
new TextEncoder().encode(timestamp + body),
encode(signature),
encode(PUBLIC_KEY),
);
const isVerified = nacl.sign.detached.verify(encode(body), encode(signature), encode(PUBLIC_KEY));

console.dir({ timestamp, signature, isVerified });
console.dir({ signature, isVerified });
console.dir(body);
if (!isVerified) {
return new Response("invalid request signature", { status: 401 });
Expand Down
23 changes: 14 additions & 9 deletions examples/express/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@ app.use(express.text({ type: "application/json" }));

app.use(async (req, res) => {
// get headers and body from POST request
const timestamp = req.headers["x-signature-timestamp"];
const signature = req.headers["x-signature-ed25519"];
const body = req.body;
const signature = request.headers.get("x-signature-ed25519");
const expiry = request.headers.get("x-signature-ed25519-expiry");
const publicKey = request.headers.get("x-signature-ed25519-public-key");

if (!timestamp) return res.send("missing required timestamp in headers").status(400);
if (!signature) return res.send("missing required signature in headers").status(400);
if (!body) return res.send("missing body").status(400);
const body = await request.text();

if (!signature) return new Response("missing required signature in headers", { status: 400 });
if (!expiry) return new Response("missing required expiry in headers", { status: 400 });
if (!publicKey) return new Response("missing required public key in headers", { status: 400 });
if (!body) return new Response("missing body", { status: 400 });

if (publicKey !== PUBLIC_KEY) return new Response("unknown public key", { status: 401 });

// validate signature using public key
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(body),
Buffer.from(signature, "hex"),
Buffer.from(PUBLIC_KEY, "hex"),
Buffer.from(PUBLIC_KEY, "hex")
);

console.dir({ timestamp, signature, isVerified });
console.dir({ signature, isVerified });
console.dir(body);
if (!isVerified) {
return res.send("invalid request signature").status(401);
Expand Down
5 changes: 3 additions & 2 deletions examples/http/ping-isVerified.http
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
POST http://localhost:3000 HTTP/1.1
content-type: application/json
x-signature-ed25519: ce31903c09e8f059df392aeccb5c5be2fd6fc317be17149eba60c6c7dc420c328490f316379a28b59bdf2506772ddbed35abf951ce7c84121279de27161e9b06
x-signature-timestamp: 1686871414
x-signature-ed25519: d26299022b13c25e4889191cdb6f4ab8fa30a524bca44b1742bedeeabb145ca99790ba09467f1365f870aee1236ec8682cdc3690eda4c8266cff512447d7270b
x-signature-ed25519-expiry: 2524626000000
x-signature-ed25519-public-key: a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9

{"message":"PING"}
4 changes: 3 additions & 1 deletion examples/http/ping-not-isVerified.http
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
POST http://localhost:3000 HTTP/1.1
content-type: application/json
x-signature-ed25519: 32c4f322a21ac05e7c9b7374bb702ccd834e56aeebe8320048440833f2e18358014a5790302fbe3ead8c956cdf2b05c9181b787c55c3e40dc6dbc3ab2cfe730f
x-signature-timestamp: 1686871505
x-signature-ed25519-expiry: 2524626000000
x-signature-ed25519-public-key: a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9


{"message":"PING"}
6 changes: 0 additions & 6 deletions examples/http/post-special-characters.http

This file was deleted.

5 changes: 3 additions & 2 deletions examples/http/post.http
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
POST http://localhost:3000 HTTP/1.1
content-type: application/json
x-signature-ed25519: 6ec208fc250059fdb0fa543e01339ee3c6967da6fc7b6bf86dcd8217fa2e130ce2e17a5258fcf9bbe415de223d00eaee2f6949ef3a44594b42e7fb1a53481802
x-signature-timestamp: 1696733583
x-signature-ed25519: d26299022b13c25e4889191cdb6f4ab8fa30a524bca44b1742bedeeabb145ca99790ba09467f1365f870aee1236ec8682cdc3690eda4c8266cff512447d7270b
x-signature-ed25519-expiry: 2524626000000
x-signature-ed25519-public-key: a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9

{"status":200,"cursor":"3ErAq5aeVa2E561uHfBu6qWwLpcyAlJrUAPhKxFLhtnz9HLH3JikBTQmaRqEkKz52RO4HQuk2I3EFi8p88JXtNa8kb4y6XdtRH5-loC_qLHscPOmawkSIu9kDrmJYdLfUzjSagj7c7tRsdLlPKaLY0BkY850fTOwizxW8IYFJqNAv3Mykm2ucMfVgf6fooJArbYgFuyinCzyBz16Kk4LO8TQZ_bN7jx1","session":{"traceId":"06eb726db08090e476eb2dbeff72f1bb","resolvedStartBlock":48458405},"clock":{"timestamp":"2023-10-08T02:53:03.000Z","number":48458410,"id":"3b54021525ec17d05946cfa86b92ab12787fb6f4fe25b59ac5380db39cd6ac73"},"manifest":{"substreamsEndpoint":"https://polygon.substreams.pinax.network:9000","moduleName":"map_block_stats","type":"subtivity.v1.BlockStats","moduleHash":"0a363b2a63aadb76a525208f1973531d3616fbae","chain":"polygon"},"data":{"transactionTraces":"36","traceCalls":"212","uaw":["d6b1cca00889daa9adc1d6e76b9a120086a13aab","675fe893a74815a35f867a12cbdd0637b7d7d6d4","42b07d313de7a38dc5cea48e326e545450cc4322","8ed47843e5030b6f06e6f204fcf2725378bb837a","9ced478d8d6fcaad332d9abf30415c8e48ac8079","21c3de23d98caddc406e3d31b25e807addf33633","2f59cde588b6d3661e8792632844f511d5e2da02","84a611b71254f5fccb1e5a619ad723cad8a03638","7ba865f70e32c9f46f67e33fe06139c8c31a2fad","18264397296fd982e432b4cd4942295c5bca50f8","258cfdaeee1b411bbb63a48cb030faed6720bb15","207cf8cdaec06610d7f9c92fec513e70520ce655","f746fb75a9c1d0f1c9799e434aea2aef90f7aa22","d3961bdbf7ad806b8e870a1cfbf7e54b5247020e","314c9a7a79ec28835ae68bcf5c0fd696141f85b4","2802fa14557b4f1afdf94af082b18c37d5786a2e","74eb675ed60a6f332e156c5a9ac376ee8d4d905d","5543ff441d3b0fcce59aa08eb52f15d27294af21","a1ab1c841898fe94900d00d9312ba954e4f81501","3dd12eb5ae0f1a106fb358c8b99830ab5690a7a2","51fafb35f31c434066267fc86ea24d8424115d2a","8709264ba5b56be8750193dad1a99f8b9d6ad3d6","c2b5f79a5768893b8087667b391c1381c502ab5c","85d8d0fc4e5a1f6dc823ee4baf486758a2fcb19c","7537cb7b7e8083ff8e68cb5c0ca18553ab54946f","d0a8cb58efcee1caee48f3c357074862ca8210dc"]}}
21 changes: 13 additions & 8 deletions examples/node/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as http from "node:http";
import "dotenv/config";
import * as http from "node:http";
import nacl from "tweetnacl";

const PORT = process.env.PORT ?? 3000;
Expand All @@ -24,22 +24,27 @@ server.on("request", async (req, res) => {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });

// get headers and body from POST request
const timestamp = String(req.headers["x-signature-timestamp"]);
const signature = String(req.headers["x-signature-ed25519"]);
const expiry = String(req.headers["x-signature-ed25519-expiry"]);
const publicKey = String(req.headers["x-signature-ed25519-public-key"]);

const body = await rawBody(req);

if (!timestamp) return res.writeHead(400).end("missing required timestamp in headers");
if (!signature) return res.writeHead(400).end("missing required signature in headers");
if (!body) return res.writeHead(400).end("missing body");
if (!signature) return new Response("missing required signature in headers", { status: 400 });
if (!expiry) return new Response("missing required expiry in headers", { status: 400 });
if (!publicKey) return new Response("missing required public key in headers", { status: 400 });
if (!body) return new Response("missing body", { status: 400 });

if (publicKey !== PUBLIC_KEY) return new Response("unknown public key", { status: 401 });

// validate signature using public key
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(body),
Buffer.from(signature, "hex"),
Buffer.from(PUBLIC_KEY, "hex"),
Buffer.from(PUBLIC_KEY, "hex")
);

console.dir({ timestamp, signature, isVerified });
console.dir({ signature, isVerified });
console.dir(body);
if (!isVerified) {
return res.writeHead(401).end("invalid request signature");
Expand Down
11 changes: 5 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import PQueue from "p-queue";
import { http, logger, setup } from "substreams-sink";
import { postWebhook } from "./src/postWebhook.js";
import { signMessage } from "./src/signMessage.js";

import type { SessionInit } from "@substreams/core/proto";
import type { WebhookRunOptions } from "./bin/cli.js";
import { banner } from "./src/banner.js";
import { toText } from "./src/http.js";
import { ping } from "./src/ping.js";

export * from "./src/auth/index.js";

export async function action(options: WebhookRunOptions) {
// Block Emitter
const { emitter, moduleHash } = await setup(options);
Expand All @@ -18,7 +19,7 @@ export async function action(options: WebhookRunOptions) {

// Ping URL to check if it's valid
if (!options.disablePing) {
if (!(await ping(options.webhookUrl, options.secretKey))) {
if (!(await ping(options.webhookUrl, options.secretKey, options.expiryTime))) {
logger.error("exiting from invalid PING response");
process.exit(1);
}
Expand Down Expand Up @@ -53,14 +54,12 @@ export async function action(options: WebhookRunOptions) {
moduleHash,
},
};
// Sign body
const seconds = Number(clock.timestamp.seconds);

const body = JSON.stringify({ ...metadata, data });
const signature = signMessage(seconds, body, options.secretKey);

// Queue POST
queue.add(async () => {
const response = await postWebhook(options.webhookUrl, body, signature, seconds);
const response = await postWebhook(options.webhookUrl, body, options.secretKey, options.expiryTime);
logger.info("POST", response, metadata);
});
});
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.6.2",
"version": "0.7.0",
"name": "substreams-sink-webhook",
"description": "Substreams Sink Webhook",
"type": "module",
Expand All @@ -19,7 +19,7 @@
"test": "bun test",
"prepublishOnly": "tsc",
"build": "bun build --compile ./index.ts --outfile substreams-sink-webhook",
"bench": "bun ./src/*.bench.ts"
"bench": "bun ./src/**/*.bench.ts"
},
"dependencies": {
"p-queue": "latest",
Expand Down
Loading

0 comments on commit 413b4b3

Please sign in to comment.