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:
26
control-plane/src/boot/session-signing.ts
Normal file
26
control-plane/src/boot/session-signing.ts
Normal 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'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user