Skip to content

Commit 2a5f500

Browse files
authoredJul 10, 2023
new package for sigstore bundle types/utilities (#599)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
1 parent e2d4935 commit 2a5f500

19 files changed

+2507
-0
lines changed
 

‎.changeset/smooth-seals-beam.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sigstore/bundle': minor
3+
---
4+
5+
Initial release

‎package-lock.json

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/bundle/LICENSE

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
Apache License
3+
Version 2.0, January 2004
4+
http://www.apache.org/licenses/
5+
6+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+
1. Definitions.
9+
10+
"License" shall mean the terms and conditions for use, reproduction,
11+
and distribution as defined by Sections 1 through 9 of this document.
12+
13+
"Licensor" shall mean the copyright owner or entity authorized by
14+
the copyright owner that is granting the License.
15+
16+
"Legal Entity" shall mean the union of the acting entity and all
17+
other entities that control, are controlled by, or are under common
18+
control with that entity. For the purposes of this definition,
19+
"control" means (i) the power, direct or indirect, to cause the
20+
direction or management of such entity, whether by contract or
21+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+
outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+
"You" (or "Your") shall mean an individual or Legal Entity
25+
exercising permissions granted by this License.
26+
27+
"Source" form shall mean the preferred form for making modifications,
28+
including but not limited to software source code, documentation
29+
source, and configuration files.
30+
31+
"Object" form shall mean any form resulting from mechanical
32+
transformation or translation of a Source form, including but
33+
not limited to compiled object code, generated documentation,
34+
and conversions to other media types.
35+
36+
"Work" shall mean the work of authorship, whether in Source or
37+
Object form, made available under the License, as indicated by a
38+
copyright notice that is included in or attached to the work
39+
(an example is provided in the Appendix below).
40+
41+
"Derivative Works" shall mean any work, whether in Source or Object
42+
form, that is based on (or derived from) the Work and for which the
43+
editorial revisions, annotations, elaborations, or other modifications
44+
represent, as a whole, an original work of authorship. For the purposes
45+
of this License, Derivative Works shall not include works that remain
46+
separable from, or merely link (or bind by name) to the interfaces of,
47+
the Work and Derivative Works thereof.
48+
49+
"Contribution" shall mean any work of authorship, including
50+
the original version of the Work and any modifications or additions
51+
to that Work or Derivative Works thereof, that is intentionally
52+
submitted to Licensor for inclusion in the Work by the copyright owner
53+
or by an individual or Legal Entity authorized to submit on behalf of
54+
the copyright owner. For the purposes of this definition, "submitted"
55+
means any form of electronic, verbal, or written communication sent
56+
to the Licensor or its representatives, including but not limited to
57+
communication on electronic mailing lists, source code control systems,
58+
and issue tracking systems that are managed by, or on behalf of, the
59+
Licensor for the purpose of discussing and improving the Work, but
60+
excluding communication that is conspicuously marked or otherwise
61+
designated in writing by the copyright owner as "Not a Contribution."
62+
63+
"Contributor" shall mean Licensor and any individual or Legal Entity
64+
on behalf of whom a Contribution has been received by Licensor and
65+
subsequently incorporated within the Work.
66+
67+
2. Grant of Copyright License. Subject to the terms and conditions of
68+
this License, each Contributor hereby grants to You a perpetual,
69+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+
copyright license to reproduce, prepare Derivative Works of,
71+
publicly display, publicly perform, sublicense, and distribute the
72+
Work and such Derivative Works in Source or Object form.
73+
74+
3. Grant of Patent License. Subject to the terms and conditions of
75+
this License, each Contributor hereby grants to You a perpetual,
76+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+
(except as stated in this section) patent license to make, have made,
78+
use, offer to sell, sell, import, and otherwise transfer the Work,
79+
where such license applies only to those patent claims licensable
80+
by such Contributor that are necessarily infringed by their
81+
Contribution(s) alone or by combination of their Contribution(s)
82+
with the Work to which such Contribution(s) was submitted. If You
83+
institute patent litigation against any entity (including a
84+
cross-claim or counterclaim in a lawsuit) alleging that the Work
85+
or a Contribution incorporated within the Work constitutes direct
86+
or contributory patent infringement, then any patent licenses
87+
granted to You under this License for that Work shall terminate
88+
as of the date such litigation is filed.
89+
90+
4. Redistribution. You may reproduce and distribute copies of the
91+
Work or Derivative Works thereof in any medium, with or without
92+
modifications, and in Source or Object form, provided that You
93+
meet the following conditions:
94+
95+
(a) You must give any other recipients of the Work or
96+
Derivative Works a copy of this License; and
97+
98+
(b) You must cause any modified files to carry prominent notices
99+
stating that You changed the files; and
100+
101+
(c) You must retain, in the Source form of any Derivative Works
102+
that You distribute, all copyright, patent, trademark, and
103+
attribution notices from the Source form of the Work,
104+
excluding those notices that do not pertain to any part of
105+
the Derivative Works; and
106+
107+
(d) If the Work includes a "NOTICE" text file as part of its
108+
distribution, then any Derivative Works that You distribute must
109+
include a readable copy of the attribution notices contained
110+
within such NOTICE file, excluding those notices that do not
111+
pertain to any part of the Derivative Works, in at least one
112+
of the following places: within a NOTICE text file distributed
113+
as part of the Derivative Works; within the Source form or
114+
documentation, if provided along with the Derivative Works; or,
115+
within a display generated by the Derivative Works, if and
116+
wherever such third-party notices normally appear. The contents
117+
of the NOTICE file are for informational purposes only and
118+
do not modify the License. You may add Your own attribution
119+
notices within Derivative Works that You distribute, alongside
120+
or as an addendum to the NOTICE text from the Work, provided
121+
that such additional attribution notices cannot be construed
122+
as modifying the License.
123+
124+
You may add Your own copyright statement to Your modifications and
125+
may provide additional or different license terms and conditions
126+
for use, reproduction, or distribution of Your modifications, or
127+
for any such Derivative Works as a whole, provided Your use,
128+
reproduction, and distribution of the Work otherwise complies with
129+
the conditions stated in this License.
130+
131+
5. Submission of Contributions. Unless You explicitly state otherwise,
132+
any Contribution intentionally submitted for inclusion in the Work
133+
by You to the Licensor shall be under the terms and conditions of
134+
this License, without any additional terms or conditions.
135+
Notwithstanding the above, nothing herein shall supersede or modify
136+
the terms of any separate license agreement you may have executed
137+
with Licensor regarding such Contributions.
138+
139+
6. Trademarks. This License does not grant permission to use the trade
140+
names, trademarks, service marks, or product names of the Licensor,
141+
except as required for reasonable and customary use in describing the
142+
origin of the Work and reproducing the content of the NOTICE file.
143+
144+
7. Disclaimer of Warranty. Unless required by applicable law or
145+
agreed to in writing, Licensor provides the Work (and each
146+
Contributor provides its Contributions) on an "AS IS" BASIS,
147+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+
implied, including, without limitation, any warranties or conditions
149+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+
PARTICULAR PURPOSE. You are solely responsible for determining the
151+
appropriateness of using or redistributing the Work and assume any
152+
risks associated with Your exercise of permissions under this License.
153+
154+
8. Limitation of Liability. In no event and under no legal theory,
155+
whether in tort (including negligence), contract, or otherwise,
156+
unless required by applicable law (such as deliberate and grossly
157+
negligent acts) or agreed to in writing, shall any Contributor be
158+
liable to You for damages, including any direct, indirect, special,
159+
incidental, or consequential damages of any character arising as a
160+
result of this License or out of the use or inability to use the
161+
Work (including but not limited to damages for loss of goodwill,
162+
work stoppage, computer failure or malfunction, or any and all
163+
other commercial damages or losses), even if such Contributor
164+
has been advised of the possibility of such damages.
165+
166+
9. Accepting Warranty or Additional Liability. While redistributing
167+
the Work or Derivative Works thereof, You may choose to offer,
168+
and charge a fee for, acceptance of support, warranty, indemnity,
169+
or other liability obligations and/or rights consistent with this
170+
License. However, in accepting such obligations, You may act only
171+
on Your own behalf and on Your sole responsibility, not on behalf
172+
of any other Contributor, and only if You agree to indemnify,
173+
defend, and hold each Contributor harmless for any liability
174+
incurred by, or claims asserted against, such Contributor by reason
175+
of your accepting any such warranty or additional liability.
176+
177+
END OF TERMS AND CONDITIONS
178+
179+
APPENDIX: How to apply the Apache License to your work.
180+
181+
To apply the Apache License to your work, attach the following
182+
boilerplate notice, with the fields enclosed by brackets "[]"
183+
replaced with your own identifying information. (Don't include
184+
the brackets!) The text should be enclosed in the appropriate
185+
comment syntax for the file format. We also recommend that a
186+
file or class name and description of purpose be included on the
187+
same "printed page" as the copyright notice for easier
188+
identification within third-party archives.
189+
190+
Copyright 2023 The Sigstore Authors
191+
192+
Licensed under the Apache License, Version 2.0 (the "License");
193+
you may not use this file except in compliance with the License.
194+
You may obtain a copy of the License at
195+
196+
http://www.apache.org/licenses/LICENSE-2.0
197+
198+
Unless required by applicable law or agreed to in writing, software
199+
distributed under the License is distributed on an "AS IS" BASIS,
200+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201+
See the License for the specific language governing permissions and
202+
limitations under the License.

‎packages/bundle/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# @sigstore/bundle &middot; [![npm version](https://img.shields.io/npm/v/@sigstore/bundle.svg?style=flat)](https://www.npmjs.com/package/@sigstore/tuf) [![CI Status](https://github.com/sigstore/sigstore-js/workflows/CI/badge.svg)](https://github.com/sigstore/sigstore-js/actions/workflows/ci.yml) [![Smoke Test Status](https://github.com/sigstore/sigstore-js/workflows/smoke-test/badge.svg)](https://github.com/sigstore/sigstore-js/actions/workflows/smoke-test.yml)
2+
3+
A JavaScript library for working with the Sigstore bundle format.
4+
5+
## Features
6+
7+
- TypeScript types for the different Sigstore bundle versions.
8+
- Bundle validation functions.
9+
- Support for serializing/deserializing bundles to/from JSON.
10+
11+
## Prerequisites
12+
13+
- Node.js version >= 14.17.0
14+
15+
## Installation
16+
17+
```
18+
npm install @sigstore/bundle
19+
```

‎packages/bundle/jest.config.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Copyright 2022 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+
const base = require('../../jest.config.base');
17+
18+
module.exports = {
19+
...base,
20+
displayName: 'bundle',
21+
testPathIgnorePatterns: ['<rootDir>/dist/'],
22+
};

‎packages/bundle/package.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@sigstore/bundle",
3+
"version": "0.0.0",
4+
"description": "Sigstore bundle type",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"scripts": {
8+
"clean": "shx rm -rf dist *.tsbuildinfo",
9+
"build": "tsc --build",
10+
"test": "jest"
11+
},
12+
"files": [
13+
"dist",
14+
"store"
15+
],
16+
"author": "bdehamer@github.com",
17+
"license": "Apache-2.0",
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/sigstore/sigstore-js.git"
21+
},
22+
"bugs": {
23+
"url": "https://github.com/sigstore/sigstore-js/issues"
24+
},
25+
"homepage": "https://github.com/sigstore/sigstore-js/tree/main/packages/bundle#readme",
26+
"publishConfig": {
27+
"provenance": true
28+
},
29+
"dependencies": {
30+
"@sigstore/protobuf-specs": "^0.1.0"
31+
},
32+
"engines": {
33+
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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 { HashAlgorithm, Bundle as ProtoBundle } from '@sigstore/protobuf-specs';
17+
import { fromPartial } from '@total-typescript/shoehorn';
18+
import {
19+
Bundle,
20+
BundleLatest,
21+
BundleV01,
22+
isBundleWithCertificateChain,
23+
isBundleWithDsseEnvelope,
24+
isBundleWithMessageSignature,
25+
isBundleWithPublicKey,
26+
} from '../bundle';
27+
28+
describe('BundleV01', () => {
29+
const bundle = {
30+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.1',
31+
verificationMaterial: {
32+
content: {
33+
$case: 'x509CertificateChain',
34+
x509CertificateChain: {
35+
certificates: [{ rawBytes: Buffer.from('') }],
36+
},
37+
},
38+
timestampVerificationData: {
39+
rfc3161Timestamps: [{ signedTimestamp: Buffer.from('') }],
40+
},
41+
tlogEntries: [
42+
{
43+
logIndex: '123',
44+
logId: { keyId: Buffer.from('') },
45+
kindVersion: { kind: 'intoto', version: '0.1.0' },
46+
canonicalizedBody: Buffer.from(''),
47+
integratedTime: '0',
48+
inclusionPromise: { signedEntryTimestamp: Buffer.from('') },
49+
inclusionProof: undefined,
50+
},
51+
],
52+
},
53+
content: {
54+
$case: 'messageSignature',
55+
messageSignature: {
56+
messageDigest: {
57+
algorithm: HashAlgorithm.SHA2_256,
58+
digest: Buffer.from(''),
59+
},
60+
signature: Buffer.from(''),
61+
},
62+
},
63+
} satisfies ProtoBundle;
64+
65+
it('is assignable from a properly formed bundle', () => {
66+
const v01Bundle: BundleV01 = bundle;
67+
expect(v01Bundle).toBeDefined();
68+
});
69+
});
70+
71+
describe('BundleLatest', () => {
72+
const bundle = {
73+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.2',
74+
verificationMaterial: {
75+
content: {
76+
$case: 'publicKey',
77+
publicKey: { hint: '' },
78+
},
79+
timestampVerificationData: {
80+
rfc3161Timestamps: [{ signedTimestamp: Buffer.from('') }],
81+
},
82+
tlogEntries: [
83+
{
84+
logIndex: '123',
85+
logId: { keyId: Buffer.from('') },
86+
kindVersion: { kind: 'intoto', version: '0.1.0' },
87+
canonicalizedBody: Buffer.from(''),
88+
integratedTime: '0',
89+
inclusionPromise: undefined,
90+
inclusionProof: {
91+
checkpoint: { envelope: '' },
92+
logIndex: '123',
93+
treeSize: '123',
94+
rootHash: Buffer.from(''),
95+
hashes: [Buffer.from('')],
96+
},
97+
},
98+
],
99+
},
100+
content: {
101+
$case: 'dsseEnvelope',
102+
dsseEnvelope: {
103+
payloadType: 'text/plain',
104+
payload: Buffer.from(''),
105+
signatures: [{ keyid: '', sig: Buffer.from('') }],
106+
},
107+
},
108+
} satisfies ProtoBundle;
109+
110+
it('is assignable from a properly formed bundle', () => {
111+
const vLatestBundle: BundleLatest = bundle;
112+
expect(vLatestBundle).toBeDefined();
113+
});
114+
});
115+
116+
describe('isBundleWithCertificateChain', () => {
117+
describe('when the bundle has a certificate chain', () => {
118+
const bundle: Bundle = fromPartial({
119+
verificationMaterial: {
120+
content: {
121+
$case: 'x509CertificateChain',
122+
x509CertificateChain: {
123+
certificates: [{ rawBytes: Buffer.from('') }],
124+
},
125+
},
126+
},
127+
});
128+
129+
it('returns true', () => {
130+
expect(isBundleWithCertificateChain(bundle)).toBe(true);
131+
});
132+
});
133+
134+
describe('when the bundle does not have a certificate chain', () => {
135+
const bundle: Bundle = fromPartial({
136+
verificationMaterial: {
137+
content: {
138+
$case: 'publicKey',
139+
publicKey: { hint: '' },
140+
},
141+
},
142+
});
143+
144+
it('returns false', () => {
145+
expect(isBundleWithCertificateChain(bundle)).toBe(false);
146+
});
147+
});
148+
});
149+
150+
describe('isBundleWithPublicKey', () => {
151+
describe('when the bundle has a public key', () => {
152+
const bundle: Bundle = fromPartial({
153+
verificationMaterial: {
154+
content: {
155+
$case: 'publicKey',
156+
publicKey: { hint: '' },
157+
},
158+
},
159+
});
160+
161+
it('returns true', () => {
162+
expect(isBundleWithPublicKey(bundle)).toBe(true);
163+
});
164+
});
165+
166+
describe('when the bundle does NOT have a public key', () => {
167+
const bundle: Bundle = fromPartial({
168+
verificationMaterial: {
169+
content: {
170+
$case: 'x509CertificateChain',
171+
x509CertificateChain: {
172+
certificates: [{ rawBytes: Buffer.from('') }],
173+
},
174+
},
175+
},
176+
});
177+
178+
it('returns false', () => {
179+
expect(isBundleWithPublicKey(bundle)).toBe(false);
180+
});
181+
});
182+
});
183+
184+
describe('isBundleWithMessageSignature', () => {
185+
describe('when the bundle has a message signature', () => {
186+
const bundle: Bundle = fromPartial({
187+
content: {
188+
$case: 'messageSignature',
189+
messageSignature: {
190+
messageDigest: {
191+
algorithm: HashAlgorithm.SHA2_256,
192+
digest: Buffer.from(''),
193+
},
194+
signature: Buffer.from(''),
195+
},
196+
},
197+
});
198+
199+
it('returns true', () => {
200+
expect(isBundleWithMessageSignature(bundle)).toBe(true);
201+
});
202+
});
203+
204+
describe('when the bundle does NOT have a message signature', () => {
205+
const bundle: Bundle = fromPartial({
206+
content: {
207+
$case: 'dsseEnvelope',
208+
dsseEnvelope: {
209+
payloadType: 'text/plain',
210+
payload: Buffer.from(''),
211+
signatures: [{ keyid: '', sig: Buffer.from('') }],
212+
},
213+
},
214+
});
215+
216+
it('returns false', () => {
217+
expect(isBundleWithMessageSignature(bundle)).toBe(false);
218+
});
219+
});
220+
});
221+
222+
describe('isBundleWithDSSEnvelope', () => {
223+
describe('when the bundle has a DSSEnvelope', () => {
224+
const bundle: Bundle = fromPartial({
225+
content: {
226+
$case: 'dsseEnvelope',
227+
dsseEnvelope: {
228+
payloadType: 'text/plain',
229+
payload: Buffer.from(''),
230+
signatures: [{ keyid: '', sig: Buffer.from('') }],
231+
},
232+
},
233+
});
234+
235+
it('returns true', () => {
236+
expect(isBundleWithDsseEnvelope(bundle)).toBe(true);
237+
});
238+
});
239+
240+
describe('when the bundle does NOT have a DSSEnvelope', () => {
241+
const bundle: Bundle = fromPartial({
242+
content: {
243+
$case: 'messageSignature',
244+
messageSignature: {
245+
messageDigest: {
246+
algorithm: HashAlgorithm.SHA2_256,
247+
digest: Buffer.from(''),
248+
},
249+
signature: Buffer.from(''),
250+
},
251+
},
252+
});
253+
254+
it('returns false', () => {
255+
expect(isBundleWithDsseEnvelope(bundle)).toBe(false);
256+
});
257+
});
258+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 { ValidationError } from '../error';
17+
18+
describe('ValidationError', () => {
19+
describe('constructor', () => {
20+
const error = new ValidationError('message', ['field1', 'field2']);
21+
22+
it('sets the message', () => {
23+
expect(error.message).toBe('message');
24+
});
25+
26+
it('sets the fields', () => {
27+
expect(error.fields).toEqual(['field1', 'field2']);
28+
});
29+
});
30+
});
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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 { fromPartial } from '@total-typescript/shoehorn';
17+
import {
18+
Bundle,
19+
BundleLatest,
20+
BundleV01,
21+
BundleWithCertificateChain,
22+
BundleWithDsseEnvelope,
23+
BundleWithMessageSignature,
24+
BundleWithPublicKey,
25+
Envelope,
26+
InclusionProof,
27+
MessageSignature,
28+
PublicKeyIdentifier,
29+
RFC3161SignedTimestamp,
30+
SerializedBundle,
31+
SerializedEnvelope,
32+
Signature,
33+
TLogEntryWithInclusionPromise,
34+
TLogEntryWithInclusionProof,
35+
TimestampVerificationData,
36+
TransparencyLogEntry,
37+
ValidationError,
38+
VerificationMaterial,
39+
X509Certificate,
40+
X509CertificateChain,
41+
assertBundle,
42+
assertBundleLatest,
43+
assertBundleV01,
44+
bundleFromJSON,
45+
bundleToJSON,
46+
isBundleV01,
47+
isBundleWithCertificateChain,
48+
isBundleWithDsseEnvelope,
49+
isBundleWithMessageSignature,
50+
isBundleWithPublicKey,
51+
} from '../index';
52+
53+
describe('public interface', () => {
54+
it('exports types', () => {
55+
const bundle: Bundle = fromPartial({});
56+
expect(bundle).toBeDefined();
57+
58+
const bundleV01: BundleV01 = fromPartial({});
59+
expect(bundleV01).toBeDefined();
60+
61+
const bundleLatest: BundleLatest = fromPartial({});
62+
expect(bundleLatest).toBeDefined();
63+
64+
const bundleWithCertificateChain: BundleWithCertificateChain = fromPartial(
65+
{}
66+
);
67+
expect(bundleWithCertificateChain).toBeDefined();
68+
69+
const bundleWithDsseEnvelope: BundleWithDsseEnvelope = fromPartial({});
70+
expect(bundleWithDsseEnvelope).toBeDefined();
71+
72+
const bundleWithMessageSignature: BundleWithMessageSignature = fromPartial(
73+
{}
74+
);
75+
expect(bundleWithMessageSignature).toBeDefined();
76+
77+
const bundleWithPublicKey: BundleWithPublicKey = fromPartial({});
78+
expect(bundleWithPublicKey).toBeDefined();
79+
80+
const envelope: Envelope = fromPartial({});
81+
expect(envelope).toBeDefined();
82+
83+
const inclusionProof: InclusionProof = fromPartial({});
84+
expect(inclusionProof).toBeDefined();
85+
86+
const messageSignature: MessageSignature = fromPartial({});
87+
expect(messageSignature).toBeDefined();
88+
89+
const publicKeyIdentifier: PublicKeyIdentifier = fromPartial({});
90+
expect(publicKeyIdentifier).toBeDefined();
91+
92+
const rfc3161SignedTimestamp: RFC3161SignedTimestamp = fromPartial({});
93+
expect(rfc3161SignedTimestamp).toBeDefined();
94+
95+
const serializedBundle: SerializedBundle = fromPartial({});
96+
expect(serializedBundle).toBeDefined();
97+
98+
const serializedEnvelope: SerializedEnvelope = fromPartial({});
99+
expect(serializedEnvelope).toBeDefined();
100+
101+
const signature: Signature = fromPartial({});
102+
expect(signature).toBeDefined();
103+
104+
const timestampVerificationData: TimestampVerificationData = fromPartial(
105+
{}
106+
);
107+
expect(timestampVerificationData).toBeDefined();
108+
109+
const transparencyLogEntry: TransparencyLogEntry = fromPartial({});
110+
expect(transparencyLogEntry).toBeDefined();
111+
112+
const tlogEntryWithProof: TLogEntryWithInclusionProof = fromPartial({});
113+
expect(tlogEntryWithProof).toBeDefined();
114+
115+
const tlogEntryWithPromise: TLogEntryWithInclusionPromise = fromPartial({});
116+
expect(tlogEntryWithPromise).toBeDefined();
117+
118+
const verificationMaterial: VerificationMaterial = fromPartial({});
119+
expect(verificationMaterial).toBeDefined();
120+
121+
const x509Certificate: X509Certificate = fromPartial({});
122+
expect(x509Certificate).toBeDefined();
123+
124+
const x509CertificateChain: X509CertificateChain = fromPartial({});
125+
expect(x509CertificateChain).toBeDefined();
126+
});
127+
128+
it('exports type guard functions', () => {
129+
expect(isBundleWithCertificateChain).toBeDefined();
130+
expect(isBundleWithDsseEnvelope).toBeDefined();
131+
expect(isBundleWithMessageSignature).toBeDefined();
132+
expect(isBundleWithPublicKey).toBeDefined();
133+
expect(isBundleV01).toBeDefined();
134+
});
135+
136+
it('exports type assertion functions', () => {
137+
expect(assertBundle).toBeDefined();
138+
expect(assertBundleLatest).toBeDefined();
139+
expect(assertBundleV01).toBeDefined();
140+
});
141+
142+
it('exports serialization functions', () => {
143+
expect(bundleFromJSON).toBeDefined();
144+
expect(bundleToJSON).toBeDefined();
145+
});
146+
147+
it('exports errors', () => {
148+
expect(ValidationError).toBeInstanceOf(Object);
149+
});
150+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
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 {
17+
Envelope,
18+
HashAlgorithm,
19+
MessageSignature,
20+
PublicKeyIdentifier,
21+
TimestampVerificationData,
22+
TransparencyLogEntry,
23+
X509CertificateChain,
24+
hashAlgorithmToJSON,
25+
} from '@sigstore/protobuf-specs';
26+
import { bundleFromJSON, bundleToJSON } from '../serialized';
27+
28+
import type { Bundle } from '../bundle';
29+
30+
describe('bundleToJSON', () => {
31+
const tlogEntries = [
32+
{
33+
logIndex: '0',
34+
logId: {
35+
keyId: Buffer.from('logId'),
36+
},
37+
kindVersion: {
38+
kind: 'kind',
39+
version: 'version',
40+
},
41+
canonicalizedBody: Buffer.from('body'),
42+
integratedTime: '2021-01-01T00:00:00Z',
43+
inclusionPromise: {
44+
signedEntryTimestamp: Buffer.from('inclusionPromise'),
45+
},
46+
inclusionProof: {
47+
logIndex: '0',
48+
rootHash: Buffer.from('rootHash'),
49+
treeSize: '0',
50+
hashes: [Buffer.from('hash')],
51+
checkpoint: {
52+
envelope: 'checkpoint',
53+
},
54+
},
55+
},
56+
] satisfies TransparencyLogEntry[];
57+
58+
const timestampVerificationData: TimestampVerificationData = {
59+
rfc3161Timestamps: [{ signedTimestamp: Buffer.from('signedTimestamp') }],
60+
};
61+
62+
const x509CertificateChain: X509CertificateChain = {
63+
certificates: [{ rawBytes: Buffer.from('certificate') }],
64+
};
65+
66+
const publicKey: PublicKeyIdentifier = {
67+
hint: 'pki-hint',
68+
};
69+
70+
describe('SerializedDSSEBundle', () => {
71+
const dsseEnvelope: Envelope = {
72+
payload: Buffer.from('payload'),
73+
payloadType: 'application/vnd.in-toto+json',
74+
signatures: [
75+
{
76+
keyid: 'keyid',
77+
sig: Buffer.from('signature'),
78+
},
79+
],
80+
};
81+
82+
const dsseBundle: Bundle = {
83+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.1',
84+
content: {
85+
$case: 'dsseEnvelope',
86+
dsseEnvelope,
87+
},
88+
verificationMaterial: {
89+
content: {
90+
$case: 'x509CertificateChain',
91+
x509CertificateChain,
92+
},
93+
tlogEntries,
94+
timestampVerificationData,
95+
},
96+
};
97+
98+
it('matches the serialized form of the Bundle', () => {
99+
const json = bundleToJSON(dsseBundle);
100+
101+
expect(json.mediaType).toEqual(dsseBundle.mediaType);
102+
expect(json.verificationMaterial).toBeTruthy();
103+
expect(json.verificationMaterial?.x509CertificateChain).toBeTruthy();
104+
expect(
105+
json.verificationMaterial?.x509CertificateChain?.certificates
106+
).toHaveLength(x509CertificateChain.certificates.length);
107+
108+
const cert =
109+
json.verificationMaterial?.x509CertificateChain?.certificates[0];
110+
expect(cert?.rawBytes).toEqual(
111+
Buffer.from(x509CertificateChain.certificates[0].rawBytes).toString(
112+
'base64'
113+
)
114+
);
115+
116+
expect(json.verificationMaterial?.tlogEntries).toHaveLength(
117+
tlogEntries.length
118+
);
119+
120+
const tlogEntry = json.verificationMaterial?.tlogEntries[0];
121+
const expectedTlogEntry = tlogEntries[0];
122+
expect(tlogEntry).toBeTruthy();
123+
expect(tlogEntry?.logId).toBeTruthy();
124+
expect(tlogEntry?.logId.keyId).toEqual(
125+
(expectedTlogEntry.logId?.keyId as Buffer).toString('base64')
126+
);
127+
expect(tlogEntry?.logIndex).toEqual(expectedTlogEntry.logIndex);
128+
expect(tlogEntry?.kindVersion).toBeTruthy();
129+
expect(tlogEntry?.kindVersion?.kind).toEqual(
130+
expectedTlogEntry.kindVersion?.kind
131+
);
132+
expect(tlogEntry?.kindVersion?.version).toEqual(
133+
expectedTlogEntry.kindVersion?.version
134+
);
135+
expect(tlogEntry?.canonicalizedBody).toEqual(
136+
expectedTlogEntry.canonicalizedBody.toString('base64')
137+
);
138+
expect(tlogEntry?.integratedTime).toEqual(
139+
expectedTlogEntry.integratedTime
140+
);
141+
expect(tlogEntry?.inclusionPromise).toBeTruthy();
142+
expect(tlogEntry?.inclusionPromise.signedEntryTimestamp).toEqual(
143+
(
144+
expectedTlogEntry.inclusionPromise?.signedEntryTimestamp as Buffer
145+
).toString('base64')
146+
);
147+
expect(tlogEntry?.inclusionProof).toBeTruthy();
148+
expect(tlogEntry?.inclusionProof?.logIndex).toEqual(
149+
expectedTlogEntry.inclusionProof?.logIndex
150+
);
151+
expect(tlogEntry?.inclusionProof?.rootHash).toEqual(
152+
(expectedTlogEntry.inclusionProof?.rootHash as Buffer).toString(
153+
'base64'
154+
)
155+
);
156+
expect(tlogEntry?.inclusionProof?.treeSize).toEqual(
157+
expectedTlogEntry.inclusionProof?.treeSize
158+
);
159+
expect(tlogEntry?.inclusionProof?.hashes).toHaveLength(
160+
expectedTlogEntry.inclusionProof?.hashes?.length as number
161+
);
162+
const hash = tlogEntry?.inclusionProof?.hashes[0];
163+
const expectedHash = expectedTlogEntry.inclusionProof?.hashes?.[0];
164+
expect(hash).toEqual((expectedHash as Buffer).toString('base64'));
165+
expect(tlogEntry?.inclusionProof?.checkpoint).toBeTruthy();
166+
expect(tlogEntry?.inclusionProof?.checkpoint.envelope).toEqual(
167+
expectedTlogEntry.inclusionProof?.checkpoint?.envelope
168+
);
169+
170+
expect(json.verificationMaterial?.timestampVerificationData).toBeTruthy();
171+
expect(
172+
json.verificationMaterial?.timestampVerificationData?.rfc3161Timestamps
173+
).toHaveLength(
174+
timestampVerificationData?.rfc3161Timestamps?.length as number
175+
);
176+
const rfc3161Timestamp =
177+
json.verificationMaterial?.timestampVerificationData
178+
?.rfc3161Timestamps[0];
179+
const expectedRfc3161Timestamp =
180+
timestampVerificationData?.rfc3161Timestamps?.[0];
181+
expect(rfc3161Timestamp).toBeTruthy();
182+
expect(rfc3161Timestamp?.signedTimestamp).toEqual(
183+
(expectedRfc3161Timestamp?.signedTimestamp as Buffer).toString('base64')
184+
);
185+
186+
expect(json.dsseEnvelope).toBeTruthy();
187+
expect(json.dsseEnvelope?.payload).toEqual(
188+
dsseEnvelope.payload.toString('base64')
189+
);
190+
expect(json.dsseEnvelope?.payloadType).toEqual(dsseEnvelope.payloadType);
191+
expect(json.dsseEnvelope?.signatures).toHaveLength(
192+
dsseEnvelope.signatures.length
193+
);
194+
const signature = json.dsseEnvelope?.signatures[0];
195+
const expectedSignature = dsseEnvelope.signatures[0];
196+
expect(signature).toBeTruthy();
197+
expect(signature?.keyid).toEqual(expectedSignature.keyid);
198+
expect(signature?.sig).toEqual(expectedSignature.sig.toString('base64'));
199+
});
200+
});
201+
202+
describe('SerializedMessageSignatureBundle', () => {
203+
const messageSignature = {
204+
messageDigest: {
205+
digest: Buffer.from('digest'),
206+
algorithm: 1,
207+
},
208+
signature: Buffer.from('signature'),
209+
} satisfies MessageSignature;
210+
211+
const messageSignatureBundle: Bundle = {
212+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.1',
213+
content: {
214+
$case: 'messageSignature',
215+
messageSignature,
216+
},
217+
verificationMaterial: {
218+
content: {
219+
$case: 'publicKey',
220+
publicKey,
221+
},
222+
tlogEntries,
223+
timestampVerificationData,
224+
},
225+
};
226+
227+
it('matches the serialized form of the Bundle', () => {
228+
const json = bundleToJSON(messageSignatureBundle);
229+
230+
expect(json.mediaType).toEqual(messageSignatureBundle.mediaType);
231+
expect(json.verificationMaterial).toBeTruthy();
232+
expect(json.verificationMaterial?.publicKey).toBeTruthy();
233+
expect(json.verificationMaterial?.publicKey?.hint).toEqual(
234+
publicKey.hint
235+
);
236+
237+
expect(json.verificationMaterial?.tlogEntries).toHaveLength(
238+
tlogEntries.length
239+
);
240+
241+
const tlogEntry = json.verificationMaterial?.tlogEntries[0];
242+
const expectedTlogEntry = tlogEntries[0];
243+
expect(tlogEntry).toBeTruthy();
244+
expect(tlogEntry?.logId).toBeTruthy();
245+
expect(tlogEntry?.logId.keyId).toEqual(
246+
(expectedTlogEntry.logId?.keyId as Buffer).toString('base64')
247+
);
248+
expect(tlogEntry?.logIndex).toEqual(expectedTlogEntry.logIndex);
249+
expect(tlogEntry?.kindVersion).toBeTruthy();
250+
expect(tlogEntry?.kindVersion?.kind).toEqual(
251+
expectedTlogEntry.kindVersion?.kind
252+
);
253+
expect(tlogEntry?.kindVersion?.version).toEqual(
254+
expectedTlogEntry.kindVersion?.version
255+
);
256+
expect(tlogEntry?.canonicalizedBody).toEqual(
257+
expectedTlogEntry.canonicalizedBody.toString('base64')
258+
);
259+
expect(tlogEntry?.integratedTime).toEqual(
260+
expectedTlogEntry.integratedTime
261+
);
262+
expect(tlogEntry?.inclusionPromise).toBeTruthy();
263+
expect(tlogEntry?.inclusionPromise.signedEntryTimestamp).toEqual(
264+
(
265+
expectedTlogEntry.inclusionPromise?.signedEntryTimestamp as Buffer
266+
).toString('base64')
267+
);
268+
expect(tlogEntry?.inclusionProof).toBeTruthy();
269+
expect(tlogEntry?.inclusionProof?.logIndex).toEqual(
270+
expectedTlogEntry.inclusionProof?.logIndex
271+
);
272+
expect(tlogEntry?.inclusionProof?.rootHash).toEqual(
273+
(expectedTlogEntry.inclusionProof?.rootHash as Buffer).toString(
274+
'base64'
275+
)
276+
);
277+
expect(tlogEntry?.inclusionProof?.treeSize).toEqual(
278+
expectedTlogEntry.inclusionProof?.treeSize
279+
);
280+
expect(tlogEntry?.inclusionProof?.hashes).toHaveLength(
281+
expectedTlogEntry.inclusionProof?.hashes?.length as number
282+
);
283+
const hash = tlogEntry?.inclusionProof?.hashes[0];
284+
const expectedHash = expectedTlogEntry.inclusionProof?.hashes?.[0];
285+
expect(hash).toEqual((expectedHash as Buffer).toString('base64'));
286+
expect(tlogEntry?.inclusionProof?.checkpoint).toBeTruthy();
287+
expect(tlogEntry?.inclusionProof?.checkpoint.envelope).toEqual(
288+
expectedTlogEntry.inclusionProof?.checkpoint?.envelope
289+
);
290+
291+
expect(json.verificationMaterial?.timestampVerificationData).toBeTruthy();
292+
expect(
293+
json.verificationMaterial?.timestampVerificationData?.rfc3161Timestamps
294+
).toHaveLength(
295+
timestampVerificationData?.rfc3161Timestamps?.length as number
296+
);
297+
const rfc3161Timestamp =
298+
json.verificationMaterial?.timestampVerificationData
299+
?.rfc3161Timestamps[0];
300+
const expectedRfc3161Timestamp =
301+
timestampVerificationData?.rfc3161Timestamps?.[0];
302+
expect(rfc3161Timestamp).toBeTruthy();
303+
expect(rfc3161Timestamp?.signedTimestamp).toEqual(
304+
(expectedRfc3161Timestamp?.signedTimestamp as Buffer).toString('base64')
305+
);
306+
307+
expect(json.messageSignature).toBeTruthy();
308+
expect(json.messageSignature?.messageDigest).toBeTruthy();
309+
expect(json.messageSignature?.messageDigest?.digest).toEqual(
310+
(messageSignature.messageDigest?.digest as Buffer).toString('base64')
311+
);
312+
expect(json.messageSignature?.messageDigest?.algorithm).toEqual(
313+
hashAlgorithmToJSON(
314+
messageSignature.messageDigest?.algorithm as HashAlgorithm
315+
)
316+
);
317+
318+
expect(json.messageSignature?.signature).toEqual(
319+
(messageSignature.signature as Buffer).toString('base64')
320+
);
321+
});
322+
});
323+
});
324+
325+
describe('bundleFromJSON', () => {
326+
const bundle: Bundle = {
327+
mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.1',
328+
verificationMaterial: {
329+
content: {
330+
$case: 'x509CertificateChain',
331+
x509CertificateChain: {
332+
certificates: [{ rawBytes: Buffer.from('FOO') }],
333+
},
334+
},
335+
tlogEntries: [
336+
{
337+
logIndex: '123',
338+
logId: { keyId: Buffer.from('123') },
339+
kindVersion: { kind: 'intoto', version: '0.1.0' },
340+
canonicalizedBody: Buffer.from(''),
341+
integratedTime: '0',
342+
inclusionPromise: { signedEntryTimestamp: Buffer.from('') },
343+
inclusionProof: undefined,
344+
},
345+
],
346+
timestampVerificationData: undefined,
347+
},
348+
content: {
349+
$case: 'dsseEnvelope',
350+
dsseEnvelope: {
351+
payload: Buffer.from('ABC'),
352+
payloadType: 'application/json',
353+
signatures: [{ sig: Buffer.from('BAR'), keyid: '' }],
354+
},
355+
},
356+
};
357+
358+
it('matches the deserialized form of the Bundle', () => {
359+
const json = bundleToJSON(bundle);
360+
const deserializedBundle = bundleFromJSON(json);
361+
expect(deserializedBundle).toEqual(bundle);
362+
});
363+
});

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

+844
Large diffs are not rendered by default.

‎packages/bundle/src/bundle.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 type {
17+
Bundle as ProtoBundle,
18+
InclusionProof as ProtoInclusionProof,
19+
MessageSignature as ProtoMessageSignature,
20+
TransparencyLogEntry as ProtoTransparencyLogEntry,
21+
VerificationMaterial as ProtoVerificationMaterial,
22+
} from '@sigstore/protobuf-specs';
23+
import type { WithRequired } from './utility';
24+
25+
// Extract types that are not explicitly defined in the protobuf specs.
26+
type DsseEnvelopeContent = Extract<
27+
ProtoBundle['content'],
28+
{ $case: 'dsseEnvelope' }
29+
>;
30+
type MessageSignatureContent = Extract<
31+
ProtoBundle['content'],
32+
{ $case: 'messageSignature' }
33+
>;
34+
35+
// Narrowed types with required fields marked as such.
36+
export type MessageSignature = WithRequired<
37+
ProtoMessageSignature,
38+
'messageDigest'
39+
>;
40+
41+
export type VerificationMaterial = WithRequired<
42+
ProtoVerificationMaterial,
43+
'content'
44+
>;
45+
46+
export type TransparencyLogEntry = WithRequired<
47+
ProtoTransparencyLogEntry,
48+
'logId' | 'kindVersion'
49+
>;
50+
51+
export type InclusionProof = WithRequired<ProtoInclusionProof, 'checkpoint'>;
52+
53+
export type TLogEntryWithInclusionPromise = WithRequired<
54+
TransparencyLogEntry,
55+
'inclusionPromise'
56+
>;
57+
58+
export type TLogEntryWithInclusionProof = TransparencyLogEntry & {
59+
inclusionProof: InclusionProof;
60+
};
61+
62+
// Common type shared by v0.1 and v0.2 of the Sigstore bundle format with all
63+
// required fields populated.
64+
export type Bundle = ProtoBundle & {
65+
verificationMaterial: VerificationMaterial & {
66+
tlogEntries: TransparencyLogEntry[];
67+
};
68+
content:
69+
| (MessageSignatureContent & { messageSignature: MessageSignature })
70+
| DsseEnvelopeContent;
71+
};
72+
73+
// Version 0.1 of the Sigstore bundle format with all required fields populated.
74+
// Ensures inclusion promise is present in each transparency log entry.
75+
export type BundleV01 = Bundle & {
76+
verificationMaterial: Bundle['verificationMaterial'] & {
77+
tlogEntries: TLogEntryWithInclusionPromise[];
78+
};
79+
};
80+
81+
// Version 0.2 of the Sigstore bundle format with all required fields populated.
82+
// Ensures inclusion proof is present in each transparency log entry.
83+
export type BundleLatest = Bundle & {
84+
verificationMaterial: Bundle['verificationMaterial'] & {
85+
tlogEntries: TLogEntryWithInclusionProof[];
86+
};
87+
};
88+
89+
// Bundle variants with specific content types.
90+
export type BundleWithCertificateChain = Bundle & {
91+
verificationMaterial: Bundle['verificationMaterial'] & {
92+
content: Extract<
93+
VerificationMaterial['content'],
94+
{ $case: 'x509CertificateChain' }
95+
>;
96+
};
97+
};
98+
99+
export type BundleWithPublicKey = Bundle & {
100+
verificationMaterial: Bundle['verificationMaterial'] & {
101+
content: Extract<VerificationMaterial['content'], { $case: 'publicKey' }>;
102+
};
103+
};
104+
105+
export type BundleWithMessageSignature = Bundle & {
106+
content: Extract<Bundle['content'], { $case: 'messageSignature' }>;
107+
};
108+
109+
export type BundleWithDsseEnvelope = Bundle & {
110+
content: Extract<Bundle['content'], { $case: 'dsseEnvelope' }>;
111+
};
112+
113+
// Type guards for bundle variants.
114+
export function isBundleWithCertificateChain(
115+
b: Bundle
116+
): b is BundleWithCertificateChain {
117+
return b.verificationMaterial.content.$case === 'x509CertificateChain';
118+
}
119+
120+
export function isBundleWithPublicKey(b: Bundle): b is BundleWithPublicKey {
121+
return b.verificationMaterial.content.$case === 'publicKey';
122+
}
123+
124+
export function isBundleWithMessageSignature(
125+
b: Bundle
126+
): b is BundleWithMessageSignature {
127+
return b.content.$case === 'messageSignature';
128+
}
129+
130+
export function isBundleWithDsseEnvelope(
131+
b: Bundle
132+
): b is BundleWithDsseEnvelope {
133+
return b.content.$case === 'dsseEnvelope';
134+
}

‎packages/bundle/src/error.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
export class ValidationError extends Error {
17+
fields: string[];
18+
19+
constructor(message: string, fields: string[]) {
20+
super(message);
21+
this.fields = fields;
22+
}
23+
}

‎packages/bundle/src/index.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
export {
17+
isBundleWithCertificateChain,
18+
isBundleWithDsseEnvelope,
19+
isBundleWithMessageSignature,
20+
isBundleWithPublicKey,
21+
} from './bundle';
22+
export { ValidationError } from './error';
23+
export { bundleFromJSON, bundleToJSON } from './serialized';
24+
export {
25+
assertBundle,
26+
assertBundleLatest,
27+
assertBundleV01,
28+
isBundleV01,
29+
} from './validate';
30+
31+
export type {
32+
Envelope,
33+
PublicKeyIdentifier,
34+
RFC3161SignedTimestamp,
35+
Signature,
36+
TimestampVerificationData,
37+
X509Certificate,
38+
X509CertificateChain,
39+
} from '@sigstore/protobuf-specs';
40+
export type {
41+
Bundle,
42+
BundleLatest,
43+
BundleV01,
44+
BundleWithCertificateChain,
45+
BundleWithDsseEnvelope,
46+
BundleWithMessageSignature,
47+
BundleWithPublicKey,
48+
InclusionProof,
49+
MessageSignature,
50+
TLogEntryWithInclusionPromise,
51+
TLogEntryWithInclusionProof,
52+
TransparencyLogEntry,
53+
VerificationMaterial,
54+
} from './bundle';
55+
export type { SerializedBundle, SerializedEnvelope } from './serialized';

‎packages/bundle/src/serialized.ts

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 { Bundle as ProtoBundle } from '@sigstore/protobuf-specs';
17+
import { assertBundle } from './validate';
18+
19+
import type { Bundle } from './bundle';
20+
import type { OneOf } from './utility';
21+
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
export const bundleFromJSON = (obj: any): Bundle => {
24+
const bundle = ProtoBundle.fromJSON(obj);
25+
assertBundle(bundle);
26+
return bundle;
27+
};
28+
29+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30+
export const bundleToJSON = (bundle: Bundle): SerializedBundle => {
31+
return ProtoBundle.toJSON(bundle) as SerializedBundle;
32+
};
33+
34+
type SerializedTLogEntry = {
35+
logIndex: string;
36+
logId: {
37+
keyId: string;
38+
};
39+
kindVersion:
40+
| {
41+
kind: string;
42+
version: string;
43+
}
44+
| undefined;
45+
integratedTime: string;
46+
inclusionPromise: {
47+
signedEntryTimestamp: string;
48+
};
49+
inclusionProof:
50+
| {
51+
logIndex: string;
52+
rootHash: string;
53+
treeSize: string;
54+
hashes: string[];
55+
checkpoint: { envelope: string };
56+
}
57+
| undefined;
58+
canonicalizedBody: string;
59+
};
60+
61+
type SerializedTimestampVerificationData = {
62+
rfc3161Timestamps: { signedTimestamp: string }[];
63+
};
64+
65+
// Serialized form of the messageSignature option in the Sigstore Bundle
66+
type SerializedMessageSignature = {
67+
messageDigest:
68+
| {
69+
algorithm: string;
70+
digest: string;
71+
}
72+
| undefined;
73+
signature: string;
74+
};
75+
76+
// Serialized form of the dsseEnvelope option in the Sigstore Bundle
77+
type SerializedDSSEEnvelope = {
78+
payload: string;
79+
payloadType: string;
80+
signatures: {
81+
sig: string;
82+
keyid: string;
83+
}[];
84+
};
85+
86+
// Serialized form of the DSSE Envelope
87+
export type { SerializedDSSEEnvelope as SerializedEnvelope };
88+
89+
// Serialized form of the Sigstore Bundle union type with all possible options
90+
// represented
91+
export type SerializedBundle = {
92+
mediaType: string;
93+
verificationMaterial: (
94+
| OneOf<{
95+
x509CertificateChain: { certificates: { rawBytes: string }[] };
96+
publicKey: { hint: string };
97+
}>
98+
| undefined
99+
) & {
100+
tlogEntries: SerializedTLogEntry[];
101+
timestampVerificationData: SerializedTimestampVerificationData | undefined;
102+
};
103+
} & OneOf<{
104+
dsseEnvelope: SerializedDSSEEnvelope;
105+
messageSignature: SerializedMessageSignature;
106+
}>;

‎packages/bundle/src/utility.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
// Returns a type that is the union of all value types in the given object.
17+
type ValueOf<Obj> = Obj[keyof Obj];
18+
19+
// Returns a type narrowing the given object to only the given key -- all
20+
// other keys must be undefined.
21+
type OneOnly<Obj, K extends keyof Obj> = {
22+
[key in Exclude<keyof Obj, K>]: undefined;
23+
} & { [key in K]: Obj[K] };
24+
25+
// Returns a type that is the union of all OneOnly types for the given object.
26+
// This type is not actually usable as no value could ever satisfy it.
27+
type OneOfByKey<Obj> = { [key in keyof Obj]: OneOnly<Obj, key> };
28+
29+
// Returns a type that is the union of all OneOnly types for the given object.
30+
export type OneOf<T> = ValueOf<OneOfByKey<T>>;
31+
32+
// Returns a type that is a copy of the given object with the specified keys
33+
// made required.
34+
export type WithRequired<T, K extends keyof T> = T & {
35+
[P in K]-?: NonNullable<T[P]>;
36+
};

‎packages/bundle/src/validate.ts

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 { ValidationError } from './error';
17+
18+
import type { Bundle as ProtoBundle } from '@sigstore/protobuf-specs';
19+
import type { Bundle, BundleLatest, BundleV01 } from './bundle';
20+
21+
// Performs basic validation of a Sigstore bundle to ensure that all required
22+
// fields are populated. This is not a complete validation of the bundle, but
23+
// rather a check that the bundle is in a valid state to be processed by the
24+
// rest of the code.
25+
export function assertBundle(b: ProtoBundle): asserts b is Bundle {
26+
const invalidValues: string[] = [];
27+
28+
// Media type validation
29+
if (
30+
b.mediaType === undefined ||
31+
!b.mediaType.startsWith('application/vnd.dev.sigstore.bundle+json;version=')
32+
) {
33+
invalidValues.push('mediaType');
34+
}
35+
36+
// Content-related validation
37+
if (b.content === undefined) {
38+
invalidValues.push('content');
39+
} else {
40+
switch (b.content.$case) {
41+
case 'messageSignature':
42+
if (b.content.messageSignature.messageDigest === undefined) {
43+
invalidValues.push('content.messageSignature.messageDigest');
44+
} else {
45+
if (b.content.messageSignature.messageDigest.digest.length === 0) {
46+
invalidValues.push('content.messageSignature.messageDigest.digest');
47+
}
48+
}
49+
50+
if (b.content.messageSignature.signature.length === 0) {
51+
invalidValues.push('content.messageSignature.signature');
52+
}
53+
break;
54+
case 'dsseEnvelope':
55+
if (b.content.dsseEnvelope.payload.length === 0) {
56+
invalidValues.push('content.dsseEnvelope.payload');
57+
}
58+
59+
if (b.content.dsseEnvelope.signatures.length !== 1) {
60+
invalidValues.push('content.dsseEnvelope.signatures');
61+
} else {
62+
if (b.content.dsseEnvelope.signatures[0].sig.length === 0) {
63+
invalidValues.push('content.dsseEnvelope.signatures[0].sig');
64+
}
65+
}
66+
67+
break;
68+
}
69+
}
70+
71+
// Verification material-related validation
72+
if (b.verificationMaterial === undefined) {
73+
invalidValues.push('verificationMaterial');
74+
} else {
75+
if (b.verificationMaterial.content === undefined) {
76+
invalidValues.push('verificationMaterial.content');
77+
} else {
78+
switch (b.verificationMaterial.content.$case) {
79+
case 'x509CertificateChain':
80+
if (
81+
b.verificationMaterial.content.x509CertificateChain.certificates
82+
.length === 0
83+
) {
84+
invalidValues.push(
85+
'verificationMaterial.content.x509CertificateChain.certificates'
86+
);
87+
}
88+
89+
b.verificationMaterial.content.x509CertificateChain.certificates.forEach(
90+
(cert, i) => {
91+
if (cert.rawBytes.length === 0) {
92+
invalidValues.push(
93+
`verificationMaterial.content.x509CertificateChain.certificates[${i}].rawBytes`
94+
);
95+
}
96+
}
97+
);
98+
break;
99+
}
100+
}
101+
102+
if (b.verificationMaterial.tlogEntries === undefined) {
103+
invalidValues.push('verificationMaterial.tlogEntries');
104+
} else {
105+
if (b.verificationMaterial.tlogEntries.length > 0) {
106+
b.verificationMaterial.tlogEntries.forEach((entry, i) => {
107+
if (entry.logId === undefined) {
108+
invalidValues.push(`verificationMaterial.tlogEntries[${i}].logId`);
109+
}
110+
111+
if (entry.kindVersion === undefined) {
112+
invalidValues.push(
113+
`verificationMaterial.tlogEntries[${i}].kindVersion`
114+
);
115+
}
116+
});
117+
}
118+
}
119+
}
120+
121+
if (invalidValues.length > 0) {
122+
throw new ValidationError('invalid bundle', invalidValues);
123+
}
124+
}
125+
126+
// Asserts that the given bundle conforms to the v0.1 bundle format.
127+
export function assertBundleV01(b: Bundle): asserts b is BundleV01 {
128+
const invalidValues: string[] = [];
129+
130+
if (
131+
b.mediaType &&
132+
b.mediaType !== 'application/vnd.dev.sigstore.bundle+json;version=0.1'
133+
) {
134+
invalidValues.push('mediaType');
135+
}
136+
137+
if (
138+
b.verificationMaterial &&
139+
b.verificationMaterial.tlogEntries?.length > 0
140+
) {
141+
b.verificationMaterial.tlogEntries.forEach((entry, i) => {
142+
if (entry.inclusionPromise === undefined) {
143+
invalidValues.push(
144+
`verificationMaterial.tlogEntries[${i}].inclusionPromise`
145+
);
146+
}
147+
});
148+
}
149+
150+
if (invalidValues.length > 0) {
151+
throw new ValidationError('invalid v0.1 bundle', invalidValues);
152+
}
153+
}
154+
155+
// Type guard to determine if Bundle is a v0.1 bundle.
156+
export function isBundleV01(b: Bundle): b is BundleV01 {
157+
try {
158+
assertBundleV01(b);
159+
return true;
160+
} catch (e) {
161+
return false;
162+
}
163+
}
164+
165+
// Asserts that the given bundle conforms to the newest (0.2) bundle format.
166+
export function assertBundleLatest(b: ProtoBundle): asserts b is BundleLatest {
167+
const invalidValues: string[] = [];
168+
169+
if (
170+
b.verificationMaterial &&
171+
b.verificationMaterial.tlogEntries?.length > 0
172+
) {
173+
b.verificationMaterial.tlogEntries.forEach((entry, i) => {
174+
if (entry.inclusionProof === undefined) {
175+
invalidValues.push(
176+
`verificationMaterial.tlogEntries[${i}].inclusionProof`
177+
);
178+
} else {
179+
if (entry.inclusionProof.checkpoint === undefined) {
180+
invalidValues.push(
181+
`verificationMaterial.tlogEntries[${i}].inclusionProof.checkpoint`
182+
);
183+
}
184+
}
185+
});
186+
}
187+
188+
if (invalidValues.length > 0) {
189+
throw new ValidationError('invalid v0.2 bundle', invalidValues);
190+
}
191+
}

‎packages/bundle/tsconfig.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"rootDir": "src",
5+
"outDir": "dist",
6+
"composite": true
7+
},
8+
"exclude": [
9+
"./dist",
10+
"**/__tests__",
11+
]
12+
}

‎tsconfig.build.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"files": [],
33
"references": [
4+
{ "path": "packages/bundle" },
45
{ "path": "packages/cli" },
56
{ "path": "packages/client" },
67
{ "path": "packages/mock" },

0 commit comments

Comments
 (0)
Please sign in to comment.