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:
Yaojia Wang
2026-07-18 13:32:05 +02:00
parent 232ef22535
commit fff011bb7f
7 changed files with 482 additions and 3 deletions

View File

@@ -0,0 +1,26 @@
/**
* B1 — load the `device:enroll` bearer SIGNING key (Ed25519) from the CP env's PKCS#8 DER.
*
* The enroll bearer must verify on the SAME §4.3 path the admin API uses (boot/verifier.ts, keyed off
* `CAPABILITY_SIGN_PUBKEY_B64`), so the login route signs with the PRIVATE half of that same keypair.
* The key is imported NON-EXPORTABLE + `sign`-only (INV9: raw key material is never held as bytes and
* never logged). A malformed / non-Ed25519 key FAILS CLOSED (throws) — the CP refuses to serve a login
* route it cannot mint from.
*/
/**
* Import the Ed25519 PKCS#8 private key as a non-exportable, sign-only `CryptoKey`. Throws (fail-closed)
* on any malformed key; the error message never echoes key material (INV9).
*/
export async function loadEnrollSigningKey(pkcs8Der: Uint8Array): Promise<CryptoKey> {
// Copy into a fresh ArrayBuffer-backed view (WebCrypto BufferSource typing / no shared pool).
const bytes = new Uint8Array(pkcs8Der.length)
bytes.set(pkcs8Der)
try {
return await globalThis.crypto.subtle.importKey('pkcs8', bytes, { name: 'Ed25519' }, false, ['sign'])
} catch (err: unknown) {
throw new Error(
`failed to load device:enroll signing key: ${err instanceof Error ? err.message : 'unknown'}`,
)
}
}