feat(control-plane): production native-CA wiring for on-disk P-256 CAs (A2-prep)
Wire the prod boot to issue frp-client + device leaves from the existing on-disk P-256 CAs: NATIVE_* env vars, a file-backed KmsResolver (loads the PEM key, validates prime256v1, never logs key material), buildFileBackedNativeCas threaded into buildControlPlane via a nativeCas override. Fail-closed on partial NATIVE_*. 281 tests pass.
This commit is contained in:
@@ -13,8 +13,9 @@
|
||||
* `x509-assembler` normalizes to DER; both expose the same `sign()` so callers stay KMS-shaped.
|
||||
*/
|
||||
import { createPublicKey, generateKeyPairSync, sign as nodeSign } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import type { ControlPlaneEnv } from '../env.js'
|
||||
import { ed25519Sign, generateEd25519, type KeyObject } from '../util/crypto.js'
|
||||
import { createPrivateKey, ed25519Sign, generateEd25519, type KeyObject } from '../util/crypto.js'
|
||||
|
||||
/** Wraps KMS `sign()`; no raw key ever crosses this boundary. */
|
||||
export interface CaSigner {
|
||||
@@ -98,3 +99,56 @@ export function inProcessP256CaSigner(privateKey?: KeyObject): CaSigner {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefix marking a KMS key ref that is actually an on-disk PEM key path (`file:<path>`). */
|
||||
const FILE_KEY_REF_PREFIX = 'file:'
|
||||
/** OpenSSL's name for the P-256 (secp256r1) curve — the ONLY curve the native-tunnel CAs use. */
|
||||
const P256_CURVE = 'prime256v1'
|
||||
|
||||
/** Build a `file:<path>` KMS key ref from an on-disk native-CA private key path. */
|
||||
export function fileKeyRef(path: string): string {
|
||||
return `${FILE_KEY_REF_PREFIX}${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a PKCS#8 PEM P-256 private key from disk and wrap it in the SAME `CaSigner` surface as
|
||||
* `inProcessP256CaSigner` (raw P1363 `r||s` over the DER TBS). FAIL-FAST on an unreadable file,
|
||||
* malformed PEM, or a non-P-256 key. The raw key stays in-process — NEVER logged/serialised (INV9).
|
||||
*/
|
||||
function loadP256FileSigner(path: string): CaSigner {
|
||||
let pem: string
|
||||
try {
|
||||
pem = readFileSync(path, 'utf8')
|
||||
} catch {
|
||||
// Never echo the path contents — only that the file was unreadable (INV9).
|
||||
throw new Error('native-CA private key file is unreadable')
|
||||
}
|
||||
let key: KeyObject
|
||||
try {
|
||||
key = createPrivateKey(pem)
|
||||
} catch {
|
||||
throw new Error('native-CA private key is not a valid PEM private key')
|
||||
}
|
||||
if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== P256_CURVE) {
|
||||
throw new Error('native-CA private key must be a P-256 (prime256v1) EC key')
|
||||
}
|
||||
return inProcessP256CaSigner(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* FILE-BACKED KmsResolver — the production native-tunnel key custody for the single-owner VPS deploy.
|
||||
* Resolves a key ref of the form `file:<path>` (or a bare path) by loading the on-disk PEM P-256 key
|
||||
* into an in-process `CaSigner`. `policyRestrictedToServicePrincipal` is TRUE because the on-disk key
|
||||
* IS the control-plane's sole custody (documented file-custody reality; a real non-exportable KMS —
|
||||
* `KmsResolver` in front of KMS/HSM — is the future upgrade). Throws if the ref cannot be resolved so
|
||||
* `buildCaSigner` fails fast at boot. NEVER logs key material (INV9).
|
||||
*/
|
||||
export function fileKmsResolver(): KmsResolver {
|
||||
return {
|
||||
async resolve(keyRef: string) {
|
||||
const path = keyRef.startsWith(FILE_KEY_REF_PREFIX) ? keyRef.slice(FILE_KEY_REF_PREFIX.length) : keyRef
|
||||
if (path.length === 0) throw new Error('file KMS key ref has an empty path')
|
||||
return { signer: loadP256FileSigner(path), policyRestrictedToServicePrincipal: true }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user