From d1fa751fb839ef199b3e20f805e52059f5bfef9b Mon Sep 17 00:00:00 2001 From: Case Wylie Date: Fri, 1 Nov 2024 09:14:35 -0400 Subject: [PATCH 1/4] chore: harden runner Signed-off-by: Case Wylie --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e388df9ea..a0db523b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,10 @@ jobs: needs: [slsa] runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit - name: Set up Node registry authentication uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: From d11211d977d10a336527f6601245ce57424cc5d2 Mon Sep 17 00:00:00 2001 From: Case Wylie Date: Fri, 1 Nov 2024 09:15:59 -0400 Subject: [PATCH 2/4] chore: pin dep to e2e Signed-off-by: Case Wylie --- .github/workflows/pepr-excellent-examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pepr-excellent-examples.yml b/.github/workflows/pepr-excellent-examples.yml index 39069bcb1..7ac3704f4 100644 --- a/.github/workflows/pepr-excellent-examples.yml +++ b/.github/workflows/pepr-excellent-examples.yml @@ -187,7 +187,7 @@ jobs: npm ci - name: run e2e tests - uses: nick-fields/retry@v3 + uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0 with: max_attempts: 3 retry_on: error From babca61189e993c1f96d116ec1933d94b5ee8ada Mon Sep 17 00:00:00 2001 From: Case Wylie Date: Fri, 1 Nov 2024 09:16:45 -0400 Subject: [PATCH 3/4] chore: other pinned deps that are flagged in ci Signed-off-by: Case Wylie --- src/sdk/cosign.e2e.test.txt | 710 ++++++++++++++++++++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 src/sdk/cosign.e2e.test.txt diff --git a/src/sdk/cosign.e2e.test.txt b/src/sdk/cosign.e2e.test.txt new file mode 100644 index 000000000..74ba7008a --- /dev/null +++ b/src/sdk/cosign.e2e.test.txt @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The Pepr Authors + +import { afterAll, beforeAll, expect } from "@jest/globals"; +import { describe, it } from "@jest/globals"; +import { promisify } from "node:util"; +import * as child_process from "node:child_process"; +const exec = promisify(child_process.exec); +import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { chmodSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import * as crypto from "node:crypto"; +import { heredoc } from "./heredoc"; + +import * as sut from "./cosign"; + +const secs = (s: number) => s * 1000; +const mins = (m: number) => m * secs(60); + +const cmd = async (command: string, opts = {}) => await exec(command, opts); +const cmdStdout = async (command: string, opts = {}) => (await cmd(command, opts)).stdout.trim(); +const cmdStderr = async (command: string, opts = {}) => (await cmd(command, opts)).stderr.trim(); +const exists = async (path: string) => { + try { + await access(path); + return true; + } catch (e) { + return false; + } +}; + +const workdirPrefix = () => join(tmpdir(), `${basename(__filename)}-`); + +async function createWorkdir() { + const prefix = workdirPrefix(); + return await mkdtemp(prefix); +} + +async function cleanWorkdirs() { + const prefix = workdirPrefix(); + const dir = dirname(prefix); + const pre = basename(prefix); + + const workdirs = readdirSync(dir) + .filter(f => f.startsWith(pre)) + .map(m => join(dir, m)); + + await Promise.all(workdirs.map(m => rm(m, { recursive: true, force: true }))); +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const timed = async (msg: string, func: () => Promise) => { + console.time(msg); + const result = await func(); + console.timeEnd(msg); + return result; +}; + +async function builderExists(name: string) { + const resultRaw = await cmdStdout(`docker buildx ls --format json`); + const result = resultRaw.split("\n").map(m => JSON.parse(m)); + const found = result.filter(f => f.Name === name).length; + + return !!found; +} + +enum OS { + Linux = "Linux", + Mac = "Darwin", +} + +enum Arch { + x86_64 = "x86_64", + arm64 = "arm64", +} + +const sniffOS = async () => await cmdStdout("uname -s"); +const sniffArch = async () => await cmdStdout("uname -m"); + +async function downloadCosign(path: string, fname: string) { + const local = join(path, fname); + + if (await exists(local)) { + return local; + } + + let os = await sniffOS(); + os = os === OS.Linux ? OS.Linux.toLowerCase() : os; + os = os === OS.Mac ? OS.Mac.toLowerCase() : os; + + let arch = await sniffArch(); + arch = arch === Arch.x86_64 ? "amd64" : arch; + + const got = await sut.get( + "https://api.github.com/repos/sigstore/cosign/releases/latest", + "application/json", + ); + const ver = JSON.parse(got.body)["tag_name"]; + + const remote = `https://github.com/sigstore/cosign/releases/download/${ver}/cosign-${os}-${arch}`; + await sut.download(remote, local); + chmodSync(local, 0o777); + + return local; +} + +/* + Useful during dev but commented out for speed & network reasons +*/ +// describe("downloadCosign()", () => { +// let workdir: string; + +// beforeAll(async () => { +// workdir = await createWorkdir(); +// }); + +// afterAll(async () => { +// await cleanWorkdirs(); +// }); + +// it("works", async () => { +// const cosign = await downloadCosign(workdir, "cosign"); +// const result = await cmdStdout(`${cosign} version`, { cwd: workdir }); +// expect(result).toMatch(/cosign: A tool/); +// }); +// }); + +async function downloadCrane(path: string, fname: string) { + const local = join(path, fname); + const localTgz = `${local}.tar.gz`; + + if (await exists(local)) { + return local; + } + + const os = await sniffOS(); + const arch = await sniffArch(); + + const got = await sut.get( + "https://api.github.com/repos/google/go-containerregistry/releases/latest", + "application/json", + ); + const ver = JSON.parse(got.body)["tag_name"]; + + const remote = `https://github.com/google/go-containerregistry/releases/download/${ver}/go-containerregistry_${os}_${arch}.tar.gz`; + await sut.download(remote, localTgz); + + await cmdStdout(`tar -zxvf ${localTgz} ${fname}`, { cwd: path }); + chmodSync(local, 0o777); + + return local; +} + +/* + Useful during dev but commented out for speed & network reasons +*/ +// describe("downloadCrane()", () => { +// let workdir: string; + +// beforeAll(async () => { +// workdir = await createWorkdir(); +// }); + +// afterAll(async () => { +// await cleanWorkdirs(); +// }); + +// it("works", async () => { +// const crane = await downloadCrane(workdir, "crane"); +// const result = await cmdStdout(`${crane} --help`, { cwd: workdir }); +// expect(result).toMatch(/Crane is a tool/); +// }); +// }); + +async function genTlsCrt(workdir: string) { + const tlsKey = `${workdir}/tls-key.pem`; + const tlsCsr = `${workdir}/tls-csr.pem`; + const tlsCrt = `${workdir}/tls-crt.pem`; + + let command = `openssl genrsa -out ${tlsKey} 1024`; + await cmd(command); + + command = + `openssl req -new -key ${tlsKey} -out ${tlsCsr}` + + ` -subj "/C=US/ST=Colorado/L=Colorado Springs/O=Defense Unicorns/CN=localhost"` + + ` -addext "subjectAltName = DNS:localhost"`; + await cmd(command); + + command = + `openssl x509 -req -in ${tlsCsr} -key ${tlsKey} -out ${tlsCrt}` + ` -copy_extensions copyall`; + await cmd(command); + + return { key: tlsKey, crt: tlsCrt }; +} + +async function startZotRegistry(workdir: string, tlsKey: string, tlsCrt: string) { + const innerKey = `/${basename(tlsKey)}`; + const innerCrt = `/${basename(tlsCrt)}`; + + // https://images.chainguard.dev/directory/image/zot/overview + const containerName = `${basename(__filename)}.zot`; + + const imageRef = "cgr.dev/chainguard/zot:latest"; + + const outerData = `${workdir}/data`; + const innerData = "/var/lib/zot/data"; + + const outerPort = "50000"; + const innerPort = "5000"; + + const outerConf = `${workdir}/zot-config.yaml`; + const innerConf = "/zot-config.yaml"; + + await mkdir(outerData); + chmodSync(outerData, 0o777); + + const yaml = heredoc` + distspecversion: 1.1.0-dev + http: + address: 0.0.0.0 + port: ${innerPort} + tls: + key: ${innerKey} + cert: ${innerCrt} + storage: + rootdirectory: ${innerData} + `; + await writeFile(outerConf, yaml); + + const command = ` + docker run --rm --detach + --name "${containerName}" + --user $(id -u):$(id -g) + --publish ${outerPort}:${innerPort} + --volume ${tlsKey}:${innerKey} + --volume ${tlsCrt}:${innerCrt} + --volume "${outerConf}":${innerConf} + --volume "${outerData}":${innerData} + ${imageRef} + serve ${innerConf} + ` + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); + + await cmdStdout(command, { cwd: workdir }); +} + +async function stopZotRegistry() { + const containerName = `${basename(__filename)}.zot`; + + let cmd = `docker ps --filter name=${containerName} --quiet`; + const containerId = await cmdStdout(cmd); + + cmd = `docker kill ${containerId}`; + await cmdStdout(cmd); +} + +/* + Useful during dev but commented out for speed & network reasons +*/ +// describe("Zot Registry", () => { +// let workdir: string; +// let tlsKey: string; +// let tlsCrt: string; + +// beforeAll(async () => { +// workdir = await createWorkdir(); +// ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); +// }); + +// afterAll(async () => { +// await cleanWorkdirs(); +// }); + +// it( +// "works", +// async () => { +// await startZotRegistry(workdir, tlsKey, tlsCrt); + +// const containerName = `${basename(__filename)}.zot`; +// const cmd = `docker ps --filter name=${containerName} --quiet`; +// const containerId = await cmdStdout(cmd); + +// await stopZotRegistry(); + +// expect(containerId).not.toBe(""); +// }, +// mins(1), +// ); +// }); + +async function startDockerRegistry(workdir: string, tlsKey: string, tlsCrt: string) { + const innerKey = `/${basename(tlsKey)}`; + const innerCrt = `/${basename(tlsCrt)}`; + + // https://distribution.github.io/distribution/about/configuration/ + const containerName = `${basename(__filename)}.dkr`; + + const imageRef = "docker.io/library/registry:latest"; + + const outerData = `${workdir}/data`; + const innerData = "/var/lib/registry"; + + const outerPort = "50001"; + const innerPort = "443"; + + const outerConf = `${workdir}/dkr-config.yaml`; + const innerConf = "/etc/docker/registry/config.yml"; + + await mkdir(outerData); + chmodSync(outerData, 0o777); + + const yaml = heredoc` + version: 0.1 + log: + level: debug + fields: + service: registry + storage: + cache: + blobdescriptor: inmemory + filesystem: + rootdirectory: ${innerData} + http: + relativeurls: true + addr: :${innerPort} + host: https://localhost:${outerPort} + headers: + X-Content-Type-Options: [nosniff] + tls: + certificate: ${innerCrt} + key: ${innerKey} + health: + storagedriver: + enabled: true + interval: 10s + threshold: 3 + `; + await writeFile(outerConf, yaml); + + const command = ` + docker run --rm --detach + --name "${containerName}" + --user $(id -u):$(id -g) + --publish ${outerPort}:${innerPort} + --volume ${tlsKey}:${innerKey} + --volume ${tlsCrt}:${innerCrt} + --volume "${outerConf}":${innerConf} + --volume "${outerData}":${innerData} + ${imageRef} + ` + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); + + await cmdStdout(command, { cwd: workdir }); +} + +async function stopDockerRegistry() { + const containerName = `${basename(__filename)}.dkr`; + + let cmd = `docker ps --filter name=${containerName} --quiet`; + const containerId = await cmdStdout(cmd); + + cmd = `docker kill ${containerId}`; + await cmdStdout(cmd); +} + +/* + Useful during dev but commented out for speed & network reasons +*/ +// describe("Docker Registry", () => { +// let workdir: string; +// let tlsKey: string; +// let tlsCrt: string; + +// beforeAll(async () => { +// workdir = await createWorkdir(); +// ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); +// }); + +// afterAll(async () => { +// await cleanWorkdirs(); +// }); + +// it( +// "works", +// async () => { +// await startDockerRegistry(workdir, tlsKey, tlsCrt); + +// const containerName = `${basename(__filename)}.dkr`; +// const cmd = `docker ps --filter name=${containerName} --quiet`; +// const containerId = await cmdStdout(cmd); + +// await stopDockerRegistry(); + +// expect(containerId).not.toBe(""); +// }, +// mins(1), +// ); +// }); + +/* + "Gold standard" -- uses cosign CLI to do what we want to do in JS + useful during dev but commented out for speed & network reasons +*/ +// describe("cosign CLI - ttl.sh - pub/prv keys", () => { +// let cosign: string; +// let workdir: string; + +// const passwd = "password"; +// const prefix = "signing"; +// const pubkey = `${prefix}.pub`; +// const prvkey = `${prefix}.key`; + +// const iref = `ttl.sh/${crypto.randomUUID()}:2m`; + +// beforeAll(async () => { +// workdir = await timed("creating workdir", createWorkdir); + +// cosign = await timed( +// "getting cosign CLI binary", +// async () => await downloadCosign(workdir, "cosign"), +// ); + +// await timed(`generating signing keypair: ${prefix}.*`, async () => +// cmdStderr(`${cosign} generate-key-pair --output-key-prefix=${prefix}`, { +// cwd: workdir, +// env: { COSIGN_PASSWORD: passwd }, +// }), +// ); + +// await writeFile(`${workdir}/Dockerfile`, "FROM docker.io/library/hello-world"); +// await timed(`uploading container image: ${iref}`, async () => +// cmdStderr(`docker build --tag ${iref} --push .`, { cwd: workdir }), +// ); + +// await timed(`signing image: ${iref}`, async () => +// cmdStderr(`${cosign} sign --tlog-upload=false --key=${prvkey} ${iref}`, { +// cwd: workdir, +// env: { COSIGN_PASSWORD: passwd }, +// }), +// ); +// }, mins(1)); + +// afterAll(async () => await cleanWorkdirs()); + +// it("can be verified via CLI", async () => { +// const result = await cmdStderr( +// `${cosign} verify --insecure-ignore-tlog=true --key=${pubkey} ${iref}`, +// { cwd: workdir, env: { COSIGN_PASSWORD: passwd } }, +// ); + +// expect(result).not.toContain("no matching signatures"); +// expect(result).toContain("signatures were verified"); +// }); +// }); + +describe("sigstore-js - zot (OCI) - pub/prv keys", () => { + let workdir: string; + let tlsKey: string; + let tlsCrt: string; + + let cosign: string; + let crane: string; + + const passwd = "password"; + const prefix = "signing"; + const rawPrvkey = `${prefix}-raw.key`; + const rawPubkey = `${prefix}-raw.pub`; + const cosPrvkey = `${prefix}-cos.key`; + // const cosPubkey = `${prefix}-cos.pub`; + + const iref = `localhost:50000/${crypto.randomUUID()}:latest`; + + beforeAll(async () => { + workdir = await timed("creating workdir", createWorkdir); + + await timed("generating TLS certificate", async () => { + ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); + }); + + await timed("starting Zot container registry", async () => { + await startZotRegistry(workdir, tlsKey, tlsCrt); + }); + + const npmBin = `${await cmdStdout("npm root")}/.bin`; + cosign = await timed( + "getting cosign CLI binary", + async () => await downloadCosign(npmBin, "cosign"), + ); + + crane = await timed( + "getting crane CLI binary", + async () => await downloadCrane(npmBin, "crane"), + ); + + await timed(`generating keypair: ${prefix}.*`, async () => { + const keypair = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + + await writeFile( + `${workdir}/${rawPubkey}`, + keypair.publicKey.export({ format: "pem", type: "spki" }).toString("ascii"), + ); + chmodSync(`${workdir}/${rawPubkey}`, 0o644); + + await writeFile( + `${workdir}/${rawPrvkey}`, + keypair.privateKey.export({ format: "pem", type: "pkcs8" }).toString("ascii"), + ); + chmodSync(`${workdir}/${rawPrvkey}`, 0o600); + }); + + await timed( + `converting ${rawPrvkey} (node crypto) to ${cosPrvkey} (cosign native)`, + async () => + await cmdStderr( + `${cosign} import-key-pair --key=${rawPrvkey} --output-key-prefix=${basename(cosPrvkey, ".key")}`, + { + cwd: workdir, + env: { COSIGN_PASSWORD: passwd }, + }, + ), + ); + + await timed( + "generating test dockerfile", + async () => await writeFile(`${workdir}/Dockerfile`, "FROM docker.io/library/hello-world"), + ); + + const builder = `${basename(__filename)}-oci`; + await timed(`creating docker buildx oci builder (${builder})`, async () => { + if (await builderExists(builder)) { + return; + } + + const cmd = ` + docker buildx create + --driver docker-container + --driver-opt image=moby/buildkit:master,network=host + --name=${builder} + --bootstrap + ` + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); + + await cmdStdout(cmd); + }); + + await timed(`uploading test container image: ${iref}`, async () => { + const dir = `${workdir}/image.dir`; + + const command = ` + docker buildx build + --platform linux/amd64 + --tag ${iref} + --output type=oci,tar=false,dest=${dir} + --builder=${builder} + . + ` + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); + + await cmd(command, { cwd: workdir }); + await cmd(`${crane} --insecure push ${dir} ${iref}`); + }); + + await timed(`removing docker buildx oci builder (${builder})`, async () => { + if (!(await builderExists(builder))) { + return; + } + await cmdStdout(`docker buildx rm ${builder}`); + }); + + await timed(`cosign signing image: ${iref}`, async () => + cmdStderr( + `${cosign} sign --allow-insecure-registry=true --tlog-upload=false --key=${cosPrvkey} ${iref}`, + { + cwd: workdir, + env: { COSIGN_PASSWORD: passwd }, + }, + ), + ); + }, mins(2)); + + afterAll(async () => { + await stopZotRegistry(); + await cleanWorkdirs(); + }); + + it("verifies successfully", async () => { + const tlsCrts = [await readFile(tlsCrt, { encoding: "utf-8" })]; + + const verified = await sut.verifyImage(iref, [`${workdir}/${rawPubkey}`], tlsCrts); + + expect(verified).toBe(true); + }); +}); + +describe("sigstore-js - registry (Docker) - pub/prv keys", () => { + let workdir: string; + let tlsKey: string; + let tlsCrt: string; + + let cosign: string; + let crane: string; + + const passwd = "password"; + const prefix = "signing"; + const rawPrvkey = `${prefix}-raw.key`; + const rawPubkey = `${prefix}-raw.pub`; + const cosPrvkey = `${prefix}-cos.key`; + // const cosPubkey = `${prefix}-cos.pub`; + + const iref = `localhost:50001/${crypto.randomUUID()}:latest`; + + beforeAll(async () => { + workdir = await timed("creating workdir", createWorkdir); + + await timed("generating TLS certificate", async () => { + ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); + }); + + await timed("starting Docker container registry", async () => { + await startDockerRegistry(workdir, tlsKey, tlsCrt); + }); + + const npmBin = `${await cmdStdout("npm root")}/.bin`; + cosign = await timed( + "getting cosign CLI binary", + async () => await downloadCosign(npmBin, "cosign"), + ); + + crane = await timed( + "getting crane CLI binary", + async () => await downloadCrane(npmBin, "crane"), + ); + + await timed(`generating signing keypair: ${prefix}.*`, async () => { + const keypair = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + + await writeFile( + `${workdir}/${rawPubkey}`, + keypair.publicKey.export({ format: "pem", type: "spki" }).toString("ascii"), + ); + chmodSync(`${workdir}/${rawPubkey}`, 0o644); + + await writeFile( + `${workdir}/${rawPrvkey}`, + keypair.privateKey.export({ format: "pem", type: "pkcs8" }).toString("ascii"), + ); + chmodSync(`${workdir}/${rawPrvkey}`, 0o600); + }); + + await timed( + `converting ${rawPrvkey} (node crypto) to ${cosPrvkey} (cosign native)`, + async () => + await cmdStderr( + `${cosign} import-key-pair --key=${rawPrvkey} --output-key-prefix=${basename(cosPrvkey, ".key")}`, + { + cwd: workdir, + env: { COSIGN_PASSWORD: passwd }, + }, + ), + ); + + await timed( + "generating test dockerfile", + async () => await writeFile(`${workdir}/Dockerfile`, "FROM docker.io/library/hello-world"), + ); + + await timed(`uploading test container image: ${iref}`, async () => { + let command = `docker build --tag ${iref} --output type=docker .`; + await cmd(command, { cwd: workdir }); + + const tar = `${workdir}/image.tar`; + command = `docker save --output ${tar} ${iref}`; + await cmd(command); + + await cmd(`${crane} push --insecure ${tar} ${iref}`); + }); + + await timed(`cosign signing image: ${iref}`, async () => + cmdStderr( + `${cosign} sign --allow-insecure-registry=true --tlog-upload=false --key=${cosPrvkey} ${iref}`, + { + cwd: workdir, + env: { COSIGN_PASSWORD: passwd }, + }, + ), + ); + }, mins(2)); + + afterAll(async () => { + await stopDockerRegistry(); + await cleanWorkdirs(); + }); + + it("verifies successfully", async () => { + const tlsCrts = [await readFile(tlsCrt, { encoding: "utf-8" })]; + + const verified = await sut.verifyImage(iref, [`${workdir}/${rawPubkey}`], tlsCrts); + + expect(verified).toBe(true); + }); +}); From 8e66a0c5afb97d888bca9a6ce6c54ab2a0412a9e Mon Sep 17 00:00:00 2001 From: Case Wylie Date: Fri, 1 Nov 2024 09:17:18 -0400 Subject: [PATCH 4/4] chore: remove unnecessary file; Signed-off-by: Case Wylie --- src/sdk/cosign.e2e.test.txt | 710 ------------------------------------ 1 file changed, 710 deletions(-) delete mode 100644 src/sdk/cosign.e2e.test.txt diff --git a/src/sdk/cosign.e2e.test.txt b/src/sdk/cosign.e2e.test.txt deleted file mode 100644 index 74ba7008a..000000000 --- a/src/sdk/cosign.e2e.test.txt +++ /dev/null @@ -1,710 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2023-Present The Pepr Authors - -import { afterAll, beforeAll, expect } from "@jest/globals"; -import { describe, it } from "@jest/globals"; -import { promisify } from "node:util"; -import * as child_process from "node:child_process"; -const exec = promisify(child_process.exec); -import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { chmodSync, readdirSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { basename, dirname, join } from "node:path"; -import * as crypto from "node:crypto"; -import { heredoc } from "./heredoc"; - -import * as sut from "./cosign"; - -const secs = (s: number) => s * 1000; -const mins = (m: number) => m * secs(60); - -const cmd = async (command: string, opts = {}) => await exec(command, opts); -const cmdStdout = async (command: string, opts = {}) => (await cmd(command, opts)).stdout.trim(); -const cmdStderr = async (command: string, opts = {}) => (await cmd(command, opts)).stderr.trim(); -const exists = async (path: string) => { - try { - await access(path); - return true; - } catch (e) { - return false; - } -}; - -const workdirPrefix = () => join(tmpdir(), `${basename(__filename)}-`); - -async function createWorkdir() { - const prefix = workdirPrefix(); - return await mkdtemp(prefix); -} - -async function cleanWorkdirs() { - const prefix = workdirPrefix(); - const dir = dirname(prefix); - const pre = basename(prefix); - - const workdirs = readdirSync(dir) - .filter(f => f.startsWith(pre)) - .map(m => join(dir, m)); - - await Promise.all(workdirs.map(m => rm(m, { recursive: true, force: true }))); -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -const timed = async (msg: string, func: () => Promise) => { - console.time(msg); - const result = await func(); - console.timeEnd(msg); - return result; -}; - -async function builderExists(name: string) { - const resultRaw = await cmdStdout(`docker buildx ls --format json`); - const result = resultRaw.split("\n").map(m => JSON.parse(m)); - const found = result.filter(f => f.Name === name).length; - - return !!found; -} - -enum OS { - Linux = "Linux", - Mac = "Darwin", -} - -enum Arch { - x86_64 = "x86_64", - arm64 = "arm64", -} - -const sniffOS = async () => await cmdStdout("uname -s"); -const sniffArch = async () => await cmdStdout("uname -m"); - -async function downloadCosign(path: string, fname: string) { - const local = join(path, fname); - - if (await exists(local)) { - return local; - } - - let os = await sniffOS(); - os = os === OS.Linux ? OS.Linux.toLowerCase() : os; - os = os === OS.Mac ? OS.Mac.toLowerCase() : os; - - let arch = await sniffArch(); - arch = arch === Arch.x86_64 ? "amd64" : arch; - - const got = await sut.get( - "https://api.github.com/repos/sigstore/cosign/releases/latest", - "application/json", - ); - const ver = JSON.parse(got.body)["tag_name"]; - - const remote = `https://github.com/sigstore/cosign/releases/download/${ver}/cosign-${os}-${arch}`; - await sut.download(remote, local); - chmodSync(local, 0o777); - - return local; -} - -/* - Useful during dev but commented out for speed & network reasons -*/ -// describe("downloadCosign()", () => { -// let workdir: string; - -// beforeAll(async () => { -// workdir = await createWorkdir(); -// }); - -// afterAll(async () => { -// await cleanWorkdirs(); -// }); - -// it("works", async () => { -// const cosign = await downloadCosign(workdir, "cosign"); -// const result = await cmdStdout(`${cosign} version`, { cwd: workdir }); -// expect(result).toMatch(/cosign: A tool/); -// }); -// }); - -async function downloadCrane(path: string, fname: string) { - const local = join(path, fname); - const localTgz = `${local}.tar.gz`; - - if (await exists(local)) { - return local; - } - - const os = await sniffOS(); - const arch = await sniffArch(); - - const got = await sut.get( - "https://api.github.com/repos/google/go-containerregistry/releases/latest", - "application/json", - ); - const ver = JSON.parse(got.body)["tag_name"]; - - const remote = `https://github.com/google/go-containerregistry/releases/download/${ver}/go-containerregistry_${os}_${arch}.tar.gz`; - await sut.download(remote, localTgz); - - await cmdStdout(`tar -zxvf ${localTgz} ${fname}`, { cwd: path }); - chmodSync(local, 0o777); - - return local; -} - -/* - Useful during dev but commented out for speed & network reasons -*/ -// describe("downloadCrane()", () => { -// let workdir: string; - -// beforeAll(async () => { -// workdir = await createWorkdir(); -// }); - -// afterAll(async () => { -// await cleanWorkdirs(); -// }); - -// it("works", async () => { -// const crane = await downloadCrane(workdir, "crane"); -// const result = await cmdStdout(`${crane} --help`, { cwd: workdir }); -// expect(result).toMatch(/Crane is a tool/); -// }); -// }); - -async function genTlsCrt(workdir: string) { - const tlsKey = `${workdir}/tls-key.pem`; - const tlsCsr = `${workdir}/tls-csr.pem`; - const tlsCrt = `${workdir}/tls-crt.pem`; - - let command = `openssl genrsa -out ${tlsKey} 1024`; - await cmd(command); - - command = - `openssl req -new -key ${tlsKey} -out ${tlsCsr}` + - ` -subj "/C=US/ST=Colorado/L=Colorado Springs/O=Defense Unicorns/CN=localhost"` + - ` -addext "subjectAltName = DNS:localhost"`; - await cmd(command); - - command = - `openssl x509 -req -in ${tlsCsr} -key ${tlsKey} -out ${tlsCrt}` + ` -copy_extensions copyall`; - await cmd(command); - - return { key: tlsKey, crt: tlsCrt }; -} - -async function startZotRegistry(workdir: string, tlsKey: string, tlsCrt: string) { - const innerKey = `/${basename(tlsKey)}`; - const innerCrt = `/${basename(tlsCrt)}`; - - // https://images.chainguard.dev/directory/image/zot/overview - const containerName = `${basename(__filename)}.zot`; - - const imageRef = "cgr.dev/chainguard/zot:latest"; - - const outerData = `${workdir}/data`; - const innerData = "/var/lib/zot/data"; - - const outerPort = "50000"; - const innerPort = "5000"; - - const outerConf = `${workdir}/zot-config.yaml`; - const innerConf = "/zot-config.yaml"; - - await mkdir(outerData); - chmodSync(outerData, 0o777); - - const yaml = heredoc` - distspecversion: 1.1.0-dev - http: - address: 0.0.0.0 - port: ${innerPort} - tls: - key: ${innerKey} - cert: ${innerCrt} - storage: - rootdirectory: ${innerData} - `; - await writeFile(outerConf, yaml); - - const command = ` - docker run --rm --detach - --name "${containerName}" - --user $(id -u):$(id -g) - --publish ${outerPort}:${innerPort} - --volume ${tlsKey}:${innerKey} - --volume ${tlsCrt}:${innerCrt} - --volume "${outerConf}":${innerConf} - --volume "${outerData}":${innerData} - ${imageRef} - serve ${innerConf} - ` - .replace(/\n/g, " ") - .replace(/\s+/g, " ") - .trim(); - - await cmdStdout(command, { cwd: workdir }); -} - -async function stopZotRegistry() { - const containerName = `${basename(__filename)}.zot`; - - let cmd = `docker ps --filter name=${containerName} --quiet`; - const containerId = await cmdStdout(cmd); - - cmd = `docker kill ${containerId}`; - await cmdStdout(cmd); -} - -/* - Useful during dev but commented out for speed & network reasons -*/ -// describe("Zot Registry", () => { -// let workdir: string; -// let tlsKey: string; -// let tlsCrt: string; - -// beforeAll(async () => { -// workdir = await createWorkdir(); -// ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); -// }); - -// afterAll(async () => { -// await cleanWorkdirs(); -// }); - -// it( -// "works", -// async () => { -// await startZotRegistry(workdir, tlsKey, tlsCrt); - -// const containerName = `${basename(__filename)}.zot`; -// const cmd = `docker ps --filter name=${containerName} --quiet`; -// const containerId = await cmdStdout(cmd); - -// await stopZotRegistry(); - -// expect(containerId).not.toBe(""); -// }, -// mins(1), -// ); -// }); - -async function startDockerRegistry(workdir: string, tlsKey: string, tlsCrt: string) { - const innerKey = `/${basename(tlsKey)}`; - const innerCrt = `/${basename(tlsCrt)}`; - - // https://distribution.github.io/distribution/about/configuration/ - const containerName = `${basename(__filename)}.dkr`; - - const imageRef = "docker.io/library/registry:latest"; - - const outerData = `${workdir}/data`; - const innerData = "/var/lib/registry"; - - const outerPort = "50001"; - const innerPort = "443"; - - const outerConf = `${workdir}/dkr-config.yaml`; - const innerConf = "/etc/docker/registry/config.yml"; - - await mkdir(outerData); - chmodSync(outerData, 0o777); - - const yaml = heredoc` - version: 0.1 - log: - level: debug - fields: - service: registry - storage: - cache: - blobdescriptor: inmemory - filesystem: - rootdirectory: ${innerData} - http: - relativeurls: true - addr: :${innerPort} - host: https://localhost:${outerPort} - headers: - X-Content-Type-Options: [nosniff] - tls: - certificate: ${innerCrt} - key: ${innerKey} - health: - storagedriver: - enabled: true - interval: 10s - threshold: 3 - `; - await writeFile(outerConf, yaml); - - const command = ` - docker run --rm --detach - --name "${containerName}" - --user $(id -u):$(id -g) - --publish ${outerPort}:${innerPort} - --volume ${tlsKey}:${innerKey} - --volume ${tlsCrt}:${innerCrt} - --volume "${outerConf}":${innerConf} - --volume "${outerData}":${innerData} - ${imageRef} - ` - .replace(/\n/g, " ") - .replace(/\s+/g, " ") - .trim(); - - await cmdStdout(command, { cwd: workdir }); -} - -async function stopDockerRegistry() { - const containerName = `${basename(__filename)}.dkr`; - - let cmd = `docker ps --filter name=${containerName} --quiet`; - const containerId = await cmdStdout(cmd); - - cmd = `docker kill ${containerId}`; - await cmdStdout(cmd); -} - -/* - Useful during dev but commented out for speed & network reasons -*/ -// describe("Docker Registry", () => { -// let workdir: string; -// let tlsKey: string; -// let tlsCrt: string; - -// beforeAll(async () => { -// workdir = await createWorkdir(); -// ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); -// }); - -// afterAll(async () => { -// await cleanWorkdirs(); -// }); - -// it( -// "works", -// async () => { -// await startDockerRegistry(workdir, tlsKey, tlsCrt); - -// const containerName = `${basename(__filename)}.dkr`; -// const cmd = `docker ps --filter name=${containerName} --quiet`; -// const containerId = await cmdStdout(cmd); - -// await stopDockerRegistry(); - -// expect(containerId).not.toBe(""); -// }, -// mins(1), -// ); -// }); - -/* - "Gold standard" -- uses cosign CLI to do what we want to do in JS - useful during dev but commented out for speed & network reasons -*/ -// describe("cosign CLI - ttl.sh - pub/prv keys", () => { -// let cosign: string; -// let workdir: string; - -// const passwd = "password"; -// const prefix = "signing"; -// const pubkey = `${prefix}.pub`; -// const prvkey = `${prefix}.key`; - -// const iref = `ttl.sh/${crypto.randomUUID()}:2m`; - -// beforeAll(async () => { -// workdir = await timed("creating workdir", createWorkdir); - -// cosign = await timed( -// "getting cosign CLI binary", -// async () => await downloadCosign(workdir, "cosign"), -// ); - -// await timed(`generating signing keypair: ${prefix}.*`, async () => -// cmdStderr(`${cosign} generate-key-pair --output-key-prefix=${prefix}`, { -// cwd: workdir, -// env: { COSIGN_PASSWORD: passwd }, -// }), -// ); - -// await writeFile(`${workdir}/Dockerfile`, "FROM docker.io/library/hello-world"); -// await timed(`uploading container image: ${iref}`, async () => -// cmdStderr(`docker build --tag ${iref} --push .`, { cwd: workdir }), -// ); - -// await timed(`signing image: ${iref}`, async () => -// cmdStderr(`${cosign} sign --tlog-upload=false --key=${prvkey} ${iref}`, { -// cwd: workdir, -// env: { COSIGN_PASSWORD: passwd }, -// }), -// ); -// }, mins(1)); - -// afterAll(async () => await cleanWorkdirs()); - -// it("can be verified via CLI", async () => { -// const result = await cmdStderr( -// `${cosign} verify --insecure-ignore-tlog=true --key=${pubkey} ${iref}`, -// { cwd: workdir, env: { COSIGN_PASSWORD: passwd } }, -// ); - -// expect(result).not.toContain("no matching signatures"); -// expect(result).toContain("signatures were verified"); -// }); -// }); - -describe("sigstore-js - zot (OCI) - pub/prv keys", () => { - let workdir: string; - let tlsKey: string; - let tlsCrt: string; - - let cosign: string; - let crane: string; - - const passwd = "password"; - const prefix = "signing"; - const rawPrvkey = `${prefix}-raw.key`; - const rawPubkey = `${prefix}-raw.pub`; - const cosPrvkey = `${prefix}-cos.key`; - // const cosPubkey = `${prefix}-cos.pub`; - - const iref = `localhost:50000/${crypto.randomUUID()}:latest`; - - beforeAll(async () => { - workdir = await timed("creating workdir", createWorkdir); - - await timed("generating TLS certificate", async () => { - ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); - }); - - await timed("starting Zot container registry", async () => { - await startZotRegistry(workdir, tlsKey, tlsCrt); - }); - - const npmBin = `${await cmdStdout("npm root")}/.bin`; - cosign = await timed( - "getting cosign CLI binary", - async () => await downloadCosign(npmBin, "cosign"), - ); - - crane = await timed( - "getting crane CLI binary", - async () => await downloadCrane(npmBin, "crane"), - ); - - await timed(`generating keypair: ${prefix}.*`, async () => { - const keypair = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); - - await writeFile( - `${workdir}/${rawPubkey}`, - keypair.publicKey.export({ format: "pem", type: "spki" }).toString("ascii"), - ); - chmodSync(`${workdir}/${rawPubkey}`, 0o644); - - await writeFile( - `${workdir}/${rawPrvkey}`, - keypair.privateKey.export({ format: "pem", type: "pkcs8" }).toString("ascii"), - ); - chmodSync(`${workdir}/${rawPrvkey}`, 0o600); - }); - - await timed( - `converting ${rawPrvkey} (node crypto) to ${cosPrvkey} (cosign native)`, - async () => - await cmdStderr( - `${cosign} import-key-pair --key=${rawPrvkey} --output-key-prefix=${basename(cosPrvkey, ".key")}`, - { - cwd: workdir, - env: { COSIGN_PASSWORD: passwd }, - }, - ), - ); - - await timed( - "generating test dockerfile", - async () => await writeFile(`${workdir}/Dockerfile`, "FROM docker.io/library/hello-world"), - ); - - const builder = `${basename(__filename)}-oci`; - await timed(`creating docker buildx oci builder (${builder})`, async () => { - if (await builderExists(builder)) { - return; - } - - const cmd = ` - docker buildx create - --driver docker-container - --driver-opt image=moby/buildkit:master,network=host - --name=${builder} - --bootstrap - ` - .replace(/\n/g, " ") - .replace(/\s+/g, " ") - .trim(); - - await cmdStdout(cmd); - }); - - await timed(`uploading test container image: ${iref}`, async () => { - const dir = `${workdir}/image.dir`; - - const command = ` - docker buildx build - --platform linux/amd64 - --tag ${iref} - --output type=oci,tar=false,dest=${dir} - --builder=${builder} - . - ` - .replace(/\n/g, " ") - .replace(/\s+/g, " ") - .trim(); - - await cmd(command, { cwd: workdir }); - await cmd(`${crane} --insecure push ${dir} ${iref}`); - }); - - await timed(`removing docker buildx oci builder (${builder})`, async () => { - if (!(await builderExists(builder))) { - return; - } - await cmdStdout(`docker buildx rm ${builder}`); - }); - - await timed(`cosign signing image: ${iref}`, async () => - cmdStderr( - `${cosign} sign --allow-insecure-registry=true --tlog-upload=false --key=${cosPrvkey} ${iref}`, - { - cwd: workdir, - env: { COSIGN_PASSWORD: passwd }, - }, - ), - ); - }, mins(2)); - - afterAll(async () => { - await stopZotRegistry(); - await cleanWorkdirs(); - }); - - it("verifies successfully", async () => { - const tlsCrts = [await readFile(tlsCrt, { encoding: "utf-8" })]; - - const verified = await sut.verifyImage(iref, [`${workdir}/${rawPubkey}`], tlsCrts); - - expect(verified).toBe(true); - }); -}); - -describe("sigstore-js - registry (Docker) - pub/prv keys", () => { - let workdir: string; - let tlsKey: string; - let tlsCrt: string; - - let cosign: string; - let crane: string; - - const passwd = "password"; - const prefix = "signing"; - const rawPrvkey = `${prefix}-raw.key`; - const rawPubkey = `${prefix}-raw.pub`; - const cosPrvkey = `${prefix}-cos.key`; - // const cosPubkey = `${prefix}-cos.pub`; - - const iref = `localhost:50001/${crypto.randomUUID()}:latest`; - - beforeAll(async () => { - workdir = await timed("creating workdir", createWorkdir); - - await timed("generating TLS certificate", async () => { - ({ key: tlsKey, crt: tlsCrt } = await genTlsCrt(workdir)); - }); - - await timed("starting Docker container registry", async () => { - await startDockerRegistry(workdir, tlsKey, tlsCrt); - }); - - const npmBin = `${await cmdStdout("npm root")}/.bin`; - cosign = await timed( - "getting cosign CLI binary", - async () => await downloadCosign(npmBin, "cosign"), - ); - - crane = await timed( - "getting crane CLI binary", - async () => await downloadCrane(npmBin, "crane"), - ); - - await timed(`generating signing keypair: ${prefix}.*`, async () => { - const keypair = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); - - await writeFile( - `${workdir}/${rawPubkey}`, - keypair.publicKey.export({ format: "pem", type: "spki" }).toString("ascii"), - ); - chmodSync(`${workdir}/${rawPubkey}`, 0o644); - - await writeFile( - `${workdir}/${rawPrvkey}`, - keypair.privateKey.export({ format: "pem", type: "pkcs8" }).toString("ascii"), - ); - chmodSync(`${workdir}/${rawPrvkey}`, 0o600); - }); - - await timed( - `converting ${rawPrvkey} (node crypto) to ${cosPrvkey} (cosign native)`, - async () => - await cmdStderr( - `${cosign} import-key-pair --key=${rawPrvkey} --output-key-prefix=${basename(cosPrvkey, ".key")}`, - { - cwd: workdir, - env: { COSIGN_PASSWORD: passwd }, - }, - ), - ); - - await timed( - "generating test dockerfile", - async () => await writeFile(`${workdir}/Dockerfile`, "FROM docker.io/library/hello-world"), - ); - - await timed(`uploading test container image: ${iref}`, async () => { - let command = `docker build --tag ${iref} --output type=docker .`; - await cmd(command, { cwd: workdir }); - - const tar = `${workdir}/image.tar`; - command = `docker save --output ${tar} ${iref}`; - await cmd(command); - - await cmd(`${crane} push --insecure ${tar} ${iref}`); - }); - - await timed(`cosign signing image: ${iref}`, async () => - cmdStderr( - `${cosign} sign --allow-insecure-registry=true --tlog-upload=false --key=${cosPrvkey} ${iref}`, - { - cwd: workdir, - env: { COSIGN_PASSWORD: passwd }, - }, - ), - ); - }, mins(2)); - - afterAll(async () => { - await stopDockerRegistry(); - await cleanWorkdirs(); - }); - - it("verifies successfully", async () => { - const tlsCrts = [await readFile(tlsCrt, { encoding: "utf-8" })]; - - const verified = await sut.verifyImage(iref, [`${workdir}/${rawPubkey}`], tlsCrts); - - expect(verified).toBe(true); - }); -});