checkpoint.js 6.8 KB

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