Files
web-terminal/control-plane/test/env.test.ts
Yaojia Wang af630143de 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.
2026-07-18 15:04:04 +02:00

176 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (16512 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/,
)
})
})