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.
176 lines
6.4 KiB
TypeScript
176 lines
6.4 KiB
TypeScript
import { describe, test, expect } from 'vitest'
|
||
import { loadEnv, DEFAULT_PAIRING_TTL_SEC, DEFAULT_PAIRING_MAX_REDEEM_ATTEMPTS } from '../src/env.js'
|
||
import { bytesToBase64 } from '../src/util/bytes.js'
|
||
|
||
const validPubkey = bytesToBase64(new Uint8Array(32).fill(7))
|
||
|
||
const base = (): NodeJS.ProcessEnv => ({
|
||
PG_URL: 'postgres://u:p@localhost:5432/cp',
|
||
REDIS_URL: 'redis://localhost:6379',
|
||
CAPABILITY_SIGN_PUBKEY_B64: validPubkey,
|
||
CA_INTERMEDIATE_KMS_KEY_REF: 'kms://key/intermediate',
|
||
CA_INTERMEDIATE_CERT_PATH: '/etc/cp/intermediate.pem',
|
||
NODE_MTLS_TRUST_BUNDLE_PATH: '/etc/cp/node-ca.pem',
|
||
BASE_DOMAIN: 'term.example.com',
|
||
})
|
||
|
||
describe('T1 loadEnv (INV9 fail-fast)', () => {
|
||
test('empty env throws listing every missing required key', () => {
|
||
let msg = ''
|
||
try {
|
||
loadEnv({})
|
||
} catch (e) {
|
||
msg = e instanceof Error ? e.message : ''
|
||
}
|
||
expect(msg).toContain('PG_URL')
|
||
expect(msg).toContain('REDIS_URL')
|
||
expect(msg).toContain('CAPABILITY_SIGN_PUBKEY_B64')
|
||
expect(msg).toContain('CA_INTERMEDIATE_KMS_KEY_REF')
|
||
expect(msg).toContain('BASE_DOMAIN')
|
||
})
|
||
|
||
test('bad pgUrl throws', () => {
|
||
expect(() => loadEnv({ ...base(), PG_URL: 'not a url' })).toThrow(/PG_URL/)
|
||
})
|
||
|
||
test('valid map parses', () => {
|
||
const env = loadEnv(base())
|
||
expect(env.baseDomain).toBe('term.example.com')
|
||
expect(env.capabilitySignPubkey.length).toBe(32)
|
||
})
|
||
|
||
test('pairing ttl / max-attempts default when unset', () => {
|
||
const env = loadEnv(base())
|
||
expect(env.pairingTtlSec).toBe(DEFAULT_PAIRING_TTL_SEC)
|
||
expect(env.pairingMaxRedeemAttempts).toBe(DEFAULT_PAIRING_MAX_REDEEM_ATTEMPTS)
|
||
})
|
||
|
||
test('explicit pairing overrides parse', () => {
|
||
const env = loadEnv({ ...base(), PAIRING_TTL_SEC: '900', PAIRING_MAX_REDEEM_ATTEMPTS: '3' })
|
||
expect(env.pairingTtlSec).toBe(900)
|
||
expect(env.pairingMaxRedeemAttempts).toBe(3)
|
||
})
|
||
|
||
test('never echoes secret VALUES on failure (INV9)', () => {
|
||
const secret = bytesToBase64(new Uint8Array(32).fill(9))
|
||
try {
|
||
loadEnv({ ...base(), CAPABILITY_SIGN_PUBKEY_B64: secret, PG_URL: 'bad' })
|
||
} catch (e) {
|
||
const msg = e instanceof Error ? e.message : ''
|
||
expect(msg).not.toContain(secret)
|
||
}
|
||
})
|
||
|
||
test('capability pubkey wrong length rejected', () => {
|
||
const short = bytesToBase64(new Uint8Array(16))
|
||
expect(() => loadEnv({ ...base(), CAPABILITY_SIGN_PUBKEY_B64: short })).toThrow(/32 bytes/)
|
||
})
|
||
})
|
||
|
||
describe('B1 operator-login env (set-together-or-none, fail-closed)', () => {
|
||
const OPERATOR_PASSWORD = 'operator-secret-abcdef123456'
|
||
const ACCOUNT = '11111111-1111-4111-8111-111111111111'
|
||
const PRIVKEY = bytesToBase64(new Uint8Array(48).fill(3)) // shape-valid base64; import validated at boot
|
||
|
||
test('none set → login fields undefined (feature off, route fail-closed at runtime)', () => {
|
||
const env = loadEnv(base())
|
||
expect(env.operatorPassword).toBeUndefined()
|
||
expect(env.operatorAccountId).toBeUndefined()
|
||
expect(env.capabilitySignPrivkey).toBeUndefined()
|
||
})
|
||
|
||
test('all three set → parsed together', () => {
|
||
const env = loadEnv({
|
||
...base(),
|
||
OPERATOR_PASSWORD,
|
||
OPERATOR_ACCOUNT_ID: ACCOUNT,
|
||
CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY,
|
||
})
|
||
expect(env.operatorPassword).toBe(OPERATOR_PASSWORD)
|
||
expect(env.operatorAccountId).toBe(ACCOUNT)
|
||
expect(env.capabilitySignPrivkey?.length).toBeGreaterThan(0)
|
||
})
|
||
|
||
test('password without account/key → fail-fast (no silent half-open)', () => {
|
||
expect(() => loadEnv({ ...base(), OPERATOR_PASSWORD })).toThrow(/OPERATOR_ACCOUNT_ID|CAPABILITY_SIGN_PRIVKEY_B64/)
|
||
})
|
||
|
||
test('too-short operator password rejected (16–512 charset rule)', () => {
|
||
expect(() =>
|
||
loadEnv({ ...base(), OPERATOR_PASSWORD: 'short', OPERATOR_ACCOUNT_ID: ACCOUNT, CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY }),
|
||
).toThrow(/OPERATOR_PASSWORD/)
|
||
})
|
||
|
||
test('non-UUID operator account rejected', () => {
|
||
expect(() =>
|
||
loadEnv({ ...base(), OPERATOR_PASSWORD, OPERATOR_ACCOUNT_ID: 'not-a-uuid', CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY }),
|
||
).toThrow(/OPERATOR_ACCOUNT_ID/)
|
||
})
|
||
|
||
test('never echoes the operator secret on a partial-config failure (INV9)', () => {
|
||
try {
|
||
loadEnv({ ...base(), OPERATOR_PASSWORD })
|
||
} catch (e) {
|
||
expect(e instanceof Error ? e.message : '').not.toContain(OPERATOR_PASSWORD)
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('A2-prep native-tunnel CA env (set-together-or-none, fail-closed)', () => {
|
||
const FRP_CERT = '/etc/cp/frp-client-ca.cert.pem'
|
||
const DEV_CERT = '/etc/cp/device-ca.cert.pem'
|
||
const ZONE = 'native.example.test'
|
||
|
||
test('none set → nativeCa undefined (injection/dev path)', () => {
|
||
const env = loadEnv(base())
|
||
expect(env.nativeCa).toBeUndefined()
|
||
})
|
||
|
||
test('cert paths + dns zone set → parsed, key paths DERIVED from cert paths', () => {
|
||
const env = loadEnv({
|
||
...base(),
|
||
NATIVE_FRP_CLIENT_CA_CERT_PATH: FRP_CERT,
|
||
NATIVE_DEVICE_CA_CERT_PATH: DEV_CERT,
|
||
NATIVE_DNS_ZONE: ZONE,
|
||
})
|
||
expect(env.nativeCa).toEqual({
|
||
frpClientCaCertPath: FRP_CERT,
|
||
frpClientCaKeyPath: '/etc/cp/frp-client-ca.key.pem',
|
||
deviceCaCertPath: DEV_CERT,
|
||
deviceCaKeyPath: '/etc/cp/device-ca.key.pem',
|
||
dnsZone: ZONE,
|
||
})
|
||
})
|
||
|
||
test('explicit key paths are used verbatim (not derived)', () => {
|
||
const env = loadEnv({
|
||
...base(),
|
||
NATIVE_FRP_CLIENT_CA_CERT_PATH: FRP_CERT,
|
||
NATIVE_FRP_CLIENT_CA_KEY_PATH: '/keys/frp.pem',
|
||
NATIVE_DEVICE_CA_CERT_PATH: DEV_CERT,
|
||
NATIVE_DEVICE_CA_KEY_PATH: '/keys/dev.pem',
|
||
NATIVE_DNS_ZONE: ZONE,
|
||
})
|
||
expect(env.nativeCa?.frpClientCaKeyPath).toBe('/keys/frp.pem')
|
||
expect(env.nativeCa?.deviceCaKeyPath).toBe('/keys/dev.pem')
|
||
})
|
||
|
||
test('partial set — only frp cert path → fail-closed listing the missing keys', () => {
|
||
expect(() => loadEnv({ ...base(), NATIVE_FRP_CLIENT_CA_CERT_PATH: FRP_CERT })).toThrow(
|
||
/NATIVE_DEVICE_CA_CERT_PATH|NATIVE_DNS_ZONE/,
|
||
)
|
||
})
|
||
|
||
test('partial set — cert paths without a dns zone → fail-closed', () => {
|
||
expect(() =>
|
||
loadEnv({ ...base(), NATIVE_FRP_CLIENT_CA_CERT_PATH: FRP_CERT, NATIVE_DEVICE_CA_CERT_PATH: DEV_CERT }),
|
||
).toThrow(/NATIVE_DNS_ZONE/)
|
||
})
|
||
|
||
test('partial set — a key path supplied WITHOUT its cert path → fail-closed', () => {
|
||
expect(() => loadEnv({ ...base(), NATIVE_FRP_CLIENT_CA_KEY_PATH: '/keys/frp.pem' })).toThrow(
|
||
/NATIVE_FRP_CLIENT_CA_CERT_PATH|NATIVE_DEVICE_CA_CERT_PATH|NATIVE_DNS_ZONE/,
|
||
)
|
||
})
|
||
})
|