@nexart/cli 0.12.2 → 0.15.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.
- package/CHANGELOG.md +83 -0
- package/README.md +102 -3
- package/dist/index.d.ts +37 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +346 -17
- package/dist/index.js.map +1 -1
- package/dist/tsaVerify.d.ts +38 -0
- package/dist/tsaVerify.d.ts.map +1 -0
- package/dist/tsaVerify.js +247 -0
- package/dist/tsaVerify.js.map +1 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1345,7 +1345,7 @@ export async function exportVerificationCommand(file, opts, stdinReader = readSt
|
|
|
1345
1345
|
* NOTE: named `verify-bundle` (not `verify`) because `nexart verify <snapshot>`
|
|
1346
1346
|
* already exists for Code Mode snapshots and must not change (additive only).
|
|
1347
1347
|
*/
|
|
1348
|
-
export function verifyBundleStandaloneCommand(file, opts) {
|
|
1348
|
+
export async function verifyBundleStandaloneCommand(file, opts) {
|
|
1349
1349
|
let bundle;
|
|
1350
1350
|
try {
|
|
1351
1351
|
bundle = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
@@ -1369,34 +1369,304 @@ export function verifyBundleStandaloneCommand(file, opts) {
|
|
|
1369
1369
|
kid: opts.kid,
|
|
1370
1370
|
verbose: opts.verbose,
|
|
1371
1371
|
});
|
|
1372
|
+
// ── OPTIONAL, ADDITIVE: RFC 3161 timestamp verification ──────────────────────
|
|
1373
|
+
// Strictly additive: this NEVER alters integrity, signature, status, or the exit
|
|
1374
|
+
// code, and the heavy ASN.1 stack is only imported when --verify-timestamp is set.
|
|
1375
|
+
// resolveTimestampResult stays synchronous unless it actually verifies a token, so
|
|
1376
|
+
// a timestamp-less bundle never introduces an await (preserving prior sync behavior).
|
|
1377
|
+
const maybeTsa = resolveTimestampResult(evaluateBundleTimestamp(bundle), opts);
|
|
1378
|
+
const tsaResult = maybeTsa instanceof Promise ? await maybeTsa : maybeTsa;
|
|
1379
|
+
renderStandaloneVerification(result, tsaResult, opts);
|
|
1380
|
+
// Exit code is determined SOLELY by integrity + signature — never by the timestamp.
|
|
1381
|
+
process.exitCode = result.status === 'verified' ? 0 : 1;
|
|
1382
|
+
return result;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Resolve the (strictly additive) RFC 3161 timestamp status for an already-located
|
|
1386
|
+
* timestamp. Shared by `verify-bundle` and `verify-record`. Every operational failure
|
|
1387
|
+
* is mapped to a timestamp STATUS — never to process.exit or a thrown error — so the
|
|
1388
|
+
* timestamp layer can never influence the exit code. The ASN.1 stack (pkijs/asn1js)
|
|
1389
|
+
* is dynamically imported ONLY when --verify-timestamp is set and a token is present.
|
|
1390
|
+
*
|
|
1391
|
+
* Returns SYNCHRONOUSLY for every non-verify path (not-present / present-unverified /
|
|
1392
|
+
* unreadable-roots) and only returns a Promise when it actually has async work to do
|
|
1393
|
+
* (the dynamic import + verification). This mirrors the original verify-bundle control
|
|
1394
|
+
* flow exactly — where the sole `await` lived behind `present && --verify-timestamp` —
|
|
1395
|
+
* so timestamp-less bundles stay fully synchronous (callers must handle both shapes).
|
|
1396
|
+
*/
|
|
1397
|
+
function resolveTimestampResult(timestampInfo, opts) {
|
|
1398
|
+
if (!timestampInfo.present)
|
|
1399
|
+
return { status: 'not-present' };
|
|
1400
|
+
if (!opts.verifyTimestamp)
|
|
1401
|
+
return { status: 'present-unverified' };
|
|
1402
|
+
let trustedRootsPem;
|
|
1403
|
+
if (opts.tsaRoots) {
|
|
1404
|
+
try {
|
|
1405
|
+
trustedRootsPem = fs.readFileSync(opts.tsaRoots, 'utf-8');
|
|
1406
|
+
}
|
|
1407
|
+
catch (err) {
|
|
1408
|
+
return {
|
|
1409
|
+
status: 'untrusted-root',
|
|
1410
|
+
reason: `could not read TSA roots "${opts.tsaRoots}": ${err instanceof Error ? err.message : String(err)}`,
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return (async () => {
|
|
1415
|
+
try {
|
|
1416
|
+
const { verifyRfc3161Timestamp } = await import('./tsaVerify.js');
|
|
1417
|
+
return await verifyRfc3161Timestamp({
|
|
1418
|
+
token: timestampInfo.token,
|
|
1419
|
+
certificateHash: timestampInfo.certificateHash ?? '',
|
|
1420
|
+
createdAt: timestampInfo.createdAt,
|
|
1421
|
+
trustedRootsPem,
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
catch (err) {
|
|
1425
|
+
return { status: 'malformed', reason: err instanceof Error ? err.message : String(err) };
|
|
1426
|
+
}
|
|
1427
|
+
})();
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Render a standalone verification result plus its (additive) timestamp status, in
|
|
1431
|
+
* either the human-readable or --json form. Shared by `verify-bundle` and
|
|
1432
|
+
* `verify-record` so the two commands are byte-for-byte identical in output. This
|
|
1433
|
+
* never sets the exit code — that stays a function of integrity + signature alone.
|
|
1434
|
+
*/
|
|
1435
|
+
function renderStandaloneVerification(result, tsaResult, opts) {
|
|
1372
1436
|
if (opts.json) {
|
|
1373
1437
|
process.stdout.write(JSON.stringify({
|
|
1374
1438
|
status: result.status,
|
|
1375
1439
|
integrity: result.integrity,
|
|
1376
1440
|
signature: result.signature,
|
|
1377
1441
|
protocolVersion: result.protocolVersion,
|
|
1442
|
+
timestamp: {
|
|
1443
|
+
status: tsaResult.status,
|
|
1444
|
+
...(tsaResult.tsa ? { tsa: tsaResult.tsa } : {}),
|
|
1445
|
+
...(tsaResult.genTime ? { genTime: tsaResult.genTime } : {}),
|
|
1446
|
+
...(tsaResult.reason ? { reason: tsaResult.reason } : {}),
|
|
1447
|
+
},
|
|
1378
1448
|
}, null, 2) + '\n');
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const mark = (p) => p === 'pass' ? '\u2714' : p === 'fail' ? '\u2716' : '\u25CB';
|
|
1452
|
+
const label = (p) => p.toUpperCase();
|
|
1453
|
+
if (opts.verbose && result.steps.length > 0) {
|
|
1454
|
+
console.log('Verification steps:');
|
|
1455
|
+
for (const s of result.steps)
|
|
1456
|
+
console.log(` - ${s}`);
|
|
1457
|
+
console.log('');
|
|
1379
1458
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1459
|
+
console.log(`${mark(result.integrity)} Integrity: ${label(result.integrity)}`);
|
|
1460
|
+
console.log(`${mark(result.signature)} Signature: ${label(result.signature)}`);
|
|
1461
|
+
console.log(`Protocol: ${result.protocolVersion}`);
|
|
1462
|
+
console.log(formatTimestampLine(tsaResult.status));
|
|
1463
|
+
const tsDetails = formatTimestampDetails(tsaResult);
|
|
1464
|
+
if (tsDetails.length > 0) {
|
|
1465
|
+
console.log('');
|
|
1466
|
+
console.log('Timestamp Details:');
|
|
1467
|
+
for (const d of tsDetails)
|
|
1468
|
+
console.log(` ${d}`);
|
|
1469
|
+
}
|
|
1470
|
+
if (result.status === 'failed' || result.reasons.length > 0) {
|
|
1471
|
+
for (const r of result.reasons)
|
|
1472
|
+
console.error(` ! ${r}`);
|
|
1473
|
+
}
|
|
1474
|
+
if (opts.verbose && tsaResult.reason && tsaResult.status !== 'valid') {
|
|
1475
|
+
console.error(` ! Timestamp: ${tsaResult.reason}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* nexart verify-record — Verify a full NexArt CER RECORD offline and independently.
|
|
1480
|
+
*
|
|
1481
|
+
* A "record" is the full verification envelope: { bundle, verification: { receipt,
|
|
1482
|
+
* receiptSignature }, timestamp? }. This command is STRICTLY ADDITIVE alongside
|
|
1483
|
+
* `verify-bundle` and reuses the exact same engine:
|
|
1484
|
+
* - INTEGRITY + SIGNATURE: the record's `verification.receipt` /
|
|
1485
|
+
* `verification.receiptSignature` are merged into a SHALLOW copy of `bundle`
|
|
1486
|
+
* (which never affects the recomputed certificateHash — receipt/signature are
|
|
1487
|
+
* not part of the certificate payload) and handed to verifyBundleStandalone().
|
|
1488
|
+
* Behaviour, including --public-key / --kid, is identical to verify-bundle.
|
|
1489
|
+
* - TIMESTAMP (optional, additive): the record-level `timestamp.token` (or a
|
|
1490
|
+
* bundle-embedded timestamp) is verified via the same resolveTimestampResult()
|
|
1491
|
+
* path used by verify-bundle. It NEVER affects integrity, signature, or the
|
|
1492
|
+
* exit code, and the ASN.1 stack loads only behind --verify-timestamp.
|
|
1493
|
+
*/
|
|
1494
|
+
export async function verifyRecordStandaloneCommand(file, opts) {
|
|
1495
|
+
let record;
|
|
1496
|
+
try {
|
|
1497
|
+
record = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1498
|
+
}
|
|
1499
|
+
catch (err) {
|
|
1500
|
+
console.error(`[nexart] Error: could not read record "${file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1501
|
+
process.exit(1);
|
|
1502
|
+
}
|
|
1503
|
+
let publicKeyMaterial;
|
|
1504
|
+
if (opts.publicKey) {
|
|
1505
|
+
try {
|
|
1506
|
+
publicKeyMaterial = fs.readFileSync(opts.publicKey, 'utf-8');
|
|
1388
1507
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
if (result.status === 'failed' || result.reasons.length > 0) {
|
|
1393
|
-
for (const r of result.reasons)
|
|
1394
|
-
console.error(` ! ${r}`);
|
|
1508
|
+
catch (err) {
|
|
1509
|
+
console.error(`[nexart] Error: could not read public key "${opts.publicKey}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1510
|
+
process.exit(1);
|
|
1395
1511
|
}
|
|
1396
1512
|
}
|
|
1513
|
+
const rec = (record && typeof record === 'object') ? record : {};
|
|
1514
|
+
const bundleObj = rec.bundle && typeof rec.bundle === 'object' ? rec.bundle : undefined;
|
|
1515
|
+
if (!bundleObj) {
|
|
1516
|
+
// Malformed record: surface a clear reason but DO NOT crash the process — fall
|
|
1517
|
+
// through to the standalone verifier, which reports a clean integrity FAIL.
|
|
1518
|
+
console.error('[nexart] Error: record is missing a "bundle" object');
|
|
1519
|
+
}
|
|
1520
|
+
// Merge the record's signed receipt into a SHALLOW copy of the bundle so the
|
|
1521
|
+
// existing verifier finds it. This never mutates the original and never changes the
|
|
1522
|
+
// recomputed certificateHash (receipt/signature are not part of the cert payload).
|
|
1523
|
+
const verification = rec.verification && typeof rec.verification === 'object' ? rec.verification : undefined;
|
|
1524
|
+
const receipt = verification?.receipt;
|
|
1525
|
+
const receiptSignature = verification?.receiptSignature;
|
|
1526
|
+
const verifyInput = { ...bundleObj };
|
|
1527
|
+
if (receipt && typeof receipt === 'object' && typeof receiptSignature === 'string') {
|
|
1528
|
+
verifyInput.receipt = receipt;
|
|
1529
|
+
verifyInput.signature = receiptSignature;
|
|
1530
|
+
}
|
|
1531
|
+
const result = verifyBundleStandalone(bundleObj ? verifyInput : undefined, {
|
|
1532
|
+
publicKeyMaterial,
|
|
1533
|
+
kid: opts.kid,
|
|
1534
|
+
verbose: opts.verbose,
|
|
1535
|
+
});
|
|
1536
|
+
// Locate the timestamp for the additive layer. The record-level `timestamp`
|
|
1537
|
+
// (singular) takes precedence; otherwise fall back to a bundle-embedded one. The
|
|
1538
|
+
// certificateHash / createdAt the verifier needs always come from the bundle.
|
|
1539
|
+
const tsLocatorInput = rec.timestamp !== undefined
|
|
1540
|
+
? { certificateHash: bundleObj?.certificateHash, createdAt: bundleObj?.createdAt, timestamp: rec.timestamp }
|
|
1541
|
+
: {
|
|
1542
|
+
certificateHash: bundleObj?.certificateHash,
|
|
1543
|
+
createdAt: bundleObj?.createdAt,
|
|
1544
|
+
timestamp: bundleObj?.timestamp,
|
|
1545
|
+
trustedTimestamps: bundleObj?.trustedTimestamps,
|
|
1546
|
+
};
|
|
1547
|
+
const maybeTsa = resolveTimestampResult(evaluateBundleTimestamp(tsLocatorInput), opts);
|
|
1548
|
+
const tsaResult = maybeTsa instanceof Promise ? await maybeTsa : maybeTsa;
|
|
1549
|
+
renderStandaloneVerification(result, tsaResult, opts);
|
|
1550
|
+
// Exit code is determined SOLELY by integrity + signature — never by the timestamp.
|
|
1397
1551
|
process.exitCode = result.status === 'verified' ? 0 : 1;
|
|
1398
1552
|
return result;
|
|
1399
1553
|
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Locate an RFC 3161 timestamp in a CER bundle WITHOUT mutating it. Supports both
|
|
1556
|
+
* the canonical `trustedTimestamps[]` array (each { type:'rfc3161', tsa, timestamp,
|
|
1557
|
+
* token }) and a singular `timestamp` object. Returns the base64 token plus the
|
|
1558
|
+
* certificateHash/createdAt needed to verify it. Reading this never affects any
|
|
1559
|
+
* other verification result.
|
|
1560
|
+
*/
|
|
1561
|
+
function evaluateBundleTimestamp(bundle) {
|
|
1562
|
+
if (!bundle || typeof bundle !== 'object')
|
|
1563
|
+
return { present: false };
|
|
1564
|
+
const b = bundle;
|
|
1565
|
+
const certificateHash = typeof b.certificateHash === 'string' ? b.certificateHash : undefined;
|
|
1566
|
+
const createdAt = typeof b.createdAt === 'string' ? b.createdAt : undefined;
|
|
1567
|
+
let token;
|
|
1568
|
+
const arr = b.trustedTimestamps;
|
|
1569
|
+
if (Array.isArray(arr)) {
|
|
1570
|
+
const entry = arr.find((e) => e && typeof e === 'object' && e.type === 'rfc3161' && typeof e.token === 'string');
|
|
1571
|
+
if (entry)
|
|
1572
|
+
token = entry.token;
|
|
1573
|
+
}
|
|
1574
|
+
if (!token && b.timestamp && typeof b.timestamp === 'object') {
|
|
1575
|
+
const t = b.timestamp;
|
|
1576
|
+
if (typeof t.token === 'string')
|
|
1577
|
+
token = t.token;
|
|
1578
|
+
}
|
|
1579
|
+
return token ? { present: true, token, certificateHash, createdAt } : { present: false };
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Render the human-readable timestamp STATUS line WITHOUT importing the ASN.1 stack.
|
|
1583
|
+
* This deliberately mirrors formatTsaLine() in tsaVerify.ts but lives here so the
|
|
1584
|
+
* present-unverified / not-present paths never load pkijs (non-negotiable: ASN.1
|
|
1585
|
+
* libs are only loaded behind --verify-timestamp). The wording — "Externally
|
|
1586
|
+
* anchored timestamp" — reflects that we are proving anchoring to an INDEPENDENT
|
|
1587
|
+
* authority, not running an internal check. Provider/time/imprint are surfaced
|
|
1588
|
+
* separately in the Timestamp Details block (formatTimestampDetails).
|
|
1589
|
+
*/
|
|
1590
|
+
export function formatTimestampLine(status) {
|
|
1591
|
+
const CHECK = '\u2714';
|
|
1592
|
+
const CROSS = '\u2716';
|
|
1593
|
+
const CIRCLE = '\u25CB';
|
|
1594
|
+
switch (status) {
|
|
1595
|
+
case 'valid':
|
|
1596
|
+
return `${CHECK} Externally anchored timestamp: valid`;
|
|
1597
|
+
case 'invalid-signature':
|
|
1598
|
+
return `${CROSS} Externally anchored timestamp: invalid (signature)`;
|
|
1599
|
+
case 'invalid-imprint':
|
|
1600
|
+
return `${CROSS} Externally anchored timestamp: invalid (imprint mismatch)`;
|
|
1601
|
+
case 'untrusted-root':
|
|
1602
|
+
return `${CROSS} Externally anchored timestamp: untrusted (unknown root)`;
|
|
1603
|
+
case 'expired':
|
|
1604
|
+
return `${CROSS} Externally anchored timestamp: invalid (expired)`;
|
|
1605
|
+
case 'invalid-time':
|
|
1606
|
+
return `${CROSS} Externally anchored timestamp: invalid (time)`;
|
|
1607
|
+
case 'malformed':
|
|
1608
|
+
return `${CROSS} Externally anchored timestamp: invalid (malformed token)`;
|
|
1609
|
+
case 'present-unverified':
|
|
1610
|
+
return `${CIRCLE} Externally anchored timestamp: present (not verified)`;
|
|
1611
|
+
case 'not-present':
|
|
1612
|
+
default:
|
|
1613
|
+
return `${CIRCLE} Externally anchored timestamp: not present`;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Build the "Timestamp Details" block lines (Provider / Time / Imprint). Returns an
|
|
1618
|
+
* empty array when no verification was attempted (flag absent → present-unverified,
|
|
1619
|
+
* or no token → not-present), so the block only appears for an actually-verified
|
|
1620
|
+
* token. Pure string logic — no ASN.1 import. The imprint state is INFERRED from the
|
|
1621
|
+
* status: the verifier checks signature → imprint → trust → validity → time in that
|
|
1622
|
+
* order, so any status at or past the trust stage implies the imprint already matched.
|
|
1623
|
+
*/
|
|
1624
|
+
function formatTimestampDetails(result) {
|
|
1625
|
+
if (result.status === 'present-unverified' || result.status === 'not-present')
|
|
1626
|
+
return [];
|
|
1627
|
+
// No genTime ⇒ the token was never parsed (operational error such as an unreadable
|
|
1628
|
+
// --tsa-roots, or a malformed token). There is nothing meaningful to show, and in
|
|
1629
|
+
// particular the imprint was NOT checked — so suppress the block entirely.
|
|
1630
|
+
if (!result.genTime)
|
|
1631
|
+
return [];
|
|
1632
|
+
const time = result.genTime.replace(/\.\d{3}Z$/, 'Z');
|
|
1633
|
+
return [
|
|
1634
|
+
`Provider: ${deriveTsaProvider(result.tsa)}`,
|
|
1635
|
+
`Time: ${time}`,
|
|
1636
|
+
`Imprint: ${imprintState(result.status)}`,
|
|
1637
|
+
];
|
|
1638
|
+
}
|
|
1639
|
+
/** Map a TSA signer common name to a friendly provider label (best-effort). */
|
|
1640
|
+
function deriveTsaProvider(tsa) {
|
|
1641
|
+
if (!tsa)
|
|
1642
|
+
return 'unknown';
|
|
1643
|
+
const n = tsa.toLowerCase();
|
|
1644
|
+
if (n.includes('digicert'))
|
|
1645
|
+
return 'DigiCert';
|
|
1646
|
+
if (n.includes('freetsa') || n.includes('free tsa'))
|
|
1647
|
+
return 'freeTSA';
|
|
1648
|
+
if (n.includes('sectigo'))
|
|
1649
|
+
return 'Sectigo';
|
|
1650
|
+
if (n.includes('globalsign'))
|
|
1651
|
+
return 'GlobalSign';
|
|
1652
|
+
if (n.includes('apple'))
|
|
1653
|
+
return 'Apple';
|
|
1654
|
+
return tsa;
|
|
1655
|
+
}
|
|
1656
|
+
/** Infer imprint state from the verification status (see formatTimestampDetails). */
|
|
1657
|
+
function imprintState(status) {
|
|
1658
|
+
switch (status) {
|
|
1659
|
+
case 'valid':
|
|
1660
|
+
case 'untrusted-root':
|
|
1661
|
+
case 'expired':
|
|
1662
|
+
case 'invalid-time':
|
|
1663
|
+
return 'match';
|
|
1664
|
+
case 'invalid-imprint':
|
|
1665
|
+
return 'mismatch \u2716';
|
|
1666
|
+
default:
|
|
1667
|
+
return 'not checked';
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1400
1670
|
/**
|
|
1401
1671
|
* nexart ai project-verify — Read a cer.project.bundle.v1 JSON from file or stdin,
|
|
1402
1672
|
* delegate all verification to verifyProjectBundle() from @nexart/ai-execution,
|
|
@@ -1876,16 +2146,75 @@ if (_isMainModule) {
|
|
|
1876
2146
|
describe: 'Show intermediate verification steps',
|
|
1877
2147
|
type: 'boolean',
|
|
1878
2148
|
default: false,
|
|
2149
|
+
})
|
|
2150
|
+
.option('verify-timestamp', {
|
|
2151
|
+
describe: 'Additionally verify an embedded RFC 3161 timestamp (offline; never affects the exit code)',
|
|
2152
|
+
type: 'boolean',
|
|
2153
|
+
default: false,
|
|
2154
|
+
})
|
|
2155
|
+
.option('tsa-roots', {
|
|
2156
|
+
describe: 'Path to trusted TSA root certificate(s) (PEM) for --verify-timestamp',
|
|
2157
|
+
type: 'string',
|
|
1879
2158
|
})
|
|
1880
2159
|
.example('$0 verify-bundle cer.json', 'Verify integrity offline')
|
|
1881
2160
|
.example('$0 verify-bundle cer.json --public-key node-keys.json', 'Verify integrity and the node signature')
|
|
1882
|
-
.example('$0 verify-bundle cer.json --json', 'Emit a machine-readable result')
|
|
2161
|
+
.example('$0 verify-bundle cer.json --json', 'Emit a machine-readable result')
|
|
2162
|
+
.example('$0 verify-bundle cer.json --verify-timestamp --tsa-roots tsa-roots.pem', 'Also verify an RFC 3161 timestamp');
|
|
2163
|
+
}, async (argv) => {
|
|
2164
|
+
await verifyBundleStandaloneCommand(argv.file, {
|
|
2165
|
+
publicKey: argv['public-key'],
|
|
2166
|
+
kid: argv.kid,
|
|
2167
|
+
json: argv.json,
|
|
2168
|
+
verbose: argv.verbose,
|
|
2169
|
+
verifyTimestamp: argv['verify-timestamp'],
|
|
2170
|
+
tsaRoots: argv['tsa-roots'],
|
|
2171
|
+
});
|
|
2172
|
+
})
|
|
2173
|
+
.command('verify-record <file>', 'Verify a full CER record (bundle + signature + externally anchored timestamp) offline', (yargs) => {
|
|
2174
|
+
return yargs
|
|
2175
|
+
.positional('file', {
|
|
2176
|
+
describe: 'Path to the CER record JSON file ({ bundle, verification, timestamp? })',
|
|
2177
|
+
type: 'string',
|
|
2178
|
+
demandOption: true,
|
|
2179
|
+
})
|
|
2180
|
+
.option('public-key', {
|
|
2181
|
+
describe: 'Path to the Ed25519 public key (PEM, JWK, NodeKeysDocument, or raw base64url)',
|
|
2182
|
+
type: 'string',
|
|
2183
|
+
})
|
|
2184
|
+
.option('kid', {
|
|
2185
|
+
describe: 'Key id to select when --public-key is a NodeKeysDocument',
|
|
2186
|
+
type: 'string',
|
|
2187
|
+
})
|
|
2188
|
+
.option('json', {
|
|
2189
|
+
describe: 'Output machine-readable JSON',
|
|
2190
|
+
type: 'boolean',
|
|
2191
|
+
default: false,
|
|
2192
|
+
})
|
|
2193
|
+
.option('verbose', {
|
|
2194
|
+
describe: 'Show intermediate verification steps',
|
|
2195
|
+
type: 'boolean',
|
|
2196
|
+
default: false,
|
|
2197
|
+
})
|
|
2198
|
+
.option('verify-timestamp', {
|
|
2199
|
+
describe: 'Additionally verify the externally anchored RFC 3161 timestamp (offline; never affects the exit code)',
|
|
2200
|
+
type: 'boolean',
|
|
2201
|
+
default: false,
|
|
2202
|
+
})
|
|
2203
|
+
.option('tsa-roots', {
|
|
2204
|
+
describe: 'Path to trusted TSA root certificate(s) (PEM) for --verify-timestamp',
|
|
2205
|
+
type: 'string',
|
|
2206
|
+
})
|
|
2207
|
+
.example('$0 verify-record record.json', 'Verify the bundle integrity offline')
|
|
2208
|
+
.example('$0 verify-record record.json --public-key node-keys.json', 'Verify integrity and the receipt signature')
|
|
2209
|
+
.example('$0 verify-record record.json --public-key node-keys.json --verify-timestamp --tsa-roots tsa-roots.pem', 'Full CER + externally anchored timestamp verification');
|
|
1883
2210
|
}, async (argv) => {
|
|
1884
|
-
|
|
2211
|
+
await verifyRecordStandaloneCommand(argv.file, {
|
|
1885
2212
|
publicKey: argv['public-key'],
|
|
1886
2213
|
kid: argv.kid,
|
|
1887
2214
|
json: argv.json,
|
|
1888
2215
|
verbose: argv.verbose,
|
|
2216
|
+
verifyTimestamp: argv['verify-timestamp'],
|
|
2217
|
+
tsaRoots: argv['tsa-roots'],
|
|
1889
2218
|
});
|
|
1890
2219
|
})
|
|
1891
2220
|
.command('export-verification [file]', 'Produce a self-contained verification package so a third party can verify a CER bundle independently', (yargs) => {
|