@totemsdk/identity 0.1.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +306 -0
- package/dist/canonical.js +6 -2
- package/dist/claims.js +15 -9
- package/dist/constants.js +4 -1
- package/dist/document.js +12 -8
- package/dist/guards.js +12 -5
- package/dist/index.js +32 -10
- package/dist/manifest-binding.js +11 -7
- package/dist/resolver.js +6 -3
- package/dist/revocation.js +6 -3
- package/dist/rotation.js +6 -3
- package/dist/signing.js +16 -12
- package/dist/types.js +2 -1
- package/dist/verify.js +13 -10
- package/package.json +29 -6
- package/src/__tests__/identity.test.ts +0 -618
- package/src/canonical.ts +0 -27
- package/src/claims.ts +0 -108
- package/src/constants.ts +0 -1
- package/src/document.ts +0 -35
- package/src/guards.ts +0 -75
- package/src/index.ts +0 -55
- package/src/manifest-binding.ts +0 -163
- package/src/resolver.ts +0 -171
- package/src/revocation.ts +0 -25
- package/src/rotation.ts +0 -23
- package/src/signing.ts +0 -38
- package/src/types.ts +0 -147
- package/src/verify.ts +0 -90
|
@@ -1,618 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @totemsdk/identity — test suite
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
IDENTITY_VERSION,
|
|
7
|
-
createIdentityDocument,
|
|
8
|
-
computeIdentityId,
|
|
9
|
-
createIdentityClaim,
|
|
10
|
-
createDelegationClaim,
|
|
11
|
-
createPaymentRecipientClaim,
|
|
12
|
-
createServiceEndpointClaim,
|
|
13
|
-
signIdentityClaim,
|
|
14
|
-
verifyIdentityClaim,
|
|
15
|
-
rotateIdentity,
|
|
16
|
-
revokeIdentity,
|
|
17
|
-
resolveIdentityGraph,
|
|
18
|
-
bindManifestToIdentity,
|
|
19
|
-
verifyManifestIdentity,
|
|
20
|
-
isTotemIdentityDocument,
|
|
21
|
-
isIdentityClaim,
|
|
22
|
-
isSignedIdentityClaim,
|
|
23
|
-
isRotationClaim,
|
|
24
|
-
isRevocationClaim,
|
|
25
|
-
} from '../index';
|
|
26
|
-
import type {
|
|
27
|
-
IdentityGraph,
|
|
28
|
-
SignedIdentityClaim,
|
|
29
|
-
} from '../index';
|
|
30
|
-
|
|
31
|
-
// We need @totemsdk/manifest to sign manifests for binding tests
|
|
32
|
-
import { signManifest } from '@totemsdk/manifest';
|
|
33
|
-
import { wotsKeypairFromSeed, wotsAddressFromKeypair } from '@totemsdk/core';
|
|
34
|
-
|
|
35
|
-
// Generate a deterministic test seed
|
|
36
|
-
function testSeed(n: number): Uint8Array {
|
|
37
|
-
const s = new Uint8Array(32);
|
|
38
|
-
s[0] = n & 0xff;
|
|
39
|
-
s[1] = (n >> 8) & 0xff;
|
|
40
|
-
return s;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const SEED_A = testSeed(1);
|
|
44
|
-
const SEED_B = testSeed(2);
|
|
45
|
-
|
|
46
|
-
/** Derive the Minima address from a seed + keyIndex without signing */
|
|
47
|
-
function deriveAddress(seed: Uint8Array, keyIndex: number): string {
|
|
48
|
-
const kp = wotsKeypairFromSeed(seed, keyIndex);
|
|
49
|
-
return wotsAddressFromKeypair(kp);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ─── Document creation ────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
describe('createIdentityDocument', () => {
|
|
55
|
-
it('creates a document with required fields', () => {
|
|
56
|
-
const doc = createIdentityDocument({
|
|
57
|
-
kind: 'device',
|
|
58
|
-
rootAddress: 'MxROOT',
|
|
59
|
-
controllerAddress: 'MxCTRL',
|
|
60
|
-
});
|
|
61
|
-
expect(doc.kind).toBe('device');
|
|
62
|
-
expect(doc.version).toBe(IDENTITY_VERSION);
|
|
63
|
-
expect(doc.rootAddress).toBe('MxROOT');
|
|
64
|
-
expect(doc.controllerAddress).toBe('MxCTRL');
|
|
65
|
-
expect(typeof doc.createdAt).toBe('number');
|
|
66
|
-
expect(doc.id).toMatch(/^totem:id:device:/);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('stable ID — does not include version', () => {
|
|
70
|
-
const id1 = computeIdentityId('device', 'MxROOT');
|
|
71
|
-
const id2 = computeIdentityId('device', 'MxROOT');
|
|
72
|
-
expect(id1).toBe(id2);
|
|
73
|
-
expect(id1).toMatch(/^totem:id:device:/);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('different rootAddress produces different ID', () => {
|
|
77
|
-
const id1 = computeIdentityId('device', 'MxROOT1');
|
|
78
|
-
const id2 = computeIdentityId('device', 'MxROOT2');
|
|
79
|
-
expect(id1).not.toBe(id2);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ─── Claim creation ───────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe('createIdentityClaim', () => {
|
|
86
|
-
it('creates a claim with deterministic ID', () => {
|
|
87
|
-
const c1 = createIdentityClaim({
|
|
88
|
-
type: 'delegates_to',
|
|
89
|
-
issuer: 'MxA',
|
|
90
|
-
subject: 'totem:id:device:abc',
|
|
91
|
-
object: 'MxB',
|
|
92
|
-
payload: { scopes: ['manifest:sign'] },
|
|
93
|
-
issuedAt: 1000,
|
|
94
|
-
});
|
|
95
|
-
const c2 = createIdentityClaim({
|
|
96
|
-
type: 'delegates_to',
|
|
97
|
-
issuer: 'MxA',
|
|
98
|
-
subject: 'totem:id:device:abc',
|
|
99
|
-
object: 'MxB',
|
|
100
|
-
payload: { scopes: ['manifest:sign'] },
|
|
101
|
-
issuedAt: 1000,
|
|
102
|
-
});
|
|
103
|
-
expect(c1.id).toBe(c2.id);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('different payload produces different claim ID', () => {
|
|
107
|
-
const c1 = createIdentityClaim({
|
|
108
|
-
type: 'delegates_to',
|
|
109
|
-
issuer: 'MxA',
|
|
110
|
-
subject: 'totem:id:device:abc',
|
|
111
|
-
object: 'MxB',
|
|
112
|
-
payload: { scopes: ['manifest:sign'] },
|
|
113
|
-
issuedAt: 1000,
|
|
114
|
-
});
|
|
115
|
-
const c2 = createIdentityClaim({
|
|
116
|
-
type: 'delegates_to',
|
|
117
|
-
issuer: 'MxA',
|
|
118
|
-
subject: 'totem:id:device:abc',
|
|
119
|
-
object: 'MxB',
|
|
120
|
-
payload: { scopes: ['claim:issue'] },
|
|
121
|
-
issuedAt: 1000,
|
|
122
|
-
});
|
|
123
|
-
expect(c1.id).not.toBe(c2.id);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// ─── Sign → verify round-trip ─────────────────────────────────────────────────
|
|
128
|
-
|
|
129
|
-
describe('signIdentityClaim / verifyIdentityClaim', () => {
|
|
130
|
-
it('sign then verify returns valid', async () => {
|
|
131
|
-
const claim = createDelegationClaim({
|
|
132
|
-
issuer: 'MxROOT',
|
|
133
|
-
subject: 'totem:id:device:xyz',
|
|
134
|
-
delegatedAddress: 'MxDELEGATE',
|
|
135
|
-
scopes: ['manifest:sign'],
|
|
136
|
-
issuedAt: 1000,
|
|
137
|
-
});
|
|
138
|
-
const signed = await signIdentityClaim(claim, SEED_A, 0);
|
|
139
|
-
const result = verifyIdentityClaim(signed);
|
|
140
|
-
expect(result.valid).toBe(true);
|
|
141
|
-
expect(result.signerAddress).toBeDefined();
|
|
142
|
-
}, 30000);
|
|
143
|
-
|
|
144
|
-
it('tampered claim fails verification', async () => {
|
|
145
|
-
const claim = createDelegationClaim({
|
|
146
|
-
issuer: 'MxROOT',
|
|
147
|
-
subject: 'totem:id:device:xyz',
|
|
148
|
-
delegatedAddress: 'MxDELEGATE',
|
|
149
|
-
scopes: ['manifest:sign'],
|
|
150
|
-
issuedAt: 1000,
|
|
151
|
-
});
|
|
152
|
-
const signed = await signIdentityClaim(claim, SEED_A, 0);
|
|
153
|
-
const tampered: SignedIdentityClaim = {
|
|
154
|
-
...signed,
|
|
155
|
-
claim: { ...signed.claim, issuer: 'MxATTACKER' },
|
|
156
|
-
};
|
|
157
|
-
const result = verifyIdentityClaim(tampered);
|
|
158
|
-
expect(result.valid).toBe(false);
|
|
159
|
-
}, 30000);
|
|
160
|
-
|
|
161
|
-
it('proof.message is not used as signing source', async () => {
|
|
162
|
-
const claim = createDelegationClaim({
|
|
163
|
-
issuer: 'MxROOT',
|
|
164
|
-
subject: 'totem:id:device:xyz',
|
|
165
|
-
delegatedAddress: 'MxDELEGATE',
|
|
166
|
-
scopes: ['manifest:sign'],
|
|
167
|
-
issuedAt: 1000,
|
|
168
|
-
});
|
|
169
|
-
const signed = await signIdentityClaim(claim, SEED_A, 0);
|
|
170
|
-
const withFakeMessage: SignedIdentityClaim = {
|
|
171
|
-
...signed,
|
|
172
|
-
proof: { ...signed.proof, message: 'fake message that was not signed' },
|
|
173
|
-
};
|
|
174
|
-
// Still valid because verifyIdentityClaim ignores proof.message
|
|
175
|
-
const result = verifyIdentityClaim(withFakeMessage);
|
|
176
|
-
expect(result.valid).toBe(true);
|
|
177
|
-
}, 30000);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// ─── Resolver ─────────────────────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
describe('resolveIdentityGraph', () => {
|
|
183
|
-
it('resolves with no claims as active', () => {
|
|
184
|
-
const doc = createIdentityDocument({
|
|
185
|
-
kind: 'agent',
|
|
186
|
-
rootAddress: 'MxROOT',
|
|
187
|
-
controllerAddress: 'MxCTRL',
|
|
188
|
-
});
|
|
189
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
190
|
-
const result = resolveIdentityGraph(graph);
|
|
191
|
-
expect(result.resolved?.status).toBe('active');
|
|
192
|
-
expect(result.resolved?.delegates).toHaveLength(0);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('resolves delegation claim', async () => {
|
|
196
|
-
// Use the actual address derived from SEED_A as rootAddress so the signer matches the issuer
|
|
197
|
-
const rootAddr = deriveAddress(SEED_A, 0);
|
|
198
|
-
const doc = createIdentityDocument({
|
|
199
|
-
kind: 'agent',
|
|
200
|
-
rootAddress: rootAddr,
|
|
201
|
-
controllerAddress: rootAddr,
|
|
202
|
-
});
|
|
203
|
-
const delClaim = createDelegationClaim({
|
|
204
|
-
issuer: rootAddr,
|
|
205
|
-
subject: doc.id,
|
|
206
|
-
delegatedAddress: 'MxDELEGATE',
|
|
207
|
-
scopes: ['manifest:sign'],
|
|
208
|
-
issuedAt: 1000,
|
|
209
|
-
});
|
|
210
|
-
const signed = await signIdentityClaim(delClaim, SEED_A, 0);
|
|
211
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
212
|
-
const result = resolveIdentityGraph(graph);
|
|
213
|
-
expect(result.resolved?.delegates).toHaveLength(1);
|
|
214
|
-
expect(result.resolved?.delegates[0].delegatedAddress).toBe('MxDELEGATE');
|
|
215
|
-
expect(result.resolved?.authorizedAddresses).toContain('MxDELEGATE');
|
|
216
|
-
}, 30000);
|
|
217
|
-
|
|
218
|
-
it('resolves payment recipient claim', async () => {
|
|
219
|
-
const rootAddr = deriveAddress(SEED_A, 0);
|
|
220
|
-
const doc = createIdentityDocument({
|
|
221
|
-
kind: 'person',
|
|
222
|
-
rootAddress: rootAddr,
|
|
223
|
-
controllerAddress: rootAddr,
|
|
224
|
-
});
|
|
225
|
-
const paymentClaim = createPaymentRecipientClaim({
|
|
226
|
-
issuer: rootAddr,
|
|
227
|
-
subject: doc.id,
|
|
228
|
-
address: 'MxPAY',
|
|
229
|
-
label: 'Main wallet',
|
|
230
|
-
issuedAt: 1000,
|
|
231
|
-
});
|
|
232
|
-
const signed = await signIdentityClaim(paymentClaim, SEED_A, 0);
|
|
233
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
234
|
-
const result = resolveIdentityGraph(graph);
|
|
235
|
-
expect(result.resolved?.paymentRecipients).toHaveLength(1);
|
|
236
|
-
expect(result.resolved?.paymentRecipients[0].address).toBe('MxPAY');
|
|
237
|
-
expect(result.resolved?.paymentRecipients[0].label).toBe('Main wallet');
|
|
238
|
-
}, 30000);
|
|
239
|
-
|
|
240
|
-
it('resolves service endpoint claim', async () => {
|
|
241
|
-
const rootAddr = deriveAddress(SEED_A, 0);
|
|
242
|
-
const doc = createIdentityDocument({
|
|
243
|
-
kind: 'service',
|
|
244
|
-
rootAddress: rootAddr,
|
|
245
|
-
controllerAddress: rootAddr,
|
|
246
|
-
});
|
|
247
|
-
const epClaim = createServiceEndpointClaim({
|
|
248
|
-
issuer: rootAddr,
|
|
249
|
-
subject: doc.id,
|
|
250
|
-
endpointType: 'https',
|
|
251
|
-
uri: 'https://api.example.com',
|
|
252
|
-
issuedAt: 1000,
|
|
253
|
-
});
|
|
254
|
-
const signed = await signIdentityClaim(epClaim, SEED_A, 0);
|
|
255
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
256
|
-
const result = resolveIdentityGraph(graph);
|
|
257
|
-
expect(result.resolved?.serviceEndpoints).toHaveLength(1);
|
|
258
|
-
expect(result.resolved?.serviceEndpoints[0].uri).toBe('https://api.example.com');
|
|
259
|
-
}, 30000);
|
|
260
|
-
|
|
261
|
-
it('detects revocation', async () => {
|
|
262
|
-
const rootAddr = deriveAddress(SEED_A, 0);
|
|
263
|
-
const doc = createIdentityDocument({
|
|
264
|
-
kind: 'device',
|
|
265
|
-
rootAddress: rootAddr,
|
|
266
|
-
controllerAddress: rootAddr,
|
|
267
|
-
});
|
|
268
|
-
const revClaim = revokeIdentity({
|
|
269
|
-
issuer: rootAddr,
|
|
270
|
-
subject: doc.id,
|
|
271
|
-
reason: 'compromised',
|
|
272
|
-
issuedAt: 2000,
|
|
273
|
-
});
|
|
274
|
-
const signed = await signIdentityClaim(revClaim, SEED_A, 0);
|
|
275
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
276
|
-
const result = resolveIdentityGraph(graph);
|
|
277
|
-
expect(result.resolved?.status).toBe('revoked');
|
|
278
|
-
expect(result.resolved?.revokedAt).toBe(2000);
|
|
279
|
-
}, 30000);
|
|
280
|
-
|
|
281
|
-
it('detects rotation', async () => {
|
|
282
|
-
const rootAddr = deriveAddress(SEED_A, 0);
|
|
283
|
-
const doc = createIdentityDocument({
|
|
284
|
-
kind: 'device',
|
|
285
|
-
rootAddress: rootAddr,
|
|
286
|
-
controllerAddress: rootAddr,
|
|
287
|
-
});
|
|
288
|
-
const rotClaim = rotateIdentity({
|
|
289
|
-
issuer: rootAddr,
|
|
290
|
-
subject: doc.id,
|
|
291
|
-
newAddress: 'MxNEWROOT',
|
|
292
|
-
issuedAt: 3000,
|
|
293
|
-
});
|
|
294
|
-
const signed = await signIdentityClaim(rotClaim, SEED_A, 0);
|
|
295
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
296
|
-
const result = resolveIdentityGraph(graph);
|
|
297
|
-
expect(result.resolved?.status).toBe('rotated');
|
|
298
|
-
expect(result.resolved?.rotationTarget).toBe('MxNEWROOT');
|
|
299
|
-
}, 30000);
|
|
300
|
-
|
|
301
|
-
it('drops claims from unauthorized issuers', async () => {
|
|
302
|
-
const doc = createIdentityDocument({
|
|
303
|
-
kind: 'agent',
|
|
304
|
-
rootAddress: 'MxROOT',
|
|
305
|
-
controllerAddress: 'MxCTRL',
|
|
306
|
-
});
|
|
307
|
-
const unauthorizedClaim = createDelegationClaim({
|
|
308
|
-
issuer: 'MxATTACKER',
|
|
309
|
-
subject: doc.id,
|
|
310
|
-
delegatedAddress: 'MxBAD',
|
|
311
|
-
scopes: ['*'],
|
|
312
|
-
issuedAt: 1000,
|
|
313
|
-
});
|
|
314
|
-
const signed = await signIdentityClaim(unauthorizedClaim, SEED_B, 0);
|
|
315
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
316
|
-
const result = resolveIdentityGraph(graph);
|
|
317
|
-
expect(result.resolved?.delegates).toHaveLength(0);
|
|
318
|
-
expect(result.resolved?.authorizedAddresses).toHaveLength(0);
|
|
319
|
-
}, 30000);
|
|
320
|
-
|
|
321
|
-
it('regression: issuer-spoofing — attacker signs claim but sets issuer=rootAddress → dropped', async () => {
|
|
322
|
-
// Attack scenario: attacker sets claim.issuer = 'MxROOT' (a privileged address they don't control)
|
|
323
|
-
// but signs the claim with their own key (SEED_A). SEED_A's address is NOT 'MxROOT'.
|
|
324
|
-
//
|
|
325
|
-
// verifyIdentityClaim only checks internal proof consistency (signature valid + proof.address
|
|
326
|
-
// is derived from proof.publicKey). It does NOT check claim.issuer === proof.address —
|
|
327
|
-
// that binding is enforced by the resolver.
|
|
328
|
-
// The resolver rejects any claim where signerAddress !== claim.issuer.
|
|
329
|
-
const doc = createIdentityDocument({
|
|
330
|
-
kind: 'agent',
|
|
331
|
-
rootAddress: 'MxROOT',
|
|
332
|
-
controllerAddress: 'MxCTRL',
|
|
333
|
-
});
|
|
334
|
-
const forgedClaim = createDelegationClaim({
|
|
335
|
-
issuer: 'MxROOT', // spoofed — attacker pretends to be root
|
|
336
|
-
subject: doc.id,
|
|
337
|
-
delegatedAddress: 'MxATTACKER_DELEGATE',
|
|
338
|
-
scopes: ['*'],
|
|
339
|
-
issuedAt: 1000,
|
|
340
|
-
});
|
|
341
|
-
// Sign with SEED_A — the attacker's actual key, whose address is NOT 'MxROOT'
|
|
342
|
-
const signed = await signIdentityClaim(forgedClaim, SEED_A, 0);
|
|
343
|
-
// proof.address is cryptographically bound to SEED_A's public key — not 'MxROOT'
|
|
344
|
-
expect(signed.proof.address).not.toBe('MxROOT');
|
|
345
|
-
// verifyIdentityClaim returns valid=true: internal proof is consistent (sig OK, address bound to pk)
|
|
346
|
-
// but signerAddress !== claim.issuer — the resolver uses this gap to reject it
|
|
347
|
-
const vr = verifyIdentityClaim(signed);
|
|
348
|
-
expect(vr.valid).toBe(true);
|
|
349
|
-
expect(vr.signerAddress).not.toBe('MxROOT'); // signer ≠ claimed issuer
|
|
350
|
-
// resolver silently drops it: signerAddress !== claim.issuer
|
|
351
|
-
const graph: IdentityGraph = { document: doc, claims: [signed] };
|
|
352
|
-
const result = resolveIdentityGraph(graph);
|
|
353
|
-
expect(result.resolved?.delegates).toHaveLength(0);
|
|
354
|
-
expect(result.resolved?.authorizedAddresses).toHaveLength(0);
|
|
355
|
-
}, 30000);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// ─── Manifest binding ─────────────────────────────────────────────────────────
|
|
359
|
-
|
|
360
|
-
describe('verifyManifestIdentity / bindManifestToIdentity', () => {
|
|
361
|
-
async function makeEdgeServiceManifest(seed: Uint8Array, keyIndex: number) {
|
|
362
|
-
const signerAddr = deriveAddress(seed, keyIndex);
|
|
363
|
-
const manifest = {
|
|
364
|
-
type: 'edge-service' as const,
|
|
365
|
-
serviceId: 'svc-1',
|
|
366
|
-
name: 'Test Service',
|
|
367
|
-
version: '1.0.0',
|
|
368
|
-
operatorAddress: signerAddr,
|
|
369
|
-
serviceType: 'sensor' as const,
|
|
370
|
-
description: 'test',
|
|
371
|
-
capabilities: [],
|
|
372
|
-
tags: [],
|
|
373
|
-
};
|
|
374
|
-
return signManifest(manifest, seed, keyIndex);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
it('binds EdgeServiceManifest to identity', async () => {
|
|
378
|
-
const signed = await makeEdgeServiceManifest(SEED_A, 0);
|
|
379
|
-
const doc = createIdentityDocument({
|
|
380
|
-
kind: 'service',
|
|
381
|
-
rootAddress: signed.authorAddress,
|
|
382
|
-
controllerAddress: signed.authorAddress,
|
|
383
|
-
});
|
|
384
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
385
|
-
const binding = await bindManifestToIdentity(signed, graph);
|
|
386
|
-
expect(binding.valid).toBe(true);
|
|
387
|
-
expect(binding.signerAddress).toBe(signed.authorAddress);
|
|
388
|
-
}, 30000);
|
|
389
|
-
|
|
390
|
-
it('binds AppManifest to identity via root address', async () => {
|
|
391
|
-
const signerAddr = deriveAddress(SEED_A, 0);
|
|
392
|
-
const appManifest = {
|
|
393
|
-
type: 'app' as const,
|
|
394
|
-
appId: 'app-1',
|
|
395
|
-
name: 'Test App',
|
|
396
|
-
version: '1.0.0',
|
|
397
|
-
authorAddress: signerAddr,
|
|
398
|
-
pearTopicKey: 'pk1',
|
|
399
|
-
price: '0',
|
|
400
|
-
category: ['utility'],
|
|
401
|
-
permissions: [] as any[],
|
|
402
|
-
description: 'test app',
|
|
403
|
-
minTotemVersion: '1.0.0',
|
|
404
|
-
};
|
|
405
|
-
const signed = await signManifest(appManifest, SEED_A, 0);
|
|
406
|
-
const doc = createIdentityDocument({
|
|
407
|
-
kind: 'person',
|
|
408
|
-
rootAddress: signed.authorAddress,
|
|
409
|
-
controllerAddress: signed.authorAddress,
|
|
410
|
-
});
|
|
411
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
412
|
-
const binding = await bindManifestToIdentity(signed, graph);
|
|
413
|
-
expect(binding.valid).toBe(true);
|
|
414
|
-
}, 30000);
|
|
415
|
-
|
|
416
|
-
it('binds CapabilityManifest to identity', async () => {
|
|
417
|
-
const signerAddr = deriveAddress(SEED_A, 0);
|
|
418
|
-
const capManifest = {
|
|
419
|
-
type: 'capability' as const,
|
|
420
|
-
capabilityId: 'cap-1',
|
|
421
|
-
capabilityName: 'Test Cap',
|
|
422
|
-
agentAddress: signerAddr,
|
|
423
|
-
agentIdentityKey: 'ik1',
|
|
424
|
-
description: 'test',
|
|
425
|
-
inputSchema: {},
|
|
426
|
-
outputSchema: {},
|
|
427
|
-
pricePerCall: '0',
|
|
428
|
-
expiresAt: Date.now() + 99999,
|
|
429
|
-
tags: [],
|
|
430
|
-
};
|
|
431
|
-
const signed = await signManifest(capManifest, SEED_A, 0);
|
|
432
|
-
const doc = createIdentityDocument({
|
|
433
|
-
kind: 'agent',
|
|
434
|
-
rootAddress: signed.authorAddress,
|
|
435
|
-
controllerAddress: signed.authorAddress,
|
|
436
|
-
});
|
|
437
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
438
|
-
const binding = await bindManifestToIdentity(signed, graph);
|
|
439
|
-
expect(binding.valid).toBe(true);
|
|
440
|
-
}, 30000);
|
|
441
|
-
|
|
442
|
-
it('binds DAppManifest to identity', async () => {
|
|
443
|
-
const signerAddr = deriveAddress(SEED_A, 0);
|
|
444
|
-
const dappManifest = {
|
|
445
|
-
type: 'dapp' as const,
|
|
446
|
-
dappId: 'dapp-1',
|
|
447
|
-
name: 'Test DApp',
|
|
448
|
-
version: '1.0.0',
|
|
449
|
-
authorAddress: signerAddr,
|
|
450
|
-
contractHash: 'deadbeef',
|
|
451
|
-
abi: [],
|
|
452
|
-
price: '0',
|
|
453
|
-
category: ['defi'],
|
|
454
|
-
description: 'test dapp',
|
|
455
|
-
};
|
|
456
|
-
const signed = await signManifest(dappManifest, SEED_A, 0);
|
|
457
|
-
const doc = createIdentityDocument({
|
|
458
|
-
kind: 'person',
|
|
459
|
-
rootAddress: signed.authorAddress,
|
|
460
|
-
controllerAddress: signed.authorAddress,
|
|
461
|
-
});
|
|
462
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
463
|
-
const binding = await bindManifestToIdentity(signed, graph);
|
|
464
|
-
expect(binding.valid).toBe(true);
|
|
465
|
-
}, 30000);
|
|
466
|
-
|
|
467
|
-
it('fails if manifest signature is invalid', async () => {
|
|
468
|
-
const signed = await makeEdgeServiceManifest(SEED_A, 0);
|
|
469
|
-
const tampered = { ...signed, signature: '00'.repeat(134) };
|
|
470
|
-
const doc = createIdentityDocument({
|
|
471
|
-
kind: 'service',
|
|
472
|
-
rootAddress: signed.authorAddress,
|
|
473
|
-
controllerAddress: signed.authorAddress,
|
|
474
|
-
});
|
|
475
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
476
|
-
const binding = await bindManifestToIdentity(tampered, graph);
|
|
477
|
-
expect(binding.valid).toBe(false);
|
|
478
|
-
expect(binding.reason).toMatch(/manifest signature invalid/);
|
|
479
|
-
}, 30000);
|
|
480
|
-
|
|
481
|
-
it('fails if signer address not associated with identity', async () => {
|
|
482
|
-
const signed = await makeEdgeServiceManifest(SEED_A, 0);
|
|
483
|
-
const doc = createIdentityDocument({
|
|
484
|
-
kind: 'service',
|
|
485
|
-
rootAddress: 'MxCOMPLETELY_DIFFERENT',
|
|
486
|
-
controllerAddress: 'MxCOMPLETELY_DIFFERENT',
|
|
487
|
-
});
|
|
488
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
489
|
-
const binding = await bindManifestToIdentity(signed, graph);
|
|
490
|
-
expect(binding.valid).toBe(false);
|
|
491
|
-
}, 30000);
|
|
492
|
-
|
|
493
|
-
it('rootIdentityProof is silently ignored if no verifier registered', async () => {
|
|
494
|
-
const signed = await makeEdgeServiceManifest(SEED_A, 0);
|
|
495
|
-
const withProof = { ...signed, rootIdentityProof: 'some-serialized-proof' };
|
|
496
|
-
const doc = createIdentityDocument({
|
|
497
|
-
kind: 'service',
|
|
498
|
-
rootAddress: signed.authorAddress,
|
|
499
|
-
controllerAddress: signed.authorAddress,
|
|
500
|
-
});
|
|
501
|
-
const graph: IdentityGraph = { document: doc, claims: [] };
|
|
502
|
-
const binding = await bindManifestToIdentity(withProof, graph);
|
|
503
|
-
expect(binding.valid).toBe(true);
|
|
504
|
-
}, 30000);
|
|
505
|
-
|
|
506
|
-
it('regression: manifest signed by controlledAddresses (any scope) must pass binding', async () => {
|
|
507
|
-
// SEED_B is a delegated/controlled address with a non-manifest scope (e.g. 'read').
|
|
508
|
-
// Per spec, controlledAddresses (any scope) are valid manifest signers.
|
|
509
|
-
const rootAddr = deriveAddress(SEED_A, 0);
|
|
510
|
-
const controlledAddr = deriveAddress(SEED_B, 0);
|
|
511
|
-
|
|
512
|
-
// Create identity whose root is SEED_A's address
|
|
513
|
-
const doc = createIdentityDocument({
|
|
514
|
-
kind: 'service',
|
|
515
|
-
rootAddress: rootAddr,
|
|
516
|
-
controllerAddress: rootAddr,
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// Root (SEED_A) issues a delegation claim to SEED_B's address with a non-manifest scope
|
|
520
|
-
const delClaim = createDelegationClaim({
|
|
521
|
-
issuer: rootAddr,
|
|
522
|
-
subject: doc.id,
|
|
523
|
-
delegatedAddress: controlledAddr,
|
|
524
|
-
scopes: ['read'], // NOT manifest:sign — only 'read' scope
|
|
525
|
-
issuedAt: 1000,
|
|
526
|
-
});
|
|
527
|
-
const signedClaim = await signIdentityClaim(delClaim, SEED_A, 0);
|
|
528
|
-
|
|
529
|
-
// SEED_B (controlled address) signs the manifest
|
|
530
|
-
const signed = await makeEdgeServiceManifest(SEED_B, 0);
|
|
531
|
-
expect(signed.authorAddress).toBe(controlledAddr);
|
|
532
|
-
|
|
533
|
-
const graph: IdentityGraph = { document: doc, claims: [signedClaim] };
|
|
534
|
-
const binding = await bindManifestToIdentity(signed, graph);
|
|
535
|
-
|
|
536
|
-
// controlledAddr is in controlledAddresses → binding must succeed
|
|
537
|
-
expect(binding.valid).toBe(true);
|
|
538
|
-
expect(binding.signerAddress).toBe(controlledAddr);
|
|
539
|
-
}, 60000);
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
// ─── Guards ───────────────────────────────────────────────────────────────────
|
|
543
|
-
|
|
544
|
-
describe('type guards', () => {
|
|
545
|
-
it('isTotemIdentityDocument', () => {
|
|
546
|
-
const doc = createIdentityDocument({
|
|
547
|
-
kind: 'device',
|
|
548
|
-
rootAddress: 'MxA',
|
|
549
|
-
controllerAddress: 'MxA',
|
|
550
|
-
});
|
|
551
|
-
expect(isTotemIdentityDocument(doc)).toBe(true);
|
|
552
|
-
expect(isTotemIdentityDocument(null)).toBe(false);
|
|
553
|
-
expect(isTotemIdentityDocument({ id: 'x' })).toBe(false);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('isIdentityClaim', () => {
|
|
557
|
-
const c = createIdentityClaim({
|
|
558
|
-
type: 'delegates_to',
|
|
559
|
-
issuer: 'MxA',
|
|
560
|
-
subject: 'id:x',
|
|
561
|
-
object: 'MxB',
|
|
562
|
-
payload: {},
|
|
563
|
-
issuedAt: 1000,
|
|
564
|
-
});
|
|
565
|
-
expect(isIdentityClaim(c)).toBe(true);
|
|
566
|
-
expect(isIdentityClaim(null)).toBe(false);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it('isSignedIdentityClaim', async () => {
|
|
570
|
-
const c = createIdentityClaim({
|
|
571
|
-
type: 'delegates_to',
|
|
572
|
-
issuer: 'MxA',
|
|
573
|
-
subject: 'id:x',
|
|
574
|
-
object: 'MxB',
|
|
575
|
-
payload: {},
|
|
576
|
-
issuedAt: 1000,
|
|
577
|
-
});
|
|
578
|
-
const signed = await signIdentityClaim(c, SEED_A, 0);
|
|
579
|
-
expect(isSignedIdentityClaim(signed)).toBe(true);
|
|
580
|
-
expect(isSignedIdentityClaim({ claim: c })).toBe(false);
|
|
581
|
-
}, 30000);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// ─── Package root export check ────────────────────────────────────────────────
|
|
585
|
-
|
|
586
|
-
describe('package root export', () => {
|
|
587
|
-
it('exports IDENTITY_VERSION', () => {
|
|
588
|
-
expect(IDENTITY_VERSION).toBe(1);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('exports all required symbols', () => {
|
|
592
|
-
const exports = require('../index');
|
|
593
|
-
const required = [
|
|
594
|
-
'IDENTITY_VERSION',
|
|
595
|
-
'createIdentityDocument',
|
|
596
|
-
'computeIdentityId',
|
|
597
|
-
'createIdentityClaim',
|
|
598
|
-
'createDelegationClaim',
|
|
599
|
-
'createPaymentRecipientClaim',
|
|
600
|
-
'createServiceEndpointClaim',
|
|
601
|
-
'signIdentityClaim',
|
|
602
|
-
'verifyIdentityClaim',
|
|
603
|
-
'rotateIdentity',
|
|
604
|
-
'revokeIdentity',
|
|
605
|
-
'resolveIdentityGraph',
|
|
606
|
-
'bindManifestToIdentity',
|
|
607
|
-
'verifyManifestIdentity',
|
|
608
|
-
'isTotemIdentityDocument',
|
|
609
|
-
'isIdentityClaim',
|
|
610
|
-
'isSignedIdentityClaim',
|
|
611
|
-
'isRotationClaim',
|
|
612
|
-
'isRevocationClaim',
|
|
613
|
-
];
|
|
614
|
-
for (const sym of required) {
|
|
615
|
-
expect(exports[sym]).toBeDefined();
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
});
|
package/src/canonical.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plain hex encoder (no 0x prefix) for use in ID URIs and deterministic identifiers.
|
|
3
|
-
* bytesToHex from @totemsdk/core adds a 0x prefix — use this for URI-safe hex IDs.
|
|
4
|
-
*/
|
|
5
|
-
export function toHex(bytes: Uint8Array): string {
|
|
6
|
-
return Array.from(bytes)
|
|
7
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
8
|
-
.join('');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Shared canonical JSON helper.
|
|
13
|
-
* Recursively sorts object keys before serializing — used for hashing and signing.
|
|
14
|
-
* Never use bare JSON.stringify on objects passed to hash or sign operations.
|
|
15
|
-
*/
|
|
16
|
-
export function canonicalJson(value: unknown): string {
|
|
17
|
-
if (value === null || typeof value !== 'object') {
|
|
18
|
-
return JSON.stringify(value);
|
|
19
|
-
}
|
|
20
|
-
if (Array.isArray(value)) {
|
|
21
|
-
return '[' + value.map(canonicalJson).join(',') + ']';
|
|
22
|
-
}
|
|
23
|
-
const obj = value as Record<string, unknown>;
|
|
24
|
-
const keys = Object.keys(obj).sort();
|
|
25
|
-
const pairs = keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`);
|
|
26
|
-
return '{' + pairs.join(',') + '}';
|
|
27
|
-
}
|