diff --git a/package.json b/package.json index 5322efc..a094fbd 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "arctic": "^1.5.0", "bits-ui": "^0.21.2", "clsx": "^2.1.0", + "libsodium-wrappers": "^0.7.13", "lucide-svelte": "^0.368.0", "mode-watcher": "^0.3.0", "svelte-sonner": "^0.3.22", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce2457f..ea1f1ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: clsx: specifier: ^2.1.0 version: 2.1.0 + libsodium-wrappers: + specifier: ^0.7.13 + version: 0.7.13 lucide-svelte: specifier: ^0.368.0 version: 0.368.0(svelte@4.2.12) @@ -2760,6 +2763,16 @@ packages: type-check: 0.4.0 dev: true + /libsodium-wrappers@0.7.13: + resolution: {integrity: sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw==} + dependencies: + libsodium: 0.7.13 + dev: false + + /libsodium@0.7.13: + resolution: {integrity: sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==} + dev: false + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} diff --git a/src/routes/api/github/createRepo/+server.ts b/src/routes/api/github/createRepo/+server.ts index f6eb9c7..03f3b12 100644 --- a/src/routes/api/github/createRepo/+server.ts +++ b/src/routes/api/github/createRepo/+server.ts @@ -49,7 +49,7 @@ export async function POST({ request, cookies }): Promise { } ); if (!readmeUpdate.ok) { - log("Updating README failed: " + JSON.stringify(readme)); + log("Updating README failed: " + JSON.stringify(readmeUpdate)); } else { log("Updated README successfully!"); } @@ -80,7 +80,7 @@ export async function POST({ request, cookies }): Promise { } ); if (!recipeUpdate.ok) { - log("Updating recipe failed: " + JSON.stringify(recipe)); + log("Updating recipe failed: " + JSON.stringify(recipeUpdate)); } else { log("Updated recipe successfully!"); } diff --git a/src/routes/api/github/setupCosign/+server.ts b/src/routes/api/github/setupCosign/+server.ts new file mode 100644 index 0000000..a17ea34 --- /dev/null +++ b/src/routes/api/github/setupCosign/+server.ts @@ -0,0 +1,74 @@ +import { ghApiGet, ghApiPost, ghApiPut } from "$lib/ts/github/api"; +import { createLogStream } from "$lib/ts/misc/logStream"; +import type { Endpoints } from "@octokit/types"; +import _sodium from "libsodium-wrappers"; + +export async function POST({ request, cookies }): Promise { + return await createLogStream(async (log) => { + const token = cookies.get("github_oauth_token"); + if (token === undefined) throw new Error("Not logged in"); + const { login, name, cosignPrivateKey, cosignPublicKey } = await request.json(); + + // Private key + log("Setting up sodium for encrypting private key for transit..."); + await _sodium.ready; + const sodium = _sodium; + + log("Fetching repository public key to encrypt private key for transit..."); + const pubKey = await ghApiGet(token, `/repos/${login}/${name}/actions/secrets/public-key`); + if (!pubKey.ok) { + log("Fetching public key failed: " + JSON.stringify(pubKey)); + } else { + log("Fetched public key successfully!"); + } + const pubKeyData = + pubKey.data as Endpoints["GET /repos/{owner}/{repo}/actions/secrets/public-key"]["response"]["data"]; + + log("Encrypting private key for transit..."); + const binRepoPubKey = sodium.from_base64(pubKeyData.key, sodium.base64_variants.ORIGINAL); + const binPrivateKey = sodium.from_string(cosignPrivateKey); + const encBytes = sodium.crypto_box_seal(binPrivateKey, binRepoPubKey); + const encPrivateKey = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL); + log("Private signing key encrypted successfully!"); + + log("Setting SIGNING_SECRET secret..."); + const signingSecret = await ghApiPut( + token, + `/repos/${login}/${name}/actions/secrets/SIGNING_SECRET`, + { + encrypted_value: encPrivateKey, + key_id: pubKeyData.key_id + } + ); + if (!signingSecret.ok) { + log("Setting signing secret failed: " + JSON.stringify(signingSecret)); + } else { + log("Set signing secret successfully!"); + } + + // Public key + log("Fetching template cosign.pub..."); + const oldPubKey = await ghApiGet(token, `/repos/${login}/${name}/contents/cosign.pub`); + if (!oldPubKey.ok) { + log("Fetching old public key failed: " + JSON.stringify(oldPubKey)); + } else { + log("Fetched old public key successfully!"); + } + const oldPubKeyData = + oldPubKey.data as Endpoints["GET /repos/{owner}/{repo}/contents/{path}"]["response"]["data"]; + + log("Updating cosign.pub..."); + const pubKeyUpdate = await ghApiPut(token, `/repos/${login}/${name}/contents/cosign.pub`, { + message: "chore(automatic): new cosign keys", + content: btoa(cosignPublicKey), + sha: oldPubKeyData.sha + }); + if (!pubKeyUpdate.ok) { + log("Updating public key failed: " + JSON.stringify(pubKeyUpdate)); + } else { + log("Updated public key successfully!"); + } + + log("Cosign setup DONE!"); + }); +} diff --git a/src/routes/new/+page.svelte b/src/routes/new/+page.svelte index 8fade0c..5d09631 100644 --- a/src/routes/new/+page.svelte +++ b/src/routes/new/+page.svelte @@ -12,12 +12,14 @@ let log: Array = []; export let data; + let repoName = document.getElementById("reponame"); + async function createRepo() { setupStep = "inprogress"; const res = await fetch("/api/github/createRepo", { method: "POST", body: JSON.stringify({ - name: document.getElementById("reponame").value, + name: repoName, login: data.githubUser.login }), headers: { @@ -32,20 +34,59 @@ }); } - function generateCosignKeys() { + async function setupCosign() { + setupStep = "inprogress"; + + log = [...log, "Generating cosign keys..."]; + const keys = await generateCosignKeys(); + log = [...log, "Generated cosign keys, sending to serverless backend..."]; + + const res = await fetch("/api/github/setupCosign", { + method: "POST", + body: JSON.stringify({ + name: repoName, + login: data.githubUser.login, + cosignPrivateKey: keys.cosignPrivateKey, + cosignPublicKey: keys.cosignPublicKey + }), + headers: { + "content-type": "application/json" + } + }); + readLogStream(res, (value) => { + log = [...log, value]; + if (value.includes("DONE!")) { + setupStep = "done"; + } + }); + } + + async function generateCosignKeys(): Promise<{ + cosignPublicKey: string; + cosignPrivateKey: string; + }> { // @ts-ignore const go = new Go(); - WebAssembly.instantiateStreaming(fetch("/cosign.wasm"), go.importObject).then( - async (obj) => { - const wasm = obj.instance; - go.run(wasm); - // @ts-ignore - console.log(cosignPublicKey); - // @ts-ignore - console.log(cosignPrivateKey); - } - ); + const obj = await WebAssembly.instantiateStreaming(fetch("/cosign.wasm"), go.importObject); + const wasm = obj.instance; + go.run(wasm); + + // The keys are set as global variables + const returnObject = { + // @ts-ignore + cosignPublicKey: window.cosignPublicKey, + // @ts-ignore + cosignPrivateKey: window.cosignPrivateKey + }; + + // Clear the keys from global variables for shallow security reasons + // @ts-ignore + window.cosignPublicKey = undefined; + // @ts-ignore + window.cosignPrivateKey = undefined; + + return returnObject; } @@ -83,18 +124,50 @@ placeholder="weird-os" class="font-mono" required + on:change={(e) => (repoName = e.target?.value)} > {:else if setupStep === "inprogress"} - - + {:else if setupStep === "cosign"} - Set up container signing - +

How do you want to set up container signing?

+

+ Container signing is used to verify the authenticity of the custom image. It is + important not to expose the cosign keys to third parties. BlueBuild can set + these up automatically for you. The keys will be generated in your browser and + transmitted over HTTPS to GitHub. If you do not trust BlueBuild to do this, you + can skip it for now and do it manually instead. +

+ +
+ + +
{:else} Done! + + {/if} + {#if setupStep !== "start"} + {/if}