@skrr-ai/cli 0.1.123 → 0.1.124
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/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/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 +25 -7
- package/oclif.manifest.json +1 -1
- 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;
|
|
@@ -28,19 +28,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var deviceKey_exports = {};
|
|
30
30
|
__export(deviceKey_exports, {
|
|
31
|
+
DEVICE_KEY_ALGS: () => DEVICE_KEY_ALGS,
|
|
31
32
|
DEVICE_PROOF_TYP: () => DEVICE_PROOF_TYP,
|
|
33
|
+
algForJwk: () => algForJwk,
|
|
32
34
|
buildProof: () => buildProof,
|
|
33
35
|
generateDeviceKeyPair: () => generateDeviceKeyPair,
|
|
36
|
+
isDevicePublicJwk: () => isDevicePublicJwk,
|
|
34
37
|
jwkThumbprint: () => jwkThumbprint,
|
|
35
38
|
normalizeHtu: () => normalizeHtu,
|
|
39
|
+
p256PublicJwkFromRaw: () => p256PublicJwkFromRaw,
|
|
40
|
+
proofSigningInput: () => proofSigningInput,
|
|
36
41
|
publicJwkFromX: () => publicJwkFromX,
|
|
37
42
|
signEd25519Proof: () => signEd25519Proof,
|
|
43
|
+
signProofWith: () => signProofWith,
|
|
38
44
|
tokenHash: () => tokenHash,
|
|
45
|
+
verifyDevicePublicKeyProof: () => verifyDevicePublicKeyProof,
|
|
39
46
|
verifyEd25519Proof: () => verifyEd25519Proof,
|
|
40
47
|
verifyProof: () => verifyProof
|
|
41
48
|
});
|
|
42
49
|
module.exports = __toCommonJS(deviceKey_exports);
|
|
43
50
|
var import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
51
|
+
const DEVICE_KEY_ALGS = ["ES256", "EdDSA"];
|
|
44
52
|
function b64u(buf) {
|
|
45
53
|
return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
46
54
|
}
|
|
@@ -64,9 +72,32 @@ function generateDeviceKeyPair() {
|
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
function jwkThumbprint(publicJwk) {
|
|
67
|
-
const canonical = `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}"}`;
|
|
75
|
+
const canonical = publicJwk.kty === "EC" ? `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}","y":"${publicJwk.y}"}` : `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}"}`;
|
|
68
76
|
return b64u(import_node_crypto.default.createHash("sha256").update(canonical, "utf-8").digest());
|
|
69
77
|
}
|
|
78
|
+
const B64URL_32 = /^[A-Za-z0-9_-]{43}$/;
|
|
79
|
+
function isDevicePublicJwk(value) {
|
|
80
|
+
if (!value || typeof value !== "object")
|
|
81
|
+
return false;
|
|
82
|
+
const jwk = value;
|
|
83
|
+
if (jwk.kty === "OKP") {
|
|
84
|
+
return jwk.crv === "Ed25519" && typeof jwk.x === "string" && B64URL_32.test(jwk.x);
|
|
85
|
+
}
|
|
86
|
+
if (jwk.kty === "EC") {
|
|
87
|
+
return jwk.crv === "P-256" && typeof jwk.x === "string" && typeof jwk.y === "string" && B64URL_32.test(jwk.x) && B64URL_32.test(jwk.y);
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
function algForJwk(jwk) {
|
|
92
|
+
return jwk.kty === "EC" ? "ES256" : "EdDSA";
|
|
93
|
+
}
|
|
94
|
+
function p256PublicJwkFromRaw(raw) {
|
|
95
|
+
if (raw.length !== 64) {
|
|
96
|
+
throw new Error(`p256PublicJwkFromRaw: expected 64 bytes (x||y), got ${raw.length}`);
|
|
97
|
+
}
|
|
98
|
+
const buf = Buffer.from(raw);
|
|
99
|
+
return { kty: "EC", crv: "P-256", x: b64u(buf.subarray(0, 32)), y: b64u(buf.subarray(32)) };
|
|
100
|
+
}
|
|
70
101
|
function publicJwkFromX(x) {
|
|
71
102
|
const raw = b64uDecode(x);
|
|
72
103
|
if (raw.length !== 32) {
|
|
@@ -100,12 +131,20 @@ function buildProof(privateJwk, publicJwk, opts) {
|
|
|
100
131
|
}
|
|
101
132
|
const DEVICE_PROOF_TYP = "oversky-dpop+jwt";
|
|
102
133
|
function signEd25519Proof(privateKey, publicJwk, typ, opts) {
|
|
134
|
+
const signingInput = proofSigningInput("EdDSA", publicJwk, typ, opts);
|
|
135
|
+
const signature = import_node_crypto.default.sign(null, Buffer.from(signingInput, "utf-8"), privateKey);
|
|
136
|
+
return `${signingInput}.${b64u(signature)}`;
|
|
137
|
+
}
|
|
138
|
+
function proofSigningInput(alg, publicJwk, typ, opts) {
|
|
103
139
|
if (typeof opts.method !== "string" || opts.method.length === 0) {
|
|
104
140
|
throw new Error("buildProof: method must be a non-empty string");
|
|
105
141
|
}
|
|
106
142
|
if (typeof opts.url !== "string" || opts.url.length === 0) {
|
|
107
143
|
throw new Error("buildProof: url must be a non-empty string");
|
|
108
144
|
}
|
|
145
|
+
if (algForJwk(publicJwk) !== alg) {
|
|
146
|
+
throw new Error(`buildProof: alg ${alg} does not match a ${publicJwk.kty} key`);
|
|
147
|
+
}
|
|
109
148
|
const claims = {
|
|
110
149
|
htm: opts.method.toUpperCase(),
|
|
111
150
|
htu: normalizeHtu(opts.url),
|
|
@@ -116,17 +155,23 @@ function signEd25519Proof(privateKey, publicJwk, typ, opts) {
|
|
|
116
155
|
claims.rth = tokenHash(opts.refreshToken);
|
|
117
156
|
if (opts.accessToken !== void 0)
|
|
118
157
|
claims.ath = tokenHash(opts.accessToken);
|
|
119
|
-
const header = { alg
|
|
158
|
+
const header = { alg, typ, jwk: publicJwk };
|
|
120
159
|
const headerB64 = b64u(Buffer.from(JSON.stringify(header), "utf-8"));
|
|
121
160
|
const payloadB64 = b64u(Buffer.from(JSON.stringify(claims), "utf-8"));
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
161
|
+
return `${headerB64}.${payloadB64}`;
|
|
162
|
+
}
|
|
163
|
+
async function signProofWith(signer, opts, typ = DEVICE_PROOF_TYP) {
|
|
164
|
+
const signingInput = proofSigningInput(signer.alg, signer.publicJwk, typ, opts);
|
|
165
|
+
const signature = await signer.sign(Buffer.from(signingInput, "utf-8"));
|
|
166
|
+
return `${signingInput}.${b64u(Buffer.from(signature))}`;
|
|
125
167
|
}
|
|
126
168
|
function verifyProof(jws, opts = {}) {
|
|
127
|
-
return
|
|
169
|
+
return verifyDevicePublicKeyProof(jws, opts, DEVICE_PROOF_TYP);
|
|
128
170
|
}
|
|
129
171
|
function verifyEd25519Proof(jws, opts, typ) {
|
|
172
|
+
return verifyDevicePublicKeyProof(jws, { ...opts, allowedAlgs: ["EdDSA"] }, typ);
|
|
173
|
+
}
|
|
174
|
+
function verifyDevicePublicKeyProof(jws, opts, typ) {
|
|
130
175
|
if (typeof jws !== "string" || jws.length === 0) {
|
|
131
176
|
return { valid: false, reason: "malformed" };
|
|
132
177
|
}
|
|
@@ -143,27 +188,24 @@ function verifyEd25519Proof(jws, opts, typ) {
|
|
|
143
188
|
} catch {
|
|
144
189
|
return { valid: false, reason: "malformed" };
|
|
145
190
|
}
|
|
146
|
-
|
|
191
|
+
const allowedAlgs = opts.allowedAlgs ?? ["EdDSA"];
|
|
192
|
+
const headerAlg = header && typeof header === "object" ? header.alg : void 0;
|
|
193
|
+
if (headerAlg !== "EdDSA" && headerAlg !== "ES256" || !allowedAlgs.includes(headerAlg)) {
|
|
147
194
|
return { valid: false, reason: "bad_alg" };
|
|
148
195
|
}
|
|
149
196
|
if (header.typ !== typ) {
|
|
150
197
|
return { valid: false, reason: "bad_typ" };
|
|
151
198
|
}
|
|
152
199
|
const headerJwk = header.jwk;
|
|
153
|
-
if (!headerJwk
|
|
200
|
+
if (!isDevicePublicJwk(headerJwk)) {
|
|
154
201
|
return { valid: false, reason: "bad_jwk" };
|
|
155
202
|
}
|
|
156
|
-
const publicJwk = {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
};
|
|
203
|
+
const publicJwk = headerJwk.kty === "EC" ? { kty: "EC", crv: "P-256", x: headerJwk.x, y: headerJwk.y } : { kty: "OKP", crv: "Ed25519", x: headerJwk.x };
|
|
204
|
+
if (algForJwk(publicJwk) !== headerAlg) {
|
|
205
|
+
return { valid: false, reason: "bad_alg" };
|
|
206
|
+
}
|
|
161
207
|
let publicKeyObj;
|
|
162
208
|
try {
|
|
163
|
-
const raw = b64uDecode(publicJwk.x);
|
|
164
|
-
if (raw.length !== 32) {
|
|
165
|
-
return { valid: false, reason: "bad_jwk" };
|
|
166
|
-
}
|
|
167
209
|
publicKeyObj = import_node_crypto.default.createPublicKey({
|
|
168
210
|
format: "jwk",
|
|
169
211
|
key: publicJwk
|
|
@@ -171,7 +213,7 @@ function verifyEd25519Proof(jws, opts, typ) {
|
|
|
171
213
|
} catch {
|
|
172
214
|
return { valid: false, reason: "bad_jwk" };
|
|
173
215
|
}
|
|
174
|
-
if (opts.pinnedPublicJwk && opts.pinnedPublicJwk
|
|
216
|
+
if (opts.pinnedPublicJwk && jwkThumbprint(opts.pinnedPublicJwk) !== jwkThumbprint(publicJwk)) {
|
|
175
217
|
return { valid: false, reason: "pin_mismatch" };
|
|
176
218
|
}
|
|
177
219
|
const signingInput = Buffer.from(`${headerB64}.${payloadB64}`, "utf-8");
|
|
@@ -183,7 +225,15 @@ function verifyEd25519Proof(jws, opts, typ) {
|
|
|
183
225
|
}
|
|
184
226
|
let sigOk;
|
|
185
227
|
try {
|
|
186
|
-
sigOk =
|
|
228
|
+
sigOk = headerAlg === "ES256" ? (
|
|
229
|
+
// JWS ES256 is the fixed-width r||s form, never DER (RFC 7518 §3.4).
|
|
230
|
+
// Node's 'ieee-p1363' encoding is that form and already refuses any
|
|
231
|
+
// other width (removing the explicit length check changes no test
|
|
232
|
+
// result), so the check below states the requirement instead of
|
|
233
|
+
// inheriting it: a DER or truncated signature must fail here rather
|
|
234
|
+
// than be re-encoded into validity, even under a Node that changes.
|
|
235
|
+
signature.length === 64 && import_node_crypto.default.verify("sha256", signingInput, { key: publicKeyObj, dsaEncoding: "ieee-p1363" }, signature)
|
|
236
|
+
) : import_node_crypto.default.verify(null, signingInput, publicKeyObj, signature);
|
|
187
237
|
} catch {
|
|
188
238
|
sigOk = false;
|
|
189
239
|
}
|
|
@@ -221,19 +271,27 @@ function verifyEd25519Proof(jws, opts, typ) {
|
|
|
221
271
|
valid: true,
|
|
222
272
|
claims,
|
|
223
273
|
publicJwk,
|
|
274
|
+
alg: headerAlg,
|
|
224
275
|
thumbprint: jwkThumbprint(publicJwk)
|
|
225
276
|
};
|
|
226
277
|
}
|
|
227
278
|
// Annotate the CommonJS export names for ESM import in node:
|
|
228
279
|
0 && (module.exports = {
|
|
280
|
+
DEVICE_KEY_ALGS,
|
|
229
281
|
DEVICE_PROOF_TYP,
|
|
282
|
+
algForJwk,
|
|
230
283
|
buildProof,
|
|
231
284
|
generateDeviceKeyPair,
|
|
285
|
+
isDevicePublicJwk,
|
|
232
286
|
jwkThumbprint,
|
|
233
287
|
normalizeHtu,
|
|
288
|
+
p256PublicJwkFromRaw,
|
|
289
|
+
proofSigningInput,
|
|
234
290
|
publicJwkFromX,
|
|
235
291
|
signEd25519Proof,
|
|
292
|
+
signProofWith,
|
|
236
293
|
tokenHash,
|
|
294
|
+
verifyDevicePublicKeyProof,
|
|
237
295
|
verifyEd25519Proof,
|
|
238
296
|
verifyProof
|
|
239
297
|
});
|
|
@@ -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,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var deviceKeyHandle_exports = {};
|
|
30
|
+
__export(deviceKeyHandle_exports, {
|
|
31
|
+
generateSoftwareP256Handle: () => generateSoftwareP256Handle,
|
|
32
|
+
softwareEd25519Handle: () => softwareEd25519Handle
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(deviceKeyHandle_exports);
|
|
35
|
+
var import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
36
|
+
var import_deviceKey = require("./deviceKey.js");
|
|
37
|
+
function softwareEd25519Handle(pair) {
|
|
38
|
+
if (pair.publicJwk.x !== pair.privateJwk.x) {
|
|
39
|
+
throw new Error("softwareEd25519Handle: public/private JWK x mismatch (corrupted keypair)");
|
|
40
|
+
}
|
|
41
|
+
const privateKey = import_node_crypto.default.createPrivateKey({
|
|
42
|
+
format: "jwk",
|
|
43
|
+
key: pair.privateJwk
|
|
44
|
+
});
|
|
45
|
+
const publicJwk = pair.publicJwk;
|
|
46
|
+
return {
|
|
47
|
+
alg: "EdDSA",
|
|
48
|
+
custody: "software",
|
|
49
|
+
publicJwk,
|
|
50
|
+
thumbprint: (0, import_deviceKey.jwkThumbprint)(publicJwk),
|
|
51
|
+
// Ed25519 hashes the message itself, so Node takes `null` for the digest.
|
|
52
|
+
sign: async (signingInput) => new Uint8Array(import_node_crypto.default.sign(null, Buffer.from(signingInput), privateKey))
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function generateSoftwareP256Handle() {
|
|
56
|
+
const { privateKey, publicKey } = import_node_crypto.default.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
57
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
58
|
+
const publicJwk = { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
|
|
59
|
+
return {
|
|
60
|
+
alg: "ES256",
|
|
61
|
+
custody: "software",
|
|
62
|
+
publicJwk,
|
|
63
|
+
thumbprint: (0, import_deviceKey.jwkThumbprint)(publicJwk),
|
|
64
|
+
sign: async (signingInput) => new Uint8Array(
|
|
65
|
+
// 'ieee-p1363' is the JWS (r||s) form; the default would be DER.
|
|
66
|
+
import_node_crypto.default.sign("sha256", Buffer.from(signingInput), {
|
|
67
|
+
key: privateKey,
|
|
68
|
+
dsaEncoding: "ieee-p1363"
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
74
|
+
0 && (module.exports = {
|
|
75
|
+
generateSoftwareP256Handle,
|
|
76
|
+
softwareEd25519Handle
|
|
77
|
+
});
|
|
@@ -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
|
+
}>;
|