diff --git a/.changeset/late-hounds-kiss.md b/.changeset/late-hounds-kiss.md new file mode 100644 index 00000000..8219f801 --- /dev/null +++ b/.changeset/late-hounds-kiss.md @@ -0,0 +1,5 @@ +--- +'@sigstore/conformance': minor +--- + +Implement `--trusted-root` flag on `verify` command. diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index c8a8e42a..4d8bf414 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -29,7 +29,6 @@ jobs: - uses: sigstore/sigstore-conformance@6bd1c54e236c9517da56f7344ad16cc00439fe19 # v0.0.13 with: entrypoint: ${{ github.workspace }}/packages/conformance/bin/run - xfail: "test_verify_with_trust_root" conformance-staging: name: Conformance Test (Staging) diff --git a/package-lock.json b/package-lock.json index e05d2147..22226489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13258,7 +13258,9 @@ "dependencies": { "@oclif/core": "^4", "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/tuf": "^3.0.0", "@sigstore/verify": "^2.0.0", "elliptic": "^6.6.1", "sigstore": "^3.0.0" diff --git a/packages/conformance/package.json b/packages/conformance/package.json index c74c3297..e566526e 100644 --- a/packages/conformance/package.json +++ b/packages/conformance/package.json @@ -19,7 +19,9 @@ "dependencies": { "@oclif/core": "^4", "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/tuf": "^3.0.0", "@sigstore/verify": "^2.0.0", "elliptic": "^6.6.1", "sigstore": "^3.0.0" diff --git a/packages/conformance/src/commands/verify-bundle.ts b/packages/conformance/src/commands/verify-bundle.ts index 5f7d0c12..d44fa45c 100644 --- a/packages/conformance/src/commands/verify-bundle.ts +++ b/packages/conformance/src/commands/verify-bundle.ts @@ -1,21 +1,11 @@ import { Args, Command, Flags } from '@oclif/core'; import { Bundle, bundleFromJSON, MessageSignature } from '@sigstore/bundle'; -import { TrustedRoot } from '@sigstore/protobuf-specs'; -import * as tuf from '@sigstore/tuf'; -import { - SignedEntity, - toSignedEntity, - toTrustMaterial, - TrustMaterial, - Verifier, -} from '@sigstore/verify'; +import { SignedEntity, toSignedEntity, 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 { TUF_STAGING_ROOT, TUF_STAGING_URL } from '../staging'; +import { trustMaterialFromPath, trustMaterialFromTUF } from '../trust'; const DIGEST_PREFIX = 'sha256:'; @@ -78,32 +68,6 @@ export default class VerifyBundle extends Command { } } -// 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, diff --git a/packages/conformance/src/commands/verify.ts b/packages/conformance/src/commands/verify.ts index f8cce508..bea29e22 100644 --- a/packages/conformance/src/commands/verify.ts +++ b/packages/conformance/src/commands/verify.ts @@ -1,10 +1,9 @@ import { Args, Command, Flags } from '@oclif/core'; -import crypto, { BinaryLike } from 'crypto'; +import { Bundle, bundleFromJSON } from '@sigstore/bundle'; +import { crypto, pem } from '@sigstore/core'; +import { toSignedEntity, Verifier } from '@sigstore/verify'; import fs from 'fs/promises'; -import os from 'os'; -import path from 'path'; -import * as sigstore from 'sigstore'; -import { TUF_STAGING_ROOT, TUF_STAGING_URL } from '../staging'; +import { trustMaterialFromPath, trustMaterialFromTUF } from '../trust'; export default class Verify extends Command { static override flags = { @@ -25,6 +24,10 @@ export default class Verify extends Command { description: 'the expected OIDC issuer for the signing certificate', required: true, }), + 'trusted-root': Flags.string({ + description: 'path to trusted root', + required: false, + }), staging: Flags.boolean({ description: 'whether to use the staging environment', default: false, @@ -42,6 +45,18 @@ export default class Verify extends Command { public async run(): Promise { const { args, flags } = await this.parse(Verify); + const trustedRootPath = flags['trusted-root']; + const trustMaterial = trustedRootPath + ? await trustMaterialFromPath(trustedRootPath) + : await trustMaterialFromTUF(flags['staging']); + + const verifier = new Verifier(trustMaterial, { + tlogThreshold: 0, + tsaThreshold: 0, + }); + + // Read the artifact, certificate, and signature and assemble them into a + // Sigstore bundle const artifact = await fs.readFile(args.file); const certificate = await fs .readFile(flags.certificate) @@ -51,43 +66,35 @@ export default class Verify extends Command { .then((data) => data.toString()); const bundle = toBundle(artifact, certificate, signature); + const signedEntity = toSignedEntity(bundle, artifact); - const options: Parameters[2] = { - certificateIdentityURI: flags['certificate-identity'], - certificateIssuer: flags['certificate-oidc-issuer'], - tlogThreshold: 0, + const policy = { + subjectAlternativeName: flags['certificate-identity'], + extensions: { issuer: 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); + verifier.verify(signedEntity, policy); } } +// Construct a Sigstore bundle from the loose artifact, certificate, and +// signature function toBundle( artifact: Buffer, certificate: string, signature: string -): sigstore.Bundle { - const artifactDigest = hash(artifact); - const certBytes = toDER(certificate); +): Bundle { + const artifactDigest = crypto.digest('sha256', artifact); + const certBytes = pem.toDER(certificate); - return { - mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.1', + return bundleFromJSON({ + mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json', verificationMaterial: { - x509CertificateChain: { - certificates: [{ rawBytes: certBytes.toString('base64') }], + certificate: { + rawBytes: certBytes.toString('base64'), }, - certificate: undefined, publicKey: undefined, + x509CertificateChain: undefined, tlogEntries: [], timestampVerificationData: undefined, }, @@ -99,26 +106,5 @@ function toBundle( }, signature, }, - }; -} - -function toDER(certificate: string): Buffer { - let der = ''; - - certificate.split('\n').forEach((line) => { - if ( - line.match(/-----BEGIN (.*)-----/) || - line.match(/-----END (.*)-----/) - ) { - return; - } - - der += line; }); - - return Buffer.from(der, 'base64'); -} - -export function hash(data: BinaryLike): Buffer { - return crypto.createHash('sha256').update(data).digest(); } diff --git a/packages/conformance/src/trust.ts b/packages/conformance/src/trust.ts new file mode 100644 index 00000000..5e6a2b2c --- /dev/null +++ b/packages/conformance/src/trust.ts @@ -0,0 +1,37 @@ +import { TrustedRoot } from '@sigstore/protobuf-specs'; +import * as tuf from '@sigstore/tuf'; +import { toTrustMaterial, TrustMaterial } from '@sigstore/verify'; +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import { TUF_STAGING_ROOT, TUF_STAGING_URL } from './staging'; + +// Initialize TrustMaterial from TUF +export 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 +export async function trustMaterialFromPath( + path: string +): Promise { + const trustedRoot = await fs + .readFile(path) + .then((data) => JSON.parse(data.toString())); + return toTrustMaterial(TrustedRoot.fromJSON(trustedRoot)); +} diff --git a/packages/conformance/tsconfig.json b/packages/conformance/tsconfig.json index 1172e3b9..9b2b9358 100644 --- a/packages/conformance/tsconfig.json +++ b/packages/conformance/tsconfig.json @@ -7,6 +7,10 @@ }, "exclude": ["./dist"], "references": [ - { "path": "../client" } + { "path": "../bundle" }, + { "path": "../client" }, + { "path": "../core" }, + { "path": "../tuf" }, + { "path": "../verify" } ] }