@le-space/orbitdb-identity-provider-webauthn-did 0.7.0 → 0.8.0
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/CHANGELOG.md +40 -0
- package/README.md +31 -0
- package/docs/API.md +17 -0
- package/package.json +1 -1
- package/src/standalone/index.js +5 -0
- package/src/standalone/webauthn/p256-wallet.js +571 -0
- package/src/varsig/credential.js +2 -0
- package/src/varsig/storage.js +3 -0
- package/src/webauthn/provider.js +3 -0
- package/types/index.d.ts +5 -0
- package/types/standalone.d.ts +52 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
Two functions for wallets, and nothing that changes for anyone else. They were written against
|
|
6
|
+
0.5.4 on 16 September and vendored into simple-todo's `escrow01` as `0.5.5-p256.8366ed8`, but
|
|
7
|
+
never pushed or released until now — which is why 0.6.0 and 0.7.0 do not have them, and why #70
|
|
8
|
+
took their absence for a removal.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Passkey primitives for wallets**, in `./standalone` (#60). A smart
|
|
13
|
+
account that verifies P-256 WebAuthn signatures on-chain — Uniswap's
|
|
14
|
+
Calibur, through webauthn-sol — can use the identity's passkey, without
|
|
15
|
+
the wallet duplicating key extraction and DER parsing or creating a
|
|
16
|
+
second passkey when it cannot recover the key.
|
|
17
|
+
- `getP256CredentialDescriptor(credential)` returns
|
|
18
|
+
`{ credentialId, rawCredentialId, x, y, rpId, userVerification }`, with
|
|
19
|
+
`x` and `y` as 32-byte big-endian `Uint8Array`s, from either credential
|
|
20
|
+
shape, a stored or JSON copy, or a P-256 `did:key` alone (a varsig DID
|
|
21
|
+
holds the key compressed). It returns `null` for RS256 and Ed25519 keys,
|
|
22
|
+
for anything unreadable, and for the placeholder `createCredential`
|
|
23
|
+
writes when it cannot read the public key — also once that has lost its
|
|
24
|
+
`synthetic` flag, which largeBlob metadata does not carry: the point has
|
|
25
|
+
to lie on the curve.
|
|
26
|
+
- `signP256Challenge(descriptor, challenge)` signs exactly 32 bytes, with
|
|
27
|
+
`allowCredentials` pinned to the descriptor whatever the discoverable
|
|
28
|
+
credential policy, and user verification required. It refuses an
|
|
29
|
+
assertion whose `rawId` differs and returns
|
|
30
|
+
`{ authenticatorData, clientDataJSON, challengeIndex, typeIndex, r, s }`:
|
|
31
|
+
`r` and `s` left-padded to 32 bytes, `s` normalised to low-s, and the
|
|
32
|
+
indices as the byte offsets of `"type":"webauthn.get"` and
|
|
33
|
+
`"challenge":"…"` that webauthn-sol reads. The signature is verified
|
|
34
|
+
against `x`/`y` before it is returned.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `createCredential` returns `rpId` on both paths, and a stored varsig
|
|
39
|
+
(hardware signer) credential keeps it. An assertion has to name the rpId
|
|
40
|
+
the credential was registered under, which need not be the page's
|
|
41
|
+
hostname.
|
|
42
|
+
|
|
3
43
|
## 0.7.0
|
|
4
44
|
|
|
5
45
|
**One breaking change, in the function 0.6.0 added.** `restoreIdentityFromAuthenticator()`
|
package/README.md
CHANGED
|
@@ -179,6 +179,37 @@ const ucanUcantoSigner = signer.toUcantoSigner({
|
|
|
179
179
|
|
|
180
180
|
The verifier side and app protocol should define which domain label is required. IPFS deployment does not change this requirement.
|
|
181
181
|
|
|
182
|
+
### Passkeys for smart accounts (P-256)
|
|
183
|
+
|
|
184
|
+
One passkey can be both the OrbitDB identity and the key of a smart account that verifies P-256 WebAuthn signatures on-chain, such as Uniswap's [Calibur](https://github.com/Uniswap/calibur) with Base's [webauthn-sol](https://github.com/base/webauthn-sol). The standalone export hands wallet code the key and the signature parts; turning them into a transaction (key hashes, ABI encoding, the account client) stays in the wallet.
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
import {
|
|
188
|
+
getP256CredentialDescriptor,
|
|
189
|
+
signP256Challenge,
|
|
190
|
+
} from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';
|
|
191
|
+
|
|
192
|
+
// A credential from either createCredential, storage, largeBlob or signer.credential
|
|
193
|
+
const descriptor = getP256CredentialDescriptor(credential);
|
|
194
|
+
if (!descriptor) throw new Error('This passkey has no usable P-256 key');
|
|
195
|
+
// { credentialId, rawCredentialId, x, y, rpId, userVerification: 'required' }
|
|
196
|
+
|
|
197
|
+
// Exactly 32 bytes, e.g. a user operation hash
|
|
198
|
+
const { authenticatorData, clientDataJSON, challengeIndex, typeIndex, r, s } =
|
|
199
|
+
await signP256Challenge(descriptor, hash);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- **Key.** `x` and `y` are 32-byte big-endian `Uint8Array`s, like `publicKey.x`/`y` on the default path; Calibur's `abi.encode(uint256 x, uint256 y)` is the two concatenated. They come from `publicKey` (`{ x, y }` or SEC1 bytes) or, failing that, from a P-256 `did:key`; where both are present they must agree. `rpId` is the one `createCredential` returns; a credential stored before it returned one falls back to `window.location.hostname`.
|
|
203
|
+
- **`null`** for RS256 and Ed25519 keys, for the placeholder `createCredential` writes when it cannot read the public key (also once largeBlob metadata has dropped its `synthetic` flag: the point is not on the curve), and for anything unreadable.
|
|
204
|
+
- **Signing.** Unlike other requests, this one pins `allowCredentials` to the descriptor, whatever `configureWebAuthn` says, and requires user verification. An assertion from another credential is refused. The challenge is used as the WebAuthn challenge itself, as Calibur passes its hash.
|
|
205
|
+
- **Result**: the fields of webauthn-sol's `WebAuthnAuth`. `r` and `s` are 32 bytes big-endian, `s` normalised to low-s (webauthn-sol refuses `s > n/2`). `clientDataJSON` is a string; `typeIndex` and `challengeIndex` are the UTF-8 byte offsets of `"type":"webauthn.get"` and `"challenge":"<base64url(challenge), unpadded>"` in it. The signature is verified against `x`/`y` before it is returned; anything a verifier would refuse throws instead.
|
|
206
|
+
|
|
207
|
+
| Signing mode | Descriptor |
|
|
208
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| Default (passkey DID), hardware P-256 (varsig) | yes: the DID encodes the same key |
|
|
210
|
+
| Keystore or worker Ed25519 DID | yes, from the credential's `x`/`y`; the DID does not commit to the passkey, so link the wallet key to it yourself |
|
|
211
|
+
| Hardware Ed25519 (varsig) | `null`: there is no P-256 key |
|
|
212
|
+
|
|
182
213
|
### Keystore-based DID (WebAuthn + OrbitDB keystore)
|
|
183
214
|
|
|
184
215
|
```mermaid
|
package/docs/API.md
CHANGED
|
@@ -77,6 +77,7 @@ Returns a credential info object containing:
|
|
|
77
77
|
- `credentialId`
|
|
78
78
|
- `rawCredentialId`
|
|
79
79
|
- `publicKey`
|
|
80
|
+
- `rpId` (the relying party the credential is registered under)
|
|
80
81
|
- `userId`
|
|
81
82
|
- `displayName`
|
|
82
83
|
- `attestationObject`
|
|
@@ -348,6 +349,22 @@ const ucanSigner = signer.toUcantoSigner({
|
|
|
348
349
|
- `clearWebAuthnCredentialSafe(key?)`
|
|
349
350
|
- `extractPrfSeedFromCredential(credential)`
|
|
350
351
|
|
|
352
|
+
### P-256 Wallet Primitives
|
|
353
|
+
|
|
354
|
+
For smart accounts that verify P-256 WebAuthn signatures (for example Calibur
|
|
355
|
+
with webauthn-sol). See the README section "Passkeys for smart accounts".
|
|
356
|
+
|
|
357
|
+
- `getP256CredentialDescriptor(credential)` →
|
|
358
|
+
`{ credentialId, rawCredentialId, x, y, rpId, userVerification: 'required' }`
|
|
359
|
+
or `null` (RS256, Ed25519, placeholder or unreadable key). `x` and `y` are
|
|
360
|
+
32-byte big-endian `Uint8Array`s.
|
|
361
|
+
- `signP256Challenge(descriptor, challenge)` → `Promise` of
|
|
362
|
+
`{ authenticatorData, clientDataJSON, challengeIndex, typeIndex, r, s }`.
|
|
363
|
+
`challenge` must be exactly 32 bytes. `allowCredentials` is pinned to the
|
|
364
|
+
descriptor, user verification is required, an assertion from another
|
|
365
|
+
credential is refused. `r` and `s` are 32 bytes with `s` low; the indices
|
|
366
|
+
are UTF-8 byte offsets in `clientDataJSON`.
|
|
367
|
+
|
|
351
368
|
### Worker Keystore Client
|
|
352
369
|
|
|
353
370
|
- `createWorkerKeystoreClient(options?)`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/orbitdb-identity-provider-webauthn-did",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "WebAuthn-based DID identity provider for OrbitDB for hardware-secured wallets and biometric Passkey authentication",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
package/src/standalone/index.js
CHANGED
|
@@ -26,6 +26,11 @@ export {
|
|
|
26
26
|
extractPrfSeedFromCredential,
|
|
27
27
|
} from './webauthn/credential.js';
|
|
28
28
|
|
|
29
|
+
export {
|
|
30
|
+
getP256CredentialDescriptor,
|
|
31
|
+
signP256Challenge,
|
|
32
|
+
} from './webauthn/p256-wallet.js';
|
|
33
|
+
|
|
29
34
|
export {
|
|
30
35
|
createWorkerKeystoreClient,
|
|
31
36
|
createWorkerSigner,
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey primitives for wallets.
|
|
3
|
+
*
|
|
4
|
+
* A smart account that verifies P-256 WebAuthn signatures on-chain — Uniswap's
|
|
5
|
+
* Calibur, through Base's webauthn-sol — can use the same passkey as the
|
|
6
|
+
* OrbitDB identity. It needs the passkey's key as x/y, the credential to ask
|
|
7
|
+
* for, and an assertion over a 32-byte challenge taken apart into the fields
|
|
8
|
+
* such a verifier reads (webauthn-sol's `WebAuthnAuth`).
|
|
9
|
+
*
|
|
10
|
+
* What turns those into a transaction — key hashes, ABI encoding, the account
|
|
11
|
+
* client — moves funds and stays out of this package.
|
|
12
|
+
*/
|
|
13
|
+
import { derToRawSignature, isDerSignature } from 'iso-webauthn-varsig';
|
|
14
|
+
|
|
15
|
+
import { WEBAUTHN_CLIENT_DATA_TYPES } from '../../constants.js';
|
|
16
|
+
import {
|
|
17
|
+
WebAuthnAuthenticationError,
|
|
18
|
+
WebAuthnIdentityError,
|
|
19
|
+
WebAuthnNotSupportedError,
|
|
20
|
+
WebAuthnVerificationError,
|
|
21
|
+
} from '../../errors.js';
|
|
22
|
+
import { buildCredentialRequestOptions } from '../../webauthn/config.js';
|
|
23
|
+
import { MULTICODEC, decodeDidKey } from '../../webauthn/proof-verification.js';
|
|
24
|
+
|
|
25
|
+
// secp256r1 (NIST P-256), SEC 2 §2.4.2: y² = x³ − 3x + b over GF(p), order n.
|
|
26
|
+
const P256_P =
|
|
27
|
+
0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn;
|
|
28
|
+
const P256_N =
|
|
29
|
+
0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
30
|
+
const P256_B =
|
|
31
|
+
0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn;
|
|
32
|
+
// webauthn-sol refuses any s above floor(n / 2), against malleability.
|
|
33
|
+
const P256_HALF_N = P256_N / 2n;
|
|
34
|
+
|
|
35
|
+
const COORDINATE_BYTES = 32;
|
|
36
|
+
const CHALLENGE_BYTES = 32;
|
|
37
|
+
|
|
38
|
+
const COSE_KTY_EC2 = 2;
|
|
39
|
+
const COSE_ALG_ES256 = -7;
|
|
40
|
+
const COSE_CRV_P256 = 1;
|
|
41
|
+
|
|
42
|
+
// rpIdHash (32) + flags (1) + signCount (4)
|
|
43
|
+
const AUTH_DATA_MIN_LENGTH = 37;
|
|
44
|
+
const FLAG_USER_PRESENT = 0x01;
|
|
45
|
+
const FLAG_USER_VERIFIED = 0x04;
|
|
46
|
+
|
|
47
|
+
const encoder = new TextEncoder();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Bytes from the shapes a byte field arrives in: a Uint8Array or other view,
|
|
51
|
+
* an ArrayBuffer, a number array, or the `{ "0": …, "1": … }` object that
|
|
52
|
+
* JSON makes of a Uint8Array. Always a copy. Anything else is null.
|
|
53
|
+
* @param {unknown} value
|
|
54
|
+
* @returns {Uint8Array|null}
|
|
55
|
+
*/
|
|
56
|
+
function readBytes(value) {
|
|
57
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
|
|
58
|
+
if (ArrayBuffer.isView(value)) {
|
|
59
|
+
return new Uint8Array(
|
|
60
|
+
value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let values;
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
values = value;
|
|
67
|
+
} else if (value && typeof value === 'object') {
|
|
68
|
+
const keys = Object.keys(value);
|
|
69
|
+
if (!keys.every((key, index) => key === String(index))) return null;
|
|
70
|
+
values = keys.map((key) => value[key]);
|
|
71
|
+
} else {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isByte = (byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255;
|
|
76
|
+
return values.every(isByte) ? Uint8Array.from(values) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A credential's byte field as storage hands it back: bytes in any shape
|
|
81
|
+
* `readBytes` takes, or base64url text (largeBlob and varsig metadata).
|
|
82
|
+
* @returns {Uint8Array|null}
|
|
83
|
+
*/
|
|
84
|
+
function readField(value) {
|
|
85
|
+
return typeof value === 'string' ? base64urlToBytes(value) : readBytes(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function equalBytes(a, b) {
|
|
89
|
+
return a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function concatBytes(...parts) {
|
|
93
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
94
|
+
let offset = 0;
|
|
95
|
+
for (const part of parts) {
|
|
96
|
+
out.set(part, offset);
|
|
97
|
+
offset += part.length;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function indexOfBytes(haystack, needle) {
|
|
103
|
+
for (let i = 0; i + needle.length <= haystack.length; i += 1) {
|
|
104
|
+
let j = 0;
|
|
105
|
+
while (j < needle.length && haystack[i + j] === needle[j]) j += 1;
|
|
106
|
+
if (j === needle.length) return i;
|
|
107
|
+
}
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** base64url without padding, as WebAuthn and webauthn-sol encode it. */
|
|
112
|
+
function bytesToBase64url(bytes) {
|
|
113
|
+
let binary = '';
|
|
114
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
115
|
+
return btoa(binary)
|
|
116
|
+
.replace(/\+/g, '-')
|
|
117
|
+
.replace(/\//g, '_')
|
|
118
|
+
.replace(/=+$/, '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @returns {Uint8Array|null} */
|
|
122
|
+
function base64urlToBytes(value) {
|
|
123
|
+
const unpadded = value.replace(/=+$/, '');
|
|
124
|
+
if (!/^[A-Za-z0-9_-]*$/.test(unpadded) || unpadded.length % 4 === 1) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const base64 = unpadded.replace(/-/g, '+').replace(/_/g, '/');
|
|
128
|
+
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));
|
|
129
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bytesToBigInt(bytes) {
|
|
133
|
+
let value = 0n;
|
|
134
|
+
for (const byte of bytes) value = (value << 8n) | BigInt(byte);
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function bigIntToBytes(value, length = COORDINATE_BYTES) {
|
|
139
|
+
const out = new Uint8Array(length);
|
|
140
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
141
|
+
out[i] = Number(value & 0xffn);
|
|
142
|
+
value >>= 8n;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mod(value, modulus) {
|
|
148
|
+
const result = value % modulus;
|
|
149
|
+
return result < 0n ? result + modulus : result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function modPow(base, exponent, modulus) {
|
|
153
|
+
let result = 1n;
|
|
154
|
+
base = mod(base, modulus);
|
|
155
|
+
while (exponent > 0n) {
|
|
156
|
+
if (exponent & 1n) result = (result * base) % modulus;
|
|
157
|
+
base = (base * base) % modulus;
|
|
158
|
+
exponent >>= 1n;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** x³ − 3x + b (mod p): what y² has to be for (x, y) to lie on the curve. */
|
|
164
|
+
function curveYSquared(x) {
|
|
165
|
+
return mod(x * x * x - 3n * x + P256_B, P256_P);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isOnCurve(x, y) {
|
|
169
|
+
const xValue = bytesToBigInt(x);
|
|
170
|
+
const yValue = bytesToBigInt(y);
|
|
171
|
+
return (
|
|
172
|
+
xValue < P256_P &&
|
|
173
|
+
yValue < P256_P &&
|
|
174
|
+
mod(yValue * yValue, P256_P) === curveYSquared(xValue)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* x/y from a SEC1 point: 65 bytes uncompressed, or 33 compressed. A varsig
|
|
180
|
+
* `did:key` carries the compressed form, so y is recomputed: p ≡ 3 (mod 4),
|
|
181
|
+
* so a square root of y² is (y²)^((p+1)/4), and the prefix picks its parity.
|
|
182
|
+
* @returns {{x: Uint8Array, y: Uint8Array}|null}
|
|
183
|
+
*/
|
|
184
|
+
function pointFromSec1(bytes) {
|
|
185
|
+
if (bytes.length === 65 && bytes[0] === 0x04) {
|
|
186
|
+
return { x: bytes.slice(1, 33), y: bytes.slice(33) };
|
|
187
|
+
}
|
|
188
|
+
if (bytes.length !== 33 || (bytes[0] !== 0x02 && bytes[0] !== 0x03)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const x = bytesToBigInt(bytes.subarray(1));
|
|
193
|
+
if (x >= P256_P) return null;
|
|
194
|
+
const ySquared = curveYSquared(x);
|
|
195
|
+
let y = modPow(ySquared, (P256_P + 1n) / 4n, P256_P);
|
|
196
|
+
if (mod(y * y, P256_P) !== ySquared) return null;
|
|
197
|
+
if ((y & 1n) !== BigInt(bytes[0] & 1)) {
|
|
198
|
+
if (y === 0n) return null;
|
|
199
|
+
y = P256_P - y;
|
|
200
|
+
}
|
|
201
|
+
return { x: bytes.slice(1), y: bigIntToBytes(y) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Absent and null both count as "not stated". */
|
|
205
|
+
function statesOtherThan(value, expected) {
|
|
206
|
+
return value !== undefined && value !== null && value !== expected;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The credential's public key, if it is P-256.
|
|
211
|
+
* @returns {{x: Uint8Array, y: Uint8Array}|null|undefined} undefined when
|
|
212
|
+
* the credential carries no key, null when it carries an unusable one.
|
|
213
|
+
*/
|
|
214
|
+
function readCredentialPublicKey(credential) {
|
|
215
|
+
const { publicKey } = credential;
|
|
216
|
+
if (publicKey === undefined || publicKey === null) return undefined;
|
|
217
|
+
|
|
218
|
+
// Default path: COSE-style { algorithm, keyType, curve, x, y }.
|
|
219
|
+
if (typeof publicKey === 'object' && ('x' in publicKey || 'y' in publicKey)) {
|
|
220
|
+
// The stand-in createCredential writes when it could not read the key.
|
|
221
|
+
if (publicKey.synthetic) return null;
|
|
222
|
+
if (
|
|
223
|
+
statesOtherThan(publicKey.algorithm, COSE_ALG_ES256) ||
|
|
224
|
+
statesOtherThan(publicKey.keyType, COSE_KTY_EC2) ||
|
|
225
|
+
statesOtherThan(publicKey.curve, COSE_CRV_P256)
|
|
226
|
+
) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const x = readField(publicKey.x);
|
|
230
|
+
const y = readField(publicKey.y);
|
|
231
|
+
if (x?.length !== COORDINATE_BYTES || y?.length !== COORDINATE_BYTES) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return { x, y };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Varsig path: SEC1 bytes (32 bytes would be Ed25519, and is refused here).
|
|
238
|
+
const bytes = readField(publicKey);
|
|
239
|
+
return bytes ? pointFromSec1(bytes) : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The key a P-256 `did:key` encodes.
|
|
244
|
+
* @returns {{x: Uint8Array, y: Uint8Array}|null|undefined} undefined when
|
|
245
|
+
* the DID is absent or does not name a P-256 key (a keystore DID does not
|
|
246
|
+
* commit to the passkey), null when it names one that cannot be read.
|
|
247
|
+
*/
|
|
248
|
+
function readDidPublicKey(did) {
|
|
249
|
+
if (typeof did !== 'string') return undefined;
|
|
250
|
+
let decoded;
|
|
251
|
+
try {
|
|
252
|
+
decoded = decodeDidKey(did);
|
|
253
|
+
} catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (decoded.codec !== MULTICODEC.P256_PUB) return undefined;
|
|
257
|
+
return pointFromSec1(decoded.keyBytes);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Raw credential id from `rawCredentialId` and/or `credentialId` (bytes, or
|
|
262
|
+
* base64url). Where both are given they must name the same credential.
|
|
263
|
+
* @returns {Uint8Array|null}
|
|
264
|
+
*/
|
|
265
|
+
function readCredentialId(credential) {
|
|
266
|
+
const candidates = [credential.rawCredentialId, credential.credentialId]
|
|
267
|
+
.filter((value) => value !== undefined && value !== null)
|
|
268
|
+
.map(readField);
|
|
269
|
+
if (candidates.length === 0 || candidates.some((id) => !id?.length)) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const [first, ...rest] = candidates;
|
|
273
|
+
return rest.every((id) => equalBytes(id, first)) ? first : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Describe a passkey's P-256 key the way wallet code needs it.
|
|
278
|
+
*
|
|
279
|
+
* Accepts a credential from either `createCredential` path, a copy restored
|
|
280
|
+
* from localStorage, JSON or largeBlob, or a signer's `credential`. The key
|
|
281
|
+
* comes from `publicKey` — `{ x, y }` or SEC1 bytes — or, failing that, from a
|
|
282
|
+
* P-256 `did:key`; where both are present they must agree.
|
|
283
|
+
*
|
|
284
|
+
* Returns null, rather than something that cannot sign, for: RS256 or Ed25519
|
|
285
|
+
* keys; the placeholder `createCredential` writes when it cannot read the key
|
|
286
|
+
* (`synthetic: true`), and one that lost that flag, since its point is not on
|
|
287
|
+
* the curve; and anything unreadable — no credential id, ids that disagree, a
|
|
288
|
+
* malformed key, or no rpId.
|
|
289
|
+
*
|
|
290
|
+
* `rpId` is the one `createCredential` registered under; credentials from
|
|
291
|
+
* before it was recorded fall back to `window.location.hostname`, the default
|
|
292
|
+
* it registers under.
|
|
293
|
+
*
|
|
294
|
+
* @param {unknown} credential
|
|
295
|
+
* @returns {{credentialId: string, rawCredentialId: Uint8Array, x: Uint8Array, y: Uint8Array, rpId: string, userVerification: 'required'}|null}
|
|
296
|
+
* `x` and `y` are 32-byte big-endian coordinates; `credentialId` is the
|
|
297
|
+
* unpadded base64url of `rawCredentialId`.
|
|
298
|
+
*/
|
|
299
|
+
export function getP256CredentialDescriptor(credential) {
|
|
300
|
+
if (!credential || typeof credential !== 'object') return null;
|
|
301
|
+
|
|
302
|
+
if (statesOtherThan(credential.algorithm, 'P-256')) return null;
|
|
303
|
+
const { cose } = credential;
|
|
304
|
+
if (
|
|
305
|
+
cose &&
|
|
306
|
+
typeof cose === 'object' &&
|
|
307
|
+
(statesOtherThan(cose.kty, COSE_KTY_EC2) ||
|
|
308
|
+
statesOtherThan(cose.alg, COSE_ALG_ES256) ||
|
|
309
|
+
statesOtherThan(cose.crv, COSE_CRV_P256))
|
|
310
|
+
) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rawCredentialId = readCredentialId(credential);
|
|
315
|
+
if (!rawCredentialId) return null;
|
|
316
|
+
|
|
317
|
+
const fromKey = readCredentialPublicKey(credential);
|
|
318
|
+
const fromDid = readDidPublicKey(credential.did);
|
|
319
|
+
if (fromKey === null || fromDid === null) return null;
|
|
320
|
+
const point = fromKey ?? fromDid;
|
|
321
|
+
if (!point) return null;
|
|
322
|
+
if (
|
|
323
|
+
fromKey &&
|
|
324
|
+
fromDid &&
|
|
325
|
+
!(equalBytes(fromKey.x, fromDid.x) && equalBytes(fromKey.y, fromDid.y))
|
|
326
|
+
) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
if (!isOnCurve(point.x, point.y)) return null;
|
|
330
|
+
|
|
331
|
+
const rpId =
|
|
332
|
+
typeof credential.rpId === 'string' && credential.rpId
|
|
333
|
+
? credential.rpId
|
|
334
|
+
: globalThis.window?.location?.hostname;
|
|
335
|
+
if (typeof rpId !== 'string' || !rpId) return null;
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
credentialId: bytesToBase64url(rawCredentialId),
|
|
339
|
+
rawCredentialId,
|
|
340
|
+
x: point.x,
|
|
341
|
+
y: point.y,
|
|
342
|
+
rpId,
|
|
343
|
+
userVerification: 'required',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function readDescriptor(descriptor) {
|
|
348
|
+
const invalid = () =>
|
|
349
|
+
new WebAuthnIdentityError(
|
|
350
|
+
'signP256Challenge needs a descriptor from getP256CredentialDescriptor()'
|
|
351
|
+
);
|
|
352
|
+
if (!descriptor || typeof descriptor !== 'object') throw invalid();
|
|
353
|
+
|
|
354
|
+
const rawCredentialId = readBytes(descriptor.rawCredentialId);
|
|
355
|
+
const x = readBytes(descriptor.x);
|
|
356
|
+
const y = readBytes(descriptor.y);
|
|
357
|
+
const { rpId, credentialId } = descriptor;
|
|
358
|
+
if (
|
|
359
|
+
!rawCredentialId?.length ||
|
|
360
|
+
x?.length !== COORDINATE_BYTES ||
|
|
361
|
+
y?.length !== COORDINATE_BYTES ||
|
|
362
|
+
typeof rpId !== 'string' ||
|
|
363
|
+
!rpId ||
|
|
364
|
+
(typeof credentialId === 'string' &&
|
|
365
|
+
credentialId.replace(/=+$/, '') !== bytesToBase64url(rawCredentialId)) ||
|
|
366
|
+
!isOnCurve(x, y)
|
|
367
|
+
) {
|
|
368
|
+
throw invalid();
|
|
369
|
+
}
|
|
370
|
+
return { rawCredentialId, x, y, rpId };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* r and s from the ASN.1 DER an authenticator returns for ES256 (WebAuthn L2
|
|
375
|
+
* §6.5.5), left-padded to 32 bytes, with s normalised to low-s.
|
|
376
|
+
*
|
|
377
|
+
* DER integers are minimal, so r or s is 31 bytes about once in 256 and 33
|
|
378
|
+
* (a sign byte) about half the time; `derToRawSignature` handles both (#58).
|
|
379
|
+
*/
|
|
380
|
+
function readEcdsaSignature(signature) {
|
|
381
|
+
if (!isDerSignature(signature)) {
|
|
382
|
+
throw new WebAuthnVerificationError(
|
|
383
|
+
'The assertion signature is not an ASN.1 DER ECDSA signature'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
let raw;
|
|
387
|
+
try {
|
|
388
|
+
raw = derToRawSignature(signature);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw new WebAuthnVerificationError(error.message, { cause: error });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const r = bytesToBigInt(raw.subarray(0, COORDINATE_BYTES));
|
|
394
|
+
let s = bytesToBigInt(raw.subarray(COORDINATE_BYTES));
|
|
395
|
+
if (r === 0n || r >= P256_N || s === 0n || s >= P256_N) {
|
|
396
|
+
throw new WebAuthnVerificationError('Signature r or s is out of range');
|
|
397
|
+
}
|
|
398
|
+
// (r, n − s) verifies wherever (r, s) does; webauthn-sol takes only the low
|
|
399
|
+
// one, and the authenticator returns either.
|
|
400
|
+
if (s > P256_HALF_N) s = P256_N - s;
|
|
401
|
+
|
|
402
|
+
return { r: raw.slice(0, COORDINATE_BYTES), s: bigIntToBytes(s) };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function verifiesWithKey({ x, y, r, s, authenticatorData, clientData }) {
|
|
406
|
+
try {
|
|
407
|
+
const key = await crypto.subtle.importKey(
|
|
408
|
+
'raw',
|
|
409
|
+
concatBytes(new Uint8Array([0x04]), x, y),
|
|
410
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
411
|
+
false,
|
|
412
|
+
['verify']
|
|
413
|
+
);
|
|
414
|
+
const clientDataHash = new Uint8Array(
|
|
415
|
+
await crypto.subtle.digest('SHA-256', clientData)
|
|
416
|
+
);
|
|
417
|
+
return await crypto.subtle.verify(
|
|
418
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
419
|
+
key,
|
|
420
|
+
concatBytes(r, s),
|
|
421
|
+
concatBytes(authenticatorData, clientDataHash)
|
|
422
|
+
);
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Sign a raw 32-byte challenge — a user operation hash, say — with the passkey
|
|
430
|
+
* a descriptor names, and return the parts a P-256 WebAuthn verifier reads.
|
|
431
|
+
*
|
|
432
|
+
* The challenge is used as the WebAuthn challenge itself, as Calibur passes
|
|
433
|
+
* its hash to webauthn-sol. The request pins `allowCredentials` to the
|
|
434
|
+
* descriptor's credential — whatever `configureWebAuthn` says about
|
|
435
|
+
* discoverable credentials — uses its `rpId` and requires user verification.
|
|
436
|
+
*
|
|
437
|
+
* Refused with a thrown error: a challenge that is not exactly 32 bytes (before
|
|
438
|
+
* any prompt), an assertion from another credential (`rawId` differs), and
|
|
439
|
+
* anything a verifier would refuse — client data that is not `webauthn.get`
|
|
440
|
+
* over this challenge, the UP or UV flag unset, a signature that is not DER or
|
|
441
|
+
* does not verify against the descriptor's x/y.
|
|
442
|
+
*
|
|
443
|
+
* @param {{rawCredentialId: Uint8Array, x: Uint8Array, y: Uint8Array, rpId: string}} descriptor
|
|
444
|
+
* from `getP256CredentialDescriptor`
|
|
445
|
+
* @param {Uint8Array|ArrayBuffer} challenge exactly 32 bytes
|
|
446
|
+
* @returns {Promise<{authenticatorData: Uint8Array, clientDataJSON: string, challengeIndex: number, typeIndex: number, r: Uint8Array, s: Uint8Array}>}
|
|
447
|
+
* `challengeIndex` and `typeIndex` are UTF-8 byte offsets in
|
|
448
|
+
* `clientDataJSON` of `"challenge":"<base64url(challenge), unpadded>"` and
|
|
449
|
+
* `"type":"webauthn.get"`; `r` and `s` are 32 bytes big-endian, `s` low.
|
|
450
|
+
*/
|
|
451
|
+
export async function signP256Challenge(descriptor, challenge) {
|
|
452
|
+
const { rawCredentialId, x, y, rpId } = readDescriptor(descriptor);
|
|
453
|
+
|
|
454
|
+
const challengeBytes =
|
|
455
|
+
challenge instanceof ArrayBuffer || ArrayBuffer.isView(challenge)
|
|
456
|
+
? readBytes(challenge)
|
|
457
|
+
: null;
|
|
458
|
+
if (challengeBytes?.length !== CHALLENGE_BYTES) {
|
|
459
|
+
throw new WebAuthnIdentityError(
|
|
460
|
+
`signP256Challenge needs a challenge of exactly ${CHALLENGE_BYTES} bytes, got ${
|
|
461
|
+
challengeBytes ? `${challengeBytes.length} bytes` : typeof challenge
|
|
462
|
+
}`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (typeof globalThis.navigator?.credentials?.get !== 'function') {
|
|
467
|
+
throw new WebAuthnNotSupportedError();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let assertion;
|
|
471
|
+
try {
|
|
472
|
+
assertion = await navigator.credentials.get(
|
|
473
|
+
buildCredentialRequestOptions({
|
|
474
|
+
challenge: challengeBytes,
|
|
475
|
+
rpId,
|
|
476
|
+
credentialId: rawCredentialId,
|
|
477
|
+
userVerification: 'required',
|
|
478
|
+
// The signature is only any use from this credential, so the browser
|
|
479
|
+
// must not offer a different passkey.
|
|
480
|
+
discoverableCredentials: false,
|
|
481
|
+
})
|
|
482
|
+
);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw new WebAuthnAuthenticationError(
|
|
485
|
+
error?.name === 'NotAllowedError'
|
|
486
|
+
? 'Passkey authentication was cancelled or not allowed'
|
|
487
|
+
: `Passkey authentication failed: ${error?.message}`,
|
|
488
|
+
{ cause: error }
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (!assertion) {
|
|
492
|
+
throw new WebAuthnAuthenticationError('Passkey authentication failed');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const rawId = readBytes(assertion.rawId);
|
|
496
|
+
if (!rawId || !equalBytes(rawId, rawCredentialId)) {
|
|
497
|
+
throw new WebAuthnAuthenticationError(
|
|
498
|
+
'The assertion came from a different credential than the descriptor names'
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const response = assertion.response ?? {};
|
|
503
|
+
const authenticatorData = readBytes(response.authenticatorData);
|
|
504
|
+
const clientData = readBytes(response.clientDataJSON);
|
|
505
|
+
const signature = readBytes(response.signature);
|
|
506
|
+
if (!authenticatorData || !clientData || !signature) {
|
|
507
|
+
throw new WebAuthnVerificationError(
|
|
508
|
+
'The assertion lacks authenticatorData, clientDataJSON or signature'
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (authenticatorData.length < AUTH_DATA_MIN_LENGTH) {
|
|
513
|
+
throw new WebAuthnVerificationError('authenticatorData is too short');
|
|
514
|
+
}
|
|
515
|
+
const flags = authenticatorData[32];
|
|
516
|
+
if (!(flags & FLAG_USER_PRESENT) || !(flags & FLAG_USER_VERIFIED)) {
|
|
517
|
+
throw new WebAuthnVerificationError(
|
|
518
|
+
'The authenticator did not report user presence and verification'
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Fatal and BOM-preserving, so the string encodes back to exactly the bytes
|
|
523
|
+
// that were signed.
|
|
524
|
+
let clientDataJSON;
|
|
525
|
+
let parsed;
|
|
526
|
+
try {
|
|
527
|
+
clientDataJSON = new TextDecoder('utf-8', {
|
|
528
|
+
fatal: true,
|
|
529
|
+
ignoreBOM: true,
|
|
530
|
+
}).decode(clientData);
|
|
531
|
+
parsed = JSON.parse(clientDataJSON);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
throw new WebAuthnVerificationError('clientDataJSON is not valid JSON', {
|
|
534
|
+
cause: error,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const encodedChallenge = bytesToBase64url(challengeBytes);
|
|
539
|
+
if (
|
|
540
|
+
parsed?.type !== WEBAUTHN_CLIENT_DATA_TYPES.GET ||
|
|
541
|
+
parsed.challenge !== encodedChallenge
|
|
542
|
+
) {
|
|
543
|
+
throw new WebAuthnVerificationError(
|
|
544
|
+
'clientDataJSON is not a webauthn.get over this challenge'
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Located as bytes: a verifier slices the UTF-8 encoding at these offsets.
|
|
549
|
+
const typeIndex = indexOfBytes(
|
|
550
|
+
clientData,
|
|
551
|
+
encoder.encode(`"type":"${WEBAUTHN_CLIENT_DATA_TYPES.GET}"`)
|
|
552
|
+
);
|
|
553
|
+
const challengeIndex = indexOfBytes(
|
|
554
|
+
clientData,
|
|
555
|
+
encoder.encode(`"challenge":"${encodedChallenge}"`)
|
|
556
|
+
);
|
|
557
|
+
if (typeIndex < 0 || challengeIndex < 0) {
|
|
558
|
+
throw new WebAuthnVerificationError(
|
|
559
|
+
'clientDataJSON does not spell out its type and challenge verbatim'
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const { r, s } = readEcdsaSignature(signature);
|
|
564
|
+
if (!(await verifiesWithKey({ x, y, r, s, authenticatorData, clientData }))) {
|
|
565
|
+
throw new WebAuthnVerificationError(
|
|
566
|
+
"The signature does not verify against the descriptor's public key"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return { authenticatorData, clientDataJSON, challengeIndex, typeIndex, r, s };
|
|
571
|
+
}
|
package/src/varsig/credential.js
CHANGED
|
@@ -140,6 +140,8 @@ async function createWebAuthnVarsigCredential(options = {}) {
|
|
|
140
140
|
did,
|
|
141
141
|
algorithm,
|
|
142
142
|
cose: { kty, alg, crv },
|
|
143
|
+
// The relying party the credential is scoped to; see createCredential.
|
|
144
|
+
rpId: domain || window.location.hostname,
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
147
|
|
package/src/varsig/storage.js
CHANGED
|
@@ -19,6 +19,7 @@ export function storeWebAuthnVarsigCredential(
|
|
|
19
19
|
did: credential.did,
|
|
20
20
|
algorithm: credential.algorithm,
|
|
21
21
|
cose: credential.cose || null,
|
|
22
|
+
...(credential.rpId ? { rpId: credential.rpId } : {}),
|
|
22
23
|
};
|
|
23
24
|
localStorage.setItem(key, JSON.stringify(payload));
|
|
24
25
|
}
|
|
@@ -40,6 +41,8 @@ export function loadWebAuthnVarsigCredential(
|
|
|
40
41
|
did: parsed.did,
|
|
41
42
|
algorithm: parsed.algorithm,
|
|
42
43
|
cose: parsed.cose || null,
|
|
44
|
+
// Absent from credentials stored before rpId was recorded.
|
|
45
|
+
...(parsed.rpId ? { rpId: parsed.rpId } : {}),
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
package/src/webauthn/provider.js
CHANGED
|
@@ -301,6 +301,9 @@ export class WebAuthnDIDProvider {
|
|
|
301
301
|
),
|
|
302
302
|
rawCredentialId: new Uint8Array(credential.rawId),
|
|
303
303
|
publicKey,
|
|
304
|
+
// The relying party the credential is scoped to. An assertion has to
|
|
305
|
+
// name the same one, and `domain` need not be this page's hostname.
|
|
306
|
+
rpId: domain || window.location.hostname,
|
|
304
307
|
userId,
|
|
305
308
|
displayName,
|
|
306
309
|
// Nothing in this package looks a credential up by handle — recovery
|
package/types/index.d.ts
CHANGED
|
@@ -89,6 +89,11 @@ export interface WebAuthnCredentialInfo {
|
|
|
89
89
|
credentialId: string;
|
|
90
90
|
rawCredentialId: Uint8Array;
|
|
91
91
|
publicKey: WebAuthnPublicKey | Uint8Array;
|
|
92
|
+
/**
|
|
93
|
+
* Relying party id the credential is registered under. Returned by
|
|
94
|
+
* `createCredential`; absent from credentials stored before it was.
|
|
95
|
+
*/
|
|
96
|
+
rpId?: string;
|
|
92
97
|
userId?: string;
|
|
93
98
|
displayName?: string;
|
|
94
99
|
/** Random 64-byte WebAuthn user handle, base64url. Not derived from userId. */
|
package/types/standalone.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface WebAuthnVarsigCredential {
|
|
|
18
18
|
publicKey: Uint8Array;
|
|
19
19
|
algorithm: WebAuthnAlgorithm;
|
|
20
20
|
cose?: unknown;
|
|
21
|
+
/** Relying party id the credential is registered under. */
|
|
22
|
+
rpId?: string;
|
|
21
23
|
[key: string]: unknown;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -134,6 +136,56 @@ export function extractPrfSeedFromCredential(
|
|
|
134
136
|
{ seed: Uint8Array; source: 'prf' } | { seed: null; source: 'none' }
|
|
135
137
|
>;
|
|
136
138
|
|
|
139
|
+
/** A passkey's P-256 key, as wallet code needs it. */
|
|
140
|
+
export interface P256CredentialDescriptor {
|
|
141
|
+
/** Unpadded base64url of `rawCredentialId`. */
|
|
142
|
+
credentialId: string;
|
|
143
|
+
rawCredentialId: Uint8Array;
|
|
144
|
+
/** Public key x coordinate: 32 bytes, big-endian. */
|
|
145
|
+
x: Uint8Array;
|
|
146
|
+
/** Public key y coordinate: 32 bytes, big-endian. */
|
|
147
|
+
y: Uint8Array;
|
|
148
|
+
rpId: string;
|
|
149
|
+
/** `signP256Challenge` always requires user verification. */
|
|
150
|
+
userVerification: 'required';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* An assertion over a 32-byte challenge, in the fields of webauthn-sol's
|
|
155
|
+
* `WebAuthnAuth` (the verifier Uniswap's Calibur uses).
|
|
156
|
+
*/
|
|
157
|
+
export interface P256ChallengeSignature {
|
|
158
|
+
authenticatorData: Uint8Array;
|
|
159
|
+
clientDataJSON: string;
|
|
160
|
+
/** UTF-8 byte offset of `"challenge":"<base64url(challenge)>"` in `clientDataJSON`. */
|
|
161
|
+
challengeIndex: number;
|
|
162
|
+
/** UTF-8 byte offset of `"type":"webauthn.get"` in `clientDataJSON`. */
|
|
163
|
+
typeIndex: number;
|
|
164
|
+
/** 32 bytes, big-endian. */
|
|
165
|
+
r: Uint8Array;
|
|
166
|
+
/** 32 bytes, big-endian, normalised to low-s (s ≤ n/2). */
|
|
167
|
+
s: Uint8Array;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Describe a credential's P-256 key, from either `createCredential` path or a
|
|
172
|
+
* stored copy. `null` for RS256 or Ed25519 keys, the placeholder key written
|
|
173
|
+
* when registration could not read the public key, and anything unreadable.
|
|
174
|
+
*/
|
|
175
|
+
export function getP256CredentialDescriptor(
|
|
176
|
+
credential: unknown
|
|
177
|
+
): P256CredentialDescriptor | null;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Sign exactly 32 bytes with the descriptor's passkey (`allowCredentials`
|
|
181
|
+
* pinned, user verification required). Rejects an assertion from another
|
|
182
|
+
* credential, and anything that would not verify against `x`/`y`.
|
|
183
|
+
*/
|
|
184
|
+
export function signP256Challenge(
|
|
185
|
+
descriptor: P256CredentialDescriptor,
|
|
186
|
+
challenge: Uint8Array | ArrayBuffer
|
|
187
|
+
): Promise<P256ChallengeSignature>;
|
|
188
|
+
|
|
137
189
|
export interface WorkerKeystoreClient {
|
|
138
190
|
initWithPrfSeed(prfSeed: Uint8Array): Promise<void>;
|
|
139
191
|
/** Derive the Ed25519 signer from the PRF seed inside the worker; only the public half comes back. */
|