Skip to content

Commit ef6de5f

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

File tree

4 files changed

+243
-43
lines changed

4 files changed

+243
-43
lines changed

.changeset/tame-wasps-decide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sigstore/conformance': minor
3+
---
4+
5+
Support verification by artifact OR artifact digest

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: 154 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
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+
1122
export default class VerifyBundle extends Command {
1223
static override flags = {
1324
bundle: Flags.string({
@@ -34,55 +45,155 @@ export default class VerifyBundle extends Command {
3445
};
3546

3647
static override args = {
37-
file: Args.file({
38-
description: 'artifact to verify',
48+
fileOrDigest: Args.string({
49+
description: 'path to the artifact to verify, or its digest',
3950
required: true,
40-
exists: true,
4151
}),
4252
};
4353

4454
public async run(): Promise<void> {
4555
const { args, flags } = await this.parse(VerifyBundle);
4656

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

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-
}
189+
function initEC(curve: string | undefined): EC {
190+
console.log(curve);
191+
switch (curve) {
192+
case 'prime256v1':
193+
return new EC('p256');
194+
case 'secp384r1':
195+
return new EC('p384');
196+
default:
197+
throw new Error(`unsupported curve: ${curve}`);
87198
}
88199
}

0 commit comments

Comments
 (0)