Skip to content

Commit

Permalink
Merge pull request #20 from pinax-network/feature/simply-auth
Browse files Browse the repository at this point in the history
Performance increase to `sign` & `verify` signature
  • Loading branch information
DenisCarriere authored Feb 13, 2024
2 parents 536e6a7 + 529477b commit a361450
Show file tree
Hide file tree
Showing 39 changed files with 451 additions and 1,645 deletions.
15 changes: 11 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# Webhook
SECRET_KEY="<Ed25519 Secret-key>"
PUBLIC_KEY="<Ed25519 Public-key>"
PRIVATE_KEY=<Ed25519 Private Key>
WEBHOOK_URL=http://127.0.0.1:3000
PORT=9102

# Get Substreams API Key
# https://app.pinax.network
# https://app.streamingfast.io/
SUBSTREAMS_API_KEY="<Substreams API Key @ https://pinax.network>"
SUBSTREAMS_API_KEY=<Substreams API Token @ https://pinax.network>
SUBSTREAMS_ENDPOINT=https://eth.substreams.pinax.network:443

# Substreams package
# Substreams Package (*.spkg)
MANIFEST=https://github.com/pinax-network/substreams/releases/download/blocks-v0.1.0/blocks-v0.1.0.spkg
MODULE_NAME=map_blocks
START_BLOCK=-10
PRODUCTION_MODE=true

# Webhook (Optional)
DISABLE_PING=false
DISABLE_SIGNATURE=false
VERBOSE=true
MAXIMUM_ATTEMPTS=100
23 changes: 0 additions & 23 deletions .github/workflows/bun-build.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ docker-compose.yml
substreams-sink-webhook
bun.lockb
.vscode
examples/**/bun.lockb
examples/**/package-lock.json
81 changes: 43 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
## HTTP Server examples

- [`Bun`](/examples/bun) - https://bun.sh/
- [`Deno`](/examples/deno) - https://deno.com/runtime
- [`Express`](/examples/express) - https://expressjs.com/
- [`node:http`](/examples/node:http) - https://nodejs.org/api/http.html
- [POST request](/examples/post.http)
- [`node:http`](/examples/node) - https://nodejs.org/api/http.html
- [POST request](/examples/http)

## 📖 References

Expand All @@ -30,8 +29,8 @@ The POST message will be a JSON object with the following structure:
```http
POST http://localhost:3000 HTTP/1.1
content-type: application/json
x-signature-ed25519: 6ec208fc250059fdb0fa543e01339ee3c6967da6fc7b6bf86dcd8217fa2e130ce2e17a5258fcf9bbe415de223d00eaee2f6949ef3a44594b42e7fb1a53481802
x-signature-timestamp: 1696733583
x-signature-ed25519: 8f01c66ccda5b987c43d913290419572ea586dbef2077fa166c4a84797e1d2c76b305bc67ed43efb1fc841562620a61cb59c4d8a13de689a2e98ead19190f80c
x-signature-timestamp: 1707776632
```

**body**
Expand All @@ -41,23 +40,23 @@ x-signature-timestamp: 1696733583
```json
{
"status": 200,
"cursor": "T0S2BNqDj6a8pKMA6bEXAaWwLpc_DFltXAvkKhhBj4L29XqRiMmiVjVzbU_UxPzyiRLsSV-q2tzLEih6oMZR7oLpwbA2vHI_F39_l9vm_ODoe6CjP1tJdekzCuzcN9DRWD7eYgv7c7EK6dXiMqeMM0ZkNsEjfmLn2j0EpYJWdaUVunUzlT2vdc6Ag_iU-dAQrOV0QLelxyOkUzJ-fx5cbJ6GNaPKuW51bQ==",
"cursor": "OJGbpO9ZnZcwvxW38_FO8KWwLpcyA1lrUQPgKRFL04Py8yCW35v1VTB1O0-Elami3RztQlOp2tmcHC9y9ZQFuoDrxLpj6yU-FXorwoHr_OfqLPumMQwTJ-hgWeuKYNLeWDjTagn4ersEtNGzbvLaY0UxZZUhK2G62z1VptdXJfEWuiJmyjmrIZrRhK-WoNAS_rEkQ7L1xCmhDzJ4K0dTPcSDNPKZuDR2",
"session": {
"traceId": "4ebea20349c16844d92bf6c961f627fa",
"resolvedStartBlock": 32900744
"traceId": "3cbb0a1c772a47a72995d95f4c6d2cff",
"resolvedStartBlock": 53448515
},
"clock": {
"timestamp": "2022-09-09T20:23:38.000Z",
"number": 32901090,
"id": "9058ded4fd65b4de2d772564366f1b61bc328bac7a4c4b87d73ca6ab4bae6be8"
"timestamp": "2024-02-12T22:23:51.000Z",
"number": 53448530,
"id": "f843bc26cea0cbd50b09699546a8a97de6a1727646c17a857c5d8d868fc26142"
},
"manifest": {
"substreamsEndpoint": "https://polygon.substreams.pinax.network:443",
"chain": "polygon",
"finalBlockOnly": "false",
"moduleName": "map_block_stats",
"type": "subtivity.v1.BlockStats",
"moduleHash": "6fb7bbc60685bbfc1cd209d26697639e05efdb24"
"moduleName": "map_blocks",
"type": "sf.substreams.v1.Clock",
"moduleHash": "44c506941d5f30db6cca01692624395d1ac40cd1"
},
"data": {
...
Expand All @@ -71,21 +70,18 @@ x-signature-timestamp: 1696733583
import nacl from "tweetnacl";

// ...HTTP server
const PUBLIC_KEY = "APPLICATION_PUBLIC_KEY";

// get headers and body from POST request
const rawBody = await request.text();
const signature = request.headers.get("x-signature-ed25519");
const expiry = Number(request.headers.get("x-signature-ed25519-expiry"));
const publicKey = request.headers.get("x-signature-ed25519-public-key");

if (new Date().getTime() >= expiry) return new Response("signature expired", { status: 401 });
const timestamp = request.headers.get("x-signature-timestamp");
const body = await request.text();

// validate signature using public key
const payload = JSON.stringify({ exp: expiry, id: publicKey });
const isVerified = nacl.sign.detached.verify(
Buffer.from(payload),
Buffer.from(timestamp + body),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex")
Buffer.from(PUBLIC_KEY, "hex")
);

if (!isVerified) {
Expand All @@ -97,18 +93,26 @@ if (!isVerified) {

```env
# Webhook
SECRET_KEY="<Ed25519 Secret-key>"
PUBLIC_KEY="<Ed25519 Public-key>"
PRIVATE_KEY=<Ed25519 Private Key>
WEBHOOK_URL=http://127.0.0.1:3000
# Substreams endpoint
SUBSTREAMS_API_TOKEN="<Substreams API Token @ https://pinax.network>"
SUBSTREAMS_ENDPOINT=https://polygon.substreams.pinax.network:443
# Substreams package
MANIFEST=https://github.com/pinax-network/subtivity-substreams/releases/download/v0.3.0/subtivity-ethereum-v0.3.0.spkg
MODULE_NAME=map_block_stats
START_BLOCK=-1
PORT=9102
# Get Substreams API Key
# https://app.pinax.network
# https://app.streamingfast.io/
SUBSTREAMS_API_KEY=<Substreams API Token @ https://pinax.network>
# Substreams Package (*.spkg)
MANIFEST=https://github.com/pinax-network/substreams/releases/download/blocks-v0.1.0/blocks-v0.1.0.spkg
MODULE_NAME=map_blocks
START_BLOCK=-10
PRODUCTION_MODE=true
# Webhook (Optional)
DISABLE_PING=false
DISABLE_SIGNATURE=false
VERBOSE=true
MAXIMUM_ATTEMPTS=100
```

## Help
Expand All @@ -127,12 +131,11 @@ Options:
-s --start-block <int> Start block to stream from (defaults to -1, which means the initialBlock of the first module you are streaming) (default: "-1", env: START_BLOCK)
-t --stop-block <int> Stop block to end stream at, inclusively (env: STOP_BLOCK)
-p, --params <string...> Set a params for parameterizable modules. Can be specified multiple times. (ex: -p module1=valA -p module2=valX&valY) (default: [], env: PARAMS)
--substreams-api-token <string> API token for the substream endpoint or API key if '--auth-issue-url' is specified (default: "", env: SUBSTREAMS_API_TOKEN)
--auth-issue-url <string> URL used to issue a token (default: "https://auth.pinax.network/v1/auth/issue", env: AUTH_ISSUE_URL)
--substreams-api-key <string> API key for the Substream endpoint (env: SUBSTREAMS_API_KEY)
--delay-before-start <int> Delay (ms) before starting Substreams (default: 0, env: DELAY_BEFORE_START)
--cursor-path <string> File path or URL to cursor lock file (default: "cursor.lock", env: CURSOR_PATH)
--http-cursor-auth <string> Basic auth credentials for http cursor (ex: username:password) (env: HTTP_CURSOR_AUTH)
--production-mode <boolean> Enable production mode, allows cached substreams data if available (default: "false", env: PRODUCTION_MODE)
--production-mode <boolean> Enable production mode, allows cached Substreams data if available (default: "false", env: PRODUCTION_MODE)
--inactivity-seconds <int> If set, the sink will stop when inactive for over a certain amount of seconds (default: 300, env: INACTIVITY_SECONDS)
--hostname <string> The process will listen on this hostname for any HTTP and Prometheus metrics requests (default: "localhost", env: HOSTNAME)
--port <int> The process will listen on this port for any HTTP and Prometheus metrics requests (default: 9102, env: PORT)
Expand All @@ -142,8 +145,10 @@ Options:
--final-blocks-only <boolean> Only process blocks that have pass finality, to prevent any reorg and undo signal by staying further away from the chain HEAD (default: "false", env: FINAL_BLOCKS_ONLY)
--verbose <boolean> Enable verbose logging (default: "false", env: VERBOSE)
--webhook-url <string> Webhook URL to send POST (env: WEBHOOK_URL)
--secret-key <string> TweetNaCl Secret-key to sign POST data payload (env: SECRET_KEY)
--disable-ping Disable ping on init (default: false, env: DISABLE_PING)
--private-key <string> Ed25519 private key to sign POST data payload (env: PRIVATE_KEY)
--disable-ping <boolean> Disable ping on init (choices: "true", "false", default: false, env: DISABLE_PING)
--disable-signature <boolean> Disable Ed25519 signature (choices: "true", "false", default: false, env: DISABLE_SIGNATURE)
--maximum-attempts <number> Maximum attempts to retry POST (default: 100, env: MAXIMUM_ATTEMPTS)
-h, --help display help for command
```

Expand Down
29 changes: 16 additions & 13 deletions bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,43 @@ import { ping } from "../src/ping.js";

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

const expirationOption = new Option("--expiry-time <number>", "Time before a transmission becomes invalid (in seconds)").env("EXPIRY_TIME").default(40)
const webhookUrlOption = new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL");
const privateKeyOption = new Option("--private-key <string>", "Ed25519 private key to sign POST data payload").makeOptionMandatory().env("PRIVATE_KEY");

// 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("--disable-ping", "Disable ping on init").env("DISABLE_PING").default(false));
command.addOption(expirationOption);
command.addOption(webhookUrlOption);
command.addOption(privateKeyOption);
command.addOption(new Option("--disable-ping <boolean>", "Disable ping on init").choices(["true", "false"]).env("DISABLE_PING").default(false));
command.addOption(new Option("--disable-signature <boolean>", "Disable Ed25519 signature").choices(["true", "false"]).env("DISABLE_SIGNATURE").default(false));
command.addOption(new Option("--maximum-attempts <number>", "Maximum attempts to retry POST").env("MAXIMUM_ATTEMPTS").default(100));
command.action(action);

program
.command("keypair")
.description("Generate TweetNaCl keypair")
.action(() => {
const { publicKey, secretKey } = keyPair();
const { publicKey, privateKey } = keyPair();
console.log(`PUBLIC_KEY=${publicKey}`);
console.log(`SECRET_KEY=${secretKey}`);
console.log(`PRIVATE_KEY=${privateKey}`);
});

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"))
.addOption(expirationOption)
.addOption(webhookUrlOption)
.addOption(privateKeyOption)
.action(async (options: WebhookRunOptions) => {
logger.settings.type = "hidden";
const response = await ping(options.webhookUrl, options.secretKey, options.expiryTime);
const response = await ping(options.webhookUrl, options.privateKey);
if (response) console.log("✅ OK");
else console.log("⁉️ ERROR");
});
Expand Down
4 changes: 2 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"formatter": {
"indentStyle": "space",
"lineWidth": 120
"lineWidth": 160
},
"linter": {
"rules": {
Expand All @@ -39,6 +39,6 @@
}
},
"organizeImports": {
"enabled": true
"enabled": false
}
}
13 changes: 1 addition & 12 deletions examples/bun/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
FROM oven/bun

WORKDIR /app

ENV PUBLIC_KEY $PUBLIC_KEY
ENV PORT $PORT

COPY bun.lockb ./
COPY package*.json ./

RUN bun install

COPY . .

RUN bun install
CMD [ "bun", "http.ts" ]
Binary file removed examples/bun/bun.lockb
Binary file not shown.
24 changes: 8 additions & 16 deletions examples/bun/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "dotenv/config";
import nacl from "tweetnacl";
import { ed25519 } from "@noble/curves/ed25519.js";

const PORT = process.env.PORT ?? 3000;
const PUBLIC_KEY = process.env.PUBLIC_KEY ?? "a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9";
Expand All @@ -12,28 +12,20 @@ export default {
async fetch(request) {
// get headers and body from POST request
const signature = request.headers.get("x-signature-ed25519");
const expiry = Number(request.headers.get("x-signature-ed25519-expiry"));
const publicKey = request.headers.get("x-signature-ed25519-public-key");

const timestamp = request.headers.get("x-signature-timestamp");
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 (!timestamp) return new Response("missing required timestamp in headers", { status: 400 });
if (!body) return new Response("missing body", { status: 400 });

if (new Date().getTime() >= expiry) return new Response("signature expired", { status: 401 });
if (publicKey !== PUBLIC_KEY) return new Response("unknown public key", { status: 401 });

// validate signature using public key
console.log({signature, expiry, publicKey});
const payload = JSON.stringify({ exp: expiry, id: publicKey });
const isVerified = nacl.sign.detached.verify(
Buffer.from(payload),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex"),
const isVerified = ed25519.verify(
signature,
Buffer.from(timestamp + body),
PUBLIC_KEY,
);
console.log({ isVerified, signature });
console.log({ isVerified, timestamp, signature });
console.log(body);

if (!isVerified) {
Expand Down
2 changes: 1 addition & 1 deletion examples/bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"start": "bun http.ts"
},
"dependencies": {
"tweetnacl": "latest",
"@noble/curves": "latest",
"dotenv": "latest"
}
}
10 changes: 0 additions & 10 deletions examples/deno/Dockerfile

This file was deleted.

Loading

0 comments on commit a361450

Please sign in to comment.