From b1a2d33bf4042a1800e124458807e11d4bb6c43a Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 26 Mar 2024 17:16:12 -0700 Subject: [PATCH] support for building v0.3 bundles Signed-off-by: Brian DeHamer --- .changeset/six-baboons-cry.md | 5 + .../__snapshots__/build.test.ts.snap | 203 +++++++++++++++++ packages/bundle/src/__tests__/build.test.ts | 205 +++++++++--------- packages/bundle/src/__tests__/index.test.ts | 7 + packages/bundle/src/build.ts | 34 ++- packages/bundle/src/bundle.ts | 13 +- packages/bundle/src/index.ts | 2 + packages/bundle/src/validate.ts | 7 +- 8 files changed, 363 insertions(+), 113 deletions(-) create mode 100644 .changeset/six-baboons-cry.md create mode 100644 packages/bundle/src/__tests__/__snapshots__/build.test.ts.snap diff --git a/.changeset/six-baboons-cry.md b/.changeset/six-baboons-cry.md new file mode 100644 index 00000000..3c3b0589 --- /dev/null +++ b/.changeset/six-baboons-cry.md @@ -0,0 +1,5 @@ +--- +"@sigstore/bundle": minor +--- + +Add support for building v0.3 bundles diff --git a/packages/bundle/src/__tests__/__snapshots__/build.test.ts.snap b/packages/bundle/src/__tests__/__snapshots__/build.test.ts.snap new file mode 100644 index 00000000..9b1e5413 --- /dev/null +++ b/packages/bundle/src/__tests__/__snapshots__/build.test.ts.snap @@ -0,0 +1,203 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`toDSSEBundle when the singleCertificate option is true when a certificate chain provided returns a valid DSSE bundle 1`] = ` +{ + "dsseEnvelope": { + "payload": "ZGF0YQ==", + "payloadType": "text/plain", + "signatures": [ + { + "keyid": "", + "sig": "c2lnbmF0dXJl", + }, + ], + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "Y2VydGlmaWNhdGU=", + }, + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + }, +} +`; + +exports[`toDSSEBundle when the singleCertificate option is true when a public key w/ hint is provided returns a valid DSSE bundle 1`] = ` +{ + "dsseEnvelope": { + "payload": "ZGF0YQ==", + "payloadType": "text/plain", + "signatures": [ + { + "keyid": "hint", + "sig": "c2lnbmF0dXJl", + }, + ], + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "publicKey": { + "hint": "hint", + }, + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + }, +} +`; + +exports[`toDSSEBundle when the singleCertificate option is true when a public key w/o hint is provided returns a valid DSSE bundle 1`] = ` +{ + "dsseEnvelope": { + "payload": "ZGF0YQ==", + "payloadType": "text/plain", + "signatures": [ + { + "keyid": "", + "sig": "c2lnbmF0dXJl", + }, + ], + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "publicKey": { + "hint": "", + }, + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + }, +} +`; + +exports[`toDSSEBundle when the singleCertificate option is undefined/false when a certificate chain provided returns a valid DSSE bundle 1`] = ` +{ + "dsseEnvelope": { + "payload": "ZGF0YQ==", + "payloadType": "text/plain", + "signatures": [ + { + "keyid": "", + "sig": "c2lnbmF0dXJl", + }, + ], + }, + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "Y2VydGlmaWNhdGU=", + }, + ], + }, + }, +} +`; + +exports[`toDSSEBundle when the singleCertificate option is undefined/false when a public key w/ hint is provided returns a valid DSSE bundle 1`] = ` +{ + "dsseEnvelope": { + "payload": "ZGF0YQ==", + "payloadType": "text/plain", + "signatures": [ + { + "keyid": "hint", + "sig": "c2lnbmF0dXJl", + }, + ], + }, + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "publicKey": { + "hint": "hint", + }, + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + }, +} +`; + +exports[`toDSSEBundle when the singleCertificate option is undefined/false when a public key w/o hint is provided returns a valid DSSE bundle 1`] = ` +{ + "dsseEnvelope": { + "payload": "ZGF0YQ==", + "payloadType": "text/plain", + "signatures": [ + { + "keyid": "", + "sig": "c2lnbmF0dXJl", + }, + ], + }, + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "publicKey": { + "hint": "", + }, + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + }, +} +`; + +exports[`toMessageSignatureBundle when the singleCertificate option is true returns a valid message signature bundle 1`] = ` +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_256", + "digest": "ZGlnZXN0", + }, + "signature": "c2lnbmF0dXJl", + }, + "verificationMaterial": { + "certificate": { + "rawBytes": "Y2VydGlmaWNhdGU=", + }, + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + }, +} +`; + +exports[`toMessageSignatureBundle when the singleCertificate option is undefined returns a valid message signature bundle 1`] = ` +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_256", + "digest": "ZGlnZXN0", + }, + "signature": "c2lnbmF0dXJl", + }, + "verificationMaterial": { + "timestampVerificationData": { + "rfc3161Timestamps": [], + }, + "tlogEntries": [], + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "Y2VydGlmaWNhdGU=", + }, + ], + }, + }, +} +`; diff --git a/packages/bundle/src/__tests__/build.test.ts b/packages/bundle/src/__tests__/build.test.ts index 7c45feba..5bc47363 100644 --- a/packages/bundle/src/__tests__/build.test.ts +++ b/packages/bundle/src/__tests__/build.test.ts @@ -13,10 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { HashAlgorithm } from '@sigstore/protobuf-specs'; -import assert from 'assert'; +import { Bundle } from '@sigstore/protobuf-specs'; import { toDSSEBundle, toMessageSignatureBundle } from '../build'; -import { BUNDLE_V02_MEDIA_TYPE } from '../bundle'; const signature = Buffer.from('signature'); const keyHint = 'hint'; @@ -24,34 +22,34 @@ const certificate = Buffer.from('certificate'); describe('toMessageSignatureBundle', () => { const digest = Buffer.from('digest'); - it('returns a valid message signature bundle', () => { - const b = toMessageSignatureBundle({ - digest, - signature, - certificate, - keyHint, + + describe('when the singleCertificate option is undefined', () => { + it('returns a valid message signature bundle', () => { + const b = toMessageSignatureBundle({ + digest, + signature, + certificate, + keyHint, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); }); + }); - expect(b).toBeTruthy(); - expect(b.mediaType).toEqual(BUNDLE_V02_MEDIA_TYPE); - - assert(b.content?.$case === 'messageSignature'); - expect(b.content.messageSignature).toBeTruthy(); - expect(b.content.messageSignature.messageDigest.algorithm).toEqual( - HashAlgorithm.SHA2_256 - ); - expect(b.content.messageSignature.messageDigest.digest).toEqual(digest); - expect(b.content.messageSignature.signature).toEqual(signature); - - expect(b.verificationMaterial).toBeTruthy(); - assert(b.verificationMaterial.content?.$case === 'x509CertificateChain'); - expect( - b.verificationMaterial.content?.x509CertificateChain.certificates - ).toHaveLength(1); - expect( - b.verificationMaterial.content?.x509CertificateChain.certificates[0] - .rawBytes - ).toEqual(certificate); + describe('when the singleCertificate option is true', () => { + it('returns a valid message signature bundle', () => { + const b = toMessageSignatureBundle({ + digest, + signature, + certificate, + keyHint, + singleCertificate: true, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); + }); }); }); @@ -59,88 +57,93 @@ describe('toDSSEBundle', () => { const artifact = Buffer.from('data'); const artifactType = 'text/plain'; - describe('when a public key w/ hint is provided', () => { - it('returns a valid DSSE bundle', () => { - const b = toDSSEBundle({ - artifact, - artifactType, - signature, - keyHint, + describe('when the singleCertificate option is undefined/false', () => { + describe('when a public key w/ hint is provided', () => { + it('returns a valid DSSE bundle', () => { + const b = toDSSEBundle({ + artifact, + artifactType, + signature, + keyHint, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); }); - - expect(b).toBeTruthy(); - expect(b.mediaType).toEqual(BUNDLE_V02_MEDIA_TYPE); - - assert(b.content?.$case === 'dsseEnvelope'); - expect(b.content.dsseEnvelope).toBeTruthy(); - expect(b.content.dsseEnvelope.payloadType).toEqual(artifactType); - expect(b.content.dsseEnvelope.payload).toEqual(artifact); - expect(b.content.dsseEnvelope.signatures).toHaveLength(1); - expect(b.content.dsseEnvelope.signatures[0].sig).toEqual(signature); - expect(b.content.dsseEnvelope.signatures[0].keyid).toEqual(keyHint); - - expect(b.verificationMaterial).toBeTruthy(); - assert(b.verificationMaterial.content?.$case === 'publicKey'); - expect(b.verificationMaterial.content?.publicKey).toBeTruthy(); - expect(b.verificationMaterial.content?.publicKey.hint).toEqual(keyHint); }); - }); - describe('when a public key w/o hint is provided', () => { - it('returns a valid DSSE bundle', () => { - const b = toDSSEBundle({ - artifact, - artifactType, - signature, + describe('when a public key w/o hint is provided', () => { + it('returns a valid DSSE bundle', () => { + const b = toDSSEBundle({ + artifact, + artifactType, + signature, + singleCertificate: false, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); }); + }); - expect(b).toBeTruthy(); - expect(b.mediaType).toEqual(BUNDLE_V02_MEDIA_TYPE); - - assert(b.content?.$case === 'dsseEnvelope'); - expect(b.content.dsseEnvelope).toBeTruthy(); - expect(b.content.dsseEnvelope.payloadType).toEqual(artifactType); - expect(b.content.dsseEnvelope.payload).toEqual(artifact); - expect(b.content.dsseEnvelope.signatures).toHaveLength(1); - expect(b.content.dsseEnvelope.signatures[0].sig).toEqual(signature); - expect(b.content.dsseEnvelope.signatures[0].keyid).toEqual(''); - - expect(b.verificationMaterial).toBeTruthy(); - assert(b.verificationMaterial.content?.$case === 'publicKey'); - expect(b.verificationMaterial.content?.publicKey).toBeTruthy(); - expect(b.verificationMaterial.content?.publicKey.hint).toEqual(''); + describe('when a certificate chain provided', () => { + it('returns a valid DSSE bundle', () => { + const b = toDSSEBundle({ + artifact, + artifactType, + signature, + certificate, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); + }); }); }); - describe('when a certificate chain provided', () => { - it('returns a valid DSSE bundle', () => { - const b = toDSSEBundle({ - artifact, - artifactType, - signature, - certificate, + describe('when the singleCertificate option is true', () => { + describe('when a public key w/ hint is provided', () => { + it('returns a valid DSSE bundle', () => { + const b = toDSSEBundle({ + artifact, + artifactType, + signature, + keyHint, + singleCertificate: true, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); }); + }); - expect(b).toBeTruthy(); - expect(b.mediaType).toEqual(BUNDLE_V02_MEDIA_TYPE); - - assert(b.content?.$case === 'dsseEnvelope'); - expect(b.content.dsseEnvelope).toBeTruthy(); - expect(b.content.dsseEnvelope.payloadType).toEqual(artifactType); - expect(b.content.dsseEnvelope.payload).toEqual(artifact); - expect(b.content.dsseEnvelope.signatures).toHaveLength(1); - expect(b.content.dsseEnvelope.signatures[0].sig).toEqual(signature); - expect(b.content.dsseEnvelope.signatures[0].keyid).toEqual(''); - - expect(b.verificationMaterial).toBeTruthy(); - assert(b.verificationMaterial.content?.$case === 'x509CertificateChain'); - expect( - b.verificationMaterial.content?.x509CertificateChain.certificates - ).toHaveLength(1); - expect( - b.verificationMaterial.content?.x509CertificateChain.certificates[0] - .rawBytes - ).toEqual(certificate); + describe('when a public key w/o hint is provided', () => { + it('returns a valid DSSE bundle', () => { + const b = toDSSEBundle({ + artifact, + artifactType, + signature, + singleCertificate: true, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); + }); + }); + + describe('when a certificate chain provided', () => { + it('returns a valid DSSE bundle', () => { + const b = toDSSEBundle({ + artifact, + artifactType, + signature, + certificate, + singleCertificate: true, + }); + + expect(b).toBeTruthy(); + expect(Bundle.toJSON(b)).toMatchSnapshot(); + }); }); }); }); diff --git a/packages/bundle/src/__tests__/index.test.ts b/packages/bundle/src/__tests__/index.test.ts index 3d37cfc9..977695a8 100644 --- a/packages/bundle/src/__tests__/index.test.ts +++ b/packages/bundle/src/__tests__/index.test.ts @@ -17,6 +17,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { BUNDLE_V01_MEDIA_TYPE, BUNDLE_V02_MEDIA_TYPE, + BUNDLE_V03_LEGACY_MEDIA_TYPE, BUNDLE_V03_MEDIA_TYPE, Bundle, BundleLatest, @@ -25,6 +26,7 @@ import { BundleWithDsseEnvelope, BundleWithMessageSignature, BundleWithPublicKey, + BundleWithSingleCertificate, Envelope, InclusionProof, MessageSignature, @@ -74,6 +76,10 @@ describe('public interface', () => { ); expect(bundleWithCertificateChain).toBeDefined(); + const bundleWithSingleCertificate: BundleWithSingleCertificate = + fromPartial({}); + expect(bundleWithSingleCertificate).toBeDefined(); + const bundleWithDsseEnvelope: BundleWithDsseEnvelope = fromPartial({}); expect(bundleWithDsseEnvelope).toBeDefined(); @@ -163,6 +169,7 @@ describe('public interface', () => { it('exports constants', () => { expect(BUNDLE_V01_MEDIA_TYPE).toBeDefined(); expect(BUNDLE_V02_MEDIA_TYPE).toBeDefined(); + expect(BUNDLE_V03_LEGACY_MEDIA_TYPE).toBeDefined(); expect(BUNDLE_V03_MEDIA_TYPE).toBeDefined(); }); diff --git a/packages/bundle/src/build.ts b/packages/bundle/src/build.ts index fe806d3f..ad39eb08 100644 --- a/packages/bundle/src/build.ts +++ b/packages/bundle/src/build.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import { HashAlgorithm } from '@sigstore/protobuf-specs'; -import { BUNDLE_V02_MEDIA_TYPE } from './bundle'; +import { BUNDLE_V02_MEDIA_TYPE, BUNDLE_V03_MEDIA_TYPE } from './bundle'; import type { Envelope, Signature } from '@sigstore/protobuf-specs'; import type { @@ -28,6 +28,11 @@ import type { type VerificationMaterialOptions = { certificate?: Buffer; keyHint?: string; + + // When set to true, the bundle verification material will use the + // certifciate field instead of the x509CertificateChain field. + // When undefied/false, a v0.2 bundle will be created. + singleCertificate?: boolean; }; type MessageSignatureBundleOptions = { @@ -46,7 +51,9 @@ export function toMessageSignatureBundle( options: MessageSignatureBundleOptions ): BundleWithMessageSignature { return { - mediaType: BUNDLE_V02_MEDIA_TYPE, + mediaType: options.singleCertificate + ? BUNDLE_V03_MEDIA_TYPE + : BUNDLE_V02_MEDIA_TYPE, content: { $case: 'messageSignature', messageSignature: { @@ -67,7 +74,9 @@ export function toDSSEBundle( options: DSSEBundleOptions ): BundleWithDsseEnvelope { return { - mediaType: BUNDLE_V02_MEDIA_TYPE, + mediaType: options.singleCertificate + ? BUNDLE_V03_MEDIA_TYPE + : BUNDLE_V02_MEDIA_TYPE, content: { $case: 'dsseEnvelope', dsseEnvelope: toEnvelope(options), @@ -107,12 +116,19 @@ function toKeyContent( options: VerificationMaterialOptions ): Bundle['verificationMaterial']['content'] { if (options.certificate) { - return { - $case: 'x509CertificateChain', - x509CertificateChain: { - certificates: [{ rawBytes: options.certificate }], - }, - }; + if (options.singleCertificate) { + return { + $case: 'certificate', + certificate: { rawBytes: options.certificate }, + }; + } else { + return { + $case: 'x509CertificateChain', + x509CertificateChain: { + certificates: [{ rawBytes: options.certificate }], + }, + }; + } } else { return { $case: 'publicKey', diff --git a/packages/bundle/src/bundle.ts b/packages/bundle/src/bundle.ts index c68bdbe4..9bcfb132 100644 --- a/packages/bundle/src/bundle.ts +++ b/packages/bundle/src/bundle.ts @@ -28,9 +28,12 @@ export const BUNDLE_V01_MEDIA_TYPE = export const BUNDLE_V02_MEDIA_TYPE = 'application/vnd.dev.sigstore.bundle+json;version=0.2'; -export const BUNDLE_V03_MEDIA_TYPE = +export const BUNDLE_V03_LEGACY_MEDIA_TYPE = 'application/vnd.dev.sigstore.bundle+json;version=0.3'; +export const BUNDLE_V03_MEDIA_TYPE = + 'application/vnd.dev.sigstore.bundle.v0.3+json'; + // Extract types that are not explicitly defined in the protobuf specs. type DsseEnvelopeContent = Extract< ProtoBundle['content'], @@ -87,7 +90,7 @@ export type BundleV01 = Bundle & { }; }; -// Version 0.2 of the Sigstore bundle format with all required fields populated. +// Version 0.3 of the Sigstore bundle format with all required fields populated. // Ensures inclusion proof is present in each transparency log entry. export type BundleLatest = Bundle & { verificationMaterial: Bundle['verificationMaterial'] & { @@ -105,6 +108,12 @@ export type BundleWithCertificateChain = Bundle & { }; }; +export type BundleWithSingleCertificate = Bundle & { + verificationMaterial: Bundle['verificationMaterial'] & { + content: Extract; + }; +}; + export type BundleWithPublicKey = Bundle & { verificationMaterial: Bundle['verificationMaterial'] & { content: Extract; diff --git a/packages/bundle/src/index.ts b/packages/bundle/src/index.ts index 04d1de6a..f3b984b7 100644 --- a/packages/bundle/src/index.ts +++ b/packages/bundle/src/index.ts @@ -17,6 +17,7 @@ export { toDSSEBundle, toMessageSignatureBundle } from './build'; export { BUNDLE_V01_MEDIA_TYPE, BUNDLE_V02_MEDIA_TYPE, + BUNDLE_V03_LEGACY_MEDIA_TYPE, BUNDLE_V03_MEDIA_TYPE, isBundleWithCertificateChain, isBundleWithDsseEnvelope, @@ -55,6 +56,7 @@ export type { BundleWithDsseEnvelope, BundleWithMessageSignature, BundleWithPublicKey, + BundleWithSingleCertificate, InclusionProof, MessageSignature, TLogEntryWithInclusionPromise, diff --git a/packages/bundle/src/validate.ts b/packages/bundle/src/validate.ts index c31dbbd5..8ecf6032 100644 --- a/packages/bundle/src/validate.ts +++ b/packages/bundle/src/validate.ts @@ -80,7 +80,12 @@ function validateBundleBase(b: ProtoBundle): string[] { // Media type validation if ( b.mediaType === undefined || - !b.mediaType.startsWith('application/vnd.dev.sigstore.bundle+json;version=') + (!b.mediaType.match( + /^application\/vnd\.dev\.sigstore\.bundle\+json;version=\d\.\d/ + ) && + !b.mediaType.match( + /^application\/vnd\.dev\.sigstore\.bundle\.v\d\.\d+json/ + )) ) { invalidValues.push('mediaType'); }