@reticulum/dacar 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 +215 -0
- package/package.json +47 -0
- package/src/challenge.js +394 -0
- package/src/config.js +154 -0
- package/src/crdt.js +318 -0
- package/src/delta.js +108 -0
- package/src/engine.js +198 -0
- package/src/hlc.js +104 -0
- package/src/index.js +88 -0
- package/src/namespace.js +183 -0
- package/src/naming.js +39 -0
- package/src/operation.js +256 -0
- package/src/threshold.js +110 -0
- package/src/transport/index.js +37 -0
- package/src/transport/lxmfSync.js +221 -0
- package/src/transport/rfedSync.js +228 -0
- package/src/transport/rnsChallenge.js +243 -0
- package/src/transport/rnsIdentity.js +84 -0
- package/src/tuple.js +120 -0
- package/src/verifier.js +183 -0
package/src/verifier.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify-on-ingest: authenticating network Deltas by Ed25519 signature.
|
|
3
|
+
*
|
|
4
|
+
* The CRDT update itself (`StateVector.apply()`) is a *pure* mutation that
|
|
5
|
+
* trusts its caller; it deliberately performs no cryptography so the layering
|
|
6
|
+
* stays simple and the hot path stays fast. Network-received Deltas instead
|
|
7
|
+
* enter the state through `StateVector.ingest()`, which **must** authenticate
|
|
8
|
+
* each Operation against the claimed Issuer's public key(s) before it is
|
|
9
|
+
* allowed to mutate state (spec §11.2.4: *"The signature remains the sole
|
|
10
|
+
* source of authorization authenticity"*).
|
|
11
|
+
*
|
|
12
|
+
* This module bridges an Issuer hash to the public-key material needed to
|
|
13
|
+
* verify it:
|
|
14
|
+
*
|
|
15
|
+
* - `IssuerKeyset` — M public keys + a threshold (1 for a single identity,
|
|
16
|
+
* N for a Threshold Group, §4.1).
|
|
17
|
+
* - `KeyResolver` — `issuerHash(16) -> IssuerKeyset | null | Promise`.
|
|
18
|
+
* - `Keyring` — a Map-backed resolver for offline / test use.
|
|
19
|
+
* - `verifyOperation()` — resolve + verify, returning a plain boolean.
|
|
20
|
+
*
|
|
21
|
+
* Authentication is *not* authorization. Verifying a signature proves the
|
|
22
|
+
* Operation was genuinely issued by the claimed Issuer; whether that Issuer is
|
|
23
|
+
* itself authorized (its authority traces to a Root Trust Anchor) is resolved
|
|
24
|
+
* later by the Evaluation Engine (§7) against the converged CRDT state.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { toHex } from "@reticulum/core";
|
|
28
|
+
import { HASH_SIZE } from "./namespace.js";
|
|
29
|
+
|
|
30
|
+
/** The full RNS public key (X25519 ‖ Ed25519) is 64 raw bytes. */
|
|
31
|
+
const RNS_PUBLIC_KEY_SIZE = 64;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Public-key material needed to verify an Operation from one Issuer.
|
|
35
|
+
*
|
|
36
|
+
* A single-identity Issuer has `threshold === 1` and one member key; a
|
|
37
|
+
* Threshold Group Issuer (§4.1) has `threshold === N` and `M >= N` member keys.
|
|
38
|
+
* Each member key is the full 64-byte RNS public key returned by
|
|
39
|
+
* `Identity.getPublicKey()` (X25519 ‖ Ed25519), reconstructable via
|
|
40
|
+
* `Identity.fromPublicKey()`.
|
|
41
|
+
*/
|
|
42
|
+
export class IssuerKeyset {
|
|
43
|
+
/**
|
|
44
|
+
* @param {Uint8Array[]} memberPublicKeys One (single identity) or M (group)
|
|
45
|
+
* 64-byte RNS public keys.
|
|
46
|
+
* @param {number} [threshold] Consensus threshold N (defaults to 1).
|
|
47
|
+
*/
|
|
48
|
+
constructor(memberPublicKeys, threshold = 1) {
|
|
49
|
+
if (!(Number.isInteger(threshold) && threshold >= 1)) {
|
|
50
|
+
throw new Error("threshold must be a positive integer");
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(memberPublicKeys) || memberPublicKeys.length < threshold) {
|
|
53
|
+
throw new Error("need at least `threshold` member public keys");
|
|
54
|
+
}
|
|
55
|
+
const keys = [];
|
|
56
|
+
for (const k of memberPublicKeys) {
|
|
57
|
+
if (!(k instanceof Uint8Array) || k.length !== RNS_PUBLIC_KEY_SIZE) {
|
|
58
|
+
throw new RangeError(`RNS public keys are ${RNS_PUBLIC_KEY_SIZE} raw bytes`);
|
|
59
|
+
}
|
|
60
|
+
keys.push(new Uint8Array(k));
|
|
61
|
+
}
|
|
62
|
+
/** @type {Uint8Array[]} */
|
|
63
|
+
this.memberPublicKeys = Object.freeze(keys);
|
|
64
|
+
/** @type {number} */
|
|
65
|
+
this.threshold = threshold;
|
|
66
|
+
Object.freeze(this);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Keyset for a single-identity Issuer (threshold 1).
|
|
71
|
+
* @param {Uint8Array} publicKey
|
|
72
|
+
* @returns {IssuerKeyset}
|
|
73
|
+
*/
|
|
74
|
+
static single(publicKey) {
|
|
75
|
+
return new IssuerKeyset([publicKey], 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Keyset for an N-of-M Threshold Group Issuer (§4.1).
|
|
80
|
+
* @param {Uint8Array[]} memberPublicKeys
|
|
81
|
+
* @param {number} threshold
|
|
82
|
+
* @returns {IssuerKeyset}
|
|
83
|
+
*/
|
|
84
|
+
static group(memberPublicKeys, threshold) {
|
|
85
|
+
return new IssuerKeyset(memberPublicKeys, threshold);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves a 16-byte Issuer hash to its verification keyset, or `null` when the
|
|
91
|
+
* Issuer is unknown (the Operation is then rejected as unverifiable). May be
|
|
92
|
+
* async (e.g. backed by RNS Identity resolution over the network).
|
|
93
|
+
* @typedef {(issuerHash: Uint8Array) => (IssuerKeyset | null | Promise<IssuerKeyset | null>)} KeyResolver
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A Map-backed {@link KeyResolver} for offline and test use.
|
|
98
|
+
*
|
|
99
|
+
* Production nodes will typically back this with RNS Identity resolution
|
|
100
|
+
* (querying the network for the public key behind a 16-byte Identity hash);
|
|
101
|
+
* this in-memory implementation is sufficient for single-node reference
|
|
102
|
+
* deployments, air-gapped sneakernet, and the test suite.
|
|
103
|
+
*/
|
|
104
|
+
export class Keyring {
|
|
105
|
+
constructor() {
|
|
106
|
+
/** @type {Map<string, IssuerKeyset>} */
|
|
107
|
+
this._map = new Map();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Map a 16-byte Issuer hash to its `IssuerKeyset`.
|
|
112
|
+
* @param {Uint8Array} issuerHash
|
|
113
|
+
* @param {IssuerKeyset} keyset
|
|
114
|
+
* @returns {Keyring}
|
|
115
|
+
*/
|
|
116
|
+
register(issuerHash, keyset) {
|
|
117
|
+
this._map.set(toHex(_asHash(issuerHash)), keyset);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {Uint8Array} issuerHash
|
|
123
|
+
* @param {Uint8Array} publicKey
|
|
124
|
+
* @returns {Keyring}
|
|
125
|
+
*/
|
|
126
|
+
registerSingle(issuerHash, publicKey) {
|
|
127
|
+
return this.register(issuerHash, IssuerKeyset.single(publicKey));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {Uint8Array} groupId
|
|
132
|
+
* @param {Uint8Array[]} memberPublicKeys
|
|
133
|
+
* @param {number} threshold
|
|
134
|
+
* @returns {Keyring}
|
|
135
|
+
*/
|
|
136
|
+
registerGroup(groupId, memberPublicKeys, threshold) {
|
|
137
|
+
return this.register(groupId, IssuerKeyset.group(memberPublicKeys, threshold));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {Uint8Array} issuerHash
|
|
142
|
+
* @returns {IssuerKeyset | null}
|
|
143
|
+
*/
|
|
144
|
+
resolve(issuerHash) {
|
|
145
|
+
return this._map.get(toHex(_asHash(issuerHash))) ?? null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Authenticate one Operation against its claimed Issuer (§5.2, §11.2.4).
|
|
151
|
+
*
|
|
152
|
+
* Returns `true` iff the Issuer hash is known to `resolver` *and* the Operation
|
|
153
|
+
* carries a valid threshold signature from the resolved keyset. An unknown
|
|
154
|
+
* Issuer or any cryptographic failure yields `false` — the Operation MUST be
|
|
155
|
+
* dropped rather than merged.
|
|
156
|
+
* @param {import("./operation.js").Operation} operation
|
|
157
|
+
* @param {KeyResolver | Keyring} resolver A function or a Keyring.
|
|
158
|
+
* @returns {Promise<boolean>}
|
|
159
|
+
*/
|
|
160
|
+
export async function verifyOperation(operation, resolver) {
|
|
161
|
+
const keyset = await resolveKeyset(resolver, operation.issuer);
|
|
162
|
+
if (!keyset) return false;
|
|
163
|
+
return operation.verifyKeyset(keyset);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {KeyResolver | Keyring} resolver
|
|
168
|
+
* @param {Uint8Array} hash
|
|
169
|
+
* @returns {Promise<IssuerKeyset | null>}
|
|
170
|
+
*/
|
|
171
|
+
async function resolveKeyset(resolver, hash) {
|
|
172
|
+
if (typeof resolver === "function") return await resolver(hash);
|
|
173
|
+
if (resolver && typeof resolver.resolve === "function") return resolver.resolve(hash);
|
|
174
|
+
throw new TypeError("resolver must be a function or a Keyring");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @param {Uint8Array} value @returns {Uint8Array} */
|
|
178
|
+
function _asHash(value) {
|
|
179
|
+
if (!(value instanceof Uint8Array) || value.length !== HASH_SIZE) {
|
|
180
|
+
throw new RangeError(`issuer hash must be ${HASH_SIZE} bytes`);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|