From c5a7e704d1c836119319ce3b308642f195b078d9 Mon Sep 17 00:00:00 2001 From: memdmp Date: Wed, 22 Jan 2025 21:19:22 +0100 Subject: feat: canarytool :3 --- src/routes/canaries/+page.svelte | 105 ++++++++++++++++++++++- src/routes/canaries/Canary.svelte | 92 ++++++++++++++++++++ src/routes/canaries/canaries.ts | 54 ++++++++++-- src/routes/canaries/fallback-keys.ts | 34 ++++++++ src/routes/canaries/keystore.ts | 71 +++++++++++---- src/routes/canaries/napatha:kyun.host/+server.ts | 6 +- 6 files changed, 337 insertions(+), 25 deletions(-) create mode 100644 src/routes/canaries/Canary.svelte create mode 100644 src/routes/canaries/fallback-keys.ts diff --git a/src/routes/canaries/+page.svelte b/src/routes/canaries/+page.svelte index adbbdcc..24e968a 100644 --- a/src/routes/canaries/+page.svelte +++ b/src/routes/canaries/+page.svelte @@ -1,5 +1,106 @@ + + -{@debug keyStore} + + Warrant Canaries - mem.estrogen.zone + + +
+
+ {#await initKeystore} + {#if browser} + Preparing... + {:else} + Awaiting Browser + + {/if} + {:then _} +
+ {#each canaries as canary} + + {/each} +
+ {:catch e} +
+ Encoutnered Error: +
{JSON.stringify(
+            {
+              raw: e,
+              fileName: "fileName" in e ? e.fileName : undefined,
+              name: "name" in e ? e.name : undefined,
+              message: "message" in e ? e.message : undefined,
+              stack: "stack" in e ? e.stack : undefined,
+            },
+            null,
+            2,
+          )}
+
+ {/await} +
+ +
+

+ + This page contains various liberty canaries + that I, or my friends, depend upon or are otherwise interested in. + + +
+ It makes an attempt to validate their signatures, however it does not attempt + to check if they've been updated in time for their next scheduled update. + It is your responsibility to ensure that the canary's contents match your + requirements. +
+ +
+ The server at + {#if typeof location !== "undefined"}{location.host ?? + location.hostname}{:else}mem.estrogen.zone{/if} + serves the keys and the canaries. Make sure you trust it to serve correct + keys and canaries, or validate the signatures of the + Signed links with keys obtained externally. +
+
If a signature check passes, the background of the respective + canary turns + white. Whilst fetching, it's + background turns blue. If a + signature check fails, it will turn + red. If the canary cannot be + found, it turns a different shade of + red.
+

+
+
diff --git a/src/routes/canaries/Canary.svelte b/src/routes/canaries/Canary.svelte new file mode 100644 index 0000000..284ed9f --- /dev/null +++ b/src/routes/canaries/Canary.svelte @@ -0,0 +1,92 @@ + + +
+
+ {canary.signer}'s canary for +

{canary.name}

+

+ {#if status === "ok"} + {canary.description} + {:else if status === "fetching"} + Fetching Canary... + {:else if status === "failed_sigcheck"} + Failed Signature Check!
+ Either the upstream key changed, or the canary is fucked! + {:else if status === "failed_fetch"} + Failed to fetch canary! + {/if} +

+
+
+
+ {#if url} + Get Canary: + + Signed + + + Stripped + + {:else if status === "failed_fetch"} + Get Canary: + + Signed + + + Stripped + + {:else} + No URL yet + {/if} +
+
+
diff --git a/src/routes/canaries/canaries.ts b/src/routes/canaries/canaries.ts index de5c44f..d28baab 100644 --- a/src/routes/canaries/canaries.ts +++ b/src/routes/canaries/canaries.ts @@ -1,25 +1,69 @@ +import { browser } from "$app/environment"; +import { validateSignature } from "./keystore"; + export const canaries: Canary[] = []; export class Canary { public constructor( public name: string, public description: string, + public signer: string, public url: string, public keyIdentifier: string, + public contentType: string = 'text/plain', ) { canaries.push(this); } + public forceContentType = false; + public async getRawText() { + if (!browser) throw new Error('This should only be done in a browser.') + const res = await fetch(this.url) + if (res.ok) { + if (!this.forceContentType) this.contentType = res.headers.get('Content-Type') || this.contentType + const text = await res.text() + return text + } else { + throw new Error(`Fetching canary failed with code ${res.status} (${res.statusText}): +${await res.text().catch(e => `Unknown (Unable to get text, ${JSON.stringify(e)})`)}`) + } + } + public async getValidatedText(rawText: string | Promise = this.getRawText()) { + return await validateSignature(await rawText, this.keyIdentifier) + } + /** Returns downloadable data url if signature passes, otherwise returns null */ + public async getUrl(rawText: string | Promise = this.getRawText()) { + const t = await rawText; + let stripped: string; + try { + stripped = await this.getValidatedText(t); + } catch (error) { + console.warn('Failed to validate signature: ', error); + return null; + } + return { + stripped: URL.createObjectURL(new Blob([stripped], { + type: this.contentType, + })), + signed: URL.createObjectURL(new Blob([t], { + type: this.contentType, + })), + } + } } new Canary( - 'estrogen.zone, memdmp', - 'This canary is responsible for services hosted around estrogen.zone and yuridick.gay', + 'estrogen.zone', + 'Services hosted around estrogen.zone and yuridick.gay', + 'memdmp', '/canaries/memdmp:estrogen.zone', 'memdmp', + 'text/plain; charset=utf-8' ); new Canary( 'kyun.host', - 'This canary is responsible for the VPS provider "kyun.host"', - // '/canaries/napatha:kyun.host', - 'https://files.kyun.host/canary.txt', + 'The VPS provider "kyun.host"', + 'napatha', + '/canaries/napatha:kyun.host', + // 'https://files.kyun.host/canary.txt', 'napatha', + 'text/plain; charset=utf-8' ); diff --git a/src/routes/canaries/fallback-keys.ts b/src/routes/canaries/fallback-keys.ts new file mode 100644 index 0000000..fa3d7a7 --- /dev/null +++ b/src/routes/canaries/fallback-keys.ts @@ -0,0 +1,34 @@ +export const fallbackKeys = new Map().set('B546778F06BBCC8EC167DB3CD919706487B8B6DE', `-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: User ID: memdmp +Comment: a.k.a.: memdmp +Comment: Valid from: 11 Sep 2024 08:29:03 +Comment: Type: 255-bit EdDSA (secret key available) +Comment: Usage: Signing, Encryption, Certifying User IDs +Comment: Fingerprint: B546778F06BBCC8EC167DB3CD919706487B8B6DE + +mDMEZuE4rxYJKwYBBAHaRw8BAQdAGFCSBdoIrbk4DcSu8YIF+X+V9v4SFSCt1wl5 +ayp3anG0HW1lbWRtcCA8bWVtZG1wQGVzdHJvZ2VuLnpvbmU+iJMEExYKADsWIQS1 +RnePBrvMjsFn2zzZGXBkh7i23gUCZwf3DQIbAwULCQgHAgIiAgYVCgkICwIEFgID +AQIeBwIXgAAKCRDZGXBkh7i23uHRAP9IQylDWCAypelUhtcBFCb+cFT4NDOX/1N5 +kqCsp0a7TQEAlXR3vkGsPkSymYpFm26837L9IJeNP4wvcBldKdbchgiIdQQQFgoA +HRYhBIkjiMT2LOvXZkCzo8SN5mcaHws1BQJnITEzAAoJEMSN5mcaHws11+oA/jSr +1C24UtDdeyLMpl8d6l3ab9nUAnErlRNWtwux//YEAQDqAHuXEOC/gZh9m+hwHF2W +bugrgEXqTktuK4sCZbsOCLQcbWVtZG1wIDxtZW1kbXBAbWVtZXdhcmUubmV0PoiT +BBMWCgA7AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEtUZ3jwa7zI7B +Z9s82RlwZIe4tt4FAmbhOnYACgkQ2RlwZIe4tt6c0QEAwS5WoDMmLpbC4VIIBho5 +1E9avJqOSbUjBz+RdJ+23cYA/0/nWStYLQWMiuguoFD3WWpl38qt1iavtpboEdAY +jDcNiHUEEBYKAB0WIQSJI4jE9izr12ZAs6PEjeZnGh8LNQUCZyExMwAKCRDEjeZn +Gh8LNekGAQDErOsA2I522vx1m9a0lgS1QPntO7h1U9PwBj34K2zGUQD8ClPU5W7D +sdp9b8zrSTbN2V1aEXqQoF4Efz7mO1d0HQK0IG1lbWRtcCBnaXQgPG1lbWRtcEBt +ZW1ld2FyZS5uZXQ+iHgEMBYKACAWIQS1RnePBrvMjsFn2zzZGXBkh7i23gUCZuE7 +LAIdIAAKCRDZGXBkh7i23pQsAP9y96dG0OZ02NBHXkEUWfkKrkTeaNTaho4bccsx ++fb2+AD+ODcnuTt/E6NZhOC3jkzL/wpwm0XtCxrMXmXf43LZmwKIkwQTFgoAOxYh +BLVGd48Gu8yOwWfbPNkZcGSHuLbeBQJm4TsjAhsDBQsJCAcCAiICBhUKCQgLAgQW +AgMBAh4HAheAAAoJENkZcGSHuLbeUVgBAKTeCPrHTd0YDfA+Bwwmrwc9CzR/PXTn +zR4BpApq3ro5AQCP3NByfBTfYaY0BqoRxkWBS4gSw3cly3cQ2BBbt+thALg4BGbh +OK8SCisGAQQBl1UBBQEBB0C3RzBpJD0UFcCV64bv7EfB/3tqb5onM+Qq9FurKyte +fAMBCAeIeAQYFgoAIAIbDBYhBLVGd48Gu8yOwWfbPNkZcGSHuLbeBQJm4Tp2AAoJ +ENkZcGSHuLbeE9QA/1USt07TSOzMm47ffajCC+rDQEiGvpu19dP8khPsUUUcAPwI +C8p0np2525hjuerND23TEiYK9EoRSprm8S7UdcwzCg== +=DPPI +-----END PGP PUBLIC KEY BLOCK-----`) \ No newline at end of file diff --git a/src/routes/canaries/keystore.ts b/src/routes/canaries/keystore.ts index af710ec..1d2aefb 100644 --- a/src/routes/canaries/keystore.ts +++ b/src/routes/canaries/keystore.ts @@ -1,7 +1,10 @@ +import { dev } from "$app/environment"; import { PublicKey, readCleartextMessage, readKey, verify } from "openpgp"; +import { fallbackKeys } from "./fallback-keys"; export const keyStore = new Map(); -export const validateSignature = async (message: string, id: string) => { - await initKeystore; +const will_debug = false; +const debug = dev && will_debug ? console.debug : () => void 0; +const _validateSignature = async (message: string, id: string) => { id = id.toUpperCase(); const key = keyStore.get(id) ?? keyStore.get(id.replace(/ /g, "")); if (!key) throw new Error("Could not find key from keystore"); @@ -14,36 +17,64 @@ export const validateSignature = async (message: string, id: string) => { expectSigned: true, }); return verificationResult.data; +} +export const validateSignature: typeof _validateSignature = async (message, id) => { + await initKeystore; + return _validateSignature(message, id) }; const pushKey = async ({ ids, key, is_url, - expectUserIds, + expect_user_ids, + expect_fingerprint, signed_by, }: { ids?: string[]; - expectUserIds?: string[]; + expect_user_ids?: string[]; + expect_fingerprint?: string; key: string; is_url?: boolean; signed_by?: string; }) => { ids = ids ?? []; if (is_url) { + const url = new URL(key, "https://keys.openpgp.org/vks/v1/by-fingerprint/"); + debug('getting key with url', url) key = await fetch( - new URL(key, "https://keys.openpgp.org/vks/v1/by-fingerprint/"), - {}, - ).then((v) => v.text()); + url, + ).then((v) => v.text()).catch(e => { + if (fallbackKeys.has(key)) { + debug('failed with error', e, 'but found fallback key') + return fallbackKeys.get(key)! + } + else { + debug('failed to fetch key, cannot find fallback') + throw e + } + }); + debug('fetched key', key) + } else { + debug('found key', key) } + if (key === null) + throw new Error('Key is null.') + if (key === '') + throw new Error('Key is empty string.') + if (typeof key !== 'string') + throw new Error(`Expected key with type string, got key of type ${key}`) if (signed_by) { - key = await validateSignature(key, signed_by); + debug('key must be signed by', signed_by) + key = await _validateSignature(key, signed_by); + debug('validated signature') } const parsedKey = await readKey({ armoredKey: key, }).then((v) => v.toPublic()); { + const ids = parsedKey.getUserIDs(); const missingUserIds = - expectUserIds?.filter((v) => !expectUserIds.includes(v)) ?? []; + expect_user_ids?.filter((v) => !ids.includes(v)) ?? []; if (missingUserIds.length) { throw new Error( `Key ${parsedKey.getFingerprint()} is missing User IDs: ${missingUserIds.join( @@ -52,24 +83,34 @@ const pushKey = async ({ ); } } + if (expect_fingerprint && parsedKey.getFingerprint().toUpperCase() !== expect_fingerprint.toUpperCase()) + throw new Error( + `Key ${parsedKey.getFingerprint()} is not ${expect_fingerprint}`, + ); + else if (expect_fingerprint) + debug('fingerprint matches expected fingerprint') + else + debug('no expected fingerprint passed') ids.push( parsedKey.getKeyID().toHex().replace(/ /g, ""), parsedKey.getFingerprint().replace(/ /g, ""), - ...(expectUserIds ?? []), + ...(expect_user_ids ?? []), ); ids = ids.filter((v, i, a) => a.indexOf(v) === i).map((v) => v.toUpperCase()); for (const id of ids) { keyStore.set(id, parsedKey); } + debug('added key', parsedKey, 'with ids', ids, 'to keystore') }; export const initKeystore = (async () => { await pushKey({ - key: "B546778F06BBCC8EC167DB3CD919706487B8B6DE", + key: 'B546778F06BBCC8EC167DB3CD919706487B8B6DE', ids: ["memdmp"], - expectUserIds: [ + expect_user_ids: [ "memdmp ", "memdmp ", ], + expect_fingerprint: 'B546778F06BBCC8EC167DB3CD919706487B8B6DE', is_url: true, }); await pushKey({ @@ -108,12 +149,12 @@ ZQ4KTbprMz8J4AD/bG33f9Kqg3AqehEyU2TldJs9U9Oni5AXGSGfKLJhmQc= `, signed_by: "memdmp ", ids: ["canary-sigkey-signing"], + expect_fingerprint: '55D3582CAE78601990A8CA1DBFD0F9E61CB7D84E' }); await pushKey({ - // TODO: adapt to the relevant url on current domain when up - key: "https://files.catbox.moe/yf4x40.sig", + key: "https://git.estrogen.zone/mem-estrogen-zone.git/plain/static/keys/external/napatha.pgp.sig", ids: ["napatha"], - expectUserIds: ["chef naphtha "], + expect_user_ids: ["chef naphtha "], is_url: true, signed_by: "canary-sigkey-signing", }); diff --git a/src/routes/canaries/napatha:kyun.host/+server.ts b/src/routes/canaries/napatha:kyun.host/+server.ts index 260d15e..cca6e7e 100644 --- a/src/routes/canaries/napatha:kyun.host/+server.ts +++ b/src/routes/canaries/napatha:kyun.host/+server.ts @@ -1,5 +1,5 @@ -import { redirect } from '@sveltejs/kit'; - +// CORS moment +export const prerender = true; export const GET = () => { - throw redirect(307, 'https://files.kyun.host/canary.txt'); + return fetch('https://files.kyun.host/canary.txt'); }; -- cgit v1.2.3