#!/usr/bin/env -S deno run --allow-env --allow-run --allow-read import {parseArgs} from "jsr:@std/cli/parse-args"; // Reference: // * https://docs.deno.com/api/deno/~/Deno.Command // * https://docs.deno.com/examples/command_line_arguments/ // { // "format": "rekor-attest-v1", // "files": [ // { // "filename": "example.rs", // "rekorId": "xxxxxxxxxxxxxxxxx" // } // ] // } // get from url or rekor-cli // const REKOR_ENTRY_URL = "https://rekor.sigstore.dev/api/v1/log/entries/${UUID}"; // ----- CONFIG ----- interface RekorAttest { format: string; files: Array; } interface RekorAttestFile { filename: string; rekorId: string; } // ----- REKOR RECORD ----- interface RekorRecord { Attestation: string; AttestationType: string; Body: RekorRecordBody; LogIndex: number; IntegratedTime: number; UUID: number; LogID: number; } interface RekorRecordBody { RekordObj: RekorRecordBodyRekordObj; } interface RekorRecordBodyRekordObj { data: RekorRecordBodyRekordObjData; signature: RekorRecordBodyRekordObjSignature; } interface RekorRecordBodyRekordObjData { hash: RekorRecordHash; } interface RekorRecordHash { algorithm: string; value: string; } interface RekorRecordBodyRekordObjSignature { content: string; format: string; publicKey: RekorRecordPublicKey; } interface RekorRecordPublicKey { content: string; } async function loadAttest(filename: string): Promise { const attest = await Deno.readTextFile(filename); return JSON.parse(attest); } function fetchRekorViaCli(uuid: string): RekorRecord { if (isStringEmpty(uuid)) { throw "Rekor UUID is empty." } const command = new Deno.Command("rekor-cli", { args: [ "get", "--format", "json", "--uuid", uuid ], }); const {code, stdout, stderr} = command.outputSync(); if (code !== 0) { console.error(`Execute command rekor-cli failed: code: ${code} stdout: ${new TextDecoder().decode(stdout)} stderr: ${new TextDecoder().decode(stderr)}`); throw `Execute command rekor-cli failed, code: ${code}` } const rekorBody = new TextDecoder().decode(stdout); return JSON.parse(rekorBody); } function isStringEmpty(str: string): boolean { return str === null || str === undefined || str == ""; } interface Args { help: boolean; attest?: string; } function parseToArgs(): Args { return parseArgs(Deno.args, { boolean: ["help"], string: ["attest"], }); } async function verifyFile(attestFile: RekorAttestFile) { const rekorRecord = fetchRekorViaCli(attestFile.rekorId); console.log(rekorRecord); // TODO ... } async function main() { const args = parseToArgs(); if (args.help) { console.log(`sigstore-verify.ts - sigstore verify cli sigstore-verify.ts [--attest sigstore-attest.json] `); return; } let attestFileName = args.attest || "sigstore-attest.json"; let attest = await loadAttest(attestFileName); if (attest.format !== "rekor-attest-v1") { throw `Bad rekor attest file format: ${attest.format}`; } for (let i = 0; i < attest.files.length; i++) { const attestFile = attest.files[i]; console.log(`Process file: ${attestFile.filename} [${i + 1} / ${attest.files.length}]`); await verifyFile(attestFile); } } await main();