feat: add encrypted local node enrollment

This commit is contained in:
冰朔 2026-08-10 11:09:12 +08:00
commit d2ec5c6c87
6 changed files with 233 additions and 14 deletions

View file

@ -12,19 +12,19 @@ import {
type NodePossessionResponse,
} from './node-possession-proof.js';
const RECORD_SCHEMA = 'gh-aios.node-keystore-record/v1' as const;
export const NODE_KEYSTORE_RECORD_SCHEMA = 'gh-aios.node-keystore-record/v1' as const;
const RESPONSE_SCHEMA = 'gh-aios.node-possession-response/v1' as const;
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/;
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
interface NodeKeyRecord {
export interface NodeKeyRecord {
encryptedPrivateKeyPkcs8: string;
nodeId: string;
nodeKeyFingerprint: string;
nodeType: 'local-terminal' | 'cloud-resident';
publicKeyPem: string;
schema: typeof RECORD_SCHEMA;
schema: typeof NODE_KEYSTORE_RECORD_SCHEMA;
}
export interface NodeKeyStoreAdapter {
@ -43,7 +43,7 @@ function hasExactKeys(record: Record<string, unknown>, keys: string[]): boolean
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
}
function parseRecord(input: unknown): NodeKeyRecord | null {
export function parseNodeKeyRecord(input: unknown): NodeKeyRecord | null {
if (!isRecord(input)
|| !hasExactKeys(input, [
'encryptedPrivateKeyPkcs8',
@ -53,7 +53,7 @@ function parseRecord(input: unknown): NodeKeyRecord | null {
'publicKeyPem',
'schema',
])
|| input.schema !== RECORD_SCHEMA
|| input.schema !== NODE_KEYSTORE_RECORD_SCHEMA
|| typeof input.nodeId !== 'string'
|| !IDENTIFIER_PATTERN.test(input.nodeId)
|| (input.nodeType !== 'local-terminal' && input.nodeType !== 'cloud-resident')
@ -69,7 +69,7 @@ function parseRecord(input: unknown): NodeKeyRecord | null {
return input as unknown as NodeKeyRecord;
}
function publicKeyFingerprint(publicKeyPem: string): string | null {
export function nodePublicKeyFingerprint(publicKeyPem: string): string | null {
try {
const publicKey = createPublicKey({ key: publicKeyPem, format: 'pem' });
if (publicKey.asymmetricKeyType !== 'ed25519') return null;
@ -92,12 +92,12 @@ export class EncryptedNodeKeyStoreProofSource implements NodePossessionProofSour
try {
assertActiveNodePossessionChallenge(challenge, this.clock());
if (!this.adapter.isAvailable()) return null;
const record = parseRecord(await this.adapter.readRecord());
const record = parseNodeKeyRecord(await this.adapter.readRecord());
if (!record
|| record.nodeId !== challenge.nodeId
|| record.nodeType !== challenge.nodeType
|| record.nodeKeyFingerprint !== challenge.nodeKeyFingerprint
|| publicKeyFingerprint(record.publicKeyPem) !== record.nodeKeyFingerprint) return null;
|| nodePublicKeyFingerprint(record.publicKeyPem) !== record.nodeKeyFingerprint) return null;
privateKeyPkcs8 = await this.adapter.decryptPrivateKeyPkcs8(record.encryptedPrivateKeyPkcs8);
if (!privateKeyPkcs8 || privateKeyPkcs8.length === 0 || privateKeyPkcs8.length > 16_384) return null;