@dexterai/vault 0.1.3 → 0.2.1
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.
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/signers/browser/index.ts
|
|
21
|
+
var browser_exports = {};
|
|
22
|
+
__export(browser_exports, {
|
|
23
|
+
WebAuthnAssertion: () => WebAuthnAssertion,
|
|
24
|
+
WebAuthnAssertionError: () => WebAuthnAssertionError,
|
|
25
|
+
derSignatureToCompactLowS: () => derSignatureToCompactLowS
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(browser_exports);
|
|
28
|
+
var WebAuthnAssertionError = class extends Error {
|
|
29
|
+
code;
|
|
30
|
+
constructor(code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.name = "WebAuthnAssertionError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var WebAuthnAssertion = class {
|
|
37
|
+
credentialId;
|
|
38
|
+
publicKeyBase64;
|
|
39
|
+
rpId;
|
|
40
|
+
allowCredentials;
|
|
41
|
+
timeoutMs;
|
|
42
|
+
userVerification;
|
|
43
|
+
constructor(config) {
|
|
44
|
+
if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
|
|
45
|
+
throw new WebAuthnAssertionError(
|
|
46
|
+
"invalid_credential_id",
|
|
47
|
+
"credentialId must be a non-empty Uint8Array"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
this.credentialId = config.credentialId;
|
|
51
|
+
this.publicKeyBase64 = config.publicKeyBase64;
|
|
52
|
+
this.rpId = config.rpId;
|
|
53
|
+
this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
|
|
54
|
+
this.timeoutMs = config.timeoutMs ?? 6e4;
|
|
55
|
+
this.userVerification = config.userVerification ?? "preferred";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
59
|
+
* three on-chain-ready buffers.
|
|
60
|
+
*
|
|
61
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
62
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
63
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
64
|
+
* does not impose policy here.
|
|
65
|
+
*/
|
|
66
|
+
async assertOver(challenge) {
|
|
67
|
+
ensureBrowser();
|
|
68
|
+
if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
|
|
69
|
+
throw new WebAuthnAssertionError(
|
|
70
|
+
"invalid_challenge",
|
|
71
|
+
"challenge must be a non-empty Uint8Array"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const requestOptions = {
|
|
75
|
+
challenge: toBufferSource(challenge),
|
|
76
|
+
allowCredentials: this.allowCredentials.map((c) => ({
|
|
77
|
+
id: toBufferSource(c.id),
|
|
78
|
+
type: "public-key",
|
|
79
|
+
transports: c.transports
|
|
80
|
+
})),
|
|
81
|
+
timeout: this.timeoutMs,
|
|
82
|
+
userVerification: this.userVerification,
|
|
83
|
+
...this.rpId ? { rpId: this.rpId } : {}
|
|
84
|
+
};
|
|
85
|
+
const credential = await navigator.credentials.get({
|
|
86
|
+
publicKey: requestOptions
|
|
87
|
+
});
|
|
88
|
+
if (!credential) {
|
|
89
|
+
throw new WebAuthnAssertionError(
|
|
90
|
+
"user_cancelled",
|
|
91
|
+
"no assertion returned from authenticator"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (credential.type !== "public-key") {
|
|
95
|
+
throw new WebAuthnAssertionError(
|
|
96
|
+
"credential_invalid",
|
|
97
|
+
`unexpected credential type: ${credential.type}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const assertion = credential.response;
|
|
101
|
+
const derSignature = new Uint8Array(assertion.signature);
|
|
102
|
+
const compactSignature = derSignatureToCompactLowS(derSignature);
|
|
103
|
+
return {
|
|
104
|
+
signature: compactSignature,
|
|
105
|
+
signatureDer: derSignature,
|
|
106
|
+
clientDataJSON: new Uint8Array(assertion.clientDataJSON),
|
|
107
|
+
authenticatorData: new Uint8Array(assertion.authenticatorData)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
112
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
113
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
114
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
115
|
+
* function, two names.
|
|
116
|
+
*/
|
|
117
|
+
sign(challenge) {
|
|
118
|
+
return this.assertOver(challenge);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function ensureBrowser() {
|
|
122
|
+
if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
|
|
123
|
+
throw new WebAuthnAssertionError(
|
|
124
|
+
"not_browser",
|
|
125
|
+
"WebAuthnAssertion requires a browser environment (navigator.credentials)"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const cred = globalThis.navigator.credentials;
|
|
129
|
+
if (!cred || typeof cred.get !== "function") {
|
|
130
|
+
throw new WebAuthnAssertionError(
|
|
131
|
+
"webauthn_unsupported",
|
|
132
|
+
"this environment does not implement navigator.credentials.get"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function toBufferSource(bytes) {
|
|
137
|
+
const out = new ArrayBuffer(bytes.length);
|
|
138
|
+
new Uint8Array(out).set(bytes);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
var P256_ORDER = BigInt(
|
|
142
|
+
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
|
|
143
|
+
);
|
|
144
|
+
var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
|
|
145
|
+
function bigintFromBytes(buf) {
|
|
146
|
+
let n = 0n;
|
|
147
|
+
for (const byte of buf) n = n << 8n | BigInt(byte);
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
function bytesFromBigint(n, length) {
|
|
151
|
+
const out = new Uint8Array(length);
|
|
152
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
153
|
+
out[i] = Number(n & 0xffn);
|
|
154
|
+
n >>= 8n;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function derSignatureToCompactLowS(der) {
|
|
159
|
+
let i = 0;
|
|
160
|
+
if (der[i++] !== 48) {
|
|
161
|
+
throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
|
|
162
|
+
}
|
|
163
|
+
i++;
|
|
164
|
+
if (der[i++] !== 2) {
|
|
165
|
+
throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
|
|
166
|
+
}
|
|
167
|
+
const rLen = der[i++];
|
|
168
|
+
if (rLen === void 0) {
|
|
169
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
|
|
170
|
+
}
|
|
171
|
+
let r = der.slice(i, i + rLen);
|
|
172
|
+
i += rLen;
|
|
173
|
+
if (der[i++] !== 2) {
|
|
174
|
+
throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
|
|
175
|
+
}
|
|
176
|
+
const sLen = der[i++];
|
|
177
|
+
if (sLen === void 0) {
|
|
178
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
|
|
179
|
+
}
|
|
180
|
+
let s = der.slice(i, i + sLen);
|
|
181
|
+
i += sLen;
|
|
182
|
+
if (r.length > 32 && r[0] === 0) r = r.slice(1);
|
|
183
|
+
if (s.length > 32 && s[0] === 0) s = s.slice(1);
|
|
184
|
+
if (r.length > 32 || s.length > 32) {
|
|
185
|
+
throw new WebAuthnAssertionError(
|
|
186
|
+
"bad_signature",
|
|
187
|
+
"DER component too large for P-256"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const rPadded = new Uint8Array(32);
|
|
191
|
+
rPadded.set(r, 32 - r.length);
|
|
192
|
+
let sN = bigintFromBytes(s);
|
|
193
|
+
if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
|
|
194
|
+
const sPadded = bytesFromBigint(sN, 32);
|
|
195
|
+
const out = new Uint8Array(64);
|
|
196
|
+
out.set(rPadded, 0);
|
|
197
|
+
out.set(sPadded, 32);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
201
|
+
0 && (module.exports = {
|
|
202
|
+
WebAuthnAssertion,
|
|
203
|
+
WebAuthnAssertionError,
|
|
204
|
+
derSignatureToCompactLowS
|
|
205
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PasskeySigner } from '../types.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebAuthnAssertion — pure-browser P-256 passkey ceremony.
|
|
5
|
+
*
|
|
6
|
+
* Runs `navigator.credentials.get()` over a server-issued challenge and
|
|
7
|
+
* returns the three bytes the on-chain secp256r1 precompile + vault
|
|
8
|
+
* program need:
|
|
9
|
+
*
|
|
10
|
+
* - signature (64-byte compact r||s with low-S enforcement)
|
|
11
|
+
* - clientDataJSON (raw, what the authenticator hashed)
|
|
12
|
+
* - authenticatorData (raw, what the authenticator signed)
|
|
13
|
+
*
|
|
14
|
+
* Zero `fetch` calls. The consumer composes this with whatever server
|
|
15
|
+
* policy they enforce (replay defense, signature counter, AAGUID
|
|
16
|
+
* capture). For Dexter that policy lives in dexter-fe's
|
|
17
|
+
* `DexterApiBrowserPasskeySigner` adapter.
|
|
18
|
+
*
|
|
19
|
+
* The DER → compact lowS conversion is the canonical implementation —
|
|
20
|
+
* it lifts verbatim from dexter-fe/app/lib/passkey.ts (which had it
|
|
21
|
+
* duplicated in passkey-anon.ts). After v0.2 lands and dexter-fe
|
|
22
|
+
* swaps, those two copies go away.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface WebAuthnAssertionConfig {
|
|
26
|
+
/** Raw credential ID bytes (NOT base64-encoded). */
|
|
27
|
+
credentialId: Uint8Array;
|
|
28
|
+
/** 33-byte SEC1 compressed P-256 pubkey, base64. Kept for symmetry / future use; not consumed by `assertOver`. */
|
|
29
|
+
publicKeyBase64?: string;
|
|
30
|
+
/** WebAuthn relying-party identifier. Defaults to omitting the field (browser uses the page's RP ID). */
|
|
31
|
+
rpId?: string;
|
|
32
|
+
/** Optional allow-list. Default: just `credentialId`. */
|
|
33
|
+
allowCredentials?: Array<{
|
|
34
|
+
id: Uint8Array;
|
|
35
|
+
transports?: AuthenticatorTransport[];
|
|
36
|
+
}>;
|
|
37
|
+
/** WebAuthn timeout in milliseconds. Default 60_000. */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** UV requirement. Default "preferred". */
|
|
40
|
+
userVerification?: UserVerificationRequirement;
|
|
41
|
+
}
|
|
42
|
+
interface WebAuthnAssertionResult {
|
|
43
|
+
/** 64-byte compact r||s P-256 signature, lowS-normalized (SIMD-0075 requires lowS). */
|
|
44
|
+
signature: Uint8Array;
|
|
45
|
+
/**
|
|
46
|
+
* Raw DER-encoded ECDSA signature as returned by the authenticator,
|
|
47
|
+
* BEFORE the compact-lowS conversion. Kept so consumers that need to
|
|
48
|
+
* forward the assertion to a WebAuthn server library (which expects
|
|
49
|
+
* DER) don't have to re-run the ceremony. The on-chain bytes are
|
|
50
|
+
* `signature` (compact); DER is for server-side verify legs.
|
|
51
|
+
*/
|
|
52
|
+
signatureDer: Uint8Array;
|
|
53
|
+
/** Raw clientDataJSON as returned by the authenticator. */
|
|
54
|
+
clientDataJSON: Uint8Array;
|
|
55
|
+
/** Raw authenticatorData as returned by the authenticator. */
|
|
56
|
+
authenticatorData: Uint8Array;
|
|
57
|
+
}
|
|
58
|
+
declare class WebAuthnAssertionError extends Error {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
constructor(code: string, message: string);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pure-browser WebAuthn assertion driver. Implements `PasskeySigner` so
|
|
64
|
+
* adapters that compose with server policy can plug straight in.
|
|
65
|
+
*/
|
|
66
|
+
declare class WebAuthnAssertion implements PasskeySigner {
|
|
67
|
+
readonly credentialId: Uint8Array;
|
|
68
|
+
readonly publicKeyBase64?: string;
|
|
69
|
+
private readonly rpId?;
|
|
70
|
+
private readonly allowCredentials;
|
|
71
|
+
private readonly timeoutMs;
|
|
72
|
+
private readonly userVerification;
|
|
73
|
+
constructor(config: WebAuthnAssertionConfig);
|
|
74
|
+
/**
|
|
75
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
76
|
+
* three on-chain-ready buffers.
|
|
77
|
+
*
|
|
78
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
79
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
80
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
81
|
+
* does not impose policy here.
|
|
82
|
+
*/
|
|
83
|
+
assertOver(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
|
|
84
|
+
/**
|
|
85
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
86
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
87
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
88
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
89
|
+
* function, two names.
|
|
90
|
+
*/
|
|
91
|
+
sign(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse an ASN.1 DER ECDSA signature and return the 64-byte (r||s) form
|
|
95
|
+
* with s normalized to lowS (s ≤ n/2). SIMD-0075 rejects high-S
|
|
96
|
+
* signatures to prevent malleability replay.
|
|
97
|
+
*
|
|
98
|
+
* Exported so byte-parity tests can lock the conversion against the
|
|
99
|
+
* dexter-fe implementation it replaces.
|
|
100
|
+
*/
|
|
101
|
+
declare function derSignatureToCompactLowS(der: Uint8Array): Uint8Array;
|
|
102
|
+
|
|
103
|
+
export { WebAuthnAssertion, type WebAuthnAssertionConfig, WebAuthnAssertionError, type WebAuthnAssertionResult, derSignatureToCompactLowS };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PasskeySigner } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebAuthnAssertion — pure-browser P-256 passkey ceremony.
|
|
5
|
+
*
|
|
6
|
+
* Runs `navigator.credentials.get()` over a server-issued challenge and
|
|
7
|
+
* returns the three bytes the on-chain secp256r1 precompile + vault
|
|
8
|
+
* program need:
|
|
9
|
+
*
|
|
10
|
+
* - signature (64-byte compact r||s with low-S enforcement)
|
|
11
|
+
* - clientDataJSON (raw, what the authenticator hashed)
|
|
12
|
+
* - authenticatorData (raw, what the authenticator signed)
|
|
13
|
+
*
|
|
14
|
+
* Zero `fetch` calls. The consumer composes this with whatever server
|
|
15
|
+
* policy they enforce (replay defense, signature counter, AAGUID
|
|
16
|
+
* capture). For Dexter that policy lives in dexter-fe's
|
|
17
|
+
* `DexterApiBrowserPasskeySigner` adapter.
|
|
18
|
+
*
|
|
19
|
+
* The DER → compact lowS conversion is the canonical implementation —
|
|
20
|
+
* it lifts verbatim from dexter-fe/app/lib/passkey.ts (which had it
|
|
21
|
+
* duplicated in passkey-anon.ts). After v0.2 lands and dexter-fe
|
|
22
|
+
* swaps, those two copies go away.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface WebAuthnAssertionConfig {
|
|
26
|
+
/** Raw credential ID bytes (NOT base64-encoded). */
|
|
27
|
+
credentialId: Uint8Array;
|
|
28
|
+
/** 33-byte SEC1 compressed P-256 pubkey, base64. Kept for symmetry / future use; not consumed by `assertOver`. */
|
|
29
|
+
publicKeyBase64?: string;
|
|
30
|
+
/** WebAuthn relying-party identifier. Defaults to omitting the field (browser uses the page's RP ID). */
|
|
31
|
+
rpId?: string;
|
|
32
|
+
/** Optional allow-list. Default: just `credentialId`. */
|
|
33
|
+
allowCredentials?: Array<{
|
|
34
|
+
id: Uint8Array;
|
|
35
|
+
transports?: AuthenticatorTransport[];
|
|
36
|
+
}>;
|
|
37
|
+
/** WebAuthn timeout in milliseconds. Default 60_000. */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** UV requirement. Default "preferred". */
|
|
40
|
+
userVerification?: UserVerificationRequirement;
|
|
41
|
+
}
|
|
42
|
+
interface WebAuthnAssertionResult {
|
|
43
|
+
/** 64-byte compact r||s P-256 signature, lowS-normalized (SIMD-0075 requires lowS). */
|
|
44
|
+
signature: Uint8Array;
|
|
45
|
+
/**
|
|
46
|
+
* Raw DER-encoded ECDSA signature as returned by the authenticator,
|
|
47
|
+
* BEFORE the compact-lowS conversion. Kept so consumers that need to
|
|
48
|
+
* forward the assertion to a WebAuthn server library (which expects
|
|
49
|
+
* DER) don't have to re-run the ceremony. The on-chain bytes are
|
|
50
|
+
* `signature` (compact); DER is for server-side verify legs.
|
|
51
|
+
*/
|
|
52
|
+
signatureDer: Uint8Array;
|
|
53
|
+
/** Raw clientDataJSON as returned by the authenticator. */
|
|
54
|
+
clientDataJSON: Uint8Array;
|
|
55
|
+
/** Raw authenticatorData as returned by the authenticator. */
|
|
56
|
+
authenticatorData: Uint8Array;
|
|
57
|
+
}
|
|
58
|
+
declare class WebAuthnAssertionError extends Error {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
constructor(code: string, message: string);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pure-browser WebAuthn assertion driver. Implements `PasskeySigner` so
|
|
64
|
+
* adapters that compose with server policy can plug straight in.
|
|
65
|
+
*/
|
|
66
|
+
declare class WebAuthnAssertion implements PasskeySigner {
|
|
67
|
+
readonly credentialId: Uint8Array;
|
|
68
|
+
readonly publicKeyBase64?: string;
|
|
69
|
+
private readonly rpId?;
|
|
70
|
+
private readonly allowCredentials;
|
|
71
|
+
private readonly timeoutMs;
|
|
72
|
+
private readonly userVerification;
|
|
73
|
+
constructor(config: WebAuthnAssertionConfig);
|
|
74
|
+
/**
|
|
75
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
76
|
+
* three on-chain-ready buffers.
|
|
77
|
+
*
|
|
78
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
79
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
80
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
81
|
+
* does not impose policy here.
|
|
82
|
+
*/
|
|
83
|
+
assertOver(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
|
|
84
|
+
/**
|
|
85
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
86
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
87
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
88
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
89
|
+
* function, two names.
|
|
90
|
+
*/
|
|
91
|
+
sign(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse an ASN.1 DER ECDSA signature and return the 64-byte (r||s) form
|
|
95
|
+
* with s normalized to lowS (s ≤ n/2). SIMD-0075 rejects high-S
|
|
96
|
+
* signatures to prevent malleability replay.
|
|
97
|
+
*
|
|
98
|
+
* Exported so byte-parity tests can lock the conversion against the
|
|
99
|
+
* dexter-fe implementation it replaces.
|
|
100
|
+
*/
|
|
101
|
+
declare function derSignatureToCompactLowS(der: Uint8Array): Uint8Array;
|
|
102
|
+
|
|
103
|
+
export { WebAuthnAssertion, type WebAuthnAssertionConfig, WebAuthnAssertionError, type WebAuthnAssertionResult, derSignatureToCompactLowS };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/signers/browser/index.ts
|
|
2
|
+
var WebAuthnAssertionError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "WebAuthnAssertionError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var WebAuthnAssertion = class {
|
|
11
|
+
credentialId;
|
|
12
|
+
publicKeyBase64;
|
|
13
|
+
rpId;
|
|
14
|
+
allowCredentials;
|
|
15
|
+
timeoutMs;
|
|
16
|
+
userVerification;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
|
|
19
|
+
throw new WebAuthnAssertionError(
|
|
20
|
+
"invalid_credential_id",
|
|
21
|
+
"credentialId must be a non-empty Uint8Array"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
this.credentialId = config.credentialId;
|
|
25
|
+
this.publicKeyBase64 = config.publicKeyBase64;
|
|
26
|
+
this.rpId = config.rpId;
|
|
27
|
+
this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
|
|
28
|
+
this.timeoutMs = config.timeoutMs ?? 6e4;
|
|
29
|
+
this.userVerification = config.userVerification ?? "preferred";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
33
|
+
* three on-chain-ready buffers.
|
|
34
|
+
*
|
|
35
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
36
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
37
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
38
|
+
* does not impose policy here.
|
|
39
|
+
*/
|
|
40
|
+
async assertOver(challenge) {
|
|
41
|
+
ensureBrowser();
|
|
42
|
+
if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
|
|
43
|
+
throw new WebAuthnAssertionError(
|
|
44
|
+
"invalid_challenge",
|
|
45
|
+
"challenge must be a non-empty Uint8Array"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const requestOptions = {
|
|
49
|
+
challenge: toBufferSource(challenge),
|
|
50
|
+
allowCredentials: this.allowCredentials.map((c) => ({
|
|
51
|
+
id: toBufferSource(c.id),
|
|
52
|
+
type: "public-key",
|
|
53
|
+
transports: c.transports
|
|
54
|
+
})),
|
|
55
|
+
timeout: this.timeoutMs,
|
|
56
|
+
userVerification: this.userVerification,
|
|
57
|
+
...this.rpId ? { rpId: this.rpId } : {}
|
|
58
|
+
};
|
|
59
|
+
const credential = await navigator.credentials.get({
|
|
60
|
+
publicKey: requestOptions
|
|
61
|
+
});
|
|
62
|
+
if (!credential) {
|
|
63
|
+
throw new WebAuthnAssertionError(
|
|
64
|
+
"user_cancelled",
|
|
65
|
+
"no assertion returned from authenticator"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (credential.type !== "public-key") {
|
|
69
|
+
throw new WebAuthnAssertionError(
|
|
70
|
+
"credential_invalid",
|
|
71
|
+
`unexpected credential type: ${credential.type}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const assertion = credential.response;
|
|
75
|
+
const derSignature = new Uint8Array(assertion.signature);
|
|
76
|
+
const compactSignature = derSignatureToCompactLowS(derSignature);
|
|
77
|
+
return {
|
|
78
|
+
signature: compactSignature,
|
|
79
|
+
signatureDer: derSignature,
|
|
80
|
+
clientDataJSON: new Uint8Array(assertion.clientDataJSON),
|
|
81
|
+
authenticatorData: new Uint8Array(assertion.authenticatorData)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
86
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
87
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
88
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
89
|
+
* function, two names.
|
|
90
|
+
*/
|
|
91
|
+
sign(challenge) {
|
|
92
|
+
return this.assertOver(challenge);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function ensureBrowser() {
|
|
96
|
+
if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
|
|
97
|
+
throw new WebAuthnAssertionError(
|
|
98
|
+
"not_browser",
|
|
99
|
+
"WebAuthnAssertion requires a browser environment (navigator.credentials)"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const cred = globalThis.navigator.credentials;
|
|
103
|
+
if (!cred || typeof cred.get !== "function") {
|
|
104
|
+
throw new WebAuthnAssertionError(
|
|
105
|
+
"webauthn_unsupported",
|
|
106
|
+
"this environment does not implement navigator.credentials.get"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function toBufferSource(bytes) {
|
|
111
|
+
const out = new ArrayBuffer(bytes.length);
|
|
112
|
+
new Uint8Array(out).set(bytes);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
var P256_ORDER = BigInt(
|
|
116
|
+
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
|
|
117
|
+
);
|
|
118
|
+
var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
|
|
119
|
+
function bigintFromBytes(buf) {
|
|
120
|
+
let n = 0n;
|
|
121
|
+
for (const byte of buf) n = n << 8n | BigInt(byte);
|
|
122
|
+
return n;
|
|
123
|
+
}
|
|
124
|
+
function bytesFromBigint(n, length) {
|
|
125
|
+
const out = new Uint8Array(length);
|
|
126
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
127
|
+
out[i] = Number(n & 0xffn);
|
|
128
|
+
n >>= 8n;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function derSignatureToCompactLowS(der) {
|
|
133
|
+
let i = 0;
|
|
134
|
+
if (der[i++] !== 48) {
|
|
135
|
+
throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
|
|
136
|
+
}
|
|
137
|
+
i++;
|
|
138
|
+
if (der[i++] !== 2) {
|
|
139
|
+
throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
|
|
140
|
+
}
|
|
141
|
+
const rLen = der[i++];
|
|
142
|
+
if (rLen === void 0) {
|
|
143
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
|
|
144
|
+
}
|
|
145
|
+
let r = der.slice(i, i + rLen);
|
|
146
|
+
i += rLen;
|
|
147
|
+
if (der[i++] !== 2) {
|
|
148
|
+
throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
|
|
149
|
+
}
|
|
150
|
+
const sLen = der[i++];
|
|
151
|
+
if (sLen === void 0) {
|
|
152
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
|
|
153
|
+
}
|
|
154
|
+
let s = der.slice(i, i + sLen);
|
|
155
|
+
i += sLen;
|
|
156
|
+
if (r.length > 32 && r[0] === 0) r = r.slice(1);
|
|
157
|
+
if (s.length > 32 && s[0] === 0) s = s.slice(1);
|
|
158
|
+
if (r.length > 32 || s.length > 32) {
|
|
159
|
+
throw new WebAuthnAssertionError(
|
|
160
|
+
"bad_signature",
|
|
161
|
+
"DER component too large for P-256"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const rPadded = new Uint8Array(32);
|
|
165
|
+
rPadded.set(r, 32 - r.length);
|
|
166
|
+
let sN = bigintFromBytes(s);
|
|
167
|
+
if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
|
|
168
|
+
const sPadded = bytesFromBigint(sN, 32);
|
|
169
|
+
const out = new Uint8Array(64);
|
|
170
|
+
out.set(rPadded, 0);
|
|
171
|
+
out.set(sPadded, 32);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
export {
|
|
175
|
+
WebAuthnAssertion,
|
|
176
|
+
WebAuthnAssertionError,
|
|
177
|
+
derSignatureToCompactLowS
|
|
178
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexterai/vault",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Canonical off-chain mirror of the dexter-vault Solana Anchor program — Solana instruction builders, byte-precise message encoders, account decoders, secp256r1/Ed25519 precompile helpers, counterfactual Swig derivation, and signer interfaces. The single source of truth for any TypeScript code that produces bytes the on-chain program will verify.",
|
|
5
5
|
"author": "Dexter",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"./reader": { "types": "./dist/reader/index.d.ts", "import": "./dist/reader/index.js", "require": "./dist/reader/index.cjs" },
|
|
16
16
|
"./precompile": { "types": "./dist/precompile/index.d.ts", "import": "./dist/precompile/index.js", "require": "./dist/precompile/index.cjs" },
|
|
17
17
|
"./signers": { "types": "./dist/signers/types.d.ts", "import": "./dist/signers/types.js", "require": "./dist/signers/types.cjs" },
|
|
18
|
-
"./signers/node": { "types": "./dist/signers/node/index.d.ts", "import": "./dist/signers/node/index.js", "require": "./dist/signers/node/index.cjs" }
|
|
18
|
+
"./signers/node": { "types": "./dist/signers/node/index.d.ts", "import": "./dist/signers/node/index.js", "require": "./dist/signers/node/index.cjs" },
|
|
19
|
+
"./signers/browser": { "types": "./dist/signers/browser/index.d.ts", "import": "./dist/signers/browser/index.js", "require": "./dist/signers/browser/index.cjs" }
|
|
19
20
|
},
|
|
20
21
|
"files": ["dist", "README.md", "LICENSE", "assets"],
|
|
21
22
|
"scripts": {
|