From 1ec3a9e5a2e4092b39a388ca4a7dde851e47edb4 Mon Sep 17 00:00:00 2001 From: sukhman Date: Thu, 19 Dec 2024 02:00:18 +0530 Subject: [PATCH 1/6] Create a JWK model in schema --- package.json | 4 +- pnpm-lock.yaml | 70 +++++++++---------- .../migration.sql | 9 +++ prisma/schema.prisma | 7 ++ prisma/seed.ts | 8 +++ src/lib/utils.ts | 30 ++++++++ 6 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql diff --git a/package.json b/package.json index aa3ce77..74f2a7a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/material": "^5.15.19", - "@prisma/client": "5.11.0", + "@prisma/client": "5.17.0", "@witnessco/client": "^0.4.2", "axios": "^1.7.2", "googleapis": "^137.1.0", @@ -46,7 +46,7 @@ "autoprefixer": "^10.4.19", "dkim": "^0.8.0", "postcss": "^8.4.38", - "prisma": "5.11.0", + "prisma": "5.17.0", "swagger-ui-react": "^5.17.14", "tsx": "^4.11.0", "typescript": "^5.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c661fe2..9a07bfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^5.15.19 version: 5.15.19(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@prisma/client': - specifier: 5.11.0 - version: 5.11.0(prisma@5.11.0) + specifier: 5.17.0 + version: 5.17.0(prisma@5.17.0) '@witnessco/client': specifier: ^0.4.2 version: 0.4.2(viem@2.13.1(typescript@5.4.5)(zod@3.23.8)) @@ -85,8 +85,8 @@ importers: specifier: ^8.4.38 version: 8.4.38 prisma: - specifier: 5.11.0 - version: 5.11.0 + specifier: 5.17.0 + version: 5.17.0 swagger-ui-react: specifier: ^5.17.14 version: 5.17.14(@types/react@18.3.3)(ramda@0.30.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -529,8 +529,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@prisma/client@5.11.0': - resolution: {integrity: sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==} + '@prisma/client@5.17.0': + resolution: {integrity: sha512-N2tnyKayT0Zf7mHjwEyE8iG7FwTmXDHFZ1GnNhQp0pJUObsuel4ZZ1XwfuAYkq5mRIiC/Kot0kt0tGCfLJ70Jw==} engines: {node: '>=16.13'} peerDependencies: prisma: '*' @@ -538,20 +538,20 @@ packages: prisma: optional: true - '@prisma/debug@5.11.0': - resolution: {integrity: sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==} + '@prisma/debug@5.17.0': + resolution: {integrity: sha512-l7+AteR3P8FXiYyo496zkuoiJ5r9jLQEdUuxIxNCN1ud8rdbH3GTxm+f+dCyaSv9l9WY+29L9czaVRXz9mULfg==} - '@prisma/engines-version@5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102': - resolution: {integrity: sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==} + '@prisma/engines-version@5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053': + resolution: {integrity: sha512-tUuxZZysZDcrk5oaNOdrBnnkoTtmNQPkzINFDjz7eG6vcs9AVDmA/F6K5Plsb2aQc/l5M2EnFqn3htng9FA4hg==} - '@prisma/engines@5.11.0': - resolution: {integrity: sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==} + '@prisma/engines@5.17.0': + resolution: {integrity: sha512-+r+Nf+JP210Jur+/X8SIPLtz+uW9YA4QO5IXA+KcSOBe/shT47bCcRMTYCbOESw3FFYFTwe7vU6KTWHKPiwvtg==} - '@prisma/fetch-engine@5.11.0': - resolution: {integrity: sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==} + '@prisma/fetch-engine@5.17.0': + resolution: {integrity: sha512-ESxiOaHuC488ilLPnrv/tM2KrPhQB5TRris/IeIV4ZvUuKeaicCl4Xj/JCQeG9IlxqOgf1cCg5h5vAzlewN91Q==} - '@prisma/get-platform@5.11.0': - resolution: {integrity: sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==} + '@prisma/get-platform@5.17.0': + resolution: {integrity: sha512-UlDgbRozCP1rfJ5Tlkf3Cnftb6srGrEQ4Nm3og+1Se2gWmCZ0hmPIi+tQikGDUVLlvOWx3Gyi9LzgRP+HTXV9w==} '@readme/better-ajv-errors@1.6.0': resolution: {integrity: sha512-9gO9rld84Jgu13kcbKRU+WHseNhaVt76wYMeRDGsUGYxwJtI3RmEJ9LY9dZCYQGI8eUZLuxb5qDja0nqklpFjQ==} @@ -1990,8 +1990,8 @@ packages: pretty-format@3.8.0: resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - prisma@5.11.0: - resolution: {integrity: sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==} + prisma@5.17.0: + resolution: {integrity: sha512-m4UWkN5lBE6yevqeOxEvmepnL5cNPEjzMw2IqDB59AcEV6w7D8vGljDLd1gPFH+W6gUxw9x7/RmN5dCS/WTPxA==} engines: {node: '>=16.13'} hasBin: true @@ -3067,30 +3067,30 @@ snapshots: '@popperjs/core@2.11.8': {} - '@prisma/client@5.11.0(prisma@5.11.0)': + '@prisma/client@5.17.0(prisma@5.17.0)': optionalDependencies: - prisma: 5.11.0 + prisma: 5.17.0 - '@prisma/debug@5.11.0': {} + '@prisma/debug@5.17.0': {} - '@prisma/engines-version@5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102': {} + '@prisma/engines-version@5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053': {} - '@prisma/engines@5.11.0': + '@prisma/engines@5.17.0': dependencies: - '@prisma/debug': 5.11.0 - '@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102 - '@prisma/fetch-engine': 5.11.0 - '@prisma/get-platform': 5.11.0 + '@prisma/debug': 5.17.0 + '@prisma/engines-version': 5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053 + '@prisma/fetch-engine': 5.17.0 + '@prisma/get-platform': 5.17.0 - '@prisma/fetch-engine@5.11.0': + '@prisma/fetch-engine@5.17.0': dependencies: - '@prisma/debug': 5.11.0 - '@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102 - '@prisma/get-platform': 5.11.0 + '@prisma/debug': 5.17.0 + '@prisma/engines-version': 5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053 + '@prisma/get-platform': 5.17.0 - '@prisma/get-platform@5.11.0': + '@prisma/get-platform@5.17.0': dependencies: - '@prisma/debug': 5.11.0 + '@prisma/debug': 5.17.0 '@readme/better-ajv-errors@1.6.0(ajv@8.14.0)': dependencies: @@ -4934,9 +4934,9 @@ snapshots: pretty-format@3.8.0: {} - prisma@5.11.0: + prisma@5.17.0: dependencies: - '@prisma/engines': 5.11.0 + '@prisma/engines': 5.17.0 prismjs@1.27.0: {} diff --git a/prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql b/prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql new file mode 100644 index 0000000..b41433a --- /dev/null +++ b/prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "JsonWebKeySets" ( + "id" SERIAL NOT NULL, + "x509Certificate" TEXT NOT NULL, + "jwks" TEXT NOT NULL, + "lastUpdated" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "JsonWebKeySets_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f4cf1f3..2092b62 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -68,3 +68,10 @@ model EmailPairGcdResult { @@id([emailSignatureA_id, emailSignatureB_id]) } + +model JsonWebKeySets { + id Int @id @default(autoincrement()) + x509Certificate String + jwks String + lastUpdated DateTime @updatedAt +} \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts index 3c85ad9..9f4b2d4 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,5 @@ import { PrismaClient } from '@prisma/client' +import { fetchJsonWebKeySet, fetchx509Cert } from '@/lib/utils' const prisma = new PrismaClient() @@ -30,6 +31,13 @@ async function main() { }); } } + await prisma.jsonWebKeySets.create({ + data: { + jwks: await fetchJsonWebKeySet(), + x509Certificate: await fetchx509Cert() + + } + }); } main() diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 936a759..283aee9 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -169,3 +169,33 @@ export function keySourceIdentifierToHumanReadable(sourceIdentifierStr: string) return 'Unknown'; } } + +export async function fetchJsonWebKeySet(): Promise { + try { + const response = await fetch('https://www.googleapis.com/oauth2/v3/certs'); + if (!response.ok) { + throw new Error('Cannot fetch Google JSON Web Key Set'); + } + const jsonData = await response.json(); + const jsonWebKeySet = JSON.stringify(jsonData); + return jsonWebKeySet; + } catch (error) { + console.error('Error fetching JSON Web Key Set:', error); + throw error; + } +} + +export async function fetchx509Cert(): Promise { + try { + const response = await fetch('https://www.googleapis.com/oauth2/v1/certs'); + if (!response.ok) { + throw new Error('Cannot fetch Google X.509 certificate'); + } + const jsonData = await response.json(); + const x509Cert = JSON.stringify(jsonData); + return x509Cert; + } catch (error) { + console.error('Error fetching X.509 certificate:', error); + throw error; + } +} \ No newline at end of file From f4179b814d72d65735e4f9db66e8028484269a05 Mon Sep 17 00:00:00 2001 From: sukhman Date: Thu, 19 Dec 2024 03:06:51 +0530 Subject: [PATCH 2/6] Add endpoints to fetch and update JWKSet --- src/app/api/key/fetchJwkSet/route.ts | 24 +++++++++++++ src/app/api/key/updateJwkSet/route.ts | 24 +++++++++++++ src/lib/db.ts | 50 ++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/app/api/key/fetchJwkSet/route.ts create mode 100644 src/app/api/key/updateJwkSet/route.ts diff --git a/src/app/api/key/fetchJwkSet/route.ts b/src/app/api/key/fetchJwkSet/route.ts new file mode 100644 index 0000000..bf64441 --- /dev/null +++ b/src/app/api/key/fetchJwkSet/route.ts @@ -0,0 +1,24 @@ +// /api/key/fetchJwkSet + +import { getJWKeySetRecord } from "@/lib/db"; +import { NextRequest, NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { RateLimiterMemory } from "rate-limiter-flexible"; +import { checkRateLimiter } from "@/lib/utils"; + +const rateLimiter = new RateLimiterMemory({ points: 5, duration: 10 }); + +export async function GET(request: NextRequest) { + try { + await checkRateLimiter(rateLimiter, headers(), 1); + } catch (error: any) { + return NextResponse.json("Rate limit exceeded", { status: 429 }); + } + + try { + const JwkSet = await getJWKeySetRecord(); + return NextResponse.json(JwkSet, { status: 200 }); + } catch (error: any) { + return NextResponse.json(error.toString(), { status: 500 }); + } +} diff --git a/src/app/api/key/updateJwkSet/route.ts b/src/app/api/key/updateJwkSet/route.ts new file mode 100644 index 0000000..8cbecca --- /dev/null +++ b/src/app/api/key/updateJwkSet/route.ts @@ -0,0 +1,24 @@ +// api/key/updateJwkSet + +import { getJWKeySetRecord, updateJWKeySet } from "@/lib/db"; +import { NextRequest, NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { RateLimiterMemory } from "rate-limiter-flexible"; +import { checkRateLimiter } from "@/lib/utils"; + +const rateLimiter = new RateLimiterMemory({ points: 5, duration: 10 }); + +export async function POST(request: NextRequest) { + try { + await checkRateLimiter(rateLimiter, headers(), 1); + } catch (error: any) { + return NextResponse.json("Rate limit exceeded", { status: 429 }); + } + + try { + const updatedJwkSet = await updateJWKeySet(); + return NextResponse.json(updatedJwkSet, { status: 200 }); + } catch (error: any) { + return NextResponse.json(error.toString(), { status: 500 }); + } +} diff --git a/src/lib/db.ts b/src/lib/db.ts index a90ecdd..6a89d3f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,5 +1,5 @@ import { PrismaClient, Prisma, DkimRecord, DomainSelectorPair } from '@prisma/client' -import { DnsDkimFetchResult } from './utils'; +import { DnsDkimFetchResult, fetchJsonWebKeySet, fetchx509Cert } from './utils'; const createPrismaClient = () => { let prismaUrl = new URL(process.env.POSTGRES_PRISMA_URL as string); @@ -92,3 +92,51 @@ export async function createDkimRecord(dsp: DomainSelectorPair, dkimDsnRecord: D console.log(`created dkim record ${recordToString(dkimRecord)} for domain/selector pair ${dspToString(dsp)}`); return dkimRecord; } + + +export async function getLastJWKeySet() { + try { + const lastJwtKey = await prisma.jsonWebKeySets.findFirst({ + orderBy: { + lastUpdated: "desc", + }, + }); + + return lastJwtKey; + } catch (error) { + console.error("Error fetching the last JWT key:", error); + throw error; + } +} + +export async function updateJWKeySet() { + try { + const lastJWKeySet = await getLastJWKeySet(); + const latestJWKeySet = await fetchJsonWebKeySet(); + if (lastJWKeySet?.jwks != latestJWKeySet) { + return await prisma.jsonWebKeySets.create({ + data: { + jwks: await fetchJsonWebKeySet(), + x509Certificate: await fetchx509Cert(), + }, + }); + } else { + return await prisma.jsonWebKeySets.update({ + where: { + id: lastJWKeySet.id, + }, + data: { + lastUpdated: new Date(), + }, + }); + } + } catch (error) { + console.error("Error updating the JWT key:", error); + throw error; + } +} + +export async function getJWKeySetRecord() { + const jwkSetRecord = await prisma.jsonWebKeySets.findMany(); + return jwkSetRecord; +} From 4090492a1b208115f8cb9718c4bd7c138b48ce8f Mon Sep 17 00:00:00 2001 From: sukhman Date: Thu, 19 Dec 2024 04:00:19 +0530 Subject: [PATCH 3/6] Fix archive updation Signed-off-by: sukhman --- src/lib/db.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/db.ts b/src/lib/db.ts index 6a89d3f..1a81fd1 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -93,7 +93,6 @@ export async function createDkimRecord(dsp: DomainSelectorPair, dkimDsnRecord: D return dkimRecord; } - export async function getLastJWKeySet() { try { const lastJwtKey = await prisma.jsonWebKeySets.findFirst({ @@ -112,9 +111,9 @@ export async function getLastJWKeySet() { export async function updateJWKeySet() { try { const lastJWKeySet = await getLastJWKeySet(); - const latestJWKeySet = await fetchJsonWebKeySet(); - if (lastJWKeySet?.jwks != latestJWKeySet) { - return await prisma.jsonWebKeySets.create({ + const latestx509Cert = await fetchx509Cert(); + if (lastJWKeySet?.x509Certificate != latestx509Cert) { + return await prisma.jsonWebKeySets.create({ data: { jwks: await fetchJsonWebKeySet(), x509Certificate: await fetchx509Cert(), From fe5de7b2c6c1365878d57b8261e845c64ddb14eb Mon Sep 17 00:00:00 2001 From: sukhman Date: Sat, 4 Jan 2025 02:28:31 +0530 Subject: [PATCH 4/6] Added a frontend for archived JWKeys and certs --- src/app/page.tsx | 39 ++++++++++--- src/components/JWKArchiveDisplay.tsx | 83 ++++++++++++++++++++++++++++ src/lib/utils.ts | 4 +- 3 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 src/components/JWKArchiveDisplay.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 6bc7777..6341601 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,21 +2,46 @@ import DomainSearchResults from "@/components/DomainSearchResults"; import { SearchInput } from "@/components/SearchInput"; +import { JWKArchiveDisplayList } from "@/components/JWKArchiveDisplay"; import Link from "next/link"; import { useState } from "react"; export default function Home({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { const domainQuery = searchParams?.domain?.toString(); const [isLoading, setIsLoading] = useState(true); + const [selectedArchive, setSelectedArchive] = useState<'dkim' | 'jwk'>('dkim'); + + const handleArchiveChange = (event: React.ChangeEvent) => { + setSelectedArchive(event.target.value as 'dkim' | 'jwk'); + }; + return (
-

- - DKIM Archive - -

- - + + +
+ +
+ + {selectedArchive === 'dkim' ? ( + <> + + + + ) : ( + <> + + + )} +

diff --git a/src/components/JWKArchiveDisplay.tsx b/src/components/JWKArchiveDisplay.tsx new file mode 100644 index 0000000..11a0579 --- /dev/null +++ b/src/components/JWKArchiveDisplay.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { FC, useEffect, useState, ReactNode } from 'react'; +import { Timestamp } from './Timestamp'; +import { cardStyle } from './styles'; + +interface CardData { + id: number; + x509Certificate: string; + jwks: string; + lastUpdated: Date; +} + +interface RowProps { + label: string; + children: ReactNode; +} + +const Row: FC = ({ label: title, children }) => ( +
+
{title}
+
{children}
+
+); + +interface CardProps { + data: CardData; +} + +export const JWKArchiveDisplay: FC = ({ data }) => ( +
+ {data.id} + +
+        {data.x509Certificate}
+      
+
+ +
+        {data.jwks}
+      
+
+ + + +
+); + +interface JWKArchiveDisplayListProps {} + +export const JWKArchiveDisplayList: FC = () => { + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/api/key/fetchJwkSet'); + const data: CardData[] = await response.json(); + console.log("data = ", data); + setRecords(data); + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (loading) { + return
Loading...
; + } + + return ( +
+ {records.map(record => ( + + ))} +
+ ); +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 283aee9..aeb28b5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -177,7 +177,7 @@ export async function fetchJsonWebKeySet(): Promise { throw new Error('Cannot fetch Google JSON Web Key Set'); } const jsonData = await response.json(); - const jsonWebKeySet = JSON.stringify(jsonData); + const jsonWebKeySet = JSON.stringify(jsonData, null, 2); return jsonWebKeySet; } catch (error) { console.error('Error fetching JSON Web Key Set:', error); @@ -192,7 +192,7 @@ export async function fetchx509Cert(): Promise { throw new Error('Cannot fetch Google X.509 certificate'); } const jsonData = await response.json(); - const x509Cert = JSON.stringify(jsonData); + const x509Cert = JSON.stringify(jsonData, null, 2); return x509Cert; } catch (error) { console.error('Error fetching X.509 certificate:', error); From ebec0e8b20fc074b97fd47c63281ebab3290951d Mon Sep 17 00:00:00 2001 From: sukhman Date: Sat, 4 Jan 2025 03:16:23 +0530 Subject: [PATCH 5/6] Pass Json web key set and timestamp to witness --- .../migration.sql | 1 + prisma/schema.prisma | 1 + prisma/seed.ts | 4 +- src/app/api/key/updateJwkSet/route.ts | 2 + src/components/JWKArchiveDisplay.tsx | 15 +++++--- src/components/SelectorResult.tsx | 9 ++--- src/lib/db.ts | 1 + src/lib/generateWitness.ts | 37 ++++++++++++++++++- src/lib/utils.ts | 20 ++++++++++ 9 files changed, 76 insertions(+), 14 deletions(-) rename prisma/migrations/{20241218200223_add_json_web_keys_set_model => 20250103214016_add_json_web_keys_set}/migration.sql (87%) diff --git a/prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql b/prisma/migrations/20250103214016_add_json_web_keys_set/migration.sql similarity index 87% rename from prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql rename to prisma/migrations/20250103214016_add_json_web_keys_set/migration.sql index b41433a..47a6e95 100644 --- a/prisma/migrations/20241218200223_add_json_web_keys_set_model/migration.sql +++ b/prisma/migrations/20250103214016_add_json_web_keys_set/migration.sql @@ -4,6 +4,7 @@ CREATE TABLE "JsonWebKeySets" ( "x509Certificate" TEXT NOT NULL, "jwks" TEXT NOT NULL, "lastUpdated" TIMESTAMP(3) NOT NULL, + "provenanceVerified" BOOLEAN, CONSTRAINT "JsonWebKeySets_pkey" PRIMARY KEY ("id") ); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2092b62..3cf1fc3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,4 +74,5 @@ model JsonWebKeySets { x509Certificate String jwks String lastUpdated DateTime @updatedAt + provenanceVerified Boolean? } \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts index 9f4b2d4..25bead3 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -34,8 +34,8 @@ async function main() { await prisma.jsonWebKeySets.create({ data: { jwks: await fetchJsonWebKeySet(), - x509Certificate: await fetchx509Cert() - + x509Certificate: await fetchx509Cert(), + provenanceVerified: false } }); } diff --git a/src/app/api/key/updateJwkSet/route.ts b/src/app/api/key/updateJwkSet/route.ts index 8cbecca..fb0c1a8 100644 --- a/src/app/api/key/updateJwkSet/route.ts +++ b/src/app/api/key/updateJwkSet/route.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from "next/server"; import { headers } from "next/headers"; import { RateLimiterMemory } from "rate-limiter-flexible"; import { checkRateLimiter } from "@/lib/utils"; +import { generateJWKWitness } from "@/lib/generateWitness"; const rateLimiter = new RateLimiterMemory({ points: 5, duration: 10 }); @@ -17,6 +18,7 @@ export async function POST(request: NextRequest) { try { const updatedJwkSet = await updateJWKeySet(); + generateJWKWitness(updatedJwkSet); return NextResponse.json(updatedJwkSet, { status: 200 }); } catch (error: any) { return NextResponse.json(error.toString(), { status: 500 }); diff --git a/src/components/JWKArchiveDisplay.tsx b/src/components/JWKArchiveDisplay.tsx index 11a0579..84abfc5 100644 --- a/src/components/JWKArchiveDisplay.tsx +++ b/src/components/JWKArchiveDisplay.tsx @@ -3,12 +3,15 @@ import { FC, useEffect, useState, ReactNode } from 'react'; import { Timestamp } from './Timestamp'; import { cardStyle } from './styles'; +import { ProvenanceIcon } from './SelectorResult'; +import { getCanonicalJWKRecordString } from '@/lib/utils'; -interface CardData { +interface JWKData { id: number; x509Certificate: string; jwks: string; lastUpdated: Date; + provenanceVerified: boolean; } interface RowProps { @@ -24,7 +27,7 @@ const Row: FC = ({ label: title, children }) => ( ); interface CardProps { - data: CardData; + data: JWKData; } export const JWKArchiveDisplay: FC = ({ data }) => ( @@ -41,7 +44,8 @@ export const JWKArchiveDisplay: FC = ({ data }) => ( - +   + {data.provenanceVerified && }
); @@ -49,15 +53,14 @@ export const JWKArchiveDisplay: FC = ({ data }) => ( interface JWKArchiveDisplayListProps {} export const JWKArchiveDisplayList: FC = () => { - const [records, setRecords] = useState([]); + const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { try { const response = await fetch('/api/key/fetchJwkSet'); - const data: CardData[] = await response.json(); - console.log("data = ", data); + const data: JWKData[] = await response.json(); setRecords(data); } catch (error) { console.error('Error fetching data:', error); diff --git a/src/components/SelectorResult.tsx b/src/components/SelectorResult.tsx index af43a84..f5ec535 100644 --- a/src/components/SelectorResult.tsx +++ b/src/components/SelectorResult.tsx @@ -23,12 +23,11 @@ const Row: FC = ({ label: title, children }) => { const witness = new WitnessClient(); interface ProvenanceIconProps { - record: RecordWithSelector; + canonicalString: string; } -const ProvenanceIcon: FC = ({ record }) => { - const canonicalRecordString = getCanonicalRecordString(record.domainSelectorPair, record.value); - const leafHash = witness.hash(canonicalRecordString); +export const ProvenanceIcon: FC = ({ canonicalString }) => { + const leafHash = witness.hash(canonicalString); const witnessUrl = `https://scan.witness.co/leaf/${leafHash}`; return ( @@ -54,7 +53,7 @@ export const SelectorResult: React.FC = ({ record }) => { {record.domainSelectorPair.selector}   - {record.provenanceVerified && } + {record.provenanceVerified && } {record.lastSeenAt && diff --git a/src/lib/db.ts b/src/lib/db.ts index 1a81fd1..dfcd944 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -117,6 +117,7 @@ export async function updateJWKeySet() { data: { jwks: await fetchJsonWebKeySet(), x509Certificate: await fetchx509Cert(), + provenanceVerified: false, }, }); } else { diff --git a/src/lib/generateWitness.ts b/src/lib/generateWitness.ts index 6959145..c2e6368 100644 --- a/src/lib/generateWitness.ts +++ b/src/lib/generateWitness.ts @@ -1,5 +1,5 @@ import { DkimRecord, DomainSelectorPair } from "@prisma/client"; -import { getCanonicalRecordString } from "./utils"; +import { getCanonicalJWKRecordString, getCanonicalRecordString, jwkSet } from "./utils"; import { WitnessClient } from "@witnessco/client"; import { prisma, recordToString } from "./db"; @@ -32,3 +32,38 @@ export async function generateWitness(dsp: DomainSelectorPair, dkimRecord: DkimR } }); } + +export async function generateJWKWitness( + JwkSet: jwkSet +) { + let canonicalRecordString = getCanonicalJWKRecordString(JwkSet); + const witness = new WitnessClient(process.env.WITNESS_API_KEY); + const leafHash = witness.hash(canonicalRecordString); + let timestamp; + try { + timestamp = await witness.postLeafAndGetTimestamp(leafHash); + } catch (error: any) { + console.error( + `witness.postLeafAndGetTimestamp failed for ${JwkSet}, leafHash ${leafHash}: ${error}` + ); + return; + } + console.log(`leaf ${leafHash} was timestamped at ${timestamp}`); + const proof = await witness.getProofForLeafHash(leafHash); + const verified = await witness.verifyProofChain(proof); + if (!verified) { + console.error("proof chain verification failed"); + return; + } + console.log( + `proof chain verified, setting provenanceVerified for ${JwkSet}` + ); + await prisma.jsonWebKeySets.update({ + where: { + id: JwkSet.id, + }, + data: { + provenanceVerified: true, + }, + }); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index aeb28b5..f8d5542 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -7,6 +7,14 @@ export type DomainAndSelector = { selector: string }; +export type jwkSet = { + id: number; + x509Certificate: string; + jwks: string; + lastUpdated: Date; + provenanceVerified: boolean | null; +} + export interface DnsDkimFetchResult { domain: string; selector: string; @@ -69,6 +77,18 @@ export function getCanonicalRecordString(dsp: DomainAndSelector, dkimRecordValue return `${dsp.selector}._domainkey.${dsp.domain} TXT "${dkimRecordValue}"`; } +export function getCanonicalJWKRecordString( + jwkSet: jwkSet +): string { + const canonicalObject = { + x509Certificate: jwkSet.x509Certificate, + jwks: jwkSet.jwks, + lastUpdated: jwkSet.lastUpdated, + provenanceVerified: jwkSet.provenanceVerified, + }; + + return JSON.stringify(canonicalObject, Object.keys(canonicalObject).sort()); +} function dataToMessage(data: any): string { if (!data) { From 1800f4fdfd68b6c1ea7b2a1cd23eac3b076db8b9 Mon Sep 17 00:00:00 2001 From: sukhman Date: Sat, 4 Jan 2025 11:54:37 +0530 Subject: [PATCH 6/6] Create cron job for keys updation --- package.json | 2 ++ pnpm-lock.yaml | 53 ++++++++++++++++++++++++++++++++++++++++------ src/app/layout.tsx | 18 ++++++++++++++++ src/lib/utils.ts | 9 ++++++-- src/util/cron.ts | 26 +++++++++++++++++++++++ 5 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 src/util/cron.ts diff --git a/package.json b/package.json index 74f2a7a..9c1eb1d 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "moment": "^2.30.1", "next": "14.2.3", "next-auth": "^4.24.7", + "node-cron": "^3.0.3", "rate-limiter-flexible": "^5.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -38,6 +39,7 @@ }, "//": "prisma 5.11.0 needed for Prisma Client Python", "devDependencies": { + "@types/cron": "^2.4.3", "@types/lodash": "^4.17.10", "@types/node": "^20.12.12", "@types/react": "^18.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a07bfe..0fd7566 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: next-auth: specifier: ^4.24.7 version: 4.24.7(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + node-cron: + specifier: ^3.0.3 + version: 3.0.3 rate-limiter-flexible: specifier: ^5.0.3 version: 5.0.3 @@ -60,6 +63,9 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: + '@types/cron': + specifier: ^2.4.3 + version: 2.4.3 '@types/lodash': specifier: ^4.17.10 version: 4.17.10 @@ -786,6 +792,10 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@types/cron@2.4.3': + resolution: {integrity: sha512-ViRBkoZD9Rk0hGeMdd2GHGaOaZuH9mDmwsE5/Zo53Ftwcvh7h9VJc8lIt2wdgEwS4EW5lbtTX6vlE0idCLPOyA==} + deprecated: This is a stub types definition. cron provides its own type definitions, so you do not need this installed. + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -798,6 +808,9 @@ packages: '@types/lodash@4.17.10': resolution: {integrity: sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/node@20.12.12': resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} @@ -1098,6 +1111,9 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + cron@3.2.1: + resolution: {integrity: sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1687,6 +1703,10 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -1799,6 +1819,10 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-cron@3.0.3: + resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} + engines: {node: '>=6.0.0'} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2439,8 +2463,8 @@ packages: ts-toolbelt@9.6.0: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tsx@4.11.0: resolution: {integrity: sha512-vzGGELOgAupsNVssAmZjbUDfdm/pWP4R+Kg8TVdsonxbXk0bEpE1qh0yV6/QxUVXaVlNemgcPajGdJJ82n3stg==} @@ -3236,7 +3260,7 @@ snapshots: '@stoplight/ordered-object-literal': 1.0.5 '@stoplight/types': 14.1.1 '@stoplight/yaml-ast-parser': 0.0.50 - tslib: 2.6.2 + tslib: 2.8.1 '@swagger-api/apidom-ast@1.0.0-alpha.3': dependencies: @@ -3575,7 +3599,11 @@ snapshots: '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 - tslib: 2.6.2 + tslib: 2.8.1 + + '@types/cron@2.4.3': + dependencies: + cron: 3.2.1 '@types/estree@1.0.5': {} @@ -3587,6 +3615,8 @@ snapshots: '@types/lodash@4.17.10': {} + '@types/luxon@3.4.2': {} + '@types/node@20.12.12': dependencies: undici-types: 5.26.5 @@ -3724,7 +3754,7 @@ snapshots: autolinker@3.16.2: dependencies: - tslib: 2.6.2 + tslib: 2.8.1 autoprefixer@10.4.19(postcss@8.4.38): dependencies: @@ -3913,6 +3943,11 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 + cron@3.2.1: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4590,6 +4625,8 @@ snapshots: dependencies: es5-ext: 0.10.64 + luxon@3.5.0: {} + magic-string@0.30.10: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -4708,6 +4745,10 @@ snapshots: node-abort-controller@3.1.1: {} + node-cron@3.0.3: + dependencies: + uuid: 8.3.2 + node-domexception@1.0.0: {} node-fetch-commonjs@3.3.2: @@ -5473,7 +5514,7 @@ snapshots: ts-toolbelt@9.6.0: {} - tslib@2.6.2: {} + tslib@2.8.1: {} tsx@4.11.0: dependencies: diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0897ea7..a59ad58 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from "next/font/google"; import Link from "next/link"; import "./globals.css"; import { NextAuthProvider } from "./session-provider"; +import { startJWKCronJob, stopCronJob } from "@/util/cron"; const inter = Inter({ subsets: ["latin"] }); @@ -31,6 +32,23 @@ const DevModeNotice: React.FC = () => { }; export default function RootLayout({ children }: { children: React.ReactNode }) { + if (typeof window === "undefined") { // To ensure it runs only on the server side + // console.log("Starting UpdateJWKCronJob..."); + startJWKCronJob(); + + // Handles graceful termination of cron job. + process.on("SIGINT", () => { + console.log("Received SIGINT. Gracefully shutting down..."); + stopCronJob(); + process.exit(0); + }); + + process.on("SIGTERM", () => { + console.log("Received SIGTERM. Gracefully shutting down..."); + stopCronJob(); + process.exit(0); + }); + } return ( diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f8d5542..5255acb 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -197,7 +197,12 @@ export async function fetchJsonWebKeySet(): Promise { throw new Error('Cannot fetch Google JSON Web Key Set'); } const jsonData = await response.json(); - const jsonWebKeySet = JSON.stringify(jsonData, null, 2); + console.log(jsonData); + const jsonWebKeySet = JSON.stringify( + jsonData, + null, + 2 + ); return jsonWebKeySet; } catch (error) { console.error('Error fetching JSON Web Key Set:', error); @@ -212,7 +217,7 @@ export async function fetchx509Cert(): Promise { throw new Error('Cannot fetch Google X.509 certificate'); } const jsonData = await response.json(); - const x509Cert = JSON.stringify(jsonData, null, 2); + const x509Cert = JSON.stringify(jsonData, Object.keys(jsonData).sort(), 2); return x509Cert; } catch (error) { console.error('Error fetching X.509 certificate:', error); diff --git a/src/util/cron.ts b/src/util/cron.ts new file mode 100644 index 0000000..f16c26a --- /dev/null +++ b/src/util/cron.ts @@ -0,0 +1,26 @@ +import cron, { ScheduledTask } from "node-cron"; +import { updateJWKeySet } from "@/lib/db"; + +let cronTask: ScheduledTask | null = null; +let initialized = false; + +// Schedule the cron job to update JWKeySet every 5 minutes +export function startJWKCronJob() { + if (initialized) { + console.log("Cron job already started."); + return; + } + + cron.schedule("*/5 * * * *", () => { + // console.log("Executing UpdateJWKCronJob..."); + updateJWKeySet(); + }); + initialized = true; +} + +export const stopCronJob = () => { + if (cronTask) { + cronTask.stop(); + console.log("Cron job stopped."); + } +};