Skip to content

Commit 2fe2606

Browse files
committed
support verify by digest in conformance suite
Signed-off-by: Brian DeHamer <[email protected]>
1 parent eabe1a6 commit 2fe2606

File tree

3 files changed

+227
-43
lines changed

3 files changed

+227
-43
lines changed

package-lock.json

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/conformance/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
"@sigstore/bundle": "^3.0.0",
2222
"@sigstore/protobuf-specs": "^0.3.2",
2323
"@sigstore/verify": "^2.0.0",
24+
"elliptic": "^6.6.1",
2425
"sigstore": "^3.0.0"
2526
},
2627
"devDependencies": {
28+
"@types/elliptic": "^6.4.18",
2729
"oclif": "^4",
2830
"tslib": "^2.8.1"
2931
},
Lines changed: 143 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import { Args, Command, Flags } from '@oclif/core';
2-
import { bundleFromJSON } from '@sigstore/bundle';
2+
import { Bundle, bundleFromJSON, MessageSignature } from '@sigstore/bundle';
33
import { TrustedRoot } from '@sigstore/protobuf-specs';
4-
import { Verifier, toSignedEntity, toTrustMaterial } from '@sigstore/verify';
4+
import * as tuf from '@sigstore/tuf';
5+
import {
6+
SignedEntity,
7+
toSignedEntity,
8+
toTrustMaterial,
9+
TrustMaterial,
10+
Verifier,
11+
} from '@sigstore/verify';
12+
import { ec as EC } from 'elliptic';
13+
import { existsSync } from 'fs';
514
import fs from 'fs/promises';
15+
import crypto from 'node:crypto';
616
import os from 'os';
717
import path from 'path';
8-
import * as sigstore from 'sigstore';
918
import { TUF_STAGING_ROOT, TUF_STAGING_URL } from '../staging';
1019

20+
const DIGEST_PREFIX = 'sha256:';
21+
22+
const ec = new EC('p256');
23+
1124
export default class VerifyBundle extends Command {
1225
static override flags = {
1326
bundle: Flags.string({
@@ -34,55 +47,142 @@ export default class VerifyBundle extends Command {
3447
};
3548

3649
static override args = {
37-
file: Args.file({
38-
description: 'artifact to verify',
50+
fileOrDigest: Args.string({
51+
description: 'path to the artifact to verify, or its digest',
3952
required: true,
40-
exists: true,
4153
}),
4254
};
4355

4456
public async run(): Promise<void> {
4557
const { args, flags } = await this.parse(VerifyBundle);
4658

59+
const trustedRootPath = flags['trusted-root'];
4760
const bundle = await fs
4861
.readFile(flags.bundle)
49-
.then((data) => JSON.parse(data.toString()));
50-
const artifact = await fs.readFile(args.file);
51-
const trustedRootPath = flags['trusted-root'];
62+
.then((data) => JSON.parse(data.toString()))
63+
.then((json) => bundleFromJSON(json));
64+
65+
const trustMaterial = trustedRootPath
66+
? await trustMaterialFromPath(trustedRootPath)
67+
: await trustMaterialFromTUF(flags['staging']);
68+
const verifier = new Verifier(trustMaterial);
69+
70+
const policy = {
71+
subjectAlternativeName: flags['certificate-identity'],
72+
extensions: { issuer: flags['certificate-oidc-issuer'] },
73+
};
74+
75+
const signedEntity = isDigest(args.fileOrDigest)
76+
? await signedEntityFromDigest(bundle, args.fileOrDigest)
77+
: await signedEntityFromFile(bundle, args.fileOrDigest);
78+
79+
verifier.verify(signedEntity, policy);
80+
}
81+
}
82+
83+
// Initialize TrustMaterial from TUF
84+
async function trustMaterialFromTUF(staging: boolean): Promise<TrustMaterial> {
85+
const opts: tuf.TUFOptions = {};
86+
87+
if (staging) {
88+
// Write the initial root.json to a temporary directory
89+
const tmpPath = await fs.mkdtemp(path.join(os.tmpdir(), 'sigstore-'));
90+
const rootPath = path.join(tmpPath, 'root.json');
91+
await fs.writeFile(rootPath, Buffer.from(TUF_STAGING_ROOT, 'base64'));
92+
93+
opts.mirrorURL = TUF_STAGING_URL;
94+
opts.rootPath = rootPath;
95+
}
96+
97+
const trustedRoot = await tuf.getTrustedRoot(opts);
98+
return toTrustMaterial(trustedRoot);
99+
}
100+
101+
// Initialize TrustMaterial from a file
102+
async function trustMaterialFromPath(path: string): Promise<TrustMaterial> {
103+
const trustedRoot = await fs
104+
.readFile(path)
105+
.then((data) => JSON.parse(data.toString()));
106+
return toTrustMaterial(TrustedRoot.fromJSON(trustedRoot));
107+
}
108+
109+
// Initialize SignedEntity with the artifact to verify
110+
async function signedEntityFromFile(
111+
bundle: Bundle,
112+
fileOrDigest: string
113+
): Promise<SignedEntity> {
114+
const artifact = await fs.readFile(fileOrDigest);
115+
return toSignedEntity(bundle, artifact);
116+
}
117+
118+
// Initialize SignedEntity with the digest of the artifact to verify
119+
async function signedEntityFromDigest(
120+
bundle: Bundle,
121+
digest: string
122+
): Promise<SignedEntity> {
123+
const signedEntity = toSignedEntity(bundle);
124+
125+
if (bundle.content.$case === 'messageSignature') {
126+
signedEntity.signature = new MessageDigestSignatureContent(
127+
bundle.content.messageSignature,
128+
digest.split(':')[1]
129+
);
130+
}
131+
132+
return signedEntity;
133+
}
134+
135+
function isDigest(fileOrDigest: string): boolean {
136+
return (
137+
fileOrDigest.startsWith(DIGEST_PREFIX) &&
138+
fileOrDigest.length === 64 + DIGEST_PREFIX.length &&
139+
!existsSync(fileOrDigest)
140+
);
141+
}
142+
143+
// Signature content implementation which can verify an artifact's signature
144+
// given the digest of the artifact. The default implementation requires the
145+
// artifact itself, which is not available when verifying a digest.
146+
// The crypto library in Node.js does not provide a way to verify a signature
147+
// given only the digest of the signed data. To work around this, we're using
148+
// the elliptic library to verify the signature on the digest directly.
149+
class MessageDigestSignatureContent {
150+
constructor(
151+
private messageSignature: MessageSignature,
152+
private digest: string
153+
) {
154+
this.messageSignature = messageSignature;
155+
this.digest = digest;
156+
}
157+
158+
get signature(): Buffer {
159+
return this.messageSignature.signature;
160+
}
161+
162+
public compareSignature(signature: Buffer): boolean {
163+
return crypto.timingSafeEqual(signature, this.signature);
164+
}
165+
166+
public compareDigest(digest: Buffer): boolean {
167+
return crypto.timingSafeEqual(
168+
digest,
169+
this.messageSignature.messageDigest.digest
170+
);
171+
}
172+
173+
verifySignature(key: crypto.KeyObject): boolean {
174+
// Export public key to JWK format
175+
const jwk = key.export({ format: 'jwk' });
176+
177+
// Create an elliptic-compatible key from the JWK
178+
const eckey = ec.keyFromPublic(
179+
{
180+
x: Buffer.from(jwk.x!, 'base64').toString('hex'),
181+
y: Buffer.from(jwk.y!, 'base64').toString('hex'),
182+
},
183+
'hex'
184+
);
52185

53-
if (!trustedRootPath) {
54-
const options: Parameters<typeof sigstore.verify>[2] = {
55-
certificateIdentityURI: flags['certificate-identity'],
56-
certificateIssuer: flags['certificate-oidc-issuer'],
57-
};
58-
59-
if (flags['staging']) {
60-
// Write the initial root.json to a temporary directory
61-
const tmpPath = await fs.mkdtemp(path.join(os.tmpdir(), 'sigstore-'));
62-
const rootPath = path.join(tmpPath, 'root.json');
63-
await fs.writeFile(rootPath, Buffer.from(TUF_STAGING_ROOT, 'base64'));
64-
65-
options.tufMirrorURL = TUF_STAGING_URL;
66-
options.tufRootPath = rootPath;
67-
}
68-
69-
sigstore.verify(bundle, artifact, options);
70-
} else {
71-
// Need to assemble the Verifier manually to pass in the trusted root
72-
const trustedRoot = await fs
73-
.readFile(trustedRootPath)
74-
.then((data) => JSON.parse(data.toString()));
75-
const trustMaterial = toTrustMaterial(TrustedRoot.fromJSON(trustedRoot));
76-
const signedEntity = toSignedEntity(bundleFromJSON(bundle), artifact);
77-
const policy = {
78-
subjectAlternativeName: flags['certificate-identity'],
79-
extensions: {
80-
issuer: flags['certificate-oidc-issuer'],
81-
},
82-
};
83-
84-
const verifier = new Verifier(trustMaterial);
85-
verifier.verify(signedEntity, policy);
86-
}
186+
return eckey.verify(this.digest, this.signature);
87187
}
88188
}

0 commit comments

Comments
 (0)