131 lines
5.9 KiB
TypeScript
131 lines
5.9 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
import { EncryptedNodeKeyStoreProofSource, type NodeKeyStoreAdapter } from './node-keystore-bridge.js';
|
|
import { createNodePossessionChallenge, verifyNodePossessionResponse } from './node-possession-proof.js';
|
|
import {
|
|
nodeRegistrationClaimSigningBytes,
|
|
parseNodeRegistrationEndpointRegistry,
|
|
resolveNodeRegistrationEndpoint,
|
|
verifyNodeRegistrationClaim,
|
|
} from './node-registration.js';
|
|
|
|
const NOW = 1_786_291_200_000;
|
|
const DOMAIN_ID = 'DOM-FIFTH-0001';
|
|
const NODE_ID = 'LOCAL-001';
|
|
const registrationKeys = generateKeyPairSync('ed25519');
|
|
const nodeKeys = generateKeyPairSync('ed25519');
|
|
const nodePublicKeyPem = nodeKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString();
|
|
const nodePrivateKeyPkcs8 = nodeKeys.privateKey.export({ format: 'der', type: 'pkcs8' });
|
|
const fingerprint = createHash('sha256')
|
|
.update(nodeKeys.publicKey.export({ format: 'der', type: 'spki' }))
|
|
.digest('hex');
|
|
|
|
function verifiedRegistration() {
|
|
const registry = parseNodeRegistrationEndpointRegistry({
|
|
endpoints: [{
|
|
algorithm: 'Ed25519',
|
|
domainIds: [DOMAIN_ID],
|
|
endpointId: 'GH-NODE-REG-001',
|
|
nodeTypes: ['local-terminal'],
|
|
publicKeyPem: registrationKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString(),
|
|
signerId: 'GH-NODE-REG-SIGNER-001',
|
|
status: 'ACTIVE',
|
|
url: 'https://guanghulab.com/api/ai/v1/node-registrations/claims',
|
|
}],
|
|
registryId: 'GH-AIOS-NODE-REGISTRATION-ENDPOINTS-001',
|
|
schema: 'gh-aios.node-registration-endpoints/v1',
|
|
state: 'CURRENT',
|
|
version: '1.0.0',
|
|
}, {
|
|
repositoryId: 'REPO-012',
|
|
sourceCommit: 'a'.repeat(40),
|
|
sourceUrl: `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${'a'.repeat(40)}/routing/node-registration-endpoints.json`,
|
|
});
|
|
const endpoint = resolveNodeRegistrationEndpoint(registry, { domainId: DOMAIN_ID, nodeType: 'local-terminal' });
|
|
assert.ok(endpoint);
|
|
const payload = {
|
|
accountId: 'bingshuo',
|
|
claimId: 'NODE-CLAIM-001',
|
|
domainId: DOMAIN_ID,
|
|
endpointId: endpoint.endpointId,
|
|
expiresAt: NOW + 120_000,
|
|
issuedAt: NOW - 1_000,
|
|
nodeId: NODE_ID,
|
|
nodeKeyFingerprint: fingerprint,
|
|
nodeType: 'local-terminal' as const,
|
|
schema: 'gh-aios.node-registration-claim/v1' as const,
|
|
signerId: endpoint.signerId,
|
|
};
|
|
return verifyNodeRegistrationClaim({
|
|
...payload,
|
|
signature: sign(null, nodeRegistrationClaimSigningBytes(payload), registrationKeys.privateKey).toString('base64'),
|
|
}, {
|
|
accountId: payload.accountId,
|
|
domainId: DOMAIN_ID,
|
|
nodeId: NODE_ID,
|
|
nodeType: 'local-terminal',
|
|
}, endpoint, NOW);
|
|
}
|
|
|
|
function keyRecord(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
encryptedPrivateKeyPkcs8: Buffer.from('encrypted-test-record').toString('base64'),
|
|
nodeId: NODE_ID,
|
|
nodeKeyFingerprint: fingerprint,
|
|
nodeType: 'local-terminal',
|
|
publicKeyPem: nodePublicKeyPem,
|
|
schema: 'gh-aios.node-keystore-record/v1',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function adapter(overrides: Partial<NodeKeyStoreAdapter> = {}): NodeKeyStoreAdapter {
|
|
return {
|
|
async decryptPrivateKeyPkcs8() { return Buffer.from(nodePrivateKeyPkcs8); },
|
|
isAvailable() { return true; },
|
|
async readRecord() { return keyRecord(); },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function challenge(id = 'NODE-CHALLENGE-KEYSTORE-001') {
|
|
return createNodePossessionChallenge(verifiedRegistration(), NOW, id, 'A'.repeat(43));
|
|
}
|
|
|
|
test('signs only an active challenge with the enrolled matching Ed25519 key', async () => {
|
|
const issued = challenge();
|
|
const source = new EncryptedNodeKeyStoreProofSource(adapter(), () => NOW + 1);
|
|
const response = await source.prove(issued);
|
|
assert.ok(response);
|
|
assert.deepEqual(Object.keys(response).sort(), ['challengeId', 'publicKeyPem', 'schema', 'signature']);
|
|
assert.ok(verifyNodePossessionResponse(response, issued, verifiedRegistration(), NOW + 2));
|
|
});
|
|
|
|
test('returns no proof when storage is unavailable or no enrollment exists', async () => {
|
|
const issued = challenge('NODE-CHALLENGE-KEYSTORE-002');
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ isAvailable: () => false }), () => NOW + 1).prove(issued), null);
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => null }), () => NOW + 1).prove(issued), null);
|
|
});
|
|
|
|
test('rejects copied challenges, unknown record fields and node binding mismatches', async () => {
|
|
const issued = challenge('NODE-CHALLENGE-KEYSTORE-003');
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter(), () => NOW + 1).prove({ ...issued }), null);
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => keyRecord({ extra: true }) }), () => NOW + 1).prove(issued), null);
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => keyRecord({ nodeId: 'OTHER-NODE' }) }), () => NOW + 1).prove(issued), null);
|
|
});
|
|
|
|
test('rejects malformed encryption payloads and mismatched private keys', async () => {
|
|
const issued = challenge('NODE-CHALLENGE-KEYSTORE-004');
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => keyRecord({ encryptedPrivateKeyPkcs8: '***' }) }), () => NOW + 1).prove(issued), null);
|
|
const other = generateKeyPairSync('ed25519').privateKey.export({ format: 'der', type: 'pkcs8' });
|
|
assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ decryptPrivateKeyPkcs8: async () => Buffer.from(other) }), () => NOW + 1).prove(issued), null);
|
|
});
|
|
|
|
test('wipes the decrypted temporary key bytes after signing', async () => {
|
|
const issued = challenge('NODE-CHALLENGE-KEYSTORE-005');
|
|
const decrypted = Buffer.from(nodePrivateKeyPkcs8);
|
|
const source = new EncryptedNodeKeyStoreProofSource(adapter({ decryptPrivateKeyPkcs8: async () => decrypted }), () => NOW + 1);
|
|
assert.ok(await source.prove(issued));
|
|
assert.ok(decrypted.every(byte => byte === 0));
|
|
});
|