@zincapp/znvault-cli 4.22.1 → 4.24.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/dist/commands/lmk-ceremony.d.ts +3 -0
- package/dist/commands/lmk-ceremony.d.ts.map +1 -0
- package/dist/commands/lmk-ceremony.js +597 -0
- package/dist/commands/lmk-ceremony.js.map +1 -0
- package/dist/commands/lmk-escrow.d.ts.map +1 -1
- package/dist/commands/lmk-escrow.js +73 -5
- package/dist/commands/lmk-escrow.js.map +1 -1
- package/dist/commands/lmk-preflight.d.ts +3 -0
- package/dist/commands/lmk-preflight.d.ts.map +1 -0
- package/dist/commands/lmk-preflight.js +117 -0
- package/dist/commands/lmk-preflight.js.map +1 -0
- package/dist/commands/lmk-restore-drill.d.ts +3 -0
- package/dist/commands/lmk-restore-drill.d.ts.map +1 -0
- package/dist/commands/lmk-restore-drill.js +160 -0
- package/dist/commands/lmk-restore-drill.js.map +1 -0
- package/dist/commands/lmk.d.ts.map +1 -1
- package/dist/commands/lmk.js +4 -0
- package/dist/commands/lmk.js.map +1 -1
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/bsk-source.d.ts +21 -0
- package/dist/lib/bsk-source.d.ts.map +1 -0
- package/dist/lib/bsk-source.js +89 -0
- package/dist/lib/bsk-source.js.map +1 -0
- package/dist/lib/ceremony/gates.d.ts +126 -0
- package/dist/lib/ceremony/gates.d.ts.map +1 -0
- package/dist/lib/ceremony/gates.js +270 -0
- package/dist/lib/ceremony/gates.js.map +1 -0
- package/dist/lib/ceremony/system.d.ts +36 -0
- package/dist/lib/ceremony/system.d.ts.map +1 -0
- package/dist/lib/ceremony/system.js +176 -0
- package/dist/lib/ceremony/system.js.map +1 -0
- package/dist/lib/ceremony/workspace.d.ts +54 -0
- package/dist/lib/ceremony/workspace.d.ts.map +1 -0
- package/dist/lib/ceremony/workspace.js +98 -0
- package/dist/lib/ceremony/workspace.js.map +1 -0
- package/dist/lib/config/index.d.ts +1 -1
- package/dist/lib/config/index.d.ts.map +1 -1
- package/dist/lib/config/index.js +1 -1
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/config/profile.d.ts +14 -0
- package/dist/lib/config/profile.d.ts.map +1 -1
- package/dist/lib/config/profile.js +22 -0
- package/dist/lib/config/profile.js.map +1 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/db/index.d.ts +9 -1
- package/dist/lib/db/index.d.ts.map +1 -1
- package/dist/lib/db/index.js +9 -0
- package/dist/lib/db/index.js.map +1 -1
- package/dist/lib/db/key-lifecycle.d.ts +86 -0
- package/dist/lib/db/key-lifecycle.d.ts.map +1 -0
- package/dist/lib/db/key-lifecycle.js +182 -0
- package/dist/lib/db/key-lifecycle.js.map +1 -0
- package/dist/lib/db/lmk-escrow.d.ts +18 -0
- package/dist/lib/db/lmk-escrow.d.ts.map +1 -1
- package/dist/lib/db/lmk-escrow.js +25 -0
- package/dist/lib/db/lmk-escrow.js.map +1 -1
- package/dist/lib/db/preflight.d.ts +42 -0
- package/dist/lib/db/preflight.d.ts.map +1 -0
- package/dist/lib/db/preflight.js +165 -0
- package/dist/lib/db/preflight.js.map +1 -0
- package/dist/lib/kcv.d.ts +34 -0
- package/dist/lib/kcv.d.ts.map +1 -0
- package/dist/lib/kcv.js +89 -0
- package/dist/lib/kcv.js.map +1 -0
- package/dist/lib/lmk-escrow-restore.d.ts +6 -2
- package/dist/lib/lmk-escrow-restore.d.ts.map +1 -1
- package/dist/lib/lmk-escrow-restore.js +1 -1
- package/dist/lib/lmk-escrow-restore.js.map +1 -1
- package/dist/lib/lmk-escrow.d.ts +14 -1
- package/dist/lib/lmk-escrow.d.ts.map +1 -1
- package/dist/lib/lmk-escrow.js +5 -1
- package/dist/lib/lmk-escrow.js.map +1 -1
- package/dist/lib/preflight.d.ts +93 -0
- package/dist/lib/preflight.d.ts.map +1 -0
- package/dist/lib/preflight.js +129 -0
- package/dist/lib/preflight.js.map +1 -0
- package/dist/lib/restore-drill.d.ts +73 -0
- package/dist/lib/restore-drill.d.ts.map +1 -0
- package/dist/lib/restore-drill.js +185 -0
- package/dist/lib/restore-drill.js.map +1 -0
- package/dist/lib/sentinel-client.d.ts +26 -0
- package/dist/lib/sentinel-client.d.ts.map +1 -0
- package/dist/lib/sentinel-client.js +165 -0
- package/dist/lib/sentinel-client.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Path: src/lib/restore-drill.ts
|
|
2
|
+
//
|
|
3
|
+
// The two gates of the isolated-restore drill.
|
|
4
|
+
//
|
|
5
|
+
// THE DRILL'S FAILURE MODE IS SUCCESS. If the restored database ends up with no
|
|
6
|
+
// LMK version above zero, `initializeFromPG()` reads "no ACTIVE version" and
|
|
7
|
+
// takes the legitimate first-boot path: `bootstrapInitialLMKVersion()` mints a
|
|
8
|
+
// brand-new LMK, wraps it under the bootstrap key on disk, inserts version 1,
|
|
9
|
+
// and the vault comes up healthy. `/v1/health` answers 200 with `status: 'ok'`
|
|
10
|
+
// and a root key whose KCV even MATCHES the escrow bundle — the BSK really was
|
|
11
|
+
// restored from it. Every signal an operator would look at says the drill
|
|
12
|
+
// passed. What actually happened is that a new key hierarchy was created over
|
|
13
|
+
// an empty database, and the only thing the drill exists to demonstrate — that
|
|
14
|
+
// this bundle brings back THIS deployment's keys — was never exercised.
|
|
15
|
+
//
|
|
16
|
+
// So neither gate may read an exit code or an HTTP status. Degraded root-key
|
|
17
|
+
// resolution returns 200 with the degradation reported inside the body, and a
|
|
18
|
+
// null resolution state omits the `rootKey` block entirely while still
|
|
19
|
+
// returning 200 and `status: 'ok'`. Both gates read state and compare it
|
|
20
|
+
// against what the bundle says, and both explain themselves: a failed drill is
|
|
21
|
+
// investigated cold, hours later, by someone who was not there.
|
|
22
|
+
//
|
|
23
|
+
// Pure functions on purpose. The orchestration around them drives docker from a
|
|
24
|
+
// shell script and cannot be unit-tested, so everything that DECIDES anything
|
|
25
|
+
// lives here where it can be.
|
|
26
|
+
/**
|
|
27
|
+
* Versions at or below zero are not real key generations.
|
|
28
|
+
*
|
|
29
|
+
* `models.postgres.sql` seeds `version=0, status='ACTIVE',
|
|
30
|
+
* key_id='ZK_MODE_PLACEHOLDER'` on every boot. Counting it would make a
|
|
31
|
+
* correctly restored database look like it has two ACTIVE versions.
|
|
32
|
+
*/
|
|
33
|
+
function positives(versions) {
|
|
34
|
+
return versions.filter((v) => v.version > 0);
|
|
35
|
+
}
|
|
36
|
+
function describe(versions) {
|
|
37
|
+
const rows = positives(versions);
|
|
38
|
+
if (rows.length === 0)
|
|
39
|
+
return '(no versions above zero)';
|
|
40
|
+
return rows
|
|
41
|
+
.map((v) => `v${String(v.version)} ${v.status}${v.hasWrappedLmk ? '' : ' [no material]'}`)
|
|
42
|
+
.join(', ');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Gate 1, run BEFORE the vault is started against the restored database.
|
|
46
|
+
*
|
|
47
|
+
* Before, not after, because once the vault mints a replacement LMK the
|
|
48
|
+
* evidence of what went wrong is overwritten: the table then holds a perfectly
|
|
49
|
+
* ordinary version 1 with material, and nothing distinguishes it from a
|
|
50
|
+
* successful restore except that it is the wrong key.
|
|
51
|
+
*
|
|
52
|
+
* @throws with a diagnosis whenever the database could not have come from the
|
|
53
|
+
* deployment the bundle describes.
|
|
54
|
+
*/
|
|
55
|
+
export function assertRestoredDatabaseIsRecoverable(versions, expectedActiveVersion) {
|
|
56
|
+
const rows = positives(versions);
|
|
57
|
+
if (rows.length === 0) {
|
|
58
|
+
throw new Error('The restored database holds no LMK version above zero. Starting a vault ' +
|
|
59
|
+
'against it would NOT fail: it would take the first-boot path, MINT a new ' +
|
|
60
|
+
'LMK, and report a completely healthy start — the drill would pass having ' +
|
|
61
|
+
'restored nothing. Check that the database dump was actually applied and ' +
|
|
62
|
+
'that it came from the same snapshot as the escrow bundle.');
|
|
63
|
+
}
|
|
64
|
+
const recoverable = rows.filter((v) => v.hasWrappedLmk);
|
|
65
|
+
if (recoverable.length === 0) {
|
|
66
|
+
throw new Error(`The restored database holds ${String(rows.length)} LMK version(s) above zero ` +
|
|
67
|
+
`but none has wrapped material (${describe(versions)}). The inventory came ` +
|
|
68
|
+
'back and the key material did not, which reaches the same mint-a-new-key ' +
|
|
69
|
+
'path as an empty table.');
|
|
70
|
+
}
|
|
71
|
+
const actives = rows.filter((v) => v.status === 'ACTIVE');
|
|
72
|
+
if (actives.length !== 1) {
|
|
73
|
+
throw new Error(`The restored database has ${String(actives.length)} ACTIVE LMK version(s) ` +
|
|
74
|
+
`above zero (${describe(versions)}); exactly one is required. A drill run ` +
|
|
75
|
+
'against an ambiguous key state proves nothing about either version.');
|
|
76
|
+
}
|
|
77
|
+
const active = actives.at(0);
|
|
78
|
+
if (active === undefined)
|
|
79
|
+
throw new Error('unreachable: one ACTIVE row expected');
|
|
80
|
+
if (!active.hasWrappedLmk) {
|
|
81
|
+
throw new Error(`The ACTIVE LMK version ${String(active.version)} has no wrapped material. ` +
|
|
82
|
+
'The vault cannot unwrap the current key from this database.');
|
|
83
|
+
}
|
|
84
|
+
if (active.version !== expectedActiveVersion) {
|
|
85
|
+
throw new Error(`The restored database is ACTIVE on LMK version ${String(active.version)}, but ` +
|
|
86
|
+
`the escrow bundle was taken when version ${String(expectedActiveVersion)} was ` +
|
|
87
|
+
'active. The bundle and the dump are from different moments, so a successful ' +
|
|
88
|
+
'boot would say nothing about either of them.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Gate 2, run AFTER the vault has started.
|
|
93
|
+
*
|
|
94
|
+
* Confirms three separate things, because each can be true while the others are
|
|
95
|
+
* not: the node resolved the key the bundle carries, it did so cleanly, and it
|
|
96
|
+
* did NOT create anything new to do it.
|
|
97
|
+
*
|
|
98
|
+
* WHAT THIS GATE CANNOT DO, measured on a bench rather than assumed. A vault
|
|
99
|
+
* started against an empty database mints LMK version 1 and reports
|
|
100
|
+
* `status: ok` with a root-key KCV that MATCHES the bundle — the bootstrap key
|
|
101
|
+
* really did come from it. If the bundle's own active version happens to be 1,
|
|
102
|
+
* every field checked below is correct and the mint passes. Production is on
|
|
103
|
+
* version 4 today, so this gate would catch it there; that is luck, not design.
|
|
104
|
+
* `assertRestoredDatabaseIsRecoverable` is what actually closes it, which is
|
|
105
|
+
* why the pre-boot gate is never "the one the post gate makes redundant".
|
|
106
|
+
*
|
|
107
|
+
* @throws with both values named whenever a comparison fails.
|
|
108
|
+
*/
|
|
109
|
+
export function assertBootedOnRestoredKeys(input) {
|
|
110
|
+
const { health, versionsAfterBoot, expectedBskKcv, expectedActiveVersion } = input;
|
|
111
|
+
// A null resolution state omits the block and still answers 200 with
|
|
112
|
+
// status 'ok'. Reading `health.rootKey?.kcv` into a comparison would compare
|
|
113
|
+
// `undefined` and quietly skip the only check that matters.
|
|
114
|
+
const rootKey = health.rootKey;
|
|
115
|
+
if (rootKey === undefined || typeof rootKey.kcv !== 'string' || rootKey.kcv === '') {
|
|
116
|
+
throw new Error('The health body carries no rootKey block, so there is nothing to compare ' +
|
|
117
|
+
'against the escrow bundle. This is what a null root-key resolution state ' +
|
|
118
|
+
'looks like from outside — HTTP 200, status "ok", and no answer to the only ' +
|
|
119
|
+
'question the drill asked.');
|
|
120
|
+
}
|
|
121
|
+
if (rootKey.degraded === true) {
|
|
122
|
+
throw new Error(`The node resolved a root key (${rootKey.kcv}) but reports DEGRADED: at least ` +
|
|
123
|
+
'one configured provider failed. On a drill whose whole point is "the ' +
|
|
124
|
+
'escrowed key opens this database", a partial answer is not an answer.');
|
|
125
|
+
}
|
|
126
|
+
if (rootKey.kcv !== expectedBskKcv) {
|
|
127
|
+
throw new Error('The node booted on a DIFFERENT bootstrap key than the escrow bundle ' +
|
|
128
|
+
`carries. Bundle: ${expectedBskKcv}. Running node: ${rootKey.kcv}. ` +
|
|
129
|
+
'Either the key file was not restored from this bundle, or another ' +
|
|
130
|
+
'root-key provider answered first.');
|
|
131
|
+
}
|
|
132
|
+
const actives = positives(versionsAfterBoot).filter((v) => v.status === 'ACTIVE');
|
|
133
|
+
const active = actives.at(0);
|
|
134
|
+
if (actives.length !== 1 || active === undefined) {
|
|
135
|
+
throw new Error(`After boot the database has ${String(actives.length)} ACTIVE LMK version(s) ` +
|
|
136
|
+
`above zero (${describe(versionsAfterBoot)}); exactly one is required.`);
|
|
137
|
+
}
|
|
138
|
+
if (active.version !== expectedActiveVersion) {
|
|
139
|
+
throw new Error(`After boot the vault is ACTIVE on LMK version ${String(active.version)}, not ` +
|
|
140
|
+
`the version ${String(expectedActiveVersion)} the escrow bundle was taken at. ` +
|
|
141
|
+
'The most likely cause is that the vault MINTED a new LMK because it found ' +
|
|
142
|
+
'nothing to load — which produces a healthy-looking node running on a key ' +
|
|
143
|
+
'that was created seconds ago and exists nowhere else. ' +
|
|
144
|
+
`Versions now: ${describe(versionsAfterBoot)}.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Refuse to run the drill against anything that is not on this machine.
|
|
149
|
+
*
|
|
150
|
+
* The drill reads a database and then asserts things about a vault; both
|
|
151
|
+
* halves are harmless. What is not harmless is running it with a production
|
|
152
|
+
* URL by accident — the pre-boot gate would report a green production, the
|
|
153
|
+
* post-boot gate would compare production's KCV against an escrow bundle, and
|
|
154
|
+
* the whole exercise would be recorded as a successful restore drill without a
|
|
155
|
+
* single byte having been restored. That is the same false green the gates
|
|
156
|
+
* exist to remove, arriving through the front door.
|
|
157
|
+
*
|
|
158
|
+
* Hostname only. Anything that resolves elsewhere at runtime (a name pointing
|
|
159
|
+
* at a remote address, an SSH tunnel) is out of scope for a lab guard, and
|
|
160
|
+
* pretending otherwise would be worse than saying so.
|
|
161
|
+
*
|
|
162
|
+
* @throws when the target host is not loopback.
|
|
163
|
+
*/
|
|
164
|
+
export function assertIsolatedTarget(rawUrl, what) {
|
|
165
|
+
let host;
|
|
166
|
+
try {
|
|
167
|
+
host = new URL(rawUrl).hostname;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new Error(`The ${what} is not a valid URL: ${rawUrl}`);
|
|
171
|
+
}
|
|
172
|
+
// `new URL().hostname` KEEPS the brackets on an IPv6 literal — `[::1]`, not
|
|
173
|
+
// `::1`. Comparing against the bare form silently rejects a legitimate
|
|
174
|
+
// loopback bench, which is the kind of guard people disable rather than fix.
|
|
175
|
+
const bare = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
176
|
+
const loopback = bare === 'localhost' || bare === '::1' || bare.startsWith('127.');
|
|
177
|
+
if (!loopback) {
|
|
178
|
+
throw new Error(`Refusing to run the restore drill against ${what} host "${host}". The drill ` +
|
|
179
|
+
'is only meaningful on an isolated bench: pointed at a live deployment it ' +
|
|
180
|
+
'would compare that deployment against the escrow bundle and record a ' +
|
|
181
|
+
'successful "restore" in which nothing was restored. Use a loopback ' +
|
|
182
|
+
'address on the bench host.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=restore-drill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-drill.js","sourceRoot":"","sources":["../../src/lib/restore-drill.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SentinelClientConfig {
|
|
2
|
+
/** Base URL of the appliance. https:// only. */
|
|
3
|
+
url: string;
|
|
4
|
+
/** CA bundle that must issue the appliance's certificate. */
|
|
5
|
+
caPath: string;
|
|
6
|
+
/** Client certificate presented to the appliance. */
|
|
7
|
+
certPath: string;
|
|
8
|
+
/** Its private key. */
|
|
9
|
+
keyPath: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SentinelClient {
|
|
13
|
+
/** Open an envelope. Returns a fresh 32-byte buffer the caller must wipe. */
|
|
14
|
+
unwrap(envelope: Buffer): Promise<Buffer>;
|
|
15
|
+
/** Liveness plus a KCV claim. No plaintext crosses the wire. */
|
|
16
|
+
probe(envelope: Buffer): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the client. Reads the certificate material eagerly so a missing or
|
|
20
|
+
* unreadable file fails now — at the top of a ceremony, with everything still
|
|
21
|
+
* locked — rather than at the moment the key is expected to arrive.
|
|
22
|
+
*
|
|
23
|
+
* @throws on a non-https URL or unreadable certificate material.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSentinelClient(config: SentinelClientConfig): SentinelClient;
|
|
26
|
+
//# sourceMappingURL=sentinel-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel-client.d.ts","sourceRoot":"","sources":["../../src/lib/sentinel-client.ts"],"names":[],"mappings":"AAuDA,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,gEAAgE;IAChE,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAuHjF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Path: src/lib/sentinel-client.ts
|
|
2
|
+
//
|
|
3
|
+
// Minimal mTLS JSON client for the Archon Sentinel appliance, so an escrow
|
|
4
|
+
// ceremony can source the bootstrap key from the hardware root instead of from
|
|
5
|
+
// a cleartext file on a production node.
|
|
6
|
+
//
|
|
7
|
+
// PORT NOTICE. This is a SECOND implementation of a protocol the server already
|
|
8
|
+
// speaks in zn-vault/src/vault-crypto/root-key/sentinel-adapter.ts. This package
|
|
9
|
+
// ships to npm on its own and cannot import from the server, so the protocol
|
|
10
|
+
// necessarily exists twice — normally the way a contract dies quietly, with both
|
|
11
|
+
// suites green and the two sides agreeing on nothing.
|
|
12
|
+
//
|
|
13
|
+
// The defence is the one used for `kcv1:`: neither side is checked against the
|
|
14
|
+
// other. Both are pinned to frozen literals — paths, field names, encoding —
|
|
15
|
+
// in test/lib/sentinel-client.test.ts here and in the adapter's own suite there.
|
|
16
|
+
// Rename a field on either side and both go red.
|
|
17
|
+
//
|
|
18
|
+
// The wire, copied verbatim from the server adapter's header:
|
|
19
|
+
//
|
|
20
|
+
// POST /v1/root/unwrap {"envelope":"<b64>"} -> 200 {"plaintext":"<b64>"}
|
|
21
|
+
// POST /v1/root/probe {"envelope":"<b64>"} -> 200 {"kcv":"kcv1:..."}
|
|
22
|
+
//
|
|
23
|
+
// DELIBERATELY NOT IMPLEMENTED: `wrap`. Its request body carries the bootstrap
|
|
24
|
+
// key, and provisioning a new root is a server-side operation with its own
|
|
25
|
+
// audit trail (`znvault superadmin rootkey wrap`). A ceremony host has no
|
|
26
|
+
// business minting envelopes, so the capability is simply absent rather than
|
|
27
|
+
// present-and-guarded.
|
|
28
|
+
import { request } from 'node:https';
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
/** Frozen wire paths. Pinned as literals in the test suite. */
|
|
31
|
+
const OP_PATHS = {
|
|
32
|
+
unwrap: '/v1/root/unwrap',
|
|
33
|
+
probe: '/v1/root/probe',
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The purpose this client asks for, sent EXPLICITLY.
|
|
37
|
+
*
|
|
38
|
+
* Protocol v1.1 binds a purpose into the envelope's cryptography so a KMIP KEK
|
|
39
|
+
* envelope can never be opened as if it were the vault's bootstrap key. The
|
|
40
|
+
* field is optional and defaults to this same value — but the daemon's own
|
|
41
|
+
* comment calls that default a "staged rollout" affordance, there so the
|
|
42
|
+
* pre-purpose vault keeps booting during the migration.
|
|
43
|
+
*
|
|
44
|
+
* New code written today has no business depending on a compatibility default
|
|
45
|
+
* that exists to be removed. Sending it also makes the ask readable on the
|
|
46
|
+
* wire, which matters when the wire is being watched during a key ceremony.
|
|
47
|
+
*/
|
|
48
|
+
const BSK_PURPOSE = 'zn-vault-bsk-v1';
|
|
49
|
+
const BSK_LEN = 32;
|
|
50
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
51
|
+
/**
|
|
52
|
+
* Build the client. Reads the certificate material eagerly so a missing or
|
|
53
|
+
* unreadable file fails now — at the top of a ceremony, with everything still
|
|
54
|
+
* locked — rather than at the moment the key is expected to arrive.
|
|
55
|
+
*
|
|
56
|
+
* @throws on a non-https URL or unreadable certificate material.
|
|
57
|
+
*/
|
|
58
|
+
export function createSentinelClient(config) {
|
|
59
|
+
const target = new URL(config.url);
|
|
60
|
+
if (target.protocol !== 'https:') {
|
|
61
|
+
// The unwrap response carries the bootstrap key. There is no version of
|
|
62
|
+
// this that may travel in clear, and a typo in a runbook must not be what
|
|
63
|
+
// decides it.
|
|
64
|
+
throw new Error(`The Sentinel URL must be https://, got ${JSON.stringify(config.url)}. ` +
|
|
65
|
+
'The unwrap response carries the bootstrap key.');
|
|
66
|
+
}
|
|
67
|
+
const tls = {
|
|
68
|
+
ca: readFileSync(config.caPath),
|
|
69
|
+
cert: readFileSync(config.certPath),
|
|
70
|
+
key: readFileSync(config.keyPath),
|
|
71
|
+
};
|
|
72
|
+
const basePath = target.pathname.replace(/\/+$/, '');
|
|
73
|
+
const timeoutMs = config.timeoutMs ?? 5000;
|
|
74
|
+
async function call(op, payload) {
|
|
75
|
+
const body = Buffer.from(JSON.stringify(payload), 'utf-8');
|
|
76
|
+
return await new Promise((resolve, reject) => {
|
|
77
|
+
const req = request({
|
|
78
|
+
method: 'POST',
|
|
79
|
+
protocol: target.protocol,
|
|
80
|
+
hostname: target.hostname,
|
|
81
|
+
port: target.port,
|
|
82
|
+
path: `${basePath}${OP_PATHS[op]}`,
|
|
83
|
+
headers: { 'content-type': 'application/json', 'content-length': body.length },
|
|
84
|
+
ca: tls.ca,
|
|
85
|
+
cert: tls.cert,
|
|
86
|
+
key: tls.key,
|
|
87
|
+
// Left at its default (true) on purpose. The CA above is the pin:
|
|
88
|
+
// without it Node would validate against the public root store and
|
|
89
|
+
// accept a certificate this deployment never issued — the wrong
|
|
90
|
+
// check, not a weaker one.
|
|
91
|
+
timeout: timeoutMs,
|
|
92
|
+
}, (res) => {
|
|
93
|
+
const chunks = [];
|
|
94
|
+
let total = 0;
|
|
95
|
+
res.on('data', (chunk) => {
|
|
96
|
+
total += chunk.length;
|
|
97
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
98
|
+
req.destroy(new Error(`Sentinel ${op} response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
chunks.push(chunk);
|
|
102
|
+
});
|
|
103
|
+
res.on('end', () => {
|
|
104
|
+
const status = res.statusCode ?? 0;
|
|
105
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
106
|
+
if (status < 200 || status >= 300) {
|
|
107
|
+
reject(new Error(`Sentinel ${op} failed: HTTP ${String(status)}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
reject(new Error(`Sentinel ${op} returned HTTP ${String(status)} with a malformed JSON body`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
119
|
+
reject(new Error(`Sentinel ${op} returned a JSON value that is not an object`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
resolve(parsed);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
req.on('timeout', () => {
|
|
126
|
+
req.destroy(new Error(`Sentinel ${op} timed out after ${String(timeoutMs)}ms`));
|
|
127
|
+
});
|
|
128
|
+
req.on('error', reject);
|
|
129
|
+
req.end(body);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
async unwrap(envelope) {
|
|
134
|
+
const response = await call('unwrap', {
|
|
135
|
+
envelope: envelope.toString('base64'),
|
|
136
|
+
purpose: BSK_PURPOSE,
|
|
137
|
+
});
|
|
138
|
+
const plaintext = response.plaintext;
|
|
139
|
+
if (typeof plaintext !== 'string') {
|
|
140
|
+
throw new Error('Sentinel unwrap returned 200 without a string "plaintext" field. ' +
|
|
141
|
+
'Something answered that is not the appliance protocol — a proxy, a ' +
|
|
142
|
+
'captive portal, or a mismatched appliance version.');
|
|
143
|
+
}
|
|
144
|
+
const key = Buffer.from(plaintext, 'base64');
|
|
145
|
+
if (key.length !== BSK_LEN) {
|
|
146
|
+
key.fill(0);
|
|
147
|
+
throw new Error(`Sentinel unwrap returned ${String(key.length)} bytes; a bootstrap key is ` +
|
|
148
|
+
`exactly ${String(BSK_LEN)} bytes.`);
|
|
149
|
+
}
|
|
150
|
+
return key;
|
|
151
|
+
},
|
|
152
|
+
async probe(envelope) {
|
|
153
|
+
const response = await call('probe', {
|
|
154
|
+
envelope: envelope.toString('base64'),
|
|
155
|
+
purpose: BSK_PURPOSE,
|
|
156
|
+
});
|
|
157
|
+
const kcv = response.kcv;
|
|
158
|
+
if (typeof kcv !== 'string') {
|
|
159
|
+
throw new Error('Sentinel probe returned 200 without a string "kcv" field.');
|
|
160
|
+
}
|
|
161
|
+
return kcv;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=sentinel-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel-client.js","sourceRoot":"","sources":["../../src/lib/sentinel-client.ts"],"names":[],"mappings":"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"}
|