@aroha-sdk/core 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/package.json +35 -0
- package/src/conformance/l0-did-identity.conformance.test.ts +183 -0
- package/src/conformance/l1-signed-envelopes.conformance.test.ts +334 -0
- package/src/crypto/encryption.test.ts +45 -0
- package/src/crypto/encryption.ts +160 -0
- package/src/crypto/index.ts +2 -0
- package/src/crypto/signing.test.ts +79 -0
- package/src/crypto/signing.ts +104 -0
- package/src/identity/credentials.ts +164 -0
- package/src/identity/did-cache.ts +113 -0
- package/src/identity/did.test.ts +232 -0
- package/src/identity/did.ts +426 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/web-did.ts +427 -0
- package/src/index.ts +14 -0
- package/src/messages/envelope.test.ts +289 -0
- package/src/messages/envelope.ts +232 -0
- package/src/messages/idempotency.ts +142 -0
- package/src/messages/index.ts +4 -0
- package/src/messages/nonce.test.ts +92 -0
- package/src/messages/nonce.ts +116 -0
- package/src/messages/types.ts +357 -0
- package/src/transport/client.ts +236 -0
- package/src/transport/http-utils.ts +30 -0
- package/src/transport/index.ts +3 -0
- package/src/transport/server.ts +383 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aroha Web-Native Identity — did:aroha-web:
|
|
3
|
+
*
|
|
4
|
+
* A DNS-anchored DID method that requires NO central Aroha registry.
|
|
5
|
+
* Identity is verified by TWO independent channels simultaneously:
|
|
6
|
+
*
|
|
7
|
+
* 1. HTTPS — DID Document served at:
|
|
8
|
+
* https://{domain}/.well-known/aroha/agents/{path}/did.json
|
|
9
|
+
*
|
|
10
|
+
* 2. DNS TXT — Key-commitment record at:
|
|
11
|
+
* _aroha.{domain} IN TXT "v=aroha1; path={path}; keyhash={sha256 of pubkey}"
|
|
12
|
+
*
|
|
13
|
+
* Both channels must agree. Domain hijacking via HTTPS alone is insufficient —
|
|
14
|
+
* the attacker would also need to control DNS to forge the key-commitment.
|
|
15
|
+
* This is a meaningful security upgrade over single-channel HTTPS identity
|
|
16
|
+
* (did:wba, did:web).
|
|
17
|
+
*
|
|
18
|
+
* Key rotation:
|
|
19
|
+
* Rotations are cryptographically chained — the new key is signed by the old
|
|
20
|
+
* key and a 24-hour cool-down window is enforced before the new key is trusted.
|
|
21
|
+
* The rotation record is embedded in the DID Document under `keyHistory`.
|
|
22
|
+
*
|
|
23
|
+
* DID format:
|
|
24
|
+
* did:aroha-web:{domain}[:{path}]
|
|
25
|
+
* e.g. did:aroha-web:stripe.com:payments-agent
|
|
26
|
+
* did:aroha-web:internal.example.corp:invoicing
|
|
27
|
+
*
|
|
28
|
+
* Resolution produces the same DIDDocument format as did:aroha: — full
|
|
29
|
+
* interoperability with the rest of the Aroha stack.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import * as ed from "@noble/ed25519";
|
|
33
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
34
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
35
|
+
import { bytesToHex, randomBytes } from "@noble/hashes/utils";
|
|
36
|
+
import {
|
|
37
|
+
type DIDDocument,
|
|
38
|
+
type AgentKeyPair,
|
|
39
|
+
type VerificationMethod,
|
|
40
|
+
type KeyRotationRecord,
|
|
41
|
+
toMultibase,
|
|
42
|
+
fromMultibase,
|
|
43
|
+
} from "./did.js";
|
|
44
|
+
|
|
45
|
+
ed.etc.sha512Sync = (...m: Parameters<typeof sha512>) => sha512(...m);
|
|
46
|
+
|
|
47
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface WebDIDDocument extends DIDDocument {
|
|
50
|
+
/** Two-factor verification metadata. */
|
|
51
|
+
arohaWeb: {
|
|
52
|
+
domain: string;
|
|
53
|
+
path: string;
|
|
54
|
+
/** SHA-256 hex of the active public key — must match DNS TXT. */
|
|
55
|
+
keyCommitmentHash: string;
|
|
56
|
+
/** Rotation chain (newest-first). Null if this is the genesis key. */
|
|
57
|
+
keyHistory: KeyRotationRecord[];
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { type KeyRotationRecord } from "./did.js";
|
|
62
|
+
|
|
63
|
+
export interface WebAgentKeyPair extends AgentKeyPair {
|
|
64
|
+
/** The DNS TXT record value to publish at _aroha.{domain}. */
|
|
65
|
+
dnsTXTRecord: string;
|
|
66
|
+
/** The URL where the DID Document should be served. */
|
|
67
|
+
wellKnownUrl: string;
|
|
68
|
+
/** The full typed document (superset of DIDDocument). */
|
|
69
|
+
webDocument: WebDIDDocument;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WebDIDResolutionResult {
|
|
73
|
+
document: WebDIDDocument;
|
|
74
|
+
/** True if DNS TXT key-commitment matched the document. */
|
|
75
|
+
dnsVerified: boolean;
|
|
76
|
+
/** True if the active key's cool-down window has passed. */
|
|
77
|
+
keyTrusted: boolean;
|
|
78
|
+
/** Reason if keyTrusted is false. */
|
|
79
|
+
trustReason?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── DID parsing ──────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** Parse `did:aroha-web:domain[:path]` into its components. */
|
|
85
|
+
export function parseWebDID(did: string): { domain: string; path: string } {
|
|
86
|
+
const prefix = "did:aroha-web:";
|
|
87
|
+
if (!did.startsWith(prefix)) {
|
|
88
|
+
throw new Error(`Not a did:aroha-web: identifier: ${did}`);
|
|
89
|
+
}
|
|
90
|
+
const rest = did.slice(prefix.length);
|
|
91
|
+
const colonIdx = rest.indexOf(":");
|
|
92
|
+
if (colonIdx === -1) {
|
|
93
|
+
return { domain: rest, path: "agent" };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
domain: rest.slice(0, colonIdx),
|
|
97
|
+
path: rest.slice(colonIdx + 1).replace(/:/g, "/"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildWebDID(domain: string, path: string): string {
|
|
102
|
+
const normalised = path.replace(/\//g, ":");
|
|
103
|
+
return `did:aroha-web:${domain}:${normalised}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isWebDID(did: string): boolean {
|
|
107
|
+
return did.startsWith("did:aroha-web:");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Well-known path ──────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The HTTP path at which the DID Document should be served.
|
|
114
|
+
* e.g. `did:aroha-web:stripe.com:payments` → `/.well-known/aroha/agents/payments/did.json`
|
|
115
|
+
*/
|
|
116
|
+
export function wellKnownPath(did: string): string {
|
|
117
|
+
const { path } = parseWebDID(did);
|
|
118
|
+
return `/.well-known/aroha/agents/${path}/did.json`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The full HTTPS URL for a given did:aroha-web: identifier.
|
|
123
|
+
*/
|
|
124
|
+
export function wellKnownUrl(did: string): string {
|
|
125
|
+
const { domain, path } = parseWebDID(did);
|
|
126
|
+
return `https://${domain}/.well-known/aroha/agents/${path}/did.json`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── DNS TXT record ───────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns the DNS TXT record VALUE (not the full DNS record) for the
|
|
133
|
+
* key-commitment. Publish at: `_aroha.{domain} IN TXT "{this value}"`
|
|
134
|
+
*
|
|
135
|
+
* Format: `v=aroha1; path={path}; keyhash={sha256hex}`
|
|
136
|
+
*/
|
|
137
|
+
export function dnsTXTValue(did: string, publicKey: Uint8Array): string {
|
|
138
|
+
const { path } = parseWebDID(did);
|
|
139
|
+
const keyhash = bytesToHex(sha256(publicKey));
|
|
140
|
+
return `v=aroha1; path=${path}; keyhash=${keyhash}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Parse a DNS TXT value back to its fields. Returns null if invalid. */
|
|
144
|
+
export function parseDNSTXT(txt: string): { path: string; keyhash: string } | null {
|
|
145
|
+
if (!txt.includes("v=aroha1")) return null;
|
|
146
|
+
const fields: Record<string, string> = {};
|
|
147
|
+
for (const part of txt.split(";")) {
|
|
148
|
+
const eq = part.indexOf("=");
|
|
149
|
+
if (eq === -1) continue;
|
|
150
|
+
fields[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
151
|
+
}
|
|
152
|
+
if (!fields.path || !fields.keyhash) return null;
|
|
153
|
+
return { path: fields.path, keyhash: fields.keyhash };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Key commitment hash ──────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
export function keyCommitmentHash(publicKey: Uint8Array): string {
|
|
159
|
+
return bytesToHex(sha256(publicKey));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Generate ─────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generate a new did:aroha-web: identity.
|
|
166
|
+
*
|
|
167
|
+
* Returns the key pair, the DID Document to serve at the well-known path,
|
|
168
|
+
* the DNS TXT value to publish, and the well-known URL.
|
|
169
|
+
*
|
|
170
|
+
* @param domain The company domain (e.g. "stripe.com")
|
|
171
|
+
* @param path Agent identifier within the domain (e.g. "payments-agent")
|
|
172
|
+
* @param endpoint The Aroha HTTP endpoint URL (e.g. "https://agents.stripe.com/aroha/v1")
|
|
173
|
+
*/
|
|
174
|
+
export async function generateWebAgent(
|
|
175
|
+
domain: string,
|
|
176
|
+
path: string,
|
|
177
|
+
endpoint?: string
|
|
178
|
+
): Promise<WebAgentKeyPair> {
|
|
179
|
+
const privateKey = ed.utils.randomPrivateKey();
|
|
180
|
+
const publicKey = await ed.getPublicKeyAsync(privateKey);
|
|
181
|
+
|
|
182
|
+
const did = buildWebDID(domain, path);
|
|
183
|
+
const keyId = `${did}#key-1`;
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const agentEndpoint = endpoint ?? `https://${domain}/aroha/v1`;
|
|
186
|
+
|
|
187
|
+
const webDoc: WebDIDDocument = {
|
|
188
|
+
"@context": [
|
|
189
|
+
"https://www.w3.org/ns/did/v1",
|
|
190
|
+
"https://w3id.org/security/suites/ed25519-2020/v1",
|
|
191
|
+
"https://aroha-labs.com/contexts/aroha-web/v1",
|
|
192
|
+
],
|
|
193
|
+
id: did,
|
|
194
|
+
verificationMethod: [
|
|
195
|
+
{
|
|
196
|
+
id: keyId,
|
|
197
|
+
type: "Ed25519VerificationKey2020",
|
|
198
|
+
controller: did,
|
|
199
|
+
publicKeyMultibase: toMultibase(publicKey),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
authentication: [keyId],
|
|
203
|
+
assertionMethod: [keyId],
|
|
204
|
+
keyAgreement: [keyId],
|
|
205
|
+
service: [
|
|
206
|
+
{
|
|
207
|
+
id: `${did}#aroha`,
|
|
208
|
+
type: "ArohaEndpoint",
|
|
209
|
+
serviceEndpoint: agentEndpoint,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
created: now,
|
|
213
|
+
updated: now,
|
|
214
|
+
arohaWeb: {
|
|
215
|
+
domain,
|
|
216
|
+
path,
|
|
217
|
+
keyCommitmentHash: keyCommitmentHash(publicKey),
|
|
218
|
+
keyHistory: [],
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const txt = dnsTXTValue(did, publicKey);
|
|
223
|
+
const url = wellKnownUrl(did);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
did,
|
|
227
|
+
privateKey,
|
|
228
|
+
publicKey,
|
|
229
|
+
document: webDoc,
|
|
230
|
+
webDocument: webDoc,
|
|
231
|
+
dnsTXTRecord: txt,
|
|
232
|
+
wellKnownUrl: url,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Key rotation ──────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Rotate to a new key pair. The old key signs a rotation record that is
|
|
240
|
+
* embedded in the new DID Document. The new key is not trusted until
|
|
241
|
+
* `coolDownHours` (default 24h) after the rotation is announced.
|
|
242
|
+
*
|
|
243
|
+
* Publish the returned `webDocument` at the well-known URL and update the
|
|
244
|
+
* DNS TXT record with `dnsTXTRecord` to complete the rotation.
|
|
245
|
+
*/
|
|
246
|
+
export async function rotateKey(
|
|
247
|
+
current: WebAgentKeyPair,
|
|
248
|
+
coolDownHours = 24
|
|
249
|
+
): Promise<WebAgentKeyPair> {
|
|
250
|
+
const newPrivateKey = ed.utils.randomPrivateKey();
|
|
251
|
+
const newPublicKey = await ed.getPublicKeyAsync(newPrivateKey);
|
|
252
|
+
|
|
253
|
+
const oldPubKeyMultibase = toMultibase(current.publicKey);
|
|
254
|
+
const now = new Date();
|
|
255
|
+
const trustAfter = new Date(now.getTime() + coolDownHours * 3_600_000);
|
|
256
|
+
|
|
257
|
+
// Rotation payload: predecessorKey ∥ newKeyHash ∥ rotatedAt
|
|
258
|
+
const newKeyHash = keyCommitmentHash(newPublicKey);
|
|
259
|
+
const payload = new TextEncoder().encode(
|
|
260
|
+
oldPubKeyMultibase + newKeyHash + now.toISOString()
|
|
261
|
+
);
|
|
262
|
+
const sig = await ed.signAsync(payload, current.privateKey);
|
|
263
|
+
|
|
264
|
+
const rotation: KeyRotationRecord = {
|
|
265
|
+
rotatedAt: now.toISOString(),
|
|
266
|
+
trustAfter: trustAfter.toISOString(),
|
|
267
|
+
predecessorKey: oldPubKeyMultibase,
|
|
268
|
+
newKeyHash,
|
|
269
|
+
predecessorSignature: Buffer.from(sig).toString("base64url"),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const { domain, path } = current.webDocument.arohaWeb;
|
|
273
|
+
const did = buildWebDID(domain, path);
|
|
274
|
+
const keyId = `${did}#key-1`;
|
|
275
|
+
const ts = now.toISOString();
|
|
276
|
+
|
|
277
|
+
const newDoc: WebDIDDocument = {
|
|
278
|
+
...current.webDocument,
|
|
279
|
+
verificationMethod: [
|
|
280
|
+
{
|
|
281
|
+
id: keyId,
|
|
282
|
+
type: "Ed25519VerificationKey2020",
|
|
283
|
+
controller: did,
|
|
284
|
+
publicKeyMultibase: toMultibase(newPublicKey),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
updated: ts,
|
|
288
|
+
arohaWeb: {
|
|
289
|
+
...current.webDocument.arohaWeb,
|
|
290
|
+
keyCommitmentHash: keyCommitmentHash(newPublicKey),
|
|
291
|
+
keyHistory: [rotation, ...current.webDocument.arohaWeb.keyHistory],
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
did,
|
|
297
|
+
privateKey: newPrivateKey,
|
|
298
|
+
publicKey: newPublicKey,
|
|
299
|
+
document: newDoc,
|
|
300
|
+
webDocument: newDoc,
|
|
301
|
+
dnsTXTRecord: dnsTXTValue(did, newPublicKey),
|
|
302
|
+
wellKnownUrl: wellKnownUrl(did),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Resolve ──────────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve a did:aroha-web: identifier.
|
|
310
|
+
*
|
|
311
|
+
* Fetches the DID Document over HTTPS and, if `verifyDNS` is true,
|
|
312
|
+
* also verifies the DNS TXT key-commitment. Both must match for full
|
|
313
|
+
* two-factor verification.
|
|
314
|
+
*
|
|
315
|
+
* In Node.js server environments, pass a custom `dnsResolver` to perform
|
|
316
|
+
* the DNS TXT lookup. In browser or edge environments, set `verifyDNS: false`
|
|
317
|
+
* (single-channel HTTPS-only, same security as did:wba).
|
|
318
|
+
*
|
|
319
|
+
* @param did The did:aroha-web: identifier to resolve
|
|
320
|
+
* @param verifyDNS Whether to check the DNS TXT record (default: true)
|
|
321
|
+
* @param dnsResolver Optional custom DNS TXT resolver
|
|
322
|
+
*/
|
|
323
|
+
export async function resolveWebDID(
|
|
324
|
+
did: string,
|
|
325
|
+
verifyDNS = true,
|
|
326
|
+
dnsResolver?: (domain: string) => Promise<string[]>,
|
|
327
|
+
fetchTimeoutMs = 10_000
|
|
328
|
+
): Promise<WebDIDResolutionResult> {
|
|
329
|
+
const url = wellKnownUrl(did);
|
|
330
|
+
|
|
331
|
+
// ── Step 1: Fetch DID Document over HTTPS ─────────────────────────────────
|
|
332
|
+
const ctrl = new AbortController();
|
|
333
|
+
const timer = setTimeout(() => ctrl.abort(), fetchTimeoutMs);
|
|
334
|
+
let res: Response;
|
|
335
|
+
try {
|
|
336
|
+
res = await fetch(url, {
|
|
337
|
+
headers: { Accept: "application/json" },
|
|
338
|
+
signal: ctrl.signal,
|
|
339
|
+
});
|
|
340
|
+
} finally {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
}
|
|
343
|
+
if (!res.ok) {
|
|
344
|
+
throw new Error(`Failed to resolve ${did}: HTTP ${res.status} from ${url}`);
|
|
345
|
+
}
|
|
346
|
+
const doc = await res.json() as WebDIDDocument;
|
|
347
|
+
|
|
348
|
+
if (doc.id !== did) {
|
|
349
|
+
throw new Error(`DID mismatch: document claims ${doc.id}, expected ${did}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const activeKey = fromMultibase(doc.verificationMethod[0].publicKeyMultibase);
|
|
353
|
+
|
|
354
|
+
// ── Step 2: Verify all rotation records in keyHistory ────────────────────
|
|
355
|
+
if (doc.arohaWeb?.keyHistory?.length > 0) {
|
|
356
|
+
for (const rotation of doc.arohaWeb.keyHistory) {
|
|
357
|
+
const rotationValid = await verifyKeyRotation(rotation);
|
|
358
|
+
if (!rotationValid) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`DID ${did}: key rotation record (rotatedAt ${rotation.rotatedAt}) has invalid predecessor signature`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Step 4: Check key rotation cool-down ─────────────────────────────────
|
|
367
|
+
let keyTrusted = true;
|
|
368
|
+
let trustReason: string | undefined;
|
|
369
|
+
if (doc.arohaWeb?.keyHistory?.length > 0) {
|
|
370
|
+
const latestRotation = doc.arohaWeb.keyHistory[0];
|
|
371
|
+
const trustAfter = new Date(latestRotation.trustAfter);
|
|
372
|
+
if (new Date() < trustAfter) {
|
|
373
|
+
keyTrusted = false;
|
|
374
|
+
trustReason = `Key rotation cool-down active until ${latestRotation.trustAfter}`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Step 5: DNS TXT key-commitment verification ────────────────────────────
|
|
379
|
+
let dnsVerified = false;
|
|
380
|
+
if (verifyDNS) {
|
|
381
|
+
const { domain, path } = parseWebDID(did);
|
|
382
|
+
let txtRecords: string[] = [];
|
|
383
|
+
if (dnsResolver) {
|
|
384
|
+
txtRecords = await dnsResolver(domain).catch(() => []);
|
|
385
|
+
} else if (typeof (globalThis as Record<string, unknown>)["Deno"] !== "undefined") {
|
|
386
|
+
// Deno DNS API
|
|
387
|
+
try {
|
|
388
|
+
const denoGlobal = (globalThis as unknown as { Deno: { resolveDns(n: string, t: string): Promise<string[][]> } }).Deno;
|
|
389
|
+
const records = await denoGlobal
|
|
390
|
+
.resolveDns(`_aroha.${domain}`, "TXT");
|
|
391
|
+
txtRecords = records.flat();
|
|
392
|
+
} catch { /* DNS unavailable — fall back to HTTPS-only */ }
|
|
393
|
+
}
|
|
394
|
+
// Node.js: caller must pass dnsResolver (avoids bundling dns module in core)
|
|
395
|
+
|
|
396
|
+
for (const txt of txtRecords) {
|
|
397
|
+
const parsed = parseDNSTXT(txt);
|
|
398
|
+
if (!parsed) continue;
|
|
399
|
+
if (parsed.path !== path) continue;
|
|
400
|
+
const expectedHash = keyCommitmentHash(activeKey);
|
|
401
|
+
if (parsed.keyhash === expectedHash) {
|
|
402
|
+
dnsVerified = true;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { document: doc, dnsVerified, keyTrusted, trustReason };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Verify a key rotation record — confirms the predecessor key signed the
|
|
413
|
+
* rotation event voluntarily (prevents unauthorized key replacement).
|
|
414
|
+
*/
|
|
415
|
+
export async function verifyKeyRotation(rotation: KeyRotationRecord): Promise<boolean> {
|
|
416
|
+
try {
|
|
417
|
+
const predKey = fromMultibase(rotation.predecessorKey);
|
|
418
|
+
const newHash = rotation.newKeyHash;
|
|
419
|
+
const payload = new TextEncoder().encode(
|
|
420
|
+
rotation.predecessorKey + newHash + rotation.rotatedAt
|
|
421
|
+
);
|
|
422
|
+
const sig = Buffer.from(rotation.predecessorSignature, "base64url");
|
|
423
|
+
return await ed.verifyAsync(sig, payload, predKey);
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aroha-sdk/core — Aroha Protocol Layers 0–3
|
|
3
|
+
*
|
|
4
|
+
* Layer 0: Transport → ArohaServer, ArohaClient
|
|
5
|
+
* Layer 1: Identity → generateDID, DIDDocument, issueCredential, verifyCredential
|
|
6
|
+
* Auth → issueCredential, verifyCredential, ArohaRole, RbacPolicy, checkPermission
|
|
7
|
+
* Layer 3: Messaging → buildEnvelope, validateEnvelope, ArohaEnvelope, all message types
|
|
8
|
+
* Crypto: → signMessage, verifyMessageSignature, encryptBody, decryptBody
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from "./identity/index.js";
|
|
12
|
+
export * from "./crypto/index.js";
|
|
13
|
+
export * from "./messages/index.js";
|
|
14
|
+
export * from "./transport/index.js";
|