• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.verifyCheckpoint = void 0;
4/*
5Copyright 2023 The Sigstore Authors.
6
7Licensed under the Apache License, Version 2.0 (the "License");
8you may not use this file except in compliance with the License.
9You may obtain a copy of the License at
10
11    http://www.apache.org/licenses/LICENSE-2.0
12
13Unless required by applicable law or agreed to in writing, software
14distributed under the License is distributed on an "AS IS" BASIS,
15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16See the License for the specific language governing permissions and
17limitations under the License.
18*/
19const core_1 = require("@sigstore/core");
20const error_1 = require("../error");
21const trust_1 = require("../trust");
22// Separator between the note and the signatures in a checkpoint
23const CHECKPOINT_SEPARATOR = '\n\n';
24// Checkpoint signatures are of the following form:
25// "– <identity> <key_hint+signature_bytes>\n"
26// where:
27// - the prefix is an emdash (U+2014).
28// - <identity> gives a human-readable representation of the signing ID.
29// - <key_hint+signature_bytes> is the first 4 bytes of the SHA256 hash of the
30//   associated public key followed by the signature bytes.
31const SIGNATURE_REGEX = /\u2014 (\S+) (\S+)\n/g;
32// Verifies the checkpoint value in the given tlog entry. There are two steps
33// to the verification:
34// 1. Verify that all signatures in the checkpoint can be verified against a
35//    trusted public key
36// 2. Verify that the root hash in the checkpoint matches the root hash in the
37//    inclusion proof
38// See: https://github.com/transparency-dev/formats/blob/main/log/README.md
39function verifyCheckpoint(entry, tlogs) {
40    // Filter tlog instances to just those which were valid at the time of the
41    // entry
42    const validTLogs = (0, trust_1.filterTLogAuthorities)(tlogs, {
43        targetDate: new Date(Number(entry.integratedTime) * 1000),
44    });
45    const inclusionProof = entry.inclusionProof;
46    const signedNote = SignedNote.fromString(inclusionProof.checkpoint.envelope);
47    const checkpoint = LogCheckpoint.fromString(signedNote.note);
48    // Verify that the signatures in the checkpoint are all valid
49    if (!verifySignedNote(signedNote, validTLogs)) {
50        throw new error_1.VerificationError({
51            code: 'TLOG_INCLUSION_PROOF_ERROR',
52            message: 'invalid checkpoint signature',
53        });
54    }
55    // Verify that the root hash from the checkpoint matches the root hash in the
56    // inclusion proof
57    if (!core_1.crypto.bufferEqual(checkpoint.logHash, inclusionProof.rootHash)) {
58        throw new error_1.VerificationError({
59            code: 'TLOG_INCLUSION_PROOF_ERROR',
60            message: 'root hash mismatch',
61        });
62    }
63}
64exports.verifyCheckpoint = verifyCheckpoint;
65// Verifies the signatures in the SignedNote. For each signature, the
66// corresponding transparency log is looked up by the key hint and the
67// signature is verified against the public key in the transparency log.
68// Throws an error if any of the signatures are invalid.
69function verifySignedNote(signedNote, tlogs) {
70    const data = Buffer.from(signedNote.note, 'utf-8');
71    return signedNote.signatures.every((signature) => {
72        // Find the transparency log instance with the matching key hint
73        const tlog = tlogs.find((tlog) => core_1.crypto.bufferEqual(tlog.logID.subarray(0, 4), signature.keyHint));
74        if (!tlog) {
75            return false;
76        }
77        return core_1.crypto.verify(data, tlog.publicKey, signature.signature);
78    });
79}
80// SignedNote represents a signed note from a transparency log checkpoint. Consists
81// of a body (or note) and one more signatures calculated over the body. See
82// https://github.com/transparency-dev/formats/blob/main/log/README.md#signed-envelope
83class SignedNote {
84    constructor(note, signatures) {
85        this.note = note;
86        this.signatures = signatures;
87    }
88    // Deserialize a SignedNote from a string
89    static fromString(envelope) {
90        if (!envelope.includes(CHECKPOINT_SEPARATOR)) {
91            throw new error_1.VerificationError({
92                code: 'TLOG_INCLUSION_PROOF_ERROR',
93                message: 'missing checkpoint separator',
94            });
95        }
96        // Split the note into the header and the data portions at the separator
97        const split = envelope.indexOf(CHECKPOINT_SEPARATOR);
98        const header = envelope.slice(0, split + 1);
99        const data = envelope.slice(split + CHECKPOINT_SEPARATOR.length);
100        // Find all the signature lines in the data portion
101        const matches = data.matchAll(SIGNATURE_REGEX);
102        // Parse each of the matched signature lines into the name and signature.
103        // The first four bytes of the signature are the key hint (should match the
104        // first four bytes of the log ID), and the rest is the signature itself.
105        const signatures = Array.from(matches, (match) => {
106            const [, name, signature] = match;
107            const sigBytes = Buffer.from(signature, 'base64');
108            if (sigBytes.length < 5) {
109                throw new error_1.VerificationError({
110                    code: 'TLOG_INCLUSION_PROOF_ERROR',
111                    message: 'malformed checkpoint signature',
112                });
113            }
114            return {
115                name,
116                keyHint: sigBytes.subarray(0, 4),
117                signature: sigBytes.subarray(4),
118            };
119        });
120        if (signatures.length === 0) {
121            throw new error_1.VerificationError({
122                code: 'TLOG_INCLUSION_PROOF_ERROR',
123                message: 'no signatures found in checkpoint',
124            });
125        }
126        return new SignedNote(header, signatures);
127    }
128}
129// LogCheckpoint represents a transparency log checkpoint. Consists of the
130// following:
131//  - origin: the name of the transparency log
132//  - logSize: the size of the log at the time of the checkpoint
133//  - logHash: the root hash of the log at the time of the checkpoint
134//  - rest: the rest of the checkpoint body, which is a list of log entries
135// See:
136// https://github.com/transparency-dev/formats/blob/main/log/README.md#checkpoint-body
137class LogCheckpoint {
138    constructor(origin, logSize, logHash, rest) {
139        this.origin = origin;
140        this.logSize = logSize;
141        this.logHash = logHash;
142        this.rest = rest;
143    }
144    static fromString(note) {
145        const lines = note.trimEnd().split('\n');
146        if (lines.length < 3) {
147            throw new error_1.VerificationError({
148                code: 'TLOG_INCLUSION_PROOF_ERROR',
149                message: 'too few lines in checkpoint header',
150            });
151        }
152        const origin = lines[0];
153        const logSize = BigInt(lines[1]);
154        const rootHash = Buffer.from(lines[2], 'base64');
155        const rest = lines.slice(3);
156        return new LogCheckpoint(origin, logSize, rootHash, rest);
157    }
158}
159