🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@sigstore/verify

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sigstore/verify - npm Package Compare versions

Comparing version
3.0.0
to
3.1.0
+11
dist/tlog/checkpoint.d.ts
import { TLogAuthority } from '../trust';
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
export declare function verifyCheckpoint(entry: TLogEntryWithInclusionProof, tlogs: TLogAuthority[]): LogCheckpoint;
export declare class LogCheckpoint {
readonly origin: string;
readonly logSize: bigint;
readonly logHash: Buffer;
readonly rest: string[];
constructor(origin: string, logSize: bigint, logHash: Buffer, rest: string[]);
static fromString(note: string): LogCheckpoint;
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogCheckpoint = void 0;
exports.verifyCheckpoint = verifyCheckpoint;
/*
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.
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.
*/
const core_1 = require("@sigstore/core");
const error_1 = require("../error");
// Separator between the note and the signatures in a checkpoint
const CHECKPOINT_SEPARATOR = '\n\n';
// Checkpoint signatures are of the following form:
// "– <identity> <key_hint+signature_bytes>\n"
// where:
// - the prefix is an emdash (U+2014).
// - <identity> gives a human-readable representation of the signing ID.
// - <key_hint+signature_bytes> is the first 4 bytes of the SHA256 hash of the
// associated public key followed by the signature bytes.
const SIGNATURE_REGEX = /\u2014 (\S+) (\S+)\n/g;
// Verifies the checkpoint value in the given tlog entry. There are two steps
// to the verification:
// 1. Verify that all signatures in the checkpoint can be verified against a
// trusted public key
// 2. Verify that the root hash in the checkpoint matches the root hash in the
// inclusion proof
// See: https://github.com/transparency-dev/formats/blob/main/log/README.md
function verifyCheckpoint(entry, tlogs) {
const inclusionProof = entry.inclusionProof;
const signedNote = SignedNote.fromString(inclusionProof.checkpoint.envelope);
const checkpoint = LogCheckpoint.fromString(signedNote.note);
// Verify that the signatures in the checkpoint are all valid
if (!verifySignedNote(signedNote, tlogs)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'invalid checkpoint signature',
});
}
return checkpoint;
}
// Verifies the signatures in the SignedNote. For each signature, the
// corresponding transparency log is looked up by the key hint and the
// signature is verified against the public key in the transparency log.
// Throws an error if any of the signatures are invalid.
function verifySignedNote(signedNote, tlogs) {
const data = Buffer.from(signedNote.note, 'utf-8');
return signedNote.signatures.some((signature) => {
// Find the transparency log instance with the matching key hint
const tlog = tlogs.find((tlog) => core_1.crypto.bufferEqual(tlog.logID.subarray(0, 4), signature.keyHint) &&
tlog.baseURL.match(signature.name) // Match the name to the base URL of the tlog
);
if (!tlog) {
return false;
}
return core_1.crypto.verify(data, tlog.publicKey, signature.signature);
});
}
// SignedNote represents a signed note from a transparency log checkpoint. Consists
// of a body (or note) and one more signatures calculated over the body. See
// https://github.com/transparency-dev/formats/blob/main/log/README.md#signed-envelope
class SignedNote {
note;
signatures;
constructor(note, signatures) {
this.note = note;
this.signatures = signatures;
}
// Deserialize a SignedNote from a string
static fromString(envelope) {
if (!envelope.includes(CHECKPOINT_SEPARATOR)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'missing checkpoint separator',
});
}
// Split the note into the header and the data portions at the separator
const split = envelope.indexOf(CHECKPOINT_SEPARATOR);
const header = envelope.slice(0, split + 1);
const data = envelope.slice(split + CHECKPOINT_SEPARATOR.length);
// Find all the signature lines in the data portion
const matches = data.matchAll(SIGNATURE_REGEX);
// Parse each of the matched signature lines into the name and signature.
// The first four bytes of the signature are the key hint (should match the
// first four bytes of the log ID), and the rest is the signature itself.
const signatures = Array.from(matches, (match) => {
const [, name, signature] = match;
const sigBytes = Buffer.from(signature, 'base64');
if (sigBytes.length < 5) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'malformed checkpoint signature',
});
}
return {
name,
keyHint: sigBytes.subarray(0, 4),
signature: sigBytes.subarray(4),
};
});
if (signatures.length === 0) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'no signatures found in checkpoint',
});
}
return new SignedNote(header, signatures);
}
}
// LogCheckpoint represents a transparency log checkpoint. Consists of the
// following:
// - origin: the name of the transparency log
// - logSize: the size of the log at the time of the checkpoint
// - logHash: the root hash of the log at the time of the checkpoint
// - rest: the rest of the checkpoint body, which is a list of log entries
// See:
// https://github.com/transparency-dev/formats/blob/main/log/README.md#checkpoint-body
class LogCheckpoint {
origin;
logSize;
logHash;
rest;
constructor(origin, logSize, logHash, rest) {
this.origin = origin;
this.logSize = logSize;
this.logHash = logHash;
this.rest = rest;
}
static fromString(note) {
const lines = note.trimEnd().split('\n');
if (lines.length < 3) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'too few lines in checkpoint header',
});
}
const origin = lines[0];
const logSize = BigInt(lines[1]);
const rootHash = Buffer.from(lines[2], 'base64');
const rest = lines.slice(3);
return new LogCheckpoint(origin, logSize, rootHash, rest);
}
}
exports.LogCheckpoint = LogCheckpoint;
import { LogCheckpoint } from './checkpoint';
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
export declare function verifyMerkleInclusion(entry: TLogEntryWithInclusionProof, checkpoint: LogCheckpoint): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyMerkleInclusion = verifyMerkleInclusion;
/*
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.
*/
const core_1 = require("@sigstore/core");
const error_1 = require("../error");
const RFC6962_LEAF_HASH_PREFIX = Buffer.from([0x00]);
const RFC6962_NODE_HASH_PREFIX = Buffer.from([0x01]);
function verifyMerkleInclusion(entry, checkpoint) {
const inclusionProof = entry.inclusionProof;
const logIndex = BigInt(inclusionProof.logIndex);
const treeSize = BigInt(checkpoint.logSize);
if (logIndex < 0n || logIndex >= treeSize) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: `invalid index: ${logIndex}`,
});
}
// Figure out which subset of hashes corresponds to the inner and border
// nodes
const { inner, border } = decompInclProof(logIndex, treeSize);
if (inclusionProof.hashes.length !== inner + border) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'invalid hash count',
});
}
const innerHashes = inclusionProof.hashes.slice(0, inner);
const borderHashes = inclusionProof.hashes.slice(inner);
// The entry's hash is the leaf hash
const leafHash = hashLeaf(entry.canonicalizedBody);
// Chain the hashes belonging to the inner and border portions
const calculatedHash = chainBorderRight(chainInner(leafHash, innerHashes, logIndex), borderHashes);
// Calculated hash should match the root hash in the inclusion proof
if (!core_1.crypto.bufferEqual(calculatedHash, checkpoint.logHash)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'calculated root hash does not match inclusion proof',
});
}
}
// Breaks down inclusion proof for a leaf at the specified index in a tree of
// the specified size. The split point is where paths to the index leaf and
// the (size - 1) leaf diverge. Returns lengths of the bottom and upper proof
// parts.
function decompInclProof(index, size) {
const inner = innerProofSize(index, size);
const border = onesCount(index >> BigInt(inner));
return { inner, border };
}
// Computes a subtree hash for a node on or below the tree's right border.
// Assumes the provided proof hashes are ordered from lower to higher levels
// and seed is the initial hash of the node specified by the index.
function chainInner(seed, hashes, index) {
return hashes.reduce((acc, h, i) => {
if ((index >> BigInt(i)) & BigInt(1)) {
return hashChildren(h, acc);
}
else {
return hashChildren(acc, h);
}
}, seed);
}
// Computes a subtree hash for nodes along the tree's right border.
function chainBorderRight(seed, hashes) {
return hashes.reduce((acc, h) => hashChildren(h, acc), seed);
}
function innerProofSize(index, size) {
return bitLength(index ^ (size - BigInt(1)));
}
// Counts the number of ones in the binary representation of the given number.
// https://en.wikipedia.org/wiki/Hamming_weight
function onesCount(num) {
return num.toString(2).split('1').length - 1;
}
// Returns the number of bits necessary to represent an integer in binary.
function bitLength(n) {
if (n === 0n) {
return 0;
}
return n.toString(2).length;
}
// Hashing logic according to RFC6962.
// https://datatracker.ietf.org/doc/html/rfc6962#section-2
function hashChildren(left, right) {
return core_1.crypto.digest('sha256', RFC6962_NODE_HASH_PREFIX, left, right);
}
function hashLeaf(leaf) {
return core_1.crypto.digest('sha256', RFC6962_LEAF_HASH_PREFIX, leaf);
}
import { TLogAuthority } from '../trust';
import type { TLogEntryWithInclusionPromise } from '@sigstore/bundle';
export declare function verifyTLogSET(entry: TLogEntryWithInclusionPromise, tlogs: TLogAuthority[]): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyTLogSET = verifyTLogSET;
/*
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.
*/
const core_1 = require("@sigstore/core");
const error_1 = require("../error");
const trust_1 = require("../trust");
// Verifies the SET for the given entry against the list of trusted
// transparency logs. Returns true if the SET can be verified against at least
// one of the trusted logs; otherwise, returns false.
function verifyTLogSET(entry, tlogs) {
// Filter the list of tlog instances to only those which might be able to
// verify the SET
const validTLogs = (0, trust_1.filterTLogAuthorities)(tlogs, {
logID: entry.logId.keyId,
targetDate: new Date(Number(entry.integratedTime) * 1000),
});
// Check to see if we can verify the SET against any of the valid tlogs
const verified = validTLogs.some((tlog) => {
// Re-create the original Rekor verification payload
const payload = toVerificationPayload(entry);
// Canonicalize the payload and turn into a buffer for verification
const data = Buffer.from(core_1.json.canonicalize(payload), 'utf8');
// Extract the SET from the tlog entry
const signature = entry.inclusionPromise.signedEntryTimestamp;
return core_1.crypto.verify(data, tlog.publicKey, signature);
});
if (!verified) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROMISE_ERROR',
message: 'inclusion promise could not be verified',
});
}
}
// Returns a properly formatted "VerificationPayload" for one of the
// transaction log entires in the given bundle which can be used for SET
// verification.
function toVerificationPayload(entry) {
const { integratedTime, logIndex, logId, canonicalizedBody } = entry;
return {
body: canonicalizedBody.toString('base64'),
integratedTime: Number(integratedTime),
logIndex: Number(logIndex),
logID: logId.keyId.toString('hex'),
};
}
+1
-0

@@ -21,2 +21,3 @@ "use strict";

class DSSESignatureContent {
env;
constructor(env) {

@@ -23,0 +24,0 @@ this.env = env;

+10
-8

@@ -12,6 +12,8 @@ "use strict";

for (const entry of tlogEntries) {
timestamps.push({
$case: 'transparency-log',
tlogEntry: entry,
});
if (entry.integratedTime && entry.integratedTime !== '0') {
timestamps.push({
$case: 'transparency-log',
tlogEntry: entry,
});
}
}

@@ -21,3 +23,3 @@ for (const ts of timestampVerificationData?.rfc3161Timestamps ?? []) {

$case: 'timestamp-authority',
timestamp: core_1.RFC3161Timestamp.parse(ts.signedTimestamp),
timestamp: core_1.RFC3161Timestamp.parse(Buffer.from(ts.signedTimestamp)),
});

@@ -50,4 +52,4 @@ }

$case: 'certificate',
certificate: core_1.X509Certificate.parse(bundle.verificationMaterial.content.x509CertificateChain
.certificates[0].rawBytes),
certificate: core_1.X509Certificate.parse(Buffer.from(bundle.verificationMaterial.content.x509CertificateChain
.certificates[0].rawBytes)),
};

@@ -57,5 +59,5 @@ case 'certificate':

$case: 'certificate',
certificate: core_1.X509Certificate.parse(bundle.verificationMaterial.content.certificate.rawBytes),
certificate: core_1.X509Certificate.parse(Buffer.from(bundle.verificationMaterial.content.certificate.rawBytes)),
};
}
}

@@ -8,2 +8,3 @@ import { crypto } from '@sigstore/core';

private readonly artifact;
private readonly hashAlgorithm;
constructor(messageSignature: MessageSignature, artifact: Buffer);

@@ -10,0 +11,0 @@ compareSignature(signature: Buffer): boolean;

@@ -20,3 +20,18 @@ "use strict";

const core_1 = require("@sigstore/core");
const protobuf_specs_1 = require("@sigstore/protobuf-specs");
// Map from the Sigstore protobuf HashAlgorithm enum to
// the string values used by the Node.js crypto module.
const HASH_ALGORITHM_MAP = {
[protobuf_specs_1.HashAlgorithm.HASH_ALGORITHM_UNSPECIFIED]: 'sha256',
[protobuf_specs_1.HashAlgorithm.SHA2_256]: 'sha256',
[protobuf_specs_1.HashAlgorithm.SHA2_384]: 'sha384',
[protobuf_specs_1.HashAlgorithm.SHA2_512]: 'sha512',
[protobuf_specs_1.HashAlgorithm.SHA3_256]: 'sha3-256',
[protobuf_specs_1.HashAlgorithm.SHA3_384]: 'sha3-384',
};
class MessageSignatureContent {
signature;
messageDigest;
artifact;
hashAlgorithm;
constructor(messageSignature, artifact) {

@@ -26,2 +41,5 @@ this.signature = messageSignature.signature;

this.artifact = artifact;
this.hashAlgorithm =
HASH_ALGORITHM_MAP[messageSignature.messageDigest.algorithm] ??
/* istanbul ignore next */ 'sha256';
}

@@ -35,5 +53,5 @@ compareSignature(signature) {

verifySignature(key) {
return core_1.crypto.verify(this.artifact, key, this.signature);
return core_1.crypto.verify(this.artifact, key, this.signature, this.hashAlgorithm);
}
}
exports.MessageSignatureContent = MessageSignatureContent;

@@ -10,3 +10,3 @@ declare class BaseError<T extends string> extends Error {

}
type VerificationErrorCode = 'NOT_IMPLEMENTED_ERROR' | 'TLOG_INCLUSION_PROOF_ERROR' | 'TLOG_INCLUSION_PROMISE_ERROR' | 'TLOG_MISSING_INCLUSION_ERROR' | 'TLOG_BODY_ERROR' | 'CERTIFICATE_ERROR' | 'PUBLIC_KEY_ERROR' | 'SIGNATURE_ERROR' | 'TIMESTAMP_ERROR';
type VerificationErrorCode = 'NOT_IMPLEMENTED_ERROR' | 'TLOG_ERROR' | 'TLOG_INCLUSION_PROOF_ERROR' | 'TLOG_INCLUSION_PROMISE_ERROR' | 'TLOG_MISSING_INCLUSION_ERROR' | 'TLOG_BODY_ERROR' | 'CERTIFICATE_ERROR' | 'PUBLIC_KEY_ERROR' | 'SIGNATURE_ERROR' | 'TIMESTAMP_ERROR';
export declare class VerificationError extends BaseError<VerificationErrorCode> {

@@ -13,0 +13,0 @@ }

@@ -20,2 +20,4 @@ "use strict";

class BaseError extends Error {
code;
cause; /* eslint-disable-line @typescript-eslint/no-explicit-any */
constructor({ code, message, cause, }) {

@@ -22,0 +24,0 @@ super(message);

@@ -35,2 +35,6 @@ "use strict";

class CertificateChainVerifier {
untrustedCert;
trustedCerts;
localCerts;
timestamp;
constructor(opts) {

@@ -37,0 +41,0 @@ this.untrustedCert = opts.untrustedCert;

import { RFC3161Timestamp } from '@sigstore/core';
import type { TransparencyLogEntry } from '@sigstore/bundle';
import type { CertAuthority, TLogAuthority } from '../trust';
import type { CertAuthority } from '../trust';
export type TimestampType = 'transparency-log' | 'timestamp-authority';

@@ -10,3 +10,3 @@ export type TimestampVerificationResult = {

};
export declare function verifyTSATimestamp(timestamp: RFC3161Timestamp, data: Buffer, timestampAuthorities: CertAuthority[]): TimestampVerificationResult;
export declare function verifyTLogTimestamp(entry: TransparencyLogEntry, tlogAuthorities: TLogAuthority[]): TimestampVerificationResult;
export declare function getTSATimestamp(timestamp: RFC3161Timestamp, data: Buffer, timestampAuthorities: CertAuthority[]): TimestampVerificationResult;
export declare function getTLogTimestamp(entry: TransparencyLogEntry): TimestampVerificationResult;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyTSATimestamp = verifyTSATimestamp;
exports.verifyTLogTimestamp = verifyTLogTimestamp;
const error_1 = require("../error");
const checkpoint_1 = require("./checkpoint");
const merkle_1 = require("./merkle");
const set_1 = require("./set");
exports.getTSATimestamp = getTSATimestamp;
exports.getTLogTimestamp = getTLogTimestamp;
const tsa_1 = require("./tsa");
function verifyTSATimestamp(timestamp, data, timestampAuthorities) {
function getTSATimestamp(timestamp, data, timestampAuthorities) {
(0, tsa_1.verifyRFC3161Timestamp)(timestamp, data, timestampAuthorities);

@@ -18,19 +14,3 @@ return {

}
function verifyTLogTimestamp(entry, tlogAuthorities) {
let inclusionVerified = false;
if (isTLogEntryWithInclusionPromise(entry)) {
(0, set_1.verifyTLogSET)(entry, tlogAuthorities);
inclusionVerified = true;
}
if (isTLogEntryWithInclusionProof(entry)) {
(0, merkle_1.verifyMerkleInclusion)(entry);
(0, checkpoint_1.verifyCheckpoint)(entry, tlogAuthorities);
inclusionVerified = true;
}
if (!inclusionVerified) {
throw new error_1.VerificationError({
code: 'TLOG_MISSING_INCLUSION_ERROR',
message: 'inclusion could not be verified',
});
}
function getTLogTimestamp(entry) {
return {

@@ -42,7 +22,1 @@ type: 'transparency-log',

}
function isTLogEntryWithInclusionPromise(entry) {
return entry.inclusionPromise !== undefined;
}
function isTLogEntryWithInclusionProof(entry) {
return entry.inclusionProof !== undefined;
}

@@ -0,3 +1,6 @@

import type { Entry } from '@sigstore/protobuf-specs/rekor/v2';
import type { ProposedDSSEEntry } from '@sigstore/rekor-types';
import type { SignatureContent } from '../shared.types';
export declare const DSSE_API_VERSION_V1 = "0.0.1";
export declare function verifyDSSETLogBody(tlogEntry: ProposedDSSEEntry, content: SignatureContent): void;
export declare function verifyDSSETLogBodyV2(tlogEntry: Entry, content: SignatureContent): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DSSE_API_VERSION_V1 = void 0;
exports.verifyDSSETLogBody = verifyDSSETLogBody;
exports.verifyDSSETLogBodyV2 = verifyDSSETLogBodyV2;
/*
Copyright 2023 The Sigstore Authors.
Copyright 2025 The Sigstore Authors.

@@ -20,6 +22,7 @@ Licensed under the Apache License, Version 2.0 (the "License");

const error_1 = require("../error");
// Compare the given intoto tlog entry to the given bundle
exports.DSSE_API_VERSION_V1 = '0.0.1';
// Compare the given dsse tlog entry to the given bundle
function verifyDSSETLogBody(tlogEntry, content) {
switch (tlogEntry.apiVersion) {
case '0.0.1':
case exports.DSSE_API_VERSION_V1:
return verifyDSSE001TLogBody(tlogEntry, content);

@@ -33,2 +36,22 @@ default:

}
// Compare the given dsse tlog entry to the given bundle. This function is
// specifically for Rekor V2 entries.
function verifyDSSETLogBodyV2(tlogEntry, content) {
const spec = tlogEntry.spec?.spec;
if (!spec) {
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: `missing dsse spec`,
});
}
switch (spec.$case) {
case 'dsseV002':
return verifyDSSE002TLogBody(spec.dsseV002, content);
default:
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: `unsupported version: ${spec.$case}`,
});
}
}
// Compare the given dsse v0.0.1 tlog entry to the given DSSE envelope.

@@ -60,1 +83,27 @@ function verifyDSSE001TLogBody(tlogEntry, content) {

}
// Compare the given dsse v0.0.2 tlog entry to the given DSSE envelope.
function verifyDSSE002TLogBody(spec, content) {
// Ensure the bundle's DSSE only contains a single signature
if (spec.signatures?.length !== 1) {
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: 'signature count mismatch',
});
}
const tlogSig = spec.signatures[0].content;
// Ensure that the signature in the bundle's DSSE matches tlog entry
if (!content.compareSignature(tlogSig))
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: 'tlog entry signature mismatch',
});
// Ensure the digest of the bundle's DSSE payload matches the digest in the
// tlog entry
const tlogHash = spec.payloadHash?.digest || Buffer.from('');
if (!content.compareDigest(tlogHash)) {
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: 'DSSE payload hash mismatch',
});
}
}

@@ -0,3 +1,6 @@

import { Entry } from '@sigstore/protobuf-specs/rekor/v2';
import type { ProposedHashedRekordEntry } from '@sigstore/rekor-types';
import type { SignatureContent } from '../shared.types';
export declare const HASHEDREKORD_API_VERSION_V1 = "0.0.1";
export declare function verifyHashedRekordTLogBody(tlogEntry: ProposedHashedRekordEntry, content: SignatureContent): void;
export declare function verifyHashedRekordTLogBodyV2(tlogEntry: Entry, content: SignatureContent): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HASHEDREKORD_API_VERSION_V1 = void 0;
exports.verifyHashedRekordTLogBody = verifyHashedRekordTLogBody;
exports.verifyHashedRekordTLogBodyV2 = verifyHashedRekordTLogBodyV2;
/*
Copyright 2023 The Sigstore Authors.
Copyright 2025 The Sigstore Authors.

@@ -20,6 +22,7 @@ Licensed under the Apache License, Version 2.0 (the "License");

const error_1 = require("../error");
exports.HASHEDREKORD_API_VERSION_V1 = '0.0.1';
// Compare the given hashedrekord tlog entry to the given bundle
function verifyHashedRekordTLogBody(tlogEntry, content) {
switch (tlogEntry.apiVersion) {
case '0.0.1':
case exports.HASHEDREKORD_API_VERSION_V1:
return verifyHashedrekord001TLogBody(tlogEntry, content);

@@ -33,2 +36,22 @@ default:

}
// Compare the given hashedrekor tlog entry to the given bundle. This function is
// specifically for Rekor V2 entries.
function verifyHashedRekordTLogBodyV2(tlogEntry, content) {
const spec = tlogEntry.spec?.spec;
if (!spec) {
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: `missing dsse spec`,
});
}
switch (spec.$case) {
case 'hashedRekordV002':
return verifyHashedrekord002TLogBody(spec.hashedRekordV002, content);
default:
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: `unsupported version: ${spec.$case}`,
});
}
}
// Compare the given hashedrekord v0.0.1 tlog entry to the given message

@@ -54,1 +77,21 @@ // signature

}
// Compare the given hashedrekord v0.0.2 tlog entry to the given message
// signature
function verifyHashedrekord002TLogBody(spec, content) {
// Ensure that the bundles message signature matches the tlog entry
const tlogSig = spec.signature?.content || Buffer.from('');
if (!content.compareSignature(tlogSig)) {
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: 'signature mismatch',
});
}
// Ensure that the bundle's message digest matches the tlog entry
const tlogHash = spec.data?.digest || Buffer.from('');
if (!content.compareDigest(tlogHash)) {
throw new error_1.VerificationError({
code: 'TLOG_BODY_ERROR',
message: 'digest mismatch',
});
}
}
import type { TransparencyLogEntry } from '@sigstore/bundle';
import type { SignatureContent } from '../shared.types';
import { TLogAuthority } from '../trust';
export declare function verifyTLogBody(entry: TransparencyLogEntry, sigContent: SignatureContent): void;
export declare function verifyTLogInclusion(entry: TransparencyLogEntry, tlogAuthorities: TLogAuthority[]): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyTLogBody = verifyTLogBody;
exports.verifyTLogInclusion = verifyTLogInclusion;
/*

@@ -19,2 +20,3 @@ Copyright 2023 The Sigstore Authors.

*/
const v2_1 = require("@sigstore/protobuf-specs/rekor/v2");
const error_1 = require("../error");

@@ -24,2 +26,5 @@ const dsse_1 = require("./dsse");

const intoto_1 = require("./intoto");
const checkpoint_1 = require("./checkpoint");
const merkle_1 = require("./merkle");
const set_1 = require("./set");
// Verifies that the given tlog entry matches the supplied signature content.

@@ -29,2 +34,3 @@ function verifyTLogBody(entry, sigContent) {

const body = JSON.parse(entry.canonicalizedBody.toString('utf8'));
// validate body
if (kind !== body.kind || version !== body.apiVersion) {

@@ -36,9 +42,23 @@ throw new error_1.VerificationError({

}
switch (body.kind) {
switch (kind) {
case 'dsse':
return (0, dsse_1.verifyDSSETLogBody)(body, sigContent);
// Rekor V1 and V2 use incompatible types so we need to branch here based on version
if (version == dsse_1.DSSE_API_VERSION_V1) {
return (0, dsse_1.verifyDSSETLogBody)(body, sigContent);
}
else {
const entryRekorV2 = v2_1.Entry.fromJSON(body);
return (0, dsse_1.verifyDSSETLogBodyV2)(entryRekorV2, sigContent);
}
case 'intoto':
return (0, intoto_1.verifyIntotoTLogBody)(body, sigContent);
case 'hashedrekord':
return (0, hashedrekord_1.verifyHashedRekordTLogBody)(body, sigContent);
// Rekor V1 and V2 use incompatible types so we need to branch here based on version
if (version == hashedrekord_1.HASHEDREKORD_API_VERSION_V1) {
return (0, hashedrekord_1.verifyHashedRekordTLogBody)(body, sigContent);
}
else {
const entryRekorV2 = v2_1.Entry.fromJSON(body);
return (0, hashedrekord_1.verifyHashedRekordTLogBodyV2)(entryRekorV2, sigContent);
}
/* istanbul ignore next */

@@ -52,1 +72,26 @@ default:

}
function verifyTLogInclusion(entry, tlogAuthorities) {
let inclusionVerified = false;
if (isTLogEntryWithInclusionPromise(entry)) {
(0, set_1.verifyTLogSET)(entry, tlogAuthorities);
inclusionVerified = true;
}
if (isTLogEntryWithInclusionProof(entry)) {
const checkpoint = (0, checkpoint_1.verifyCheckpoint)(entry, tlogAuthorities);
(0, merkle_1.verifyMerkleInclusion)(entry, checkpoint);
inclusionVerified = true;
}
if (!inclusionVerified) {
throw new error_1.VerificationError({
code: 'TLOG_MISSING_INCLUSION_ERROR',
message: 'inclusion could not be verified',
});
}
return;
}
function isTLogEntryWithInclusionPromise(entry) {
return entry.inclusionPromise !== undefined;
}
function isTLogEntryWithInclusionProof(entry) {
return entry.inclusionProof !== undefined;
}

@@ -47,4 +47,8 @@ "use strict";

: 'spki';
/* istanbul ignore next */
return {
logID: tlogInstance.logId.keyId,
baseURL: tlogInstance.baseUrl,
logID: tlogInstance.checkpointKeyId
? tlogInstance.checkpointKeyId.keyId
: tlogInstance.logId.keyId,
publicKey: core_1.crypto.createPublicKey(tlogInstance.publicKey.rawBytes, keyType),

@@ -61,3 +65,3 @@ validFor: {

certChain: ca.certChain.certificates.map((cert) => {
return core_1.X509Certificate.parse(cert.rawBytes);
return core_1.X509Certificate.parse(Buffer.from(cert.rawBytes));
}),

@@ -64,0 +68,0 @@ validFor: {

import type { X509Certificate, crypto } from '@sigstore/core';
export type TLogAuthority = {
logID: Buffer;
baseURL: string;
publicKey: crypto.KeyObject;

@@ -5,0 +6,0 @@ validFor: {

import type { SignedEntity, Signer, VerificationPolicy } from './shared.types';
import type { TrustMaterial } from './trust';
export type VerifierOptions = {
/**
* Configuration options for the verifier.
*
* @public
*/
export interface VerifierOptions {
/** Minimum number of transparency log entries required for verification */
tlogThreshold?: number;
/** Minimum number of certificate transparency log entries required */
ctlogThreshold?: number;
/**
* Minimum number of timestamp authority timestamps required for verification
* @deprecated Use timestampThreshold instead
*/
tsaThreshold?: number;
};
/** Minimum number of timestamps required for verification */
timestampThreshold?: number;
}
export declare class Verifier {

@@ -9,0 +22,0 @@ private trustMaterial;

@@ -26,2 +26,4 @@ "use strict";

class Verifier {
trustMaterial;
options;
constructor(trustMaterial, options = {}) {

@@ -32,3 +34,4 @@ this.trustMaterial = trustMaterial;

tlogThreshold: options.tlogThreshold ?? 1,
tsaThreshold: options.tsaThreshold ?? 0,
timestampThreshold: options.timestampThreshold ?? options.tsaThreshold ?? 1,
tsaThreshold: 0,
};

@@ -48,12 +51,11 @@ }

verifyTimestamps(entity) {
let tlogCount = 0;
let tsaCount = 0;
let timestampCount = 0;
const timestamps = entity.timestamps.map((timestamp) => {
switch (timestamp.$case) {
case 'timestamp-authority':
tsaCount++;
return (0, timestamp_1.verifyTSATimestamp)(timestamp.timestamp, entity.signature.signature, this.trustMaterial.timestampAuthorities);
timestampCount++;
return (0, timestamp_1.getTSATimestamp)(timestamp.timestamp, entity.signature.signature, this.trustMaterial.timestampAuthorities);
case 'transparency-log':
tlogCount++;
return (0, timestamp_1.verifyTLogTimestamp)(timestamp.tlogEntry, this.trustMaterial.tlogs);
timestampCount++;
return (0, timestamp_1.getTLogTimestamp)(timestamp.tlogEntry);
}

@@ -68,14 +70,8 @@ });

}
if (tlogCount < this.options.tlogThreshold) {
if (timestampCount < this.options.timestampThreshold) {
throw new error_1.VerificationError({
code: 'TIMESTAMP_ERROR',
message: `expected ${this.options.tlogThreshold} tlog timestamps, got ${tlogCount}`,
message: `expected ${this.options.timestampThreshold} timestamps, got ${timestampCount}`,
});
}
if (tsaCount < this.options.tsaThreshold) {
throw new error_1.VerificationError({
code: 'TIMESTAMP_ERROR',
message: `expected ${this.options.tsaThreshold} tsa timestamps, got ${tsaCount}`,
});
}
return timestamps.map((t) => t.timestamp);

@@ -111,3 +107,14 @@ }

verifyTLogs({ signature: content, tlogEntries }) {
tlogEntries.forEach((entry) => (0, tlog_1.verifyTLogBody)(entry, content));
let tlogCount = 0;
tlogEntries.forEach((entry) => {
tlogCount++;
(0, tlog_1.verifyTLogInclusion)(entry, this.trustMaterial.tlogs);
(0, tlog_1.verifyTLogBody)(entry, content);
});
if (tlogCount < this.options.tlogThreshold) {
throw new error_1.VerificationError({
code: 'TLOG_ERROR',
message: `expected ${this.options.tlogThreshold} tlog entries, got ${tlogCount}`,
});
}
}

@@ -114,0 +121,0 @@ // Checks that the signature is valid for the supplied content

{
"name": "@sigstore/verify",
"version": "3.0.0",
"version": "3.1.0",
"description": "Verification of Sigstore signatures",

@@ -31,3 +31,3 @@ "main": "dist/index.js",

"@sigstore/bundle": "^4.0.0",
"@sigstore/core": "^3.0.0"
"@sigstore/core": "^3.1.0"
},

@@ -34,0 +34,0 @@ "engines": {

import { TLogAuthority } from '../trust';
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
export declare function verifyCheckpoint(entry: TLogEntryWithInclusionProof, tlogs: TLogAuthority[]): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyCheckpoint = verifyCheckpoint;
/*
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.
*/
const core_1 = require("@sigstore/core");
const error_1 = require("../error");
const trust_1 = require("../trust");
// Separator between the note and the signatures in a checkpoint
const CHECKPOINT_SEPARATOR = '\n\n';
// Checkpoint signatures are of the following form:
// "– <identity> <key_hint+signature_bytes>\n"
// where:
// - the prefix is an emdash (U+2014).
// - <identity> gives a human-readable representation of the signing ID.
// - <key_hint+signature_bytes> is the first 4 bytes of the SHA256 hash of the
// associated public key followed by the signature bytes.
const SIGNATURE_REGEX = /\u2014 (\S+) (\S+)\n/g;
// Verifies the checkpoint value in the given tlog entry. There are two steps
// to the verification:
// 1. Verify that all signatures in the checkpoint can be verified against a
// trusted public key
// 2. Verify that the root hash in the checkpoint matches the root hash in the
// inclusion proof
// See: https://github.com/transparency-dev/formats/blob/main/log/README.md
function verifyCheckpoint(entry, tlogs) {
// Filter tlog instances to just those which were valid at the time of the
// entry
const validTLogs = (0, trust_1.filterTLogAuthorities)(tlogs, {
targetDate: new Date(Number(entry.integratedTime) * 1000),
});
const inclusionProof = entry.inclusionProof;
const signedNote = SignedNote.fromString(inclusionProof.checkpoint.envelope);
const checkpoint = LogCheckpoint.fromString(signedNote.note);
// Verify that the signatures in the checkpoint are all valid
if (!verifySignedNote(signedNote, validTLogs)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'invalid checkpoint signature',
});
}
// Verify that the root hash from the checkpoint matches the root hash in the
// inclusion proof
if (!core_1.crypto.bufferEqual(checkpoint.logHash, inclusionProof.rootHash)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'root hash mismatch',
});
}
}
// Verifies the signatures in the SignedNote. For each signature, the
// corresponding transparency log is looked up by the key hint and the
// signature is verified against the public key in the transparency log.
// Throws an error if any of the signatures are invalid.
function verifySignedNote(signedNote, tlogs) {
const data = Buffer.from(signedNote.note, 'utf-8');
return signedNote.signatures.every((signature) => {
// Find the transparency log instance with the matching key hint
const tlog = tlogs.find((tlog) => core_1.crypto.bufferEqual(tlog.logID.subarray(0, 4), signature.keyHint));
if (!tlog) {
return false;
}
return core_1.crypto.verify(data, tlog.publicKey, signature.signature);
});
}
// SignedNote represents a signed note from a transparency log checkpoint. Consists
// of a body (or note) and one more signatures calculated over the body. See
// https://github.com/transparency-dev/formats/blob/main/log/README.md#signed-envelope
class SignedNote {
constructor(note, signatures) {
this.note = note;
this.signatures = signatures;
}
// Deserialize a SignedNote from a string
static fromString(envelope) {
if (!envelope.includes(CHECKPOINT_SEPARATOR)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'missing checkpoint separator',
});
}
// Split the note into the header and the data portions at the separator
const split = envelope.indexOf(CHECKPOINT_SEPARATOR);
const header = envelope.slice(0, split + 1);
const data = envelope.slice(split + CHECKPOINT_SEPARATOR.length);
// Find all the signature lines in the data portion
const matches = data.matchAll(SIGNATURE_REGEX);
// Parse each of the matched signature lines into the name and signature.
// The first four bytes of the signature are the key hint (should match the
// first four bytes of the log ID), and the rest is the signature itself.
const signatures = Array.from(matches, (match) => {
const [, name, signature] = match;
const sigBytes = Buffer.from(signature, 'base64');
if (sigBytes.length < 5) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'malformed checkpoint signature',
});
}
return {
name,
keyHint: sigBytes.subarray(0, 4),
signature: sigBytes.subarray(4),
};
});
if (signatures.length === 0) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'no signatures found in checkpoint',
});
}
return new SignedNote(header, signatures);
}
}
// LogCheckpoint represents a transparency log checkpoint. Consists of the
// following:
// - origin: the name of the transparency log
// - logSize: the size of the log at the time of the checkpoint
// - logHash: the root hash of the log at the time of the checkpoint
// - rest: the rest of the checkpoint body, which is a list of log entries
// See:
// https://github.com/transparency-dev/formats/blob/main/log/README.md#checkpoint-body
class LogCheckpoint {
constructor(origin, logSize, logHash, rest) {
this.origin = origin;
this.logSize = logSize;
this.logHash = logHash;
this.rest = rest;
}
static fromString(note) {
const lines = note.trimEnd().split('\n');
if (lines.length < 3) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'too few lines in checkpoint header',
});
}
const origin = lines[0];
const logSize = BigInt(lines[1]);
const rootHash = Buffer.from(lines[2], 'base64');
const rest = lines.slice(3);
return new LogCheckpoint(origin, logSize, rootHash, rest);
}
}
import type { TLogEntryWithInclusionProof } from '@sigstore/bundle';
export declare function verifyMerkleInclusion(entry: TLogEntryWithInclusionProof): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyMerkleInclusion = verifyMerkleInclusion;
/*
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.
*/
const core_1 = require("@sigstore/core");
const error_1 = require("../error");
const RFC6962_LEAF_HASH_PREFIX = Buffer.from([0x00]);
const RFC6962_NODE_HASH_PREFIX = Buffer.from([0x01]);
function verifyMerkleInclusion(entry) {
const inclusionProof = entry.inclusionProof;
const logIndex = BigInt(inclusionProof.logIndex);
const treeSize = BigInt(inclusionProof.treeSize);
if (logIndex < 0n || logIndex >= treeSize) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: `invalid index: ${logIndex}`,
});
}
// Figure out which subset of hashes corresponds to the inner and border
// nodes
const { inner, border } = decompInclProof(logIndex, treeSize);
if (inclusionProof.hashes.length !== inner + border) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'invalid hash count',
});
}
const innerHashes = inclusionProof.hashes.slice(0, inner);
const borderHashes = inclusionProof.hashes.slice(inner);
// The entry's hash is the leaf hash
const leafHash = hashLeaf(entry.canonicalizedBody);
// Chain the hashes belonging to the inner and border portions
const calculatedHash = chainBorderRight(chainInner(leafHash, innerHashes, logIndex), borderHashes);
// Calculated hash should match the root hash in the inclusion proof
if (!core_1.crypto.bufferEqual(calculatedHash, inclusionProof.rootHash)) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROOF_ERROR',
message: 'calculated root hash does not match inclusion proof',
});
}
}
// Breaks down inclusion proof for a leaf at the specified index in a tree of
// the specified size. The split point is where paths to the index leaf and
// the (size - 1) leaf diverge. Returns lengths of the bottom and upper proof
// parts.
function decompInclProof(index, size) {
const inner = innerProofSize(index, size);
const border = onesCount(index >> BigInt(inner));
return { inner, border };
}
// Computes a subtree hash for a node on or below the tree's right border.
// Assumes the provided proof hashes are ordered from lower to higher levels
// and seed is the initial hash of the node specified by the index.
function chainInner(seed, hashes, index) {
return hashes.reduce((acc, h, i) => {
if ((index >> BigInt(i)) & BigInt(1)) {
return hashChildren(h, acc);
}
else {
return hashChildren(acc, h);
}
}, seed);
}
// Computes a subtree hash for nodes along the tree's right border.
function chainBorderRight(seed, hashes) {
return hashes.reduce((acc, h) => hashChildren(h, acc), seed);
}
function innerProofSize(index, size) {
return bitLength(index ^ (size - BigInt(1)));
}
// Counts the number of ones in the binary representation of the given number.
// https://en.wikipedia.org/wiki/Hamming_weight
function onesCount(num) {
return num.toString(2).split('1').length - 1;
}
// Returns the number of bits necessary to represent an integer in binary.
function bitLength(n) {
if (n === 0n) {
return 0;
}
return n.toString(2).length;
}
// Hashing logic according to RFC6962.
// https://datatracker.ietf.org/doc/html/rfc6962#section-2
function hashChildren(left, right) {
return core_1.crypto.digest('sha256', RFC6962_NODE_HASH_PREFIX, left, right);
}
function hashLeaf(leaf) {
return core_1.crypto.digest('sha256', RFC6962_LEAF_HASH_PREFIX, leaf);
}
import { TLogAuthority } from '../trust';
import type { TLogEntryWithInclusionPromise } from '@sigstore/bundle';
export declare function verifyTLogSET(entry: TLogEntryWithInclusionPromise, tlogs: TLogAuthority[]): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyTLogSET = verifyTLogSET;
/*
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.
*/
const core_1 = require("@sigstore/core");
const error_1 = require("../error");
const trust_1 = require("../trust");
// Verifies the SET for the given entry against the list of trusted
// transparency logs. Returns true if the SET can be verified against at least
// one of the trusted logs; otherwise, returns false.
function verifyTLogSET(entry, tlogs) {
// Filter the list of tlog instances to only those which might be able to
// verify the SET
const validTLogs = (0, trust_1.filterTLogAuthorities)(tlogs, {
logID: entry.logId.keyId,
targetDate: new Date(Number(entry.integratedTime) * 1000),
});
// Check to see if we can verify the SET against any of the valid tlogs
const verified = validTLogs.some((tlog) => {
// Re-create the original Rekor verification payload
const payload = toVerificationPayload(entry);
// Canonicalize the payload and turn into a buffer for verification
const data = Buffer.from(core_1.json.canonicalize(payload), 'utf8');
// Extract the SET from the tlog entry
const signature = entry.inclusionPromise.signedEntryTimestamp;
return core_1.crypto.verify(data, tlog.publicKey, signature);
});
if (!verified) {
throw new error_1.VerificationError({
code: 'TLOG_INCLUSION_PROMISE_ERROR',
message: 'inclusion promise could not be verified',
});
}
}
// Returns a properly formatted "VerificationPayload" for one of the
// transaction log entires in the given bundle which can be used for SET
// verification.
function toVerificationPayload(entry) {
const { integratedTime, logIndex, logId, canonicalizedBody } = entry;
return {
body: canonicalizedBody.toString('base64'),
integratedTime: Number(integratedTime),
logIndex: Number(logIndex),
logID: logId.keyId.toString('hex'),
};
}