Skip to content

Commit

Permalink
support verify by digest in conformance suite
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <bdehamer@github.com>
  • Loading branch information
bdehamer committed Dec 2, 2024
1 parent eabe1a6 commit 227a2a2
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 45 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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@e472219febb4fe9c6ce62033be8a811963ef4f27 # v0.0.12
with:
entrypoint: ${{ github.workspace }}/packages/conformance/bin/run
xfail: "test_verify_with_trust_root"
Expand All @@ -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@e472219febb4fe9c6ce62033be8a811963ef4f27 # v0.0.12
with:
entrypoint: ${{ github.workspace }}/packages/conformance/bin/run
environment: staging
82 changes: 82 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/conformance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
186 changes: 143 additions & 43 deletions packages/conformance/src/commands/verify-bundle.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
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:';

const ec = new EC('p256');

export default class VerifyBundle extends Command {
static override flags = {
bundle: Flags.string({
Expand All @@ -34,55 +47,142 @@ 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<void> {
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<TrustMaterial> {
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<TrustMaterial> {
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<SignedEntity> {
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<SignedEntity> {
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' });

// 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'
);

if (!trustedRootPath) {
const options: Parameters<typeof sigstore.verify>[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);
}
return eckey.verify(this.digest, this.signature);
}
}

0 comments on commit 227a2a2

Please sign in to comment.