@smartledger/bsv 9.8.0 → 9.10.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,311 @@
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
+ // Both RFC 4648 alphabets (§4 standard, §5 URL-safe) and missing padding are accepted, as
105
+ // the reference accepts them. What is NOT accepted is anything that is not base64: a
106
+ // character outside the alphabet, padding out of place, or a length no byte string
107
+ // encodes. Buffer.from skips the first and truncates the last, turning a corrupted field
108
+ // into different bytes instead of an 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
+ var body = value.replace(/=+$/, '')
135
+ if ((body.length !== value.length && value.length % 4 !== 0) || body.length % 4 === 1) {
136
+ throw new Error(name + ' is not a valid base64 length')
137
+ }
138
+ return Buffer.from(value, 'base64')
139
+ }
140
+ throw new Error('encoding must be "hex" or "base64", not ' + JSON.stringify(encoding))
47
141
  }
48
142
 
49
- function hex (buf, name) {
143
+ function encodeBytes (buf, encoding, name) {
50
144
  if (!Buffer.isBuffer(buf)) {
51
145
  throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
52
146
  }
147
+ return encoding === Certificate.ENCODING.BASE64 ? buf.toString('base64') : buf.toString('hex')
148
+ }
149
+
150
+ /** A 32-byte hash field as lowercase hex, from a Buffer or a hex string. */
151
+ function hashHex (value, name) {
152
+ var buf = Buffer.isBuffer(value) ? value : Certificate.decodeBytes(value, 'hex', name)
153
+ if (buf.length !== 32) throw new Error(name + ' must be 32 bytes')
53
154
  return buf.toString('hex')
54
155
  }
55
156
 
157
+ function resolveMode (mode) {
158
+ if (mode === Certificate.MODE.FULL || mode === Certificate.MODE.HYBRID) {
159
+ return { mode: mode, batch: false }
160
+ }
161
+ // The numeric on-chain mode bytes, which is what 8.3.0–9.8.0 took here.
162
+ if (mode === NotaryScript.MODE.FULL) return { mode: 'full', batch: false }
163
+ if (mode === NotaryScript.MODE.HYBRID) return { mode: 'hybrid', batch: false }
164
+ if (mode === NotaryScript.MODE.BATCH) return { mode: 'full', batch: true }
165
+ if (mode === 'batch') {
166
+ throw new Error('batch is an anchor type, not a mode: pass mode "full" or "hybrid" with a merkle proof')
167
+ }
168
+ throw new Error('mode is required: "full" or "hybrid"')
169
+ }
170
+
171
+ function resolveEncoding (encoding) {
172
+ if (encoding === undefined) return Certificate.ENCODING.HEX
173
+ if (encoding === Certificate.ENCODING.HEX || encoding === Certificate.ENCODING.BASE64) {
174
+ return encoding
175
+ }
176
+ // The legacy values named the signature's BYTE format. Both were written as hex, so
177
+ // both mean "hex" in the reference format.
178
+ if (encoding === Certificate.ENCODING.RAW || encoding === Certificate.ENCODING.DER) {
179
+ return Certificate.ENCODING.HEX
180
+ }
181
+ throw new Error('encoding must be "hex" or "base64"')
182
+ }
183
+
56
184
  /**
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
185
+ * An audit path in the `{ hash, side }` form certificates carry, from either that form
186
+ * or the bare hashes 8.3.0–9.8.0 wrote. Bare hashes get their sides from the index and
187
+ * tree size, which is exactly how RFC 6962 determines them.
76
188
  */
77
- Certificate.build = function (params) {
78
- $.checkArgument(params && typeof params === 'object', 'params is required')
189
+ function sidedPath (path, leafIndex, leafCount) {
190
+ if (!Array.isArray(path)) throw new Error('merkle.path must be an array')
191
+ var sided = path.length > 0 && path.every(function (n) {
192
+ return n && typeof n === 'object' && !Buffer.isBuffer(n) && 'side' in n
193
+ })
194
+ if (sided || path.length === 0) {
195
+ return path.map(function (n, i) {
196
+ if (n.side !== 'left' && n.side !== 'right') {
197
+ throw new Error('merkle.path[' + i + '].side must be "left" or "right"')
198
+ }
199
+ return { hash: hashHex(n.hash, 'merkle.path[' + i + '].hash'), side: n.side }
200
+ })
201
+ }
202
+ var sides = Merkle.pathSides(leafIndex, leafCount)
203
+ if (sides.length !== path.length) {
204
+ throw new Error('merkle.path has ' + path.length + ' nodes; a tree of ' + leafCount +
205
+ ' leaves needs ' + sides.length + ' for leaf ' + leafIndex)
206
+ }
207
+ return path.map(function (n, i) {
208
+ return { hash: hashHex(n, 'merkle.path[' + i + ']'), side: sides[i] }
209
+ })
210
+ }
211
+
212
+ function buildMerkle (merkle) {
213
+ $.checkArgument(merkle && typeof merkle === 'object',
214
+ 'batch certificates require a merkle inclusion proof')
215
+ $.checkArgument(Number.isInteger(merkle.leafIndex) && merkle.leafIndex >= 0,
216
+ 'merkle.leafIndex must be a non-negative integer')
217
+ $.checkArgument(Number.isInteger(merkle.leafCount) && merkle.leafCount > merkle.leafIndex,
218
+ 'merkle.leafCount must be an integer greater than leafIndex')
219
+ return {
220
+ root: hashHex(merkle.root, 'merkle.root'),
221
+ leafIndex: merkle.leafIndex,
222
+ leafCount: merkle.leafCount,
223
+ path: sidedPath(merkle.path, merkle.leafIndex, merkle.leafCount)
224
+ }
225
+ }
226
+
227
+ function buildAnchor (anchor, batch) {
228
+ $.checkArgument(anchor && typeof anchor === 'object', 'anchor is required')
229
+ $.checkArgument(typeof anchor.txid === 'string', 'anchor.txid must be a string')
230
+ var type = anchor.type || (batch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT)
231
+ $.checkArgument(type === Certificate.ANCHOR_TYPE.DIRECT || type === Certificate.ANCHOR_TYPE.BATCH,
232
+ 'anchor.type must be "direct" or "batch"')
233
+ $.checkArgument((type === Certificate.ANCHOR_TYPE.BATCH) === batch,
234
+ 'anchor.type is "batch" exactly when a merkle inclusion proof is given')
235
+ return {
236
+ type: type,
237
+ network: anchor.network || Certificate.DEFAULT_NETWORK,
238
+ txid: anchor.txid,
239
+ vout: anchor.vout === undefined ? 0 : anchor.vout,
240
+ blockHeight: anchor.blockHeight === undefined ? null : anchor.blockHeight,
241
+ blockTime: anchor.blockTime === undefined ? null : anchor.blockTime
242
+ }
243
+ }
244
+
245
+ /**
246
+ * The reference format. `createdAt` is written the way the reference writes it — the
247
+ * whole seconds that go into proofHash, as ISO 8601 — so two implementations given the
248
+ * same proof produce the same JSON.
249
+ */
250
+ function buildReference (params) {
251
+ var resolved = resolveMode(params.mode)
252
+ var batch = resolved.batch || params.merkle !== undefined ||
253
+ !!(params.anchor && params.anchor.type === Certificate.ANCHOR_TYPE.BATCH)
254
+ if (batch) {
255
+ $.checkArgument(params.merkle && typeof params.merkle === 'object',
256
+ 'batch certificates require a merkle inclusion proof')
257
+ }
258
+ var encoding = resolveEncoding(params.encoding)
259
+ var anchor = buildAnchor(params.anchor, batch)
260
+
261
+ var createdAtUnix = params.createdAtUnix !== undefined
262
+ ? params.createdAtUnix
263
+ : Encoding.toUnixSeconds(params.createdAt === undefined ? new Date() : params.createdAt)
264
+ $.checkArgument(Number.isInteger(createdAtUnix) && createdAtUnix >= 0,
265
+ 'createdAtUnix must be a non-negative whole number of seconds')
266
+
267
+ var proofHash = Encoding.proofHash({
268
+ algorithm: params.algorithm,
269
+ hashAlgorithm: params.hashAlgorithm,
270
+ payloadHash: params.payloadHash,
271
+ publicKey: params.publicKey,
272
+ signature: params.signature,
273
+ createdAtUnix: createdAtUnix
274
+ })
275
+
276
+ var certificate = {
277
+ protocol: Certificate.PROTOCOL,
278
+ version: Certificate.REFERENCE_VERSION,
279
+ mode: resolved.mode,
280
+ algorithm: params.algorithm,
281
+ hashAlgorithm: params.hashAlgorithm,
282
+ payloadHash: encodeBytes(params.payloadHash, 'hex', 'payloadHash'),
283
+ publicKey: encodeBytes(params.publicKey, encoding, 'publicKey'),
284
+ signature: encodeBytes(params.signature, encoding, 'signature'),
285
+ encoding: encoding,
286
+ proofHash: proofHash.toString('hex'),
287
+ createdAt: new Date(createdAtUnix * 1000).toISOString(),
288
+ anchor: anchor
289
+ }
290
+
291
+ if (batch) certificate.merkle = buildMerkle(params.merkle)
292
+
293
+ return certificate
294
+ }
295
+
296
+ function legacyHex (buf, name) {
297
+ if (!Buffer.isBuffer(buf)) {
298
+ throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
299
+ }
300
+ return buf.toString('hex')
301
+ }
302
+
303
+ /**
304
+ * The legacy format: 9.8.0's build(), unchanged, so a caller on the 9.x default gets the
305
+ * same certificate byte for byte. test/notaryhash/certificate.js checks that against
306
+ * certificates the 9.8.0 code itself built. Do not tidy this — its quirks (the mode
307
+ * passed through as given, createdAt kept as supplied) are what 9.8.0 wrote.
308
+ */
309
+ function buildLegacy (params) {
79
310
  $.checkArgument(params.anchor && typeof params.anchor === 'object', 'anchor is required')
80
311
  $.checkArgument(typeof params.anchor.txid === 'string', 'anchor.txid must be a string')
81
312
 
@@ -86,7 +317,6 @@ Certificate.build = function (params) {
86
317
  var createdAt = params.createdAt
87
318
  ? (params.createdAt instanceof Date ? params.createdAt.toISOString() : params.createdAt)
88
319
  : new Date().toISOString()
89
-
90
320
  var createdAtUnix = Encoding.toUnixSeconds(createdAt)
91
321
 
92
322
  var proofHash = Encoding.proofHash({
@@ -100,13 +330,13 @@ Certificate.build = function (params) {
100
330
 
101
331
  var certificate = {
102
332
  protocol: Certificate.PROTOCOL,
103
- version: Certificate.VERSION,
333
+ version: LEGACY_VERSION,
104
334
  mode: params.mode,
105
335
  algorithm: params.algorithm,
106
336
  hashAlgorithm: params.hashAlgorithm,
107
- payloadHash: hex(params.payloadHash, 'payloadHash'),
108
- publicKey: hex(params.publicKey, 'publicKey'),
109
- signature: hex(params.signature, 'signature'),
337
+ payloadHash: legacyHex(params.payloadHash, 'payloadHash'),
338
+ publicKey: legacyHex(params.publicKey, 'publicKey'),
339
+ signature: legacyHex(params.signature, 'signature'),
110
340
  encoding: encoding,
111
341
  proofHash: proofHash.toString('hex'),
112
342
  createdAt: createdAt,
@@ -130,35 +360,169 @@ Certificate.build = function (params) {
130
360
  return certificate
131
361
  }
132
362
 
363
+ /**
364
+ * Build a certificate.
365
+ *
366
+ * `proofHash` is computed here rather than accepted, so a caller cannot supply one that
367
+ * does not match the fields beside it.
368
+ *
369
+ * @param {Object} params
370
+ * @param {String} [params.format] - 'reference' writes the BRC-220 reference format.
371
+ * Omitted, 9.x writes the 8.3.0–9.8.0 format and warns once; the default becomes
372
+ * 'reference' in 10.0.0. 'legacy' pins the old format without the notice.
373
+ * @param {String|Number} params.mode - reference: 'full' | 'hybrid' (the numeric
374
+ * NotaryScript.MODE values are still accepted; MODE.BATCH means a full proof on a
375
+ * batch anchor). legacy: a NotaryScript.MODE value.
376
+ * @param {String} params.algorithm
377
+ * @param {String} params.hashAlgorithm - 'SHA-256' for every algorithm the spec lists
378
+ * @param {Buffer} params.payloadHash - raw 32 bytes
379
+ * @param {Buffer} params.publicKey - raw bytes, FULL even in hybrid mode
380
+ * @param {Buffer} params.signature - raw bytes as the signer produced them
381
+ * @param {String} [params.encoding] - reference: 'hex' (default) | 'base64'.
382
+ * legacy: 'raw' (default) | 'der'
383
+ * @param {String|Date} [params.createdAt] - defaults to now
384
+ * @param {Number} [params.createdAtUnix] - reference only; whole seconds
385
+ * @param {Object} params.anchor - reference: { txid, vout?, network?, blockHeight?,
386
+ * blockTime?, type? }. legacy: { txid, blockHeight }
387
+ * @param {Object} [params.merkle] - batch: { root, leafIndex, leafCount, path }. In the
388
+ * reference format path is Merkle.auditPath() output; bare Merkle.path() hashes are
389
+ * converted.
390
+ * @returns {Object} certificate
391
+ */
392
+ Certificate.build = function (params) {
393
+ $.checkArgument(params && typeof params === 'object', 'params is required')
394
+
395
+ var format = params.format
396
+ if (format === undefined) {
397
+ // Per STABILITY.md: mark it in a minor, flip it in a major. 9.x does not change what
398
+ // build() returns, so the default stays the legacy format and the notice carries the
399
+ // migration — as LTP.Claim does for its canonicalization.
400
+ deprecate({
401
+ what: 'NotaryHash.Certificate.build() without a format',
402
+ since: '9.9.0',
403
+ removeIn: '10.0.0',
404
+ use: "NotaryHash.Certificate.build({ ...params, format: 'reference' })",
405
+ why: 'the default format is this library\'s own, which the BRC-220 reference implementation cannot verify'
406
+ })
407
+ return buildLegacy(params)
408
+ }
409
+
410
+ // An unrecognised value throws rather than falling back: a typo quietly selecting the
411
+ // legacy format would recreate the exact failure the format option exists to remove.
412
+ $.checkArgument(format === Certificate.FORMAT.REFERENCE || format === Certificate.FORMAT.LEGACY,
413
+ 'Unknown certificate format: ' + format +
414
+ ". Expected 'reference' or 'legacy' (NotaryHash.Certificate.FORMAT)")
415
+
416
+ return format === Certificate.FORMAT.REFERENCE ? buildReference(params) : buildLegacy(params)
417
+ }
418
+
419
+ /**
420
+ * Is this a certificate in the 8.3.0–9.8.0 format?
421
+ *
422
+ * `version: 1` marks one. So does a numeric `mode` with no version at all: the reference
423
+ * format never has a numeric mode, and 9.8.0's verifyBatchInclusion accepted such objects.
424
+ *
425
+ * @param {Object} certificate
426
+ * @returns {Boolean}
427
+ */
428
+ Certificate.isLegacy = function (certificate) {
429
+ return !!certificate && typeof certificate === 'object' &&
430
+ (certificate.version === LEGACY_VERSION ||
431
+ (certificate.version === undefined && typeof certificate.mode === 'number'))
432
+ }
433
+
434
+ /**
435
+ * Map a certificate onto the reference format.
436
+ *
437
+ * A legacy certificate is translated: `mode` 0/1/2 becomes 'full'/'hybrid'/'full' on a
438
+ * batch anchor, `encoding` "raw"/"der" becomes "hex" (both were hex), the anchor gains
439
+ * the fields the reference requires, and a bare-hash Merkle path gains its sides. A
440
+ * missing version stays missing, so validateShape still reports it. proofHash is
441
+ * untouched: it is over the canonical proof bytes, which the translation does not change.
442
+ *
443
+ * Anything else is returned as it was given, for validateShape to judge. Normalising
444
+ * only the format this library actually wrote keeps the two from blurring into a third
445
+ * that nobody writes.
446
+ *
447
+ * @param {Object} certificate
448
+ * @returns {Object} the reference-format certificate (a new object if translated)
449
+ */
450
+ Certificate.normalize = function (certificate) {
451
+ if (!Certificate.isLegacy(certificate)) return certificate
452
+
453
+ var legacyBatch = certificate.mode === NotaryScript.MODE.BATCH
454
+ var out = Object.assign({}, certificate)
455
+ out.version = certificate.version === LEGACY_VERSION ? Certificate.REFERENCE_VERSION : certificate.version
456
+ out.mode = LEGACY_MODE[certificate.mode] !== undefined ? LEGACY_MODE[certificate.mode] : certificate.mode
457
+ if (certificate.encoding === Certificate.ENCODING.RAW || certificate.encoding === Certificate.ENCODING.DER) {
458
+ out.encoding = Certificate.ENCODING.HEX
459
+ }
460
+
461
+ var a = certificate.anchor && typeof certificate.anchor === 'object' ? certificate.anchor : {}
462
+ out.anchor = {
463
+ type: legacyBatch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT,
464
+ network: a.network || Certificate.DEFAULT_NETWORK,
465
+ txid: a.txid,
466
+ vout: a.vout === undefined ? 0 : a.vout,
467
+ blockHeight: a.blockHeight === undefined ? null : a.blockHeight,
468
+ blockTime: a.blockTime === undefined ? null : a.blockTime
469
+ }
470
+
471
+ if (certificate.merkle && typeof certificate.merkle === 'object') {
472
+ var m = certificate.merkle
473
+ var path = m.path
474
+ try {
475
+ path = sidedPath(m.path, m.leafIndex, m.leafCount)
476
+ } catch (e) {
477
+ // Left as it was: validateShape reports it, and verification then fails.
478
+ }
479
+ out.merkle = { root: m.root, leafIndex: m.leafIndex, leafCount: m.leafCount, path: path }
480
+ }
481
+
482
+ return out
483
+ }
484
+
485
+ /**
486
+ * The raw proof fields a certificate's strings decode to — what the canonical proof
487
+ * bytes, the signature check and the on-chain record comparison all operate on.
488
+ *
489
+ * @param {Object} certificate
490
+ * @returns {Object} { algorithm, hashAlgorithm, payloadHash, publicKey, signature, createdAtUnix }
491
+ */
492
+ Certificate.toProofInput = function (certificate) {
493
+ $.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
494
+ var c = Certificate.normalize(certificate)
495
+ return {
496
+ algorithm: c.algorithm,
497
+ hashAlgorithm: c.hashAlgorithm,
498
+ payloadHash: Certificate.decodeBytes(c.payloadHash, 'hex', 'payloadHash'),
499
+ publicKey: Certificate.decodeBytes(c.publicKey, c.encoding, 'publicKey'),
500
+ signature: Certificate.decodeBytes(c.signature, c.encoding, 'signature'),
501
+ createdAtUnix: Encoding.toUnixSeconds(c.createdAt)
502
+ }
503
+ }
504
+
133
505
  /**
134
506
  * Recompute the proofHash a certificate's own fields imply.
135
507
  *
136
508
  * 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.
509
+ * decodes the string fields back to bytes first: the canonical proof bytes are over the
510
+ * raw bytes, and hashing the strings would produce a value that is wrong in a way that
511
+ * still looks like a hash.
140
512
  *
141
513
  * @param {Object} certificate
142
514
  * @returns {Buffer} 32 bytes
143
515
  */
144
516
  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
- })
517
+ return Encoding.proofHash(Certificate.toProofInput(certificate))
154
518
  }
155
519
 
156
520
  /**
157
521
  * Does the stated proofHash match the fields beside it?
158
522
  *
159
523
  * 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.
524
+ * "this certificate is not valid" is the honest answer to one that cannot be parsed, and
525
+ * a caller writing `if (proofHashMatches(c))` must not get a truthy object.
162
526
  *
163
527
  * @param {Object} certificate
164
528
  * @returns {Boolean}
@@ -166,7 +530,7 @@ Certificate.recomputeProofHash = function (certificate) {
166
530
  Certificate.proofHashMatches = function (certificate) {
167
531
  try {
168
532
  if (!certificate || typeof certificate.proofHash !== 'string') return false
169
- var stated = Buffer.from(certificate.proofHash, 'hex')
533
+ var stated = Certificate.decodeBytes(certificate.proofHash, 'hex', 'proofHash')
170
534
  if (stated.length !== 32) return false
171
535
  return Certificate.recomputeProofHash(certificate).equals(stated)
172
536
  } catch (e) {
@@ -180,46 +544,131 @@ Certificate.REQUIRED_FIELDS = [
180
544
  'publicKey', 'signature', 'encoding', 'proofHash', 'createdAt', 'anchor'
181
545
  ]
182
546
 
547
+ function checkHash32 (value, name, problems) {
548
+ if (value === undefined) return
549
+ var clean = typeof value === 'string' && value.slice(0, 2) === '0x' ? value.slice(2) : value
550
+ if (typeof clean !== 'string' || !HEX_RE.test(clean) || clean.length % 2 !== 0) {
551
+ problems.push(name + ' must be a hex string')
552
+ } else if (clean.length !== 64) {
553
+ problems.push(name + ' must be 32 bytes (64 hex chars)')
554
+ }
555
+ }
556
+
557
+ function isIntegerOrNull (v) { return v === null || Number.isInteger(v) }
558
+
183
559
  /**
184
560
  * Check a certificate's SHAPE — that the required fields are present and well-formed.
185
561
  *
186
562
  * This is NOT verification. It says nothing about whether the signature is valid, whether
187
563
  * the proofHash matches, or whether the anchor exists. It exists so that those checks can
188
564
  * 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.
565
+ * the caller can report which field is wrong. A legacy certificate is normalised first,
566
+ * so it is judged in the reference format like any other.
190
567
  *
191
568
  * @param {Object} certificate
192
569
  * @returns {Array<String>} problems; empty means the shape is fine
193
570
  */
194
571
  Certificate.validateShape = function (certificate) {
195
- var problems = []
196
-
197
572
  if (!certificate || typeof certificate !== 'object') {
198
573
  return ['certificate must be an object']
199
574
  }
575
+ var c = Certificate.normalize(certificate)
576
+ var problems = []
200
577
 
201
578
  Certificate.REQUIRED_FIELDS.forEach(function (field) {
202
- if (certificate[field] === undefined) problems.push('missing required field: ' + field)
579
+ if (c[field] === undefined) problems.push('missing required field: ' + field)
203
580
  })
204
581
 
205
- if (certificate.protocol !== undefined && certificate.protocol !== Certificate.PROTOCOL) {
582
+ if (c.protocol !== undefined && c.protocol !== Certificate.PROTOCOL) {
206
583
  problems.push('protocol must be "' + Certificate.PROTOCOL + '"')
207
584
  }
208
- if (certificate.version !== undefined && certificate.version !== Certificate.VERSION) {
209
- problems.push('unsupported version: ' + certificate.version)
585
+ if (c.version !== undefined && c.version !== Certificate.REFERENCE_VERSION) {
586
+ problems.push('unsupported version: ' + JSON.stringify(c.version))
587
+ }
588
+ if (c.mode !== undefined && c.mode !== Certificate.MODE.FULL && c.mode !== Certificate.MODE.HYBRID) {
589
+ problems.push('mode must be "full" or "hybrid"')
210
590
  }
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)')
591
+ ;['algorithm', 'hashAlgorithm'].forEach(function (field) {
592
+ if (c[field] !== undefined && (typeof c[field] !== 'string' || c[field].length === 0)) {
593
+ problems.push(field + ' must be a non-empty string')
594
+ }
595
+ })
596
+ checkHash32(c.payloadHash, 'payloadHash', problems)
597
+ checkHash32(c.proofHash, 'proofHash', problems)
598
+
599
+ if (c.encoding !== undefined) {
600
+ if (c.encoding !== Certificate.ENCODING.HEX && c.encoding !== Certificate.ENCODING.BASE64) {
601
+ problems.push('encoding must be "hex" or "base64"')
602
+ } else {
603
+ ;['publicKey', 'signature'].forEach(function (field) {
604
+ if (c[field] === undefined) return
605
+ try {
606
+ if (Certificate.decodeBytes(c[field], c.encoding, field).length === 0) {
607
+ problems.push(field + ' must not be empty')
608
+ }
609
+ } catch (e) {
610
+ problems.push(field + ' is not valid ' + c.encoding)
611
+ }
612
+ })
613
+ }
614
+ }
615
+
616
+ if (c.createdAt !== undefined && (typeof c.createdAt !== 'string' || isNaN(new Date(c.createdAt).getTime()))) {
617
+ problems.push('createdAt must be an ISO 8601 date')
618
+ }
619
+
620
+ if (c.anchor !== undefined) {
621
+ var a = c.anchor
622
+ if (!a || typeof a !== 'object') {
623
+ problems.push('anchor must be an object')
624
+ } else {
625
+ if (a.type !== Certificate.ANCHOR_TYPE.DIRECT && a.type !== Certificate.ANCHOR_TYPE.BATCH) {
626
+ problems.push('anchor.type must be "direct" or "batch"')
218
627
  }
219
- })
628
+ if (typeof a.txid !== 'string' || !/^[0-9a-fA-F]{64}$/.test(a.txid)) {
629
+ problems.push('anchor.txid must be a 32-byte hex string')
630
+ }
631
+ if (a.vout !== undefined && !(Number.isInteger(a.vout) && a.vout >= 0)) {
632
+ problems.push('anchor.vout must be a non-negative integer')
633
+ }
634
+ if (a.blockHeight !== undefined && !isIntegerOrNull(a.blockHeight)) {
635
+ problems.push('anchor.blockHeight must be an integer or null')
636
+ }
637
+ if (a.blockTime !== undefined && !isIntegerOrNull(a.blockTime)) {
638
+ problems.push('anchor.blockTime must be an integer or null')
639
+ }
640
+ if (a.type === Certificate.ANCHOR_TYPE.BATCH && c.merkle === undefined) {
641
+ problems.push('batch certificates require a merkle inclusion proof')
642
+ }
643
+ }
644
+ }
220
645
 
221
- if (certificate.mode === NotaryScript.MODE.BATCH && certificate.merkle === undefined) {
222
- problems.push('batch certificates require a merkle inclusion proof')
646
+ if (c.merkle !== undefined) {
647
+ var m = c.merkle
648
+ if (!m || typeof m !== 'object') {
649
+ problems.push('merkle must be an object')
650
+ } else {
651
+ checkHash32(m.root, 'merkle.root', problems)
652
+ if (!(Number.isInteger(m.leafIndex) && m.leafIndex >= 0)) {
653
+ problems.push('merkle.leafIndex must be a non-negative integer')
654
+ }
655
+ if (!(Number.isInteger(m.leafCount) && m.leafCount >= 1)) {
656
+ problems.push('merkle.leafCount must be a positive integer')
657
+ } else if (Number.isInteger(m.leafIndex) && m.leafIndex >= m.leafCount) {
658
+ problems.push('merkle.leafIndex must be less than leafCount')
659
+ }
660
+ if (!Array.isArray(m.path)) {
661
+ problems.push('merkle.path must be an array')
662
+ } else {
663
+ m.path.forEach(function (n, i) {
664
+ if (!n || typeof n !== 'object' || (n.side !== 'left' && n.side !== 'right')) {
665
+ problems.push('merkle.path[' + i + '] must be { hash, side: "left" | "right" }')
666
+ } else {
667
+ checkHash32(n.hash, 'merkle.path[' + i + '].hash', problems)
668
+ }
669
+ })
670
+ }
671
+ }
223
672
  }
224
673
 
225
674
  return problems