wire-mesh-core 4.1.1 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/node-identity.cjs +7 -1
- package/dist/adapters/node-identity.d.cts +1 -1
- package/dist/adapters/node-identity.d.mts +1 -1
- package/dist/adapters/node-identity.mjs +7 -1
- package/dist/adapters/threshold-identity.d.cts +1 -1
- package/dist/adapters/threshold-identity.d.mts +1 -1
- package/dist/domain/capability-grant.d.cts +1 -1
- package/dist/domain/capability-grant.d.mts +1 -1
- package/dist/domain/capability-request.d.cts +1 -1
- package/dist/domain/capability-request.d.mts +1 -1
- package/dist/domain/data-sync.d.cts +1 -1
- package/dist/domain/data-sync.d.mts +1 -1
- package/dist/domain/ed25519-identity-key.cjs +152 -0
- package/dist/domain/ed25519-identity-key.d.cts +13 -0
- package/dist/domain/ed25519-identity-key.d.mts +13 -0
- package/dist/domain/ed25519-identity-key.mjs +150 -0
- package/dist/domain/mesh-session.cjs +105 -49
- package/dist/domain/mesh-session.d.cts +3 -3
- package/dist/domain/mesh-session.d.mts +3 -3
- package/dist/domain/mesh-session.mjs +105 -49
- package/dist/domain/notice-board.d.cts +1 -1
- package/dist/domain/notice-board.d.mts +1 -1
- package/dist/domain/peer-advert.d.cts +1 -1
- package/dist/domain/peer-advert.d.mts +1 -1
- package/dist/domain/room-rekey.d.cts +1 -1
- package/dist/domain/room-rekey.d.mts +1 -1
- package/dist/domain/room.cjs +2 -1
- package/dist/domain/room.d.cts +1 -1
- package/dist/domain/room.d.mts +1 -1
- package/dist/domain/room.mjs +2 -1
- package/dist/domain/tokens.cjs +3 -2
- package/dist/domain/tokens.d.cts +1 -1
- package/dist/domain/tokens.d.mts +1 -1
- package/dist/domain/tokens.mjs +3 -2
- package/dist/{identity-C5jq53NV.d.cts → identity-CthkHRzB.d.cts} +3 -1
- package/dist/{identity-BioGOV9M.d.mts → identity-zLL5kQGJ.d.mts} +3 -1
- package/dist/ports/identity.cjs +10 -0
- package/dist/ports/identity.d.cts +2 -2
- package/dist/ports/identity.d.mts +2 -2
- package/dist/ports/identity.mjs +10 -1
- package/package.json +5 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_ed25519_identity_key = require("../domain/ed25519-identity-key.cjs");
|
|
2
3
|
const require_ports_identity = require("../ports/identity.cjs");
|
|
3
4
|
let node_crypto = require("node:crypto");
|
|
4
5
|
//#region src/adapters/node-identity.ts
|
|
@@ -38,6 +39,10 @@ function importedKeyId(key) {
|
|
|
38
39
|
}
|
|
39
40
|
async function importPublicKey(key) {
|
|
40
41
|
if (key["public-key"].length !== publicKeySize(key.alg)) throw new require_ports_identity.IdentityKeyError("invalid_key", `identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
|
|
42
|
+
if (key.alg === EDDSA) {
|
|
43
|
+
const fault = require_domain_ed25519_identity_key.ed25519KeyFault(key["public-key"]);
|
|
44
|
+
if (fault !== void 0) throw new require_ports_identity.IdentityKeyError("invalid_key", `identity-key public-key is not a usable Ed25519 key (${fault})`);
|
|
45
|
+
}
|
|
41
46
|
const id = importedKeyId(key);
|
|
42
47
|
const held = importedKeys.get(id);
|
|
43
48
|
if (held !== void 0) {
|
|
@@ -54,7 +59,7 @@ async function importPublicKey(key) {
|
|
|
54
59
|
}
|
|
55
60
|
return pending;
|
|
56
61
|
}
|
|
57
|
-
/** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519
|
|
62
|
+
/** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519 key is checked before this by `ed25519KeyFault`, since the import itself accepts keys that are not points. */
|
|
58
63
|
async function importRawPublicKey(key, algorithm) {
|
|
59
64
|
try {
|
|
60
65
|
return await node_crypto.webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), algorithm, false, ["verify"]);
|
|
@@ -75,6 +80,7 @@ async function deriveDeviceId(publicKey) {
|
|
|
75
80
|
}
|
|
76
81
|
async function verifyWithPublicKey(key, message, signature) {
|
|
77
82
|
const cryptoKey = await importPublicKey(key);
|
|
83
|
+
if (key.alg === EDDSA && !require_domain_ed25519_identity_key.ed25519SignatureIsStrict(signature)) return false;
|
|
78
84
|
return node_crypto.webcrypto.subtle.verify(algParams(key.alg), cryptoKey, toBufferSource(signature), toBufferSource(message));
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ed25519KeyFault, ed25519SignatureIsStrict } from "../domain/ed25519-identity-key.mjs";
|
|
1
2
|
import { IdentityKeyError } from "../ports/identity.mjs";
|
|
2
3
|
import { webcrypto } from "node:crypto";
|
|
3
4
|
//#region src/adapters/node-identity.ts
|
|
@@ -37,6 +38,10 @@ function importedKeyId(key) {
|
|
|
37
38
|
}
|
|
38
39
|
async function importPublicKey(key) {
|
|
39
40
|
if (key["public-key"].length !== publicKeySize(key.alg)) throw new IdentityKeyError("invalid_key", `identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
|
|
41
|
+
if (key.alg === EDDSA) {
|
|
42
|
+
const fault = ed25519KeyFault(key["public-key"]);
|
|
43
|
+
if (fault !== void 0) throw new IdentityKeyError("invalid_key", `identity-key public-key is not a usable Ed25519 key (${fault})`);
|
|
44
|
+
}
|
|
40
45
|
const id = importedKeyId(key);
|
|
41
46
|
const held = importedKeys.get(id);
|
|
42
47
|
if (held !== void 0) {
|
|
@@ -53,7 +58,7 @@ async function importPublicKey(key) {
|
|
|
53
58
|
}
|
|
54
59
|
return pending;
|
|
55
60
|
}
|
|
56
|
-
/** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519
|
|
61
|
+
/** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519 key is checked before this by `ed25519KeyFault`, since the import itself accepts keys that are not points. */
|
|
57
62
|
async function importRawPublicKey(key, algorithm) {
|
|
58
63
|
try {
|
|
59
64
|
return await webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), algorithm, false, ["verify"]);
|
|
@@ -74,6 +79,7 @@ async function deriveDeviceId(publicKey) {
|
|
|
74
79
|
}
|
|
75
80
|
async function verifyWithPublicKey(key, message, signature) {
|
|
76
81
|
const cryptoKey = await importPublicKey(key);
|
|
82
|
+
if (key.alg === EDDSA && !ed25519SignatureIsStrict(signature)) return false;
|
|
77
83
|
return webcrypto.subtle.verify(algParams(key.alg), cryptoKey, toBufferSource(signature), toBufferSource(message));
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { DeviceKeyed } from "./threshold-wasm.cjs";
|
|
4
4
|
import { ThresholdSubject } from "../domain/threshold-subject.cjs";
|
|
5
5
|
//#region src/adapters/threshold-identity.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { DeviceKeyed } from "./threshold-wasm.mjs";
|
|
4
4
|
import { ThresholdSubject } from "../domain/threshold-subject.mjs";
|
|
5
5
|
//#region src/adapters/threshold-identity.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-lg4TtUxm.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-CrieP1WI.mjs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
|
|
3
|
-
import { r as IdentityPort } from "../identity-
|
|
3
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
4
4
|
//#region src/domain/data-sync.d.ts
|
|
5
5
|
export interface DataSyncOptions {
|
|
6
6
|
identity: IdentityPort;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
|
|
3
|
-
import { r as IdentityPort } from "../identity-
|
|
3
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
4
4
|
//#region src/domain/data-sync.d.ts
|
|
5
5
|
export interface DataSyncOptions {
|
|
6
6
|
identity: IdentityPort;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_device_id = require("./device-id.cjs");
|
|
3
|
+
//#region src/domain/ed25519-identity-key.ts
|
|
4
|
+
/**
|
|
5
|
+
* What an Ed25519 identity-key must be, and what an Ed25519 signature under one must look like, decided here and nowhere else, before the platform's crypto library is consulted.
|
|
6
|
+
*
|
|
7
|
+
* A key is usable when it is exactly 32 bytes, is the canonical encoding of a point on the curve (its y coordinate is below the field prime, and it decodes), is not a point of small order (the eight points of the torsion subgroup, the identity element among them), and lies in the prime-order subgroup (it has no torsion component). A signature is acceptable to verify when its S is below the group order and its R is a canonical encoding that is not a small-order point. The signature equation itself is the cofactorless one, `[S]B = R + [k]A` compared as encodings, which Web Crypto's Ed25519 verification does on the platforms the conformance vectors are run on: a platform that verifies differently fails the mixed-order vectors there. The Rust crates apply the same rules through ed25519-dalek's `is_weak`, `is_torsion_free` and `verify_strict`.
|
|
8
|
+
*/
|
|
9
|
+
const BITS_PER_BYTE = 8;
|
|
10
|
+
const THREE = 3;
|
|
11
|
+
const FOUR = 4;
|
|
12
|
+
const EIGHT = 8;
|
|
13
|
+
const FIELD_BITS = 255;
|
|
14
|
+
const PRIME_OFFSET = 19;
|
|
15
|
+
const ORDER_BITS = 252;
|
|
16
|
+
const PUBLIC_KEY_BYTES = 32;
|
|
17
|
+
const SIGNATURE_BYTES = 64;
|
|
18
|
+
const BYTE_BITS = BigInt(BITS_PER_BYTE);
|
|
19
|
+
const SIGN_BIT_BYTE = 31;
|
|
20
|
+
const SIGN_BIT_MASK = 127;
|
|
21
|
+
const ZERO = BigInt(0);
|
|
22
|
+
const ONE = BigInt(1);
|
|
23
|
+
const TWO = BigInt(2);
|
|
24
|
+
/** The field prime, 2^255 - 19. */
|
|
25
|
+
const FIELD_PRIME = (ONE << BigInt(FIELD_BITS)) - BigInt(PRIME_OFFSET);
|
|
26
|
+
/** The order of the prime-order subgroup. */
|
|
27
|
+
const GROUP_ORDER = (ONE << BigInt(ORDER_BITS)) + BigInt("27742317777372353535851937790883648493");
|
|
28
|
+
/** The curve constant d = -121665 / 121666, as a field element. */
|
|
29
|
+
const CURVE_D = BigInt("37095705934669439343138083508754565189542113879843219016388785533085940283555");
|
|
30
|
+
/**
|
|
31
|
+
* The y coordinates of the eight small-order points: 0 (two points of order 4), 1 (the identity element), -1 (order 2) and the two pairs of order 8. Both signs of x share each y, so a key is small-order exactly when its y is one of these, whatever its sign bit.
|
|
32
|
+
*/
|
|
33
|
+
const SMALL_ORDER_Y = new Set([
|
|
34
|
+
"0",
|
|
35
|
+
"1",
|
|
36
|
+
"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec",
|
|
37
|
+
"5fc536d880238b13933c6d305acdfd5f098eff289f4c345b027b2c28f95e826",
|
|
38
|
+
"7a03ac9277fdc74ec6cc392cfa53202a0f67100d760b3cba4fd84d3d706a17c7"
|
|
39
|
+
].map((hex) => BigInt(`0x${hex}`)));
|
|
40
|
+
function littleEndian(bytes) {
|
|
41
|
+
let value = ZERO;
|
|
42
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) value = value << BYTE_BITS | BigInt(bytes[index] ?? 0);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/** The y coordinate an encoding carries: the 255 low bits, with the sign bit of x cleared. */
|
|
46
|
+
function encodedY(encoding) {
|
|
47
|
+
const bytes = Uint8Array.from(encoding);
|
|
48
|
+
bytes[SIGN_BIT_BYTE] = (bytes[SIGN_BIT_BYTE] ?? 0) & SIGN_BIT_MASK;
|
|
49
|
+
return littleEndian(bytes);
|
|
50
|
+
}
|
|
51
|
+
function modPow(base, exponent) {
|
|
52
|
+
let result = ONE;
|
|
53
|
+
let factor = base % FIELD_PRIME;
|
|
54
|
+
let remaining = exponent;
|
|
55
|
+
while (remaining > ZERO) {
|
|
56
|
+
if ((remaining & ONE) === ONE) result = result * factor % FIELD_PRIME;
|
|
57
|
+
factor = factor * factor % FIELD_PRIME;
|
|
58
|
+
remaining >>= ONE;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function mod(value) {
|
|
63
|
+
const reduced = value % FIELD_PRIME;
|
|
64
|
+
return reduced >= ZERO ? reduced : reduced + FIELD_PRIME;
|
|
65
|
+
}
|
|
66
|
+
/** sqrt(-1) modulo the prime: 2^((p - 1) / 4). */
|
|
67
|
+
const SQRT_MINUS_ONE = modPow(TWO, (FIELD_PRIME - ONE) / BigInt(FOUR));
|
|
68
|
+
/** A point with the given y, with either sign of x (negating a point preserves its order, so the sign does not matter to a subgroup test), or `undefined` when no point has that y. */
|
|
69
|
+
function pointWithY(y) {
|
|
70
|
+
const ySquared = mod(y * y);
|
|
71
|
+
const target = mod(mod(ySquared - ONE) * modPow(mod(CURVE_D * ySquared + ONE), FIELD_PRIME - TWO));
|
|
72
|
+
const candidate = modPow(target, (FIELD_PRIME + BigInt(THREE)) / BigInt(EIGHT));
|
|
73
|
+
for (const x of [candidate, mod(candidate * SQRT_MINUS_ONE)]) if (mod(x * x) === target) return {
|
|
74
|
+
x,
|
|
75
|
+
y,
|
|
76
|
+
z: ONE,
|
|
77
|
+
t: mod(x * y)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** The sum of two points: the unified, complete addition of Hisil, Wong, Carter and Dawson for a = -1, which doubles as the doubling. */
|
|
81
|
+
function addPoints(p, q) {
|
|
82
|
+
const a = mod((p.y - p.x) * (q.y - q.x));
|
|
83
|
+
const b = mod((p.y + p.x) * (q.y + q.x));
|
|
84
|
+
const c = mod(p.t * TWO * CURVE_D * q.t);
|
|
85
|
+
const d = mod(p.z * TWO * q.z);
|
|
86
|
+
const e = b - a;
|
|
87
|
+
const f = d - c;
|
|
88
|
+
const g = d + c;
|
|
89
|
+
const h = b + a;
|
|
90
|
+
return {
|
|
91
|
+
x: mod(e * f),
|
|
92
|
+
y: mod(g * h),
|
|
93
|
+
z: mod(f * g),
|
|
94
|
+
t: mod(e * h)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const IDENTITY_POINT = {
|
|
98
|
+
x: ZERO,
|
|
99
|
+
y: ONE,
|
|
100
|
+
z: ONE,
|
|
101
|
+
t: ZERO
|
|
102
|
+
};
|
|
103
|
+
/** Whether the point is in the prime-order subgroup: multiplying it by the group order gives the identity element. The point and the order are public, so the double-and-add loop leaks nothing. It costs one doubling per bit of the order below the top (252) and one addition per set bit below the top (72), each a handful of field multiplications, once per distinct key. */
|
|
104
|
+
function isTorsionFree(point) {
|
|
105
|
+
let result = IDENTITY_POINT;
|
|
106
|
+
for (let bit = ORDER_BITS; bit >= 0; bit -= 1) {
|
|
107
|
+
result = addPoints(result, result);
|
|
108
|
+
if ((GROUP_ORDER >> BigInt(bit) & ONE) === ONE) result = addPoints(result, point);
|
|
109
|
+
}
|
|
110
|
+
return result.x === ZERO && result.y === result.z;
|
|
111
|
+
}
|
|
112
|
+
/** How many key verdicts are remembered, so a key that signs many messages is checked for torsion once and a flood of distinct keys cannot grow the memory without bound. */
|
|
113
|
+
const VERDICT_CAPACITY = 1024;
|
|
114
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
115
|
+
function keyFaultUncached(publicKey) {
|
|
116
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES) return "wrong_size";
|
|
117
|
+
const y = encodedY(publicKey);
|
|
118
|
+
if (y >= FIELD_PRIME) return "non_canonical";
|
|
119
|
+
const point = pointWithY(y);
|
|
120
|
+
if (point === void 0) return "not_a_point";
|
|
121
|
+
if (SMALL_ORDER_Y.has(y)) return "small_order";
|
|
122
|
+
return isTorsionFree(point) ? void 0 : "mixed_order";
|
|
123
|
+
}
|
|
124
|
+
/** The first reason an Ed25519 identity-key's bytes are refused, or `undefined` for a usable key. The checks are independent of the platform's crypto library, and the verdict for a key is remembered (the least recently used first goes once more than a fixed number are held), so verifying many signatures under one key tests it for torsion once. */
|
|
125
|
+
function ed25519KeyFault(publicKey) {
|
|
126
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES) return "wrong_size";
|
|
127
|
+
const id = require_domain_device_id.bytesToHex(publicKey);
|
|
128
|
+
if (verdicts.has(id)) {
|
|
129
|
+
const held = verdicts.get(id);
|
|
130
|
+
verdicts.delete(id);
|
|
131
|
+
verdicts.set(id, held);
|
|
132
|
+
return held;
|
|
133
|
+
}
|
|
134
|
+
const fault = keyFaultUncached(publicKey);
|
|
135
|
+
verdicts.set(id, fault);
|
|
136
|
+
for (const oldest of verdicts.keys()) {
|
|
137
|
+
if (verdicts.size <= VERDICT_CAPACITY) break;
|
|
138
|
+
verdicts.delete(oldest);
|
|
139
|
+
}
|
|
140
|
+
return fault;
|
|
141
|
+
}
|
|
142
|
+
/** Whether a 64-byte Ed25519 signature is acceptable to verify: S below the group order, and R a canonical encoding of a point that is not of small order. A signature that fails this does not verify, whatever the platform's library would say. */
|
|
143
|
+
function ed25519SignatureIsStrict(signature) {
|
|
144
|
+
if (signature.length !== SIGNATURE_BYTES) return false;
|
|
145
|
+
const r = signature.subarray(0, PUBLIC_KEY_BYTES);
|
|
146
|
+
const s = signature.subarray(PUBLIC_KEY_BYTES);
|
|
147
|
+
const rY = encodedY(r);
|
|
148
|
+
return littleEndian(s) < GROUP_ORDER && rY < FIELD_PRIME && !SMALL_ORDER_Y.has(rY);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
exports.ed25519KeyFault = ed25519KeyFault;
|
|
152
|
+
exports.ed25519SignatureIsStrict = ed25519SignatureIsStrict;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/domain/ed25519-identity-key.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What an Ed25519 identity-key must be, and what an Ed25519 signature under one must look like, decided here and nowhere else, before the platform's crypto library is consulted.
|
|
4
|
+
*
|
|
5
|
+
* A key is usable when it is exactly 32 bytes, is the canonical encoding of a point on the curve (its y coordinate is below the field prime, and it decodes), is not a point of small order (the eight points of the torsion subgroup, the identity element among them), and lies in the prime-order subgroup (it has no torsion component). A signature is acceptable to verify when its S is below the group order and its R is a canonical encoding that is not a small-order point. The signature equation itself is the cofactorless one, `[S]B = R + [k]A` compared as encodings, which Web Crypto's Ed25519 verification does on the platforms the conformance vectors are run on: a platform that verifies differently fails the mixed-order vectors there. The Rust crates apply the same rules through ed25519-dalek's `is_weak`, `is_torsion_free` and `verify_strict`.
|
|
6
|
+
*/
|
|
7
|
+
/** Why a key is refused, in the order the checks run. */
|
|
8
|
+
export type Ed25519KeyFault = "wrong_size" | "non_canonical" | "not_a_point" | "small_order" | "mixed_order";
|
|
9
|
+
/** The first reason an Ed25519 identity-key's bytes are refused, or `undefined` for a usable key. The checks are independent of the platform's crypto library, and the verdict for a key is remembered (the least recently used first goes once more than a fixed number are held), so verifying many signatures under one key tests it for torsion once. */
|
|
10
|
+
export declare function ed25519KeyFault(publicKey: Uint8Array): Ed25519KeyFault | undefined;
|
|
11
|
+
/** Whether a 64-byte Ed25519 signature is acceptable to verify: S below the group order, and R a canonical encoding of a point that is not of small order. A signature that fails this does not verify, whatever the platform's library would say. */
|
|
12
|
+
export declare function ed25519SignatureIsStrict(signature: Uint8Array): boolean;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/domain/ed25519-identity-key.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What an Ed25519 identity-key must be, and what an Ed25519 signature under one must look like, decided here and nowhere else, before the platform's crypto library is consulted.
|
|
4
|
+
*
|
|
5
|
+
* A key is usable when it is exactly 32 bytes, is the canonical encoding of a point on the curve (its y coordinate is below the field prime, and it decodes), is not a point of small order (the eight points of the torsion subgroup, the identity element among them), and lies in the prime-order subgroup (it has no torsion component). A signature is acceptable to verify when its S is below the group order and its R is a canonical encoding that is not a small-order point. The signature equation itself is the cofactorless one, `[S]B = R + [k]A` compared as encodings, which Web Crypto's Ed25519 verification does on the platforms the conformance vectors are run on: a platform that verifies differently fails the mixed-order vectors there. The Rust crates apply the same rules through ed25519-dalek's `is_weak`, `is_torsion_free` and `verify_strict`.
|
|
6
|
+
*/
|
|
7
|
+
/** Why a key is refused, in the order the checks run. */
|
|
8
|
+
export type Ed25519KeyFault = "wrong_size" | "non_canonical" | "not_a_point" | "small_order" | "mixed_order";
|
|
9
|
+
/** The first reason an Ed25519 identity-key's bytes are refused, or `undefined` for a usable key. The checks are independent of the platform's crypto library, and the verdict for a key is remembered (the least recently used first goes once more than a fixed number are held), so verifying many signatures under one key tests it for torsion once. */
|
|
10
|
+
export declare function ed25519KeyFault(publicKey: Uint8Array): Ed25519KeyFault | undefined;
|
|
11
|
+
/** Whether a 64-byte Ed25519 signature is acceptable to verify: S below the group order, and R a canonical encoding of a point that is not of small order. A signature that fails this does not verify, whatever the platform's library would say. */
|
|
12
|
+
export declare function ed25519SignatureIsStrict(signature: Uint8Array): boolean;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { bytesToHex } from "./device-id.mjs";
|
|
2
|
+
//#region src/domain/ed25519-identity-key.ts
|
|
3
|
+
/**
|
|
4
|
+
* What an Ed25519 identity-key must be, and what an Ed25519 signature under one must look like, decided here and nowhere else, before the platform's crypto library is consulted.
|
|
5
|
+
*
|
|
6
|
+
* A key is usable when it is exactly 32 bytes, is the canonical encoding of a point on the curve (its y coordinate is below the field prime, and it decodes), is not a point of small order (the eight points of the torsion subgroup, the identity element among them), and lies in the prime-order subgroup (it has no torsion component). A signature is acceptable to verify when its S is below the group order and its R is a canonical encoding that is not a small-order point. The signature equation itself is the cofactorless one, `[S]B = R + [k]A` compared as encodings, which Web Crypto's Ed25519 verification does on the platforms the conformance vectors are run on: a platform that verifies differently fails the mixed-order vectors there. The Rust crates apply the same rules through ed25519-dalek's `is_weak`, `is_torsion_free` and `verify_strict`.
|
|
7
|
+
*/
|
|
8
|
+
const BITS_PER_BYTE = 8;
|
|
9
|
+
const THREE = 3;
|
|
10
|
+
const FOUR = 4;
|
|
11
|
+
const EIGHT = 8;
|
|
12
|
+
const FIELD_BITS = 255;
|
|
13
|
+
const PRIME_OFFSET = 19;
|
|
14
|
+
const ORDER_BITS = 252;
|
|
15
|
+
const PUBLIC_KEY_BYTES = 32;
|
|
16
|
+
const SIGNATURE_BYTES = 64;
|
|
17
|
+
const BYTE_BITS = BigInt(BITS_PER_BYTE);
|
|
18
|
+
const SIGN_BIT_BYTE = 31;
|
|
19
|
+
const SIGN_BIT_MASK = 127;
|
|
20
|
+
const ZERO = BigInt(0);
|
|
21
|
+
const ONE = BigInt(1);
|
|
22
|
+
const TWO = BigInt(2);
|
|
23
|
+
/** The field prime, 2^255 - 19. */
|
|
24
|
+
const FIELD_PRIME = (ONE << BigInt(FIELD_BITS)) - BigInt(PRIME_OFFSET);
|
|
25
|
+
/** The order of the prime-order subgroup. */
|
|
26
|
+
const GROUP_ORDER = (ONE << BigInt(ORDER_BITS)) + BigInt("27742317777372353535851937790883648493");
|
|
27
|
+
/** The curve constant d = -121665 / 121666, as a field element. */
|
|
28
|
+
const CURVE_D = BigInt("37095705934669439343138083508754565189542113879843219016388785533085940283555");
|
|
29
|
+
/**
|
|
30
|
+
* The y coordinates of the eight small-order points: 0 (two points of order 4), 1 (the identity element), -1 (order 2) and the two pairs of order 8. Both signs of x share each y, so a key is small-order exactly when its y is one of these, whatever its sign bit.
|
|
31
|
+
*/
|
|
32
|
+
const SMALL_ORDER_Y = new Set([
|
|
33
|
+
"0",
|
|
34
|
+
"1",
|
|
35
|
+
"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec",
|
|
36
|
+
"5fc536d880238b13933c6d305acdfd5f098eff289f4c345b027b2c28f95e826",
|
|
37
|
+
"7a03ac9277fdc74ec6cc392cfa53202a0f67100d760b3cba4fd84d3d706a17c7"
|
|
38
|
+
].map((hex) => BigInt(`0x${hex}`)));
|
|
39
|
+
function littleEndian(bytes) {
|
|
40
|
+
let value = ZERO;
|
|
41
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) value = value << BYTE_BITS | BigInt(bytes[index] ?? 0);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/** The y coordinate an encoding carries: the 255 low bits, with the sign bit of x cleared. */
|
|
45
|
+
function encodedY(encoding) {
|
|
46
|
+
const bytes = Uint8Array.from(encoding);
|
|
47
|
+
bytes[SIGN_BIT_BYTE] = (bytes[SIGN_BIT_BYTE] ?? 0) & SIGN_BIT_MASK;
|
|
48
|
+
return littleEndian(bytes);
|
|
49
|
+
}
|
|
50
|
+
function modPow(base, exponent) {
|
|
51
|
+
let result = ONE;
|
|
52
|
+
let factor = base % FIELD_PRIME;
|
|
53
|
+
let remaining = exponent;
|
|
54
|
+
while (remaining > ZERO) {
|
|
55
|
+
if ((remaining & ONE) === ONE) result = result * factor % FIELD_PRIME;
|
|
56
|
+
factor = factor * factor % FIELD_PRIME;
|
|
57
|
+
remaining >>= ONE;
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function mod(value) {
|
|
62
|
+
const reduced = value % FIELD_PRIME;
|
|
63
|
+
return reduced >= ZERO ? reduced : reduced + FIELD_PRIME;
|
|
64
|
+
}
|
|
65
|
+
/** sqrt(-1) modulo the prime: 2^((p - 1) / 4). */
|
|
66
|
+
const SQRT_MINUS_ONE = modPow(TWO, (FIELD_PRIME - ONE) / BigInt(FOUR));
|
|
67
|
+
/** A point with the given y, with either sign of x (negating a point preserves its order, so the sign does not matter to a subgroup test), or `undefined` when no point has that y. */
|
|
68
|
+
function pointWithY(y) {
|
|
69
|
+
const ySquared = mod(y * y);
|
|
70
|
+
const target = mod(mod(ySquared - ONE) * modPow(mod(CURVE_D * ySquared + ONE), FIELD_PRIME - TWO));
|
|
71
|
+
const candidate = modPow(target, (FIELD_PRIME + BigInt(THREE)) / BigInt(EIGHT));
|
|
72
|
+
for (const x of [candidate, mod(candidate * SQRT_MINUS_ONE)]) if (mod(x * x) === target) return {
|
|
73
|
+
x,
|
|
74
|
+
y,
|
|
75
|
+
z: ONE,
|
|
76
|
+
t: mod(x * y)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The sum of two points: the unified, complete addition of Hisil, Wong, Carter and Dawson for a = -1, which doubles as the doubling. */
|
|
80
|
+
function addPoints(p, q) {
|
|
81
|
+
const a = mod((p.y - p.x) * (q.y - q.x));
|
|
82
|
+
const b = mod((p.y + p.x) * (q.y + q.x));
|
|
83
|
+
const c = mod(p.t * TWO * CURVE_D * q.t);
|
|
84
|
+
const d = mod(p.z * TWO * q.z);
|
|
85
|
+
const e = b - a;
|
|
86
|
+
const f = d - c;
|
|
87
|
+
const g = d + c;
|
|
88
|
+
const h = b + a;
|
|
89
|
+
return {
|
|
90
|
+
x: mod(e * f),
|
|
91
|
+
y: mod(g * h),
|
|
92
|
+
z: mod(f * g),
|
|
93
|
+
t: mod(e * h)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const IDENTITY_POINT = {
|
|
97
|
+
x: ZERO,
|
|
98
|
+
y: ONE,
|
|
99
|
+
z: ONE,
|
|
100
|
+
t: ZERO
|
|
101
|
+
};
|
|
102
|
+
/** Whether the point is in the prime-order subgroup: multiplying it by the group order gives the identity element. The point and the order are public, so the double-and-add loop leaks nothing. It costs one doubling per bit of the order below the top (252) and one addition per set bit below the top (72), each a handful of field multiplications, once per distinct key. */
|
|
103
|
+
function isTorsionFree(point) {
|
|
104
|
+
let result = IDENTITY_POINT;
|
|
105
|
+
for (let bit = ORDER_BITS; bit >= 0; bit -= 1) {
|
|
106
|
+
result = addPoints(result, result);
|
|
107
|
+
if ((GROUP_ORDER >> BigInt(bit) & ONE) === ONE) result = addPoints(result, point);
|
|
108
|
+
}
|
|
109
|
+
return result.x === ZERO && result.y === result.z;
|
|
110
|
+
}
|
|
111
|
+
/** How many key verdicts are remembered, so a key that signs many messages is checked for torsion once and a flood of distinct keys cannot grow the memory without bound. */
|
|
112
|
+
const VERDICT_CAPACITY = 1024;
|
|
113
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
114
|
+
function keyFaultUncached(publicKey) {
|
|
115
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES) return "wrong_size";
|
|
116
|
+
const y = encodedY(publicKey);
|
|
117
|
+
if (y >= FIELD_PRIME) return "non_canonical";
|
|
118
|
+
const point = pointWithY(y);
|
|
119
|
+
if (point === void 0) return "not_a_point";
|
|
120
|
+
if (SMALL_ORDER_Y.has(y)) return "small_order";
|
|
121
|
+
return isTorsionFree(point) ? void 0 : "mixed_order";
|
|
122
|
+
}
|
|
123
|
+
/** The first reason an Ed25519 identity-key's bytes are refused, or `undefined` for a usable key. The checks are independent of the platform's crypto library, and the verdict for a key is remembered (the least recently used first goes once more than a fixed number are held), so verifying many signatures under one key tests it for torsion once. */
|
|
124
|
+
function ed25519KeyFault(publicKey) {
|
|
125
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES) return "wrong_size";
|
|
126
|
+
const id = bytesToHex(publicKey);
|
|
127
|
+
if (verdicts.has(id)) {
|
|
128
|
+
const held = verdicts.get(id);
|
|
129
|
+
verdicts.delete(id);
|
|
130
|
+
verdicts.set(id, held);
|
|
131
|
+
return held;
|
|
132
|
+
}
|
|
133
|
+
const fault = keyFaultUncached(publicKey);
|
|
134
|
+
verdicts.set(id, fault);
|
|
135
|
+
for (const oldest of verdicts.keys()) {
|
|
136
|
+
if (verdicts.size <= VERDICT_CAPACITY) break;
|
|
137
|
+
verdicts.delete(oldest);
|
|
138
|
+
}
|
|
139
|
+
return fault;
|
|
140
|
+
}
|
|
141
|
+
/** Whether a 64-byte Ed25519 signature is acceptable to verify: S below the group order, and R a canonical encoding of a point that is not of small order. A signature that fails this does not verify, whatever the platform's library would say. */
|
|
142
|
+
function ed25519SignatureIsStrict(signature) {
|
|
143
|
+
if (signature.length !== SIGNATURE_BYTES) return false;
|
|
144
|
+
const r = signature.subarray(0, PUBLIC_KEY_BYTES);
|
|
145
|
+
const s = signature.subarray(PUBLIC_KEY_BYTES);
|
|
146
|
+
const rY = encodedY(r);
|
|
147
|
+
return littleEndian(s) < GROUP_ORDER && rY < FIELD_PRIME && !SMALL_ORDER_Y.has(rY);
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { ed25519KeyFault, ed25519SignatureIsStrict };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_adapters_frame_codec = require("../adapters/frame-codec.cjs");
|
|
3
|
-
const require_ports_identity = require("../ports/identity.cjs");
|
|
4
3
|
const require_domain_device_id = require("./device-id.cjs");
|
|
4
|
+
const require_ports_identity = require("../ports/identity.cjs");
|
|
5
5
|
const require_domain_token_scope = require("./token-scope.cjs");
|
|
6
6
|
const require_gossip_extensions = require("../gossip-extensions-BBYgMZ2N.cjs");
|
|
7
7
|
const require_domain_peer_advert = require("./peer-advert.cjs");
|
|
@@ -280,73 +280,129 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
|
|
|
280
280
|
function createRelayChannels(identity, sendHello, options = {}) {
|
|
281
281
|
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
282
282
|
const channels = /* @__PURE__ */ new Map();
|
|
283
|
-
/**
|
|
284
|
-
function
|
|
283
|
+
/** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it is reported once and swallowed. */
|
|
284
|
+
function reportFault(error) {
|
|
285
|
+
const report = (faultError) => {
|
|
286
|
+
console.error("the local fault callback failed", faultError);
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
const result = options.onFault?.(error);
|
|
290
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
291
|
+
} catch (faultError) {
|
|
292
|
+
report(faultError);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
|
|
296
|
+
function abandon(entry, error) {
|
|
297
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
298
|
+
if (channels.get(entry.key) === entry) channels.delete(entry.key);
|
|
299
|
+
entry.settle.reject(failure);
|
|
300
|
+
entry.sent.reject(failure);
|
|
301
|
+
}
|
|
302
|
+
/** Starts a handshake with the peer: sends this side's hello, and registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second. `answering` is true when a received hello started this, so nobody awaits the outcome and a failure to send is reported to the owner. `track` is false for a handshake answering a hello while a channel with that peer is established: it is not registered, so the established channel stays in place and in use until the new one is up and this side's hello has been sent. A hello that could not be sent leaves no channel behind: the peer would never learn this side's key. */
|
|
303
|
+
function start(peer, { answering, track } = {
|
|
304
|
+
answering: false,
|
|
305
|
+
track: true
|
|
306
|
+
}) {
|
|
285
307
|
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
286
|
-
const settle = Promise.withResolvers();
|
|
287
308
|
const pending = beginHandshake(identity, peer);
|
|
288
309
|
const entry = {
|
|
289
310
|
status: "awaiting",
|
|
290
311
|
pending,
|
|
291
|
-
settle
|
|
312
|
+
settle: Promise.withResolvers(),
|
|
313
|
+
key,
|
|
314
|
+
sent: Promise.withResolvers()
|
|
292
315
|
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
entry.settle.promise.catch(() => void 0);
|
|
317
|
+
entry.sent.promise.catch(() => void 0);
|
|
318
|
+
pending.then(async ({ hello }) => {
|
|
319
|
+
try {
|
|
320
|
+
await sendHello(peer, hello);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
abandon(entry, error);
|
|
323
|
+
entry.settle.promise.then((channel) => {
|
|
324
|
+
const held = channels.get(key);
|
|
325
|
+
if (held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
326
|
+
}, () => void 0);
|
|
327
|
+
if (answering && track) reportFault(error);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
entry.sent.resolve(void 0);
|
|
331
|
+
}, (error) => {
|
|
332
|
+
abandon(entry, error);
|
|
297
333
|
});
|
|
334
|
+
if (track) channels.set(key, entry);
|
|
298
335
|
return entry;
|
|
299
336
|
}
|
|
337
|
+
/** Completes the handshake a verified hello belongs to. Throws on any failure of this side; the caller abandons the handshake. */
|
|
338
|
+
async function complete({ entry, hello, peer, replaces }) {
|
|
339
|
+
const { key } = entry;
|
|
340
|
+
const established = await establishChannelFrom(identity, {
|
|
341
|
+
pending: await entry.pending,
|
|
342
|
+
remote: hello,
|
|
343
|
+
peer,
|
|
344
|
+
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
345
|
+
});
|
|
346
|
+
if (established === void 0) {
|
|
347
|
+
abandon(entry, /* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (replaces !== void 0) await entry.sent.promise;
|
|
351
|
+
if (channels.get(key) !== (replaces ?? entry)) return;
|
|
352
|
+
const channel = {
|
|
353
|
+
...established,
|
|
354
|
+
seal: async (frame) => {
|
|
355
|
+
try {
|
|
356
|
+
return await established.seal(frame);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const held = channels.get(key);
|
|
359
|
+
if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
channels.set(key, {
|
|
365
|
+
status: "established",
|
|
366
|
+
channel,
|
|
367
|
+
remoteEphemeralKey: hello["ephemeral-key"]
|
|
368
|
+
});
|
|
369
|
+
entry.settle.resolve(channel);
|
|
370
|
+
}
|
|
300
371
|
return {
|
|
301
372
|
ensure: async (peer) => {
|
|
302
|
-
const
|
|
373
|
+
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
374
|
+
const existing = channels.get(key);
|
|
303
375
|
if (existing?.status === "established") return existing.channel;
|
|
304
376
|
return (existing ?? start(peer)).settle.promise;
|
|
305
377
|
},
|
|
306
378
|
applyHello: async (hello) => {
|
|
307
|
-
let
|
|
379
|
+
let entry;
|
|
308
380
|
try {
|
|
309
|
-
verdict = await verifyHello(identity, hello);
|
|
381
|
+
const verdict = await verifyHello(identity, hello);
|
|
382
|
+
if (!verdict.ok) return;
|
|
383
|
+
const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
|
|
384
|
+
const current = channels.get(key);
|
|
385
|
+
if (current?.status === "established" && require_domain_token_scope.bytesEqual(current.remoteEphemeralKey, hello["ephemeral-key"])) return;
|
|
386
|
+
const replaces = current?.status === "established" ? current : void 0;
|
|
387
|
+
entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId, {
|
|
388
|
+
answering: true,
|
|
389
|
+
track: replaces === void 0
|
|
390
|
+
});
|
|
391
|
+
await complete({
|
|
392
|
+
entry,
|
|
393
|
+
hello,
|
|
394
|
+
peer: verdict.peer,
|
|
395
|
+
replaces
|
|
396
|
+
});
|
|
310
397
|
} catch (error) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
if (!verdict.ok) return;
|
|
315
|
-
const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
|
|
316
|
-
const current = channels.get(key);
|
|
317
|
-
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
318
|
-
const established = await establishChannelFrom(identity, {
|
|
319
|
-
pending: await entry.pending,
|
|
320
|
-
remote: hello,
|
|
321
|
-
peer: verdict.peer,
|
|
322
|
-
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
323
|
-
});
|
|
324
|
-
if (established === void 0) {
|
|
325
|
-
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
326
|
-
if (channels.get(key) === entry) channels.delete(key);
|
|
327
|
-
return;
|
|
398
|
+
if (entry !== void 0) abandon(entry, error);
|
|
399
|
+
reportFault(error);
|
|
328
400
|
}
|
|
329
|
-
const channel = {
|
|
330
|
-
...established,
|
|
331
|
-
seal: async (frame) => {
|
|
332
|
-
try {
|
|
333
|
-
return await established.seal(frame);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
const held = channels.get(key);
|
|
336
|
-
if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
channels.set(key, {
|
|
342
|
-
status: "established",
|
|
343
|
-
channel
|
|
344
|
-
});
|
|
345
|
-
entry.settle.resolve(channel);
|
|
346
401
|
},
|
|
347
402
|
open: async (data, claimedFrom) => {
|
|
348
403
|
if (claimedFrom === void 0) return;
|
|
349
|
-
const
|
|
404
|
+
const key = require_domain_device_id.deviceIdToHex(claimedFrom);
|
|
405
|
+
const entry = channels.get(key);
|
|
350
406
|
if (entry?.status !== "established") return;
|
|
351
407
|
const frame = await entry.channel.open(data);
|
|
352
408
|
return frame === void 0 ? void 0 : {
|
|
@@ -355,13 +411,13 @@ function createRelayChannels(identity, sendHello, options = {}) {
|
|
|
355
411
|
};
|
|
356
412
|
},
|
|
357
413
|
reset: (reason) => {
|
|
358
|
-
for (const entry of channels.values()) if (entry.status === "awaiting") entry
|
|
414
|
+
for (const entry of [...channels.values()]) if (entry.status === "awaiting") abandon(entry, new Error(reason));
|
|
359
415
|
channels.clear();
|
|
360
416
|
},
|
|
361
417
|
forget: (peer) => {
|
|
362
418
|
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
363
419
|
const entry = channels.get(key);
|
|
364
|
-
if (entry?.status === "awaiting") entry
|
|
420
|
+
if (entry?.status === "awaiting") abandon(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
|
|
365
421
|
channels.delete(key);
|
|
366
422
|
}
|
|
367
423
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
|
|
2
2
|
import { r as Transport, t as Connection } from "../transport-gDbSHO-r.cjs";
|
|
3
|
-
import { r as IdentityPort } from "../identity-
|
|
3
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
@@ -137,8 +137,8 @@ addresses?: readonly string[],
|
|
|
137
137
|
onPeerAdvert?: (advert: PeerAdvert) => void,
|
|
138
138
|
/** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
|
|
139
139
|
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
|
|
140
|
-
/** Told of a failure that is local to this session and did not end it: an identity port that failed
|
|
141
|
-
onLocalFault?: (error: unknown) => void): MeshSession;
|
|
140
|
+
/** Told of a failure that is local to this session and did not end it, while handling a hello from a peer: an identity port that failed to check or sign, an answering hello that could not be sent, a channel that could not be established. The hello is dropped, any half-made handshake for that peer is discarded, and the peer is told nothing, so no failure in handling a hello ends the session. Called once per failing hello; if it throws, or returns a promise that rejects, that is reported once to the console and swallowed. Omit for a caller with no use for it. */
|
|
141
|
+
onLocalFault?: (error: unknown) => void | Promise<void>): MeshSession;
|
|
142
142
|
/** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
|
|
143
143
|
export interface AcceptedMeshSession extends MeshSession {
|
|
144
144
|
/** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
|
|
2
2
|
import { r as Transport, t as Connection } from "../transport-bD3p_TO_.mjs";
|
|
3
|
-
import { r as IdentityPort } from "../identity-
|
|
3
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
@@ -137,8 +137,8 @@ addresses?: readonly string[],
|
|
|
137
137
|
onPeerAdvert?: (advert: PeerAdvert) => void,
|
|
138
138
|
/** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
|
|
139
139
|
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
|
|
140
|
-
/** Told of a failure that is local to this session and did not end it: an identity port that failed
|
|
141
|
-
onLocalFault?: (error: unknown) => void): MeshSession;
|
|
140
|
+
/** Told of a failure that is local to this session and did not end it, while handling a hello from a peer: an identity port that failed to check or sign, an answering hello that could not be sent, a channel that could not be established. The hello is dropped, any half-made handshake for that peer is discarded, and the peer is told nothing, so no failure in handling a hello ends the session. Called once per failing hello; if it throws, or returns a promise that rejects, that is reported once to the console and swallowed. Omit for a caller with no use for it. */
|
|
141
|
+
onLocalFault?: (error: unknown) => void | Promise<void>): MeshSession;
|
|
142
142
|
/** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
|
|
143
143
|
export interface AcceptedMeshSession extends MeshSession {
|
|
144
144
|
/** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { messageFromFrame, tryDecodeFrame, wrapRelayData } from "../adapters/frame-codec.mjs";
|
|
2
|
-
import { IdentityKeyError } from "../ports/identity.mjs";
|
|
3
2
|
import { deviceIdToHex } from "./device-id.mjs";
|
|
3
|
+
import { IdentityKeyError } from "../ports/identity.mjs";
|
|
4
4
|
import { bytesEqual } from "./token-scope.mjs";
|
|
5
5
|
import { a as OWN_VERSION, i as CORE_VERSION_GOSSIP_KEY, n as TOPOLOGY_PEERS_GOSSIP_KEY, o as isVersionGetCommand, r as validateGossipExtensions, s as versionGetOutcome } from "../gossip-extensions-B6WJjkAr.mjs";
|
|
6
6
|
import { signPeerAdvert, verifyPeerAdvert } from "./peer-advert.mjs";
|
|
@@ -279,73 +279,129 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
|
|
|
279
279
|
function createRelayChannels(identity, sendHello, options = {}) {
|
|
280
280
|
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
281
281
|
const channels = /* @__PURE__ */ new Map();
|
|
282
|
-
/**
|
|
283
|
-
function
|
|
282
|
+
/** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it is reported once and swallowed. */
|
|
283
|
+
function reportFault(error) {
|
|
284
|
+
const report = (faultError) => {
|
|
285
|
+
console.error("the local fault callback failed", faultError);
|
|
286
|
+
};
|
|
287
|
+
try {
|
|
288
|
+
const result = options.onFault?.(error);
|
|
289
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
290
|
+
} catch (faultError) {
|
|
291
|
+
report(faultError);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
|
|
295
|
+
function abandon(entry, error) {
|
|
296
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
297
|
+
if (channels.get(entry.key) === entry) channels.delete(entry.key);
|
|
298
|
+
entry.settle.reject(failure);
|
|
299
|
+
entry.sent.reject(failure);
|
|
300
|
+
}
|
|
301
|
+
/** Starts a handshake with the peer: sends this side's hello, and registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second. `answering` is true when a received hello started this, so nobody awaits the outcome and a failure to send is reported to the owner. `track` is false for a handshake answering a hello while a channel with that peer is established: it is not registered, so the established channel stays in place and in use until the new one is up and this side's hello has been sent. A hello that could not be sent leaves no channel behind: the peer would never learn this side's key. */
|
|
302
|
+
function start(peer, { answering, track } = {
|
|
303
|
+
answering: false,
|
|
304
|
+
track: true
|
|
305
|
+
}) {
|
|
284
306
|
const key = deviceIdToHex(peer);
|
|
285
|
-
const settle = Promise.withResolvers();
|
|
286
307
|
const pending = beginHandshake(identity, peer);
|
|
287
308
|
const entry = {
|
|
288
309
|
status: "awaiting",
|
|
289
310
|
pending,
|
|
290
|
-
settle
|
|
311
|
+
settle: Promise.withResolvers(),
|
|
312
|
+
key,
|
|
313
|
+
sent: Promise.withResolvers()
|
|
291
314
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
315
|
+
entry.settle.promise.catch(() => void 0);
|
|
316
|
+
entry.sent.promise.catch(() => void 0);
|
|
317
|
+
pending.then(async ({ hello }) => {
|
|
318
|
+
try {
|
|
319
|
+
await sendHello(peer, hello);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
abandon(entry, error);
|
|
322
|
+
entry.settle.promise.then((channel) => {
|
|
323
|
+
const held = channels.get(key);
|
|
324
|
+
if (held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
325
|
+
}, () => void 0);
|
|
326
|
+
if (answering && track) reportFault(error);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
entry.sent.resolve(void 0);
|
|
330
|
+
}, (error) => {
|
|
331
|
+
abandon(entry, error);
|
|
296
332
|
});
|
|
333
|
+
if (track) channels.set(key, entry);
|
|
297
334
|
return entry;
|
|
298
335
|
}
|
|
336
|
+
/** Completes the handshake a verified hello belongs to. Throws on any failure of this side; the caller abandons the handshake. */
|
|
337
|
+
async function complete({ entry, hello, peer, replaces }) {
|
|
338
|
+
const { key } = entry;
|
|
339
|
+
const established = await establishChannelFrom(identity, {
|
|
340
|
+
pending: await entry.pending,
|
|
341
|
+
remote: hello,
|
|
342
|
+
peer,
|
|
343
|
+
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
344
|
+
});
|
|
345
|
+
if (established === void 0) {
|
|
346
|
+
abandon(entry, /* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (replaces !== void 0) await entry.sent.promise;
|
|
350
|
+
if (channels.get(key) !== (replaces ?? entry)) return;
|
|
351
|
+
const channel = {
|
|
352
|
+
...established,
|
|
353
|
+
seal: async (frame) => {
|
|
354
|
+
try {
|
|
355
|
+
return await established.seal(frame);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const held = channels.get(key);
|
|
358
|
+
if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
channels.set(key, {
|
|
364
|
+
status: "established",
|
|
365
|
+
channel,
|
|
366
|
+
remoteEphemeralKey: hello["ephemeral-key"]
|
|
367
|
+
});
|
|
368
|
+
entry.settle.resolve(channel);
|
|
369
|
+
}
|
|
299
370
|
return {
|
|
300
371
|
ensure: async (peer) => {
|
|
301
|
-
const
|
|
372
|
+
const key = deviceIdToHex(peer);
|
|
373
|
+
const existing = channels.get(key);
|
|
302
374
|
if (existing?.status === "established") return existing.channel;
|
|
303
375
|
return (existing ?? start(peer)).settle.promise;
|
|
304
376
|
},
|
|
305
377
|
applyHello: async (hello) => {
|
|
306
|
-
let
|
|
378
|
+
let entry;
|
|
307
379
|
try {
|
|
308
|
-
verdict = await verifyHello(identity, hello);
|
|
380
|
+
const verdict = await verifyHello(identity, hello);
|
|
381
|
+
if (!verdict.ok) return;
|
|
382
|
+
const key = deviceIdToHex(verdict.peer.deviceId);
|
|
383
|
+
const current = channels.get(key);
|
|
384
|
+
if (current?.status === "established" && bytesEqual(current.remoteEphemeralKey, hello["ephemeral-key"])) return;
|
|
385
|
+
const replaces = current?.status === "established" ? current : void 0;
|
|
386
|
+
entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId, {
|
|
387
|
+
answering: true,
|
|
388
|
+
track: replaces === void 0
|
|
389
|
+
});
|
|
390
|
+
await complete({
|
|
391
|
+
entry,
|
|
392
|
+
hello,
|
|
393
|
+
peer: verdict.peer,
|
|
394
|
+
replaces
|
|
395
|
+
});
|
|
309
396
|
} catch (error) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
if (!verdict.ok) return;
|
|
314
|
-
const key = deviceIdToHex(verdict.peer.deviceId);
|
|
315
|
-
const current = channels.get(key);
|
|
316
|
-
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
317
|
-
const established = await establishChannelFrom(identity, {
|
|
318
|
-
pending: await entry.pending,
|
|
319
|
-
remote: hello,
|
|
320
|
-
peer: verdict.peer,
|
|
321
|
-
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
322
|
-
});
|
|
323
|
-
if (established === void 0) {
|
|
324
|
-
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
325
|
-
if (channels.get(key) === entry) channels.delete(key);
|
|
326
|
-
return;
|
|
397
|
+
if (entry !== void 0) abandon(entry, error);
|
|
398
|
+
reportFault(error);
|
|
327
399
|
}
|
|
328
|
-
const channel = {
|
|
329
|
-
...established,
|
|
330
|
-
seal: async (frame) => {
|
|
331
|
-
try {
|
|
332
|
-
return await established.seal(frame);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
const held = channels.get(key);
|
|
335
|
-
if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
336
|
-
throw error;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
channels.set(key, {
|
|
341
|
-
status: "established",
|
|
342
|
-
channel
|
|
343
|
-
});
|
|
344
|
-
entry.settle.resolve(channel);
|
|
345
400
|
},
|
|
346
401
|
open: async (data, claimedFrom) => {
|
|
347
402
|
if (claimedFrom === void 0) return;
|
|
348
|
-
const
|
|
403
|
+
const key = deviceIdToHex(claimedFrom);
|
|
404
|
+
const entry = channels.get(key);
|
|
349
405
|
if (entry?.status !== "established") return;
|
|
350
406
|
const frame = await entry.channel.open(data);
|
|
351
407
|
return frame === void 0 ? void 0 : {
|
|
@@ -354,13 +410,13 @@ function createRelayChannels(identity, sendHello, options = {}) {
|
|
|
354
410
|
};
|
|
355
411
|
},
|
|
356
412
|
reset: (reason) => {
|
|
357
|
-
for (const entry of channels.values()) if (entry.status === "awaiting") entry
|
|
413
|
+
for (const entry of [...channels.values()]) if (entry.status === "awaiting") abandon(entry, new Error(reason));
|
|
358
414
|
channels.clear();
|
|
359
415
|
},
|
|
360
416
|
forget: (peer) => {
|
|
361
417
|
const key = deviceIdToHex(peer);
|
|
362
418
|
const entry = channels.get(key);
|
|
363
|
-
if (entry?.status === "awaiting") entry
|
|
419
|
+
if (entry?.status === "awaiting") abandon(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
|
|
364
420
|
channels.delete(key);
|
|
365
421
|
}
|
|
366
422
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
|
|
3
|
-
import { r as IdentityPort } from "../identity-
|
|
3
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
6
6
|
import { RoomNoticeVerdictReason } from "./room.cjs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
|
|
3
|
-
import { r as IdentityPort } from "../identity-
|
|
3
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
|
6
6
|
import { RoomNoticeVerdictReason } from "./room.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
//#region src/domain/peer-advert.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The most a verifier's verdict memo may charge across all its entries, in bytes of key.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
//#region src/domain/peer-advert.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The most a verifier's verdict memo may charge across all its entries, in bytes of key.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
package/dist/domain/room.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
|
+
const require_ports_identity = require("../ports/identity.cjs");
|
|
3
4
|
const require_domain_group_key = require("./group-key.cjs");
|
|
4
5
|
const require_domain_room_token_verification = require("./room-token-verification.cjs");
|
|
5
6
|
let cbor2 = require("cbor2");
|
|
@@ -79,7 +80,7 @@ async function verifyRoomNotice(notice, options) {
|
|
|
79
80
|
ok: false,
|
|
80
81
|
reason: "wrong_room"
|
|
81
82
|
};
|
|
82
|
-
if (!await options.identity
|
|
83
|
+
if (!await require_ports_identity.verifiesOrRefuses(options.identity, claims["poster-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
83
84
|
ok: false,
|
|
84
85
|
reason: "bad_signature"
|
|
85
86
|
};
|
package/dist/domain/room.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { At as RoomPath, Ot as RoomNotice, kt as RoomNoticeClaims, p as CapabilityToken, q as MessageRef } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { RevocationCheck } from "./tokens.cjs";
|
|
5
5
|
import { r as RoomTokenVerdictReason } from "../room-token-verification-Dh7SGJfK.cjs";
|
package/dist/domain/room.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { At as RoomPath, Ot as RoomNotice, kt as RoomNoticeClaims, p as CapabilityToken, q as MessageRef } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { RevocationCheck } from "./tokens.mjs";
|
|
5
5
|
import { r as RoomTokenVerdictReason } from "../room-token-verification-Hb1LkpFh.mjs";
|
package/dist/domain/room.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { roomNoticeClaimsSchema } from "../generated/protocol.mjs";
|
|
2
|
+
import { verifiesOrRefuses } from "../ports/identity.mjs";
|
|
2
3
|
import { isEncryptedContentType } from "./group-key.mjs";
|
|
3
4
|
import { verifyRoomToken } from "./room-token-verification.mjs";
|
|
4
5
|
import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
|
|
@@ -78,7 +79,7 @@ async function verifyRoomNotice(notice, options) {
|
|
|
78
79
|
ok: false,
|
|
79
80
|
reason: "wrong_room"
|
|
80
81
|
};
|
|
81
|
-
if (!await options.identity
|
|
82
|
+
if (!await verifiesOrRefuses(options.identity, claims["poster-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
82
83
|
ok: false,
|
|
83
84
|
reason: "bad_signature"
|
|
84
85
|
};
|
package/dist/domain/tokens.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
3
|
const require_domain_device_id = require("./device-id.cjs");
|
|
4
|
+
const require_ports_identity = require("../ports/identity.cjs");
|
|
4
5
|
const require_sequential = require("../sequential-CtQVlW8M.cjs");
|
|
5
6
|
const require_domain_token_scope = require("./token-scope.cjs");
|
|
6
7
|
const require_domain_token_predicates = require("./token-predicates.cjs");
|
|
@@ -118,7 +119,7 @@ async function verifyTokenChain(token, options, { subjectClaims, depth, seen } =
|
|
|
118
119
|
reason: "malformed"
|
|
119
120
|
};
|
|
120
121
|
const claims = claimsResult.data;
|
|
121
|
-
if (!await options.identity
|
|
122
|
+
if (!await require_ports_identity.verifiesOrRefuses(options.identity, claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
122
123
|
ok: false,
|
|
123
124
|
reason: "bad_signature"
|
|
124
125
|
};
|
|
@@ -299,7 +300,7 @@ async function verifyRevocationEntry(entry, options) {
|
|
|
299
300
|
reason: "malformed"
|
|
300
301
|
};
|
|
301
302
|
const claims = claimsResult.data;
|
|
302
|
-
if (!await options.identity
|
|
303
|
+
if (!await require_ports_identity.verifiesOrRefuses(options.identity, claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
303
304
|
ok: false,
|
|
304
305
|
reason: "bad_signature"
|
|
305
306
|
};
|
package/dist/domain/tokens.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-CthkHRzB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { l as GrantSubject, m as TokenDelegateHandler, r as ConditionsContext, u as NarrowingCandidate } from "../token-predicates-lg4TtUxm.cjs";
|
|
5
5
|
import { PredicateNode } from "trilean";
|
package/dist/domain/tokens.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import { r as IdentityPort } from "../identity-
|
|
2
|
+
import { r as IdentityPort } from "../identity-zLL5kQGJ.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { l as GrantSubject, m as TokenDelegateHandler, r as ConditionsContext, u as NarrowingCandidate } from "../token-predicates-CrieP1WI.mjs";
|
|
5
5
|
import { PredicateNode } from "trilean";
|
package/dist/domain/tokens.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { capabilityTokenSchema, capabilityVerbSchema, revocationClaimsSchema, tokenClaimsSchema } from "../generated/protocol.mjs";
|
|
2
2
|
import { bytesToHex } from "./device-id.mjs";
|
|
3
|
+
import { verifiesOrRefuses } from "../ports/identity.mjs";
|
|
3
4
|
import { n as firstAsync } from "../sequential-D_W3THly.mjs";
|
|
4
5
|
import { bytesEqual, scopeNarrows } from "./token-scope.mjs";
|
|
5
6
|
import { conditionsListSchema, evaluateConditions, evaluateNarrowing } from "./token-predicates.mjs";
|
|
@@ -117,7 +118,7 @@ async function verifyTokenChain(token, options, { subjectClaims, depth, seen } =
|
|
|
117
118
|
reason: "malformed"
|
|
118
119
|
};
|
|
119
120
|
const claims = claimsResult.data;
|
|
120
|
-
if (!await options.identity
|
|
121
|
+
if (!await verifiesOrRefuses(options.identity, claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
121
122
|
ok: false,
|
|
122
123
|
reason: "bad_signature"
|
|
123
124
|
};
|
|
@@ -298,7 +299,7 @@ async function verifyRevocationEntry(entry, options) {
|
|
|
298
299
|
reason: "malformed"
|
|
299
300
|
};
|
|
300
301
|
const claims = claimsResult.data;
|
|
301
|
-
if (!await options.identity
|
|
302
|
+
if (!await verifiesOrRefuses(options.identity, claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
302
303
|
ok: false,
|
|
303
304
|
reason: "bad_signature"
|
|
304
305
|
};
|
|
@@ -9,6 +9,8 @@ declare class IdentityKeyError extends Error {
|
|
|
9
9
|
cause: unknown;
|
|
10
10
|
}>);
|
|
11
11
|
}
|
|
12
|
+
/** Verifies a signature and treats a key the port cannot use as a signature that does not verify, so a verifier of attacker-supplied bytes always gets a verdict: an `IdentityKeyError` is a refusal of those bytes, while any other error is a failure of the port and propagates. */
|
|
13
|
+
declare function verifiesOrRefuses(identity: Readonly<Pick<IdentityPort, "verify">>, key: IdentityKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
|
12
14
|
/**
|
|
13
15
|
* Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract).
|
|
14
16
|
*/
|
|
@@ -25,4 +27,4 @@ interface IdentityPort {
|
|
|
25
27
|
deriveSharedSecret?: (peerKey: IdentityKey) => Promise<Uint8Array<ArrayBuffer>>;
|
|
26
28
|
}
|
|
27
29
|
//#endregion
|
|
28
|
-
export { IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
|
|
30
|
+
export { verifiesOrRefuses as i, IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
|
|
@@ -9,6 +9,8 @@ declare class IdentityKeyError extends Error {
|
|
|
9
9
|
cause: unknown;
|
|
10
10
|
}>);
|
|
11
11
|
}
|
|
12
|
+
/** Verifies a signature and treats a key the port cannot use as a signature that does not verify, so a verifier of attacker-supplied bytes always gets a verdict: an `IdentityKeyError` is a refusal of those bytes, while any other error is a failure of the port and propagates. */
|
|
13
|
+
declare function verifiesOrRefuses(identity: Readonly<Pick<IdentityPort, "verify">>, key: IdentityKey, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
|
12
14
|
/**
|
|
13
15
|
* Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract).
|
|
14
16
|
*/
|
|
@@ -25,4 +27,4 @@ interface IdentityPort {
|
|
|
25
27
|
deriveSharedSecret?: (peerKey: IdentityKey) => Promise<Uint8Array<ArrayBuffer>>;
|
|
26
28
|
}
|
|
27
29
|
//#endregion
|
|
28
|
-
export { IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
|
|
30
|
+
export { verifiesOrRefuses as i, IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
|
package/dist/ports/identity.cjs
CHANGED
|
@@ -9,5 +9,15 @@ var IdentityKeyError = class extends Error {
|
|
|
9
9
|
this.fault = fault;
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
+
/** Verifies a signature and treats a key the port cannot use as a signature that does not verify, so a verifier of attacker-supplied bytes always gets a verdict: an `IdentityKeyError` is a refusal of those bytes, while any other error is a failure of the port and propagates. */
|
|
13
|
+
async function verifiesOrRefuses(identity, key, message, signature) {
|
|
14
|
+
try {
|
|
15
|
+
return await identity.verify(key, message, signature);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof IdentityKeyError) return false;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
//#endregion
|
|
13
22
|
exports.IdentityKeyError = IdentityKeyError;
|
|
23
|
+
exports.verifiesOrRefuses = verifiesOrRefuses;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as IdentityKeyFault, r as IdentityPort, t as IdentityKeyError } from "../identity-
|
|
2
|
-
export { IdentityKeyError, IdentityKeyFault, IdentityPort };
|
|
1
|
+
import { i as verifiesOrRefuses, n as IdentityKeyFault, r as IdentityPort, t as IdentityKeyError } from "../identity-CthkHRzB.cjs";
|
|
2
|
+
export { IdentityKeyError, IdentityKeyFault, IdentityPort, verifiesOrRefuses };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as IdentityKeyFault, r as IdentityPort, t as IdentityKeyError } from "../identity-
|
|
2
|
-
export { IdentityKeyError, IdentityKeyFault, IdentityPort };
|
|
1
|
+
import { i as verifiesOrRefuses, n as IdentityKeyFault, r as IdentityPort, t as IdentityKeyError } from "../identity-zLL5kQGJ.mjs";
|
|
2
|
+
export { IdentityKeyError, IdentityKeyFault, IdentityPort, verifiesOrRefuses };
|
package/dist/ports/identity.mjs
CHANGED
|
@@ -8,5 +8,14 @@ var IdentityKeyError = class extends Error {
|
|
|
8
8
|
this.fault = fault;
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
|
+
/** Verifies a signature and treats a key the port cannot use as a signature that does not verify, so a verifier of attacker-supplied bytes always gets a verdict: an `IdentityKeyError` is a refusal of those bytes, while any other error is a failure of the port and propagates. */
|
|
12
|
+
async function verifiesOrRefuses(identity, key, message, signature) {
|
|
13
|
+
try {
|
|
14
|
+
return await identity.verify(key, message, signature);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof IdentityKeyError) return false;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
11
20
|
//#endregion
|
|
12
|
-
export { IdentityKeyError };
|
|
21
|
+
export { IdentityKeyError, verifiesOrRefuses };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wire-mesh-core",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.3",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"cbor2": "2.3.0",
|
|
6
6
|
"cddl.js": "1.0.1",
|
|
@@ -106,6 +106,10 @@
|
|
|
106
106
|
"import": "./dist/domain/direct-manage-request.mjs",
|
|
107
107
|
"require": "./dist/domain/direct-manage-request.cjs"
|
|
108
108
|
},
|
|
109
|
+
"./domain/ed25519-identity-key": {
|
|
110
|
+
"import": "./dist/domain/ed25519-identity-key.mjs",
|
|
111
|
+
"require": "./dist/domain/ed25519-identity-key.cjs"
|
|
112
|
+
},
|
|
109
113
|
"./domain/erasure-coding": {
|
|
110
114
|
"import": "./dist/domain/erasure-coding.mjs",
|
|
111
115
|
"require": "./dist/domain/erasure-coding.cjs"
|