@virtonetwork/authenticators-webauthn 1.0.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/README.md +91 -0
- package/dist/cjs/index.js +261 -0
- package/dist/cjs/types.js +27 -0
- package/dist/esm/index.d.ts +92 -0
- package/dist/esm/index.js +181 -0
- package/dist/esm/types.d.ts +28 -0
- package/dist/esm/types.js +24 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# WebAuthn Authenticator for Virto Network
|
|
2
|
+
|
|
3
|
+
A TypeScript helper that wires **passkeys** (WebAuthn resident credentials) to the [@virtonetwork/signer](https://github.com/virto-network/papi-signers) stack. It exposes a single class, `WebAuthn`, that fulfils the `Authenticator<number>` interface used by `PassSigner`.
|
|
4
|
+
The implementation is **browser‑only** and keeps all credential mapping in the caller’s hands — perfect for SPAs or wallet extensions that already manage users.
|
|
5
|
+
|
|
6
|
+
## ✨ Features
|
|
7
|
+
|
|
8
|
+
* **One‑line setup** → `await new WebAuthn(user).setup()`
|
|
9
|
+
* **Kreivo‑compatible challenges** for secure on‑chain attestations
|
|
10
|
+
* Deterministic `deviceId = Blake2‑256(credentialId)`
|
|
11
|
+
* Produces SCALE‑encoded `Attestation` / `PassAuthenticate` objects
|
|
12
|
+
* Zero persistence: inject or register credentials as you see fit
|
|
13
|
+
|
|
14
|
+
## 📦 Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i @virtonetwork/authenticators-webauthn
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 🚀 Quick start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { WebAuthn } from "@virtonetwork/authenticators-webauthn";
|
|
24
|
+
import { PassSigner } from "@virtonetwork/signer";
|
|
25
|
+
|
|
26
|
+
// 1️⃣ Restore user → credential mapping (from DB, localStorage…)
|
|
27
|
+
const savedId = await db.getCredentialId("alice@example.com");
|
|
28
|
+
|
|
29
|
+
// 2️⃣ Bootstrap helper
|
|
30
|
+
const wa = await new WebAuthn("alice@example.com", savedId).setup();
|
|
31
|
+
|
|
32
|
+
// 3️⃣ Enrol a new pass‑key if needed
|
|
33
|
+
if (!savedId) {
|
|
34
|
+
const att = await wa.register(blockNumber, blockHash);
|
|
35
|
+
await db.saveCredentialId("alice@example.com", att.credentialId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 4️⃣ Sign any runtime challenge
|
|
39
|
+
await passSigner.credentials(
|
|
40
|
+
await wa.authenticate(challenge, blockNumber),
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 🛠️ API
|
|
45
|
+
|
|
46
|
+
| Method | Returns | Notes |
|
|
47
|
+
| --------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------- |
|
|
48
|
+
| `setup()` | `Promise< this >` | Computes `hashedUserId`. Call once. |
|
|
49
|
+
| `register(blockNo, blockHash, [displayName])` | `Promise<TAttestation<number>>` | Generates a WebAuthn credential and attestation. Throws if `credentialId` already set. |
|
|
50
|
+
| `authenticate(challenge, context)` | `Promise<TPassAuthenticate>` | Signs an arbitrary 32‑byte challenge. Requires `credentialId`. |
|
|
51
|
+
| `getDeviceId(webAuthn) | `Promise<DeviceId>` | `Blake2‑256(credentialId)` wrapped in `Binary`. |
|
|
52
|
+
| `setCredentialId(id)` | `void` | Inject credential id after construction. |
|
|
53
|
+
|
|
54
|
+
> **Type parameter** `<number>` → `context` inside attestations/assertions is the **block number**.
|
|
55
|
+
|
|
56
|
+
## 📝 Persistence Strategy
|
|
57
|
+
|
|
58
|
+
This package **does not** store credential ids. A typical strategy is:
|
|
59
|
+
|
|
60
|
+
1. During **registration**, persist `attestation.publicKey.bytes` keyed by `userId`.
|
|
61
|
+
2. On next load, feed that id into the `WebAuthn` constructor.
|
|
62
|
+
3. For multiple devices per account, maintain an *array* of ids and pick one UI‑side.
|
|
63
|
+
|
|
64
|
+
## ⚠️ Error Handling
|
|
65
|
+
|
|
66
|
+
| Error message | Cause | Fix |
|
|
67
|
+
| ------------------------------ | -------------------------------------------------------- | --------------------------------------------- |
|
|
68
|
+
| `Already have a credentialId…` | Called `register()` when id already present | Skip registration or call with a new instance |
|
|
69
|
+
| `credentialId unknown…` | Tried to authenticate/get device id without a credential | Inject stored id or call `register()` |
|
|
70
|
+
| `DOMException: …` | User dismissed the WebAuthn prompt | Ask user to retry |
|
|
71
|
+
|
|
72
|
+
## 🧳 Dependencies
|
|
73
|
+
|
|
74
|
+
* **@virtonetwork/signer** ≥ 0.10 — interfaces, `KreivoBlockChallenger`, `PassSigner`
|
|
75
|
+
* **@polkadot-api/substrate-bindings** — `Binary`, `Blake2256`
|
|
76
|
+
* Browser with WebAuthn (Chrome ≥ 109, Firefox ≥ 106, Safari ≥ 16)
|
|
77
|
+
|
|
78
|
+
## 🩹 Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# lint & type‑check
|
|
82
|
+
npm run lint && npm run typecheck
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Tests
|
|
86
|
+
|
|
87
|
+
Go to `tests/test.ts` to check out our tests.
|
|
88
|
+
|
|
89
|
+
## 📄 License
|
|
90
|
+
|
|
91
|
+
MIT © Virto Network contributors
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.WebAuthn = exports.KREIVO_AUTHORITY_ID = void 0;
|
|
40
|
+
/**
|
|
41
|
+
* WebAuthn pass‑key authenticator for Virto Network.
|
|
42
|
+
*
|
|
43
|
+
* Exposes a browser‑side implementation of {@link Authenticator} that creates,
|
|
44
|
+
* stores, and uses WebAuthn resident credentials ("passkeys") while producing
|
|
45
|
+
* SCALE‑encoded data structures understood by the Kreivo signer pallet.
|
|
46
|
+
*
|
|
47
|
+
* Responsibilities
|
|
48
|
+
* ─────────────────────────────────────────────────────────
|
|
49
|
+
* • Derive a deterministic `deviceId` from the raw credential id
|
|
50
|
+
* • Emit `TAttestation<number>` during registration
|
|
51
|
+
* • Emit `TPassAuthenticate` during authentication
|
|
52
|
+
* • Never persist the credential mapping; that is delegated to the caller
|
|
53
|
+
*
|
|
54
|
+
* @module WebAuthn
|
|
55
|
+
*/
|
|
56
|
+
var signer_1 = require("@virtonetwork/signer");
|
|
57
|
+
var substrate_bindings_1 = require("@polkadot-api/substrate-bindings");
|
|
58
|
+
var types_ts_1 = require("./types.js");
|
|
59
|
+
var utils_1 = require("polkadot-api/utils");
|
|
60
|
+
/** Fixed authority id for Kreivo pass‑key attestors. */
|
|
61
|
+
exports.KREIVO_AUTHORITY_ID = substrate_bindings_1.Binary.fromText("kreivo_p".padEnd(32, "\0"));
|
|
62
|
+
/**
|
|
63
|
+
* Browser‑side Authenticator that wraps the WebAuthn API.
|
|
64
|
+
*
|
|
65
|
+
* The generic type parameter `<number>` indicates that the **context**
|
|
66
|
+
* carried inside attestations and assertions is the block number that
|
|
67
|
+
* originated the challenge.
|
|
68
|
+
*
|
|
69
|
+
* @implements {Authenticator<number>}
|
|
70
|
+
*/
|
|
71
|
+
var WebAuthn = /** @class */ (function () {
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new WebAuthn helper.
|
|
74
|
+
*
|
|
75
|
+
* @param userId - Logical user identifier (e‑mail, DID, etc.).
|
|
76
|
+
* @param [credentialId] - Raw credential id obtained from a previous
|
|
77
|
+
* registration flow; omit it if the user must enrol a new pass‑key.
|
|
78
|
+
*/
|
|
79
|
+
function WebAuthn(userId, credentialId) {
|
|
80
|
+
this.userId = userId;
|
|
81
|
+
this.credentialId = credentialId;
|
|
82
|
+
/**
|
|
83
|
+
* SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused
|
|
84
|
+
* for all WebAuthn operations.
|
|
85
|
+
*/
|
|
86
|
+
this.hashedUserId = new Uint8Array(32);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Deterministic identifier of the hardware/software authenticator
|
|
90
|
+
* (`deviceId = Blake2‑256(credentialId)`).
|
|
91
|
+
*
|
|
92
|
+
* @returns DeviceId suitable for on‑chain storage.
|
|
93
|
+
* @throws Error If this instance does not yet know a credential id.
|
|
94
|
+
*/
|
|
95
|
+
WebAuthn.getDeviceId = function (wa) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
97
|
+
return __generator(this, function (_a) {
|
|
98
|
+
if (!wa.credentialId) {
|
|
99
|
+
throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
|
|
100
|
+
}
|
|
101
|
+
return [2 /*return*/, substrate_bindings_1.Binary.fromBytes((0, substrate_bindings_1.Blake2256)(wa.credentialId))];
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Injects a credential id discovered by the caller after construction.
|
|
107
|
+
*
|
|
108
|
+
* @param id - Raw credential id obtained from storage or backend.
|
|
109
|
+
*/
|
|
110
|
+
WebAuthn.prototype.setCredentialId = function (id) {
|
|
111
|
+
this.credentialId = id;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Pre‑computes {@link hashedUserId}.
|
|
115
|
+
*
|
|
116
|
+
* Must be awaited **once** before any other interaction.
|
|
117
|
+
* Returns `this` for fluent chaining.
|
|
118
|
+
*/
|
|
119
|
+
WebAuthn.prototype.setup = function () {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
121
|
+
var _a, _b;
|
|
122
|
+
return __generator(this, function (_c) {
|
|
123
|
+
switch (_c.label) {
|
|
124
|
+
case 0:
|
|
125
|
+
_a = this;
|
|
126
|
+
_b = Uint8Array.bind;
|
|
127
|
+
return [4 /*yield*/, crypto.subtle.digest("SHA-256", new TextEncoder().encode(this.userId))];
|
|
128
|
+
case 1:
|
|
129
|
+
_a.hashedUserId = new (_b.apply(Uint8Array, [void 0, _c.sent()]))();
|
|
130
|
+
return [2 /*return*/, this];
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Registers a **new** resident credential (pass‑key) with the user’s
|
|
137
|
+
* authenticator and returns a SCALE‑ready attestation.
|
|
138
|
+
*
|
|
139
|
+
* @param blockNumber - The number of the block whose hash seeds the challenge.
|
|
140
|
+
* @param blockHash - The block hash used to derive a deterministic challenge.
|
|
141
|
+
* @param [displayName=this.userId] - Friendly name shown by the authenticator.
|
|
142
|
+
*
|
|
143
|
+
* @throws Error If this instance already has a credential id.
|
|
144
|
+
* @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object.
|
|
145
|
+
*/
|
|
146
|
+
WebAuthn.prototype.register = function (blockNumber_1, blockHash_1) {
|
|
147
|
+
return __awaiter(this, arguments, void 0, function (blockNumber, blockHash, displayName) {
|
|
148
|
+
var challenger, challenge, credentials, _a, attestationObject, clientDataJSON, getPublicKey, publicKey;
|
|
149
|
+
var _b, _c;
|
|
150
|
+
if (displayName === void 0) { displayName = this.userId; }
|
|
151
|
+
return __generator(this, function (_d) {
|
|
152
|
+
switch (_d.label) {
|
|
153
|
+
case 0:
|
|
154
|
+
if (this.credentialId) {
|
|
155
|
+
throw new Error("Already have a credentialId; no need to register");
|
|
156
|
+
}
|
|
157
|
+
challenger = new signer_1.KreivoBlockChallenger();
|
|
158
|
+
challenge = challenger.generate((0, utils_1.fromHex)(blockHash), new Uint8Array());
|
|
159
|
+
return [4 /*yield*/, navigator.credentials.create({
|
|
160
|
+
publicKey: {
|
|
161
|
+
challenge: challenge,
|
|
162
|
+
rp: { name: "Virto Passkeys" },
|
|
163
|
+
user: {
|
|
164
|
+
id: this.hashedUserId,
|
|
165
|
+
name: this.userId,
|
|
166
|
+
displayName: displayName,
|
|
167
|
+
},
|
|
168
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 /* ES256 */ }],
|
|
169
|
+
authenticatorSelection: { userVerification: "preferred" },
|
|
170
|
+
attestation: "none",
|
|
171
|
+
timeout: 60000,
|
|
172
|
+
},
|
|
173
|
+
})];
|
|
174
|
+
case 1:
|
|
175
|
+
credentials = (_d.sent());
|
|
176
|
+
_a = credentials.response, attestationObject = _a.attestationObject, clientDataJSON = _a.clientDataJSON, getPublicKey = _a.getPublicKey;
|
|
177
|
+
// Save raw credential id for future auth calls
|
|
178
|
+
this.credentialId = new Uint8Array(credentials.rawId);
|
|
179
|
+
publicKey = getPublicKey();
|
|
180
|
+
if (!publicKey) {
|
|
181
|
+
throw new Error("The credentials don't expose a public key. Please use another authenticator device.");
|
|
182
|
+
}
|
|
183
|
+
_b = {};
|
|
184
|
+
_c = {
|
|
185
|
+
authorityId: exports.KREIVO_AUTHORITY_ID
|
|
186
|
+
};
|
|
187
|
+
return [4 /*yield*/, WebAuthn.getDeviceId(this)];
|
|
188
|
+
case 2: return [2 /*return*/, (_b.meta = (_c.deviceId = _d.sent(),
|
|
189
|
+
_c.context = blockNumber,
|
|
190
|
+
_c),
|
|
191
|
+
_b.authenticatorData = substrate_bindings_1.Binary.fromBytes(new Uint8Array(attestationObject)),
|
|
192
|
+
_b.clientData = substrate_bindings_1.Binary.fromBytes(new Uint8Array(clientDataJSON)),
|
|
193
|
+
_b.publicKey = substrate_bindings_1.Binary.fromBytes(new Uint8Array(publicKey)),
|
|
194
|
+
_b)];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Signs an arbitrary challenge with the pass‑key and produces a
|
|
201
|
+
* {@link TPassAuthenticate} payload understood by `PassSigner`.
|
|
202
|
+
*
|
|
203
|
+
* @param challenge - 32‑byte buffer supplied by the runtime.
|
|
204
|
+
* @param context - Block number (or any numeric context expected by the pallet).
|
|
205
|
+
*
|
|
206
|
+
* @returns SCALE‑encoded authentication payload.
|
|
207
|
+
* @throws Error If no credential id is available.
|
|
208
|
+
*/
|
|
209
|
+
WebAuthn.prototype.authenticate = function (challenge, context) {
|
|
210
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
211
|
+
var publicKey, cred, _a, authenticatorData, clientDataJSON, signature, assertion;
|
|
212
|
+
var _b;
|
|
213
|
+
return __generator(this, function (_c) {
|
|
214
|
+
switch (_c.label) {
|
|
215
|
+
case 0:
|
|
216
|
+
if (!this.credentialId) {
|
|
217
|
+
throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
|
|
218
|
+
}
|
|
219
|
+
publicKey = {
|
|
220
|
+
challenge: challenge,
|
|
221
|
+
allowCredentials: [
|
|
222
|
+
{
|
|
223
|
+
id: this.credentialId.buffer,
|
|
224
|
+
type: "public-key",
|
|
225
|
+
transports: ["usb", "ble", "nfc", "internal"],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
userVerification: "preferred",
|
|
229
|
+
timeout: 60000,
|
|
230
|
+
};
|
|
231
|
+
return [4 /*yield*/, navigator.credentials.get({
|
|
232
|
+
publicKey: publicKey,
|
|
233
|
+
})];
|
|
234
|
+
case 1:
|
|
235
|
+
cred = (_c.sent());
|
|
236
|
+
_a = cred.response, authenticatorData = _a.authenticatorData, clientDataJSON = _a.clientDataJSON, signature = _a.signature;
|
|
237
|
+
assertion = {
|
|
238
|
+
meta: {
|
|
239
|
+
authorityId: exports.KREIVO_AUTHORITY_ID,
|
|
240
|
+
userId: substrate_bindings_1.Binary.fromBytes(this.hashedUserId),
|
|
241
|
+
context: context,
|
|
242
|
+
},
|
|
243
|
+
authenticatorData: substrate_bindings_1.Binary.fromBytes(new Uint8Array(authenticatorData)),
|
|
244
|
+
clientData: substrate_bindings_1.Binary.fromBytes(new Uint8Array(clientDataJSON)),
|
|
245
|
+
signature: substrate_bindings_1.Binary.fromBytes(new Uint8Array(signature)),
|
|
246
|
+
};
|
|
247
|
+
_b = {};
|
|
248
|
+
return [4 /*yield*/, WebAuthn.getDeviceId(this)];
|
|
249
|
+
case 2: return [2 /*return*/, (_b.deviceId = _c.sent(),
|
|
250
|
+
_b.credentials = {
|
|
251
|
+
tag: "WebAuthn",
|
|
252
|
+
value: types_ts_1.Assertion.enc(assertion),
|
|
253
|
+
},
|
|
254
|
+
_b)];
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
return WebAuthn;
|
|
260
|
+
}());
|
|
261
|
+
exports.WebAuthn = WebAuthn;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Assertion = exports.Attestation = void 0;
|
|
4
|
+
var substrate_bindings_1 = require("@polkadot-api/substrate-bindings");
|
|
5
|
+
var scale_ts_1 = require("scale-ts");
|
|
6
|
+
var AttestationMeta = (0, scale_ts_1.Struct)({
|
|
7
|
+
authorityId: (0, substrate_bindings_1.Bin)(32),
|
|
8
|
+
deviceId: (0, substrate_bindings_1.Bin)(32),
|
|
9
|
+
context: scale_ts_1.u32,
|
|
10
|
+
});
|
|
11
|
+
exports.Attestation = (0, scale_ts_1.Struct)({
|
|
12
|
+
meta: AttestationMeta,
|
|
13
|
+
authenticatorData: (0, substrate_bindings_1.Bin)(),
|
|
14
|
+
clientData: (0, substrate_bindings_1.Bin)(),
|
|
15
|
+
publicKey: (0, substrate_bindings_1.Bin)(),
|
|
16
|
+
});
|
|
17
|
+
var AssertionMeta = (0, scale_ts_1.Struct)({
|
|
18
|
+
authorityId: (0, substrate_bindings_1.Bin)(32),
|
|
19
|
+
userId: (0, substrate_bindings_1.Bin)(32),
|
|
20
|
+
context: scale_ts_1.u32,
|
|
21
|
+
});
|
|
22
|
+
exports.Assertion = (0, scale_ts_1.Struct)({
|
|
23
|
+
meta: AssertionMeta,
|
|
24
|
+
authenticatorData: (0, substrate_bindings_1.Bin)(),
|
|
25
|
+
clientData: (0, substrate_bindings_1.Bin)(),
|
|
26
|
+
signature: (0, substrate_bindings_1.Bin)(),
|
|
27
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn pass‑key authenticator for Virto Network.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a browser‑side implementation of {@link Authenticator} that creates,
|
|
5
|
+
* stores, and uses WebAuthn resident credentials ("passkeys") while producing
|
|
6
|
+
* SCALE‑encoded data structures understood by the Kreivo signer pallet.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities
|
|
9
|
+
* ─────────────────────────────────────────────────────────
|
|
10
|
+
* • Derive a deterministic `deviceId` from the raw credential id
|
|
11
|
+
* • Emit `TAttestation<number>` during registration
|
|
12
|
+
* • Emit `TPassAuthenticate` during authentication
|
|
13
|
+
* • Never persist the credential mapping; that is delegated to the caller
|
|
14
|
+
*
|
|
15
|
+
* @module WebAuthn
|
|
16
|
+
*/
|
|
17
|
+
import { Authenticator, DeviceId } from "@virtonetwork/signer";
|
|
18
|
+
import { Binary } from "@polkadot-api/substrate-bindings";
|
|
19
|
+
import type { BlockHash, TAttestation } from "./types.ts";
|
|
20
|
+
import type { TPassAuthenticate } from "@virtonetwork/signer";
|
|
21
|
+
/** Fixed authority id for Kreivo pass‑key attestors. */
|
|
22
|
+
export declare const KREIVO_AUTHORITY_ID: Binary;
|
|
23
|
+
/**
|
|
24
|
+
* Browser‑side Authenticator that wraps the WebAuthn API.
|
|
25
|
+
*
|
|
26
|
+
* The generic type parameter `<number>` indicates that the **context**
|
|
27
|
+
* carried inside attestations and assertions is the block number that
|
|
28
|
+
* originated the challenge.
|
|
29
|
+
*
|
|
30
|
+
* @implements {Authenticator<number>}
|
|
31
|
+
*/
|
|
32
|
+
export declare class WebAuthn implements Authenticator<number> {
|
|
33
|
+
readonly userId: string;
|
|
34
|
+
credentialId?: Uint8Array | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused
|
|
37
|
+
* for all WebAuthn operations.
|
|
38
|
+
*/
|
|
39
|
+
hashedUserId: Uint8Array;
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new WebAuthn helper.
|
|
42
|
+
*
|
|
43
|
+
* @param userId - Logical user identifier (e‑mail, DID, etc.).
|
|
44
|
+
* @param [credentialId] - Raw credential id obtained from a previous
|
|
45
|
+
* registration flow; omit it if the user must enrol a new pass‑key.
|
|
46
|
+
*/
|
|
47
|
+
constructor(userId: string, credentialId?: Uint8Array | undefined);
|
|
48
|
+
/**
|
|
49
|
+
* Deterministic identifier of the hardware/software authenticator
|
|
50
|
+
* (`deviceId = Blake2‑256(credentialId)`).
|
|
51
|
+
*
|
|
52
|
+
* @returns DeviceId suitable for on‑chain storage.
|
|
53
|
+
* @throws Error If this instance does not yet know a credential id.
|
|
54
|
+
*/
|
|
55
|
+
static getDeviceId(wa: WebAuthn): Promise<DeviceId>;
|
|
56
|
+
/**
|
|
57
|
+
* Injects a credential id discovered by the caller after construction.
|
|
58
|
+
*
|
|
59
|
+
* @param id - Raw credential id obtained from storage or backend.
|
|
60
|
+
*/
|
|
61
|
+
setCredentialId(id: Uint8Array): void;
|
|
62
|
+
/**
|
|
63
|
+
* Pre‑computes {@link hashedUserId}.
|
|
64
|
+
*
|
|
65
|
+
* Must be awaited **once** before any other interaction.
|
|
66
|
+
* Returns `this` for fluent chaining.
|
|
67
|
+
*/
|
|
68
|
+
setup(): Promise<this>;
|
|
69
|
+
/**
|
|
70
|
+
* Registers a **new** resident credential (pass‑key) with the user’s
|
|
71
|
+
* authenticator and returns a SCALE‑ready attestation.
|
|
72
|
+
*
|
|
73
|
+
* @param blockNumber - The number of the block whose hash seeds the challenge.
|
|
74
|
+
* @param blockHash - The block hash used to derive a deterministic challenge.
|
|
75
|
+
* @param [displayName=this.userId] - Friendly name shown by the authenticator.
|
|
76
|
+
*
|
|
77
|
+
* @throws Error If this instance already has a credential id.
|
|
78
|
+
* @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object.
|
|
79
|
+
*/
|
|
80
|
+
register(blockNumber: number, blockHash: BlockHash, displayName?: string): Promise<TAttestation<number>>;
|
|
81
|
+
/**
|
|
82
|
+
* Signs an arbitrary challenge with the pass‑key and produces a
|
|
83
|
+
* {@link TPassAuthenticate} payload understood by `PassSigner`.
|
|
84
|
+
*
|
|
85
|
+
* @param challenge - 32‑byte buffer supplied by the runtime.
|
|
86
|
+
* @param context - Block number (or any numeric context expected by the pallet).
|
|
87
|
+
*
|
|
88
|
+
* @returns SCALE‑encoded authentication payload.
|
|
89
|
+
* @throws Error If no credential id is available.
|
|
90
|
+
*/
|
|
91
|
+
authenticate(challenge: Uint8Array, context: number): Promise<TPassAuthenticate>;
|
|
92
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn pass‑key authenticator for Virto Network.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a browser‑side implementation of {@link Authenticator} that creates,
|
|
5
|
+
* stores, and uses WebAuthn resident credentials ("passkeys") while producing
|
|
6
|
+
* SCALE‑encoded data structures understood by the Kreivo signer pallet.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities
|
|
9
|
+
* ─────────────────────────────────────────────────────────
|
|
10
|
+
* • Derive a deterministic `deviceId` from the raw credential id
|
|
11
|
+
* • Emit `TAttestation<number>` during registration
|
|
12
|
+
* • Emit `TPassAuthenticate` during authentication
|
|
13
|
+
* • Never persist the credential mapping; that is delegated to the caller
|
|
14
|
+
*
|
|
15
|
+
* @module WebAuthn
|
|
16
|
+
*/
|
|
17
|
+
import { KreivoBlockChallenger, } from "@virtonetwork/signer";
|
|
18
|
+
import { Binary, Blake2256 } from "@polkadot-api/substrate-bindings";
|
|
19
|
+
import { Assertion } from "./types.js";
|
|
20
|
+
import { fromHex } from "polkadot-api/utils";
|
|
21
|
+
/** Fixed authority id for Kreivo pass‑key attestors. */
|
|
22
|
+
export const KREIVO_AUTHORITY_ID = Binary.fromText("kreivo_p".padEnd(32, "\0"));
|
|
23
|
+
/**
|
|
24
|
+
* Browser‑side Authenticator that wraps the WebAuthn API.
|
|
25
|
+
*
|
|
26
|
+
* The generic type parameter `<number>` indicates that the **context**
|
|
27
|
+
* carried inside attestations and assertions is the block number that
|
|
28
|
+
* originated the challenge.
|
|
29
|
+
*
|
|
30
|
+
* @implements {Authenticator<number>}
|
|
31
|
+
*/
|
|
32
|
+
export class WebAuthn {
|
|
33
|
+
userId;
|
|
34
|
+
credentialId;
|
|
35
|
+
/**
|
|
36
|
+
* SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused
|
|
37
|
+
* for all WebAuthn operations.
|
|
38
|
+
*/
|
|
39
|
+
hashedUserId = new Uint8Array(32);
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new WebAuthn helper.
|
|
42
|
+
*
|
|
43
|
+
* @param userId - Logical user identifier (e‑mail, DID, etc.).
|
|
44
|
+
* @param [credentialId] - Raw credential id obtained from a previous
|
|
45
|
+
* registration flow; omit it if the user must enrol a new pass‑key.
|
|
46
|
+
*/
|
|
47
|
+
constructor(userId, credentialId) {
|
|
48
|
+
this.userId = userId;
|
|
49
|
+
this.credentialId = credentialId;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic identifier of the hardware/software authenticator
|
|
53
|
+
* (`deviceId = Blake2‑256(credentialId)`).
|
|
54
|
+
*
|
|
55
|
+
* @returns DeviceId suitable for on‑chain storage.
|
|
56
|
+
* @throws Error If this instance does not yet know a credential id.
|
|
57
|
+
*/
|
|
58
|
+
static async getDeviceId(wa) {
|
|
59
|
+
if (!wa.credentialId) {
|
|
60
|
+
throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
|
|
61
|
+
}
|
|
62
|
+
return Binary.fromBytes(Blake2256(wa.credentialId));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Injects a credential id discovered by the caller after construction.
|
|
66
|
+
*
|
|
67
|
+
* @param id - Raw credential id obtained from storage or backend.
|
|
68
|
+
*/
|
|
69
|
+
setCredentialId(id) {
|
|
70
|
+
this.credentialId = id;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Pre‑computes {@link hashedUserId}.
|
|
74
|
+
*
|
|
75
|
+
* Must be awaited **once** before any other interaction.
|
|
76
|
+
* Returns `this` for fluent chaining.
|
|
77
|
+
*/
|
|
78
|
+
async setup() {
|
|
79
|
+
this.hashedUserId = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(this.userId)));
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Registers a **new** resident credential (pass‑key) with the user’s
|
|
84
|
+
* authenticator and returns a SCALE‑ready attestation.
|
|
85
|
+
*
|
|
86
|
+
* @param blockNumber - The number of the block whose hash seeds the challenge.
|
|
87
|
+
* @param blockHash - The block hash used to derive a deterministic challenge.
|
|
88
|
+
* @param [displayName=this.userId] - Friendly name shown by the authenticator.
|
|
89
|
+
*
|
|
90
|
+
* @throws Error If this instance already has a credential id.
|
|
91
|
+
* @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object.
|
|
92
|
+
*/
|
|
93
|
+
async register(blockNumber, blockHash, displayName = this.userId) {
|
|
94
|
+
if (this.credentialId) {
|
|
95
|
+
throw new Error("Already have a credentialId; no need to register");
|
|
96
|
+
}
|
|
97
|
+
const challenger = new KreivoBlockChallenger();
|
|
98
|
+
const challenge = challenger.generate(fromHex(blockHash), new Uint8Array());
|
|
99
|
+
const credentials = (await navigator.credentials.create({
|
|
100
|
+
publicKey: {
|
|
101
|
+
challenge,
|
|
102
|
+
rp: { name: "Virto Passkeys" },
|
|
103
|
+
user: {
|
|
104
|
+
id: this.hashedUserId,
|
|
105
|
+
name: this.userId,
|
|
106
|
+
displayName,
|
|
107
|
+
},
|
|
108
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 /* ES256 */ }],
|
|
109
|
+
authenticatorSelection: { userVerification: "preferred" },
|
|
110
|
+
attestation: "none",
|
|
111
|
+
timeout: 60_000,
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
114
|
+
const { attestationObject, clientDataJSON, getPublicKey } = credentials.response;
|
|
115
|
+
// Save raw credential id for future auth calls
|
|
116
|
+
this.credentialId = new Uint8Array(credentials.rawId);
|
|
117
|
+
// Ensure publicKey is obtained in the registration process.
|
|
118
|
+
const publicKey = getPublicKey();
|
|
119
|
+
if (!publicKey) {
|
|
120
|
+
throw new Error("The credentials don't expose a public key. Please use another authenticator device.");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
meta: {
|
|
124
|
+
authorityId: KREIVO_AUTHORITY_ID,
|
|
125
|
+
deviceId: await WebAuthn.getDeviceId(this),
|
|
126
|
+
context: blockNumber,
|
|
127
|
+
},
|
|
128
|
+
authenticatorData: Binary.fromBytes(new Uint8Array(attestationObject)),
|
|
129
|
+
clientData: Binary.fromBytes(new Uint8Array(clientDataJSON)),
|
|
130
|
+
publicKey: Binary.fromBytes(new Uint8Array(publicKey)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Signs an arbitrary challenge with the pass‑key and produces a
|
|
135
|
+
* {@link TPassAuthenticate} payload understood by `PassSigner`.
|
|
136
|
+
*
|
|
137
|
+
* @param challenge - 32‑byte buffer supplied by the runtime.
|
|
138
|
+
* @param context - Block number (or any numeric context expected by the pallet).
|
|
139
|
+
*
|
|
140
|
+
* @returns SCALE‑encoded authentication payload.
|
|
141
|
+
* @throws Error If no credential id is available.
|
|
142
|
+
*/
|
|
143
|
+
async authenticate(challenge, context) {
|
|
144
|
+
if (!this.credentialId) {
|
|
145
|
+
throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
|
|
146
|
+
}
|
|
147
|
+
const publicKey = {
|
|
148
|
+
challenge,
|
|
149
|
+
allowCredentials: [
|
|
150
|
+
{
|
|
151
|
+
id: this.credentialId.buffer,
|
|
152
|
+
type: "public-key",
|
|
153
|
+
transports: ["usb", "ble", "nfc", "internal"],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
userVerification: "preferred",
|
|
157
|
+
timeout: 60_000,
|
|
158
|
+
};
|
|
159
|
+
const cred = (await navigator.credentials.get({
|
|
160
|
+
publicKey,
|
|
161
|
+
}));
|
|
162
|
+
const { authenticatorData, clientDataJSON, signature } = cred.response;
|
|
163
|
+
const assertion = {
|
|
164
|
+
meta: {
|
|
165
|
+
authorityId: KREIVO_AUTHORITY_ID,
|
|
166
|
+
userId: Binary.fromBytes(this.hashedUserId),
|
|
167
|
+
context,
|
|
168
|
+
},
|
|
169
|
+
authenticatorData: Binary.fromBytes(new Uint8Array(authenticatorData)),
|
|
170
|
+
clientData: Binary.fromBytes(new Uint8Array(clientDataJSON)),
|
|
171
|
+
signature: Binary.fromBytes(new Uint8Array(signature)),
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
deviceId: await WebAuthn.getDeviceId(this),
|
|
175
|
+
credentials: {
|
|
176
|
+
tag: "WebAuthn",
|
|
177
|
+
value: Assertion.enc(assertion),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AuthorityId, DeviceId, HashedUserId } from "@virtonetwork/signer";
|
|
2
|
+
import { Binary, HexString } from "@polkadot-api/substrate-bindings";
|
|
3
|
+
import { Codec } from "scale-ts";
|
|
4
|
+
export type BlockHash = HexString;
|
|
5
|
+
export type TAttestationMeta<Cx> = {
|
|
6
|
+
authorityId: AuthorityId;
|
|
7
|
+
deviceId: DeviceId;
|
|
8
|
+
context: Cx;
|
|
9
|
+
};
|
|
10
|
+
export type TAttestation<Cx> = {
|
|
11
|
+
meta: TAttestationMeta<Cx>;
|
|
12
|
+
authenticatorData: Binary;
|
|
13
|
+
clientData: Binary;
|
|
14
|
+
publicKey: Binary;
|
|
15
|
+
};
|
|
16
|
+
export declare const Attestation: Codec<TAttestation<number>>;
|
|
17
|
+
export type TAssertionMeta<Cx> = {
|
|
18
|
+
authorityId: AuthorityId;
|
|
19
|
+
userId: HashedUserId;
|
|
20
|
+
context: Cx;
|
|
21
|
+
};
|
|
22
|
+
export type TAssertion<Cx> = {
|
|
23
|
+
meta: TAssertionMeta<Cx>;
|
|
24
|
+
authenticatorData: Binary;
|
|
25
|
+
clientData: Binary;
|
|
26
|
+
signature: Binary;
|
|
27
|
+
};
|
|
28
|
+
export declare const Assertion: Codec<TAssertion<number>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Bin } from "@polkadot-api/substrate-bindings";
|
|
2
|
+
import { Struct, u32 } from "scale-ts";
|
|
3
|
+
const AttestationMeta = Struct({
|
|
4
|
+
authorityId: Bin(32),
|
|
5
|
+
deviceId: Bin(32),
|
|
6
|
+
context: u32,
|
|
7
|
+
});
|
|
8
|
+
export const Attestation = Struct({
|
|
9
|
+
meta: AttestationMeta,
|
|
10
|
+
authenticatorData: Bin(),
|
|
11
|
+
clientData: Bin(),
|
|
12
|
+
publicKey: Bin(),
|
|
13
|
+
});
|
|
14
|
+
const AssertionMeta = Struct({
|
|
15
|
+
authorityId: Bin(32),
|
|
16
|
+
userId: Bin(32),
|
|
17
|
+
context: u32,
|
|
18
|
+
});
|
|
19
|
+
export const Assertion = Struct({
|
|
20
|
+
meta: AssertionMeta,
|
|
21
|
+
authenticatorData: Bin(),
|
|
22
|
+
clientData: Bin(),
|
|
23
|
+
signature: Bin(),
|
|
24
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@virtonetwork/authenticators-webauthn",
|
|
3
|
+
"description": "An Authenticator compatible with KreivoPassSigner that uses the WebAuthn standard",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"module": "./dist/esm/index.js",
|
|
13
|
+
"import": "./dist/esm/index.js",
|
|
14
|
+
"require": "./dist/cjs/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/cjs/index.js",
|
|
19
|
+
"module": "./dist/esm/index.js",
|
|
20
|
+
"browser": "./dist/esm/index.js",
|
|
21
|
+
"types": "./dist/esm/index.d.ts",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --loader ts-node/esm test/test.ts",
|
|
24
|
+
"build": "tsc && tsc -p tsconfig.cjs.json",
|
|
25
|
+
"prepack": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"author": "Virto Network <contact@virto.networks>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"virto-sdk",
|
|
31
|
+
"signer",
|
|
32
|
+
"papi",
|
|
33
|
+
"scale",
|
|
34
|
+
"polkadot.js",
|
|
35
|
+
"polkadot"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@simplewebauthn/server": "^13.1.1",
|
|
39
|
+
"@virtonetwork/signer": "^1.0.4",
|
|
40
|
+
"nid-webauthn-emulator": "^0.2.4"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"esmock": "^2.7.0",
|
|
44
|
+
"sinon": "^20.0.0",
|
|
45
|
+
"ts-node": "^10.9.2"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"url": "https://github.com/virto-network/papi-signers",
|
|
49
|
+
"directory": "authenticators/webauthn"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"registry": "https://registry.npmjs.org/",
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|