aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/canaries
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes/canaries')
-rw-r--r--src/routes/canaries/+page.svelte105
-rw-r--r--src/routes/canaries/Canary.svelte92
-rw-r--r--src/routes/canaries/canaries.ts54
-rw-r--r--src/routes/canaries/fallback-keys.ts34
-rw-r--r--src/routes/canaries/keystore.ts71
-rw-r--r--src/routes/canaries/napatha:kyun.host/+server.ts6
6 files changed, 337 insertions, 25 deletions
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 @@
+<svelte:options runes />
+
<script lang="ts">
- import keyStore from "./keystore";
+ import { browser } from "$app/environment";
+ import { canaries } from "./canaries";
+ import Canary from "./Canary.svelte";
+ import { initKeystore } from "./keystore";
</script>
-{@debug keyStore}
+<svelte:head>
+ <title>Warrant Canaries - mem.estrogen.zone</title>
+</svelte:head>
+
+<div class="flex min-h-full min-w-full flex-col justify-between">
+ <div>
+ {#await initKeystore}
+ {#if browser}
+ Preparing...
+ {:else}
+ Awaiting Browser
+ <noscript>
+ If you're using noscript, here are the raw canaries:
+ {#each canaries as canary}
+ <a href={canary.url}>{canary.signer}'s canary for {canary.name}</a>
+ {/each}
+ </noscript>
+ {/if}
+ {:then _}
+ <div class="canaries flex gap-4 flex-wrap max-w-full">
+ {#each canaries as canary}
+ <Canary {canary} />
+ {/each}
+ </div>
+ {:catch e}
+ <div class="p-4 bg-red-700 text-white max-w-full overflow-x-auto">
+ Encoutnered Error:
+ <pre>{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,
+ )}</pre>
+ </div>
+ {/await}
+ </div>
+
+ <div
+ class="mt-3 sticky bottom-0 block bg-black bg-opacity-30 p-4 rounded-xl backdrop-blur-xl"
+ >
+ <p class="text-xs text-white">
+ <span class="about opacity-60 hover:opacity-80 transition-all">
+ This page contains various <a
+ href="https://en.wikipedia.org/wiki/Warrant_canary"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="opacity-90 hover:underline text-blue-400 hover:text-blue-300"
+ >liberty canaries</a
+ >
+ that I, or my friends, depend upon or are otherwise interested in.
+ </span>
+ <span
+ class="validation-attempt opacity-60 hover:opacity-80 transition-all"
+ class:hidden={!browser}
+ >
+ <br />
+ 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.
+ </span>
+ <span
+ class="trust-server opacity-60 hover:opacity-80 transition-all"
+ class:hidden={!browser}
+ >
+ <br />
+ The server at
+ <code class="font-grub px-1 bg-white bg-opacity-5 rounded-md"
+ >{#if typeof location !== "undefined"}{location.host ??
+ location.hostname}{:else}mem.estrogen.zone{/if}</code
+ >
+ serves the keys and the canaries. Make sure you trust it to serve correct
+ keys and canaries, or validate the signatures of the
+ <code class="font-grub px-1 bg-white bg-opacity-5 rounded-md"
+ >Signed</code
+ > links with keys obtained externally.
+ </span>
+ <span
+ class="legend opacity-60 hover:opacity-80 transition-all"
+ class:hidden={!browser}
+ ><br />If a signature check passes, the background of the respective
+ canary turns
+ <span class="bg-white text-black">white</span>. Whilst fetching, it's
+ background turns <span class="bg-blue-500 text-white">blue</span>. If a
+ signature check fails, it will turn
+ <span class="bg-red-500 text-white">red</span>. If the canary cannot be
+ found, it turns a different shade of
+ <span class="bg-red-800 text-white">red</span>.</span
+ >
+ </p>
+ </div>
+</div>
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 @@
+<script lang="ts">
+ import type { Canary } from "./canaries";
+ let {
+ canary,
+ }: {
+ canary: Canary;
+ } = $props();
+ let rawText = $state(null as string | null);
+ let failedFetch = $state(false);
+ let url = $state(
+ null as {
+ stripped: string;
+ signed: string;
+ } | null,
+ );
+ let status = $derived(
+ url !== null
+ ? ("ok" as const)
+ : failedFetch
+ ? ("failed_fetch" as const)
+ : rawText === null
+ ? ("fetching" as const)
+ : ("failed_sigcheck" as const),
+ );
+ $effect(() => {
+ (async (canary) => {
+ rawText = await canary.getRawText();
+ })(canary);
+ });
+ $effect(() => {
+ (async (canary) => {
+ url = await canary.getUrl();
+ })(canary);
+ });
+</script>
+
+<div
+ class="canary p-4 bg-opacity-10 flex-1 max-w-[1024px] min-w-[min(100%,500px)] overflow-x-auto flex flex-col justify-between rounded-2xl"
+ class:bg-white={status === "ok"}
+ class:bg-blue-500={status === "fetching"}
+ class:bg-red-500={status === "failed_sigcheck"}
+ class:bg-red-800={status === "failed_fetch"}
+>
+ <div class="top">
+ <small class="opacity-50">{canary.signer}'s canary for</small>
+ <h2 class="canary-name text-lg">{canary.name}</h2>
+ <p class="opacity-80 text-white font-normal mt-1">
+ {#if status === "ok"}
+ {canary.description}
+ {:else if status === "fetching"}
+ Fetching Canary...
+ {:else if status === "failed_sigcheck"}
+ Failed Signature Check!<br />
+ Either the upstream key changed, or the canary is fucked!
+ {:else if status === "failed_fetch"}
+ Failed to fetch canary!
+ {/if}
+ </p>
+ </div>
+ <div class="bottom">
+ <div class="mt-2">
+ {#if url}
+ <span class="opacity-70">Get Canary:</span>
+ <a
+ class="opacity-90 hover:underline text-blue-400 hover:text-blue-300"
+ href={url.signed}
+ >
+ Signed
+ </a>
+ <a
+ class="opacity-90 hover:underline text-blue-400 hover:text-blue-300"
+ href={url.stripped}
+ >
+ Stripped
+ </a>
+ {:else if status === "failed_fetch"}
+ <span class="opacity-70">Get Canary:</span>
+ <a
+ class="opacity-90 hover:underline text-blue-400 hover:text-blue-300"
+ href={canary.url}
+ >
+ Signed
+ </a>
+ <span class="opacity-70 text-blue-400 grayscale cursor-not-allowed">
+ Stripped
+ </span>
+ {:else}
+ No URL yet
+ {/if}
+ </div>
+ </div>
+</div>
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<string> = 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<string> = 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<string, string>().set('B546778F06BBCC8EC167DB3CD919706487B8B6DE', `-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: User ID: memdmp <memdmp@estrogen.zone>
+Comment: a.k.a.: memdmp <memdmp@memeware.net>
+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<string, PublicKey>();
-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@estrogen.zone>",
"memdmp <memdmp@memeware.net>",
],
+ expect_fingerprint: 'B546778F06BBCC8EC167DB3CD919706487B8B6DE',
is_url: true,
});
await pushKey({
@@ -108,12 +149,12 @@ ZQ4KTbprMz8J4AD/bG33f9Kqg3AqehEyU2TldJs9U9Oni5AXGSGfKLJhmQc=
`,
signed_by: "memdmp <memdmp@memeware.net>",
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 <naphtha@kyun.host>"],
+ expect_user_ids: ["chef naphtha <naphtha@kyun.host>"],
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');
};