feat(control-plane): POST /auth/login mints device:enroll bearer (B1)
Unblocks the phone-enrollment track: an operator-password login mints a short-lived device:enroll capability token that POST /device/enroll requires. Constant-time (SHA-256 fixed-length) compare, per-client rate-limit, fail-closed when unset. 260 tests pass.
This commit is contained in:
@@ -43,6 +43,9 @@ import { createFrpClientLeafSigner } from './ca/frpclient-issue.js'
|
||||
import { createDeviceLeafSigner, type DeviceLeafSigner } from './ca/device-issue.js'
|
||||
import { createLeafRenewer } from './ca/rotate.js'
|
||||
import { buildDeviceEnrollRouter, type SubdomainOwnershipResolver } from './api/device-enroll.js'
|
||||
import { buildAuthLoginRouter } from './api/auth-login.js'
|
||||
import { loadEnrollSigningKey } from './boot/session-signing.js'
|
||||
import type { LoginSeamConfig } from './auth/session.js'
|
||||
import { buildRenewRouter } from './api/renew.js'
|
||||
import { buildNativeCas, DEFAULT_NATIVE_DNS_ZONE, type NativeCas, type NativeCaMaterial } from './boot/native-ca.js'
|
||||
import type { RevocationBus } from 'relay-contracts'
|
||||
@@ -243,6 +246,18 @@ export async function buildControlPlane(
|
||||
await app.register(buildRouter({ authorizer, accounts, hosts, pairingIssuer, redeemer, deprovisioner, nativeEnroller }))
|
||||
// Device enrollment is bearer-gated by the SAME capability verifier seam the admin API uses.
|
||||
await app.register(buildDeviceEnrollRouter({ verifier, devices: deviceRegistry, signer: deviceSigner, ownership }))
|
||||
// B1 — operator login → device:enroll bearer mint. The route ALWAYS registers (so a phone client
|
||||
// gets a coherent response), but is FAIL-CLOSED (503) unless the operator triplet is env-configured
|
||||
// (env.ts cross-validates set-together-or-none). The bearer is signed with the PRIVATE half of
|
||||
// `CAPABILITY_SIGN_PUBKEY_B64` so it verifies on the same §4.3 path /device/enroll checks. INV9: the
|
||||
// signing key is imported non-exportable + sign-only; the operator secret is never logged.
|
||||
const enrollSigningKey =
|
||||
env.capabilitySignPrivkey !== undefined ? await loadEnrollSigningKey(env.capabilitySignPrivkey) : null
|
||||
const loginConfig: LoginSeamConfig =
|
||||
env.operatorPassword !== undefined && env.operatorAccountId !== undefined
|
||||
? { operatorCredential: env.operatorPassword, accountId: env.operatorAccountId }
|
||||
: {}
|
||||
await app.register(buildAuthLoginRouter({ signingKey: enrollSigningKey, loginConfig }))
|
||||
// Leaf renewal is mTLS-authenticated (current client cert) — anchors chain-validate the presented cert.
|
||||
await app.register(
|
||||
buildRenewRouter({
|
||||
|
||||
Reference in New Issue
Block a user