Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated signing process #17

Merged
merged 12 commits into from
Nov 29, 2023
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