Skip to content

Commit

Permalink
feat: added request validation (outcommented for the moment)
Browse files Browse the repository at this point in the history
can't really test it until an actual proposal comes in,
so we'll test this out in prod with the monitoring
events from sorted oracles once that's ready
  • Loading branch information
chapati23 committed Jul 22, 2024
1 parent 99f7d3b commit 2640b6e
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 10 deletions.
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
# Get it via `gcloud projects list --filter="name:governance-watchdog*" --format="value(projectId)")`
GCP_PROJECT_ID=

# Required for the function to be able to look up the Discord Webhook URL and Telegram Bot Token in GCP Secret Manager.
# Get it via `gcloud secrets list`
DISCORD_WEBHOOK_URL_SECRET_ID=
TELEGRAM_BOT_TOKEN_SECRET_ID=
# Required for the function to be able to look up secrets in GCP Secret Manager.
# Get them via `gcloud secrets list`
DISCORD_WEBHOOK_URL_SECRET_ID=discord-webhook-url
QUICKNODE_SECURITY_TOKEN_SECRET_ID=quicknode-security-token
TELEGRAM_BOT_TOKEN_SECRET_ID=telegram-bot-token

# Get it via inviting @MissRose_bot to the telegram group and then using the `/id` command (please remove the bot after you're done)
TELEGRAM_CHAT_ID=
9 changes: 5 additions & 4 deletions infra/cloud_function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" {

environment_variables = {
# Necessary for the function to be able to find the secrets in Secret Manager
GCP_PROJECT_ID = module.bootstrap.seed_project_id
DISCORD_WEBHOOK_URL_SECRET_ID = google_secret_manager_secret.discord_webhook_url.secret_id
TELEGRAM_BOT_TOKEN_SECRET_ID = google_secret_manager_secret.telegram_bot_token.secret_id
TELEGRAM_CHAT_ID = var.telegram_chat_id
GCP_PROJECT_ID = module.bootstrap.seed_project_id
DISCORD_WEBHOOK_URL_SECRET_ID = google_secret_manager_secret.discord_webhook_url.secret_id
TELEGRAM_BOT_TOKEN_SECRET_ID = google_secret_manager_secret.telegram_bot_token.secret_id
TELEGRAM_CHAT_ID = var.telegram_chat_id
QUICKNODE_SECURITY_TOKEN_SECRET_ID = google_secret_manager_secret.quicknode_security_token.secret_id

# Logs execution ID for easier debugging => https://cloud.google.com/functions/docs/monitoring/logging#viewing_runtime_logs
LOG_EXECUTION_ID = true
Expand Down
17 changes: 15 additions & 2 deletions infra/secret-manager.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ resource "google_secret_manager_secret_version" "discord_webhook_url" {
}

# Creates a new secret for the Telegram Bot Token.
# Terraform will try to look up the webhook URL from terraform.tfvars,
# and if it can't find it locally it will prompt the user to enter it manually.
resource "google_secret_manager_secret" "telegram_bot_token" {
project = module.bootstrap.seed_project_id
secret_id = "telegram-bot-token"
Expand All @@ -31,3 +29,18 @@ resource "google_secret_manager_secret_version" "telegram_bot_token" {
secret = google_secret_manager_secret.telegram_bot_token.id
secret_data = var.telegram_bot_token
}

# Creates a new secret for the Quicknode Security Token which is used to verify that requests to the Cloud Function are coming from Quicknode.
resource "google_secret_manager_secret" "quicknode_security_token" {
project = module.bootstrap.seed_project_id
secret_id = "quicknode-security-token"

replication {
auto {}
}
}

resource "google_secret_manager_secret_version" "quicknode_security_token" {
secret = google_secret_manager_secret.quicknode_security_token.id
secret_data = quicknode_destination.destination.token
}
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { JSONSchemaType, envSchema } from "env-schema";
export interface Env {
GCP_PROJECT_ID: string;
DISCORD_WEBHOOK_URL_SECRET_ID: string;
QUICKNODE_SECURITY_TOKEN_SECRET_ID: string;
TELEGRAM_BOT_TOKEN_SECRET_ID: string;
TELEGRAM_CHAT_ID: string;
}
Expand All @@ -12,12 +13,14 @@ const schema: JSONSchemaType<Env> = {
required: [
"GCP_PROJECT_ID",
"DISCORD_WEBHOOK_URL_SECRET_ID",
"QUICKNODE_SECURITY_TOKEN_SECRET_ID",
"TELEGRAM_BOT_TOKEN_SECRET_ID",
"TELEGRAM_CHAT_ID",
],
properties: {
GCP_PROJECT_ID: { type: "string" },
DISCORD_WEBHOOK_URL_SECRET_ID: { type: "string" },
QUICKNODE_SECURITY_TOKEN_SECRET_ID: { type: "string" },
TELEGRAM_BOT_TOKEN_SECRET_ID: { type: "string" },
TELEGRAM_CHAT_ID: { type: "string" },
},
Expand Down
28 changes: 28 additions & 0 deletions src/get-quicknode-security-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
import config from "./config.js";

/**
* Get the Quicknode Security Token from GCloud Secret Manager
*
* NOTE: This will fail locally because the local function will lack the necessary permissions to access the secret.
* That's why read the webhook URL from our .env file when running locally. We could probably make it work by having
* the local function impersonate the service account used by the function in GCP, but that was a rabbit hole I didn't
* want to go down when a simple .env approach also works for local testing.
*/
export default async function getQuicknodeSecurityToken(): Promise<string> {
const secretManager = new SecretManagerServiceClient();
const secretFullResourceName = `projects/${config.GCP_PROJECT_ID}/secrets/${config.QUICKNODE_SECURITY_TOKEN_SECRET_ID}/versions/latest`;
const [version] = await secretManager.accessSecretVersion({
name: secretFullResourceName,
});

const securityToken = version.payload?.data?.toString();

if (!securityToken) {
throw new Error(
"Failed to retrieve Quicknode security token from secret manager",
);
}

return securityToken;
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import type {
import parseTransactionReceipts from "./parse-transaction-receipts";
import sendDiscordNotification from "./send-discord-notification";
import sendTelegramNotification from "./send-telegram-notification";
// import validateRequestOrigin from "./validate-request-origin";

export const watchdogNotifier: HttpFunction = async (
req: Request,
res: Response,
) => {
try {
// TODO: Activate this after we've verified it's working via SortedOracle events
// if (process.env.NODE_ENV !== "development") {
// await validateRequestOrigin(req);
// }
const parsedEvents = parseTransactionReceipts(req.body);

for (const parsedEvent of parsedEvents) {
Expand Down
48 changes: 48 additions & 0 deletions src/validate-request-origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Request } from "@google-cloud/functions-framework";
import crypto from "crypto";
import getQuicknodeSecurityToken from "./get-quicknode-security-token";

export default async function validateRequestOrigin(req: Request) {
const quicknodeSecurityToken = await getQuicknodeSecurityToken();
const givenSignature = req.headers["x-qn-signature"];
const nonce = req.headers["x-qn-nonce"];
const contentHash = req.headers["x-qn-content-hash"];
const timestamp = req.headers["x-qn-timestamp"];

if (!nonce || typeof nonce !== "string") {
console.error("No valid quicknode nonce found in request headers:", nonce);
return;
}

if (!contentHash || typeof contentHash !== "string") {
console.error(
"No valid quicknode content hash found in request headers:",
contentHash,
);
return;
}

if (!timestamp || typeof timestamp !== "string") {
console.error(
"No valid quicknode timestamp found in request headers:",
contentHash,
);
return;
}

const hmac = crypto.createHmac("sha256", quicknodeSecurityToken);
hmac.update(`${nonce}${contentHash}${timestamp}`);

const expectedSignature = hmac.digest("base64");

if (givenSignature === expectedSignature) {
console.log(
"The signature given matches the expected signature and is valid.",
);
return;
} else {
throw new Error(
"Signature validation failed for the request's Quicknode signature header",
);
}
}

0 comments on commit 2640b6e

Please sign in to comment.