79 lines
3.6 KiB
TypeScript
79 lines
3.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
|
import { EncryptedLocalNodeEnrollmentStore, type NodeKeyEnrollmentAdapter } from './node-keystore-enrollment.js';
|
|
import { nodePublicKeyFingerprint, parseNodeKeyRecord, type NodeKeyRecord } from './node-keystore-bridge.js';
|
|
|
|
function adapter(overrides: Partial<NodeKeyEnrollmentAdapter> = {}) {
|
|
let record: Readonly<NodeKeyRecord> | null = null;
|
|
let exportedPrivateKey: Buffer | null = null;
|
|
const value: NodeKeyEnrollmentAdapter = {
|
|
isAvailable: () => true,
|
|
readRecord: async () => record,
|
|
async encryptPrivateKeyPkcs8(privateKeyPkcs8) {
|
|
exportedPrivateKey = Buffer.from(privateKeyPkcs8);
|
|
return Buffer.from('sealed-local-node-key').toString('base64');
|
|
},
|
|
async writeRecord(next) { record = next; },
|
|
...overrides,
|
|
};
|
|
return { adapter: value, privateKey: () => exportedPrivateKey, record: () => record };
|
|
}
|
|
|
|
test('creates one encrypted local-terminal identity and returns only public enrollment material', async () => {
|
|
const harness = adapter();
|
|
const store = new EncryptedLocalNodeEnrollmentStore(harness.adapter, () => 'HL-LOCAL-TEST-001');
|
|
const enrollment = await store.ensureLocalTerminal();
|
|
const record = parseNodeKeyRecord(harness.record());
|
|
assert.ok(record);
|
|
assert.equal(enrollment.created, true);
|
|
assert.equal(enrollment.registrationState, 'LOCAL_KEY_READY');
|
|
assert.equal(enrollment.nodeId, 'HL-LOCAL-TEST-001');
|
|
assert.equal(enrollment.nodeKeyFingerprint, nodePublicKeyFingerprint(enrollment.publicKeyPem));
|
|
assert.deepEqual(Object.keys(enrollment).sort(), [
|
|
'created', 'nodeId', 'nodeKeyFingerprint', 'nodeType', 'publicKeyPem', 'registrationState',
|
|
]);
|
|
assert.equal('encryptedPrivateKeyPkcs8' in enrollment, false);
|
|
|
|
const privateKey = createPrivateKey({ key: harness.privateKey()!, format: 'der', type: 'pkcs8' });
|
|
const derivedPublic = createPublicKey(privateKey).export({ format: 'pem', type: 'spki' }).toString();
|
|
assert.equal(derivedPublic, enrollment.publicKeyPem);
|
|
});
|
|
|
|
test('reuses a valid enrolled identity and never rotates it implicitly', async () => {
|
|
const harness = adapter();
|
|
const firstStore = new EncryptedLocalNodeEnrollmentStore(harness.adapter, () => 'HL-LOCAL-TEST-001');
|
|
const first = await firstStore.ensureLocalTerminal();
|
|
const secondStore = new EncryptedLocalNodeEnrollmentStore(harness.adapter, () => 'HL-LOCAL-TEST-002');
|
|
const second = await secondStore.ensureLocalTerminal();
|
|
assert.equal(second.created, false);
|
|
assert.equal(second.nodeId, first.nodeId);
|
|
assert.equal(second.nodeKeyFingerprint, first.nodeKeyFingerprint);
|
|
});
|
|
|
|
test('fails closed when encryption is unavailable or an existing record is malformed', async () => {
|
|
const unavailable = adapter({ isAvailable: () => false });
|
|
await assert.rejects(
|
|
new EncryptedLocalNodeEnrollmentStore(unavailable.adapter).ensureLocalTerminal(),
|
|
/node_keystore_encryption_unavailable/,
|
|
);
|
|
|
|
const malformed = adapter({ readRecord: async () => ({ schema: 'wrong' }) });
|
|
await assert.rejects(
|
|
new EncryptedLocalNodeEnrollmentStore(malformed.adapter).ensureLocalTerminal(),
|
|
/node_keystore_record_invalid/,
|
|
);
|
|
});
|
|
|
|
test('does not write a record when encryption output is invalid', async () => {
|
|
let writes = 0;
|
|
const harness = adapter({
|
|
encryptPrivateKeyPkcs8: async () => '***',
|
|
writeRecord: async () => { writes += 1; },
|
|
});
|
|
await assert.rejects(
|
|
new EncryptedLocalNodeEnrollmentStore(harness.adapter).ensureLocalTerminal(),
|
|
/node_keystore_enrollment_failed/,
|
|
);
|
|
assert.equal(writes, 0);
|
|
});
|