Skip to content

Commit f1b8bad

Browse files
bdehamerfeelepxyz
andauthoredJul 12, 2023
verification of v0.2 Sigstore bundles (#605)
* verification of v0.2 Sigstore bundles Signed-off-by: Brian DeHamer <bdehamer@github.com> * Update .changeset/good-penguins-brake.md Co-authored-by: Philip Harrison <philip@mailharrison.com> Signed-off-by: Brian DeHamer <bdehamer@github.com> * add test for v0.3 bundle verification Signed-off-by: Brian DeHamer <bdehamer@github.com> --------- Signed-off-by: Brian DeHamer <bdehamer@github.com> Co-authored-by: Philip Harrison <philip@mailharrison.com>
1 parent 6ec4f0c commit f1b8bad

26 files changed

+546
-232
lines changed
 

‎.changeset/fast-carrots-tan.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sigstore/bundle': minor
3+
---
4+
5+
Export bundle media type constants, refine `SerializedTLogEntry` type

‎.changeset/good-penguins-brake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sigstore': minor
3+
---
4+
5+
Support for verifying v0.2 Sigstore bundles that contain inclusion proofs from Rekor

‎packages/bundle/src/__tests__/index.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ limitations under the License.
1515
*/
1616
import { fromPartial } from '@total-typescript/shoehorn';
1717
import {
18+
BUNDLE_V01_MEDIA_TYPE,
19+
BUNDLE_V02_MEDIA_TYPE,
1820
Bundle,
1921
BundleLatest,
2022
BundleV01,
@@ -144,6 +146,11 @@ describe('public interface', () => {
144146
expect(bundleToJSON).toBeDefined();
145147
});
146148

149+
it('exports constants', () => {
150+
expect(BUNDLE_V01_MEDIA_TYPE).toBeDefined();
151+
expect(BUNDLE_V02_MEDIA_TYPE).toBeDefined();
152+
});
153+
147154
it('exports errors', () => {
148155
expect(ValidationError).toBeInstanceOf(Object);
149156
});

‎packages/bundle/src/__tests__/serialized.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ describe('bundleToJSON', () => {
139139
expectedTlogEntry.integratedTime
140140
);
141141
expect(tlogEntry?.inclusionPromise).toBeTruthy();
142-
expect(tlogEntry?.inclusionPromise.signedEntryTimestamp).toEqual(
142+
expect(tlogEntry?.inclusionPromise?.signedEntryTimestamp).toEqual(
143143
(
144144
expectedTlogEntry.inclusionPromise?.signedEntryTimestamp as Buffer
145145
).toString('base64')
@@ -260,7 +260,7 @@ describe('bundleToJSON', () => {
260260
expectedTlogEntry.integratedTime
261261
);
262262
expect(tlogEntry?.inclusionPromise).toBeTruthy();
263-
expect(tlogEntry?.inclusionPromise.signedEntryTimestamp).toEqual(
263+
expect(tlogEntry?.inclusionPromise?.signedEntryTimestamp).toEqual(
264264
(
265265
expectedTlogEntry.inclusionPromise?.signedEntryTimestamp as Buffer
266266
).toString('base64')

‎packages/bundle/src/bundle.ts

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import type {
2222
} from '@sigstore/protobuf-specs';
2323
import type { WithRequired } from './utility';
2424

25+
export const BUNDLE_V01_MEDIA_TYPE =
26+
'application/vnd.dev.sigstore.bundle+json;version=0.1';
27+
28+
export const BUNDLE_V02_MEDIA_TYPE =
29+
'application/vnd.dev.sigstore.bundle+json;version=0.2';
30+
2531
// Extract types that are not explicitly defined in the protobuf specs.
2632
type DsseEnvelopeContent = Extract<
2733
ProtoBundle['content'],

‎packages/bundle/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616
export {
17+
BUNDLE_V01_MEDIA_TYPE,
18+
BUNDLE_V02_MEDIA_TYPE,
1719
isBundleWithCertificateChain,
1820
isBundleWithDsseEnvelope,
1921
isBundleWithMessageSignature,

‎packages/bundle/src/serialized.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ type SerializedTLogEntry = {
4343
}
4444
| undefined;
4545
integratedTime: string;
46-
inclusionPromise: {
47-
signedEntryTimestamp: string;
48-
};
46+
inclusionPromise:
47+
| {
48+
signedEntryTimestamp: string;
49+
}
50+
| undefined;
4951
inclusionProof:
5052
| {
5153
logIndex: string;

‎packages/bundle/src/validate.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16+
import { BUNDLE_V01_MEDIA_TYPE } from './bundle';
1617
import { ValidationError } from './error';
1718

1819
import type { Bundle as ProtoBundle } from '@sigstore/protobuf-specs';
@@ -127,10 +128,7 @@ export function assertBundle(b: ProtoBundle): asserts b is Bundle {
127128
export function assertBundleV01(b: Bundle): asserts b is BundleV01 {
128129
const invalidValues: string[] = [];
129130

130-
if (
131-
b.mediaType &&
132-
b.mediaType !== 'application/vnd.dev.sigstore.bundle+json;version=0.1'
133-
) {
131+
if (b.mediaType && b.mediaType !== BUNDLE_V01_MEDIA_TYPE) {
134132
invalidValues.push('mediaType');
135133
}
136134

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2023 The Sigstore Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
import crypto from 'crypto';
17+
import signature from './signature';
18+
19+
export default { signature };
20+
21+
const tlogKey = crypto.createPublicKey(`-----BEGIN PUBLIC KEY-----
22+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
23+
kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
24+
-----END PUBLIC KEY-----`);
25+
26+
const tlogKeyID = crypto
27+
.createHash('sha256')
28+
.update(tlogKey.export({ format: 'der', type: 'spki' }))
29+
.digest()
30+
.toString('hex');
31+
32+
export const tlogKeys = { [tlogKeyID]: tlogKey };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
Copyright 2023 The Sigstore Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
///////////////////////////////////////////////////////////////////////////////
17+
// VALID BUNDLES
18+
///////////////////////////////////////////////////////////////////////////////
19+
20+
// Valid messageSignature bundle signed with a Fulcio signing certificate
21+
const validBundleWithSigningCert = {
22+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.2',
23+
verificationMaterial: {
24+
x509CertificateChain: {
25+
certificates: [
26+
{
27+
rawBytes:
28+
'MIICoDCCAiagAwIBAgIUevae+nLQ8mg6OyOB43MKJ10F2CEwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjIxMTA5MDEzMzA5WhcNMjIxMTA5MDE0MzA5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9DbYBIMQLtWb6J5gtL69jgRwwEfdtQtKvvG4+o3ZzlOroJplpXaVgF6wBDob++rNG9/AzSaBmApkEwI52XBjWqOCAUUwggFBMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUVIIFc08z6uV9Y96S+v5oDbbmHEYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEWgUGQwAABAMARzBFAiEAlKycMBC2q+QM+mct60RNENxpURHes6vgOBWdx71XcXgCIAtnMzw/cBw5h0hrYJ8b1PJjoxn3k1N2TdgofqvMhbSTMAoGCCqGSM49BAMDA2gAMGUCMQC2KLFYSiD/+S1WEsyf9czf52w+E577Hi77r8pGUM1rQ/Bzg1aGvQs0/kAg3S/JSDgCMEdN5dIS0tRm1SOMbOFcW+1yzR+OiCVJ7DVFwUdI3D/7ERxtN9e/LJ6uaRnR/Sanrw==',
29+
},
30+
],
31+
},
32+
publicKey: undefined,
33+
tlogEntries: [
34+
{
35+
logIndex: '6757503',
36+
logId: { keyId: 'wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=' },
37+
kindVersion: { kind: 'hashedrekord', version: '0.0.1' },
38+
integratedTime: '1667957590',
39+
inclusionPromise: undefined,
40+
inclusionProof: {
41+
logIndex: '2594072',
42+
rootHash: 'kAnoYYy8iB3NjC5tE2l6pGBqY3uw3CBJ6x2cBBQXu0U=',
43+
treeSize: '22954907',
44+
hashes: [
45+
'qEpgYkIiW7jVzbHp54MraVJQ1AE72Zvr5XSohvcdBN4=',
46+
'wtdXKmzwBO1Lr1bY5gOXpVUiP0OxYRRa9ZodfVYRKw8=',
47+
'ikD2dl7XVH3EKAPc6k21SYog5TYdwp/8DayXZ8Eedtw=',
48+
'3oHeiTXTqKZMOpundZhKh4c6dznt7SdFj88Gog5DCYY=',
49+
'4By9NfYQqHZOn5CusfRqIGw9/NeQr5E1nG4ICulNnUo=',
50+
'p3BgRy0uSg6SRAqcKt8qXUIDhhJhox1tCAIaHdT5tac=',
51+
'lJvUc0jjih3wNA1S7cbtw1q5HYX3JxYY5fO9ytPIKLU=',
52+
'5vWL6hRP9EBDNAuXS3E236YUwutNv6qvIWTfcdzywFA=',
53+
'1ODC3wToc5Hqky2sJQ2w3mBFggDWdZROOAv4MXWWLw0=',
54+
'QqwionvWKT5a3Kqsx1UWIYDsBIMK7H+pvKZNon1g4A4=',
55+
'9Ckxujk8Sg094zTRpBWmwd4ZWNT7W72H/S2JPKbZiBY=',
56+
'/gKT0/YRP2WbANUct+sWMGGQ2a9lQlNFBb/XYAhb/j8=',
57+
'f+eeYNJFCZRAI6IKsab+xTmMUl9g6Km2h6KUztMHpxM=',
58+
'P8eLjDLaNzX9cTdqiFIKYjyVJv4cNwxPBh1Ppg8eDvM=',
59+
'7NR456rTv4HEGWxCwUOTYm7ze69yMkqG4f8MbhE43oU=',
60+
'Ul2YswjUyBqbJ3eka2zE0MI0QxT4ez8sCJ1Z3+vvMw8=',
61+
'ucRPSmGLhm/SyHL7chQ5vBEFull08HzsqtAC0TQ91tY=',
62+
'EiS8ntcvGnB1xcGZg9Cf3fTkV1wBcJNVtSWKIYVZqAU=',
63+
'Mx1LEx7szsPd62CGkL6HM+NWkOy9YwZTwukJEVgH7Cw=',
64+
's2Z13KVYurVY6F1AUhr8Uby4RE3RXW1XEC2tWWdzCjI=',
65+
'QRfYxLEHh/FwMZqWnxNNW+x3lY7o3LM86BW+z0MpMN4=',
66+
'J0dGjQ7V5bETi7p7eWg2ephCQ32QBLMWY5HxFcuGfR4=',
67+
'uFGzOQorMYmYZ2yumLpgr1tvXvZaL+tTTCqaXa7Hdds=',
68+
'Lksw/hm/y+1p33SaEF8/60gPvFVNkueBpDWJ1tAVcAo=',
69+
'o4Smg8NUiGzxKxvvvgjtH2NV82EZSBLcUDUo9IpzS0Y=',
70+
],
71+
checkpoint: {
72+
envelope:
73+
'rekor.sigstore.dev - 2605736670972794746\n22954907\nkAnoYYy8iB3NjC5tE2l6pGBqY3uw3CBJ6x2cBBQXu0U=\nTimestamp: 1689107716054191855\n\n— rekor.sigstore.dev wNI9ajBFAiEA8OpuifHq4iqd6ZJSRiVQbe00eTdZllaQ51fgfAVxAPkCIDC64vV4bCtkn3S8CyMaTHHWgD2E/a+nm0eFBADK/LFP\n',
74+
},
75+
},
76+
canonicalizedBody:
77+
'eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI2OGU2NTZiMjUxZTY3ZTgzNThiZWY4NDgzYWIwZDUxYzY2MTlmM2U3YTFhOWYwZTc1ODM4ZDQxZmYzNjhmNzI4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSHM1YVV1bHExSHBSK2Z3bVNLcExrL29Bd3E1TzlDRE5GSGhaQUtmRzVHbUFpQndjVm5mMm9ienNDR1ZsZjBBSXZidkhyMjFOWHQ3dHBMQmw0K0JyaDZPS0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnZSRU5EUVdsaFowRjNTVUpCWjBsVlpYWmhaU3R1VEZFNGJXYzJUM2xQUWpRelRVdEtNVEJHTWtORmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFVUVRWTlJFVjZUWHBCTlZkb1kwNU5ha2w0VFZSQk5VMUVSVEJOZWtFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVU1UkdKWlFrbE5VVXgwVjJJMlNqVm5kRXcyT1dwblVuZDNSV1prZEZGMFMzWjJSelFLSzI4elducHNUM0p2U25Cc2NGaGhWbWRHTm5kQ1JHOWlLeXR5VGtjNUwwRjZVMkZDYlVGd2EwVjNTVFV5V0VKcVYzRlBRMEZWVlhkblowWkNUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZXU1VsR0NtTXdPSG8yZFZZNVdUazJVeXQyTlc5RVltSnRTRVZaZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoM1dVUldVakJTUVZGSUwwSkNWWGRGTkVWU1dXNUtjRmxYTlVGYVIxWnZXVmN4YkdOcE5XcGlNakIzVEVGWlMwdDNXVUpDUVVkRWRucEJRZ3BCVVZGbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKNGRsb3liSFZNTWpsb1pGaFNiMDFKUjB0Q1oyOXlRbWRGUlVGa1dqVkJaMUZEQ2tKSWQwVmxaMEkwUVVoWlFUTlVNSGRoYzJKSVJWUktha2RTTkdOdFYyTXpRWEZLUzFoeWFtVlFTek12YURSd2VXZERPSEEzYnpSQlFVRkhSVmRuVlVjS1VYZEJRVUpCVFVGU2VrSkdRV2xGUVd4TGVXTk5Ra015Y1N0UlRTdHRZM1EyTUZKT1JVNTRjRlZTU0dWek5uWm5UMEpYWkhnM01WaGpXR2REU1VGMGJncE5lbmN2WTBKM05XZ3dhSEpaU2poaU1WQkthbTk0YmpOck1VNHlWR1JuYjJaeGRrMW9ZbE5VVFVGdlIwTkRjVWRUVFRRNVFrRk5SRUV5WjBGTlIxVkRDazFSUXpKTFRFWlpVMmxFTHl0VE1WZEZjM2xtT1dONlpqVXlkeXRGTlRjM1NHazNOM0k0Y0VkVlRURnlVUzlDZW1jeFlVZDJVWE13TDJ0Qlp6TlRMMG9LVTBSblEwMUZaRTQxWkVsVE1IUlNiVEZUVDAxaVQwWmpWeXN4ZVhwU0swOXBRMVpLTjBSV1JuZFZaRWt6UkM4M1JWSjRkRTQ1WlM5TVNqWjFZVkp1VWdvdlUyRnVjbmM5UFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19',
78+
},
79+
],
80+
timestampVerificationData: { rfc3161Timestamps: [] },
81+
},
82+
messageSignature: {
83+
messageDigest: {
84+
algorithm: 'SHA2_256',
85+
digest: 'aOZWslHmfoNYvvhIOrDVHGYZ8+ehqfDnWDjUH/No9yg=',
86+
},
87+
signature:
88+
'MEQCIHs5aUulq1HpR+fwmSKpLk/oAwq5O9CDNFHhZAKfG5GmAiBwcVnf2obzsCGVlf0AIvbvHr21NXt7tpLBl4+Brh6OKA==',
89+
},
90+
dsseEnvelope: undefined,
91+
};
92+
93+
// Valid messageSignature bundle signed with a public key
94+
const validBundleWithPublicKey = {
95+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.2',
96+
verificationMaterial: {
97+
publicKey: {
98+
hint: '9a76331edc1cfd3933040996615b1c06adbe6f9b4f11df4106dcceb66e3bdb1b',
99+
},
100+
tlogEntries: [
101+
{
102+
logIndex: '6757503',
103+
logId: { keyId: 'wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=' },
104+
kindVersion: { kind: 'hashedrekord', version: '0.0.1' },
105+
integratedTime: '1667957590',
106+
inclusionPromise: undefined,
107+
inclusionProof: {
108+
logIndex: '2594072',
109+
rootHash: 'kAnoYYy8iB3NjC5tE2l6pGBqY3uw3CBJ6x2cBBQXu0U=',
110+
treeSize: '22954907',
111+
hashes: [
112+
'qEpgYkIiW7jVzbHp54MraVJQ1AE72Zvr5XSohvcdBN4=',
113+
'wtdXKmzwBO1Lr1bY5gOXpVUiP0OxYRRa9ZodfVYRKw8=',
114+
'ikD2dl7XVH3EKAPc6k21SYog5TYdwp/8DayXZ8Eedtw=',
115+
'3oHeiTXTqKZMOpundZhKh4c6dznt7SdFj88Gog5DCYY=',
116+
'4By9NfYQqHZOn5CusfRqIGw9/NeQr5E1nG4ICulNnUo=',
117+
'p3BgRy0uSg6SRAqcKt8qXUIDhhJhox1tCAIaHdT5tac=',
118+
'lJvUc0jjih3wNA1S7cbtw1q5HYX3JxYY5fO9ytPIKLU=',
119+
'5vWL6hRP9EBDNAuXS3E236YUwutNv6qvIWTfcdzywFA=',
120+
'1ODC3wToc5Hqky2sJQ2w3mBFggDWdZROOAv4MXWWLw0=',
121+
'QqwionvWKT5a3Kqsx1UWIYDsBIMK7H+pvKZNon1g4A4=',
122+
'9Ckxujk8Sg094zTRpBWmwd4ZWNT7W72H/S2JPKbZiBY=',
123+
'/gKT0/YRP2WbANUct+sWMGGQ2a9lQlNFBb/XYAhb/j8=',
124+
'f+eeYNJFCZRAI6IKsab+xTmMUl9g6Km2h6KUztMHpxM=',
125+
'P8eLjDLaNzX9cTdqiFIKYjyVJv4cNwxPBh1Ppg8eDvM=',
126+
'7NR456rTv4HEGWxCwUOTYm7ze69yMkqG4f8MbhE43oU=',
127+
'Ul2YswjUyBqbJ3eka2zE0MI0QxT4ez8sCJ1Z3+vvMw8=',
128+
'ucRPSmGLhm/SyHL7chQ5vBEFull08HzsqtAC0TQ91tY=',
129+
'EiS8ntcvGnB1xcGZg9Cf3fTkV1wBcJNVtSWKIYVZqAU=',
130+
'Mx1LEx7szsPd62CGkL6HM+NWkOy9YwZTwukJEVgH7Cw=',
131+
's2Z13KVYurVY6F1AUhr8Uby4RE3RXW1XEC2tWWdzCjI=',
132+
'QRfYxLEHh/FwMZqWnxNNW+x3lY7o3LM86BW+z0MpMN4=',
133+
'J0dGjQ7V5bETi7p7eWg2ephCQ32QBLMWY5HxFcuGfR4=',
134+
'uFGzOQorMYmYZ2yumLpgr1tvXvZaL+tTTCqaXa7Hdds=',
135+
'Lksw/hm/y+1p33SaEF8/60gPvFVNkueBpDWJ1tAVcAo=',
136+
'o4Smg8NUiGzxKxvvvgjtH2NV82EZSBLcUDUo9IpzS0Y=',
137+
],
138+
checkpoint: {
139+
envelope:
140+
'rekor.sigstore.dev - 2605736670972794746\n22954907\nkAnoYYy8iB3NjC5tE2l6pGBqY3uw3CBJ6x2cBBQXu0U=\nTimestamp: 1689107716054191855\n\n— rekor.sigstore.dev wNI9ajBFAiEA8OpuifHq4iqd6ZJSRiVQbe00eTdZllaQ51fgfAVxAPkCIDC64vV4bCtkn3S8CyMaTHHWgD2E/a+nm0eFBADK/LFP\n',
141+
},
142+
},
143+
canonicalizedBody:
144+
'eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI2OGU2NTZiMjUxZTY3ZTgzNThiZWY4NDgzYWIwZDUxYzY2MTlmM2U3YTFhOWYwZTc1ODM4ZDQxZmYzNjhmNzI4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSHM1YVV1bHExSHBSK2Z3bVNLcExrL29Bd3E1TzlDRE5GSGhaQUtmRzVHbUFpQndjVm5mMm9ienNDR1ZsZjBBSXZidkhyMjFOWHQ3dHBMQmw0K0JyaDZPS0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnZSRU5EUVdsaFowRjNTVUpCWjBsVlpYWmhaU3R1VEZFNGJXYzJUM2xQUWpRelRVdEtNVEJHTWtORmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFVUVRWTlJFVjZUWHBCTlZkb1kwNU5ha2w0VFZSQk5VMUVSVEJOZWtFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVU1UkdKWlFrbE5VVXgwVjJJMlNqVm5kRXcyT1dwblVuZDNSV1prZEZGMFMzWjJSelFLSzI4elducHNUM0p2U25Cc2NGaGhWbWRHTm5kQ1JHOWlLeXR5VGtjNUwwRjZVMkZDYlVGd2EwVjNTVFV5V0VKcVYzRlBRMEZWVlhkblowWkNUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZXU1VsR0NtTXdPSG8yZFZZNVdUazJVeXQyTlc5RVltSnRTRVZaZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoM1dVUldVakJTUVZGSUwwSkNWWGRGTkVWU1dXNUtjRmxYTlVGYVIxWnZXVmN4YkdOcE5XcGlNakIzVEVGWlMwdDNXVUpDUVVkRWRucEJRZ3BCVVZGbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKNGRsb3liSFZNTWpsb1pGaFNiMDFKUjB0Q1oyOXlRbWRGUlVGa1dqVkJaMUZEQ2tKSWQwVmxaMEkwUVVoWlFUTlVNSGRoYzJKSVJWUktha2RTTkdOdFYyTXpRWEZLUzFoeWFtVlFTek12YURSd2VXZERPSEEzYnpSQlFVRkhSVmRuVlVjS1VYZEJRVUpCVFVGU2VrSkdRV2xGUVd4TGVXTk5Ra015Y1N0UlRTdHRZM1EyTUZKT1JVNTRjRlZTU0dWek5uWm5UMEpYWkhnM01WaGpXR2REU1VGMGJncE5lbmN2WTBKM05XZ3dhSEpaU2poaU1WQkthbTk0YmpOck1VNHlWR1JuYjJaeGRrMW9ZbE5VVFVGdlIwTkRjVWRUVFRRNVFrRk5SRUV5WjBGTlIxVkRDazFSUXpKTFRFWlpVMmxFTHl0VE1WZEZjM2xtT1dONlpqVXlkeXRGTlRjM1NHazNOM0k0Y0VkVlRURnlVUzlDZW1jeFlVZDJVWE13TDJ0Qlp6TlRMMG9LVTBSblEwMUZaRTQxWkVsVE1IUlNiVEZUVDAxaVQwWmpWeXN4ZVhwU0swOXBRMVpLTjBSV1JuZFZaRWt6UkM4M1JWSjRkRTQ1WlM5TVNqWjFZVkp1VWdvdlUyRnVjbmM5UFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19',
145+
},
146+
],
147+
timestampVerificationData: { rfc3161Timestamps: [] },
148+
},
149+
messageSignature: {
150+
messageDigest: {
151+
algorithm: 'SHA2_256',
152+
digest: 'aOZWslHmfoNYvvhIOrDVHGYZ8+ehqfDnWDjUH/No9yg=',
153+
},
154+
signature:
155+
'MEQCIHs5aUulq1HpR+fwmSKpLk/oAwq5O9CDNFHhZAKfG5GmAiBwcVnf2obzsCGVlf0AIvbvHr21NXt7tpLBl4+Brh6OKA==',
156+
},
157+
};
158+
159+
export default {
160+
artifact: Buffer.from('hello, world!'),
161+
valid: {
162+
withSigningCert: validBundleWithSigningCert,
163+
withPublicKey: validBundleWithPublicKey,
164+
},
165+
invalid: {},
166+
};

‎packages/client/src/__tests__/ca/verify/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616
import { bundleFromJSON, BundleWithCertificateChain } from '@sigstore/bundle';
1717
import { verifySigningCertificate } from '../../../ca/verify';
1818
import * as sigstore from '../../../types/sigstore';
19-
import bundles from '../../__fixtures__/bundles/';
19+
import bundles from '../../__fixtures__/bundles/v01';
2020
import { trustedRoot } from '../../__fixtures__/trust';
2121

2222
describe('verifySigningCertificate', () => {

‎packages/client/src/__tests__/sigstore.test.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import mocktuf, { Target } from '@tufjs/repo-mock';
2929
import { PolicyError, VerificationError } from '../error';
3030
import { Signer } from '../sign';
3131
import { attest, createVerifier, sign, tuf, verify } from '../sigstore';
32-
import bundles from './__fixtures__/bundles';
32+
import bundles from './__fixtures__/bundles/v01';
33+
import bundlesV02 from './__fixtures__/bundles/v02';
3334
import { trustedRoot } from './__fixtures__/trust';
3435

3536
import type { TUFOptions, VerifyOptions } from '../config';
@@ -318,6 +319,31 @@ describe('#verify', () => {
318319
);
319320
});
320321
});
322+
323+
describe('when the bundle is a v0.2 bundle', () => {
324+
const bundle = bundlesV02.signature.valid.withSigningCert;
325+
const artifact = bundlesV02.signature.artifact;
326+
327+
it('does not throw an error', async () => {
328+
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
329+
undefined
330+
);
331+
});
332+
});
333+
334+
describe('when the bundle is newer then v0.2', () => {
335+
// Check a theoretical v0.3 bundle that is the same shape as a v0.2 bundle
336+
const bundle = { ...bundlesV02.signature.valid.withSigningCert };
337+
bundle.mediaType = 'application/vnd.dev.sigstore.bundle+json;version=0.3';
338+
339+
const artifact = bundlesV02.signature.artifact;
340+
341+
it('does not throw an error', async () => {
342+
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
343+
undefined
344+
);
345+
});
346+
});
321347
});
322348

323349
describe('#createVerifier', () => {

‎packages/client/src/__tests__/tlog/verify/body.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616
import { bundleFromJSON, TransparencyLogEntry } from '@sigstore/bundle';
1717
import { verifyTLogBody } from '../../../tlog/verify/body';
18-
import bundles from '../../__fixtures__/bundles';
18+
import bundles from '../../__fixtures__/bundles/v01';
1919

2020
describe('verifyTLogBody', () => {
2121
describe('when a message signature bundle is provided', () => {
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TransparencyLogEntry } from '@sigstore/bundle';
1+
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
22
import { fromPartial } from '@total-typescript/shoehorn';
33
import { VerificationError } from '../../../error';
44
import { verifyCheckpoint } from '../../../tlog/verify/checkpoint';
@@ -43,55 +43,34 @@ describe('verifyCheckpoint', () => {
4343
const checkpoint =
4444
'rekor.sigstore.dev - 2605736670972794746\n21428036\nrxnoKyFZlJ7/R6bMh/d3lcqwKqAy5CL1LcNBJP17kgQ=\nTimestamp: 1688058656037355364\n\n— rekor.sigstore.dev wNI9ajBFAiEAuDk7uu5Ae8Own/MjhSZNuVzbLuYH2jBMxbSA0WaNDNACIDV4reKpYiOpkwtvazCClnpUuduF2o/th2xR3gRZAUU4\n';
4545

46-
const inclusionProof: TransparencyLogEntry['inclusionProof'] = fromPartial({
47-
checkpoint: { envelope: checkpoint },
48-
rootHash: Buffer.from(
49-
'rxnoKyFZlJ7/R6bMh/d3lcqwKqAy5CL1LcNBJP17kgQ=',
50-
'base64'
51-
),
52-
});
46+
const inclusionProof: TLogEntryWithInclusionProof['inclusionProof'] =
47+
fromPartial({
48+
checkpoint: { envelope: checkpoint },
49+
rootHash: Buffer.from(
50+
'rxnoKyFZlJ7/R6bMh/d3lcqwKqAy5CL1LcNBJP17kgQ=',
51+
'base64'
52+
),
53+
});
5354

54-
const entry: TransparencyLogEntry = fromPartial({
55+
const entry: TLogEntryWithInclusionProof = fromPartial({
5556
inclusionProof: inclusionProof,
5657
integratedTime: '1688058655',
5758
});
5859

5960
describe('when the entry has a valid checkpoint', () => {
60-
it('does NOT throw an error', () => {
61-
expect(() => verifyCheckpoint(entry, tlogs)).not.toThrow();
62-
});
63-
});
64-
65-
describe('when the entry has no inclusion proof', () => {
66-
const entryWithoutProof: TransparencyLogEntry = fromPartial({
67-
logId: { keyId: keyID },
68-
});
69-
70-
it('throws a VerificationError', () => {
71-
expect(() => verifyCheckpoint(entryWithoutProof, tlogs)).toThrow(
72-
VerificationError
73-
);
74-
});
75-
});
76-
77-
describe('when the entry has no checkpoint', () => {
78-
const entryWithoutCheckpoint: TransparencyLogEntry = fromPartial({
79-
inclusionProof: {},
80-
});
81-
82-
it('throws a VerificationError', () => {
83-
expect(() => verifyCheckpoint(entryWithoutCheckpoint, tlogs)).toThrow(
84-
VerificationError
85-
);
61+
it('returns true', () => {
62+
expect(verifyCheckpoint(entry, tlogs)).toBe(true);
8663
});
8764
});
8865

8966
describe('when the checkpoint has no separator', () => {
90-
const entryWithInvalidCheckpoint: TransparencyLogEntry = fromPartial({
91-
inclusionProof: {
92-
checkpoint: { envelope: 'rekor.sigstore.dev - 2605736670972794746' },
93-
},
94-
});
67+
const entryWithInvalidCheckpoint: TLogEntryWithInclusionProof = fromPartial(
68+
{
69+
inclusionProof: {
70+
checkpoint: { envelope: 'rekor.sigstore.dev - 2605736670972794746' },
71+
},
72+
}
73+
);
9574

9675
it('throws a VerificationError', () => {
9776
expect(() => verifyCheckpoint(entryWithInvalidCheckpoint, tlogs)).toThrow(
@@ -101,14 +80,16 @@ describe('verifyCheckpoint', () => {
10180
});
10281

10382
describe('when the checkpoint signature is malformed', () => {
104-
const entryWithInvalidCheckpoint: TransparencyLogEntry = fromPartial({
105-
inclusionProof: {
106-
checkpoint: {
107-
envelope:
108-
'rekor.sigstore.dev - 2605736670972794746\n\n— rekor.sigstore.dev foo\n',
83+
const entryWithInvalidCheckpoint: TLogEntryWithInclusionProof = fromPartial(
84+
{
85+
inclusionProof: {
86+
checkpoint: {
87+
envelope:
88+
'rekor.sigstore.dev - 2605736670972794746\n\n— rekor.sigstore.dev foo\n',
89+
},
10990
},
110-
},
111-
});
91+
}
92+
);
11293

11394
it('throws a VerificationError', () => {
11495
expect(() => verifyCheckpoint(entryWithInvalidCheckpoint, tlogs)).toThrow(
@@ -118,7 +99,7 @@ describe('verifyCheckpoint', () => {
11899
});
119100

120101
describe('when the checkpoint has no signature', () => {
121-
const entryWitInvalidCheckpoint: TransparencyLogEntry = fromPartial({
102+
const entryWitInvalidCheckpoint: TLogEntryWithInclusionProof = fromPartial({
122103
inclusionProof: {
123104
checkpoint: {
124105
envelope: 'rekor.sigstore.dev - 2605736670972794746\n\n',
@@ -134,14 +115,16 @@ describe('verifyCheckpoint', () => {
134115
});
135116

136117
describe('when the checkpoint header is too short', () => {
137-
const entryWithInvalidCheckpoint: TransparencyLogEntry = fromPartial({
138-
inclusionProof: {
139-
checkpoint: {
140-
envelope:
141-
'rekor.sigstore.dev\n\n— rekor.sigstore.dev wNI9ajBFAiEAu\n',
118+
const entryWithInvalidCheckpoint: TLogEntryWithInclusionProof = fromPartial(
119+
{
120+
inclusionProof: {
121+
checkpoint: {
122+
envelope:
123+
'rekor.sigstore.dev\n\n— rekor.sigstore.dev wNI9ajBFAiEAu\n',
124+
},
142125
},
143-
},
144-
});
126+
}
127+
);
145128

146129
it('throws a VerificationError', () => {
147130
expect(() => verifyCheckpoint(entryWithInvalidCheckpoint, tlogs)).toThrow(
@@ -151,13 +134,15 @@ describe('verifyCheckpoint', () => {
151134
});
152135

153136
describe('when the checkpoint origin is empty', () => {
154-
const entryWithInvalidCheckpoint: TransparencyLogEntry = fromPartial({
155-
inclusionProof: {
156-
checkpoint: {
157-
envelope: '\nA\nB\nC\n\n— rekor.sigstore.dev wNI9ajBFAiEAu\n',
137+
const entryWithInvalidCheckpoint: TLogEntryWithInclusionProof = fromPartial(
138+
{
139+
inclusionProof: {
140+
checkpoint: {
141+
envelope: '\nA\nB\nC\n\n— rekor.sigstore.dev wNI9ajBFAiEAu\n',
142+
},
158143
},
159-
},
160-
});
144+
}
145+
);
161146

162147
it('throws a VerificationError', () => {
163148
expect(() => verifyCheckpoint(entryWithInvalidCheckpoint, tlogs)).toThrow(
@@ -167,46 +152,43 @@ describe('verifyCheckpoint', () => {
167152
});
168153

169154
describe('when the entry checkpoint has the wrong root hash', () => {
170-
const entry: TransparencyLogEntry = fromPartial({
155+
const entry: TLogEntryWithInclusionProof = fromPartial({
171156
inclusionProof: { ...inclusionProof, rootHash: Buffer.from('foo') },
172157
});
173-
it('does NOT throw an error', () => {
174-
expect(() => verifyCheckpoint(entry, tlogs)).toThrow(VerificationError);
158+
159+
it('returns false', () => {
160+
expect(verifyCheckpoint(entry, tlogs)).toBe(false);
175161
});
176162
});
177163

178164
describe('when the entry checkpoint has a bad signature', () => {
179165
const badSignatureCheckpoint =
180166
'rekor.sigstore.dev - 2605736670972794746\n21428036\nrxnoKyFZlJ7/R6bMh/d3lcqwKqAy5CL1LcNBJP17kgQ=\nTimestamp: 1688058656037355364\n\n— rekor.sigstore.dev wNI9ajBFAiEAuDk7uu5Ae8Own\n';
181167

182-
const entryWithBadCheckpointSig: TransparencyLogEntry = fromPartial({
168+
const entryWithBadCheckpointSig: TLogEntryWithInclusionProof = fromPartial({
183169
inclusionProof: {
184170
...inclusionProof,
185171
checkpoint: { envelope: badSignatureCheckpoint },
186172
},
187173
});
188174

189-
it('throws a VerificationError', () => {
190-
expect(() => verifyCheckpoint(entryWithBadCheckpointSig, tlogs)).toThrow(
191-
VerificationError
192-
);
175+
it('returns false', () => {
176+
expect(verifyCheckpoint(entryWithBadCheckpointSig, tlogs)).toBe(false);
193177
});
194178
});
195179

196180
describe('when there is no transparency log with the given key ID', () => {
197181
const checkpointWithBadKeyHint =
198182
'rekor.sigstore.dev - 2605736670972794746\n21428036\nrxnoKyFZlJ7/R6bMh/d3lcqwKqAy5CL1LcNBJP17kgQ=\nTimestamp: 1688058656037355364\n\n— rekor.sigstore.dev xNI9ajBFAiEAuDk7uu5Ae8Own/MjhSZNuVzbLuYH2jBMxbSA0WaNDNACIDV4reKpYiOpkwtvazCClnpUuduF2o/th2xR3gRZAUU4\n';
199-
const entryWithBadLogID: TransparencyLogEntry = fromPartial({
183+
const entryWithBadLogID: TLogEntryWithInclusionProof = fromPartial({
200184
inclusionProof: {
201185
...inclusionProof,
202186
checkpoint: { envelope: checkpointWithBadKeyHint },
203187
},
204188
});
205189

206-
it('throws a VerificationError', () => {
207-
expect(() => verifyCheckpoint(entryWithBadLogID, tlogs)).toThrow(
208-
VerificationError
209-
);
190+
it('returns false', () => {
191+
expect(verifyCheckpoint(entryWithBadLogID, tlogs)).toBe(false);
210192
});
211193
});
212194

@@ -221,10 +203,8 @@ describe('verifyCheckpoint', () => {
221203
},
222204
];
223205

224-
it('throws a VerificationError', () => {
225-
expect(() => verifyCheckpoint(entry, invalidTLogs)).toThrow(
226-
VerificationError
227-
);
206+
it('returns false', () => {
207+
expect(verifyCheckpoint(entry, invalidTLogs)).toBe(false);
228208
});
229209
});
230210

@@ -238,10 +218,9 @@ describe('verifyCheckpoint', () => {
238218
},
239219
},
240220
];
241-
it('throws a VerificationError', () => {
242-
expect(() => verifyCheckpoint(entry, invalidTLogs)).toThrow(
243-
VerificationError
244-
);
221+
222+
it('returns false', () => {
223+
expect(verifyCheckpoint(entry, invalidTLogs)).toBe(false);
245224
});
246225
});
247226

@@ -258,10 +237,9 @@ describe('verifyCheckpoint', () => {
258237
},
259238
},
260239
];
261-
it('throws a VerificationError', () => {
262-
expect(() => verifyCheckpoint(entry, invalidTLogs)).toThrow(
263-
VerificationError
264-
);
240+
241+
it('returns false', () => {
242+
expect(verifyCheckpoint(entry, invalidTLogs)).toBe(false);
265243
});
266244
});
267245
});

‎packages/client/src/__tests__/tlog/verify/index.test.ts

+96-38
Original file line numberDiff line numberDiff line change
@@ -17,63 +17,121 @@ import { bundleFromJSON } from '@sigstore/bundle';
1717
import { VerificationError } from '../../../error';
1818
import { verifyTLogEntries } from '../../../tlog/verify/index';
1919
import * as sigstore from '../../../types/sigstore';
20-
import bundles from '../../__fixtures__/bundles';
20+
import bundles from '../../__fixtures__/bundles/v01';
21+
import bundlesV02 from '../../__fixtures__/bundles/v02';
2122
import { trustedRoot } from '../../__fixtures__/trust';
2223

2324
describe('verifyTLogEntries', () => {
24-
const bundle = bundleFromJSON(bundles.signature.valid.withSigningCert);
25-
2625
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
2726
disable: false,
2827
performOnlineVerification: false,
2928
threshold: 1,
3029
};
3130

32-
describe('when everything is valid', () => {
33-
describe('when the bundle has a signing certificate', () => {
34-
it('does NOT throw an error', () => {
35-
expect(() =>
36-
verifyTLogEntries(bundle, trustedRoot, options)
37-
).not.toThrow();
31+
describe('when the bundle is a v02 bundle', () => {
32+
describe('when everything is valid', () => {
33+
describe('when the bundle has a signing certificate', () => {
34+
const bundle = bundleFromJSON(
35+
bundlesV02.signature.valid.withSigningCert
36+
);
37+
38+
it('does NOT throw an error', () => {
39+
expect(() =>
40+
verifyTLogEntries(bundle, trustedRoot, options)
41+
).not.toThrow();
42+
});
43+
});
44+
45+
describe('when the bundle does NOT have a signing certificate', () => {
46+
const bundle = bundleFromJSON(bundlesV02.signature.valid.withPublicKey);
47+
48+
it('does NOT throw an error', () => {
49+
expect(() =>
50+
verifyTLogEntries(bundle, trustedRoot, options)
51+
).not.toThrow();
52+
});
3853
});
3954
});
4055

41-
describe('when the bundle does NOT have a signing certificate', () => {
42-
const bundle = bundleFromJSON(bundles.signature.valid.withPublicKey);
56+
describe('when the verification threshold is not met', () => {
57+
const bundle = bundleFromJSON(bundlesV02.signature.valid.withSigningCert);
58+
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
59+
disable: false,
60+
performOnlineVerification: false,
61+
threshold: Number.MAX_SAFE_INTEGER,
62+
};
4363

44-
it('does NOT throw an error', () => {
45-
expect(() =>
46-
verifyTLogEntries(bundle, trustedRoot, options)
47-
).not.toThrow();
64+
it('throws an error', () => {
65+
expect(() => verifyTLogEntries(bundle, trustedRoot, options)).toThrow(
66+
VerificationError
67+
);
4868
});
4969
});
50-
});
5170

52-
describe('when the verification threshold is not met', () => {
53-
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
54-
disable: false,
55-
performOnlineVerification: false,
56-
threshold: Number.MAX_SAFE_INTEGER,
57-
};
58-
59-
it('throws an error', () => {
60-
expect(() => verifyTLogEntries(bundle, trustedRoot, options)).toThrow(
61-
VerificationError
62-
);
71+
describe('when online verification is requested', () => {
72+
const bundle = bundleFromJSON(bundlesV02.signature.valid.withSigningCert);
73+
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
74+
disable: false,
75+
performOnlineVerification: true,
76+
threshold: 1,
77+
};
78+
79+
it('throws an error', () => {
80+
expect(() => verifyTLogEntries(bundle, trustedRoot, options)).toThrow(
81+
VerificationError
82+
);
83+
});
6384
});
6485
});
6586

66-
describe('when online verification is requested', () => {
67-
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
68-
disable: false,
69-
performOnlineVerification: true,
70-
threshold: 1,
71-
};
72-
73-
it('throws an error', () => {
74-
expect(() => verifyTLogEntries(bundle, trustedRoot, options)).toThrow(
75-
VerificationError
76-
);
87+
describe('when the bundle is a v01 bundle', () => {
88+
const bundle = bundleFromJSON(bundles.signature.valid.withSigningCert);
89+
describe('when everything is valid', () => {
90+
describe('when the bundle has a signing certificate', () => {
91+
it('does NOT throw an error', () => {
92+
expect(() =>
93+
verifyTLogEntries(bundle, trustedRoot, options)
94+
).not.toThrow();
95+
});
96+
});
97+
98+
describe('when the bundle does NOT have a signing certificate', () => {
99+
const bundle = bundleFromJSON(bundles.signature.valid.withPublicKey);
100+
101+
it('does NOT throw an error', () => {
102+
expect(() =>
103+
verifyTLogEntries(bundle, trustedRoot, options)
104+
).not.toThrow();
105+
});
106+
});
107+
});
108+
109+
describe('when the verification threshold is not met', () => {
110+
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
111+
disable: false,
112+
performOnlineVerification: false,
113+
threshold: Number.MAX_SAFE_INTEGER,
114+
};
115+
116+
it('throws an error', () => {
117+
expect(() => verifyTLogEntries(bundle, trustedRoot, options)).toThrow(
118+
VerificationError
119+
);
120+
});
121+
});
122+
123+
describe('when online verification is requested', () => {
124+
const options: sigstore.ArtifactVerificationOptions_TlogOptions = {
125+
disable: false,
126+
performOnlineVerification: true,
127+
threshold: 1,
128+
};
129+
130+
it('throws an error', () => {
131+
expect(() => verifyTLogEntries(bundle, trustedRoot, options)).toThrow(
132+
VerificationError
133+
);
134+
});
77135
});
78136
});
79137
});

‎packages/client/src/__tests__/tlog/verify/merkle.test.ts

+8-21
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16-
import type { TransparencyLogEntry } from '@sigstore/bundle';
16+
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
1717
import { VerificationError } from '../../../error';
1818
import { verifyMerkleInclusion } from '../../../tlog/verify/merkle';
1919

@@ -122,7 +122,7 @@ describe('verifyMerkleInclusion', () => {
122122
const entry = {
123123
canonicalizedBody,
124124
inclusionProof,
125-
} as TransparencyLogEntry;
125+
} as TLogEntryWithInclusionProof;
126126

127127
it('returns true', () => {
128128
expect(verifyMerkleInclusion(entry)).toBe(true);
@@ -133,7 +133,7 @@ describe('verifyMerkleInclusion', () => {
133133
const invalidEntry = {
134134
canonicalizedBody: Buffer.from('invalid'),
135135
inclusionProof,
136-
} as TransparencyLogEntry;
136+
} as TLogEntryWithInclusionProof;
137137

138138
it('returns false', () => {
139139
expect(verifyMerkleInclusion(invalidEntry)).toBe(false);
@@ -144,20 +144,7 @@ describe('verifyMerkleInclusion', () => {
144144
const invalidEntry = {
145145
canonicalizedBody,
146146
inclusionProof: { ...inclusionProof, logIndex: '-1' },
147-
} as TransparencyLogEntry;
148-
149-
it('throws an error', () => {
150-
expect(() => verifyMerkleInclusion(invalidEntry)).toThrow(
151-
VerificationError
152-
);
153-
});
154-
});
155-
156-
describe('when the entry does NOT contain an inclusion proof', () => {
157-
const invalidEntry = {
158-
canonicalizedBody,
159-
inclusionProof: undefined,
160-
} as TransparencyLogEntry;
147+
} as TLogEntryWithInclusionProof;
161148

162149
it('throws an error', () => {
163150
expect(() => verifyMerkleInclusion(invalidEntry)).toThrow(
@@ -170,9 +157,9 @@ describe('verifyMerkleInclusion', () => {
170157
const invalidEntry = {
171158
canonicalizedBody,
172159
inclusionProof: { ...inclusionProof, treeSize: '99', logIndex: '100' },
173-
} as TransparencyLogEntry;
160+
} as TLogEntryWithInclusionProof;
174161

175-
it('throws an error true', () => {
162+
it('throws an error', () => {
176163
expect(() => verifyMerkleInclusion(invalidEntry)).toThrow(
177164
VerificationError
178165
);
@@ -186,9 +173,9 @@ describe('verifyMerkleInclusion', () => {
186173
...inclusionProof,
187174
hashes: inclusionProof.hashes.slice(0, 1),
188175
},
189-
} as TransparencyLogEntry;
176+
} as TLogEntryWithInclusionProof;
190177

191-
it('throws an error true', () => {
178+
it('throws an error', () => {
192179
expect(() => verifyMerkleInclusion(invalidEntry)).toThrow(
193180
VerificationError
194181
);

‎packages/client/src/__tests__/tlog/verify/set.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { verifyTLogSET } from '../../../tlog/verify/set';
2121
import * as sigstore from '../../../types/sigstore';
2222
import { crypto } from '../../../util';
23-
import bundles from '../../__fixtures__/bundles';
23+
import bundles from '../../__fixtures__/bundles/v01';
2424

2525
describe('verifyTLogSET', () => {
2626
const keyBytes = Buffer.from(

‎packages/client/src/__tests__/types/sigstore/index.test.ts ‎packages/client/src/__tests__/types/sigstore.test.ts

+20-16
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16-
import type { Entry } from '../../../external/rekor';
17-
import { SignatureMaterial } from '../../../types/signature';
18-
import * as sigstore from '../../../types/sigstore';
19-
import { encoding as enc, pem } from '../../../util';
16+
import type { Entry } from '../../external/rekor';
17+
import { SignatureMaterial } from '../../types/signature';
18+
import * as sigstore from '../../types/sigstore';
19+
import { encoding as enc, pem } from '../../util';
2020

2121
describe('isCAVerificationOptions', () => {
2222
describe('when the verification options are for a CA', () => {
@@ -92,13 +92,12 @@ describe('bundle', () => {
9292
const signature = Buffer.from('signature');
9393
const certificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----`;
9494

95-
const sigMaterial: SignatureMaterial = {
96-
signature,
97-
certificates: [certificate],
98-
key: undefined,
99-
};
100-
10195
describe('toDSSEBundle', () => {
96+
const sigMaterial: SignatureMaterial = {
97+
signature,
98+
certificates: [certificate],
99+
key: undefined,
100+
};
102101
const envelope: sigstore.Envelope = {
103102
payloadType: 'application/vnd.in-toto+json',
104103
payload: Buffer.from('payload'),
@@ -211,6 +210,12 @@ describe('bundle', () => {
211210
});
212211

213212
describe('toMessageSignatureBundle', () => {
213+
const sigMaterial: SignatureMaterial = {
214+
signature,
215+
certificates: undefined,
216+
key: { value: 'key', id: 'hint' },
217+
};
218+
214219
const digest = Buffer.from('digest');
215220

216221
it('returns a valid message signature bundle', () => {
@@ -236,13 +241,12 @@ describe('bundle', () => {
236241
}
237242

238243
// Verification material
239-
if (b.verificationMaterial?.content?.$case === 'x509CertificateChain') {
240-
const chain = b.verificationMaterial.content.x509CertificateChain;
241-
expect(chain).toBeTruthy();
242-
expect(chain.certificates).toHaveLength(1);
243-
expect(chain.certificates[0].rawBytes).toEqual(pem.toDER(certificate));
244+
if (b.verificationMaterial?.content?.$case === 'publicKey') {
245+
const publicKey = b.verificationMaterial.content.publicKey;
246+
expect(publicKey).toBeTruthy();
247+
expect(publicKey.hint).toEqual('hint');
244248
} else {
245-
fail('Expected x509CertificateChain');
249+
fail('Expected publicKey');
246250
}
247251

248252
expect(b.verificationMaterial?.timestampVerificationData).toBeUndefined();

‎packages/client/src/__tests__/verify.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { bundleFromJSON } from '@sigstore/bundle';
1717
import { VerificationError } from '../error';
1818
import * as sigstore from '../types/sigstore';
1919
import { Verifier } from '../verify';
20-
import bundles from './__fixtures__/bundles';
20+
import bundles from './__fixtures__/bundles/v01';
2121
import { trustedRoot } from './__fixtures__/trust';
2222

2323
describe('Verifier', () => {

‎packages/client/src/tlog/verify/checkpoint.ts

+11-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TransparencyLogEntry } from '@sigstore/bundle';
1+
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
22
import { VerificationError } from '../../error';
33
import * as sigstore from '../../types/sigstore';
44
import { crypto } from '../../util';
@@ -29,40 +29,24 @@ interface TLogSignature {
2929
// inclusion proof
3030
// See: https://github.com/transparency-dev/formats/blob/main/log/README.md
3131
export function verifyCheckpoint(
32-
entry: TransparencyLogEntry,
32+
entry: TLogEntryWithInclusionProof,
3333
tlogs: sigstore.TransparencyLogInstance[]
34-
) {
35-
const inclusionProof = entry.inclusionProof;
36-
if (!inclusionProof) {
37-
throw new VerificationError('tlog entry has no inclusion proof');
38-
}
39-
40-
const envelope = inclusionProof?.checkpoint?.envelope;
41-
if (!envelope) {
42-
throw new VerificationError('tlog entry has no checkpoint');
43-
}
44-
34+
): boolean {
4535
// Filter tlog instances to just those which were valid at the time of the
4636
// entry
4737
const validTLogs = filterTLogInstances(tlogs, entry.integratedTime);
4838

49-
const signedNote = SignedNote.fromString(envelope);
39+
const inclusionProof = entry.inclusionProof;
40+
const signedNote = SignedNote.fromString(inclusionProof.checkpoint.envelope);
5041
const checkpoint = LogCheckpoint.fromString(signedNote.note);
5142

52-
// Verify that the signatures in the checkpoint are all valid
53-
if (!signedNote.verify(validTLogs)) {
54-
throw new VerificationError(
55-
'inclusion proof contains invalid checkpoint signature'
56-
);
57-
}
58-
59-
// Verify that the root hash from the checkpoint matches the root hash in the
43+
// Verify that the signatures in the checkpoint are all valid, also check
44+
// that the root hash from the checkpoint matches the root hash in the
6045
// inclusion proof
61-
if (!crypto.bufferEqual(checkpoint.logHash, inclusionProof.rootHash)) {
62-
throw new VerificationError(
63-
'inclusion proof contains invalid root hash signature'
64-
);
65-
}
46+
return (
47+
signedNote.verify(validTLogs) &&
48+
crypto.bufferEqual(checkpoint.logHash, inclusionProof.rootHash)
49+
);
6650
}
6751

6852
// SignedNote represents a signed note from a transparency log checkpoint. Consists

‎packages/client/src/tlog/verify/index.ts

+76-18
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616
import {
17+
BUNDLE_V01_MEDIA_TYPE,
1718
Bundle,
19+
BundleLatest,
20+
BundleV01,
1821
TLogEntryWithInclusionPromise,
19-
TransparencyLogEntry,
22+
TLogEntryWithInclusionProof,
23+
assertBundleLatest,
24+
assertBundleV01,
2025
isBundleWithCertificateChain,
2126
} from '@sigstore/bundle';
2227
import { VerificationError } from '../../error';
2328
import * as sigstore from '../../types/sigstore';
2429
import { x509Certificate } from '../../x509/cert';
2530
import { verifyTLogBody } from './body';
31+
import { verifyCheckpoint } from './checkpoint';
32+
import { verifyMerkleInclusion } from './merkle';
2633
import { verifyTLogSET } from './set';
2734

2835
// Verifies that the number of tlog entries that pass offline verification
@@ -31,6 +38,20 @@ export function verifyTLogEntries(
3138
bundle: Bundle,
3239
trustedRoot: sigstore.TrustedRoot,
3340
options: sigstore.ArtifactVerificationOptions_TlogOptions
41+
): void {
42+
if (bundle.mediaType === BUNDLE_V01_MEDIA_TYPE) {
43+
assertBundleV01(bundle);
44+
verifyTLogEntriesForBundleV01(bundle, trustedRoot, options);
45+
} else {
46+
assertBundleLatest(bundle);
47+
verifyTLogEntriesForBundleLatest(bundle, trustedRoot, options);
48+
}
49+
}
50+
51+
function verifyTLogEntriesForBundleV01(
52+
bundle: BundleV01,
53+
trustedRoot: sigstore.TrustedRoot,
54+
options: sigstore.ArtifactVerificationOptions_TlogOptions
3455
): void {
3556
if (options.performOnlineVerification) {
3657
throw new VerificationError('Online verification not implemented');
@@ -41,8 +62,8 @@ export function verifyTLogEntries(
4162

4263
// Iterate over the tlog entries and verify each one
4364
const verifiedEntries = bundle.verificationMaterial.tlogEntries.filter(
44-
(entry: TransparencyLogEntry) =>
45-
verifyTLogEntryOffline(
65+
(entry: TLogEntryWithInclusionPromise) =>
66+
verifyTLogEntryWithInclusionPromise(
4667
entry,
4768
bundle.content,
4869
trustedRoot.tlogs,
@@ -55,8 +76,36 @@ export function verifyTLogEntries(
5576
}
5677
}
5778

58-
function verifyTLogEntryOffline(
59-
entry: TransparencyLogEntry,
79+
function verifyTLogEntriesForBundleLatest(
80+
bundle: BundleLatest,
81+
trustedRoot: sigstore.TrustedRoot,
82+
options: sigstore.ArtifactVerificationOptions_TlogOptions
83+
): void {
84+
if (options.performOnlineVerification) {
85+
throw new VerificationError('Online verification not implemented');
86+
}
87+
88+
// Extract the signing cert, if available
89+
const signingCert = signingCertificate(bundle);
90+
91+
// Iterate over the tlog entries and verify each one
92+
const verifiedEntries = bundle.verificationMaterial.tlogEntries.filter(
93+
(entry: TLogEntryWithInclusionProof) =>
94+
verifyTLogEntryWithInclusionProof(
95+
entry,
96+
bundle.content,
97+
trustedRoot.tlogs,
98+
signingCert
99+
)
100+
);
101+
102+
if (verifiedEntries.length < options.threshold) {
103+
throw new VerificationError('tlog verification failed');
104+
}
105+
}
106+
107+
function verifyTLogEntryWithInclusionPromise(
108+
entry: TLogEntryWithInclusionPromise,
60109
bundleContent: Bundle['content'],
61110
tlogs: sigstore.TransparencyLogInstance[],
62111
signingCert?: x509Certificate
@@ -69,16 +118,31 @@ function verifyTLogEntryOffline(
69118
signingCert.validForDate(new Date(Number(entry.integratedTime) * 1000))
70119
: () => true;
71120

72-
// For 0.1 Bundle, the SET is required. If it is missing, the entry is invalid.
73-
// Will revisit this when we have a 0.2 Bundle.
74-
const verifyTLogSignedEntryTimestamp =
75-
isTransparencyLogEntryWithInclusionPromise(entry)
76-
? () => verifyTLogSET(entry, tlogs)
77-
: () => false;
121+
return (
122+
verifyTLogBody(entry, bundleContent) &&
123+
verifyTLogSET(entry, tlogs) &&
124+
verifyTLogIntegrationTime()
125+
);
126+
}
127+
128+
function verifyTLogEntryWithInclusionProof(
129+
entry: TLogEntryWithInclusionProof,
130+
bundleContent: Bundle['content'],
131+
tlogs: sigstore.TransparencyLogInstance[],
132+
signingCert?: x509Certificate
133+
): boolean {
134+
// If there is a signing certificate availble, check that the tlog integrated
135+
// time is within the certificate's validity period; otherwise, skip this
136+
// check.
137+
const verifyTLogIntegrationTime = signingCert
138+
? () =>
139+
signingCert.validForDate(new Date(Number(entry.integratedTime) * 1000))
140+
: () => true;
78141

79142
return (
80143
verifyTLogBody(entry, bundleContent) &&
81-
verifyTLogSignedEntryTimestamp() &&
144+
verifyMerkleInclusion(entry) &&
145+
verifyCheckpoint(entry, tlogs) &&
82146
verifyTLogIntegrationTime()
83147
);
84148
}
@@ -92,9 +156,3 @@ function signingCertificate(bundle: Bundle): x509Certificate | undefined {
92156
bundle.verificationMaterial.content.x509CertificateChain.certificates[0];
93157
return x509Certificate.parse(signingCert.rawBytes);
94158
}
95-
96-
function isTransparencyLogEntryWithInclusionPromise(
97-
entry: TransparencyLogEntry
98-
): entry is TLogEntryWithInclusionPromise {
99-
return entry.inclusionPromise !== undefined;
100-
}

‎packages/client/src/tlog/verify/merkle.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,15 @@ limitations under the License.
1616
import crypto from 'crypto';
1717
import { VerificationError } from '../../error';
1818

19-
import type { TransparencyLogEntry } from '@sigstore/bundle';
19+
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
2020

2121
const RFC6962_LEAF_HASH_PREFIX = Buffer.from([0x00]);
2222
const RFC6962_NODE_HASH_PREFIX = Buffer.from([0x01]);
2323

24-
export function verifyMerkleInclusion(entry: TransparencyLogEntry): boolean {
24+
export function verifyMerkleInclusion(
25+
entry: TLogEntryWithInclusionProof
26+
): boolean {
2527
const inclusionProof = entry.inclusionProof;
26-
27-
if (!inclusionProof) {
28-
throw new VerificationError('tlog entry has no inclusion proof');
29-
}
30-
3128
const logIndex = BigInt(inclusionProof.logIndex);
3229
const treeSize = BigInt(inclusionProof.treeSize);
3330

‎packages/client/src/types/sigstore.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16+
import { BUNDLE_V01_MEDIA_TYPE } from '@sigstore/bundle';
1617
import { HashAlgorithm } from '@sigstore/protobuf-specs';
1718
import { encoding as enc, pem } from '../util';
1819
import { SignatureMaterial } from './signature';
@@ -56,9 +57,6 @@ export type {
5657
TrustedRoot,
5758
} from '@sigstore/protobuf-specs';
5859

59-
const BUNDLE_MEDIA_TYPE =
60-
'application/vnd.dev.sigstore.bundle+json;version=0.1';
61-
6260
export type RequiredArtifactVerificationOptions = WithRequired<
6361
ArtifactVerificationOptions,
6462
'ctlogOptions' | 'tlogOptions'
@@ -106,7 +104,7 @@ export function toDSSEBundle({
106104
timestamp?: Buffer;
107105
}): Bundle {
108106
return {
109-
mediaType: BUNDLE_MEDIA_TYPE,
107+
mediaType: BUNDLE_V01_MEDIA_TYPE,
110108
content: { $case: 'dsseEnvelope', dsseEnvelope: envelope },
111109
verificationMaterial: toVerificationMaterial({
112110
signature,
@@ -128,7 +126,7 @@ export function toMessageSignatureBundle({
128126
timestamp?: Buffer;
129127
}): Bundle {
130128
return {
131-
mediaType: BUNDLE_MEDIA_TYPE,
129+
mediaType: BUNDLE_V01_MEDIA_TYPE,
132130
content: {
133131
$case: 'messageSignature',
134132
messageSignature: {
@@ -148,6 +146,7 @@ export function toMessageSignatureBundle({
148146
}
149147

150148
function toTransparencyLogEntry(entry: Entry): TransparencyLogEntry {
149+
/* istanbul ignore next */
151150
const b64SET = entry.verification?.signedEntryTimestamp || '';
152151
const set = Buffer.from(b64SET, 'base64');
153152
const logID = Buffer.from(entry.logID, 'hex');

0 commit comments

Comments
 (0)
Please sign in to comment.