@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.
@@ -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
+ }