132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
||
|
|
import {
|
||
|
|
createNodePossessionChallenge,
|
||
|
|
nodePossessionChallengeSigningBytes,
|
||
|
|
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 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 responseFor(challenge: ReturnType<typeof createNodePossessionChallenge>) {
|
||
|
|
return {
|
||
|
|
challengeId: challenge.challengeId,
|
||
|
|
publicKeyPem: nodePublicKeyPem,
|
||
|
|
schema: 'gh-aios.node-possession-response/v1',
|
||
|
|
signature: sign(null, nodePossessionChallengeSigningBytes(challenge), nodeKeys.privateKey).toString('base64'),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
test('verifies a fresh proof from the key fingerprint bound into the signed registration', () => {
|
||
|
|
const registration = verifiedRegistration();
|
||
|
|
const challenge = createNodePossessionChallenge(
|
||
|
|
registration,
|
||
|
|
NOW,
|
||
|
|
'NODE-CHALLENGE-001',
|
||
|
|
'A'.repeat(43),
|
||
|
|
);
|
||
|
|
const response = responseFor(challenge);
|
||
|
|
assert.equal(verifyNodePossessionResponse(response, challenge, registration, NOW + 1), true);
|
||
|
|
assert.throws(() => verifyNodePossessionResponse(response, challenge, registration, NOW + 2), /proof_invalid/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects a forged challenge that was not issued by the verifier', () => {
|
||
|
|
const registration = verifiedRegistration();
|
||
|
|
const challenge = {
|
||
|
|
...createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43)),
|
||
|
|
nonce: 'B'.repeat(43),
|
||
|
|
};
|
||
|
|
assert.throws(() => verifyNodePossessionResponse(responseFor(challenge), challenge, registration, NOW + 1), /proof_invalid/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects a response signed by a different node key', () => {
|
||
|
|
const registration = verifiedRegistration();
|
||
|
|
const challenge = createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43));
|
||
|
|
const otherKeys = generateKeyPairSync('ed25519');
|
||
|
|
const response = {
|
||
|
|
challengeId: challenge.challengeId,
|
||
|
|
publicKeyPem: otherKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString(),
|
||
|
|
schema: 'gh-aios.node-possession-response/v1',
|
||
|
|
signature: sign(null, nodePossessionChallengeSigningBytes(challenge), otherKeys.privateKey).toString('base64'),
|
||
|
|
};
|
||
|
|
assert.throws(() => verifyNodePossessionResponse(response, challenge, registration, NOW + 1), /proof_invalid/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects expired, replayed-to-another-challenge and unknown-field responses', () => {
|
||
|
|
const registration = verifiedRegistration();
|
||
|
|
const first = createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43));
|
||
|
|
const second = createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-002', 'B'.repeat(43));
|
||
|
|
assert.throws(() => verifyNodePossessionResponse(responseFor(first), second, registration, NOW + 1), /proof_invalid/);
|
||
|
|
assert.throws(() => verifyNodePossessionResponse({ ...responseFor(first), extra: true }, first, registration, NOW + 1), /proof_invalid/);
|
||
|
|
assert.throws(() => verifyNodePossessionResponse(responseFor(first), first, registration, NOW + 60_000), /proof_invalid/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('does not accept a structurally copied registration as verified provenance', () => {
|
||
|
|
const copied = { ...verifiedRegistration() };
|
||
|
|
assert.throws(() => createNodePossessionChallenge(copied, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43)), /proof_invalid/);
|
||
|
|
});
|