Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/angry-hands-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 1 addition & 1 deletion packages/sign/src/__tests__/external/rekor-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('RekorV2', () => {
expect(typeof result.integratedTime).toBe('string');
expect(result.kindVersion).toBeDefined();
expect(result.kindVersion?.kind).toBe('hashedrekord');
expect(result.inclusionPromise).toBeDefined();
expect(result.inclusionPromise).toBeUndefined();
});
});

Expand Down
209 changes: 207 additions & 2 deletions packages/sign/src/__tests__/witness/tlog/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2023 The Sigstore Authors.
Copyright 2025 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.
Expand All @@ -15,7 +15,14 @@
*/
import nock from 'nock';
import { InternalError } from '../../../error';
import { ProposedEntry, TLogClient } from '../../../witness/tlog/client';
import {
ProposedEntry,
TLogClient,
TLogV2Client,
} from '../../../witness/tlog/client';

import type { CreateEntryRequest } from '@sigstore/protobuf-specs/rekor/v2';
import assert from 'assert';

describe('TLogClient', () => {
const rekorBaseURL = 'http://localhost:8080';
Expand Down Expand Up @@ -194,3 +201,201 @@
});
});
});

describe('TLogV2Client', () => {
const rekorBaseURL = 'http://localhost:8080';

describe('constructor', () => {
it('should create a new instance', () => {
const client = new TLogV2Client({ rekorBaseURL });
expect(client).toBeDefined();
});
});

describe('createEntry', () => {
const subject = new TLogV2Client({ rekorBaseURL, retry: false });

const createEntryRequest: CreateEntryRequest = {
spec: {
$case: 'hashedRekordRequestV002',
hashedRekordRequestV002: {
digest: Buffer.from('digest'),
signature: {
content: Buffer.from('signature'),
verifier: {
keyDetails: 5, // PKIX_ECDSA_P256_SHA_256
verifier: {
$case: 'x509Certificate',
x509Certificate: {
rawBytes: Buffer.from('certificate'),
},
},
},
},
},
},
};

const rekorEntry = {
logIndex: '2513258',
logId: {
keyId: 'zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=',
},
kindVersion: {
kind: 'hashedrekord',
version: '0.0.2',
},
integratedTime: '0',
inclusionPromise: null,
inclusionProof: {
logIndex: '2513258',
rootHash: '+8vUkEgBK/ansexBUomzocaWoPEmPIzxJC/y+xNMQN4=',
treeSize: '412115',
hashes: [
'KIoYVJ0TqmaEkFboP7YWTjSh8vFjVECmokcTOAByfIM=',
'Umf0h0cK2hegTzNnSgsXszyiA4bp5OvEvP+GrWq3C8w=',
'vNQeSNBfepYZI2Ez3ViKdCft0JH87ZS8IGKwixxUVjc=',
'JBAugd5awOqmHXIKgz1MOjlR5f37VqmP0bWoRVcHX5M=',
'xYH2mAxGxfOgvSOLnItT2LsJt+Z2a2egjf8QJFwK7jA=',
'PbZWM3NitzChx9A22m/kddDtzh2bAKX5Fy7j76l3z2k=',
'sKX9Sbsahvw5DiC2oP6pbZsDi1NzuNS1nIULXkCC57o=',
'pfTCxXHnCM253jwYxVJcuUhmoTTtDxznn92QhN0M4Ws=',
'cJ8uk2ZvZyfg+HRILKOHcyHu2pvI8Fz3R1MyMvyzWtA=',
],
checkpoint: {
envelope:
'log2025-1.rekor.sigstore.dev\n412115\n+8vUkEgBK/ansexBUomzocaWoPEmPIzxJC/y+xNMQN4=\n\n— log2025-1.rekor.sigstore.dev zxGZFahCZ/+MqTjH4rC5MWcdLDWbpetE5l30RZfQc4BQkRjWSoKipEUPjvHENeZDHIlAsuezJcLzUVvItpNjaSRoMAs=\n',
},
},
canonicalizedBody: Buffer.from(
JSON.stringify(createEntryRequest)
).toString('base64'),
};

describe('when Rekor returns an error', () => {
beforeEach(() => {
nock(rekorBaseURL)
.matchHeader('Accept', 'application/json')
.matchHeader('Content-Type', 'application/json')
.post('/api/v2/log/entries')
.reply(500, {});
});

it('returns an error', async () => {
await expect(
subject.createEntry(createEntryRequest)
).rejects.toThrowWithCode(InternalError, 'TLOG_CREATE_ENTRY_ERROR');
});
});

describe('when Rekor returns a valid response', () => {
beforeEach(() => {
nock(rekorBaseURL)
.matchHeader('Accept', 'application/json')
.matchHeader('Content-Type', 'application/json')
.post('/api/v2/log/entries')
.reply(201, rekorEntry);
});

it('returns a tlog entry', async () => {
const entry = await subject.createEntry(createEntryRequest);

expect(entry.logIndex).toEqual(rekorEntry.logIndex);
expect(entry.logId.keyId).toEqual(
Buffer.from(rekorEntry.logId.keyId, 'base64')
);
expect(entry.integratedTime).toEqual(rekorEntry.integratedTime);
expect(entry.kindVersion).toEqual(rekorEntry.kindVersion);
expect(entry.inclusionPromise).toBeUndefined();
assert(entry.inclusionProof);
expect(entry.inclusionProof.logIndex).toEqual(
rekorEntry.inclusionProof.logIndex
);
expect(entry.inclusionProof.rootHash).toEqual(
Buffer.from(rekorEntry.inclusionProof.rootHash, 'base64')
);
expect(entry.inclusionProof.treeSize).toEqual(
rekorEntry.inclusionProof.treeSize
);
expect(entry.inclusionProof.hashes).toEqual(
rekorEntry.inclusionProof.hashes.map((h: string) =>
Buffer.from(h, 'base64')
)
);
assert(entry.inclusionProof.checkpoint);
expect(entry.inclusionProof.checkpoint.envelope).toEqual(
rekorEntry.inclusionProof.checkpoint.envelope
);
expect(entry.canonicalizedBody).toEqual(
Buffer.from(rekorEntry.canonicalizedBody, 'base64')
);
});
});

describe('when Rekor returns an incomplete response', () => {
describe('when logId is missing', () => {
const incompleteEntry = {
...rekorEntry,
logId: undefined,
};

beforeEach(() => {
nock(rekorBaseURL)
.matchHeader('Accept', 'application/json')
.matchHeader('Content-Type', 'application/json')
.post('/api/v2/log/entries')
.reply(201, incompleteEntry);
});

it('returns an error', async () => {
await expect(
subject.createEntry(createEntryRequest)
).rejects.toThrowWithCode(InternalError, 'TLOG_CREATE_ENTRY_ERROR');
});
});

describe('when kindVersion is missing', () => {
const incompleteEntry = {
...rekorEntry,
kindVersion: undefined,
};

beforeEach(() => {
nock(rekorBaseURL)
.matchHeader('Accept', 'application/json')
.matchHeader('Content-Type', 'application/json')
.post('/api/v2/log/entries')
.reply(201, incompleteEntry);
});

it('returns an error', async () => {
await expect(
subject.createEntry(createEntryRequest)
).rejects.toThrowWithCode(InternalError, 'TLOG_CREATE_ENTRY_ERROR');
});
});
});

describe('when Rekor returns a valid response after retry', () => {
let scope: nock.Scope;

Check failure on line 380 in packages/sign/src/__tests__/witness/tlog/client.test.ts

View workflow job for this annotation

GitHub Actions / Lint/build code

'scope' is assigned a value but never used

beforeEach(() => {
scope = nock(rekorBaseURL)
.post('/api/v2/log/entries')
.reply(500, { message: 'oops' })
.post('/api/v2/log/entries')
.reply(201, rekorEntry);
});

it('returns a tlog entry', async () => {
const subject = new TLogV2Client({
rekorBaseURL,
retry: { retries: 1, factor: 0, minTimeout: 1, maxTimeout: 1 },
});
const entry = await subject.createEntry(createEntryRequest);

expect(entry.logIndex).toEqual(rekorEntry.logIndex);
});
});
});
});
145 changes: 141 additions & 4 deletions packages/sign/src/__tests__/witness/tlog/entry.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2023 The Sigstore Authors.
Copyright 2025 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.
Expand All @@ -14,10 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { envelopeToJSON } from '@sigstore/bundle';
import { HashAlgorithm } from '@sigstore/protobuf-specs';
import { HashAlgorithm, PublicKeyDetails } from '@sigstore/protobuf-specs';
import assert from 'assert';
import { crypto, encoding as enc } from '../../../util';
import { toProposedEntry } from '../../../witness/tlog/entry';
import { crypto, encoding as enc, pem } from '../../../util';
import {
toCreateEntryRequest,
toProposedEntry,
} from '../../../witness/tlog/entry';

import type { SignatureBundle } from '../../../witness';

Expand Down Expand Up @@ -234,3 +237,137 @@ describe('toProposedEntry', () => {
});
});
});

describe('toCreateEntryRequest', () => {
const publicKey = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
const signature = Buffer.from('signature');

describe('when a message signature is provided', () => {
const sigBundle: SignatureBundle = {
$case: 'messageSignature',
messageSignature: {
signature: signature,
messageDigest: {
algorithm: HashAlgorithm.SHA2_256,
digest: Buffer.from('digest'),
},
},
};

it('returns a valid CreateEntryRequest with hashedRekordRequestV002', () => {
const request = toCreateEntryRequest(sigBundle, publicKey);

expect(request.spec).toBeTruthy();
expect(request.spec?.$case).toBe('hashedRekordRequestV002');

if (request.spec?.$case === 'hashedRekordRequestV002') {
const hashedRekord = request.spec.hashedRekordRequestV002;

// Check digest
expect(hashedRekord.digest).toEqual(
sigBundle.messageSignature.messageDigest.digest
);

// Check signature content
expect(hashedRekord.signature).toBeTruthy();
assert(hashedRekord.signature);
expect(hashedRekord.signature.content).toEqual(
sigBundle.messageSignature.signature
);

// Check verifier
expect(hashedRekord.signature.verifier).toBeTruthy();
assert(hashedRekord.signature.verifier);
expect(hashedRekord.signature.verifier.keyDetails).toBe(
PublicKeyDetails.PKIX_ECDSA_P256_SHA_256
);
expect(hashedRekord.signature.verifier.verifier?.$case).toBe(
'x509Certificate'
);

if (
hashedRekord.signature.verifier.verifier?.$case === 'x509Certificate'
) {
expect(
hashedRekord.signature.verifier.verifier.x509Certificate.rawBytes
).toEqual(Buffer.from(publicKey, 'base64'));
}
}
});
});

describe('when a DSSE envelope is provided', () => {
describe('when the keyid is a non-empty string', () => {
const sigBundle: SignatureBundle = {
$case: 'dsseEnvelope',
dsseEnvelope: {
signatures: [{ keyid: '123', sig: signature }],
payloadType: 'application/vnd.in-toto+json',
payload: Buffer.from('payload'),
},
};

it('returns a valid CreateEntryRequest with dsseRequestV002', () => {
const request = toCreateEntryRequest(sigBundle, publicKey);

expect(request.spec).toBeTruthy();
expect(request.spec?.$case).toBe('dsseRequestV002');

if (request.spec?.$case === 'dsseRequestV002') {
const dsseRequest = request.spec.dsseRequestV002;

// Check envelope
expect(dsseRequest.envelope).toEqual(sigBundle.dsseEnvelope);

// Check verifiers array
expect(dsseRequest.verifiers).toHaveLength(1);
const verifier = dsseRequest.verifiers[0];

expect(verifier.keyDetails).toBe(
PublicKeyDetails.PKIX_ECDSA_P256_SHA_256
);
expect(verifier.verifier?.$case).toBe('x509Certificate');

if (verifier.verifier?.$case === 'x509Certificate') {
expect(verifier.verifier.x509Certificate.rawBytes).toEqual(
pem.toDER(publicKey)
);
}
}
});
});

describe('when the keyid is an empty string', () => {
const sigBundle: SignatureBundle = {
$case: 'dsseEnvelope',
dsseEnvelope: {
signatures: [{ keyid: '', sig: signature }],
payloadType: 'application/vnd.in-toto+json',
payload: Buffer.from('payload'),
},
};

it('returns a valid CreateEntryRequest with dsseRequestV002', () => {
const request = toCreateEntryRequest(sigBundle, publicKey);

expect(request.spec).toBeTruthy();
expect(request.spec?.$case).toBe('dsseRequestV002');

if (request.spec?.$case === 'dsseRequestV002') {
const dsseRequest = request.spec.dsseRequestV002;

// Check envelope is preserved exactly
assert(dsseRequest.envelope);
expect(dsseRequest.envelope).toEqual(sigBundle.dsseEnvelope);
expect(dsseRequest.envelope.signatures[0].keyid).toBe('');

// Check verifiers
expect(dsseRequest.verifiers).toHaveLength(1);
expect(dsseRequest.verifiers[0].keyDetails).toBe(
PublicKeyDetails.PKIX_ECDSA_P256_SHA_256
);
}
});
});
});
});
Loading
Loading