@skrr-ai/cli 0.1.123 → 0.1.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents/chat.d.ts +26 -0
- package/dist/commands/agents/chat.js +48 -10
- package/dist/commands/agents/delete.d.ts +8 -0
- package/dist/commands/agents/delete.js +16 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/search/workspace.js +1 -1
- package/dist/commands/store/gallery-admissions/approve.js +1 -1
- package/dist/commands/store/gallery-admissions/preview.js +1 -1
- package/dist/commands/store/gallery-admissions/refuse.js +1 -1
- package/dist/commands/store/gallery-admissions/show.js +1 -1
- package/dist/generated/agent-guides.json +19 -19
- package/dist/lib/agentic-stream.js +15 -1
- package/dist/lib/auth-core-init.d.ts +15 -2
- package/dist/lib/auth-core-init.js +47 -6
- package/dist/lib/command-hygiene.js +30 -1
- package/dist/lib/daemonBrokerRefusal.js +5 -1
- package/dist/lib/keychain.js +41 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +13 -28
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +133 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +387 -90
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +102 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +11 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +43 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +33 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +15 -37
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +133 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +536 -101
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +13 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +43 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +104 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +14 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1094 -1067
- package/oclif.manifest.json +16 -3
- package/package.json +4 -1
|
@@ -50,6 +50,33 @@ export interface Ed25519PublicJwk {
|
|
|
50
50
|
export interface Ed25519PrivateJwk extends Ed25519PublicJwk {
|
|
51
51
|
readonly d: string;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* NIST P-256 public JSON Web Key — RFC 7518 §6.2 ("Key Type 'EC'").
|
|
55
|
+
*
|
|
56
|
+
* The only curve a Secure Enclave or TPM will sign with, which is why a
|
|
57
|
+
* hardware-bound device key is a different `alg` (ES256) rather than a
|
|
58
|
+
* different storage backend for the same algorithm. `x` and `y` are the
|
|
59
|
+
* 32-byte affine coordinates in base64url-no-padding. There is deliberately
|
|
60
|
+
* no private form: a P-256 device key in this codebase is non-exportable, so
|
|
61
|
+
* nothing here ever holds its scalar.
|
|
62
|
+
*/
|
|
63
|
+
export interface P256PublicJwk {
|
|
64
|
+
readonly kty: 'EC';
|
|
65
|
+
readonly crv: 'P-256';
|
|
66
|
+
readonly x: string;
|
|
67
|
+
readonly y: string;
|
|
68
|
+
}
|
|
69
|
+
/** Any public key a device proof may be signed with. */
|
|
70
|
+
export type DevicePublicJwk = Ed25519PublicJwk | P256PublicJwk;
|
|
71
|
+
/**
|
|
72
|
+
* The JWS algorithms a device proof may use. `EdDSA` is the software key every
|
|
73
|
+
* install has had since L11; `ES256` is the hardware-bound key. A server pins
|
|
74
|
+
* ONE per device and never requires a client to use an algorithm it did not
|
|
75
|
+
* announce.
|
|
76
|
+
*/
|
|
77
|
+
export type DeviceKeyAlg = 'EdDSA' | 'ES256';
|
|
78
|
+
/** Every algorithm this build can verify, in the order a client prefers them. */
|
|
79
|
+
export declare const DEVICE_KEY_ALGS: readonly DeviceKeyAlg[];
|
|
53
80
|
/**
|
|
54
81
|
* The full keypair returned at generation time. Holding both forms here
|
|
55
82
|
* is convenient for the daemon-side bridge — the public form is what we
|
|
@@ -57,6 +84,11 @@ export interface Ed25519PrivateJwk extends Ed25519PublicJwk {
|
|
|
57
84
|
*
|
|
58
85
|
* `thumbprint` is the RFC 7638 JWK thumbprint of the public key — the
|
|
59
86
|
* canonical stable id we use as the device handle on the server.
|
|
87
|
+
*
|
|
88
|
+
* This is the SOFTWARE Ed25519 key only, and only the sealed-file store and
|
|
89
|
+
* key generation touch its private half. Everything that signs holds a
|
|
90
|
+
* `DeviceKeyHandle` (deviceKeyHandle.ts) instead, which cannot return the
|
|
91
|
+
* private scalar and so works unchanged for a key that has none.
|
|
60
92
|
*/
|
|
61
93
|
export interface DeviceKeyPair {
|
|
62
94
|
readonly publicJwk: Ed25519PublicJwk;
|
|
@@ -83,7 +115,23 @@ export declare function generateDeviceKeyPair(): DeviceKeyPair;
|
|
|
83
115
|
* `kid` in JWS headers and as the registered identifier on the server's
|
|
84
116
|
* `Daemon` table.
|
|
85
117
|
*/
|
|
86
|
-
export declare function jwkThumbprint(publicJwk:
|
|
118
|
+
export declare function jwkThumbprint(publicJwk: DevicePublicJwk): string;
|
|
119
|
+
/**
|
|
120
|
+
* Structural check for a public device key: the right `kty`/`crv` pair, and
|
|
121
|
+
* coordinates that are exactly 32 bytes of base64url. It does not prove the
|
|
122
|
+
* point is on the curve (importing the key does that); it proves the value is
|
|
123
|
+
* safe to build a thumbprint from.
|
|
124
|
+
*/
|
|
125
|
+
export declare function isDevicePublicJwk(value: unknown): value is DevicePublicJwk;
|
|
126
|
+
/** The JWS algorithm a public key signs with. */
|
|
127
|
+
export declare function algForJwk(jwk: DevicePublicJwk): DeviceKeyAlg;
|
|
128
|
+
/**
|
|
129
|
+
* A P-256 public JWK from the 64-byte `x || y` form a Secure Enclave reports
|
|
130
|
+
* (CryptoKit's `rawRepresentation`). Rejects any other length so a helper that
|
|
131
|
+
* returns an x9.63 point (65 bytes, leading 0x04) fails loudly instead of being
|
|
132
|
+
* mis-split into a key nothing can verify.
|
|
133
|
+
*/
|
|
134
|
+
export declare function p256PublicJwkFromRaw(raw: Uint8Array): P256PublicJwk;
|
|
87
135
|
/**
|
|
88
136
|
* Construct an `Ed25519PublicJwk` from a base64url-encoded 32-byte
|
|
89
137
|
* public point. Used by the persistence layer when reading a stored
|
|
@@ -124,13 +172,13 @@ export interface DeviceProofClaims {
|
|
|
124
172
|
ath?: string;
|
|
125
173
|
}
|
|
126
174
|
export interface DeviceProofHeader {
|
|
127
|
-
alg:
|
|
175
|
+
alg: DeviceKeyAlg;
|
|
128
176
|
/** Custom `typ` so the server can refuse generic JWTs that happen
|
|
129
|
-
* to be
|
|
177
|
+
* to be signed by a stolen device key. RFC 9449 uses
|
|
130
178
|
* `dpop+jwt`; we differentiate so OverSky proofs can't be
|
|
131
179
|
* cross-replayed into a generic DPoP resource server. */
|
|
132
180
|
typ: 'oversky-dpop+jwt';
|
|
133
|
-
jwk:
|
|
181
|
+
jwk: DevicePublicJwk;
|
|
134
182
|
}
|
|
135
183
|
export interface BuildProofOptions {
|
|
136
184
|
method: string;
|
|
@@ -172,11 +220,35 @@ export declare const DEVICE_PROOF_TYP = "oversky-dpop+jwt";
|
|
|
172
220
|
* never verify as the other.
|
|
173
221
|
*/
|
|
174
222
|
export declare function signEd25519Proof(privateKey: crypto.KeyObject, publicJwk: Ed25519PublicJwk, typ: string, opts: BuildProofOptions): string;
|
|
223
|
+
/**
|
|
224
|
+
* The `header.payload` half of a proof: everything the key signs. Split out so
|
|
225
|
+
* a key that cannot be handed to this process (a Secure Enclave key signs
|
|
226
|
+
* inside a helper) signs the very same bytes a software key does, and nothing
|
|
227
|
+
* about the wire format depends on where the signature came from.
|
|
228
|
+
*
|
|
229
|
+
* @internal Shared by `signEd25519Proof` and `signProofWith`.
|
|
230
|
+
*/
|
|
231
|
+
export declare function proofSigningInput(alg: DeviceKeyAlg, publicJwk: DevicePublicJwk, typ: string, opts: BuildProofOptions): string;
|
|
232
|
+
/**
|
|
233
|
+
* Anything that can sign a device proof (a `DeviceKeySigner`): a software key held in this process,
|
|
234
|
+
* or a hardware key that signs elsewhere. `sign` returns the JWS signature for
|
|
235
|
+
* `alg` (RFC 7515 §A.3 / RFC 8037: raw r||s for ES256, the 64 bytes for EdDSA),
|
|
236
|
+
* never DER.
|
|
237
|
+
*/
|
|
238
|
+
export interface DeviceKeySigner {
|
|
239
|
+
readonly alg: DeviceKeyAlg;
|
|
240
|
+
readonly publicJwk: DevicePublicJwk;
|
|
241
|
+
sign(signingInput: Uint8Array): Promise<Uint8Array>;
|
|
242
|
+
}
|
|
243
|
+
/** Sign a device-identity proof with any `DeviceKeySigner`. */
|
|
244
|
+
export declare function signProofWith(signer: DeviceKeySigner, opts: BuildProofOptions, typ?: string): Promise<string>;
|
|
175
245
|
export type VerifyFailureReason = 'malformed' | 'bad_alg' | 'bad_typ' | 'bad_jwk' | 'sig_invalid' | 'iat_old' | 'iat_future' | 'htm_mismatch' | 'htu_mismatch' | 'rth_mismatch' | 'ath_mismatch' | 'pin_mismatch';
|
|
176
246
|
export interface VerifySuccess {
|
|
177
247
|
valid: true;
|
|
178
248
|
claims: DeviceProofClaims;
|
|
179
|
-
publicJwk:
|
|
249
|
+
publicJwk: DevicePublicJwk;
|
|
250
|
+
/** The algorithm the proof was signed with; always one `allowedAlgs` admitted. */
|
|
251
|
+
alg: DeviceKeyAlg;
|
|
180
252
|
thumbprint: string;
|
|
181
253
|
}
|
|
182
254
|
export interface VerifyFailure {
|
|
@@ -199,8 +271,17 @@ export interface VerifyOptions {
|
|
|
199
271
|
expectedAth?: string;
|
|
200
272
|
/** Public JWK pinned at the server side. When provided, the proof's
|
|
201
273
|
* inlined JWK MUST equal this — defends against a stolen valid
|
|
202
|
-
* proof signed by a different (e.g., revoked) device.
|
|
203
|
-
|
|
274
|
+
* proof signed by a different (e.g., revoked) device. Compared by
|
|
275
|
+
* thumbprint, so a pin on one algorithm never admits a key of another. */
|
|
276
|
+
pinnedPublicJwk?: DevicePublicJwk;
|
|
277
|
+
/**
|
|
278
|
+
* The algorithms this verification admits. Defaults to `['EdDSA']`: a caller
|
|
279
|
+
* that has not decided it accepts hardware-bound keys keeps exactly the
|
|
280
|
+
* behaviour it had before ES256 existed. A server that pins a device to ES256
|
|
281
|
+
* passes the algorithm of the PIN, not `DEVICE_KEY_ALGS`, so a proof cannot
|
|
282
|
+
* choose a weaker algorithm than the device enrolled with.
|
|
283
|
+
*/
|
|
284
|
+
allowedAlgs?: readonly DeviceKeyAlg[];
|
|
204
285
|
/** Override the wall clock — testing hook. Defaults to `Date.now()`. */
|
|
205
286
|
now?: number;
|
|
206
287
|
}
|
|
@@ -217,7 +298,25 @@ export declare function verifyProof(jws: string, opts?: VerifyOptions): VerifySu
|
|
|
217
298
|
/**
|
|
218
299
|
* Verify a DPoP-shaped Ed25519 proof whose header `typ` must equal `typ`.
|
|
219
300
|
*
|
|
301
|
+
* Always EdDSA, whatever `opts.allowedAlgs` says: the RFC 9449 access-token
|
|
302
|
+
* proof (`dpop.ts`) is signed by the CLI's own in-memory Ed25519 key, which is a
|
|
303
|
+
* different key from the device key and deliberately stays software.
|
|
304
|
+
*
|
|
220
305
|
* @internal See `signEd25519Proof`. `verifyProof` (device identity) and
|
|
221
306
|
* `verifyDpopProof` (`dpop.ts`, RFC 9449) are the two public doors.
|
|
222
307
|
*/
|
|
223
308
|
export declare function verifyEd25519Proof(jws: string, opts: VerifyOptions, typ: string): VerifySuccess | VerifyFailure;
|
|
309
|
+
/**
|
|
310
|
+
* Verify a DPoP-shaped proof signed by an Ed25519 or P-256 key, whose header
|
|
311
|
+
* `typ` must equal `typ` and whose `alg` must be in `opts.allowedAlgs`.
|
|
312
|
+
*
|
|
313
|
+
* The algorithm comes from the header and is checked twice before any key is
|
|
314
|
+
* built: it must be one the caller admitted, and it must be the algorithm the
|
|
315
|
+
* header's own key type signs with. Without the second check a header could
|
|
316
|
+
* pair `alg: ES256` with an Ed25519 key (or the reverse) and steer the
|
|
317
|
+
* verifier into a code path nobody tested; with it, the only thing an attacker
|
|
318
|
+
* controls about the algorithm is a choice among the caller's own allow-list.
|
|
319
|
+
*
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
export declare function verifyDevicePublicKeyProof(jws: string, opts: VerifyOptions, typ: string): VerifySuccess | VerifyFailure;
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
* - Wire-up to the daemon's outgoing HTTP path (Phase 2 of L11).
|
|
36
36
|
*/
|
|
37
37
|
import crypto from 'node:crypto';
|
|
38
|
+
/** Every algorithm this build can verify, in the order a client prefers them. */
|
|
39
|
+
export const DEVICE_KEY_ALGS = ['ES256', 'EdDSA'];
|
|
38
40
|
// ---------------------------------------------------------------------------
|
|
39
41
|
// Base64url helpers — same shape as `credentialEnvelope.ts` so the two
|
|
40
42
|
// modules are visually consistent in the auth-core codebase.
|
|
@@ -91,9 +93,55 @@ export function jwkThumbprint(publicJwk) {
|
|
|
91
93
|
// guarantee key order across Node versions, even though current V8
|
|
92
94
|
// preserves insertion order. Build the bytes ourselves to defend
|
|
93
95
|
// against a hypothetical future regression.
|
|
94
|
-
|
|
96
|
+
//
|
|
97
|
+
// RFC 7638 §3.2: an EC key's required members are crv, kty, x, y; an OKP
|
|
98
|
+
// key's are crv, kty, x. The caller has validated the members as base64url
|
|
99
|
+
// (`isDevicePublicJwk`), so nothing here can inject a quote into the input.
|
|
100
|
+
const canonical = publicJwk.kty === 'EC'
|
|
101
|
+
? `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}","y":"${publicJwk.y}"}`
|
|
102
|
+
: `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}"}`;
|
|
95
103
|
return b64u(crypto.createHash('sha256').update(canonical, 'utf-8').digest());
|
|
96
104
|
}
|
|
105
|
+
const B64URL_32 = /^[A-Za-z0-9_-]{43}$/;
|
|
106
|
+
/**
|
|
107
|
+
* Structural check for a public device key: the right `kty`/`crv` pair, and
|
|
108
|
+
* coordinates that are exactly 32 bytes of base64url. It does not prove the
|
|
109
|
+
* point is on the curve (importing the key does that); it proves the value is
|
|
110
|
+
* safe to build a thumbprint from.
|
|
111
|
+
*/
|
|
112
|
+
export function isDevicePublicJwk(value) {
|
|
113
|
+
if (!value || typeof value !== 'object')
|
|
114
|
+
return false;
|
|
115
|
+
const jwk = value;
|
|
116
|
+
if (jwk.kty === 'OKP') {
|
|
117
|
+
return jwk.crv === 'Ed25519' && typeof jwk.x === 'string' && B64URL_32.test(jwk.x);
|
|
118
|
+
}
|
|
119
|
+
if (jwk.kty === 'EC') {
|
|
120
|
+
return (jwk.crv === 'P-256' &&
|
|
121
|
+
typeof jwk.x === 'string' &&
|
|
122
|
+
typeof jwk.y === 'string' &&
|
|
123
|
+
B64URL_32.test(jwk.x) &&
|
|
124
|
+
B64URL_32.test(jwk.y));
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/** The JWS algorithm a public key signs with. */
|
|
129
|
+
export function algForJwk(jwk) {
|
|
130
|
+
return jwk.kty === 'EC' ? 'ES256' : 'EdDSA';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A P-256 public JWK from the 64-byte `x || y` form a Secure Enclave reports
|
|
134
|
+
* (CryptoKit's `rawRepresentation`). Rejects any other length so a helper that
|
|
135
|
+
* returns an x9.63 point (65 bytes, leading 0x04) fails loudly instead of being
|
|
136
|
+
* mis-split into a key nothing can verify.
|
|
137
|
+
*/
|
|
138
|
+
export function p256PublicJwkFromRaw(raw) {
|
|
139
|
+
if (raw.length !== 64) {
|
|
140
|
+
throw new Error(`p256PublicJwkFromRaw: expected 64 bytes (x||y), got ${raw.length}`);
|
|
141
|
+
}
|
|
142
|
+
const buf = Buffer.from(raw);
|
|
143
|
+
return { kty: 'EC', crv: 'P-256', x: b64u(buf.subarray(0, 32)), y: b64u(buf.subarray(32)) };
|
|
144
|
+
}
|
|
97
145
|
/**
|
|
98
146
|
* Construct an `Ed25519PublicJwk` from a base64url-encoded 32-byte
|
|
99
147
|
* public point. Used by the persistence layer when reading a stored
|
|
@@ -180,12 +228,34 @@ export const DEVICE_PROOF_TYP = 'oversky-dpop+jwt';
|
|
|
180
228
|
* never verify as the other.
|
|
181
229
|
*/
|
|
182
230
|
export function signEd25519Proof(privateKey, publicJwk, typ, opts) {
|
|
231
|
+
const signingInput = proofSigningInput('EdDSA', publicJwk, typ, opts);
|
|
232
|
+
// EdDSA over Ed25519 — Node's crypto.sign expects `null` as the
|
|
233
|
+
// digest algorithm because Ed25519 hashes the message internally.
|
|
234
|
+
const signature = crypto.sign(null, Buffer.from(signingInput, 'utf-8'), privateKey);
|
|
235
|
+
return `${signingInput}.${b64u(signature)}`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The `header.payload` half of a proof: everything the key signs. Split out so
|
|
239
|
+
* a key that cannot be handed to this process (a Secure Enclave key signs
|
|
240
|
+
* inside a helper) signs the very same bytes a software key does, and nothing
|
|
241
|
+
* about the wire format depends on where the signature came from.
|
|
242
|
+
*
|
|
243
|
+
* @internal Shared by `signEd25519Proof` and `signProofWith`.
|
|
244
|
+
*/
|
|
245
|
+
export function proofSigningInput(alg, publicJwk, typ, opts) {
|
|
183
246
|
if (typeof opts.method !== 'string' || opts.method.length === 0) {
|
|
184
247
|
throw new Error('buildProof: method must be a non-empty string');
|
|
185
248
|
}
|
|
186
249
|
if (typeof opts.url !== 'string' || opts.url.length === 0) {
|
|
187
250
|
throw new Error('buildProof: url must be a non-empty string');
|
|
188
251
|
}
|
|
252
|
+
// A header that claims an algorithm its own key cannot produce is a bug in the
|
|
253
|
+
// caller, and the one a verifier is built to reject (`bad_alg`). Refuse to
|
|
254
|
+
// emit it rather than ship a proof that fails on the server with a reason
|
|
255
|
+
// nobody connects to this line.
|
|
256
|
+
if (algForJwk(publicJwk) !== alg) {
|
|
257
|
+
throw new Error(`buildProof: alg ${alg} does not match a ${publicJwk.kty} key`);
|
|
258
|
+
}
|
|
189
259
|
const claims = {
|
|
190
260
|
htm: opts.method.toUpperCase(),
|
|
191
261
|
htu: normalizeHtu(opts.url),
|
|
@@ -196,14 +266,16 @@ export function signEd25519Proof(privateKey, publicJwk, typ, opts) {
|
|
|
196
266
|
claims.rth = tokenHash(opts.refreshToken);
|
|
197
267
|
if (opts.accessToken !== undefined)
|
|
198
268
|
claims.ath = tokenHash(opts.accessToken);
|
|
199
|
-
const header = { alg
|
|
269
|
+
const header = { alg, typ, jwk: publicJwk };
|
|
200
270
|
const headerB64 = b64u(Buffer.from(JSON.stringify(header), 'utf-8'));
|
|
201
271
|
const payloadB64 = b64u(Buffer.from(JSON.stringify(claims), 'utf-8'));
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
272
|
+
return `${headerB64}.${payloadB64}`;
|
|
273
|
+
}
|
|
274
|
+
/** Sign a device-identity proof with any `DeviceKeySigner`. */
|
|
275
|
+
export async function signProofWith(signer, opts, typ = DEVICE_PROOF_TYP) {
|
|
276
|
+
const signingInput = proofSigningInput(signer.alg, signer.publicJwk, typ, opts);
|
|
277
|
+
const signature = await signer.sign(Buffer.from(signingInput, 'utf-8'));
|
|
278
|
+
return `${signingInput}.${b64u(Buffer.from(signature))}`;
|
|
207
279
|
}
|
|
208
280
|
/**
|
|
209
281
|
* Verify a device-identity proof. Returns a discriminated union — the
|
|
@@ -215,15 +287,35 @@ export function signEd25519Proof(privateKey, publicJwk, typ, opts) {
|
|
|
215
287
|
* `sig_invalid` to keep the failure surface narrow.
|
|
216
288
|
*/
|
|
217
289
|
export function verifyProof(jws, opts = {}) {
|
|
218
|
-
return
|
|
290
|
+
return verifyDevicePublicKeyProof(jws, opts, DEVICE_PROOF_TYP);
|
|
219
291
|
}
|
|
220
292
|
/**
|
|
221
293
|
* Verify a DPoP-shaped Ed25519 proof whose header `typ` must equal `typ`.
|
|
222
294
|
*
|
|
295
|
+
* Always EdDSA, whatever `opts.allowedAlgs` says: the RFC 9449 access-token
|
|
296
|
+
* proof (`dpop.ts`) is signed by the CLI's own in-memory Ed25519 key, which is a
|
|
297
|
+
* different key from the device key and deliberately stays software.
|
|
298
|
+
*
|
|
223
299
|
* @internal See `signEd25519Proof`. `verifyProof` (device identity) and
|
|
224
300
|
* `verifyDpopProof` (`dpop.ts`, RFC 9449) are the two public doors.
|
|
225
301
|
*/
|
|
226
302
|
export function verifyEd25519Proof(jws, opts, typ) {
|
|
303
|
+
return verifyDevicePublicKeyProof(jws, { ...opts, allowedAlgs: ['EdDSA'] }, typ);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Verify a DPoP-shaped proof signed by an Ed25519 or P-256 key, whose header
|
|
307
|
+
* `typ` must equal `typ` and whose `alg` must be in `opts.allowedAlgs`.
|
|
308
|
+
*
|
|
309
|
+
* The algorithm comes from the header and is checked twice before any key is
|
|
310
|
+
* built: it must be one the caller admitted, and it must be the algorithm the
|
|
311
|
+
* header's own key type signs with. Without the second check a header could
|
|
312
|
+
* pair `alg: ES256` with an Ed25519 key (or the reverse) and steer the
|
|
313
|
+
* verifier into a code path nobody tested; with it, the only thing an attacker
|
|
314
|
+
* controls about the algorithm is a choice among the caller's own allow-list.
|
|
315
|
+
*
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
318
|
+
export function verifyDevicePublicKeyProof(jws, opts, typ) {
|
|
227
319
|
if (typeof jws !== 'string' || jws.length === 0) {
|
|
228
320
|
return { valid: false, reason: 'malformed' };
|
|
229
321
|
}
|
|
@@ -241,33 +333,34 @@ export function verifyEd25519Proof(jws, opts, typ) {
|
|
|
241
333
|
catch {
|
|
242
334
|
return { valid: false, reason: 'malformed' };
|
|
243
335
|
}
|
|
244
|
-
|
|
336
|
+
const allowedAlgs = opts.allowedAlgs ?? ['EdDSA'];
|
|
337
|
+
const headerAlg = header && typeof header === 'object' ? header.alg : undefined;
|
|
338
|
+
if ((headerAlg !== 'EdDSA' && headerAlg !== 'ES256') ||
|
|
339
|
+
!allowedAlgs.includes(headerAlg)) {
|
|
245
340
|
return { valid: false, reason: 'bad_alg' };
|
|
246
341
|
}
|
|
247
342
|
if (header.typ !== typ) {
|
|
248
343
|
return { valid: false, reason: 'bad_typ' };
|
|
249
344
|
}
|
|
345
|
+
// Take only the members a key of this type has. Whatever else the header's
|
|
346
|
+
// `jwk` carries (a stray `d`, `use`, `key_ops`) is dropped here, so nothing
|
|
347
|
+
// downstream can come to depend on a member we never validated, and the
|
|
348
|
+
// thumbprint is over exactly what was checked.
|
|
250
349
|
const headerJwk = header.jwk;
|
|
251
|
-
if (!headerJwk
|
|
252
|
-
typeof headerJwk !== 'object' ||
|
|
253
|
-
headerJwk.kty !== 'OKP' ||
|
|
254
|
-
headerJwk.crv !== 'Ed25519' ||
|
|
255
|
-
typeof headerJwk.x !== 'string') {
|
|
350
|
+
if (!isDevicePublicJwk(headerJwk)) {
|
|
256
351
|
return { valid: false, reason: 'bad_jwk' };
|
|
257
352
|
}
|
|
258
|
-
const publicJwk =
|
|
259
|
-
kty: '
|
|
260
|
-
crv: 'Ed25519',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
353
|
+
const publicJwk = headerJwk.kty === 'EC'
|
|
354
|
+
? { kty: 'EC', crv: 'P-256', x: headerJwk.x, y: headerJwk.y }
|
|
355
|
+
: { kty: 'OKP', crv: 'Ed25519', x: headerJwk.x };
|
|
356
|
+
// The algorithm and the key must agree (see the function comment).
|
|
357
|
+
if (algForJwk(publicJwk) !== headerAlg) {
|
|
358
|
+
return { valid: false, reason: 'bad_alg' };
|
|
359
|
+
}
|
|
360
|
+
// Importing the key validates it: an Ed25519 point of the wrong length, or a
|
|
361
|
+
// P-256 point that is not on the curve, throws here.
|
|
265
362
|
let publicKeyObj;
|
|
266
363
|
try {
|
|
267
|
-
const raw = b64uDecode(publicJwk.x);
|
|
268
|
-
if (raw.length !== 32) {
|
|
269
|
-
return { valid: false, reason: 'bad_jwk' };
|
|
270
|
-
}
|
|
271
364
|
publicKeyObj = crypto.createPublicKey({
|
|
272
365
|
format: 'jwk',
|
|
273
366
|
key: publicJwk,
|
|
@@ -276,7 +369,7 @@ export function verifyEd25519Proof(jws, opts, typ) {
|
|
|
276
369
|
catch {
|
|
277
370
|
return { valid: false, reason: 'bad_jwk' };
|
|
278
371
|
}
|
|
279
|
-
if (opts.pinnedPublicJwk && opts.pinnedPublicJwk
|
|
372
|
+
if (opts.pinnedPublicJwk && jwkThumbprint(opts.pinnedPublicJwk) !== jwkThumbprint(publicJwk)) {
|
|
280
373
|
return { valid: false, reason: 'pin_mismatch' };
|
|
281
374
|
}
|
|
282
375
|
// Verify the signature itself before validating claims — a bad
|
|
@@ -292,7 +385,17 @@ export function verifyEd25519Proof(jws, opts, typ) {
|
|
|
292
385
|
}
|
|
293
386
|
let sigOk;
|
|
294
387
|
try {
|
|
295
|
-
sigOk =
|
|
388
|
+
sigOk =
|
|
389
|
+
headerAlg === 'ES256'
|
|
390
|
+
? // JWS ES256 is the fixed-width r||s form, never DER (RFC 7518 §3.4).
|
|
391
|
+
// Node's 'ieee-p1363' encoding is that form and already refuses any
|
|
392
|
+
// other width (removing the explicit length check changes no test
|
|
393
|
+
// result), so the check below states the requirement instead of
|
|
394
|
+
// inheriting it: a DER or truncated signature must fail here rather
|
|
395
|
+
// than be re-encoded into validity, even under a Node that changes.
|
|
396
|
+
signature.length === 64 &&
|
|
397
|
+
crypto.verify('sha256', signingInput, { key: publicKeyObj, dsaEncoding: 'ieee-p1363' }, signature)
|
|
398
|
+
: crypto.verify(null, signingInput, publicKeyObj, signature);
|
|
296
399
|
}
|
|
297
400
|
catch {
|
|
298
401
|
sigOk = false;
|
|
@@ -334,6 +437,7 @@ export function verifyEd25519Proof(jws, opts, typ) {
|
|
|
334
437
|
valid: true,
|
|
335
438
|
claims,
|
|
336
439
|
publicJwk,
|
|
440
|
+
alg: headerAlg,
|
|
337
441
|
thumbprint: jwkThumbprint(publicJwk),
|
|
338
442
|
};
|
|
339
443
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type DeviceKeyPair, type DeviceKeySigner } from './deviceKey.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where the private half lives.
|
|
4
|
+
*
|
|
5
|
+
* - `software`: an exportable scalar, sealed in a file under the credential
|
|
6
|
+
* envelope. Anything that can read that file and the envelope key owns the
|
|
7
|
+
* device identity.
|
|
8
|
+
* - `hardware`: a non-exportable key inside a Secure Enclave / TPM. A copy of
|
|
9
|
+
* every file on the disk is still not the key.
|
|
10
|
+
*/
|
|
11
|
+
export type DeviceKeyCustody = 'software' | 'hardware';
|
|
12
|
+
export interface DeviceKeyHandle extends DeviceKeySigner {
|
|
13
|
+
readonly custody: DeviceKeyCustody;
|
|
14
|
+
/** RFC 7638 thumbprint of `publicJwk`; the id the server pins. */
|
|
15
|
+
readonly thumbprint: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A handle over a software Ed25519 key. Throws if the pair is internally
|
|
19
|
+
* inconsistent (public and private halves from different keys, or a private
|
|
20
|
+
* JWK Node cannot import) so a corrupted identity fails at load, where the
|
|
21
|
+
* caller already handles it, rather than at the first signature.
|
|
22
|
+
*/
|
|
23
|
+
export declare function softwareEd25519Handle(pair: DeviceKeyPair): DeviceKeyHandle;
|
|
24
|
+
/**
|
|
25
|
+
* A software P-256 handle with a fresh key. NOT a device-key backend: a
|
|
26
|
+
* software P-256 key gives up exactly what P-256 is for, and nothing in the
|
|
27
|
+
* product creates one. It exists so the ES256 wire format (sign, verify, pin)
|
|
28
|
+
* can be exercised end to end without a Secure Enclave, and so a fake hardware
|
|
29
|
+
* helper in a test signs with the real algorithm instead of a stub.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export declare function generateSoftwareP256Handle(): DeviceKeyHandle;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deviceKeyHandle.ts — the one thing the rest of the codebase holds instead of a
|
|
3
|
+
* private key.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Every signing site used to be handed a `DeviceKeyPair` and
|
|
6
|
+
* call `buildProof(kp.privateJwk, kp.publicJwk, …)`. That is correct for a
|
|
7
|
+
* software key and impossible for any other kind: a Secure Enclave or TPM key
|
|
8
|
+
* has no scalar to put in a JWK, and the next kind of key would have meant
|
|
9
|
+
* editing every signing site again. A handle inverts it. The holder asks the
|
|
10
|
+
* key to sign bytes (`sign`) and to describe itself (`alg`, `publicJwk`,
|
|
11
|
+
* `custody`); how the signature is made, and whether the key could be copied
|
|
12
|
+
* out of this machine, is the key's business.
|
|
13
|
+
*
|
|
14
|
+
* The consequence that matters in practice: `privateJwk` is read in exactly two
|
|
15
|
+
* places — the sealed-file store that persists a software key and the function
|
|
16
|
+
* here that turns it into a handle. A handle holds a `KeyObject` in a closure
|
|
17
|
+
* and offers no way to get the scalar back, so no new caller can come to depend
|
|
18
|
+
* on having it. That is the property that makes "the device key is
|
|
19
|
+
* extractable" (docs/architecture/device-key-custody-2026-10-29.md §3.2) a
|
|
20
|
+
* property of ONE backend rather than of the whole design.
|
|
21
|
+
*
|
|
22
|
+
* `custody` is the honest name for that difference, and it is surfaced (status,
|
|
23
|
+
* telemetry, UI) rather than inferred: `software` means a file on this disk can
|
|
24
|
+
* be copied to another machine and used there; `hardware` means it cannot.
|
|
25
|
+
*/
|
|
26
|
+
import crypto from 'node:crypto';
|
|
27
|
+
import { jwkThumbprint, } from './deviceKey.js';
|
|
28
|
+
/**
|
|
29
|
+
* A handle over a software Ed25519 key. Throws if the pair is internally
|
|
30
|
+
* inconsistent (public and private halves from different keys, or a private
|
|
31
|
+
* JWK Node cannot import) so a corrupted identity fails at load, where the
|
|
32
|
+
* caller already handles it, rather than at the first signature.
|
|
33
|
+
*/
|
|
34
|
+
export function softwareEd25519Handle(pair) {
|
|
35
|
+
if (pair.publicJwk.x !== pair.privateJwk.x) {
|
|
36
|
+
// Public and private must come from the same keypair. A mismatch is a
|
|
37
|
+
// partial write, a manual edit, or one file swapped for another.
|
|
38
|
+
throw new Error('softwareEd25519Handle: public/private JWK x mismatch (corrupted keypair)');
|
|
39
|
+
}
|
|
40
|
+
// Cast through `unknown`: Node's `JsonWebKey` wants an index signature our
|
|
41
|
+
// narrow OKP type deliberately does not declare.
|
|
42
|
+
const privateKey = crypto.createPrivateKey({
|
|
43
|
+
format: 'jwk',
|
|
44
|
+
key: pair.privateJwk,
|
|
45
|
+
});
|
|
46
|
+
const publicJwk = pair.publicJwk;
|
|
47
|
+
return {
|
|
48
|
+
alg: 'EdDSA',
|
|
49
|
+
custody: 'software',
|
|
50
|
+
publicJwk,
|
|
51
|
+
thumbprint: jwkThumbprint(publicJwk),
|
|
52
|
+
// Ed25519 hashes the message itself, so Node takes `null` for the digest.
|
|
53
|
+
sign: async (signingInput) => new Uint8Array(crypto.sign(null, Buffer.from(signingInput), privateKey)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A software P-256 handle with a fresh key. NOT a device-key backend: a
|
|
58
|
+
* software P-256 key gives up exactly what P-256 is for, and nothing in the
|
|
59
|
+
* product creates one. It exists so the ES256 wire format (sign, verify, pin)
|
|
60
|
+
* can be exercised end to end without a Secure Enclave, and so a fake hardware
|
|
61
|
+
* helper in a test signs with the real algorithm instead of a stub.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export function generateSoftwareP256Handle() {
|
|
66
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
67
|
+
const jwk = publicKey.export({ format: 'jwk' });
|
|
68
|
+
const publicJwk = { kty: 'EC', crv: 'P-256', x: jwk.x, y: jwk.y };
|
|
69
|
+
return {
|
|
70
|
+
alg: 'ES256',
|
|
71
|
+
custody: 'software',
|
|
72
|
+
publicJwk,
|
|
73
|
+
thumbprint: jwkThumbprint(publicJwk),
|
|
74
|
+
sign: async (signingInput) => new Uint8Array(
|
|
75
|
+
// 'ieee-p1363' is the JWS (r||s) form; the default would be DER.
|
|
76
|
+
crypto.sign('sha256', Buffer.from(signingInput), {
|
|
77
|
+
key: privateKey,
|
|
78
|
+
dsaEncoding: 'ieee-p1363',
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type P256PublicJwk } from './deviceKey.js';
|
|
2
|
+
import type { DeviceKeyHandle } from './deviceKeyHandle.js';
|
|
3
|
+
/** File name of the helper next to the daemon binary and under `<machine root>/bin`. */
|
|
4
|
+
export declare const HARDWARE_KEY_HELPER_NAME = "skrr-native-device-key";
|
|
5
|
+
export declare const HARDWARE_KEY_PROTOCOL = 1;
|
|
6
|
+
export type HardwareDeviceKeyErrorCode = 'helper_missing' | 'helper_failed' | 'timeout' | 'bad_response' | 'key_mismatch' | 'unsupported';
|
|
7
|
+
export declare class HardwareDeviceKeyError extends Error {
|
|
8
|
+
readonly code: HardwareDeviceKeyErrorCode;
|
|
9
|
+
constructor(code: HardwareDeviceKeyErrorCode, message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Tell auth-core where the helper is. The daemon calls this at boot with the
|
|
13
|
+
* path next to its own binary; a process that never does (the CLI, a test)
|
|
14
|
+
* falls back to the places the helper is installed: beside this process's
|
|
15
|
+
* executable, then `<machine root>/bin`. Pass `null` to forget a resolver.
|
|
16
|
+
*/
|
|
17
|
+
export declare function configureHardwareDeviceKeyHelper(resolver: (() => string | null) | null): void;
|
|
18
|
+
/** Absolute path of an executable helper, or null when this machine has none. */
|
|
19
|
+
export declare function resolveHardwareDeviceKeyHelper(): string | null;
|
|
20
|
+
/** @internal Test seam — bound every helper call by `ms`. `null` restores the real limits. */
|
|
21
|
+
export declare function __setHardwareHelperTimeoutForTest(ms: number | null): void;
|
|
22
|
+
export type HardwareKeyCapability = {
|
|
23
|
+
available: true;
|
|
24
|
+
} | {
|
|
25
|
+
available: false;
|
|
26
|
+
reason: 'helper_missing' | 'no_secure_hardware' | 'probe_failed';
|
|
27
|
+
message?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Can this machine create and use a hardware-bound device key right now? Looked
|
|
31
|
+
* up, never assumed: the answer needs both a helper that runs and hardware that
|
|
32
|
+
* answers. A positive result is kept for the life of the process; a negative one
|
|
33
|
+
* is retried after a minute, so a helper installed by an update is picked up
|
|
34
|
+
* without a restart and a broken one is not spawned on every proof.
|
|
35
|
+
*/
|
|
36
|
+
export declare function probeHardwareDeviceKey(): Promise<HardwareKeyCapability>;
|
|
37
|
+
/** @internal Test seam — forget the cached capability. */
|
|
38
|
+
export declare function __resetHardwareProbeForTest(): void;
|
|
39
|
+
/** Contents of `device-key.se`. The blob is opaque and machine-bound; see the file header. */
|
|
40
|
+
export interface HardwareKeyRecord {
|
|
41
|
+
readonly blob: string;
|
|
42
|
+
readonly publicJwk: P256PublicJwk;
|
|
43
|
+
}
|
|
44
|
+
export declare function serializeHardwareKeyRecord(record: HardwareKeyRecord, createdAt?: Date): string;
|
|
45
|
+
/**
|
|
46
|
+
* Parse `device-key.se`. Null for anything that is not a well-formed v1 record
|
|
47
|
+
* whose stored thumbprint matches its own public key: a record is trusted for
|
|
48
|
+
* exactly what it proves about itself, so a half-written or hand-edited file
|
|
49
|
+
* reads as unreadable (which never replaces it) and not as a different key.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseHardwareKeyRecord(text: string): HardwareKeyRecord | null;
|
|
52
|
+
/** A handle whose `sign` asks the helper, so no scalar is ever in this process. */
|
|
53
|
+
export declare function hardwareDeviceKeyHandle(record: HardwareKeyRecord): DeviceKeyHandle;
|
|
54
|
+
/**
|
|
55
|
+
* Create a hardware key and PROVE it before anyone relies on it: sign random
|
|
56
|
+
* bytes and verify them with Node's own crypto against the public key the helper
|
|
57
|
+
* reported. A helper that returns a mis-encoded signature, or a public key that
|
|
58
|
+
* is not the signer's, fails here with nothing yet persisted and nothing yet sent
|
|
59
|
+
* to the server, instead of as a rejected login that leaves a pin half-moved.
|
|
60
|
+
*
|
|
61
|
+
* The returned record is NOT persisted; the caller decides when it becomes the
|
|
62
|
+
* device's identity (see `commitDeviceKeyCandidate`). Dropping it destroys the
|
|
63
|
+
* key: the Secure Enclave keeps nothing the blob does not.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createHardwareDeviceKey(): Promise<{
|
|
66
|
+
record: HardwareKeyRecord;
|
|
67
|
+
handle: DeviceKeyHandle;
|
|
68
|
+
}>;
|