From 75ba6cde85eb565f4eb5d6156b8935778ff51f51 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Mon, 24 Jul 2023 09:40:19 -0700 Subject: [PATCH] integrate @sigstore/sign package into client (#629) Signed-off-by: Brian DeHamer --- .changeset/khaki-camels-train.md | 5 + package-lock.json | 4 + packages/client/package.json | 2 + .../client/src/__tests__/ca/format.test.ts | 38 -- .../client/src/__tests__/ca/index.test.ts | 136 ---- packages/client/src/__tests__/config.test.ts | 137 ++-- .../src/__tests__/external/error.test.ts | 52 -- .../src/__tests__/external/fulcio.test.ts | 90 --- .../src/__tests__/external/rekor.test.ts | 433 ------------ .../client/src/__tests__/external/tsa.test.ts | 67 -- .../client/src/__tests__/identity/ci.test.ts | 87 --- packages/client/src/__tests__/index.test.ts | 15 + packages/client/src/__tests__/sign.test.ts | 632 ------------------ .../src/__tests__/sigstore-utils.test.ts | 62 +- .../client/src/__tests__/sigstore.test.ts | 230 ++----- .../client/src/__tests__/tlog/format.test.ts | 247 ------- .../client/src/__tests__/tlog/index.test.ts | 305 --------- .../__tests__/tlog/verify/checkpoint.test.ts | 20 +- .../src/__tests__/tlog/verify/set.test.ts | 11 +- .../client/src/__tests__/tsa/index.test.ts | 73 -- .../src/__tests__/types/signature.test.ts | 119 +++- .../src/__tests__/types/sigstore.test.ts | 170 ----- .../client/src/__tests__/util/crypto.test.ts | 32 +- .../client/src/__tests__/util/oidc.test.ts | 56 -- .../client/src/__tests__/util/promise.test.ts | 51 -- packages/client/src/__tests__/util/ua.test.ts | 22 - packages/client/src/ca/format.ts | 38 -- packages/client/src/ca/index.ts | 75 --- packages/client/src/config.ts | 214 +++--- packages/client/src/error.ts | 41 +- packages/client/src/external/error.ts | 40 -- packages/client/src/external/fulcio.ts | 84 --- packages/client/src/external/index.ts | 19 - packages/client/src/external/rekor.ts | 165 ----- packages/client/src/external/tsa.ts | 61 -- packages/client/src/identity/ci.ts | 84 --- packages/client/src/identity/index.ts | 21 +- packages/client/src/identity/oauth.ts | 8 +- packages/client/src/identity/provider.ts | 20 - packages/client/src/index.ts | 2 +- packages/client/src/sign.ts | 159 ----- packages/client/src/sigstore-utils.ts | 76 ++- packages/client/src/sigstore.ts | 33 +- packages/client/src/tlog/format.ts | 185 ----- packages/client/src/tlog/index.ts | 118 ---- packages/client/src/tlog/verify/body.ts | 2 +- packages/client/src/tsa/index.ts | 57 -- packages/client/src/types/signature.ts | 58 +- packages/client/src/types/sigstore.ts | 169 +---- packages/client/src/util/crypto.ts | 18 +- packages/client/src/util/index.ts | 3 - packages/client/src/util/oidc.ts | 35 - packages/client/src/util/promise.ts | 34 - packages/client/src/util/ua.ts | 27 - packages/client/tsconfig.json | 2 + 55 files changed, 621 insertions(+), 4323 deletions(-) create mode 100644 .changeset/khaki-camels-train.md delete mode 100644 packages/client/src/__tests__/ca/format.test.ts delete mode 100644 packages/client/src/__tests__/ca/index.test.ts delete mode 100644 packages/client/src/__tests__/external/error.test.ts delete mode 100644 packages/client/src/__tests__/external/fulcio.test.ts delete mode 100644 packages/client/src/__tests__/external/rekor.test.ts delete mode 100644 packages/client/src/__tests__/external/tsa.test.ts delete mode 100644 packages/client/src/__tests__/identity/ci.test.ts delete mode 100644 packages/client/src/__tests__/sign.test.ts delete mode 100644 packages/client/src/__tests__/tlog/format.test.ts delete mode 100644 packages/client/src/__tests__/tlog/index.test.ts delete mode 100644 packages/client/src/__tests__/tsa/index.test.ts delete mode 100644 packages/client/src/__tests__/util/oidc.test.ts delete mode 100644 packages/client/src/__tests__/util/promise.test.ts delete mode 100644 packages/client/src/__tests__/util/ua.test.ts delete mode 100644 packages/client/src/ca/format.ts delete mode 100644 packages/client/src/ca/index.ts delete mode 100644 packages/client/src/external/error.ts delete mode 100644 packages/client/src/external/fulcio.ts delete mode 100644 packages/client/src/external/index.ts delete mode 100644 packages/client/src/external/rekor.ts delete mode 100644 packages/client/src/external/tsa.ts delete mode 100644 packages/client/src/identity/ci.ts delete mode 100644 packages/client/src/identity/provider.ts delete mode 100644 packages/client/src/sign.ts delete mode 100644 packages/client/src/tlog/format.ts delete mode 100644 packages/client/src/tlog/index.ts delete mode 100644 packages/client/src/tsa/index.ts delete mode 100644 packages/client/src/util/oidc.ts delete mode 100644 packages/client/src/util/promise.ts delete mode 100644 packages/client/src/util/ua.ts diff --git a/.changeset/khaki-camels-train.md b/.changeset/khaki-camels-train.md new file mode 100644 index 00000000..8d256ac1 --- /dev/null +++ b/.changeset/khaki-camels-train.md @@ -0,0 +1,5 @@ +--- +'sigstore': minor +--- + +Integrate `@sigstore/sign` package diff --git a/package-lock.json b/package-lock.json index 9d44131d..12cac950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13494,6 +13494,7 @@ "dependencies": { "@sigstore/bundle": "^1.0.0", "@sigstore/protobuf-specs": "^0.2.0", + "@sigstore/sign": "^0.0.0", "@sigstore/tuf": "^1.0.3", "make-fetch-happen": "^11.0.1" }, @@ -13502,6 +13503,7 @@ }, "devDependencies": { "@sigstore/jest": "^0.0.0", + "@sigstore/mock": "^0.1.1", "@sigstore/rekor-types": "^1.0.0", "@tufjs/repo-mock": "^1.1.0", "@types/make-fetch-happen": "^10.0.0" @@ -22210,8 +22212,10 @@ "requires": { "@sigstore/bundle": "^1.0.0", "@sigstore/jest": "^0.0.0", + "@sigstore/mock": "^0.1.1", "@sigstore/protobuf-specs": "^0.2.0", "@sigstore/rekor-types": "^1.0.0", + "@sigstore/sign": "^0.0.0", "@sigstore/tuf": "^1.0.3", "@tufjs/repo-mock": "^1.1.0", "@types/make-fetch-happen": "^10.0.0", diff --git a/packages/client/package.json b/packages/client/package.json index e672c9b9..44b31720 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -32,12 +32,14 @@ "devDependencies": { "@sigstore/rekor-types": "^1.0.0", "@sigstore/jest": "^0.0.0", + "@sigstore/mock": "^0.1.1", "@tufjs/repo-mock": "^1.1.0", "@types/make-fetch-happen": "^10.0.0" }, "dependencies": { "@sigstore/bundle": "^1.0.0", "@sigstore/protobuf-specs": "^0.2.0", + "@sigstore/sign": "^0.0.0", "@sigstore/tuf": "^1.0.3", "make-fetch-happen": "^11.0.1" }, diff --git a/packages/client/src/__tests__/ca/format.test.ts b/packages/client/src/__tests__/ca/format.test.ts deleted file mode 100644 index 0a560e70..00000000 --- a/packages/client/src/__tests__/ca/format.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 * as crypto from 'crypto'; -import { toCertificateRequest } from '../../ca/format'; - -describe('toCertificateRequest', () => { - const identityToken = 'a.b.c'; - const key = crypto.generateKeyPairSync('ec', { - namedCurve: 'P-256', - }).publicKey; - const challenge = Buffer.from('challenge'); - - it('returns a CertificateRequest', () => { - const cr = toCertificateRequest(identityToken, key, challenge); - - expect(cr.credentials.oidcIdentityToken).toEqual(identityToken); - expect(cr.publicKeyRequest.publicKey.algorithm).toEqual('ECDSA'); - expect(cr.publicKeyRequest.publicKey.content).toEqual( - key.export({ type: 'spki', format: 'pem' }).toString('ascii') - ); - expect(cr.publicKeyRequest.proofOfPossession).toEqual( - challenge.toString('base64') - ); - }); -}); diff --git a/packages/client/src/__tests__/ca/index.test.ts b/packages/client/src/__tests__/ca/index.test.ts deleted file mode 100644 index afae62ce..00000000 --- a/packages/client/src/__tests__/ca/index.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 crypto from 'crypto'; -import nock from 'nock'; -import { CAClient } from '../../ca'; -import { InternalError } from '../../error'; - -describe('CAClient', () => { - const baseURL = 'http://localhost:8080'; - - describe('constructor', () => { - it('should create a new instance', () => { - const client = new CAClient({ fulcioBaseURL: baseURL }); - expect(client).toBeDefined(); - }); - }); - - describe('createSigningCertificate', () => { - const subject = new CAClient({ fulcioBaseURL: baseURL }); - - const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n`; - const rootCertificate = `-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----\n`; - const certChain = [leafCertificate, rootCertificate]; - - // Request data - const identityToken = 'a.b.c'; - - const pem = `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtZO/hiYFB3WveI+iYoN4I6w17rSA -tbn02XdfIl+ZhQqUZv88dgDB86bfKyoOokA7fagAEOulkquhKKoOxdOySQ== ------END PUBLIC KEY-----`; - const publicKey = crypto.createPublicKey(pem); - const challenge = Buffer.from('challenge'); - - const certRequest = { - credentials: { - oidcIdentityToken: identityToken, - }, - publicKeyRequest: { - publicKey: { - algorithm: 'ECDSA', - content: publicKey - .export({ type: 'spki', format: 'pem' }) - .toString('ascii'), - }, - proofOfPossession: challenge.toString('base64'), - }, - }; - - describe('when Fulcio returns a valid response', () => { - beforeEach(() => { - // Mock Fulcio request - nock(baseURL) - .matchHeader('Content-Type', 'application/json') - .matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')) - .post('/api/v2/signingCert', certRequest) - .reply(201, { - signedCertificateEmbeddedSct: { - chain: { certificates: certChain }, - }, - }); - }); - - it('returns the certificate chain', async () => { - const result = await subject.createSigningCertificate( - identityToken, - publicKey, - challenge - ); - - expect(result).toEqual([leafCertificate]); - }); - }); - - describe('when Fulcio returns a valid response (with detached SCT)', () => { - beforeEach(() => { - // Mock Fulcio request - nock(baseURL) - .matchHeader('Content-Type', 'application/json') - .matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')) - .post('/api/v2/signingCert', certRequest) - .reply(201, { - signedCertificateDetachedSct: { - chain: { certificates: certChain }, - signedCertificateTimestamp: 'sct', - }, - }); - }); - - it('returns the certificate chain', async () => { - const result = await subject.createSigningCertificate( - identityToken, - publicKey, - challenge - ); - - expect(result).toEqual([leafCertificate]); - }); - }); - - describe('when Fulcio returns an error response', () => { - beforeEach(() => { - // Mock Fulcio request - nock(baseURL) - .matchHeader('Accept', 'application/pem-certificate-chain') - .matchHeader('Content-Type', 'application/json') - .matchHeader('Authorization', `Bearer ${identityToken}`) - .matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')) - .post('/api/v1/signingCert', certRequest) - .reply(500, {}); - }); - - it('throws an error', async () => { - await expect( - subject.createSigningCertificate(identityToken, publicKey, challenge) - ).rejects.toThrowWithCode( - InternalError, - 'CA_CREATE_SIGNING_CERTIFICATE_ERROR' - ); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/config.test.ts b/packages/client/src/__tests__/config.test.ts index d6600eaf..2d2ba38a 100644 --- a/packages/client/src/__tests__/config.test.ts +++ b/packages/client/src/__tests__/config.test.ts @@ -1,11 +1,99 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 { SubjectAlternativeNameType } from '@sigstore/protobuf-specs'; import { - artifactVerificationOptions, - IdentityProviderOptions, - identityProviders, + DSSEBundleBuilder, + MessageSignatureBundleBuilder, +} from '@sigstore/sign'; +import { VerifyOptions, + artifactVerificationOptions, + createBundleBuilder, } from '../config'; +describe('createBundleBuilder', () => { + describe('when the bundleType is messageSignature', () => { + const bundleType = 'messageSignature'; + + describe('when a custom signer is provided', () => { + const options = { signer: jest.fn() }; + + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, options); + expect(bundler).toBeInstanceOf(MessageSignatureBundleBuilder); + }); + }); + + describe('when a custom signer is NOT provided', () => { + describe('when a hard-coded OIDC token is provided', () => { + const options = { identityToken: 'abc' }; + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, options); + expect(bundler).toBeInstanceOf(MessageSignatureBundleBuilder); + }); + }); + + describe('when an OIDC issuer is provided', () => { + const options = { + oidcIssuer: 'https://example.com', + oidcClientID: 'abc', + }; + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, options); + expect(bundler).toBeInstanceOf(MessageSignatureBundleBuilder); + }); + }); + + describe('when no OIDC options are provided', () => { + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, {}); + expect(bundler).toBeInstanceOf(MessageSignatureBundleBuilder); + }); + }); + }); + + describe('when Rekor is disabled', () => { + const options = { tlogUpload: false }; + + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, options); + expect(bundler).toBeInstanceOf(MessageSignatureBundleBuilder); + }); + }); + + describe('when TSA is enabled', () => { + const options = { tsaServerURL: 'https://tsa.example.com' }; + + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, options); + expect(bundler).toBeInstanceOf(MessageSignatureBundleBuilder); + }); + }); + }); + + describe('when the bundleType is dsseEnvelope', () => { + const bundleType = 'dsseEnvelope'; + + it('returns a MessageSignatureBundleBuilder', () => { + const bundler = createBundleBuilder(bundleType, {}); + expect(bundler).toBeInstanceOf(DSSEBundleBuilder); + }); + }); +}); + describe('artifactVerificationOptions', () => { describe('when no certificate issuer is provided', () => { it('returns the default options', () => { @@ -179,46 +267,3 @@ describe('artifactVerificationOptions', () => { }); }); }); - -describe('identityProvider', () => { - describe('when no options are supplied', () => { - const options: IdentityProviderOptions = {}; - - it('returns the static IdentityProvider', async () => { - const result = identityProviders(options); - expect(result).toBeDefined(); - expect(result).toHaveLength(1); - - const { getToken } = result[0]; - await expect(getToken()).rejects.toThrowError(); - }); - }); - - describe('when a static token is provided', () => { - const options: IdentityProviderOptions = { - identityToken: 'token', - }; - - it('returns the CI IdentityProvider', async () => { - const result = identityProviders(options); - expect(result).toBeDefined(); - expect(result).toHaveLength(1); - - const { getToken } = result[0]; - await expect(getToken()).resolves.toEqual(options.identityToken); - }); - }); - - describe('when OAuth config options are provided', () => { - const options: IdentityProviderOptions = { - oidcIssuer: 'https://example.com', - oidcClientID: 'client-id', - }; - - it('returns both the CI and OAuth IdentityProviders', async () => { - const result = identityProviders(options); - expect(result).toBeDefined(); - expect(result).toHaveLength(2); - }); - }); -}); diff --git a/packages/client/src/__tests__/external/error.test.ts b/packages/client/src/__tests__/external/error.test.ts deleted file mode 100644 index 90c2093f..00000000 --- a/packages/client/src/__tests__/external/error.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 fetch from 'make-fetch-happen'; -import { checkStatus, HTTPError } from '../../external/error'; - -type Response = Awaited>; - -describe('checkStatus', () => { - describe('when the response is OK', () => { - const response: Response = { - status: 200, - statusText: 'OK', - ok: true, - } as Response; - - it('returns the response', () => { - expect(checkStatus(response)).toEqual(response); - }); - }); - - describe('when the response is not OK', () => { - const response: Response = { - status: 404, - statusText: 'Not Found', - ok: false, - } as Response; - - it('throws an error', () => { - try { - checkStatus(response); - fail('Expected an error to be thrown'); - } catch (e) { - if (!(e instanceof HTTPError)) fail('Expected an HTTPError'); - expect(e.message).toEqual('HTTP Error: 404 Not Found'); - expect(e.statusCode).toEqual(404); - } - }); - }); -}); diff --git a/packages/client/src/__tests__/external/fulcio.test.ts b/packages/client/src/__tests__/external/fulcio.test.ts deleted file mode 100644 index 7c054166..00000000 --- a/packages/client/src/__tests__/external/fulcio.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; - -import { - Fulcio, - SigningCertificateRequest, - SigningCertificateResponse, -} from '../../external/fulcio'; - -describe('Fulcio', () => { - const baseURL = 'http://localhost:8000'; - const subject = new Fulcio({ baseURL }); - - it('should create an instance', () => { - expect(subject).toBeTruthy(); - }); - - describe('#createSigningCertificate', () => { - const identityToken = `a.b.c`; - const certRequest = { - credentials: { - oidcIdentityToken: identityToken, - }, - publicKeyRequest: { - publicKey: { - algorithm: 'ECDSA', - content: - 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', - }, - proofOfPossession: 'MEUCIEntw6QwoyDHb52HUIUVDnqFeGBI4oaCBMCoOtcbVKQ=', - }, - } satisfies SigningCertificateRequest; - - describe('when the certificate request is valid', () => { - const certificateResponse: SigningCertificateResponse = { - signedCertificateEmbeddedSct: { - chain: { - certificates: [ - `-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----`, - ], - }, - }, - }; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Content-Type', 'application/json') - .matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')) - .post('/api/v2/signingCert', certRequest) - .reply(200, certificateResponse); - }); - - it('returns the signing certificate', async () => { - const result = await subject.createSigningCertificate(certRequest); - expect(result).toEqual(certificateResponse); - }); - }); - - describe('when the certificate request is invalid', () => { - const responseBody = { - code: 400, - message: 'Invalid certificate request', - }; - - beforeEach(() => { - nock(baseURL).post('/api/v2/signingCert').reply(400, responseBody); - }); - - it('returns an error', async () => { - await expect( - subject.createSigningCertificate(certRequest) - ).rejects.toThrow('HTTP Error: 400 Bad Request'); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/external/rekor.test.ts b/packages/client/src/__tests__/external/rekor.test.ts deleted file mode 100644 index e258ac59..00000000 --- a/packages/client/src/__tests__/external/rekor.test.ts +++ /dev/null @@ -1,433 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; -import { Rekor } from '../../external'; - -import type { ProposedHashedRekordEntry } from '../../external/rekor'; - -describe('Rekor', () => { - const baseURL = 'http://localhost:8080'; - const subject = new Rekor({ baseURL }); - - it('should create an instance', () => { - expect(subject).toBeTruthy(); - }); - - describe('#createEntry', () => { - const proposedEntry: ProposedHashedRekordEntry = { - apiVersion: '0.0.1', - kind: 'hashedrekord', - spec: { - data: { - hash: { - algorithm: 'sha256', - value: - '1c025a6e48ceb8bf10e01b367089732326eabe3541d03d348724c79040382c65', - }, - }, - signature: { - content: - 'MEUCIDB2SWDabztSC8RrlfRCWUf04LBN0E2CEwiDZJLacDS8AiEA3bQHMBpodxA3dvJ+JK1SALkuzju/w4oCg3S89c8CtN8=', - publicKey: { - content: - 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K', - }, - }, - }, - }; - - describe('when the entry is successfully added', () => { - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - const responseBody = { - [uuid]: { - body: 'Zm9vCg==', - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - }, - }, - }; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')) - .post('/api/v1/log/entries') - .reply(201, responseBody); - }); - - it('returns the new entry', async () => { - const result = await subject.createEntry(proposedEntry); - - expect(result.uuid).toBe(uuid); - expect(result.body).toBe(responseBody[uuid].body); - expect(result.integratedTime).toBe(responseBody[uuid].integratedTime); - expect(result.logID).toBe(responseBody[uuid].logID); - expect(result.logIndex).toBe(responseBody[uuid].logIndex); - expect(result.verification).toEqual(responseBody[uuid].verification); - }); - }); - - describe('when a matching entry already exists', () => { - const responseBody = { - code: 409, - message: 'An equivalent entry already exists', - }; - - beforeEach(() => { - nock(baseURL).post('/api/v1/log/entries').reply(409, responseBody); - }); - - it('returns an error', async () => { - await expect(subject.createEntry(proposedEntry)).rejects.toThrow( - 'HTTP Error: 409 Conflict' - ); - }); - }); - }); - - describe('#getEntry', () => { - describe('when the entry exists', () => { - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - const responseBody = { - [uuid]: { - body: 'Zm9vCg==', - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - }, - }, - }; - - beforeEach(() => { - nock(baseURL) - .get(`/api/v1/log/entries/${uuid}`) - .reply(200, responseBody); - }); - - it('returns the requested entry', async () => { - const result = await subject.getEntry(uuid); - - expect(result.uuid).toBe(uuid); - expect(result.body).toBe(responseBody[uuid].body); - expect(result.integratedTime).toBe(responseBody[uuid].integratedTime); - expect(result.logID).toBe(responseBody[uuid].logID); - expect(result.logIndex).toBe(responseBody[uuid].logIndex); - expect(result.verification).toEqual(responseBody[uuid].verification); - }); - }); - - describe('when the entry does not exist', () => { - const responseBody = { - code: 404, - message: 'Entry not found', - }; - - beforeEach(() => { - nock(baseURL).get('/api/v1/log/entries/foo').reply(404, responseBody); - }); - - it('returns an error', async () => { - await expect(subject.getEntry('foo')).rejects.toThrow( - 'HTTP Error: 404 Not Found' - ); - }); - }); - - describe('when there are multiple entries in the response', () => { - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - const responseBody = { - [uuid]: { body: 'foo' }, - '456': { body: 'bar' }, - }; - - beforeEach(() => { - nock(baseURL) - .get(`/api/v1/log/entries/${uuid}`) - .reply(200, responseBody); - }); - - it('returns an error', async () => { - await expect(subject.getEntry(uuid)).rejects.toThrow( - 'Received multiple entries in Rekor response' - ); - }); - }); - }); - - describe('#searchIndex', () => { - describe('when matching entries exist', () => { - const sha = - 'sha256:04c0c13721a28c60f38daf09a05326c301a2cf57ad2beb953eb29d61383db47e'; - const responseBody = ['deadbeef', 'abcd1234']; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/index/retrieve', { hash: sha }) - .reply(200, responseBody); - }); - - it('returns matching entries', async () => { - const response = await subject.searchIndex({ hash: sha }); - expect(response).toEqual(responseBody); - }); - }); - - describe('when no matching entries exist', () => { - const responseBody = [] as string[]; - - beforeEach(() => { - nock(baseURL).post('/api/v1/index/retrieve').reply(200, responseBody); - }); - - it('returns an empty array', async () => { - const sha = - 'sha256:04c0c13721a28c60f38daf09a05326c301a2cf57ad2beb953eb29d61383db47e'; - - const response = await subject.searchIndex({ hash: sha }); - - expect(response).toEqual([]); - }); - }); - - describe('when an error occurs', () => { - const responseBody = { - code: 422, - message: 'Invalid query', - }; - - beforeEach(() => { - nock(baseURL).get('/api/v1/log/entries/foo').reply(422, responseBody); - }); - - it('returns an error', async () => { - await expect(subject.getEntry('foo')).rejects.toThrow( - 'HTTP Error: 422 Unprocessable Entity' - ); - }); - }); - }); - - describe('#searchLog', () => { - const searchLogURL = '/api/v1/log/entries/retrieve'; - - const searchLogEntryUUID = - '5c1419ace1915869eef4426701d0763232da44d453e31ffc06d60ea61de36e25'; - const searchLogIndex = 3244486; - - const searchLogEntry = { - kind: 'hashedrekord', - apiVersion: '0.0.1', - spec: { - data: { - hash: { - value: - 'c0afcf83aee6ee83b2ecfa226db62853fd77bcaf550e1b4bf277acbe38a67bca', - algorithm: 'sha256', - }, - }, - signature: { - content: - 'MEYCIQDXxhsvLwEXG9HFrYyF/FgGbo0Ja25ADoaUhs+4DqaCQgIhAJziarK6mxXB5coQu75jpHSbXAGQyDE8tu8sTrcXndGV', - publicKey: { - content: - 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNvRENDQWlhZ0F3SUJBZ0lVZTR4VXljdWRlbW9sVG5tenpCVnM0MVNNQU9Jd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09ESXlNVGd3T1RBeVdoY05Nakl3T0RJeU1UZ3hPVEF5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVDV3dEM1F6WUwwN2JDY0JrZURWU3hUOE9FQnM1MnJXcS9aZGYKMEZFSlZneng3cGpyZFFmOExBOEp6dkZkVnhKeXBGSDVVcythMkRRRWVUdytlZUNRcEtPQ0FVVXdnZ0ZCTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVodUt1CklsWFVHa3VrM2prbWJ3SlhpeDh1cFdrd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0h3WURWUjBSQVFIL0JCVXdFNEVSWW5KcFlXNUFaR1ZvWVcxbGNpNWpiMjB3TEFZS0t3WUJCQUdEdnpBQgpBUVFlYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4dloybHVMMjloZFhSb01JR0tCZ29yQmdFRUFkWjVBZ1FDCkJId0VlZ0I0QUhZQUNHQ1M4Q2hTLzJoRjBkRnJKNFNjUldjWXJCWTl3empTYmVhOElnWTJiM0lBQUFHQ3hyNWkKZ3dBQUJBTUFSekJGQWlCOEFPQ2t2VnA1bGRhVkVuMk96NlAzUE5YOGd2cGt2WmlqUEZ2SHY1ZHMvZ0loQUtWNAplK1lpKzVpa2VCYTU3UlV1alMrV3RIdklSeEJBVVRNK2hTOE5XWHFjTUFvR0NDcUdTTTQ5QkFNREEyZ0FNR1VDCk1RQ3JIK3krYkhDeCtBR0lsc3JOeXhOa2VsS3hSNDAwbEpzd1FBVDVOVk5aUmVSUDBhbDJKN1dmSHhyZVhqS0oKT2ZBQ01FTnpzUjhIeVZpOEJuZDBPd1YzOGswMGRnZW9PTGoycTgwZVdEbWJWN1ptVklyeDRpc2M4aFVnNHFHLwpKN2x4TEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==', - }, - }, - }, - }; - - const searchLogResponseBody = [ - { - [searchLogEntryUUID]: { - body: 'eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjMGFmY2Y4M2FlZTZlZTgzYjJlY2ZhMjI2ZGI2Mjg1M2ZkNzdiY2FmNTUwZTFiNGJmMjc3YWNiZTM4YTY3YmNhIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURYeGhzdkx3RVhHOUhGcll5Ri9GZ0dibzBKYTI1QURvYVVocys0RHFhQ1FnSWhBSnppYXJLNm14WEI1Y29RdTc1anBIU2JYQUdReURFOHR1OHNUcmNYbmRHViIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnZSRU5EUVdsaFowRjNTVUpCWjBsVlpUUjRWWGxqZFdSbGJXOXNWRzV0ZW5wQ1ZuTTBNVk5OUVU5SmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1hsTlZHZDNUMVJCZVZkb1kwNU5ha2wzVDBSSmVVMVVaM2hQVkVGNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZEVjNkRU0xRjZXVXd3TjJKRFkwSnJaVVJXVTNoVU9FOUZRbk0xTW5KWGNTOWFaR1lLTUVaRlNsWm5lbmczY0dweVpGRm1PRXhCT0VwNmRrWmtWbmhLZVhCR1NEVlZjeXRoTWtSUlJXVlVkeXRsWlVOUmNFdFBRMEZWVlhkblowWkNUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZvZFV0MUNrbHNXRlZIYTNWck0ycHJiV0ozU2xocGVEaDFjRmRyZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoM1dVUldVakJTUVZGSUwwSkNWWGRGTkVWU1dXNUtjRmxYTlVGYVIxWnZXVmN4YkdOcE5XcGlNakIzVEVGWlMwdDNXVUpDUVVkRWRucEJRZ3BCVVZGbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKNGRsb3liSFZNTWpsb1pGaFNiMDFKUjB0Q1oyOXlRbWRGUlVGa1dqVkJaMUZEQ2tKSWQwVmxaMEkwUVVoWlFVTkhRMU00UTJoVEx6Sm9SakJrUm5KS05GTmpVbGRqV1hKQ1dUbDNlbXBUWW1WaE9FbG5XVEppTTBsQlFVRkhRM2h5TldrS1ozZEJRVUpCVFVGU2VrSkdRV2xDT0VGUFEydDJWbkExYkdSaFZrVnVNazk2TmxBelVFNVlPR2QyY0d0MldtbHFVRVoyU0hZMVpITXZaMGxvUVV0V05BcGxLMWxwS3pWcGEyVkNZVFUzVWxWMWFsTXJWM1JJZGtsU2VFSkJWVlJOSzJoVE9FNVhXSEZqVFVGdlIwTkRjVWRUVFRRNVFrRk5SRUV5WjBGTlIxVkRDazFSUTNKSUsza3JZa2hEZUN0QlIwbHNjM0pPZVhoT2EyVnNTM2hTTkRBd2JFcHpkMUZCVkRWT1ZrNWFVbVZTVURCaGJESktOMWRtU0hoeVpWaHFTMG9LVDJaQlEwMUZUbnB6VWpoSWVWWnBPRUp1WkRCUGQxWXpPR3N3TUdSblpXOVBUR295Y1Rnd1pWZEViV0pXTjFwdFZrbHllRFJwYzJNNGFGVm5OSEZITHdwS04yeDRURUU5UFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19', - integratedTime: 1661191742, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: searchLogIndex, - verification: { - inclusionProof: { - hashes: [ - 'a95557f9140686a8232d87372d63f77d238b9b592546793122fee610a98bdc4a', - '755c2d4215b65ff1e89922eaf3d0b0c2a256bb73f01c9987d0baf7da6bd05ce9', - 'a40ef3cc1d74b6c51d7f10bd27c3fdaf32a0ef5ff3b7d1a920e2cab3fd7b15fa', - 'f13869de51fd02503c70fc7b60a0a45f63e5ac7f5e09e1f8481fb3c27dde3787', - '8f940f0ea5c5a2f851c0bb5a736a1be56a2fb823c5e805521ba7e354d0046e4a', - '7158f9f9011dae7f3ac2bd2ca20b3dc8994498ba16cf58d5be94586db4b7049b', - '7905df410a38ed98d7d3f84a2ec4aea550e1b958ccd767c0daefcee7a2b9037a', - '497906c322703a983d84cb7618b74ba0804b245c63ef0a276cf1db8b9cee487a', - 'a547f0e05745e0594c9569c04e5d7eff0aed47f88eaf50e373b051ed465bd61d', - '0d7e8078654eb19540634cb8e1c5a864424e4b03e65f7fb139746a279fd0b3cc', - 'a64451821ac29b58a6ced95c615e426fc339d3607b0001c4298c474b7fdaed59', - 'b71e915675080279e58cdd0daa859a47da7e824ffc55df612becfcfb2fd9aae8', - '6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b', - 'efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b', - ], - logIndex: searchLogIndex, - rootHash: - 'ba3be49521f0cf6eed57b0e1ffe4b47a2f5b96bf43e9b6eb5ec3130f80e53c8d', - treeSize: 3244712, - }, - signedEntryTimestamp: - 'MEUCIQDLNuDkFSpqhFBpp3o1ApYu4WH0f4tP2OWZC/o+f79cQAIgFC6uELUYcUpPNACyu89pNynxmmrWMdzu08CNNYl83/c=', - }, - }, - }, - ]; - - describe('when matching by entry', () => { - const entries = [searchLogEntry]; - const responseBody = searchLogResponseBody; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post(searchLogURL, { entries }) - .reply(200, responseBody); - }); - - it('returns matching entries', async () => { - const response = await subject.searchLog({ - entries: entries as ProposedHashedRekordEntry[], - }); - - expect(response).toHaveLength(1); - expect(response[0].uuid).toEqual( - Object.keys(searchLogResponseBody[0])[0] - ); - expect(response[0].logIndex).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].logIndex - ); - expect(response[0].body).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].body - ); - expect(response[0].integratedTime).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].integratedTime - ); - expect(response[0].logID).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].logID - ); - expect(response[0].verification).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].verification - ); - }); - }); - - describe('when matching by log index', () => { - const logIndexes = [searchLogIndex]; - const responseBody = searchLogResponseBody; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post(searchLogURL, { logIndexes }) - .reply(200, responseBody); - }); - - it('returns matching entries', async () => { - const response = await subject.searchLog({ logIndexes }); - expect(response).toHaveLength(1); - expect(response[0].uuid).toEqual( - Object.keys(searchLogResponseBody[0])[0] - ); - expect(response[0].logIndex).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].logIndex - ); - expect(response[0].body).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].body - ); - expect(response[0].integratedTime).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].integratedTime - ); - expect(response[0].logID).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].logID - ); - expect(response[0].verification).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].verification - ); - }); - }); - - describe('when matching by UUID', () => { - const entryUUIDs = [searchLogEntryUUID]; - - const responseBody = searchLogResponseBody; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post(searchLogURL, { entryUUIDs }) - .reply(200, responseBody); - }); - - it('returns matching entries', async () => { - const response = await subject.searchLog({ entryUUIDs }); - expect(response).toHaveLength(1); - expect(response[0].uuid).toEqual( - Object.keys(searchLogResponseBody[0])[0] - ); - expect(response[0].logIndex).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].logIndex - ); - expect(response[0].body).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].body - ); - expect(response[0].integratedTime).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].integratedTime - ); - expect(response[0].logID).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].logID - ); - expect(response[0].verification).toEqual( - searchLogResponseBody[0][searchLogEntryUUID].verification - ); - }); - }); - - describe('when no matching entries exist', () => { - const responseBody = [] as string[]; - - beforeEach(() => { - nock(baseURL).post(searchLogURL).reply(200, responseBody); - }); - - it('returns an empty array', async () => { - const entryUUIDs = [searchLogEntryUUID]; - - const response = await subject.searchLog({ entryUUIDs }); - - expect(response).toEqual([]); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/external/tsa.test.ts b/packages/client/src/__tests__/external/tsa.test.ts deleted file mode 100644 index 2b01e062..00000000 --- a/packages/client/src/__tests__/external/tsa.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2023 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; -import { TimestampAuthority, TimestampRequest } from '../../external/tsa'; - -describe('TimestampAuthority', () => { - const baseURL = 'http://localhost:8000'; - const subject = new TimestampAuthority({ baseURL }); - - it('should create an instance', () => { - expect(subject).toBeTruthy(); - }); - - describe('#createTimestamp', () => { - const timestampRequest = { - artifactHash: 'artifacthash', - hashAlgorithm: 'sha256', - } satisfies TimestampRequest; - - describe('when the timestamp request is valid', () => { - const timestamp = Buffer.from('timestamp'); - - beforeEach(() => { - nock(baseURL) - .matchHeader('Content-Type', 'application/json') - .matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')) - .post('/api/v1/timestamp', timestampRequest) - .reply(200, timestamp); - }); - - it('returns the timestamp', async () => { - const result = await subject.createTimestamp(timestampRequest); - expect(result).toEqual(timestamp); - }); - }); - - describe('when the timestamp request is invalid', () => { - const responseBody = { - code: 400, - message: 'Error generating timestamp response', - }; - - beforeEach(() => { - nock(baseURL).post('/api/v1/timestamp').reply(400, responseBody); - }); - - it('returns an error', async () => { - await expect(subject.createTimestamp(timestampRequest)).rejects.toThrow( - 'HTTP Error: 400 Bad Request' - ); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/identity/ci.test.ts b/packages/client/src/__tests__/identity/ci.test.ts deleted file mode 100644 index 17b4a63c..00000000 --- a/packages/client/src/__tests__/identity/ci.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; -import { CIContextProvider } from '../../identity/ci'; - -describe('CIContextProvider', () => { - const subject = new CIContextProvider('sigstore'); - - it('creates an instance', () => { - expect(subject).toBeTruthy(); - }); - - describe('#getToken', () => { - describe('when the GHA environment variables are set', () => { - const requestURL = 'http://localhost:8080'; - const requestToken = 'abc123'; - - const oidcToken = 'x.y.z'; - - beforeEach(() => { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = requestURL; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = requestToken; - - nock(requestURL) - .get('/') - .query({ audience: 'sigstore' }) - .reply(200, { value: oidcToken }); - }); - - afterEach(() => { - delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL; - delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - }); - - it('returns the token', async () => { - const token = await subject.getToken(); - expect(token).toBe(oidcToken); - }); - }); - - describe('when the GHA environment variables are NOT set', () => { - it('returns undefined', async () => { - const token = subject.getToken(); - await expect(token).rejects.toBe('CI: no tokens available'); - }); - }); - }); - - describe('#getEnv', () => { - describe('when the sigstore environment variables are set', () => { - const token = 'hunter2'; - - beforeEach(() => { - process.env.SIGSTORE_ID_TOKEN = token; - }); - - afterEach(() => { - delete process.env.SIGSTORE_ID_TOKEN; - }); - - it('returns the token', async () => { - const token = await subject.getToken(); - expect(token).toBe(token); - }); - }); - - describe('when the sigstore environment variables are NOT set', () => { - it('returns undefined', async () => { - const token = subject.getToken(); - await expect(token).rejects.toBe('CI: no tokens available'); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/index.test.ts b/packages/client/src/__tests__/index.test.ts index b214f588..e749501b 100644 --- a/packages/client/src/__tests__/index.test.ts +++ b/packages/client/src/__tests__/index.test.ts @@ -1,3 +1,18 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 { IdentityProvider, sigstore } from '..'; describe('sigstore', () => { diff --git a/packages/client/src/__tests__/sign.test.ts b/packages/client/src/__tests__/sign.test.ts deleted file mode 100644 index 3341cdb9..00000000 --- a/packages/client/src/__tests__/sign.test.ts +++ /dev/null @@ -1,632 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; -import { CAClient } from '../ca'; -import { InternalError } from '../error'; -import { Signer } from '../sign'; -import { TLogClient } from '../tlog'; -import { TSAClient } from '../tsa'; -import { SignatureMaterial, SignerFunc } from '../types/signature'; -import { HashAlgorithm } from '../types/sigstore'; -import { pem } from '../util'; - -import type { LogEntry } from '@sigstore/rekor-types'; - -describe('Signer', () => { - const fulcioBaseURL = 'http://localhost:8001'; - const rekorBaseURL = 'http://localhost:8002'; - const tsaBaseURL = 'http://localhost:8003'; - const jwtPayload = { - iss: 'https://example.com', - sub: 'foo@bar.com', - }; - const jwt = `.${Buffer.from(JSON.stringify(jwtPayload)).toString('base64')}.`; - - const ca = new CAClient({ fulcioBaseURL }); - const tlog = new TLogClient({ rekorBaseURL }); - const tsa = new TSAClient({ tsaBaseURL }); - const idp = { getToken: () => Promise.resolve(jwt) }; - - const subject = new Signer({ - ca, - tlog, - identityProviders: [idp], - tlogUpload: true, - }); - - it('should create an instance', () => { - expect(subject).toBeTruthy(); - }); - - describe('#signBlob', () => { - // Input - const payload = Buffer.from('Hello, world!'); - - describe('when using the default signer', () => { - describe('when no identity provider returns a token', () => { - const noIDTokenSubject = new Signer({ - ca, - tlog, - identityProviders: [], - }); - - it('throws an error', async () => { - await expect(noIDTokenSubject.signBlob(payload)).rejects.toThrow( - 'Identity token providers failed: ' - ); - }); - }); - - describe('when Fulcio returns an error', () => { - beforeEach(() => { - nock(fulcioBaseURL).post('/api/v1/signingCert').reply(500, {}); - }); - - it('returns an error', async () => { - await expect(subject.signBlob(payload)).rejects.toThrowWithCode( - InternalError, - 'CA_CREATE_SIGNING_CERTIFICATE_ERROR' - ); - }); - }); - - describe('when Fulcio returns successfully', () => { - // Fulcio output - const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----`; - const rootCertificate = `-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----`; - - beforeEach(() => { - // Mock Fulcio request - nock(fulcioBaseURL) - .matchHeader('Content-Type', 'application/json') - .post('/api/v2/signingCert', { - credentials: { - oidcIdentityToken: jwt, - }, - publicKeyRequest: { - publicKey: { - algorithm: 'ECDSA', - content: /.+/i, - }, - proofOfPossession: /.+/i, - }, - }) - .reply(200, { - signedCertificateEmbeddedSct: { - chain: { certificates: [leafCertificate, rootCertificate] }, - }, - }); - }); - - describe('when tlog upload is enabled', () => { - describe('when Rekor returns successfully', () => { - // Rekor output - const signature = 'ABC123'; - const b64Cert = Buffer.from(leafCertificate).toString('base64'); - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - - const signatureBundle = { - kind: 'hashedrekord', - apiVersion: '0.0.1', - spec: { - signature: { - content: signature, - publicKey: { content: b64Cert }, - }, - }, - }; - - const rekorEntry = { - [uuid]: { - body: Buffer.from(JSON.stringify(signatureBundle)).toString( - 'base64' - ), - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - inclusionProof: { - hashes: ['deadbeef', 'feedface'], - logIndex: 12345, - rootHash: 'fee1dead', - treeSize: 12346, - checkpoint: 'checkpoint', - }, - }, - }, - } satisfies LogEntry; - - beforeEach(() => { - // Mock Rekor request - nock(rekorBaseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries') - .reply(201, rekorEntry); - }); - - it('returns a signature bundle', async () => { - const bundle = await subject.signBlob(payload); - - expect(bundle).toBeTruthy(); - expect(bundle.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - - if (bundle.content?.$case === 'messageSignature') { - const ms = bundle.content.messageSignature; - expect(ms.messageDigest).toBeTruthy(); - expect(ms.messageDigest?.algorithm).toEqual( - HashAlgorithm.SHA2_256 - ); - expect(ms.messageDigest?.digest).toBeTruthy(); - expect(ms.signature).toBeTruthy(); - } else { - fail('Expected messageSignature'); - } - - // Verification material - if ( - bundle.verificationMaterial?.content?.$case === - 'x509CertificateChain' - ) { - const chain = - bundle.verificationMaterial.content.x509CertificateChain; - expect(chain).toBeTruthy(); - expect(chain.certificates).toHaveLength(1); - expect(chain.certificates[0].rawBytes).toEqual( - pem.toDER(leafCertificate) - ); - } else { - fail('Expected x509CertificateChain'); - } - - expect( - bundle.verificationMaterial?.timestampVerificationData - ).toBeUndefined(); - expect(bundle.verificationMaterial?.tlogEntries).toHaveLength(1); - - const tlog = bundle.verificationMaterial?.tlogEntries[0]; - expect(tlog?.inclusionPromise).toBeTruthy(); - expect(tlog?.inclusionPromise?.signedEntryTimestamp).toBeTruthy(); - expect( - tlog?.inclusionPromise?.signedEntryTimestamp.toString('base64') - ).toEqual(rekorEntry[uuid].verification.signedEntryTimestamp); - expect(tlog?.integratedTime).toEqual( - rekorEntry[uuid].integratedTime.toString() - ); - expect(tlog?.logId).toBeTruthy(); - expect(tlog?.logId?.keyId).toBeTruthy(); - expect(tlog?.logId?.keyId.toString('hex')).toEqual( - rekorEntry[uuid].logID - ); - expect(tlog?.logIndex).toEqual( - rekorEntry[uuid].logIndex.toString() - ); - expect(tlog?.inclusionProof?.checkpoint?.envelope).toEqual( - rekorEntry[uuid].verification.inclusionProof.checkpoint - ); - expect(tlog?.inclusionProof?.hashes).toHaveLength(2); - expect(tlog?.inclusionProof?.hashes[0]).toEqual( - Buffer.from( - rekorEntry[uuid].verification.inclusionProof.hashes[0], - 'hex' - ) - ); - expect(tlog?.inclusionProof?.hashes[1]).toEqual( - Buffer.from( - rekorEntry[uuid].verification.inclusionProof.hashes[1], - 'hex' - ) - ); - expect(tlog?.inclusionProof?.logIndex).toEqual( - rekorEntry[uuid].verification.inclusionProof.logIndex.toString() - ); - expect(tlog?.inclusionProof?.rootHash).toEqual( - Buffer.from( - rekorEntry[uuid].verification.inclusionProof.rootHash, - 'hex' - ) - ); - expect(tlog?.inclusionProof?.treeSize).toEqual( - rekorEntry[uuid].verification.inclusionProof.treeSize.toString() - ); - expect(tlog?.kindVersion?.kind).toEqual('hashedrekord'); - expect(tlog?.kindVersion?.version).toEqual('0.0.1'); - }); - }); - - describe('when Rekor returns an error', () => { - beforeEach(() => { - nock(rekorBaseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries') - .reply(500, {}); - }); - - it('returns an error', async () => { - await expect(subject.signBlob(payload)).rejects.toThrowWithCode( - InternalError, - 'TLOG_CREATE_ENTRY_ERROR' - ); - }); - }); - }); - - describe('when tlog upload is disabled', () => { - const subject = new Signer({ - ca, - tlog, - identityProviders: [idp], - tlogUpload: false, - }); - - it('returns a signature bundle', async () => { - const bundle = await subject.signBlob(payload); - - expect(bundle).toBeTruthy(); - expect(bundle.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - - if (bundle.content?.$case === 'messageSignature') { - const ms = bundle.content.messageSignature; - expect(ms.messageDigest).toBeTruthy(); - expect(ms.messageDigest?.algorithm).toEqual( - HashAlgorithm.SHA2_256 - ); - expect(ms.messageDigest?.digest).toBeTruthy(); - expect(ms.signature).toBeTruthy(); - } else { - fail('Expected messageSignature'); - } - - // Verification material - if ( - bundle.verificationMaterial?.content?.$case === - 'x509CertificateChain' - ) { - const chain = - bundle.verificationMaterial.content.x509CertificateChain; - expect(chain).toBeTruthy(); - expect(chain.certificates).toHaveLength(1); - expect(chain.certificates[0].rawBytes).toEqual( - pem.toDER(leafCertificate) - ); - } else { - fail('Expected x509CertificateChain'); - } - - expect( - bundle.verificationMaterial?.timestampVerificationData - ).toBeUndefined(); - expect(bundle.verificationMaterial?.tlogEntries).toHaveLength(0); - }); - }); - - describe('when timestamping is enabled', () => { - const timestamp = Buffer.from('timestamp'); - - const subject = new Signer({ - ca, - tlog, - tsa, - identityProviders: [idp], - tlogUpload: false, - }); - - beforeEach(() => { - nock(tsaBaseURL) - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/timestamp') - .reply(200, timestamp); - }); - - it('returns a signature bundle', async () => { - const bundle = await subject.signBlob(payload); - expect(bundle).toBeTruthy(); - - expect( - bundle.verificationMaterial?.timestampVerificationData - ).toBeTruthy(); - expect( - bundle.verificationMaterial?.timestampVerificationData - ?.rfc3161Timestamps - ).toHaveLength(1); - expect( - bundle.verificationMaterial?.timestampVerificationData - ?.rfc3161Timestamps[0].signedTimestamp - ).toEqual(timestamp); - }); - }); - }); - }); - - describe('when using a custom signer', () => { - const sigMaterial: SignatureMaterial = { - signature: Buffer.from('ABC123'), - certificates: undefined, - key: { - id: 'key-id', - value: 'key-value', - }, - }; - - const signer = jest.fn().mockResolvedValueOnce(sigMaterial) as SignerFunc; - - beforeEach(() => { - nock(rekorBaseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries') - .reply(500, {}); - }); - - it('invokes the custom signer', async () => { - const s = new Signer({ - ca, - tlog, - identityProviders: [], - signer, - }); - - await expect(s.signBlob(payload)).rejects.toThrowWithCode( - InternalError, - 'TLOG_CREATE_ENTRY_ERROR' - ); - expect(signer).toHaveBeenCalledWith(payload); - }); - }); - }); - - describe('#signAttestation', () => { - // Input - const payload = Buffer.from('Hello, world!'); - const payloadType = 'text/plain'; - - describe('when Fulcio returns successfully', () => { - // Fulcio output - const certificate = `-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----`; - - beforeEach(() => { - // Mock Fulcio request - nock(fulcioBaseURL) - .matchHeader('Content-Type', 'application/json') - .post('/api/v2/signingCert', { - credentials: { - oidcIdentityToken: jwt, - }, - publicKeyRequest: { - publicKey: { - algorithm: 'ECDSA', - content: /.+/i, - }, - proofOfPossession: /.+/i, - }, - }) - .reply(200, { - signedCertificateEmbeddedSct: { - chain: { certificates: [certificate] }, - }, - }); - }); - - describe('when tlog upload is enabled', () => { - describe('when Rekor returns successfully', () => { - // Rekor output - const signature = 'ABC123'; - const b64Cert = Buffer.from(certificate).toString('base64'); - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - - const signatureBundle = { - kind: 'intoto', - apiVersion: '0.0.2', - spec: { - signature: { - content: signature, - publicKey: { content: b64Cert }, - }, - }, - }; - - const rekorEntry = { - [uuid]: { - body: Buffer.from(JSON.stringify(signatureBundle)).toString( - 'base64' - ), - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - }, - }, - }; - - beforeEach(() => { - // Mock Rekor request - nock(rekorBaseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries') - .reply(201, rekorEntry); - }); - - it('returns a signature bundle', async () => { - const bundle = await subject.signAttestation(payload, payloadType); - - expect(bundle).toBeTruthy(); - expect(bundle.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - - if (bundle.content?.$case === 'dsseEnvelope') { - const env = bundle.content.dsseEnvelope; - expect(env.payloadType).toEqual(payloadType); - expect(env.payload.toString('base64')).toEqual( - payload.toString('base64') - ); - expect(env.signatures).toHaveLength(1); - expect(env.signatures[0].keyid).toEqual(''); - } else { - fail('Expected dsseEnvelope'); - } - - // Verification material - if ( - bundle.verificationMaterial?.content?.$case === - 'x509CertificateChain' - ) { - const chain = - bundle.verificationMaterial.content.x509CertificateChain; - expect(chain).toBeTruthy(); - expect(chain.certificates).toHaveLength(1); - expect(chain.certificates[0].rawBytes).toEqual( - pem.toDER(certificate) - ); - } else { - fail('Expected x509CertificateChain'); - } - - expect( - bundle.verificationMaterial?.timestampVerificationData - ).toBeUndefined(); - expect(bundle.verificationMaterial?.tlogEntries).toHaveLength(1); - - const tlog = bundle.verificationMaterial?.tlogEntries[0]; - expect(tlog?.inclusionPromise).toBeTruthy(); - expect(tlog?.inclusionPromise?.signedEntryTimestamp).toBeTruthy(); - expect( - tlog?.inclusionPromise?.signedEntryTimestamp.toString('base64') - ).toEqual(rekorEntry[uuid].verification.signedEntryTimestamp); - expect(tlog?.integratedTime).toEqual( - rekorEntry[uuid].integratedTime.toString() - ); - expect(tlog?.logId).toBeTruthy(); - expect(tlog?.logId?.keyId).toBeTruthy(); - expect(tlog?.logId?.keyId.toString('hex')).toEqual( - rekorEntry[uuid].logID - ); - expect(tlog?.logIndex).toEqual( - rekorEntry[uuid].logIndex.toString() - ); - expect(tlog?.inclusionProof).toBeFalsy(); - expect(tlog?.kindVersion?.kind).toEqual('intoto'); - expect(tlog?.kindVersion?.version).toEqual('0.0.2'); - }); - }); - }); - - describe('when tlog upload is disabled', () => { - const subject = new Signer({ - ca, - tlog, - identityProviders: [idp], - tlogUpload: false, - }); - - it('returns a signature bundle', async () => { - const bundle = await subject.signAttestation(payload, payloadType); - - expect(bundle).toBeTruthy(); - expect(bundle.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - - if (bundle.content?.$case === 'dsseEnvelope') { - const env = bundle.content.dsseEnvelope; - expect(env.payloadType).toEqual(payloadType); - expect(env.payload.toString('base64')).toEqual( - payload.toString('base64') - ); - expect(env.signatures).toHaveLength(1); - expect(env.signatures[0].keyid).toEqual(''); - } else { - fail('Expected dsseEnvelope'); - } - - // Verification material - if ( - bundle.verificationMaterial?.content?.$case === - 'x509CertificateChain' - ) { - const chain = - bundle.verificationMaterial.content.x509CertificateChain; - expect(chain).toBeTruthy(); - expect(chain.certificates).toHaveLength(1); - expect(chain.certificates[0].rawBytes).toEqual( - pem.toDER(certificate) - ); - } else { - fail('Expected x509CertificateChain'); - } - - expect( - bundle.verificationMaterial?.timestampVerificationData - ).toBeUndefined(); - expect(bundle.verificationMaterial?.tlogEntries).toHaveLength(0); - }); - }); - - describe('when timestamping is enabled', () => { - const timestamp = Buffer.from('timestamp'); - const subject = new Signer({ - ca, - tlog, - tsa, - identityProviders: [idp], - tlogUpload: false, - }); - - beforeEach(() => { - // Mock TSA request - nock(tsaBaseURL) - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/timestamp') - .reply(200, timestamp); - }); - - it('returns a signature bundle', async () => { - const bundle = await subject.signAttestation(payload, payloadType); - expect(bundle).toBeTruthy(); - expect(bundle.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - - expect( - bundle.verificationMaterial?.timestampVerificationData - ).toBeTruthy(); - expect( - bundle.verificationMaterial?.timestampVerificationData - ?.rfc3161Timestamps - ).toHaveLength(1); - expect( - bundle.verificationMaterial?.timestampVerificationData - ?.rfc3161Timestamps[0].signedTimestamp - ).toEqual(timestamp); - }); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/sigstore-utils.test.ts b/packages/client/src/__tests__/sigstore-utils.test.ts index fc13d2a8..7e50c3cd 100644 --- a/packages/client/src/__tests__/sigstore-utils.test.ts +++ b/packages/client/src/__tests__/sigstore-utils.test.ts @@ -13,19 +13,22 @@ 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 { createDSSEEnvelope } from '../sigstore-utils'; +import { envelopeToJSON } from '@sigstore/bundle'; +import { mockRekor } from '@sigstore/mock'; +import { createDSSEEnvelope, createRekorEntry } from '../sigstore-utils'; import { dsse } from '../util'; -describe('createDSSEEnvelope', () => { - const payload = Buffer.from('Hello, world!'); - const payloadType = 'text/plain'; - const sigMaterial = { - key: { - id: 'sha256-1234', - }, - signature: Buffer.from('abc'), - }; +const payload = Buffer.from('Hello, world!'); +const payloadType = 'text/plain'; +const sigMaterial = { + key: { + id: 'sha256-1234', + value: 'key', + }, + signature: Buffer.from('abc'), +}; +describe('createDSSEEnvelope', () => { const mockSign = jest.fn(); const options = { signer: mockSign, @@ -65,3 +68,42 @@ describe('createDSSEEnvelope', () => { }); }); }); + +describe('createRekorEntry', () => { + const rekorURL = 'https://rekor.example.com'; + + const envelope = envelopeToJSON({ + payloadType, + payload, + signatures: [ + { + keyid: 'sha256-1234', + sig: Buffer.from('deadbeef', 'hex'), + }, + ], + }); + + beforeEach(async () => { + await mockRekor({ baseURL: rekorURL }); + }); + + it('returns a bundle', async () => { + const bundle = await createRekorEntry(envelope, 'foo', { rekorURL }); + + expect(bundle).toBeDefined(); + expect(bundle.dsseEnvelope?.payloadType).toBe(payloadType); + expect(bundle.dsseEnvelope?.payload).toBe(payload.toString('base64')); + expect(bundle.dsseEnvelope?.signatures).toHaveLength(1); + + expect(bundle.verificationMaterial.publicKey?.hint).toBe('sha256-1234'); + + expect(bundle.verificationMaterial.tlogEntries).toHaveLength(1); + expect(bundle.verificationMaterial.tlogEntries[0].kindVersion?.kind).toBe( + 'intoto' + ); + + expect( + bundle.verificationMaterial.timestampVerificationData + ).toBeUndefined(); + }); +}); diff --git a/packages/client/src/__tests__/sigstore.test.ts b/packages/client/src/__tests__/sigstore.test.ts index 391a625b..d27a4973 100644 --- a/packages/client/src/__tests__/sigstore.test.ts +++ b/packages/client/src/__tests__/sigstore.test.ts @@ -15,127 +15,66 @@ limitations under the License. */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SerializedBundle } from '@sigstore/bundle'; -import { - Bundle, - HashAlgorithm, - TimestampVerificationData, - TransparencyLogEntry, - TrustedRoot, - X509CertificateChain, -} from '@sigstore/protobuf-specs'; +import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'; +import { TrustedRoot } from '@sigstore/protobuf-specs'; import { TUFError } from '@sigstore/tuf'; import { fromPartial } from '@total-typescript/shoehorn'; import mocktuf, { Target } from '@tufjs/repo-mock'; import { PolicyError, VerificationError } from '../error'; -import { Signer } from '../sign'; import { attest, createVerifier, sign, tuf, verify } from '../sigstore'; import bundles from './__fixtures__/bundles/v01'; import bundlesV02 from './__fixtures__/bundles/v02'; import { trustedRoot } from './__fixtures__/trust'; -import type { TUFOptions, VerifyOptions } from '../config'; - -jest.mock('../sign'); - -const tlogEntries: TransparencyLogEntry[] = [ - { - logIndex: '0', - logId: { - keyId: Buffer.from('logId'), - }, - kindVersion: { - kind: 'kind', - version: 'version', - }, - canonicalizedBody: Buffer.from('body'), - integratedTime: '2021-01-01T00:00:00Z', - inclusionPromise: { - signedEntryTimestamp: Buffer.from('inclusionPromise'), - }, - inclusionProof: { - logIndex: '0', - rootHash: Buffer.from('rootHash'), - treeSize: '0', - hashes: [Buffer.from('hash')], - checkpoint: { - envelope: 'checkpoint', - }, - }, - }, -]; +import type { SignOptions, TUFOptions, VerifyOptions } from '../config'; + +const fulcioURL = 'https://fulcio.example.com'; +const rekorURL = 'https://rekor.example.com'; +const tsaURL = 'https://tsa.example.com'; -const timestampVerificationData: TimestampVerificationData = { - rfc3161Timestamps: [{ signedTimestamp: Buffer.from('signedTimestamp') }], -}; +const subject = 'foo@bar.com'; +const oidcPayload = { sub: subject, iss: '' }; +const oidc = `.${Buffer.from(JSON.stringify(oidcPayload)).toString( + 'base64' +)}.}`; -const x509CertificateChain: X509CertificateChain = { - certificates: [{ rawBytes: Buffer.from('certificate') }], -}; +const idp = { getToken: () => Promise.resolve(oidc) }; describe('sign', () => { const payload = Buffer.from('Hello, world!'); - // Signer output - const bundle: Bundle = { - mediaType: 'test/output', - verificationMaterial: { - content: { - $case: 'x509CertificateChain', - x509CertificateChain: x509CertificateChain, - }, - tlogEntries, - timestampVerificationData, - }, - content: { - $case: 'messageSignature', - messageSignature: { - messageDigest: { - algorithm: HashAlgorithm.SHA2_256, - digest: Buffer.from('messageDigest'), - }, - signature: Buffer.from('signature'), - }, - }, - }; - - const mockSigner = jest.mocked(Signer); - const mockSign = jest.fn(); - - beforeEach(() => { - mockSigner.mockClear(); - - mockSign.mockClear(); - mockSign.mockResolvedValueOnce(bundle); - jest.spyOn(Signer.prototype, 'signBlob').mockImplementation(mockSign); + beforeEach(async () => { + await mockFulcio({ baseURL: fulcioURL }); + await mockRekor({ baseURL: rekorURL }); + await mockTSA({ baseURL: tsaURL }); }); - it('constructs the Signer with the correct options', async () => { - await sign(payload); - - // Signer was constructed - expect(mockSigner).toHaveBeenCalledTimes(1); - const args = mockSigner.mock.calls[0]; - - // Signer was constructed with options - expect(args).toHaveLength(1); - const options = args[0]; - - // Signer was constructed with the correct options - expect(options).toHaveProperty('ca', expect.anything()); - expect(options).toHaveProperty('tlog', expect.anything()); - expect(options.identityProviders).toHaveLength(1); - }); + it('returns the signed bundle', async () => { + const options: SignOptions = { + fulcioURL, + rekorURL, + tsaServerURL: tsaURL, + identityProvider: idp, + }; + const bundle = await sign(payload, options); - it('invokes the Signer instance with the correct params', async () => { - await sign(payload); + expect(bundle).toBeDefined(); + expect(bundle.messageSignature).toBeDefined(); + expect(bundle.messageSignature?.signature).toBeDefined(); + expect(bundle.messageSignature?.messageDigest).toBeDefined(); - expect(mockSign).toHaveBeenCalledWith(payload); - }); + expect( + bundle.verificationMaterial.x509CertificateChain?.certificates + ).toHaveLength(1); - it('returns the correct envelope', async () => { - const sig = await sign(payload); + expect(bundle.verificationMaterial.tlogEntries).toHaveLength(1); + expect(bundle.verificationMaterial.tlogEntries[0].kindVersion?.kind).toBe( + 'hashedrekord' + ); - expect(sig).toEqual(Bundle.toJSON(bundle)); + expect( + bundle.verificationMaterial.timestampVerificationData?.rfc3161Timestamps + ).toHaveLength(1); }); }); @@ -143,74 +82,37 @@ describe('signAttestation', () => { const payload = Buffer.from('Hello, world!'); const payloadType = 'text/plain'; - // Signer output - const bundle: Bundle = { - mediaType: 'test/output', - verificationMaterial: { - content: { - $case: 'x509CertificateChain', - x509CertificateChain: x509CertificateChain, - }, - tlogEntries, - timestampVerificationData, - }, - content: { - $case: 'dsseEnvelope', - dsseEnvelope: { - payload: payload, - payloadType: payloadType, - signatures: [ - { - keyid: 'keyid', - sig: Buffer.from('signature'), - }, - ], - }, - }, - }; - - const mockSigner = jest.mocked(Signer); - const mockSign = jest.fn(); - - beforeEach(() => { - mockSigner.mockClear(); - - mockSign.mockClear(); - mockSign.mockResolvedValueOnce(bundle); - jest - .spyOn(Signer.prototype, 'signAttestation') - .mockImplementation(mockSign); + beforeEach(async () => { + await mockFulcio({ baseURL: fulcioURL }); + await mockRekor({ baseURL: rekorURL }); + await mockTSA({ baseURL: tsaURL }); }); it('constructs the Signer with the correct options', async () => { - const identityProvider = { getToken: () => Promise.resolve('token') }; - await attest(payload, payloadType, { identityProvider }); - - // Signer was constructed - expect(mockSigner).toHaveBeenCalledTimes(1); - const args = mockSigner.mock.calls[0]; - - // Signer was constructed with options - expect(args).toHaveLength(1); - const options = args[0]; - - // Signer was constructed with the correct options - expect(options).toHaveProperty('ca', expect.anything()); - expect(options).toHaveProperty('tlog', expect.anything()); - expect(options.identityProviders).toHaveLength(1); - expect(options.identityProviders[0]).toBe(identityProvider); - }); - - it('invokes the Signer instance with the correct params', async () => { - await attest(payload, payloadType); - - expect(mockSign).toHaveBeenCalledWith(payload, payloadType); - }); - - it('returns the correct envelope', async () => { - const sig = await attest(payload, payloadType); + const options: SignOptions = { + fulcioURL, + rekorURL, + tsaServerURL: tsaURL, + identityProvider: idp, + }; + const bundle = await attest(payload, payloadType, options); + expect(bundle).toBeDefined(); + expect(bundle.dsseEnvelope?.payloadType).toBe(payloadType); + expect(bundle.dsseEnvelope?.payload).toBe(payload.toString('base64')); + expect(bundle.dsseEnvelope?.signatures).toHaveLength(1); + + expect( + bundle.verificationMaterial.x509CertificateChain?.certificates + ).toHaveLength(1); + + expect(bundle.verificationMaterial.tlogEntries).toHaveLength(1); + expect(bundle.verificationMaterial.tlogEntries[0].kindVersion?.kind).toBe( + 'intoto' + ); - expect(sig).toEqual(Bundle.toJSON(bundle)); + expect( + bundle.verificationMaterial.timestampVerificationData?.rfc3161Timestamps + ).toHaveLength(1); }); }); diff --git a/packages/client/src/__tests__/tlog/format.test.ts b/packages/client/src/__tests__/tlog/format.test.ts deleted file mode 100644 index f3cfdf0d..00000000 --- a/packages/client/src/__tests__/tlog/format.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { - toProposedDSSEEntry, - toProposedHashedRekordEntry, - toProposedIntotoEntry, -} from '../../tlog/format'; -import { SignatureMaterial } from '../../types/signature'; -import { Envelope } from '../../types/sigstore'; -import { crypto, encoding as enc } from '../../util'; - -describe('format', () => { - const cert = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----'; - const signature = Buffer.from('signature'); - const sigMaterial: SignatureMaterial = { - signature: signature, - certificates: [cert], - key: undefined, - }; - - describe('toProposedHashedRekordEntry', () => { - const digest = Buffer.from('digest'); - const signature = Buffer.from('signature'); - - it('returns a valid hashedrekord entry', () => { - const entry = toProposedHashedRekordEntry(digest, sigMaterial); - - expect(entry.apiVersion).toEqual('0.0.1'); - expect(entry.kind).toEqual('hashedrekord'); - expect(entry.spec).toBeTruthy(); - - expect(entry.spec.data).toBeTruthy(); - expect(entry.spec.data.hash).toBeTruthy(); - expect(entry.spec.data.hash?.algorithm).toBe('sha256'); - expect(entry.spec.data.hash?.value).toBe(digest.toString('hex')); - - expect(entry.spec.signature).toBeTruthy(); - expect(entry.spec.signature?.content).toBe(signature.toString('base64')); - expect(entry.spec.signature?.publicKey).toBeTruthy(); - expect(entry.spec.signature?.publicKey?.content).toBe( - enc.base64Encode(cert) - ); - }); - }); - - describe('toProposedDSSEEntry', () => { - describe('when there is a single signature in the envelope', () => { - const envelope: Envelope = { - payloadType: 'application/vnd.in-toto+json', - payload: Buffer.from('payload'), - signatures: [{ keyid: '123', sig: signature }], - }; - - it('returns a valid dsse entry', () => { - const entry = toProposedDSSEEntry(envelope, sigMaterial); - - expect(entry.apiVersion).toEqual('0.0.1'); - expect(entry.kind).toEqual('dsse'); - expect(entry.spec).toBeTruthy(); - expect(entry.spec.proposedContent).toBeTruthy(); - expect(typeof entry.spec.proposedContent?.envelope).toBe('string'); - expect(entry.spec.proposedContent?.verifiers).toHaveLength(1); - expect(entry.spec.proposedContent?.verifiers[0]).toEqual( - enc.base64Encode(cert) - ); - - // ensure we have the expected JSON object stored in the string - if (typeof entry.spec.proposedContent?.envelope === 'string') { - const envObj = JSON.parse(entry.spec.proposedContent?.envelope); - expect(envObj).toEqual(Envelope.toJSON(envelope)); - // ensure we only have 1 signature specified in the object - expect(envObj.signatures).toHaveLength(1); - } else { - fail('dsse envelope should be set as JSON string'); - } - - // we don't want the persisted properties to show up in a proposed entry - expect(entry.spec.signatures).toBeUndefined(); - }); - }); - - describe('when there are multiple signatures in the envelope', () => { - const envelope: Envelope = { - payloadType: 'application/vnd.in-toto+json', - payload: Buffer.from('payload'), - signatures: [ - { keyid: '123', sig: signature }, - { keyid: '456', sig: signature }, - ], - }; - - it('returns a valid dsse entry', () => { - const entry = toProposedDSSEEntry(envelope, sigMaterial); - - expect(entry.apiVersion).toEqual('0.0.1'); - expect(entry.kind).toEqual('dsse'); - expect(entry.spec).toBeTruthy(); - expect(entry.spec.proposedContent).toBeTruthy(); - expect(typeof entry.spec.proposedContent?.envelope).toBe('string'); - expect(entry.spec.proposedContent?.verifiers).toHaveLength(1); - expect(entry.spec.proposedContent?.verifiers[0]).toEqual( - enc.base64Encode(cert) - ); - - // ensure we have the expected JSON object stored in the string - if (typeof entry.spec.proposedContent?.envelope === 'string') { - const envObj = JSON.parse(entry.spec.proposedContent?.envelope); - expect(envObj).toEqual(Envelope.toJSON(envelope)); - // ensure we have 2 signatures specified in the object - expect(envObj.signatures).toHaveLength(2); - } else { - fail('dsse envelope should be set as JSON string'); - } - - // we don't want the persisted properties to show up in a proposed entry - expect(entry.spec.signatures).toBeUndefined(); - }); - }); - }); - - describe('toProposedIntotoEntry', () => { - describe('when the keyid is a non-empty string', () => { - const envelope: Envelope = { - payloadType: 'application/vnd.in-toto+json', - payload: Buffer.from('payload'), - signatures: [{ keyid: '123', sig: signature }], - }; - - it('returns a valid intoto entry', () => { - const entry = toProposedIntotoEntry(envelope, sigMaterial); - - expect(entry.apiVersion).toEqual('0.0.2'); - expect(entry.kind).toEqual('intoto'); - expect(entry.spec).toBeTruthy(); - expect(entry.spec.content).toBeTruthy(); - expect(entry.spec.content.envelope).toBeTruthy(); - - if (typeof entry.spec.content.envelope !== 'string') { - const e = entry.spec.content.envelope; - expect(e?.payloadType).toEqual(envelope.payloadType); - expect(e?.payload).toEqual( - enc.base64Encode(envelope.payload.toString('base64')) - ); - expect(e?.signatures).toHaveLength(1); - expect(e?.signatures[0].keyid).toEqual(envelope.signatures[0].keyid); - expect(e?.signatures[0].sig).toEqual( - enc.base64Encode(envelope.signatures[0].sig.toString('base64')) - ); - expect(e?.signatures[0].publicKey).toEqual(enc.base64Encode(cert)); - } else { - fail('intoto type is v0.0.1 but expecting v0.0.2'); - } - - expect(entry.spec.content.payloadHash).toBeTruthy(); - expect(entry.spec.content.payloadHash?.algorithm).toBe('sha256'); - expect(entry.spec.content.payloadHash?.value).toBe( - crypto.hash(envelope.payload).toString('hex') - ); - expect(entry.spec.content.hash).toBeTruthy(); - expect(entry.spec.content.hash?.algorithm).toBe('sha256'); - - // This hard-coded hash value helps us detect if we've unintentionally - // changed the hashing algorithm. - expect(entry.spec.content.hash?.value).toBe( - '37d47ab456ca63a84f6457be655dd49799542f2e1db5d05160b214fb0b9a7f55' - ); - }); - }); - - describe('when the keyid is an empty string', () => { - const envelope: Envelope = { - payloadType: 'application/vnd.in-toto+json', - payload: Buffer.from('payload'), - signatures: [{ keyid: '', sig: signature }], - }; - - it('returns a valid intoto entry', () => { - const entry = toProposedIntotoEntry(envelope, sigMaterial); - - if (typeof entry.spec.content.envelope === 'string') { - fail('intoto type is v0.0.1 but expecting v0.0.2'); - } - - // Ensure the keyid is not included in the envelope. - const e = entry.spec.content.envelope; - expect(e?.signatures).toHaveLength(1); - expect(e?.signatures[0].keyid).toBeUndefined(); - expect(e?.signatures[0].sig).toEqual( - enc.base64Encode(envelope.signatures[0].sig.toString('base64')) - ); - - // This hard-coded hash value helps us detect if we've unintentionally - // changed the hashing algorithm. - expect(entry.spec.content.hash?.value).toBe( - 'f39ab279af9d9be421342ce4c8e5c422b5bc3dd20602703b1893283a934fbe72' - ); - }); - }); - - describe('when there are multiple signatures in the envelope', () => { - const envelope: Envelope = { - payloadType: 'application/vnd.in-toto+json', - payload: Buffer.from('payload'), - signatures: [ - { keyid: '123', sig: signature }, - { keyid: '', sig: signature }, - ], - }; - - it('returns a valid intoto entry', () => { - const entry = toProposedIntotoEntry(envelope, sigMaterial); - - if (typeof entry.spec.content.envelope === 'string') { - fail('intoto type is v0.0.1 but expecting v0.0.2'); - } - - // Check to ensure only the first signature is included in the envelope - const e = entry.spec.content.envelope; - expect(e?.signatures).toHaveLength(1); - expect(e?.signatures[0].keyid).toEqual(envelope.signatures[0].keyid); - expect(e?.signatures[0].sig).toEqual( - enc.base64Encode(envelope.signatures[0].sig.toString('base64')) - ); - expect(e?.signatures[0].publicKey).toEqual(enc.base64Encode(cert)); - - // This hard-coded hash value helps us detect if we've unintentionally - // changed the hashing algorithm. - expect(entry.spec.content.hash?.value).toBe( - '37d47ab456ca63a84f6457be655dd49799542f2e1db5d05160b214fb0b9a7f55' - ); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/tlog/index.test.ts b/packages/client/src/__tests__/tlog/index.test.ts deleted file mode 100644 index 26ab05e5..00000000 --- a/packages/client/src/__tests__/tlog/index.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; -import { InternalError } from '../../error'; -import { - toProposedHashedRekordEntry, - toProposedIntotoEntry, -} from '../../tlog/format'; -import { TLogClient } from '../../tlog/index'; -import { SignatureMaterial } from '../../types/signature'; -import { Envelope } from '../../types/sigstore'; - -describe('TLogClient', () => { - const baseURL = 'http://localhost:8080'; - - describe('constructor', () => { - it('should create a new instance', () => { - const client = new TLogClient({ rekorBaseURL: baseURL }); - expect(client).toBeDefined(); - }); - }); - - describe('createMessageSignatureEntry', () => { - const subject = new TLogClient({ rekorBaseURL: baseURL }); - - const digest = Buffer.from('digest'); - - const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----`; - const rootCertificate = `-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----`; - - const sigMaterial: SignatureMaterial = { - signature: Buffer.from('signature'), - certificates: [leafCertificate, rootCertificate], - key: undefined, - }; - - // Rekor input - const proposedEntry = JSON.stringify( - toProposedHashedRekordEntry(digest, sigMaterial) - ); - - describe('when Rekor returns an error', () => { - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries', proposedEntry) - .reply(500, {}); - }); - - it('returns an error', async () => { - await expect( - subject.createMessageSignatureEntry(digest, sigMaterial) - ).rejects.toThrowWithCode(InternalError, 'TLOG_CREATE_ENTRY_ERROR'); - }); - }); - - describe('when Rekor returns a valid response', () => { - // Rekor output - const b64Cert = Buffer.from(leafCertificate).toString('base64'); - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - - const signatureBundle = { - kind: 'hashedrekord', - apiVersion: '0.0.1', - spec: { - signature: { - content: sigMaterial.signature.toString('hex'), - publicKey: { content: b64Cert }, - }, - }, - }; - - const rekorEntry = { - [uuid]: { - body: Buffer.from(JSON.stringify(signatureBundle)).toString('base64'), - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - }, - }, - }; - - beforeEach(() => { - // Mock Rekor request - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries', proposedEntry) - .reply(201, rekorEntry); - }); - - it('returns a tlog entry', async () => { - const entry = await subject.createMessageSignatureEntry( - digest, - sigMaterial - ); - - expect(entry.uuid).toEqual(uuid); - expect(entry.logID).toEqual(rekorEntry[uuid].logID); - expect(entry.logIndex).toEqual(rekorEntry[uuid].logIndex); - expect(entry.integratedTime).toEqual(rekorEntry[uuid].integratedTime); - expect(entry.verification?.signedEntryTimestamp).toEqual( - rekorEntry[uuid].verification.signedEntryTimestamp - ); - expect(entry.body).toEqual(rekorEntry[uuid].body); - }); - }); - }); - - describe('createDSSEEntry', () => { - const subject = new TLogClient({ rekorBaseURL: baseURL }); - - // Input - const payload = Buffer.from('Hello, world!'); - const payloadType = 'text/plain'; - const signature = Buffer.from('signature'); - - const dsse: Envelope = { - payload, - payloadType, - signatures: [{ keyid: '', sig: signature }], - }; - const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----`; - const rootCertificate = `-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----`; - - const sigMaterial: SignatureMaterial = { - signature, - certificates: [leafCertificate, rootCertificate], - key: undefined, - }; - - // Rekor input - const proposedEntry = JSON.stringify( - toProposedIntotoEntry(dsse, sigMaterial) - ); - - describe('when Rekor returns a 500 error', () => { - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries', proposedEntry) - .reply(500, {}); - }); - - it('returns an error', async () => { - await expect( - subject.createDSSEEntry(dsse, sigMaterial) - ).rejects.toThrowWithCode(InternalError, 'TLOG_CREATE_ENTRY_ERROR'); - }); - }); - - describe('when Rekor returns a 409 conflict error', () => { - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - - beforeEach(() => { - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries', proposedEntry) - .reply(409, {}, { Location: `/api/v1/log/entries/${uuid}` }); - }); - - describe('when fetchOnConflict is false', () => { - it('returns an error', async () => { - await expect( - subject.createDSSEEntry(dsse, sigMaterial, { - fetchOnConflict: false, - }) - ).rejects.toThrowWithCode(InternalError, 'TLOG_CREATE_ENTRY_ERROR'); - }); - }); - - describe('when fetchOnConflict is true', () => { - describe('when the fetch is successful', () => { - const signatureBundle = { - kind: 'intoto', - apiVersion: '0.0.2', - spec: { - signature: { - content: signature, - publicKey: { content: leafCertificate }, - }, - }, - }; - - const rekorEntry = { - [uuid]: { - body: Buffer.from(JSON.stringify(signatureBundle)).toString( - 'base64' - ), - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - }, - }, - }; - - beforeEach(() => { - nock(baseURL) - .get(`/api/v1/log/entries/${uuid}`) - .reply(200, rekorEntry); - }); - - it('returns a tlog entry', async () => { - const entry = await subject.createDSSEEntry(dsse, sigMaterial, { - fetchOnConflict: true, - }); - expect(entry).toBeTruthy(); - }); - }); - - describe('when the fetch returns an error', () => { - beforeEach(() => { - nock(baseURL).get(`/api/v1/log/entries/${uuid}`).reply(404, {}); - }); - - it('returns an error', async () => { - await expect( - subject.createDSSEEntry(dsse, sigMaterial, { - fetchOnConflict: true, - }) - ).rejects.toThrowWithCode(InternalError, 'TLOG_FETCH_ENTRY_ERROR'); - }); - }); - }); - }); - - describe('when Rekor returns a valid response', () => { - const uuid = - '69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6'; - - const signatureBundle = { - kind: 'intoto', - apiVersion: '0.0.2', - spec: { - signature: { - content: signature, - publicKey: { content: leafCertificate }, - }, - }, - }; - - const rekorEntry = { - [uuid]: { - body: Buffer.from(JSON.stringify(signatureBundle)).toString('base64'), - integratedTime: 1654015743, - logID: - 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: - 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=', - }, - }, - }; - - beforeEach(() => { - // Mock Rekor request - nock(baseURL) - .matchHeader('Accept', 'application/json') - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/log/entries', proposedEntry) - .reply(201, rekorEntry); - }); - - it('returns a tlog entry', async () => { - const entry = await subject.createDSSEEntry(dsse, sigMaterial); - - expect(entry.uuid).toEqual(uuid); - expect(entry.logID).toEqual(rekorEntry[uuid].logID); - expect(entry.logIndex).toEqual(rekorEntry[uuid].logIndex); - expect(entry.integratedTime).toEqual(rekorEntry[uuid].integratedTime); - expect(entry.verification?.signedEntryTimestamp).toEqual( - rekorEntry[uuid].verification.signedEntryTimestamp - ); - expect(entry.body).toEqual(rekorEntry[uuid].body); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/tlog/verify/checkpoint.test.ts b/packages/client/src/__tests__/tlog/verify/checkpoint.test.ts index 3327cb47..995d6706 100644 --- a/packages/client/src/__tests__/tlog/verify/checkpoint.test.ts +++ b/packages/client/src/__tests__/tlog/verify/checkpoint.test.ts @@ -1,4 +1,20 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 type { TLogEntryWithInclusionProof } from '@sigstore/bundle'; +import { HashAlgorithm, PublicKeyDetails } from '@sigstore/protobuf-specs'; import { fromPartial } from '@total-typescript/shoehorn'; import { VerificationError } from '../../../error'; import { verifyCheckpoint } from '../../../tlog/verify/checkpoint'; @@ -14,12 +30,12 @@ describe('verifyCheckpoint', () => { const publicKey: sigstore.PublicKey = { rawBytes: keyBytes, - keyDetails: sigstore.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + keyDetails: PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, }; const tlogInstance: sigstore.TransparencyLogInstance = { baseUrl: 'https://tlog.sigstore.dev', - hashAlgorithm: sigstore.HashAlgorithm.SHA2_256, + hashAlgorithm: HashAlgorithm.SHA2_256, publicKey, logId: { keyId: keyID }, }; diff --git a/packages/client/src/__tests__/tlog/verify/set.test.ts b/packages/client/src/__tests__/tlog/verify/set.test.ts index 34a254af..c44b6d8c 100644 --- a/packages/client/src/__tests__/tlog/verify/set.test.ts +++ b/packages/client/src/__tests__/tlog/verify/set.test.ts @@ -17,6 +17,7 @@ import { bundleFromJSON, TLogEntryWithInclusionPromise, } from '@sigstore/bundle'; +import { HashAlgorithm, PublicKeyDetails } from '@sigstore/protobuf-specs'; import { verifyTLogSET } from '../../../tlog/verify/set'; import * as sigstore from '../../../types/sigstore'; import { crypto } from '../../../util'; @@ -31,22 +32,22 @@ describe('verifyTLogSET', () => { const publicKey: sigstore.PublicKey = { rawBytes: keyBytes, - keyDetails: sigstore.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + keyDetails: PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, }; const validTLog: sigstore.TransparencyLogInstance = { baseUrl: 'https://tlog.sigstore.dev', - hashAlgorithm: sigstore.HashAlgorithm.SHA2_256, + hashAlgorithm: HashAlgorithm.SHA2_256, publicKey, logId: { keyId: keyID }, }; const invalidTLog: sigstore.TransparencyLogInstance = { - hashAlgorithm: sigstore.HashAlgorithm.SHA2_256, + hashAlgorithm: HashAlgorithm.SHA2_256, baseUrl: 'https://invalid.tlog.example.com', logId: { keyId: Buffer.from('invalid') }, publicKey: { - keyDetails: sigstore.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + keyDetails: PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, rawBytes: Buffer.from('invalid'), }, }; @@ -107,7 +108,7 @@ describe('verifyTLogSET', () => { ...validTLog, publicKey: { rawBytes: undefined, - keyDetails: sigstore.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + keyDetails: PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, validFor: { start: undefined }, }, }, diff --git a/packages/client/src/__tests__/tsa/index.test.ts b/packages/client/src/__tests__/tsa/index.test.ts deleted file mode 100644 index 3ab2c7f5..00000000 --- a/packages/client/src/__tests__/tsa/index.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2023 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 nock from 'nock'; -import { InternalError } from '../../error'; -import { TSAClient } from '../../tsa/'; -import { crypto } from '../../util'; - -describe('TSAClient', () => { - const baseURL = 'http://localhost:8080'; - - describe('constructor', () => { - it('should create a new instance', () => { - const client = new TSAClient({ tsaBaseURL: baseURL }); - expect(client).toBeDefined(); - }); - }); - - describe('createTimestamp', () => { - const subject = new TSAClient({ tsaBaseURL: baseURL }); - - const signature = Buffer.from('signature'); - - const request = { - artifactHash: crypto.hash(signature).toString('base64'), - hashAlgorithm: 'sha256', - }; - - describe('when TSA returns a timestamp', () => { - const timestamp = Buffer.from('timestamp'); - - beforeEach(() => { - nock(baseURL) - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/timestamp', request) - .reply(201, timestamp); - }); - - it('returns the timestamp', async () => { - const result = await subject.createTimestamp(signature); - expect(result).toEqual(timestamp); - }); - }); - - describe('when TSA returns an error', () => { - beforeEach(() => { - nock(baseURL) - .matchHeader('Content-Type', 'application/json') - .post('/api/v1/timestamp', request) - .reply(500, {}); - }); - - it('returns an error', async () => { - await expect( - subject.createTimestamp(signature) - ).rejects.toThrowWithCode(InternalError, 'TSA_CREATE_TIMESTAMP_ERROR'); - }); - }); - }); -}); diff --git a/packages/client/src/__tests__/types/signature.test.ts b/packages/client/src/__tests__/types/signature.test.ts index a8eaf948..bbbb4285 100644 --- a/packages/client/src/__tests__/types/signature.test.ts +++ b/packages/client/src/__tests__/types/signature.test.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Sigstore Authors. +Copyright 2023 The Sigstore Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,30 +13,97 @@ 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 { extractSignatureMaterial } from '../../types/signature'; -import { Envelope } from '../../types/sigstore'; - -describe('extractSignatureMaterial', () => { - const envelope: Envelope = { - payload: Buffer.from('Hello, world!'), - payloadType: 'text/plain', - signatures: [ - { - keyid: 'keyid', - sig: Buffer.from('signature'), - }, - ], - }; - - const publicKey = '-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----'; - - it('returns the correct signature material', () => { - const sigMaterial = extractSignatureMaterial(envelope, publicKey); - - expect(sigMaterial.key).toBeDefined(); - expect(sigMaterial.signature).toEqual(envelope.signatures[0].sig); - expect(sigMaterial.key?.id).toEqual(envelope.signatures[0].keyid); - expect(sigMaterial.key?.value).toEqual(publicKey); - expect(sigMaterial.certificates).toBeUndefined(); +import assert from 'assert'; +import { SignatureError } from '../../error'; +import { + CallbackSigner, + SignatureMaterial, + SignerFunc, +} from '../../types/signature'; + +describe('CallbackSigner', () => { + const data = Buffer.from('artifact'); + + const signer = jest.fn(); + + describe('constructor', () => { + it('should create a new instance', () => { + const client = new CallbackSigner({ signer }); + expect(client).toBeDefined(); + }); + }); + + describe('sign', () => { + describe('when the callback returns valid data', () => { + const sigMaterial: SignatureMaterial = { + signature: Buffer.from('signature'), + certificates: undefined, + key: { value: 'key', id: 'hint' }, + }; + + const signer = jest + .fn() + .mockResolvedValue(sigMaterial) satisfies SignerFunc; + + const subject = new CallbackSigner({ signer }); + + it('invokes the callback', async () => { + await subject.sign(data); + + expect(signer).toHaveBeenCalledTimes(1); + expect(signer).toHaveBeenCalledWith(data); + }); + + it('returns a signature', async () => { + const endorsement = await subject.sign(data); + + expect(endorsement).toBeTruthy(); + expect(endorsement.signature).toEqual(sigMaterial.signature); + expect(endorsement.key).toBeTruthy(); + assert(endorsement.key.$case === 'publicKey'); + expect(endorsement.key.publicKey).toEqual(sigMaterial.key.value); + expect(endorsement.key.hint).toEqual(sigMaterial.key.id); + }); + }); + + describe('when the callback returns data with a missing signature', () => { + const sigMaterial = { + certificates: undefined, + key: { value: 'key', id: 'hint' }, + }; + + const signer = jest + .fn() + .mockResolvedValue(sigMaterial) satisfies SignerFunc; + + const subject = new CallbackSigner({ signer }); + + it('returns a signature', async () => { + await expect(subject.sign(data)).rejects.toThrowWithCode( + SignatureError, + 'MISSING_SIGNATURE_ERROR' + ); + }); + }); + + describe('when the callback returns data with a missing key', () => { + const sigMaterial = { + signature: Buffer.from('signature'), + certificates: undefined, + }; + + const signer = jest + .fn() + .mockResolvedValue(sigMaterial) satisfies SignerFunc; + + const subject = new CallbackSigner({ signer }); + + it('returns a signature', async () => { + await expect(subject.sign(data)).rejects.toThrowWithCode( + SignatureError, + 'MISSING_PUBLIC_KEY_ERROR' + ); + }); + }); }); }); diff --git a/packages/client/src/__tests__/types/sigstore.test.ts b/packages/client/src/__tests__/types/sigstore.test.ts index 220743a7..2d1c50d5 100644 --- a/packages/client/src/__tests__/types/sigstore.test.ts +++ b/packages/client/src/__tests__/types/sigstore.test.ts @@ -13,10 +13,7 @@ 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 type { Entry } from '../../external/rekor'; -import { SignatureMaterial } from '../../types/signature'; import * as sigstore from '../../types/sigstore'; -import { encoding as enc, pem } from '../../util'; describe('isCAVerificationOptions', () => { describe('when the verification options are for a CA', () => { @@ -87,170 +84,3 @@ describe('isCAVerificationOptions', () => { }); }); }); - -describe('bundle', () => { - const signature = Buffer.from('signature'); - const certificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----`; - - describe('toDSSEBundle', () => { - const sigMaterial: SignatureMaterial = { - signature, - certificates: [certificate], - key: undefined, - }; - const envelope: sigstore.Envelope = { - payloadType: 'application/vnd.in-toto+json', - payload: Buffer.from('payload'), - signatures: [ - { - keyid: '', - sig: signature, - }, - ], - }; - - const entryKind = { - kind: 'intoto', - apiVersion: '0.0.2', - }; - - const rekorEntry = { - uuid: 'a12bc3', - body: enc.base64Encode(JSON.stringify(entryKind)), - integratedTime: 1654015743, - logID: 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d', - logIndex: 2513258, - verification: { - signedEntryTimestamp: Buffer.from('set').toString('base64'), - inclusionProof: { - hashes: ['deadbeef', 'feedface'], - logIndex: 12345, - rootHash: 'fee1dead', - treeSize: 12346, - checkpoint: 'checkpoint', - }, - }, - } satisfies Entry; - - const timestamp = Buffer.from('timestamp'); - - it('returns a valid DSSE bundle', () => { - const b = sigstore.toDSSEBundle({ - envelope, - signature: sigMaterial, - tlogEntry: rekorEntry, - timestamp: timestamp, - }); - - expect(b).toBeTruthy(); - expect(b.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - if (b.content?.$case === 'dsseEnvelope') { - expect(b.content.dsseEnvelope).toEqual(envelope); - } else { - fail('Expected dsseEnvelope'); - } - - // Verification material - if (b.verificationMaterial?.content?.$case === 'x509CertificateChain') { - const chain = b.verificationMaterial.content.x509CertificateChain; - expect(chain).toBeTruthy(); - expect(chain.certificates).toHaveLength(1); - expect(chain.certificates[0].rawBytes).toEqual(pem.toDER(certificate)); - } else { - fail('Expected x509CertificateChain'); - } - - // TLog entry - expect(b.verificationMaterial?.tlogEntries).toHaveLength(1); - - const tlog = b.verificationMaterial?.tlogEntries[0]; - expect(tlog?.inclusionPromise).toBeTruthy(); - expect( - tlog?.inclusionPromise?.signedEntryTimestamp.toString('base64') - ).toEqual(rekorEntry.verification.signedEntryTimestamp); - expect(tlog?.integratedTime).toEqual( - rekorEntry.integratedTime.toString() - ); - expect(tlog?.logId).toBeTruthy(); - expect(tlog?.logId?.keyId).toBeTruthy(); - expect(tlog?.logId?.keyId.toString('hex')).toEqual(rekorEntry.logID); - expect(tlog?.logIndex).toEqual(rekorEntry.logIndex.toString()); - expect(tlog?.kindVersion?.kind).toEqual(entryKind.kind); - expect(tlog?.kindVersion?.version).toEqual(entryKind.apiVersion); - expect(tlog?.inclusionProof?.checkpoint?.envelope).toEqual( - rekorEntry.verification.inclusionProof.checkpoint - ); - expect(tlog?.inclusionProof?.hashes).toHaveLength(2); - expect(tlog?.inclusionProof?.hashes[0]).toEqual( - Buffer.from(rekorEntry.verification.inclusionProof.hashes[0], 'hex') - ); - expect(tlog?.inclusionProof?.hashes[1]).toEqual( - Buffer.from(rekorEntry.verification.inclusionProof.hashes[1], 'hex') - ); - expect(tlog?.inclusionProof?.logIndex).toEqual( - rekorEntry.verification.inclusionProof.logIndex.toString() - ); - expect(tlog?.inclusionProof?.rootHash).toEqual( - Buffer.from(rekorEntry.verification.inclusionProof.rootHash, 'hex') - ); - expect(tlog?.inclusionProof?.treeSize).toEqual( - rekorEntry.verification.inclusionProof.treeSize.toString() - ); - - // Timestamp verification data - expect( - b.verificationMaterial?.timestampVerificationData?.rfc3161Timestamps - ).toHaveLength(1); - const ts = - b.verificationMaterial?.timestampVerificationData?.rfc3161Timestamps[0]; - expect(ts?.signedTimestamp).toEqual(timestamp); - }); - }); - - describe('toMessageSignatureBundle', () => { - const sigMaterial: SignatureMaterial = { - signature, - certificates: undefined, - key: { value: 'key', id: 'hint' }, - }; - - const digest = Buffer.from('digest'); - - it('returns a valid message signature bundle', () => { - const b = sigstore.toMessageSignatureBundle({ - digest, - signature: sigMaterial, - }); - - expect(b).toBeTruthy(); - expect(b.mediaType).toEqual( - 'application/vnd.dev.sigstore.bundle+json;version=0.1' - ); - if (b.content?.$case === 'messageSignature') { - expect(b.content.messageSignature.messageDigest?.algorithm).toEqual( - sigstore.HashAlgorithm.SHA2_256 - ); - expect(b.content.messageSignature.messageDigest?.digest).toEqual( - digest - ); - expect(b.content.messageSignature.signature).toEqual(signature); - } else { - fail('Expected messageSignature'); - } - - // Verification material - if (b.verificationMaterial?.content?.$case === 'publicKey') { - const publicKey = b.verificationMaterial.content.publicKey; - expect(publicKey).toBeTruthy(); - expect(publicKey.hint).toEqual('hint'); - } else { - fail('Expected publicKey'); - } - - expect(b.verificationMaterial?.timestampVerificationData).toBeUndefined(); - expect(b.verificationMaterial?.tlogEntries).toHaveLength(0); - }); - }); -}); diff --git a/packages/client/src/__tests__/util/crypto.test.ts b/packages/client/src/__tests__/util/crypto.test.ts index 1d3a202c..8c7d89b6 100644 --- a/packages/client/src/__tests__/util/crypto.test.ts +++ b/packages/client/src/__tests__/util/crypto.test.ts @@ -13,29 +13,15 @@ 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 { KeyObject } from 'crypto'; +import { KeyObject, generateKeyPairSync, sign } from 'crypto'; import { bufferEqual, createPublicKey, - generateKeyPair, hash, randomBytes, - signBlob, verifyBlob, } from '../../util/crypto'; -describe('generateKeyPair', () => { - it('generates an EC keypair', () => { - const keypair = generateKeyPair(); - - expect(keypair.privateKey).toBeDefined(); - expect(keypair.privateKey.asymmetricKeyType).toBe('ec'); - - expect(keypair.publicKey).toBeDefined(); - expect(keypair.publicKey.asymmetricKeyType).toBe('ec'); - }); -}); - describe('createPublicKey', () => { describe('when the input in a PEM-encoded key', () => { const pem = `-----BEGIN PUBLIC KEY----- @@ -74,20 +60,12 @@ describe('hash', () => { }); }); -describe('signBlob', () => { - const key = generateKeyPair(); - it('returns the signature of the blob', () => { - const blob = Buffer.from('hello world'); - const signature = signBlob(blob, key.privateKey); - - expect(signature).toBeTruthy(); - }); -}); - describe('verifyBlob', () => { - const key = generateKeyPair(); + const key = generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); const blob = Buffer.from('hello world'); - const signature = signBlob(blob, key.privateKey); + const signature = sign(null, blob, key.privateKey); describe('when the signature is valid', () => { it('returns true', () => { diff --git a/packages/client/src/__tests__/util/oidc.test.ts b/packages/client/src/__tests__/util/oidc.test.ts deleted file mode 100644 index d3a4aef6..00000000 --- a/packages/client/src/__tests__/util/oidc.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { extractJWTSubject } from '../../util/oidc'; - -describe('extractJWTSubject', () => { - describe('when the JWT is issued by accounts.google.com', () => { - const payload = { - iss: 'https://accounts.google.com', - email: 'foo@bar.com', - }; - - const jwt = `.${Buffer.from(JSON.stringify(payload)).toString('base64')}.`; - - it('should return the email address', () => { - expect(extractJWTSubject(jwt)).toBe(payload.email); - }); - }); - - describe('when the JWT is issued by sigstore.dev', () => { - const payload = { - iss: 'https://oauth2.sigstore.dev/auth', - email: 'foo@bar.com', - }; - - const jwt = `.${Buffer.from(JSON.stringify(payload)).toString('base64')}.`; - - it('should return the email address', () => { - expect(extractJWTSubject(jwt)).toBe(payload.email); - }); - }); - - describe('when the JWT is a generic JWT', () => { - const payload = { - iss: 'https://example.com', - sub: 'foo@bar.com', - }; - const jwt = `.${Buffer.from(JSON.stringify(payload)).toString('base64')}.`; - - it('should return the subject', () => { - expect(extractJWTSubject(jwt)).toBe(payload.sub); - }); - }); -}); diff --git a/packages/client/src/__tests__/util/promise.test.ts b/packages/client/src/__tests__/util/promise.test.ts deleted file mode 100644 index 3398ab70..00000000 --- a/packages/client/src/__tests__/util/promise.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { promiseAny } from '../../util/promise'; - -describe('promiseAny', () => { - describe('when all promises resolve', () => { - it('should return the first resolved promise', async () => { - const promise1 = Promise.resolve('foo'); - const promise2 = Promise.resolve('bar'); - const promise3 = Promise.resolve('baz'); - - const result = await promiseAny([promise1, promise2, promise3]); - expect(result).toBe('foo'); - }); - }); - - describe('when only one promise resolves', () => { - it('should return the first resolved promise', async () => { - const promise1 = Promise.reject('err'); - const promise2 = Promise.resolve('bar'); - const promise3 = Promise.reject('err'); - - const result = await promiseAny([promise1, promise2, promise3]); - expect(result).toBe('bar'); - }); - }); - - describe('when all promises reject', () => { - it('should return all rejections', async () => { - const promise1 = Promise.reject('err1'); - const promise2 = Promise.reject('err2'); - const promise3 = Promise.reject('err3'); - - const result = promiseAny([promise1, promise2, promise3]); - await expect(result).rejects.toEqual(['err1', 'err2', 'err3']); - }); - }); -}); diff --git a/packages/client/src/__tests__/util/ua.test.ts b/packages/client/src/__tests__/util/ua.test.ts deleted file mode 100644 index ff46cc8f..00000000 --- a/packages/client/src/__tests__/util/ua.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { getUserAgent } from '../../util/ua'; - -describe('getUserAgent', () => { - it('returns a user agent string', () => { - expect(getUserAgent()).toMatch(new RegExp('sigstore-js\\/\\d+.\\d+.\\d+')); - }); -}); diff --git a/packages/client/src/ca/format.ts b/packages/client/src/ca/format.ts deleted file mode 100644 index ea72e35b..00000000 --- a/packages/client/src/ca/format.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { KeyObject } from 'crypto'; -import type { SigningCertificateRequest } from '../external/fulcio'; - -export function toCertificateRequest( - identityToken: string, - publicKey: KeyObject, - challenge: Buffer -): SigningCertificateRequest { - return { - credentials: { - oidcIdentityToken: identityToken, - }, - publicKeyRequest: { - publicKey: { - algorithm: 'ECDSA', - content: publicKey - .export({ format: 'pem', type: 'spki' }) - .toString('ascii'), - }, - proofOfPossession: challenge.toString('base64'), - }, - }; -} diff --git a/packages/client/src/ca/index.ts b/packages/client/src/ca/index.ts deleted file mode 100644 index 3c933abe..00000000 --- a/packages/client/src/ca/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { KeyObject } from 'crypto'; -import { InternalError } from '../error'; -import { Fulcio } from '../external'; -import { toCertificateRequest } from './format'; - -import type { FetchOptions } from '../types/fetch'; - -export interface CA { - createSigningCertificate: ( - identityToken: string, - publicKey: KeyObject, - challenge: Buffer - ) => Promise; -} - -export type CAClientOptions = { - fulcioBaseURL: string; -} & FetchOptions; - -export class CAClient implements CA { - private fulcio: Fulcio; - - constructor(options: CAClientOptions) { - this.fulcio = new Fulcio({ - baseURL: options.fulcioBaseURL, - retry: options.retry, - timeout: options.timeout, - }); - } - - public async createSigningCertificate( - identityToken: string, - publicKey: KeyObject, - challenge: Buffer - ): Promise { - const request = toCertificateRequest(identityToken, publicKey, challenge); - - try { - const resp = await this.fulcio.createSigningCertificate(request); - - // Account for the fact that the response may contain either a - // signedCertificateEmbeddedSct or a signedCertificateDetachedSct. - const cert = resp.signedCertificateEmbeddedSct - ? resp.signedCertificateEmbeddedSct - : resp.signedCertificateDetachedSct; - - // Return the first certificate in the chain, which is the signing - // certificate. Specifically not returning the rest of the chain to - // mitigate the risk of errors when verifying the certificate chain. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return cert!.chain.certificates.slice(0, 1); - } catch (err) { - throw new InternalError({ - code: 'CA_CREATE_SIGNING_CERTIFICATE_ERROR', - message: 'error creating signing certificate', - cause: err, - }); - } - } -} diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 4f341fe5..8f34ceb8 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -13,35 +13,26 @@ 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 { CA, CAClient } from './ca'; -import identity, { Provider } from './identity'; -import { TLog, TLogClient } from './tlog'; -import { TSA, TSAClient } from './tsa'; +import { + BundleBuilder, + BundleBuilderOptions, + CIContextProvider, + DSSEBundleBuilder, + FulcioSigner, + IdentityProvider, + MessageSignatureBundleBuilder, + RekorWitness, + Signer, + TSAWitness, + Witness, +} from '@sigstore/sign'; +import identity from './identity'; +import { CallbackSigner, SignerFunc } from './types/signature'; import * as sigstore from './types/sigstore'; import type { FetchOptions, Retry } from './types/fetch'; import type { KeySelector } from './verify'; -interface CAOptions { - fulcioURL?: string; -} - -interface TLogOptions { - rekorURL?: string; -} - -interface TSAOptions { - tsaServerURL?: string; -} - -export interface IdentityProviderOptions { - identityToken?: string; - oidcIssuer?: string; - oidcClientID?: string; - oidcClientSecret?: string; - oidcRedirectURL?: string; -} - export type TUFOptions = { tufMirrorURL?: string; tufRootPath?: string; @@ -49,13 +40,18 @@ export type TUFOptions = { } & FetchOptions; export type SignOptions = { - identityProvider?: Provider; + fulcioURL?: string; + identityProvider?: IdentityProvider; + identityToken?: string; + oidcIssuer?: string; + oidcClientID?: string; + oidcClientSecret?: string; + oidcRedirectURL?: string; + rekorURL?: string; + signer?: SignerFunc; tlogUpload?: boolean; -} & CAOptions & - TLogOptions & - TSAOptions & - FetchOptions & - IdentityProviderOptions; + tsaServerURL?: string; +} & FetchOptions; export type VerifyOptions = { ctLogThreshold?: number; @@ -65,8 +61,8 @@ export type VerifyOptions = { certificateIdentityURI?: string; certificateOIDs?: Record; keySelector?: KeySelector; -} & TLogOptions & - TUFOptions; + rekorURL?: string; +} & TUFOptions; export type CreateVerifierOptions = { keySelector?: KeySelector; @@ -78,32 +74,118 @@ export const DEFAULT_REKOR_URL = 'https://rekor.sigstore.dev'; export const DEFAULT_RETRY: Retry = { retries: 2 }; export const DEFAULT_TIMEOUT = 5000; -export function createCAClient(options: CAOptions & FetchOptions): CA { - return new CAClient({ - fulcioBaseURL: options.fulcioURL || DEFAULT_FULCIO_URL, - retry: options.retry ?? DEFAULT_RETRY, - timeout: options.timeout ?? DEFAULT_TIMEOUT, - }); +export type BundleType = 'messageSignature' | 'dsseEnvelope'; + +export function createBundleBuilder( + bundleType: 'messageSignature', + options: SignOptions +): MessageSignatureBundleBuilder; +export function createBundleBuilder( + bundleType: 'dsseEnvelope', + options: SignOptions +): DSSEBundleBuilder; +export function createBundleBuilder( + bundleType: BundleType, + options: SignOptions +): BundleBuilder { + const bundlerOptions: BundleBuilderOptions = { + signer: initSigner(options), + witnesses: initWitnesses(options), + }; + + switch (bundleType) { + case 'messageSignature': + return new MessageSignatureBundleBuilder(bundlerOptions); + case 'dsseEnvelope': + return new DSSEBundleBuilder(bundlerOptions); + } +} + +// Instantiate a signer based on the supplied options. If a signer function is +// provided, use that. Otherwise, if a Fulcio URL is provided, use the Fulcio +// signer. Otherwise, throw an error. +function initSigner(options: SignOptions): Signer { + if (isCallbackSignerEnabled(options)) { + return new CallbackSigner(options); + } else { + return new FulcioSigner({ + fulcioBaseURL: options.fulcioURL || DEFAULT_FULCIO_URL, + identityProvider: + options.identityProvider || initIdentityProvider(options), + retry: options.retry ?? DEFAULT_RETRY, + timeout: options.timeout ?? DEFAULT_TIMEOUT, + }); + } } -export function createTLogClient(options: TLogOptions & FetchOptions): TLog { - return new TLogClient({ - rekorBaseURL: options.rekorURL || DEFAULT_REKOR_URL, - retry: options.retry ?? DEFAULT_RETRY, - timeout: options.timeout ?? DEFAULT_TIMEOUT, - }); +// Instantiate an identity provider based on the supplied options. If an +// explicit identity token is provided, use that. Otherwise, if an OIDC issuer +// and client ID are provided, use the OIDC provider. Otherwise, use the CI +// context provider. +function initIdentityProvider(options: SignOptions): IdentityProvider { + const token = options.identityToken; + + if (token) { + return { getToken: () => Promise.resolve(token) }; + } else if (options.oidcIssuer && options.oidcClientID) { + return identity.oauthProvider({ + issuer: options.oidcIssuer, + clientID: options.oidcClientID, + clientSecret: options.oidcClientSecret, + redirectURL: options.oidcRedirectURL, + }); + } else { + return new CIContextProvider('sigstore'); + } } -export function createTSAClient( - options: TSAOptions & FetchOptions -): TSA | undefined { - return options.tsaServerURL - ? new TSAClient({ +// Instantiate a collection of witnesses based on the supplied options. +function initWitnesses(options: SignOptions): Witness[] { + const witnesses: Witness[] = []; + + if (isRekorEnabled(options)) { + witnesses.push( + new RekorWitness({ + rekorBaseURL: options.rekorURL || DEFAULT_REKOR_URL, + fetchOnConflict: false, + retry: options.retry ?? DEFAULT_RETRY, + timeout: options.timeout ?? DEFAULT_TIMEOUT, + }) + ); + } + + if (isTSAEnabled(options)) { + witnesses.push( + new TSAWitness({ tsaBaseURL: options.tsaServerURL, retry: options.retry ?? DEFAULT_RETRY, timeout: options.timeout ?? DEFAULT_TIMEOUT, }) - : undefined; + ); + } + + return witnesses; +} + +// Type assertion to ensure that the signer is enabled +function isCallbackSignerEnabled( + options: SignOptions +): options is SignOptions & { signer: SignerFunc } { + return options.signer !== undefined; +} + +// Type assertion to ensure that Rekor is enabled +function isRekorEnabled( + options: SignOptions +): options is SignOptions & { tlogUpload: boolean } { + return options.tlogUpload !== false; +} + +// Type assertion to ensure that TSA is enabled +function isTSAEnabled( + options: SignOptions +): options is SignOptions & { tsaServerURL: string } { + return options.tsaServerURL !== undefined; } // Assembles the AtifactVerificationOptions from the supplied VerifyOptions. @@ -134,7 +216,7 @@ export function artifactVerificationOptions( } const oids = Object.entries( - options.certificateOIDs || {} + options.certificateOIDs || /* istanbul ignore next */ {} ).map(([oid, value]) => ({ oid: { id: oid.split('.').map((s) => parseInt(s, 10)) }, value: Buffer.from(value), @@ -169,33 +251,3 @@ export function artifactVerificationOptions( signers, }; } - -// Translates the IdenityProviderOptions into a list of Providers which -// should be queried to retrieve an identity token. -export function identityProviders( - options: IdentityProviderOptions -): Provider[] { - const idps: Provider[] = []; - const token = options.identityToken; - - // If an explicit identity token is provided, use that. Setup a dummy - // provider that just returns the token. Otherwise, setup the CI context - // provider and (optionally) the OAuth provider. - if (token) { - idps.push({ getToken: () => Promise.resolve(token) }); - } else { - idps.push(identity.ciContextProvider()); - if (options.oidcIssuer && options.oidcClientID) { - idps.push( - identity.oauthProvider({ - issuer: options.oidcIssuer, - clientID: options.oidcClientID, - clientSecret: options.oidcClientSecret, - redirectURL: options.oidcRedirectURL, - }) - ); - } - } - - return idps; -} diff --git a/packages/client/src/error.ts b/packages/client/src/error.ts index 27ebb609..42eff2df 100644 --- a/packages/client/src/error.ts +++ b/packages/client/src/error.ts @@ -24,33 +24,38 @@ class BaseError extends Error { } } -export class VerificationError extends BaseError {} - -export class PolicyError extends BaseError {} - -type InternalErrorCode = - | 'TLOG_FETCH_ENTRY_ERROR' - | 'TLOG_CREATE_ENTRY_ERROR' - | 'CA_CREATE_SIGNING_CERTIFICATE_ERROR' - | 'TSA_CREATE_TIMESTAMP_ERROR' - | 'TUF_FIND_TARGET_ERROR' - | 'TUF_REFRESH_METADATA_ERROR' - | 'TUF_DOWNLOAD_TARGET_ERROR' - | 'TUF_READ_TARGET_ERROR'; - -export class InternalError extends BaseError { - code: InternalErrorCode; +class ErrorWithCode extends BaseError { + code: T; constructor({ code, message, cause, }: { - code: InternalErrorCode; + code: T; message: string; - cause?: any; + cause?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */; }) { super(message, cause); this.code = code; + this.name = this.constructor.name; } } + +export class VerificationError extends BaseError {} + +export class PolicyError extends BaseError {} + +type InternalErrorCode = + | 'TUF_FIND_TARGET_ERROR' + | 'TUF_REFRESH_METADATA_ERROR' + | 'TUF_DOWNLOAD_TARGET_ERROR' + | 'TUF_READ_TARGET_ERROR'; + +export class InternalError extends ErrorWithCode {} + +type SignatureErrorCode = + | 'MISSING_SIGNATURE_ERROR' + | 'MISSING_PUBLIC_KEY_ERROR'; + +export class SignatureError extends ErrorWithCode {} diff --git a/packages/client/src/external/error.ts b/packages/client/src/external/error.ts deleted file mode 100644 index b7c5fdb8..00000000 --- a/packages/client/src/external/error.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 fetch from 'make-fetch-happen'; - -// Convoluted way of getting at the Response type used by make-fetch-happen -type Response = Awaited>; - -export class HTTPError extends Error { - public response: Response; - public statusCode: number; - public location?: string; - - constructor(response: Response) { - super(`HTTP Error: ${response.status} ${response.statusText}`); - this.response = response; - this.statusCode = response.status; - this.location = response.headers?.get('Location') || undefined; - } -} - -export const checkStatus = (response: Response): Response => { - if (response.ok) { - return response; - } else { - throw new HTTPError(response); - } -}; diff --git a/packages/client/src/external/fulcio.ts b/packages/client/src/external/fulcio.ts deleted file mode 100644 index d7d0ceef..00000000 --- a/packages/client/src/external/fulcio.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 fetch, { FetchInterface } from 'make-fetch-happen'; -import { ua } from '../util'; -import { checkStatus } from './error'; - -import type { FetchOptions } from '../types/fetch'; - -export type FulcioOptions = { - baseURL: string; -} & FetchOptions; - -export interface SigningCertificateRequest { - credentials: { - oidcIdentityToken: string; - }; - publicKeyRequest: { - publicKey: { - algorithm: string; - content: string; - }; - proofOfPossession: string; - }; -} - -export interface SigningCertificateResponse { - signedCertificateEmbeddedSct?: { - chain: { certificates: string[] }; - }; - signedCertificateDetachedSct?: { - chain: { - certificates: string[]; - }; - signedCertificateTimestamp: string; - }; -} - -/** - * Fulcio API client. - */ -export class Fulcio { - private fetch: FetchInterface; - private baseUrl: string; - - constructor(options: FulcioOptions) { - this.fetch = fetch.defaults({ - retry: options.retry, - timeout: options.timeout, - headers: { - 'Content-Type': 'application/json', - 'User-Agent': ua.getUserAgent(), - }, - }); - this.baseUrl = options.baseURL; - } - - public async createSigningCertificate( - request: SigningCertificateRequest - ): Promise { - const url = `${this.baseUrl}/api/v2/signingCert`; - - const response = await this.fetch(url, { - method: 'POST', - body: JSON.stringify(request), - }); - checkStatus(response); - - const data = await response.json(); - return data; - } -} diff --git a/packages/client/src/external/index.ts b/packages/client/src/external/index.ts deleted file mode 100644 index 8206a047..00000000 --- a/packages/client/src/external/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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. -*/ -export { HTTPError } from './error'; -export { Fulcio } from './fulcio'; -export { Rekor } from './rekor'; -export { TimestampAuthority } from './tsa'; diff --git a/packages/client/src/external/rekor.ts b/packages/client/src/external/rekor.ts deleted file mode 100644 index 06336c7c..00000000 --- a/packages/client/src/external/rekor.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 fetch, { FetchInterface } from 'make-fetch-happen'; -import { ua } from '../util'; -import { checkStatus } from './error'; - -import type { - LogEntry, - ProposedDSSEEntry, - ProposedEntry, - ProposedHashedRekordEntry, - ProposedIntotoEntry, - InclusionProof as RekorInclusionProof, - SearchIndex, - SearchLogQuery, -} from '@sigstore/rekor-types'; -import type { FetchOptions } from '../types/fetch'; - -export type { - ProposedDSSEEntry, - ProposedEntry, - ProposedHashedRekordEntry, - ProposedIntotoEntry, - RekorInclusionProof, - SearchIndex, - SearchLogQuery, -}; - -// The LogEntry type from @sigstore/rekor-types is a Record type -// mapping the entry's UUID to the entry's data. This is really -// inconvenient to work with, so we define a new type here that -// flattens the data -- the entry's UUID is now a property of the -// entry's data. -export type Entry = { - uuid: string; -} & LogEntry['x']; - -// Client options -export type RekorOptions = { - baseURL: string; -} & FetchOptions; - -/** - * Rekor API client. - */ -export class Rekor { - private fetch: FetchInterface; - - private baseUrl: string; - constructor(options: RekorOptions) { - this.fetch = fetch.defaults({ - retry: options.retry, - timeout: options.timeout, - headers: { - Accept: 'application/json', - 'User-Agent': ua.getUserAgent(), - }, - }); - - this.baseUrl = options.baseURL; - } - - /** - * Create a new entry in the Rekor log. - * @param propsedEntry {ProposedEntry} Data to create a new entry - * @returns {Promise} The created entry - */ - public async createEntry(propsedEntry: ProposedEntry): Promise { - const url = `${this.baseUrl}/api/v1/log/entries`; - - const response = await this.fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(propsedEntry), - }); - checkStatus(response); - - const data = await response.json(); - return entryFromResponse(data); - } - - /** - * Get an entry from the Rekor log. - * @param uuid {string} The UUID of the entry to retrieve - * @returns {Promise} The retrieved entry - */ - public async getEntry(uuid: string): Promise { - const url = `${this.baseUrl}/api/v1/log/entries/${uuid}`; - - const response = await this.fetch(url); - checkStatus(response); - - const data: LogEntry = await response.json(); - return entryFromResponse(data); - } - - /** - * Search the Rekor log index for entries matching the given query. - * @param opts {SearchIndex} Options to search the Rekor log - * @returns {Promise} UUIDs of matching entries - */ - public async searchIndex(opts: SearchIndex): Promise { - const url = `${this.baseUrl}/api/v1/index/retrieve`; - - const response = await this.fetch(url, { - method: 'POST', - body: JSON.stringify(opts), - headers: { 'Content-Type': 'application/json' }, - }); - checkStatus(response); - - const data = await response.json(); - return data; - } - - /** - * Search the Rekor logs for matching the given query. - * @param opts {SearchLogQuery} Query to search the Rekor log - * @returns {Promise} List of matching entries - */ - public async searchLog(opts: SearchLogQuery): Promise { - const url = `${this.baseUrl}/api/v1/log/entries/retrieve`; - - const response = await this.fetch(url, { - method: 'POST', - body: JSON.stringify(opts), - headers: { 'Content-Type': 'application/json' }, - }); - checkStatus(response); - - const rawData: LogEntry[] = await response.json(); - const data = rawData.map((d) => entryFromResponse(d)); - return data; - } -} - -// Unpack the response from the Rekor API into a more convenient format. -function entryFromResponse(data: LogEntry): Entry { - const entries = Object.entries(data); - - if (entries.length != 1) { - throw new Error('Received multiple entries in Rekor response'); - } - - // Grab UUID and entry data from the response - const [uuid, entry] = entries[0]; - - return { - ...entry, - uuid, - }; -} diff --git a/packages/client/src/external/tsa.ts b/packages/client/src/external/tsa.ts deleted file mode 100644 index 6b6e9b67..00000000 --- a/packages/client/src/external/tsa.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2023 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 fetch, { FetchInterface } from 'make-fetch-happen'; -import { ua } from '../util'; -import { checkStatus } from './error'; - -import type { FetchOptions } from '../types/fetch'; - -export interface TimestampRequest { - artifactHash: string; - hashAlgorithm: string; - certificates?: boolean; - nonce?: number; - tsaPolicyOID?: string; -} - -export type TimestampAuthorityOptions = { - baseURL: string; -} & FetchOptions; - -export class TimestampAuthority { - private fetch: FetchInterface; - private baseUrl: string; - - constructor(options: TimestampAuthorityOptions) { - this.fetch = fetch.defaults({ - retry: options.retry, - timeout: options.timeout, - headers: { - 'Content-Type': 'application/json', - 'User-Agent': ua.getUserAgent(), - }, - }); - this.baseUrl = options.baseURL; - } - - public async createTimestamp(request: TimestampRequest): Promise { - const url = `${this.baseUrl}/api/v1/timestamp`; - - const response = await this.fetch(url, { - method: 'POST', - body: JSON.stringify(request), - }); - checkStatus(response); - - return response.buffer(); - } -} diff --git a/packages/client/src/identity/ci.ts b/packages/client/src/identity/ci.ts deleted file mode 100644 index 963dc82b..00000000 --- a/packages/client/src/identity/ci.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 fetch from 'make-fetch-happen'; -import { Provider } from './provider'; -import { promise } from '../util'; - -type ProviderFunc = (audience: string) => Promise; - -// Collection of all the CI-specific providers we have implemented -const providers: ProviderFunc[] = [getGHAToken, getEnv]; - -/** - * CIContextProvider is a composite identity provider which will iterate - * over all of the CI-specific providers and return the token from the first - * one that resolves. - */ -export class CIContextProvider implements Provider { - private audience: string; - - constructor(audience: string) { - this.audience = audience; - } - - // Invoke all registered ProviderFuncs and return the value of whichever one - // resolves first. - public async getToken() { - return promise - .promiseAny(providers.map((getToken) => getToken(this.audience))) - .catch(() => Promise.reject('CI: no tokens available')); - } -} - -/** - * getGHAToken can retrieve an OIDC token when running in a GitHub Actions - * workflow - */ -async function getGHAToken(audience: string): Promise { - // Check to see if we're running in GitHub Actions - if ( - !process.env.ACTIONS_ID_TOKEN_REQUEST_URL || - !process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN - ) { - return Promise.reject('no token available'); - } - - // Construct URL to request token w/ appropriate audience - const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL); - url.searchParams.append('audience', audience); - - const response = await fetch(url.href, { - retry: 2, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`, - }, - }); - - return response.json().then((data) => data.value); -} - -/** - * getEnv can retrieve an OIDC token from an environment variable. - * This matches the behavior of https://github.com/sigstore/cosign/tree/main/pkg/providers/envvar - */ -async function getEnv(): Promise { - if (!process.env.SIGSTORE_ID_TOKEN) { - return Promise.reject('no token available'); - } - - return process.env.SIGSTORE_ID_TOKEN; -} diff --git a/packages/client/src/identity/index.ts b/packages/client/src/identity/index.ts index ed4f4949..8fdef60a 100644 --- a/packages/client/src/identity/index.ts +++ b/packages/client/src/identity/index.ts @@ -13,10 +13,9 @@ 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 { CIContextProvider } from './ci'; +import { IdentityProvider } from '@sigstore/sign'; import { Issuer } from './issuer'; import { OAuthProvider } from './oauth'; -import { Provider } from './provider'; /** * oauthProvider returns a new Provider instance which attempts to retrieve @@ -25,14 +24,14 @@ import { Provider } from './provider'; * @param issuer Base URL of the issuer * @param clientID Client ID for the issuer * @param clientSecret Client secret for the issuer (optional) - * @returns {Provider} + * @returns {IdentityProvider} */ function oauthProvider(options: { issuer: string; clientID: string; clientSecret?: string; redirectURL?: string; -}): Provider { +}): IdentityProvider { return new OAuthProvider({ issuer: new Issuer(options.issuer), clientID: options.clientID, @@ -41,20 +40,6 @@ function oauthProvider(options: { }); } -/** - * ciContextProvider returns a new Provider instance which attempts to retrieve - * an identity token from the CI context. - * - * @param audience audience claim for the generated token - * @returns {Provider} - */ -function ciContextProvider(audience = 'sigstore'): Provider { - return new CIContextProvider(audience); -} - export default { - ciContextProvider, oauthProvider, }; - -export { Provider } from './provider'; diff --git a/packages/client/src/identity/oauth.ts b/packages/client/src/identity/oauth.ts index c168db27..5116da98 100644 --- a/packages/client/src/identity/oauth.ts +++ b/packages/client/src/identity/oauth.ts @@ -19,10 +19,10 @@ import http from 'http'; import fetch from 'make-fetch-happen'; import { AddressInfo, Socket } from 'net'; import { URL, URLSearchParams } from 'url'; - import { crypto, encoding as enc } from '../util'; import { Issuer } from './issuer'; -import { Provider } from './provider'; + +import type { IdentityProvider } from '@sigstore/sign'; interface OAuthProviderOptions { issuer: Issuer; @@ -31,7 +31,7 @@ interface OAuthProviderOptions { redirectURL?: string; } -export class OAuthProvider implements Provider { +export class OAuthProvider implements IdentityProvider { private clientID: string; private clientSecret: string; private issuer: Issuer; @@ -198,7 +198,7 @@ export class OAuthProvider implements Provider { // Open the supplied URL in the user's default browser private async openURL(url: string) { return new Promise((resolve, reject) => { - let open = null; + let open: string | null = null; let command = `"${url}"`; switch (process.platform) { case 'darwin': diff --git a/packages/client/src/identity/provider.ts b/packages/client/src/identity/provider.ts deleted file mode 100644 index af4113b9..00000000 --- a/packages/client/src/identity/provider.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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. -*/ -// Interface representing any identity provider which is capable of returning -// an OIDC token. -export interface Provider { - getToken: () => Promise; -} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 34e5f54c..ffd300c0 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -13,5 +13,5 @@ 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. */ -export type { Provider as IdentityProvider } from './identity'; +export type { IdentityProvider } from '@sigstore/sign'; export * as sigstore from './sigstore'; diff --git a/packages/client/src/sign.ts b/packages/client/src/sign.ts deleted file mode 100644 index 32217b4c..00000000 --- a/packages/client/src/sign.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { SignatureMaterial, SignerFunc } from './types/signature'; -import * as sigstore from './types/sigstore'; -import { crypto, dsse, oidc } from './util'; - -import type { Bundle } from '@sigstore/bundle'; -import type { CA } from './ca'; -import type { Provider } from './identity'; -import type { TLog } from './tlog'; -import type { TSA } from './tsa'; - -export interface SignOptions { - ca: CA; - tlog: TLog; - tsa?: TSA; - identityProviders: Provider[]; - tlogUpload?: boolean; - signer?: SignerFunc; -} - -export class Signer { - private ca: CA; - private tlog: TLog; - private tsa?: TSA; - private tlogUpload: boolean; - private signer: SignerFunc; - - private identityProviders: Provider[] = []; - - constructor(options: SignOptions) { - this.ca = options.ca; - this.tlog = options.tlog; - this.tsa = options.tsa; - this.identityProviders = options.identityProviders; - this.tlogUpload = options.tlogUpload ?? true; - this.signer = options.signer || this.signWithEphemeralKey.bind(this); - } - - public async signBlob(payload: Buffer): Promise { - // Get signature and verification material for payload - const sigMaterial = await this.signer(payload); - - // Calculate artifact digest - const digest = crypto.hash(payload); - - // Create a Rekor entry (if tlogUpload is enabled) - const entry = this.tlogUpload - ? await this.tlog.createMessageSignatureEntry(digest, sigMaterial) - : undefined; - - return sigstore.toMessageSignatureBundle({ - digest, - signature: sigMaterial, - tlogEntry: entry, - timestamp: this.tsa - ? await this.tsa.createTimestamp(sigMaterial.signature) - : undefined, - }); - } - - public async signAttestation( - payload: Buffer, - payloadType: string - ): Promise { - // Pre-authentication encoding to be signed - const paeBuffer = dsse.preAuthEncoding(payloadType, payload); - - // Get signature and verification material for pae - const sigMaterial = await this.signer(paeBuffer); - - const envelope: sigstore.Envelope = { - payloadType, - payload: payload, - signatures: [ - { - keyid: sigMaterial.key?.id || '', - sig: sigMaterial.signature, - }, - ], - }; - - // Create a Rekor entry (if tlogUpload is enabled) - const entry = this.tlogUpload - ? await this.tlog.createDSSEEntry(envelope, sigMaterial) - : undefined; - - return sigstore.toDSSEBundle({ - envelope, - signature: sigMaterial, - tlogEntry: entry, - timestamp: this.tsa - ? await this.tsa.createTimestamp(sigMaterial.signature) - : undefined, - }); - } - - private async signWithEphemeralKey( - payload: Buffer - ): Promise { - // Create emphemeral key pair - const keypair = crypto.generateKeyPair(); - - // Retrieve identity token from one of the supplied identity providers - const identityToken = await this.getIdentityToken(); - - // Extract challenge claim from OIDC token - const subject = oidc.extractJWTSubject(identityToken); - - // Construct challenge value by encrypting subject with private key - const challenge = crypto.signBlob(Buffer.from(subject), keypair.privateKey); - - // Create signing certificate - const certificates = await this.ca.createSigningCertificate( - identityToken, - keypair.publicKey, - challenge - ); - - // Generate artifact signature - const signature = crypto.signBlob(payload, keypair.privateKey); - - return { - signature, - certificates, - key: undefined, - }; - } - - private async getIdentityToken(): Promise { - const aggErrs = []; - - for (const provider of this.identityProviders) { - try { - const token = await provider.getToken(); - if (token) { - return token; - } - } catch (err) { - aggErrs.push(err); - } - } - - throw new Error(`Identity token providers failed: ${aggErrs}`); - } -} diff --git a/packages/client/src/sigstore-utils.ts b/packages/client/src/sigstore-utils.ts index 9dd869de..93f1f0b6 100644 --- a/packages/client/src/sigstore-utils.ts +++ b/packages/client/src/sigstore-utils.ts @@ -14,14 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ import { + BUNDLE_V01_MEDIA_TYPE, + Bundle, SerializedBundle, SerializedEnvelope, bundleToJSON, + envelopeFromJSON, + envelopeToJSON, } from '@sigstore/bundle'; -import { SignOptions, createTLogClient } from './config'; -import { SignerFunc, extractSignatureMaterial } from './types/signature'; -import * as sigstore from './types/sigstore'; -import { dsse } from './util'; +import { RekorWitness, SignatureBundle } from '@sigstore/sign'; +import { + DEFAULT_REKOR_URL, + DEFAULT_RETRY, + DEFAULT_TIMEOUT, + SignOptions, + createBundleBuilder, +} from './config'; +import { SignerFunc } from './types/signature'; export async function createDSSEEnvelope( payload: Buffer, @@ -30,24 +39,12 @@ export async function createDSSEEnvelope( signer: SignerFunc; } ): Promise { - // Pre-authentication encoding to be signed - const paeBuffer = dsse.preAuthEncoding(payloadType, payload); - - // Get signature and verification material for pae - const sigMaterial = await options.signer(paeBuffer); - - const envelope: sigstore.Envelope = { - payloadType, - payload, - signatures: [ - { - keyid: sigMaterial.key?.id || '', - sig: sigMaterial.signature, - }, - ], - }; - - return sigstore.Envelope.toJSON(envelope) as SerializedEnvelope; + const bundler = createBundleBuilder('dsseEnvelope', { + signer: options.signer, + tlogUpload: false, + }); + const bundle = await bundler.create({ data: payload, type: payloadType }); + return envelopeToJSON(bundle.content.dsseEnvelope); } // Accepts a signed DSSE envelope and a PEM-encoded public key to be added to the @@ -55,20 +52,37 @@ export async function createDSSEEnvelope( export async function createRekorEntry( dsseEnvelope: SerializedEnvelope, publicKey: string, + /* istanbul ignore next */ options: SignOptions = {} ): Promise { - const envelope = sigstore.Envelope.fromJSON(dsseEnvelope); - const tlog = createTLogClient(options); + const envelope = envelopeFromJSON(dsseEnvelope); + const content: SignatureBundle = { + $case: 'dsseEnvelope', + dsseEnvelope: envelope, + }; - const sigMaterial = extractSignatureMaterial(envelope, publicKey); - const entry = await tlog.createDSSEEntry(envelope, sigMaterial, { + const tlog = new RekorWitness({ + rekorBaseURL: + options.rekorURL || /* istanbul ignore next */ DEFAULT_REKOR_URL, fetchOnConflict: true, + retry: options.retry ?? DEFAULT_RETRY, + timeout: options.timeout ?? DEFAULT_TIMEOUT, }); - const bundle = sigstore.toDSSEBundle({ - envelope, - signature: sigMaterial, - tlogEntry: entry, - }); + const vm = await tlog.testify(content, publicKey); + + const bundle: Bundle = { + mediaType: BUNDLE_V01_MEDIA_TYPE, + content, + verificationMaterial: { + content: { + $case: 'publicKey', + publicKey: { hint: dsseEnvelope.signatures[0].keyid }, + }, + timestampVerificationData: undefined, + tlogEntries: [...vm.tlogEntries], + }, + }; + return bundleToJSON(bundle); } diff --git a/packages/client/src/sigstore.ts b/packages/client/src/sigstore.ts index 799ae16e..e1737f95 100644 --- a/packages/client/src/sigstore.ts +++ b/packages/client/src/sigstore.ts @@ -20,26 +20,14 @@ import { } from '@sigstore/bundle'; import * as tuf from '@sigstore/tuf'; import * as config from './config'; -import { Signer } from './sign'; import { Verifier } from './verify'; export async function sign( payload: Buffer, options: config.SignOptions = {} ): Promise { - const ca = config.createCAClient(options); - const tlog = config.createTLogClient(options); - const idps = config.identityProviders(options); - const signer = new Signer({ - ca, - tlog, - identityProviders: options.identityProvider - ? [options.identityProvider] - : idps, - tlogUpload: options.tlogUpload, - }); - - const bundle = await signer.signBlob(payload); + const bundler = config.createBundleBuilder('messageSignature', options); + const bundle = await bundler.create({ data: payload }); return bundleToJSON(bundle); } @@ -48,21 +36,8 @@ export async function attest( payloadType: string, options: config.SignOptions = {} ): Promise { - const ca = config.createCAClient(options); - const tlog = config.createTLogClient(options); - const tsa = config.createTSAClient(options); - const idps = config.identityProviders(options); - const signer = new Signer({ - ca, - tlog, - tsa, - identityProviders: options.identityProvider - ? [options.identityProvider] - : idps, - tlogUpload: options.tlogUpload, - }); - - const bundle = await signer.signAttestation(payload, payloadType); + const bundler = config.createBundleBuilder('dsseEnvelope', options); + const bundle = await bundler.create({ data: payload, type: payloadType }); return bundleToJSON(bundle); } diff --git a/packages/client/src/tlog/format.ts b/packages/client/src/tlog/format.ts deleted file mode 100644 index 440b3163..00000000 --- a/packages/client/src/tlog/format.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { SignatureMaterial } from '../types/signature'; -import { Envelope } from '../types/sigstore'; -import { crypto, encoding as enc, json } from '../util'; - -import type { - ProposedDSSEEntry, - ProposedHashedRekordEntry, - ProposedIntotoEntry, -} from '../external/rekor'; - -const DEFAULT_DSSE_API_VERSION = '0.0.1'; -const DEFAULT_HASHEDREKORD_API_VERSION = '0.0.1'; -const DEFAULT_INTOTO_API_VERSION = '0.0.2'; - -// Returns a properly formatted Rekor "dsse" entry for the given DSSE -// envelope and signature -export function toProposedDSSEEntry( - envelope: Envelope, - signature: SignatureMaterial, - apiVersion = DEFAULT_DSSE_API_VERSION -): ProposedDSSEEntry { - switch (apiVersion) { - case '0.0.1': - return toProposedDSSEV001Entry(envelope, signature); - default: - throw new Error(`Unsupported dsse kind API version: ${apiVersion}`); - } -} - -// Returns a properly formatted Rekor "hashedrekord" entry for the given digest -// and signature -export function toProposedHashedRekordEntry( - digest: Buffer, - signature: SignatureMaterial -): ProposedHashedRekordEntry { - const hexDigest = digest.toString('hex'); - const b64Signature = signature.signature.toString('base64'); - const b64Key = enc.base64Encode(toPublicKey(signature)); - - return { - apiVersion: DEFAULT_HASHEDREKORD_API_VERSION, - kind: 'hashedrekord', - spec: { - data: { - hash: { - algorithm: 'sha256', - value: hexDigest, - }, - }, - signature: { - content: b64Signature, - publicKey: { - content: b64Key, - }, - }, - }, - }; -} - -// Returns a properly formatted Rekor "intoto" entry for the given DSSE -// envelope and signature -export function toProposedIntotoEntry( - envelope: Envelope, - signature: SignatureMaterial, - apiVersion = DEFAULT_INTOTO_API_VERSION -): ProposedIntotoEntry { - switch (apiVersion) { - case '0.0.2': - return toProposedIntotoV002Entry(envelope, signature); - default: - throw new Error(`Unsupported intoto kind API version: ${apiVersion}`); - } -} -function toProposedDSSEV001Entry( - envelope: Envelope, - signature: SignatureMaterial -): ProposedDSSEEntry { - return { - apiVersion: '0.0.1', - kind: 'dsse', - spec: { - proposedContent: { - envelope: JSON.stringify(Envelope.toJSON(envelope)), - verifiers: [enc.base64Encode(toPublicKey(signature))], - }, - }, - }; -} - -function toProposedIntotoV002Entry( - envelope: Envelope, - signature: SignatureMaterial -): ProposedIntotoEntry { - // Calculate the value for the payloadHash field in the Rekor entry - const payloadHash = crypto.hash(envelope.payload).toString('hex'); - - // Calculate the value for the hash field in the Rekor entry - const envelopeHash = calculateDSSEHash(envelope, signature); - - // Collect values for re-creating the DSSE envelope. - // Double-encode payload and signature cause that's what Rekor expects - const payload = enc.base64Encode(envelope.payload.toString('base64')); - const sig = enc.base64Encode(envelope.signatures[0].sig.toString('base64')); - const keyid = envelope.signatures[0].keyid; - const publicKey = enc.base64Encode(toPublicKey(signature)); - - // Create the envelope portion of the entry. Note the inclusion of the - // publicKey in the signature struct is not a standard part of a DSSE - // envelope, but is required by Rekor. - const dsseEnv: ProposedIntotoEntry['spec']['content']['envelope'] = { - payloadType: envelope.payloadType, - payload: payload, - signatures: [{ sig, publicKey }], - }; - - // If the keyid is an empty string, Rekor seems to remove it altogether. We - // need to do the same here so that we can properly recreate the entry for - // verification. - if (keyid.length > 0) { - dsseEnv.signatures[0].keyid = keyid; - } - - return { - apiVersion: '0.0.2', - kind: 'intoto', - spec: { - content: { - envelope: dsseEnv, - hash: { algorithm: 'sha256', value: envelopeHash }, - payloadHash: { algorithm: 'sha256', value: payloadHash }, - }, - }, - }; -} - -// Calculates the hash of a DSSE envelope for inclusion in a Rekor entry. -// There is no standard way to do this, so the scheme we're using as as -// follows: -// * payload is base64 encoded -// * signature is base64 encoded (only the first signature is used) -// * keyid is included ONLY if it is NOT an empty string -// * The resulting JSON is canonicalized and hashed to a hex string -function calculateDSSEHash( - envelope: Envelope, - signature: SignatureMaterial -): string { - const dsseEnv: ProposedIntotoEntry['spec']['content']['envelope'] = { - payloadType: envelope.payloadType, - payload: envelope.payload.toString('base64'), - signatures: [ - { - sig: envelope.signatures[0].sig.toString('base64'), - publicKey: toPublicKey(signature), - }, - ], - }; - - // If the keyid is an empty string, Rekor seems to remove it altogether. - if (envelope.signatures[0].keyid.length > 0) { - dsseEnv.signatures[0].keyid = envelope.signatures[0].keyid; - } - - return crypto.hash(json.canonicalize(dsseEnv)).toString('hex'); -} - -function toPublicKey(signature: SignatureMaterial): string { - return signature.certificates - ? signature.certificates[0] - : signature.key.value; -} diff --git a/packages/client/src/tlog/index.ts b/packages/client/src/tlog/index.ts deleted file mode 100644 index e794973b..00000000 --- a/packages/client/src/tlog/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { InternalError } from '../error'; -import { HTTPError, Rekor } from '../external'; -import { SignatureMaterial } from '../types/signature'; -import * as sigstore from '../types/sigstore'; -import { toProposedHashedRekordEntry, toProposedIntotoEntry } from './format'; - -import type { Entry, ProposedEntry } from '../external/rekor'; -import type { FetchOptions } from '../types/fetch'; - -interface CreateEntryOptions { - fetchOnConflict?: boolean; -} - -export interface TLog { - createMessageSignatureEntry: ( - digest: Buffer, - sigMaterial: SignatureMaterial - ) => Promise; - - createDSSEEntry: ( - envelope: sigstore.Envelope, - sigMaterial: SignatureMaterial, - options?: CreateEntryOptions - ) => Promise; -} - -export type TLogClientOptions = { - rekorBaseURL: string; -} & FetchOptions; - -export class TLogClient implements TLog { - private rekor: Rekor; - - constructor(options: TLogClientOptions) { - this.rekor = new Rekor({ - baseURL: options.rekorBaseURL, - retry: options.retry, - timeout: options.timeout, - }); - } - - async createMessageSignatureEntry( - digest: Buffer, - sigMaterial: SignatureMaterial, - options: CreateEntryOptions = {} - ): Promise { - const proposedEntry = toProposedHashedRekordEntry(digest, sigMaterial); - return this.createEntry(proposedEntry, options.fetchOnConflict); - } - - async createDSSEEntry( - envelope: sigstore.Envelope, - sigMaterial: SignatureMaterial, - options: CreateEntryOptions = {} - ): Promise { - const proposedEntry = toProposedIntotoEntry(envelope, sigMaterial); - return this.createEntry(proposedEntry, options.fetchOnConflict); - } - - private async createEntry( - proposedEntry: ProposedEntry, - fetchOnConflict = false - ): Promise { - let entry: Entry; - - try { - entry = await this.rekor.createEntry(proposedEntry); - } catch (err) { - // If the entry already exists, fetch it (if enabled) - if (entryExistsError(err) && fetchOnConflict) { - // Grab the UUID of the existing entry from the location header - const uuid = err.location.split('/').pop() || ''; - try { - entry = await this.rekor.getEntry(uuid); - } catch (err) { - throw new InternalError({ - code: 'TLOG_FETCH_ENTRY_ERROR', - message: 'error fetching tlog entry', - cause: err, - }); - } - } else { - throw new InternalError({ - code: 'TLOG_CREATE_ENTRY_ERROR', - message: 'error creating tlog entry', - cause: err, - }); - } - } - - return entry; - } -} - -function entryExistsError( - value: unknown -): value is HTTPError & { location: string } { - return ( - value instanceof HTTPError && - value.statusCode === 409 && - value.location !== undefined - ); -} diff --git a/packages/client/src/tlog/verify/body.ts b/packages/client/src/tlog/verify/body.ts index 847a61a1..dec8a64a 100644 --- a/packages/client/src/tlog/verify/body.ts +++ b/packages/client/src/tlog/verify/body.ts @@ -27,7 +27,7 @@ import type { ProposedEntry, ProposedHashedRekordEntry, ProposedIntotoEntry, -} from '../../external/rekor'; +} from '@sigstore/rekor-types'; const TLOG_MISMATCH_ERROR_MSG = 'bundle content and tlog entry do not match'; diff --git a/packages/client/src/tsa/index.ts b/packages/client/src/tsa/index.ts deleted file mode 100644 index f95735c9..00000000 --- a/packages/client/src/tsa/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 { InternalError } from '../error'; -import { TimestampAuthority } from '../external'; -import { crypto } from '../util'; - -import type { FetchOptions } from '../types/fetch'; - -export interface TSA { - createTimestamp: (signature: Buffer) => Promise; -} - -export type TSAClientOptions = { - tsaBaseURL: string; -} & FetchOptions; - -export class TSAClient implements TSA { - private tsa: TimestampAuthority; - - constructor(options: TSAClientOptions) { - this.tsa = new TimestampAuthority({ - baseURL: options.tsaBaseURL, - retry: options.retry, - timeout: options.timeout, - }); - } - - public async createTimestamp(signature: Buffer): Promise { - const request = { - artifactHash: crypto.hash(signature).toString('base64'), - hashAlgorithm: 'sha256', - }; - - try { - return await this.tsa.createTimestamp(request); - } catch (err) { - throw new InternalError({ - code: 'TSA_CREATE_TIMESTAMP_ERROR', - message: 'error creating timestamp', - cause: err, - }); - } - } -} diff --git a/packages/client/src/types/signature.ts b/packages/client/src/types/signature.ts index 841c6189..1ccacf4b 100644 --- a/packages/client/src/types/signature.ts +++ b/packages/client/src/types/signature.ts @@ -13,7 +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 { Envelope } from './sigstore'; +import { Signature, Signer } from '@sigstore/sign'; +import { SignatureError } from '../error'; import { OneOf } from './utility'; interface VerificationMaterial { @@ -32,18 +33,45 @@ export type SignatureMaterial = { export type SignerFunc = (payload: Buffer) => Promise; -export function extractSignatureMaterial( - dsseEnvelope: Envelope, - publicKey: string -): SignatureMaterial { - const signature = dsseEnvelope.signatures[0]; - - return { - signature: signature.sig, - key: { - id: signature.keyid, - value: publicKey, - }, - certificates: undefined, - }; +type CallbackSignerOptions = { + signer: SignerFunc; +}; + +// Adapter to allow the legacy SignerFunc callback to be used as a new Signer +// interface. +export class CallbackSigner implements Signer { + private signer: SignerFunc; + + constructor(options: CallbackSignerOptions) { + this.signer = options.signer; + } + + public async sign(data: Buffer): Promise { + const sigMaterial = await this.signer(data); + + // Since we're getting data from an external source, we need to validate + // that it's well-formed and complete. + if (!sigMaterial.signature) { + throw new SignatureError({ + code: 'MISSING_SIGNATURE_ERROR', + message: 'no signature returned from signer', + }); + } + + if (!sigMaterial.key?.value) { + throw new SignatureError({ + code: 'MISSING_PUBLIC_KEY_ERROR', + message: 'no key returned from signer', + }); + } + + return { + signature: sigMaterial.signature, + key: { + $case: 'publicKey', + hint: sigMaterial.key.id, + publicKey: sigMaterial.key.value, + }, + }; + } } diff --git a/packages/client/src/types/sigstore.ts b/packages/client/src/types/sigstore.ts index 5f4e67c7..a5767285 100644 --- a/packages/client/src/types/sigstore.ts +++ b/packages/client/src/types/sigstore.ts @@ -13,35 +13,17 @@ 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 { BUNDLE_V01_MEDIA_TYPE } from '@sigstore/bundle'; -import { HashAlgorithm } from '@sigstore/protobuf-specs'; -import { encoding as enc, pem } from '../util'; -import { SignatureMaterial } from './signature'; -import type { Bundle, TransparencyLogEntry } from '@sigstore/bundle'; import type { ArtifactVerificationOptions, - Envelope, - InclusionProof, PublicKey, - TimestampVerificationData, TransparencyLogInstance, } from '@sigstore/protobuf-specs'; -import type { - Entry, - ProposedEntry, - RekorInclusionProof, -} from '../external/rekor'; import type { WithRequired } from './utility'; // Enums from protobuf-specs -// TODO: Move Envelope to "type" export once @sigstore/sign is a thing -export { - Envelope, - HashAlgorithm, - PublicKeyDetails, - SubjectAlternativeNameType, -} from '@sigstore/protobuf-specs'; +export { SubjectAlternativeNameType } from '@sigstore/protobuf-specs'; + // Types from protobuf-specs export type { ArtifactVerificationOptions, @@ -50,6 +32,7 @@ export type { CertificateAuthority, CertificateIdentities, CertificateIdentity, + Envelope, ObjectIdentifierValuePair, PublicKey, SubjectAlternativeName, @@ -88,149 +71,3 @@ export type ViableTransparencyLogInstance = TransparencyLogInstance & { logId: NonNullable; publicKey: WithRequired; }; - -// All of the following functions are used to construct a ValidBundle -// from various types of input. When this code moves into the -// @sigstore/sign package, these functions will be exported from there. -export function toDSSEBundle({ - envelope, - signature, - tlogEntry, - timestamp, -}: { - envelope: Envelope; - signature: SignatureMaterial; - tlogEntry?: Entry; - timestamp?: Buffer; -}): Bundle { - return { - mediaType: BUNDLE_V01_MEDIA_TYPE, - content: { $case: 'dsseEnvelope', dsseEnvelope: envelope }, - verificationMaterial: toVerificationMaterial({ - signature, - tlogEntry, - timestamp, - }), - }; -} - -export function toMessageSignatureBundle({ - digest, - signature, - tlogEntry, - timestamp, -}: { - digest: Buffer; - signature: SignatureMaterial; - tlogEntry?: Entry; - timestamp?: Buffer; -}): Bundle { - return { - mediaType: BUNDLE_V01_MEDIA_TYPE, - content: { - $case: 'messageSignature', - messageSignature: { - messageDigest: { - algorithm: HashAlgorithm.SHA2_256, - digest: digest, - }, - signature: signature.signature, - }, - }, - verificationMaterial: toVerificationMaterial({ - signature, - tlogEntry, - timestamp, - }), - }; -} - -function toTransparencyLogEntry(entry: Entry): TransparencyLogEntry { - /* istanbul ignore next */ - const b64SET = entry.verification?.signedEntryTimestamp || ''; - const set = Buffer.from(b64SET, 'base64'); - const logID = Buffer.from(entry.logID, 'hex'); - const proof = entry.verification?.inclusionProof - ? toInclusionProof(entry.verification.inclusionProof) - : undefined; - - // Parse entry body so we can extract the kind and version. - const bodyJSON = enc.base64Decode(entry.body); - const entryBody: ProposedEntry = JSON.parse(bodyJSON); - - return { - inclusionPromise: { - signedEntryTimestamp: set, - }, - logIndex: entry.logIndex.toString(), - logId: { - keyId: logID, - }, - integratedTime: entry.integratedTime.toString(), - kindVersion: { - kind: entryBody.kind, - version: entryBody.apiVersion, - }, - inclusionProof: proof, - canonicalizedBody: Buffer.from(entry.body, 'base64'), - }; -} - -function toInclusionProof(proof: RekorInclusionProof): InclusionProof { - return { - logIndex: proof.logIndex.toString(), - rootHash: Buffer.from(proof.rootHash, 'hex'), - treeSize: proof.treeSize.toString(), - checkpoint: { - envelope: proof.checkpoint, - }, - hashes: proof.hashes.map((h) => Buffer.from(h, 'hex')), - }; -} - -function toVerificationMaterial({ - signature, - tlogEntry, - timestamp, -}: { - signature: SignatureMaterial; - tlogEntry?: Entry; - timestamp?: Buffer; -}): Bundle['verificationMaterial'] { - return { - content: signature.certificates - ? toVerificationMaterialx509CertificateChain(signature.certificates) - : toVerificationMaterialPublicKey(signature.key.id || ''), - tlogEntries: tlogEntry ? [toTransparencyLogEntry(tlogEntry)] : [], - timestampVerificationData: timestamp - ? toTimestampVerificationData(timestamp) - : undefined, - }; -} - -function toVerificationMaterialx509CertificateChain( - certificates: string[] -): Bundle['verificationMaterial']['content'] { - return { - $case: 'x509CertificateChain', - x509CertificateChain: { - certificates: certificates.map((c) => ({ - rawBytes: pem.toDER(c), - })), - }, - }; -} - -function toVerificationMaterialPublicKey( - hint: string -): Bundle['verificationMaterial']['content'] { - return { $case: 'publicKey', publicKey: { hint } }; -} - -function toTimestampVerificationData( - timestamp: Buffer -): TimestampVerificationData { - return { - rfc3161Timestamps: [{ signedTimestamp: timestamp }], - }; -} diff --git a/packages/client/src/util/crypto.ts b/packages/client/src/util/crypto.ts index 0e921d72..d9eb3828 100644 --- a/packages/client/src/util/crypto.ts +++ b/packages/client/src/util/crypto.ts @@ -13,18 +13,10 @@ 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 crypto, { BinaryLike, KeyLike, KeyPairKeyObjectResult } from 'crypto'; +import crypto, { BinaryLike, KeyLike } from 'crypto'; -const EC_KEYPAIR_TYPE = 'ec'; -const P256_CURVE = 'P-256'; const SHA256_ALGORITHM = 'sha256'; -export function generateKeyPair(): KeyPairKeyObjectResult { - return crypto.generateKeyPairSync(EC_KEYPAIR_TYPE, { - namedCurve: P256_CURVE, - }); -} - export function createPublicKey(key: string | Buffer): KeyLike { if (typeof key === 'string') { return crypto.createPublicKey(key); @@ -33,13 +25,6 @@ export function createPublicKey(key: string | Buffer): KeyLike { } } -export function signBlob( - data: NodeJS.ArrayBufferView, - privateKey: KeyLike -): Buffer { - return crypto.sign(null, data, privateKey); -} - export function verifyBlob( data: Buffer, key: KeyLike, @@ -51,6 +36,7 @@ export function verifyBlob( try { return crypto.verify(algorithm, data, key, signature); } catch (e) { + /* istanbul ignore next */ return false; } } diff --git a/packages/client/src/util/index.ts b/packages/client/src/util/index.ts index c7853b72..ca980a02 100644 --- a/packages/client/src/util/index.ts +++ b/packages/client/src/util/index.ts @@ -18,7 +18,4 @@ export * as crypto from './crypto'; export * as dsse from './dsse'; export * as encoding from './encoding'; export * as json from './json'; -export * as oidc from './oidc'; export * as pem from './pem'; -export * as promise from './promise'; -export * as ua from './ua'; diff --git a/packages/client/src/util/oidc.ts b/packages/client/src/util/oidc.ts deleted file mode 100644 index 77626e40..00000000 --- a/packages/client/src/util/oidc.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 * as enc from './encoding'; - -type JWTSubject = { - iss: string; - sub: string; - email: string; -}; - -export function extractJWTSubject(jwt: string): string { - const parts = jwt.split('.', 3); - const payload: JWTSubject = JSON.parse(enc.base64Decode(parts[1])); - - switch (payload.iss) { - case 'https://accounts.google.com': - case 'https://oauth2.sigstore.dev/auth': - return payload.email; - default: - return payload.sub; - } -} diff --git a/packages/client/src/util/promise.ts b/packages/client/src/util/promise.ts deleted file mode 100644 index 977b45f7..00000000 --- a/packages/client/src/util/promise.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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. -*/ - -// Implementation of Promise.any (not available until Node v15). -// We're basically inverting the logic of Promise.all and taking advantage -// of the fact that Promise.all will return early on the first rejection. -// By reversing the resolve/reject logic we can use this to return early -// on the first resolved promise. -export const promiseAny = async ( - values: Iterable> -): Promise => { - return Promise.all( - [...values].map( - (promise) => - new Promise((resolve, reject) => promise.then(reject, resolve)) - ) - ).then( - (errors) => Promise.reject(errors), - (value) => Promise.resolve(value) - ); -}; diff --git a/packages/client/src/util/ua.ts b/packages/client/src/util/ua.ts deleted file mode 100644 index 2b23ee0e..00000000 --- a/packages/client/src/util/ua.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2022 The Sigstore Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -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 os from 'os'; - -// Format User-Agent: / () -// source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent -export const getUserAgent = (): string => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const packageVersion = require('../../package.json').version; - const nodeVersion = process.version; - const platformName = os.platform(); - const archName = os.arch(); - return `sigstore-js/${packageVersion} (Node ${nodeVersion}) (${platformName}/${archName})`; -}; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 98f1b6a3..4d6e6762 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -12,6 +12,8 @@ ], "references": [ { "path": "../bundle" }, + { "path": "../mock" }, + { "path": "../sign" }, { "path": "../tuf" }, { "path": "../rekor-types" }, { "path": "../jest" }