From ef6de5f8e913ddd20ecad86117b9233a9e7ed51c Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Mon, 2 Dec 2024 15:23:09 -0800 Subject: [PATCH 1/2] support verify by digest in conformance suite Signed-off-by: Brian DeHamer --- .changeset/tame-wasps-decide.md | 5 + package-lock.json | 82 ++++++++ packages/conformance/package.json | 2 + .../conformance/src/commands/verify-bundle.ts | 197 ++++++++++++++---- 4 files changed, 243 insertions(+), 43 deletions(-) create mode 100644 .changeset/tame-wasps-decide.md diff --git a/.changeset/tame-wasps-decide.md b/.changeset/tame-wasps-decide.md new file mode 100644 index 000000000..7a8c3e9f9 --- /dev/null +++ b/.changeset/tame-wasps-decide.md @@ -0,0 +1,5 @@ +--- +'@sigstore/conformance': minor +--- + +Support verification by artifact OR artifact digest diff --git a/package-lock.json b/package-lock.json index eb9f15738..e05d21478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5432,6 +5432,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bn.js": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.6.tgz", + "integrity": "sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "dev": true, @@ -5449,6 +5459,16 @@ "@types/node": "*" } }, + "node_modules/@types/elliptic": { + "version": "6.4.18", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.18.tgz", + "integrity": "sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bn.js": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "dev": true, @@ -6243,6 +6263,12 @@ "node": ">=4" } }, + "node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "license": "MIT", @@ -6299,6 +6325,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.23.0", "dev": true, @@ -7048,6 +7080,21 @@ "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emittery": { "version": "0.13.1", "dev": true, @@ -8163,6 +8210,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "license": "MIT", @@ -8182,6 +8239,17 @@ "tslib": "^2.0.3" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "dev": true, @@ -10597,6 +10665,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "9.0.5", "license": "ISC", @@ -13180,12 +13260,14 @@ "@sigstore/bundle": "^3.0.0", "@sigstore/protobuf-specs": "^0.3.2", "@sigstore/verify": "^2.0.0", + "elliptic": "^6.6.1", "sigstore": "^3.0.0" }, "bin": { "sigstore": "bin/run" }, "devDependencies": { + "@types/elliptic": "^6.4.18", "oclif": "^4", "tslib": "^2.8.1" }, diff --git a/packages/conformance/package.json b/packages/conformance/package.json index 386af96fc..c74c32977 100644 --- a/packages/conformance/package.json +++ b/packages/conformance/package.json @@ -21,9 +21,11 @@ "@sigstore/bundle": "^3.0.0", "@sigstore/protobuf-specs": "^0.3.2", "@sigstore/verify": "^2.0.0", + "elliptic": "^6.6.1", "sigstore": "^3.0.0" }, "devDependencies": { + "@types/elliptic": "^6.4.18", "oclif": "^4", "tslib": "^2.8.1" }, diff --git a/packages/conformance/src/commands/verify-bundle.ts b/packages/conformance/src/commands/verify-bundle.ts index 75c6bf91c..5f7d0c122 100644 --- a/packages/conformance/src/commands/verify-bundle.ts +++ b/packages/conformance/src/commands/verify-bundle.ts @@ -1,13 +1,24 @@ import { Args, Command, Flags } from '@oclif/core'; -import { bundleFromJSON } from '@sigstore/bundle'; +import { Bundle, bundleFromJSON, MessageSignature } from '@sigstore/bundle'; import { TrustedRoot } from '@sigstore/protobuf-specs'; -import { Verifier, toSignedEntity, toTrustMaterial } from '@sigstore/verify'; +import * as tuf from '@sigstore/tuf'; +import { + SignedEntity, + toSignedEntity, + toTrustMaterial, + TrustMaterial, + Verifier, +} from '@sigstore/verify'; +import { ec as EC } from 'elliptic'; +import { existsSync } from 'fs'; import fs from 'fs/promises'; +import crypto from 'node:crypto'; import os from 'os'; import path from 'path'; -import * as sigstore from 'sigstore'; import { TUF_STAGING_ROOT, TUF_STAGING_URL } from '../staging'; +const DIGEST_PREFIX = 'sha256:'; + export default class VerifyBundle extends Command { static override flags = { bundle: Flags.string({ @@ -34,55 +45,155 @@ export default class VerifyBundle extends Command { }; static override args = { - file: Args.file({ - description: 'artifact to verify', + fileOrDigest: Args.string({ + description: 'path to the artifact to verify, or its digest', required: true, - exists: true, }), }; public async run(): Promise { const { args, flags } = await this.parse(VerifyBundle); + const trustedRootPath = flags['trusted-root']; const bundle = await fs .readFile(flags.bundle) - .then((data) => JSON.parse(data.toString())); - const artifact = await fs.readFile(args.file); - const trustedRootPath = flags['trusted-root']; + .then((data) => JSON.parse(data.toString())) + .then((json) => bundleFromJSON(json)); + + const trustMaterial = trustedRootPath + ? await trustMaterialFromPath(trustedRootPath) + : await trustMaterialFromTUF(flags['staging']); + const verifier = new Verifier(trustMaterial); + + const policy = { + subjectAlternativeName: flags['certificate-identity'], + extensions: { issuer: flags['certificate-oidc-issuer'] }, + }; + + const signedEntity = isDigest(args.fileOrDigest) + ? await signedEntityFromDigest(bundle, args.fileOrDigest) + : await signedEntityFromFile(bundle, args.fileOrDigest); + + verifier.verify(signedEntity, policy); + } +} + +// Initialize TrustMaterial from TUF +async function trustMaterialFromTUF(staging: boolean): Promise { + const opts: tuf.TUFOptions = {}; + + if (staging) { + // Write the initial root.json to a temporary directory + const tmpPath = await fs.mkdtemp(path.join(os.tmpdir(), 'sigstore-')); + const rootPath = path.join(tmpPath, 'root.json'); + await fs.writeFile(rootPath, Buffer.from(TUF_STAGING_ROOT, 'base64')); + + opts.mirrorURL = TUF_STAGING_URL; + opts.rootPath = rootPath; + } + + const trustedRoot = await tuf.getTrustedRoot(opts); + return toTrustMaterial(trustedRoot); +} + +// Initialize TrustMaterial from a file +async function trustMaterialFromPath(path: string): Promise { + const trustedRoot = await fs + .readFile(path) + .then((data) => JSON.parse(data.toString())); + return toTrustMaterial(TrustedRoot.fromJSON(trustedRoot)); +} + +// Initialize SignedEntity with the artifact to verify +async function signedEntityFromFile( + bundle: Bundle, + fileOrDigest: string +): Promise { + const artifact = await fs.readFile(fileOrDigest); + return toSignedEntity(bundle, artifact); +} + +// Initialize SignedEntity with the digest of the artifact to verify +async function signedEntityFromDigest( + bundle: Bundle, + digest: string +): Promise { + const signedEntity = toSignedEntity(bundle); + + if (bundle.content.$case === 'messageSignature') { + signedEntity.signature = new MessageDigestSignatureContent( + bundle.content.messageSignature, + digest.split(':')[1] + ); + } + + return signedEntity; +} + +function isDigest(fileOrDigest: string): boolean { + return ( + fileOrDigest.startsWith(DIGEST_PREFIX) && + fileOrDigest.length === 64 + DIGEST_PREFIX.length && + !existsSync(fileOrDigest) + ); +} + +// Signature content implementation which can verify an artifact's signature +// given the digest of the artifact. The default implementation requires the +// artifact itself, which is not available when verifying a digest. +// The crypto library in Node.js does not provide a way to verify a signature +// given only the digest of the signed data. To work around this, we're using +// the elliptic library to verify the signature on the digest directly. +class MessageDigestSignatureContent { + constructor( + private messageSignature: MessageSignature, + private digest: string + ) { + this.messageSignature = messageSignature; + this.digest = digest; + } + + get signature(): Buffer { + return this.messageSignature.signature; + } + + public compareSignature(signature: Buffer): boolean { + return crypto.timingSafeEqual(signature, this.signature); + } + + public compareDigest(digest: Buffer): boolean { + return crypto.timingSafeEqual( + digest, + this.messageSignature.messageDigest.digest + ); + } + + verifySignature(key: crypto.KeyObject): boolean { + // Export public key to JWK format + const jwk = key.export({ format: 'jwk' }); + const ec = initEC(key.asymmetricKeyDetails?.namedCurve); + + // Create an elliptic-compatible key from the JWK + const eckey = ec.keyFromPublic( + { + x: Buffer.from(jwk.x!, 'base64').toString('hex'), + y: Buffer.from(jwk.y!, 'base64').toString('hex'), + }, + 'hex' + ); + + return eckey.verify(this.digest, this.signature); + } +} - if (!trustedRootPath) { - const options: Parameters[2] = { - certificateIdentityURI: flags['certificate-identity'], - certificateIssuer: flags['certificate-oidc-issuer'], - }; - - if (flags['staging']) { - // Write the initial root.json to a temporary directory - const tmpPath = await fs.mkdtemp(path.join(os.tmpdir(), 'sigstore-')); - const rootPath = path.join(tmpPath, 'root.json'); - await fs.writeFile(rootPath, Buffer.from(TUF_STAGING_ROOT, 'base64')); - - options.tufMirrorURL = TUF_STAGING_URL; - options.tufRootPath = rootPath; - } - - sigstore.verify(bundle, artifact, options); - } else { - // Need to assemble the Verifier manually to pass in the trusted root - const trustedRoot = await fs - .readFile(trustedRootPath) - .then((data) => JSON.parse(data.toString())); - const trustMaterial = toTrustMaterial(TrustedRoot.fromJSON(trustedRoot)); - const signedEntity = toSignedEntity(bundleFromJSON(bundle), artifact); - const policy = { - subjectAlternativeName: flags['certificate-identity'], - extensions: { - issuer: flags['certificate-oidc-issuer'], - }, - }; - - const verifier = new Verifier(trustMaterial); - verifier.verify(signedEntity, policy); - } +function initEC(curve: string | undefined): EC { + console.log(curve); + switch (curve) { + case 'prime256v1': + return new EC('p256'); + case 'secp384r1': + return new EC('p384'); + default: + throw new Error(`unsupported curve: ${curve}`); } } From 02e391979f38985fc8b1cca4eb9ae8417d026ef8 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 3 Dec 2024 19:30:44 -0800 Subject: [PATCH 2/2] bump conformance suite to v0.0.13 Signed-off-by: Brian DeHamer --- .github/workflows/conformance.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 5d0ae1880..c8a8e42a7 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -26,7 +26,7 @@ jobs: run: npm ci - name: Build sigstore-js run: npm run build - - uses: sigstore/sigstore-conformance@ee4de0e602873beed74cf9e49d5332529fe69bf6 # v0.0.11 + - uses: sigstore/sigstore-conformance@6bd1c54e236c9517da56f7344ad16cc00439fe19 # v0.0.13 with: entrypoint: ${{ github.workspace }}/packages/conformance/bin/run xfail: "test_verify_with_trust_root" @@ -46,7 +46,7 @@ jobs: run: npm ci - name: Build sigstore-js run: npm run build - - uses: sigstore/sigstore-conformance@ee4de0e602873beed74cf9e49d5332529fe69bf6 # v0.0.11 + - uses: sigstore/sigstore-conformance@6bd1c54e236c9517da56f7344ad16cc00439fe19 # v0.0.13 with: entrypoint: ${{ github.workspace }}/packages/conformance/bin/run environment: staging