@smartledger/bsv 9.7.0 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,80 +2,307 @@
2
2
 
3
3
  var JCS = require('../util/jcs')
4
4
  var Encoding = require('./encoding')
5
+ var Merkle = require('./merkle')
5
6
  var NotaryScript = require('./script')
7
+ var deprecate = require('../util/deprecate')
6
8
  var $ = require('../util/preconditions')
7
9
 
8
10
  /**
9
11
  * BRC-220 certificate — the self-contained object a verifier is handed.
10
12
  *
11
- * Required fields, per the spec: protocol, version, mode, algorithm, hashAlgorithm,
12
- * payloadHash, publicKey, signature, encoding, proofHash, createdAt, anchor. A batched
13
- * certificate additionally carries `merkle`.
13
+ * There are two JSON formats, and this module reads both.
14
14
  *
15
- * Two things about it are easy to get wrong, and both are load-bearing:
15
+ * The REFERENCE format is the BRC-220 reference implementation's, field for field. A
16
+ * certificate exists to be checked by someone other than its issuer, and this is the
17
+ * format every other BRC-220 verifier reads:
18
+ *
19
+ * {
20
+ * protocol: 'NotaryHash',
21
+ * version: '1.0',
22
+ * mode: 'full' | 'hybrid', // how the proof sits on chain
23
+ * algorithm, hashAlgorithm,
24
+ * payloadHash: <hex>,
25
+ * publicKey, signature: <per encoding>, // FULL blobs, even in hybrid mode
26
+ * encoding: 'hex' | 'base64', // how publicKey and signature are written
27
+ * proofHash: <hex>,
28
+ * createdAt: <ISO 8601, whole seconds>,
29
+ * anchor: { type: 'direct' | 'batch', network, txid, vout, blockHeight, blockTime },
30
+ * merkle: { root, leafIndex, leafCount, path: [{ hash, side }] }, // iff batch
31
+ * spv: { rawTx, blockHash, blockHeight, merkleProof, format } // once mined
32
+ * }
33
+ *
34
+ * Batch is NOT a mode there. A batched proof is still full or hybrid; what makes it a
35
+ * batch is that its anchor holds a Merkle root rather than the proof itself, which is why
36
+ * the reference marks it on `anchor.type`.
37
+ *
38
+ * The LEGACY format is what 8.3.0–9.8.0 wrote: `version: 1`, the numeric on-chain mode
39
+ * byte, `encoding: "raw" | "der"`, `anchor: { txid, blockHeight }` and bare-hash Merkle
40
+ * paths. BRC-220 lists the required fields but not their JSON values, and those releases
41
+ * filled the gap with choices of their own, so the reference could verify none of their
42
+ * certificates and they could verify none of the reference's. The cryptography agreed
43
+ * throughout; only the JSON did not.
44
+ *
45
+ * Reading: every check normalises a legacy certificate onto the reference format first,
46
+ * so both verify.
47
+ *
48
+ * Writing: `build({ format: 'reference' })` writes the reference format. Through 9.x,
49
+ * omitting `format` still writes the legacy one, byte for byte as 9.8.0 did, and warns
50
+ * once — STABILITY.md does not let a minor change what an API returns. The default
51
+ * becomes 'reference' in 10.0.0.
52
+ *
53
+ * Two properties are load-bearing and both are asserted by tests:
16
54
  *
17
55
  * - The certificate carries the FULL publicKey and signature in every mode, including
18
56
  * hybrid, where only their SHA-256 digests go on chain. That asymmetry is the point of
19
57
  * hybrid mode: the chain stays small, the certificate stays complete.
20
- * - The SPV envelope is ADDITIVE. The spec is explicit that attaching it never changes
21
- * proofHash and never invalidates a previously issued certificate — which follows from
22
- * proofHash being computed over the canonical proof bytes, not over the certificate
23
- * JSON. A test asserts it rather than trusting the reasoning.
58
+ * - The SPV envelope is ADDITIVE. Attaching it never changes proofHash, because
59
+ * proofHash is over the canonical proof bytes, not over the certificate JSON.
24
60
  */
25
61
 
26
62
  var Certificate = {}
27
63
 
28
64
  Certificate.PROTOCOL = 'NotaryHash'
65
+
66
+ /**
67
+ * The `version` the 9.x DEFAULT format writes — the 8.3.0–9.8.0 format. It becomes
68
+ * REFERENCE_VERSION in 10.0.0, with the default.
69
+ */
29
70
  Certificate.VERSION = 1
30
71
 
72
+ /** The `version` the reference format writes. */
73
+ Certificate.REFERENCE_VERSION = '1.0'
74
+
75
+ /** What `build()` writes. See the module comment. */
76
+ Certificate.FORMAT = { REFERENCE: 'reference', LEGACY: 'legacy' }
77
+
78
+ /** A reference-format certificate's `mode`. */
79
+ Certificate.MODE = { FULL: 'full', HYBRID: 'hybrid' }
80
+
31
81
  /**
32
- * The byte representation of publicKey and signature.
82
+ * `encoding` values. In the reference format, HEX or BASE64: how `publicKey` and
83
+ * `signature` are written into the JSON, and nothing about their bytes — a signature is
84
+ * whatever the signer produced, for ECDSA 64-byte `r || s` or DER, told apart by the
85
+ * bytes themselves. `payloadHash`, `proofHash` and the Merkle hashes are always hex.
33
86
  *
34
- * The spec requires the `encoding` field but never enumerates its values; this library
35
- * defines them and proposes the definition upstream — see
36
- * docs/BRC220_ENCODING_AMENDMENT.md.
87
+ * RAW and DER are the legacy format's values. They named the signature's byte form, and
88
+ * both were written out as hex.
89
+ */
90
+ Certificate.ENCODING = { HEX: 'hex', BASE64: 'base64', RAW: 'raw', DER: 'der' }
91
+
92
+ Certificate.ANCHOR_TYPE = { DIRECT: 'direct', BATCH: 'batch' }
93
+ Certificate.DEFAULT_NETWORK = 'bsv-mainnet'
94
+
95
+ // What 8.3.0–9.8.0 wrote in `version`. Not exported under this name: in 9.x it is
96
+ // Certificate.VERSION, and a second constant for the same value invites the two to drift.
97
+ var LEGACY_VERSION = 1
98
+
99
+ // What each legacy numeric mode becomes. Legacy batch (2) was a mode; here it is a full
100
+ // proof on a batch anchor.
101
+ var LEGACY_MODE = { 0: 'full', 1: 'hybrid', 2: 'full' }
102
+
103
+ var HEX_RE = /^[0-9a-fA-F]*$/
104
+ // The reference decodes base64 with Buffer.from(value, 'base64'), which also takes the
105
+ // URL-safe alphabet and missing padding. Those are accepted here too, so nothing the
106
+ // reference reads is refused. What is NOT accepted is any other character: Buffer.from
107
+ // skips those silently, turning a corrupted field into different bytes instead of an
108
+ // error.
109
+ var BASE64_RE = /^[A-Za-z0-9+/_-]*={0,2}$/
110
+
111
+ /**
112
+ * Decode a certificate string field to bytes.
113
+ *
114
+ * Hex takes an optional `0x` prefix, because the reference accepts one. Both forms are
115
+ * validated rather than handed to Buffer.from, which silently drops characters it does
116
+ * not recognise and would turn a malformed field into different bytes instead of an
117
+ * error.
37
118
  *
38
- * RAW is what everything should emit. For ECDSA-secp256k1 that is 64 bytes of r || s,
39
- * each a 32-byte big-endian integer, with a 33-byte compressed public key. DER is
40
- * accepted for Bitcoin-native signers that already hold it, and discouraged: DER is not
41
- * canonical, so the same signature can encode to 69, 70 or 71 bytes, and those bytes are
42
- * inside proofHash.
119
+ * @param {String} value
120
+ * @param {String} encoding - 'hex' | 'base64'
121
+ * @param {String} [name] - for the error message
122
+ * @returns {Buffer}
43
123
  */
44
- Certificate.ENCODING = {
45
- RAW: 'raw',
46
- DER: 'der'
124
+ Certificate.decodeBytes = function (value, encoding, name) {
125
+ name = name || 'value'
126
+ if (typeof value !== 'string') throw new Error(name + ' must be a string')
127
+ if (encoding === Certificate.ENCODING.HEX) {
128
+ var clean = value.slice(0, 2) === '0x' ? value.slice(2) : value
129
+ if (clean.length % 2 !== 0 || !HEX_RE.test(clean)) throw new Error(name + ' must be a hex string')
130
+ return Buffer.from(clean, 'hex')
131
+ }
132
+ if (encoding === Certificate.ENCODING.BASE64) {
133
+ if (!BASE64_RE.test(value)) throw new Error(name + ' must be base64')
134
+ return Buffer.from(value, 'base64')
135
+ }
136
+ throw new Error('encoding must be "hex" or "base64", not ' + JSON.stringify(encoding))
47
137
  }
48
138
 
49
- function hex (buf, name) {
139
+ function encodeBytes (buf, encoding, name) {
50
140
  if (!Buffer.isBuffer(buf)) {
51
141
  throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
52
142
  }
143
+ return encoding === Certificate.ENCODING.BASE64 ? buf.toString('base64') : buf.toString('hex')
144
+ }
145
+
146
+ /** A 32-byte hash field as lowercase hex, from a Buffer or a hex string. */
147
+ function hashHex (value, name) {
148
+ var buf = Buffer.isBuffer(value) ? value : Certificate.decodeBytes(value, 'hex', name)
149
+ if (buf.length !== 32) throw new Error(name + ' must be 32 bytes')
53
150
  return buf.toString('hex')
54
151
  }
55
152
 
153
+ function resolveMode (mode) {
154
+ if (mode === Certificate.MODE.FULL || mode === Certificate.MODE.HYBRID) {
155
+ return { mode: mode, batch: false }
156
+ }
157
+ // The numeric on-chain mode bytes, which is what 8.3.0–9.8.0 took here.
158
+ if (mode === NotaryScript.MODE.FULL) return { mode: 'full', batch: false }
159
+ if (mode === NotaryScript.MODE.HYBRID) return { mode: 'hybrid', batch: false }
160
+ if (mode === NotaryScript.MODE.BATCH) return { mode: 'full', batch: true }
161
+ if (mode === 'batch') {
162
+ throw new Error('batch is an anchor type, not a mode: pass mode "full" or "hybrid" with a merkle proof')
163
+ }
164
+ throw new Error('mode is required: "full" or "hybrid"')
165
+ }
166
+
167
+ function resolveEncoding (encoding) {
168
+ if (encoding === undefined) return Certificate.ENCODING.HEX
169
+ if (encoding === Certificate.ENCODING.HEX || encoding === Certificate.ENCODING.BASE64) {
170
+ return encoding
171
+ }
172
+ // The legacy values named the signature's BYTE format. Both were written as hex, so
173
+ // both mean "hex" in the reference format.
174
+ if (encoding === Certificate.ENCODING.RAW || encoding === Certificate.ENCODING.DER) {
175
+ return Certificate.ENCODING.HEX
176
+ }
177
+ throw new Error('encoding must be "hex" or "base64"')
178
+ }
179
+
56
180
  /**
57
- * Build a certificate.
58
- *
59
- * `proofHash` is computed here rather than accepted, so a caller cannot supply one that
60
- * does not match the fields beside it. A certificate whose stated proofHash disagrees
61
- * with its own contents is exactly the artefact this protocol exists to make impossible,
62
- * and accepting the value would let one be constructed by mistake.
63
- *
64
- * @param {Object} params
65
- * @param {Number} params.mode - NotaryScript.MODE.*
66
- * @param {String} params.algorithm
67
- * @param {String} params.hashAlgorithm - 'SHA-256' for every algorithm the spec lists
68
- * @param {Buffer} params.payloadHash - raw 32 bytes
69
- * @param {Buffer} params.publicKey - raw bytes, FULL even in hybrid mode
70
- * @param {Buffer} params.signature - raw bytes, FULL even in hybrid mode
71
- * @param {String} [params.encoding='raw']
72
- * @param {String|Date} [params.createdAt] - defaults to now
73
- * @param {Object} params.anchor - { txid, blockHeight }
74
- * @param {Object} [params.merkle] - batch mode: { root, leafIndex, leafCount, path }
75
- * @returns {Object} certificate
181
+ * An audit path in the `{ hash, side }` form certificates carry, from either that form
182
+ * or the bare hashes 8.3.0–9.8.0 wrote. Bare hashes get their sides from the index and
183
+ * tree size, which is exactly how RFC 6962 determines them.
76
184
  */
77
- Certificate.build = function (params) {
78
- $.checkArgument(params && typeof params === 'object', 'params is required')
185
+ function sidedPath (path, leafIndex, leafCount) {
186
+ if (!Array.isArray(path)) throw new Error('merkle.path must be an array')
187
+ var sided = path.length > 0 && path.every(function (n) {
188
+ return n && typeof n === 'object' && !Buffer.isBuffer(n) && 'side' in n
189
+ })
190
+ if (sided || path.length === 0) {
191
+ return path.map(function (n, i) {
192
+ if (n.side !== 'left' && n.side !== 'right') {
193
+ throw new Error('merkle.path[' + i + '].side must be "left" or "right"')
194
+ }
195
+ return { hash: hashHex(n.hash, 'merkle.path[' + i + '].hash'), side: n.side }
196
+ })
197
+ }
198
+ var sides = Merkle.pathSides(leafIndex, leafCount)
199
+ if (sides.length !== path.length) {
200
+ throw new Error('merkle.path has ' + path.length + ' nodes; a tree of ' + leafCount +
201
+ ' leaves needs ' + sides.length + ' for leaf ' + leafIndex)
202
+ }
203
+ return path.map(function (n, i) {
204
+ return { hash: hashHex(n, 'merkle.path[' + i + ']'), side: sides[i] }
205
+ })
206
+ }
207
+
208
+ function buildMerkle (merkle) {
209
+ $.checkArgument(merkle && typeof merkle === 'object',
210
+ 'batch certificates require a merkle inclusion proof')
211
+ $.checkArgument(Number.isInteger(merkle.leafIndex) && merkle.leafIndex >= 0,
212
+ 'merkle.leafIndex must be a non-negative integer')
213
+ $.checkArgument(Number.isInteger(merkle.leafCount) && merkle.leafCount > merkle.leafIndex,
214
+ 'merkle.leafCount must be an integer greater than leafIndex')
215
+ return {
216
+ root: hashHex(merkle.root, 'merkle.root'),
217
+ leafIndex: merkle.leafIndex,
218
+ leafCount: merkle.leafCount,
219
+ path: sidedPath(merkle.path, merkle.leafIndex, merkle.leafCount)
220
+ }
221
+ }
222
+
223
+ function buildAnchor (anchor, batch) {
224
+ $.checkArgument(anchor && typeof anchor === 'object', 'anchor is required')
225
+ $.checkArgument(typeof anchor.txid === 'string', 'anchor.txid must be a string')
226
+ var type = anchor.type || (batch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT)
227
+ $.checkArgument(type === Certificate.ANCHOR_TYPE.DIRECT || type === Certificate.ANCHOR_TYPE.BATCH,
228
+ 'anchor.type must be "direct" or "batch"')
229
+ $.checkArgument((type === Certificate.ANCHOR_TYPE.BATCH) === batch,
230
+ 'anchor.type is "batch" exactly when a merkle inclusion proof is given')
231
+ return {
232
+ type: type,
233
+ network: anchor.network || Certificate.DEFAULT_NETWORK,
234
+ txid: anchor.txid,
235
+ vout: anchor.vout === undefined ? 0 : anchor.vout,
236
+ blockHeight: anchor.blockHeight === undefined ? null : anchor.blockHeight,
237
+ blockTime: anchor.blockTime === undefined ? null : anchor.blockTime
238
+ }
239
+ }
240
+
241
+ /**
242
+ * The reference format. `createdAt` is written the way the reference writes it — the
243
+ * whole seconds that go into proofHash, as ISO 8601 — so two implementations given the
244
+ * same proof produce the same JSON.
245
+ */
246
+ function buildReference (params) {
247
+ var resolved = resolveMode(params.mode)
248
+ var batch = resolved.batch || params.merkle !== undefined ||
249
+ !!(params.anchor && params.anchor.type === Certificate.ANCHOR_TYPE.BATCH)
250
+ if (batch) {
251
+ $.checkArgument(params.merkle && typeof params.merkle === 'object',
252
+ 'batch certificates require a merkle inclusion proof')
253
+ }
254
+ var encoding = resolveEncoding(params.encoding)
255
+ var anchor = buildAnchor(params.anchor, batch)
256
+
257
+ var createdAtUnix = params.createdAtUnix !== undefined
258
+ ? params.createdAtUnix
259
+ : Encoding.toUnixSeconds(params.createdAt === undefined ? new Date() : params.createdAt)
260
+ $.checkArgument(Number.isInteger(createdAtUnix) && createdAtUnix >= 0,
261
+ 'createdAtUnix must be a non-negative whole number of seconds')
262
+
263
+ var proofHash = Encoding.proofHash({
264
+ algorithm: params.algorithm,
265
+ hashAlgorithm: params.hashAlgorithm,
266
+ payloadHash: params.payloadHash,
267
+ publicKey: params.publicKey,
268
+ signature: params.signature,
269
+ createdAtUnix: createdAtUnix
270
+ })
271
+
272
+ var certificate = {
273
+ protocol: Certificate.PROTOCOL,
274
+ version: Certificate.REFERENCE_VERSION,
275
+ mode: resolved.mode,
276
+ algorithm: params.algorithm,
277
+ hashAlgorithm: params.hashAlgorithm,
278
+ payloadHash: encodeBytes(params.payloadHash, 'hex', 'payloadHash'),
279
+ publicKey: encodeBytes(params.publicKey, encoding, 'publicKey'),
280
+ signature: encodeBytes(params.signature, encoding, 'signature'),
281
+ encoding: encoding,
282
+ proofHash: proofHash.toString('hex'),
283
+ createdAt: new Date(createdAtUnix * 1000).toISOString(),
284
+ anchor: anchor
285
+ }
286
+
287
+ if (batch) certificate.merkle = buildMerkle(params.merkle)
288
+
289
+ return certificate
290
+ }
291
+
292
+ function legacyHex (buf, name) {
293
+ if (!Buffer.isBuffer(buf)) {
294
+ throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
295
+ }
296
+ return buf.toString('hex')
297
+ }
298
+
299
+ /**
300
+ * The legacy format: 9.8.0's build(), unchanged, so a caller on the 9.x default gets the
301
+ * same certificate byte for byte. test/notaryhash/certificate.js checks that against
302
+ * certificates the 9.8.0 code itself built. Do not tidy this — its quirks (the mode
303
+ * passed through as given, createdAt kept as supplied) are what 9.8.0 wrote.
304
+ */
305
+ function buildLegacy (params) {
79
306
  $.checkArgument(params.anchor && typeof params.anchor === 'object', 'anchor is required')
80
307
  $.checkArgument(typeof params.anchor.txid === 'string', 'anchor.txid must be a string')
81
308
 
@@ -86,7 +313,6 @@ Certificate.build = function (params) {
86
313
  var createdAt = params.createdAt
87
314
  ? (params.createdAt instanceof Date ? params.createdAt.toISOString() : params.createdAt)
88
315
  : new Date().toISOString()
89
-
90
316
  var createdAtUnix = Encoding.toUnixSeconds(createdAt)
91
317
 
92
318
  var proofHash = Encoding.proofHash({
@@ -100,13 +326,13 @@ Certificate.build = function (params) {
100
326
 
101
327
  var certificate = {
102
328
  protocol: Certificate.PROTOCOL,
103
- version: Certificate.VERSION,
329
+ version: LEGACY_VERSION,
104
330
  mode: params.mode,
105
331
  algorithm: params.algorithm,
106
332
  hashAlgorithm: params.hashAlgorithm,
107
- payloadHash: hex(params.payloadHash, 'payloadHash'),
108
- publicKey: hex(params.publicKey, 'publicKey'),
109
- signature: hex(params.signature, 'signature'),
333
+ payloadHash: legacyHex(params.payloadHash, 'payloadHash'),
334
+ publicKey: legacyHex(params.publicKey, 'publicKey'),
335
+ signature: legacyHex(params.signature, 'signature'),
110
336
  encoding: encoding,
111
337
  proofHash: proofHash.toString('hex'),
112
338
  createdAt: createdAt,
@@ -130,35 +356,169 @@ Certificate.build = function (params) {
130
356
  return certificate
131
357
  }
132
358
 
359
+ /**
360
+ * Build a certificate.
361
+ *
362
+ * `proofHash` is computed here rather than accepted, so a caller cannot supply one that
363
+ * does not match the fields beside it.
364
+ *
365
+ * @param {Object} params
366
+ * @param {String} [params.format] - 'reference' writes the BRC-220 reference format.
367
+ * Omitted, 9.x writes the 8.3.0–9.8.0 format and warns once; the default becomes
368
+ * 'reference' in 10.0.0. 'legacy' pins the old format without the notice.
369
+ * @param {String|Number} params.mode - reference: 'full' | 'hybrid' (the numeric
370
+ * NotaryScript.MODE values are still accepted; MODE.BATCH means a full proof on a
371
+ * batch anchor). legacy: a NotaryScript.MODE value.
372
+ * @param {String} params.algorithm
373
+ * @param {String} params.hashAlgorithm - 'SHA-256' for every algorithm the spec lists
374
+ * @param {Buffer} params.payloadHash - raw 32 bytes
375
+ * @param {Buffer} params.publicKey - raw bytes, FULL even in hybrid mode
376
+ * @param {Buffer} params.signature - raw bytes as the signer produced them
377
+ * @param {String} [params.encoding] - reference: 'hex' (default) | 'base64'.
378
+ * legacy: 'raw' (default) | 'der'
379
+ * @param {String|Date} [params.createdAt] - defaults to now
380
+ * @param {Number} [params.createdAtUnix] - reference only; whole seconds
381
+ * @param {Object} params.anchor - reference: { txid, vout?, network?, blockHeight?,
382
+ * blockTime?, type? }. legacy: { txid, blockHeight }
383
+ * @param {Object} [params.merkle] - batch: { root, leafIndex, leafCount, path }. In the
384
+ * reference format path is Merkle.auditPath() output; bare Merkle.path() hashes are
385
+ * converted.
386
+ * @returns {Object} certificate
387
+ */
388
+ Certificate.build = function (params) {
389
+ $.checkArgument(params && typeof params === 'object', 'params is required')
390
+
391
+ var format = params.format
392
+ if (format === undefined) {
393
+ // Per STABILITY.md: mark it in a minor, flip it in a major. 9.x does not change what
394
+ // build() returns, so the default stays the legacy format and the notice carries the
395
+ // migration — as LTP.Claim does for its canonicalization.
396
+ deprecate({
397
+ what: 'NotaryHash.Certificate.build() without a format',
398
+ since: '9.9.0',
399
+ removeIn: '10.0.0',
400
+ use: "NotaryHash.Certificate.build({ ...params, format: 'reference' })",
401
+ why: 'the default format is this library\'s own, which the BRC-220 reference implementation cannot verify'
402
+ })
403
+ return buildLegacy(params)
404
+ }
405
+
406
+ // An unrecognised value throws rather than falling back: a typo quietly selecting the
407
+ // legacy format would recreate the exact failure the format option exists to remove.
408
+ $.checkArgument(format === Certificate.FORMAT.REFERENCE || format === Certificate.FORMAT.LEGACY,
409
+ 'Unknown certificate format: ' + format +
410
+ ". Expected 'reference' or 'legacy' (NotaryHash.Certificate.FORMAT)")
411
+
412
+ return format === Certificate.FORMAT.REFERENCE ? buildReference(params) : buildLegacy(params)
413
+ }
414
+
415
+ /**
416
+ * Is this a certificate in the 8.3.0–9.8.0 format?
417
+ *
418
+ * `version: 1` marks one. So does a numeric `mode` with no version at all: the reference
419
+ * format never has a numeric mode, and 9.8.0's verifyBatchInclusion accepted such objects.
420
+ *
421
+ * @param {Object} certificate
422
+ * @returns {Boolean}
423
+ */
424
+ Certificate.isLegacy = function (certificate) {
425
+ return !!certificate && typeof certificate === 'object' &&
426
+ (certificate.version === LEGACY_VERSION ||
427
+ (certificate.version === undefined && typeof certificate.mode === 'number'))
428
+ }
429
+
430
+ /**
431
+ * Map a certificate onto the reference format.
432
+ *
433
+ * A legacy certificate is translated: `mode` 0/1/2 becomes 'full'/'hybrid'/'full' on a
434
+ * batch anchor, `encoding` "raw"/"der" becomes "hex" (both were hex), the anchor gains
435
+ * the fields the reference requires, and a bare-hash Merkle path gains its sides. A
436
+ * missing version stays missing, so validateShape still reports it. proofHash is
437
+ * untouched: it is over the canonical proof bytes, which the translation does not change.
438
+ *
439
+ * Anything else is returned as it was given, for validateShape to judge. Normalising
440
+ * only the format this library actually wrote keeps the two from blurring into a third
441
+ * that nobody writes.
442
+ *
443
+ * @param {Object} certificate
444
+ * @returns {Object} the reference-format certificate (a new object if translated)
445
+ */
446
+ Certificate.normalize = function (certificate) {
447
+ if (!Certificate.isLegacy(certificate)) return certificate
448
+
449
+ var legacyBatch = certificate.mode === NotaryScript.MODE.BATCH
450
+ var out = Object.assign({}, certificate)
451
+ out.version = certificate.version === LEGACY_VERSION ? Certificate.REFERENCE_VERSION : certificate.version
452
+ out.mode = LEGACY_MODE[certificate.mode] !== undefined ? LEGACY_MODE[certificate.mode] : certificate.mode
453
+ if (certificate.encoding === Certificate.ENCODING.RAW || certificate.encoding === Certificate.ENCODING.DER) {
454
+ out.encoding = Certificate.ENCODING.HEX
455
+ }
456
+
457
+ var a = certificate.anchor && typeof certificate.anchor === 'object' ? certificate.anchor : {}
458
+ out.anchor = {
459
+ type: legacyBatch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT,
460
+ network: a.network || Certificate.DEFAULT_NETWORK,
461
+ txid: a.txid,
462
+ vout: a.vout === undefined ? 0 : a.vout,
463
+ blockHeight: a.blockHeight === undefined ? null : a.blockHeight,
464
+ blockTime: a.blockTime === undefined ? null : a.blockTime
465
+ }
466
+
467
+ if (certificate.merkle && typeof certificate.merkle === 'object') {
468
+ var m = certificate.merkle
469
+ var path = m.path
470
+ try {
471
+ path = sidedPath(m.path, m.leafIndex, m.leafCount)
472
+ } catch (e) {
473
+ // Left as it was: validateShape reports it, and verification then fails.
474
+ }
475
+ out.merkle = { root: m.root, leafIndex: m.leafIndex, leafCount: m.leafCount, path: path }
476
+ }
477
+
478
+ return out
479
+ }
480
+
481
+ /**
482
+ * The raw proof fields a certificate's strings decode to — what the canonical proof
483
+ * bytes, the signature check and the on-chain record comparison all operate on.
484
+ *
485
+ * @param {Object} certificate
486
+ * @returns {Object} { algorithm, hashAlgorithm, payloadHash, publicKey, signature, createdAtUnix }
487
+ */
488
+ Certificate.toProofInput = function (certificate) {
489
+ $.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
490
+ var c = Certificate.normalize(certificate)
491
+ return {
492
+ algorithm: c.algorithm,
493
+ hashAlgorithm: c.hashAlgorithm,
494
+ payloadHash: Certificate.decodeBytes(c.payloadHash, 'hex', 'payloadHash'),
495
+ publicKey: Certificate.decodeBytes(c.publicKey, c.encoding, 'publicKey'),
496
+ signature: Certificate.decodeBytes(c.signature, c.encoding, 'signature'),
497
+ createdAtUnix: Encoding.toUnixSeconds(c.createdAt)
498
+ }
499
+ }
500
+
133
501
  /**
134
502
  * Recompute the proofHash a certificate's own fields imply.
135
503
  *
136
504
  * This is validity check 2 of the three the spec requires, and it needs no network. It
137
- * decodes the hex fields back to bytes first: the canonical proof bytes are over the raw
138
- * bytes, and hex is twice as long, so hashing the strings would produce a value that is
139
- * wrong in a way that still looks like a hash.
505
+ * decodes the string fields back to bytes first: the canonical proof bytes are over the
506
+ * raw bytes, and hashing the strings would produce a value that is wrong in a way that
507
+ * still looks like a hash.
140
508
  *
141
509
  * @param {Object} certificate
142
510
  * @returns {Buffer} 32 bytes
143
511
  */
144
512
  Certificate.recomputeProofHash = function (certificate) {
145
- $.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
146
- return Encoding.proofHash({
147
- algorithm: certificate.algorithm,
148
- hashAlgorithm: certificate.hashAlgorithm,
149
- payloadHash: Buffer.from(certificate.payloadHash, 'hex'),
150
- publicKey: Buffer.from(certificate.publicKey, 'hex'),
151
- signature: Buffer.from(certificate.signature, 'hex'),
152
- createdAtUnix: Encoding.toUnixSeconds(certificate.createdAt)
153
- })
513
+ return Encoding.proofHash(Certificate.toProofInput(certificate))
154
514
  }
155
515
 
156
516
  /**
157
517
  * Does the stated proofHash match the fields beside it?
158
518
  *
159
519
  * Strict boolean. Returns false rather than throwing on a malformed certificate, because
160
- * "this certificate is not valid" is the honest answer to a certificate that cannot be
161
- * parsed, and a caller writing `if (proofHashMatches(c))` must not get a truthy object.
520
+ * "this certificate is not valid" is the honest answer to one that cannot be parsed, and
521
+ * a caller writing `if (proofHashMatches(c))` must not get a truthy object.
162
522
  *
163
523
  * @param {Object} certificate
164
524
  * @returns {Boolean}
@@ -166,7 +526,7 @@ Certificate.recomputeProofHash = function (certificate) {
166
526
  Certificate.proofHashMatches = function (certificate) {
167
527
  try {
168
528
  if (!certificate || typeof certificate.proofHash !== 'string') return false
169
- var stated = Buffer.from(certificate.proofHash, 'hex')
529
+ var stated = Certificate.decodeBytes(certificate.proofHash, 'hex', 'proofHash')
170
530
  if (stated.length !== 32) return false
171
531
  return Certificate.recomputeProofHash(certificate).equals(stated)
172
532
  } catch (e) {
@@ -180,46 +540,131 @@ Certificate.REQUIRED_FIELDS = [
180
540
  'publicKey', 'signature', 'encoding', 'proofHash', 'createdAt', 'anchor'
181
541
  ]
182
542
 
543
+ function checkHash32 (value, name, problems) {
544
+ if (value === undefined) return
545
+ var clean = typeof value === 'string' && value.slice(0, 2) === '0x' ? value.slice(2) : value
546
+ if (typeof clean !== 'string' || !HEX_RE.test(clean) || clean.length % 2 !== 0) {
547
+ problems.push(name + ' must be a hex string')
548
+ } else if (clean.length !== 64) {
549
+ problems.push(name + ' must be 32 bytes (64 hex chars)')
550
+ }
551
+ }
552
+
553
+ function isIntegerOrNull (v) { return v === null || Number.isInteger(v) }
554
+
183
555
  /**
184
556
  * Check a certificate's SHAPE — that the required fields are present and well-formed.
185
557
  *
186
558
  * This is NOT verification. It says nothing about whether the signature is valid, whether
187
559
  * the proofHash matches, or whether the anchor exists. It exists so that those checks can
188
560
  * assume a parseable object, and it returns a list of problems rather than a boolean so
189
- * the caller can report which field is wrong.
561
+ * the caller can report which field is wrong. A legacy certificate is normalised first,
562
+ * so it is judged in the reference format like any other.
190
563
  *
191
564
  * @param {Object} certificate
192
565
  * @returns {Array<String>} problems; empty means the shape is fine
193
566
  */
194
567
  Certificate.validateShape = function (certificate) {
195
- var problems = []
196
-
197
568
  if (!certificate || typeof certificate !== 'object') {
198
569
  return ['certificate must be an object']
199
570
  }
571
+ var c = Certificate.normalize(certificate)
572
+ var problems = []
200
573
 
201
574
  Certificate.REQUIRED_FIELDS.forEach(function (field) {
202
- if (certificate[field] === undefined) problems.push('missing required field: ' + field)
575
+ if (c[field] === undefined) problems.push('missing required field: ' + field)
203
576
  })
204
577
 
205
- if (certificate.protocol !== undefined && certificate.protocol !== Certificate.PROTOCOL) {
578
+ if (c.protocol !== undefined && c.protocol !== Certificate.PROTOCOL) {
206
579
  problems.push('protocol must be "' + Certificate.PROTOCOL + '"')
207
580
  }
208
- if (certificate.version !== undefined && certificate.version !== Certificate.VERSION) {
209
- problems.push('unsupported version: ' + certificate.version)
581
+ if (c.version !== undefined && c.version !== Certificate.REFERENCE_VERSION) {
582
+ problems.push('unsupported version: ' + JSON.stringify(c.version))
583
+ }
584
+ if (c.mode !== undefined && c.mode !== Certificate.MODE.FULL && c.mode !== Certificate.MODE.HYBRID) {
585
+ problems.push('mode must be "full" or "hybrid"')
210
586
  }
211
- ;[[certificate.payloadHash, 'payloadHash', 32], [certificate.proofHash, 'proofHash', 32]]
212
- .forEach(function (pair) {
213
- if (pair[0] === undefined) return
214
- if (typeof pair[0] !== 'string' || !/^[0-9a-fA-F]*$/.test(pair[0])) {
215
- problems.push(pair[1] + ' must be a hex string')
216
- } else if (pair[0].length !== pair[2] * 2) {
217
- problems.push(pair[1] + ' must be ' + pair[2] + ' bytes (' + pair[2] * 2 + ' hex chars)')
587
+ ;['algorithm', 'hashAlgorithm'].forEach(function (field) {
588
+ if (c[field] !== undefined && (typeof c[field] !== 'string' || c[field].length === 0)) {
589
+ problems.push(field + ' must be a non-empty string')
590
+ }
591
+ })
592
+ checkHash32(c.payloadHash, 'payloadHash', problems)
593
+ checkHash32(c.proofHash, 'proofHash', problems)
594
+
595
+ if (c.encoding !== undefined) {
596
+ if (c.encoding !== Certificate.ENCODING.HEX && c.encoding !== Certificate.ENCODING.BASE64) {
597
+ problems.push('encoding must be "hex" or "base64"')
598
+ } else {
599
+ ;['publicKey', 'signature'].forEach(function (field) {
600
+ if (c[field] === undefined) return
601
+ try {
602
+ if (Certificate.decodeBytes(c[field], c.encoding, field).length === 0) {
603
+ problems.push(field + ' must not be empty')
604
+ }
605
+ } catch (e) {
606
+ problems.push(field + ' is not valid ' + c.encoding)
607
+ }
608
+ })
609
+ }
610
+ }
611
+
612
+ if (c.createdAt !== undefined && (typeof c.createdAt !== 'string' || isNaN(new Date(c.createdAt).getTime()))) {
613
+ problems.push('createdAt must be an ISO 8601 date')
614
+ }
615
+
616
+ if (c.anchor !== undefined) {
617
+ var a = c.anchor
618
+ if (!a || typeof a !== 'object') {
619
+ problems.push('anchor must be an object')
620
+ } else {
621
+ if (a.type !== Certificate.ANCHOR_TYPE.DIRECT && a.type !== Certificate.ANCHOR_TYPE.BATCH) {
622
+ problems.push('anchor.type must be "direct" or "batch"')
218
623
  }
219
- })
624
+ if (typeof a.txid !== 'string' || !/^[0-9a-fA-F]{64}$/.test(a.txid)) {
625
+ problems.push('anchor.txid must be a 32-byte hex string')
626
+ }
627
+ if (a.vout !== undefined && !(Number.isInteger(a.vout) && a.vout >= 0)) {
628
+ problems.push('anchor.vout must be a non-negative integer')
629
+ }
630
+ if (a.blockHeight !== undefined && !isIntegerOrNull(a.blockHeight)) {
631
+ problems.push('anchor.blockHeight must be an integer or null')
632
+ }
633
+ if (a.blockTime !== undefined && !isIntegerOrNull(a.blockTime)) {
634
+ problems.push('anchor.blockTime must be an integer or null')
635
+ }
636
+ if (a.type === Certificate.ANCHOR_TYPE.BATCH && c.merkle === undefined) {
637
+ problems.push('batch certificates require a merkle inclusion proof')
638
+ }
639
+ }
640
+ }
220
641
 
221
- if (certificate.mode === NotaryScript.MODE.BATCH && certificate.merkle === undefined) {
222
- problems.push('batch certificates require a merkle inclusion proof')
642
+ if (c.merkle !== undefined) {
643
+ var m = c.merkle
644
+ if (!m || typeof m !== 'object') {
645
+ problems.push('merkle must be an object')
646
+ } else {
647
+ checkHash32(m.root, 'merkle.root', problems)
648
+ if (!(Number.isInteger(m.leafIndex) && m.leafIndex >= 0)) {
649
+ problems.push('merkle.leafIndex must be a non-negative integer')
650
+ }
651
+ if (!(Number.isInteger(m.leafCount) && m.leafCount >= 1)) {
652
+ problems.push('merkle.leafCount must be a positive integer')
653
+ } else if (Number.isInteger(m.leafIndex) && m.leafIndex >= m.leafCount) {
654
+ problems.push('merkle.leafIndex must be less than leafCount')
655
+ }
656
+ if (!Array.isArray(m.path)) {
657
+ problems.push('merkle.path must be an array')
658
+ } else {
659
+ m.path.forEach(function (n, i) {
660
+ if (!n || typeof n !== 'object' || (n.side !== 'left' && n.side !== 'right')) {
661
+ problems.push('merkle.path[' + i + '] must be { hash, side: "left" | "right" }')
662
+ } else {
663
+ checkHash32(n.hash, 'merkle.path[' + i + '].hash', problems)
664
+ }
665
+ })
666
+ }
667
+ }
223
668
  }
224
669
 
225
670
  return problems