@metalabel/dfos-protocol 0.4.0 → 0.6.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 +18 -5
- package/dist/chain/index.d.ts +107 -32
- package/dist/chain/index.js +17 -5
- package/dist/{chunk-GEVJ3SEV.js → chunk-QKHP7UVL.js} +251 -54
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -4
- package/package.json +2 -5
- package/CONTENT-MODEL.md +0 -148
- package/DID-METHOD.md +0 -325
- package/PROTOCOL.md +0 -1121
package/README.md
CHANGED
|
@@ -32,11 +32,11 @@ import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/mer
|
|
|
32
32
|
|
|
33
33
|
## Specifications
|
|
34
34
|
|
|
35
|
-
| Document
|
|
36
|
-
|
|
|
37
|
-
| [PROTOCOL.md](
|
|
38
|
-
| [DID-METHOD.md](
|
|
39
|
-
| [CONTENT-MODEL.md](
|
|
35
|
+
| Document | Description |
|
|
36
|
+
| ------------------------------------------------ | -------------------------------------------------------------- |
|
|
37
|
+
| [PROTOCOL.md](../../specs/PROTOCOL.md) | Core protocol — chains, signatures, verification, test vectors |
|
|
38
|
+
| [DID-METHOD.md](../../specs/DID-METHOD.md) | W3C DID method specification for `did:dfos` |
|
|
39
|
+
| [CONTENT-MODEL.md](../../specs/CONTENT-MODEL.md) | Standard content schemas (post, profile, manifest) |
|
|
40
40
|
|
|
41
41
|
## Examples
|
|
42
42
|
|
|
@@ -53,6 +53,19 @@ The `examples/` directory contains deterministic reference fixtures that can be
|
|
|
53
53
|
- `merkle-tree.json` — 5 content IDs → sorted tree → root, with inclusion proof
|
|
54
54
|
- `beacon.json` — signed merkle root announcement with witness countersignature
|
|
55
55
|
|
|
56
|
+
## Cross-Language Verification
|
|
57
|
+
|
|
58
|
+
The `verify/` directory contains independent verification suites that re-derive CIDs and verify signatures from the reference fixtures — proving protocol correctness across implementations:
|
|
59
|
+
|
|
60
|
+
| Language | Path | Status |
|
|
61
|
+
| -------- | ---------------- | ------- |
|
|
62
|
+
| Go | `verify/go/` | Passing |
|
|
63
|
+
| Python | `verify/python/` | Passing |
|
|
64
|
+
| Rust | `verify/rust/` | Passing |
|
|
65
|
+
| Swift | `verify/swift/` | Passing |
|
|
66
|
+
|
|
67
|
+
Each suite uses only its language's native Ed25519, dag-cbor, and multihash implementations — no shared code with the TypeScript reference.
|
|
68
|
+
|
|
56
69
|
## License
|
|
57
70
|
|
|
58
71
|
MIT
|
package/dist/chain/index.d.ts
CHANGED
|
@@ -113,6 +113,28 @@ declare const BeaconPayload: z.ZodObject<{
|
|
|
113
113
|
createdAt: z.ZodISODateTime;
|
|
114
114
|
}, z.core.$strict>;
|
|
115
115
|
type BeaconPayload = z.infer<typeof BeaconPayload>;
|
|
116
|
+
/** Max CBOR-encoded payload size for artifacts (bytes) — protocol constant */
|
|
117
|
+
declare const MAX_ARTIFACT_PAYLOAD_SIZE = 16384;
|
|
118
|
+
/** Artifact: standalone signed inline document, immutable, CID-addressable */
|
|
119
|
+
declare const ArtifactPayload: z.ZodObject<{
|
|
120
|
+
version: z.ZodLiteral<1>;
|
|
121
|
+
type: z.ZodLiteral<"artifact">;
|
|
122
|
+
did: z.ZodString;
|
|
123
|
+
content: z.ZodObject<{
|
|
124
|
+
$schema: z.ZodString;
|
|
125
|
+
}, z.core.$catchall<z.ZodUnknown>>;
|
|
126
|
+
createdAt: z.ZodISODateTime;
|
|
127
|
+
}, z.core.$strict>;
|
|
128
|
+
type ArtifactPayload = z.infer<typeof ArtifactPayload>;
|
|
129
|
+
/** Countersign: standalone witness attestation referencing a target operation by CID */
|
|
130
|
+
declare const CountersignPayload: z.ZodObject<{
|
|
131
|
+
version: z.ZodLiteral<1>;
|
|
132
|
+
type: z.ZodLiteral<"countersign">;
|
|
133
|
+
did: z.ZodString;
|
|
134
|
+
targetCID: z.ZodString;
|
|
135
|
+
createdAt: z.ZodISODateTime;
|
|
136
|
+
}, z.core.$strict>;
|
|
137
|
+
type CountersignPayload = z.infer<typeof CountersignPayload>;
|
|
116
138
|
|
|
117
139
|
/** Ed25519 public key multicodec value */
|
|
118
140
|
declare const ED25519_PUB_MULTICODEC = 237;
|
|
@@ -172,6 +194,33 @@ declare const verifyIdentityChain: (input: {
|
|
|
172
194
|
didPrefix: string;
|
|
173
195
|
log: string[];
|
|
174
196
|
}) => Promise<VerifiedIdentity>;
|
|
197
|
+
/**
|
|
198
|
+
* Verify a single new operation against already-verified identity state
|
|
199
|
+
*
|
|
200
|
+
* The caller guarantees that `currentState` was produced by a correct prior
|
|
201
|
+
* verification (full chain replay or a chain of trusted extensions from a
|
|
202
|
+
* verified genesis). This function performs one signature verification and one
|
|
203
|
+
* state transition — constant time regardless of chain length.
|
|
204
|
+
*
|
|
205
|
+
* Note: key-ID consistency across the full chain history is NOT checked here.
|
|
206
|
+
* That invariant is established during genesis verification and maintained by
|
|
207
|
+
* the protocol's key consistency rules. Periodic full re-verification can
|
|
208
|
+
* audit this property.
|
|
209
|
+
*/
|
|
210
|
+
declare const verifyIdentityExtensionFromTrustedState: (input: {
|
|
211
|
+
/** Previously verified identity state */
|
|
212
|
+
currentState: VerifiedIdentity;
|
|
213
|
+
/** CID of the most recent operation in the chain */
|
|
214
|
+
headCID: string;
|
|
215
|
+
/** createdAt timestamp of the most recent operation */
|
|
216
|
+
lastCreatedAt: string;
|
|
217
|
+
/** The new JWS operation to verify */
|
|
218
|
+
newOp: string;
|
|
219
|
+
}) => Promise<{
|
|
220
|
+
state: VerifiedIdentity;
|
|
221
|
+
operationCID: string;
|
|
222
|
+
createdAt: string;
|
|
223
|
+
}>;
|
|
175
224
|
|
|
176
225
|
interface VerifiedContentChain {
|
|
177
226
|
/** Content identifier — bare 22-char hash derived from genesis CID */
|
|
@@ -227,6 +276,29 @@ declare const verifyContentChain: (input: {
|
|
|
227
276
|
*/
|
|
228
277
|
enforceAuthorization?: boolean;
|
|
229
278
|
}) => Promise<VerifiedContentChain>;
|
|
279
|
+
/**
|
|
280
|
+
* Verify a single new content operation against already-verified chain state
|
|
281
|
+
*
|
|
282
|
+
* Same trust model as verifyIdentityExtensionFromTrustedState — the caller
|
|
283
|
+
* guarantees `currentState` was correctly verified. One signature verification,
|
|
284
|
+
* one key resolution, one state transition.
|
|
285
|
+
*/
|
|
286
|
+
declare const verifyContentExtensionFromTrustedState: (input: {
|
|
287
|
+
/** Previously verified content chain state */
|
|
288
|
+
currentState: VerifiedContentChain;
|
|
289
|
+
/** createdAt timestamp of the most recent operation */
|
|
290
|
+
lastCreatedAt: string;
|
|
291
|
+
/** The new JWS operation to verify */
|
|
292
|
+
newOp: string;
|
|
293
|
+
/** Resolve a kid (DID URL) to the raw Ed25519 public key bytes */
|
|
294
|
+
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
295
|
+
/** Enforce creator-sovereignty authorization (see verifyContentChain) */
|
|
296
|
+
enforceAuthorization?: boolean;
|
|
297
|
+
}) => Promise<{
|
|
298
|
+
state: VerifiedContentChain;
|
|
299
|
+
operationCID: string;
|
|
300
|
+
createdAt: string;
|
|
301
|
+
}>;
|
|
230
302
|
|
|
231
303
|
/**
|
|
232
304
|
* Sign a beacon announcement as a JWS
|
|
@@ -253,57 +325,60 @@ declare const verifyBeacon: (input: {
|
|
|
253
325
|
now?: number;
|
|
254
326
|
}) => Promise<VerifiedBeacon>;
|
|
255
327
|
|
|
328
|
+
interface VerifiedCountersignature {
|
|
329
|
+
/** CID of this countersign operation (distinct from the target) */
|
|
330
|
+
countersignCID: string;
|
|
331
|
+
/** The witness DID (payload.did — the DID that signed this attestation) */
|
|
332
|
+
witnessDID: string;
|
|
333
|
+
/** The CID being attested to */
|
|
334
|
+
targetCID: string;
|
|
335
|
+
}
|
|
256
336
|
/**
|
|
257
|
-
* Sign
|
|
258
|
-
*
|
|
259
|
-
* The witness signs the same payload as the author, producing a different
|
|
260
|
-
* JWS token with their own kid. The CID is identical because the payload
|
|
261
|
-
* is identical.
|
|
337
|
+
* Sign a countersignature attesting to a target operation by CID
|
|
262
338
|
*/
|
|
263
339
|
declare const signCountersignature: (input: {
|
|
264
|
-
|
|
265
|
-
operationPayload: ContentOperation;
|
|
266
|
-
/** Witness signer */
|
|
340
|
+
payload: CountersignPayload;
|
|
267
341
|
signer: Signer;
|
|
268
|
-
/** Witness kid — DID URL of the witness (must differ from payload.did) */
|
|
269
342
|
kid: string;
|
|
270
343
|
}) => Promise<{
|
|
271
344
|
jwsToken: string;
|
|
272
|
-
|
|
345
|
+
countersignCID: string;
|
|
273
346
|
}>;
|
|
274
|
-
interface VerifiedCountersignature {
|
|
275
|
-
operationCID: string;
|
|
276
|
-
/** The DID that authored the operation (payload.did) */
|
|
277
|
-
authorDID: string;
|
|
278
|
-
/** The DID that witnessed the operation (kid DID) */
|
|
279
|
-
witnessDID: string;
|
|
280
|
-
}
|
|
281
347
|
/**
|
|
282
|
-
* Verify a countersignature JWS
|
|
348
|
+
* Verify a countersignature JWS — stateless verification
|
|
283
349
|
*
|
|
284
|
-
* Checks: valid signature, CID
|
|
350
|
+
* Checks: valid signature, CID integrity, payload schema. Does NOT check
|
|
351
|
+
* whether the target exists or whether the witness differs from the target
|
|
352
|
+
* author — those are relay-level semantic checks.
|
|
285
353
|
*/
|
|
286
354
|
declare const verifyCountersignature: (input: {
|
|
287
355
|
jwsToken: string;
|
|
288
|
-
expectedCID: string;
|
|
289
356
|
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
290
357
|
}) => Promise<VerifiedCountersignature>;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
/** The DID that witnessed the beacon (kid DID) */
|
|
296
|
-
witnessDID: string;
|
|
358
|
+
|
|
359
|
+
interface VerifiedArtifact {
|
|
360
|
+
payload: ArtifactPayload;
|
|
361
|
+
artifactCID: string;
|
|
297
362
|
}
|
|
298
363
|
/**
|
|
299
|
-
*
|
|
364
|
+
* Sign an artifact as a JWS
|
|
300
365
|
*
|
|
301
|
-
*
|
|
366
|
+
* Enforces the protocol size limit on the CBOR-encoded payload.
|
|
367
|
+
*/
|
|
368
|
+
declare const signArtifact: (input: {
|
|
369
|
+
payload: ArtifactPayload;
|
|
370
|
+
signer: Signer;
|
|
371
|
+
kid: string;
|
|
372
|
+
}) => Promise<{
|
|
373
|
+
jwsToken: string;
|
|
374
|
+
artifactCID: string;
|
|
375
|
+
}>;
|
|
376
|
+
/**
|
|
377
|
+
* Verify an artifact JWS — signature, CID, payload schema, size limit
|
|
302
378
|
*/
|
|
303
|
-
declare const
|
|
379
|
+
declare const verifyArtifact: (input: {
|
|
304
380
|
jwsToken: string;
|
|
305
|
-
expectedCID: string;
|
|
306
381
|
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
307
|
-
}) => Promise<
|
|
382
|
+
}) => Promise<VerifiedArtifact>;
|
|
308
383
|
|
|
309
|
-
export { BeaconPayload, ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, type Signer, type
|
|
384
|
+
export { ArtifactPayload, BeaconPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MAX_ARTIFACT_PAYLOAD_SIZE, MultikeyPublicKey, type Signer, type VerifiedArtifact, type VerifiedBeacon, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signArtifact, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyArtifact, verifyBeacon, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState };
|
package/dist/chain/index.js
CHANGED
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArtifactPayload,
|
|
2
3
|
BeaconPayload,
|
|
3
4
|
ContentOperation,
|
|
5
|
+
CountersignPayload,
|
|
4
6
|
ED25519_PRIV_MULTICODEC,
|
|
5
7
|
ED25519_PUB_MULTICODEC,
|
|
6
8
|
IdentityOperation,
|
|
9
|
+
MAX_ARTIFACT_PAYLOAD_SIZE,
|
|
7
10
|
MultikeyPublicKey,
|
|
8
11
|
VerifiedIdentity,
|
|
9
12
|
decodeMultikey,
|
|
10
13
|
deriveChainIdentifier,
|
|
11
14
|
deriveContentId,
|
|
12
15
|
encodeEd25519Multikey,
|
|
16
|
+
signArtifact,
|
|
13
17
|
signBeacon,
|
|
14
18
|
signContentOperation,
|
|
15
19
|
signCountersignature,
|
|
16
20
|
signIdentityOperation,
|
|
21
|
+
verifyArtifact,
|
|
17
22
|
verifyBeacon,
|
|
18
|
-
verifyBeaconCountersignature,
|
|
19
23
|
verifyContentChain,
|
|
24
|
+
verifyContentExtensionFromTrustedState,
|
|
20
25
|
verifyCountersignature,
|
|
21
|
-
verifyIdentityChain
|
|
22
|
-
|
|
26
|
+
verifyIdentityChain,
|
|
27
|
+
verifyIdentityExtensionFromTrustedState
|
|
28
|
+
} from "../chunk-QKHP7UVL.js";
|
|
23
29
|
import "../chunk-CZSEEZLL.js";
|
|
24
30
|
import "../chunk-ZXXP5W5N.js";
|
|
25
31
|
export {
|
|
32
|
+
ArtifactPayload,
|
|
26
33
|
BeaconPayload,
|
|
27
34
|
ContentOperation,
|
|
35
|
+
CountersignPayload,
|
|
28
36
|
ED25519_PRIV_MULTICODEC,
|
|
29
37
|
ED25519_PUB_MULTICODEC,
|
|
30
38
|
IdentityOperation,
|
|
39
|
+
MAX_ARTIFACT_PAYLOAD_SIZE,
|
|
31
40
|
MultikeyPublicKey,
|
|
32
41
|
VerifiedIdentity,
|
|
33
42
|
decodeMultikey,
|
|
34
43
|
deriveChainIdentifier,
|
|
35
44
|
deriveContentId,
|
|
36
45
|
encodeEd25519Multikey,
|
|
46
|
+
signArtifact,
|
|
37
47
|
signBeacon,
|
|
38
48
|
signContentOperation,
|
|
39
49
|
signCountersignature,
|
|
40
50
|
signIdentityOperation,
|
|
51
|
+
verifyArtifact,
|
|
41
52
|
verifyBeacon,
|
|
42
|
-
verifyBeaconCountersignature,
|
|
43
53
|
verifyContentChain,
|
|
54
|
+
verifyContentExtensionFromTrustedState,
|
|
44
55
|
verifyCountersignature,
|
|
45
|
-
verifyIdentityChain
|
|
56
|
+
verifyIdentityChain,
|
|
57
|
+
verifyIdentityExtensionFromTrustedState
|
|
46
58
|
};
|
|
@@ -104,6 +104,23 @@ var BeaconPayload = z.strictObject({
|
|
|
104
104
|
merkleRoot: z.string().regex(/^[0-9a-f]{64}$/),
|
|
105
105
|
createdAt: Iso8601
|
|
106
106
|
});
|
|
107
|
+
var MAX_SCHEMA = 256;
|
|
108
|
+
var MAX_ARTIFACT_PAYLOAD_SIZE = 16384;
|
|
109
|
+
var ArtifactContent = z.object({ $schema: z.string().max(MAX_SCHEMA) }).catchall(z.unknown());
|
|
110
|
+
var ArtifactPayload = z.strictObject({
|
|
111
|
+
version: z.literal(1),
|
|
112
|
+
type: z.literal("artifact"),
|
|
113
|
+
did: z.string().max(MAX_DID),
|
|
114
|
+
content: ArtifactContent,
|
|
115
|
+
createdAt: Iso8601
|
|
116
|
+
});
|
|
117
|
+
var CountersignPayload = z.strictObject({
|
|
118
|
+
version: z.literal(1),
|
|
119
|
+
type: z.literal("countersign"),
|
|
120
|
+
did: z.string().max(MAX_DID),
|
|
121
|
+
targetCID: CIDString,
|
|
122
|
+
createdAt: Iso8601
|
|
123
|
+
});
|
|
107
124
|
|
|
108
125
|
// src/chain/multikey.ts
|
|
109
126
|
import { base58btc } from "multiformats/bases/base58";
|
|
@@ -299,6 +316,78 @@ var verifyIdentityChain = async (input) => {
|
|
|
299
316
|
controllerKeys: state.controllerKeys
|
|
300
317
|
};
|
|
301
318
|
};
|
|
319
|
+
var verifyIdentityExtensionFromTrustedState = async (input) => {
|
|
320
|
+
const { currentState, headCID, lastCreatedAt, newOp } = input;
|
|
321
|
+
if (currentState.isDeleted) {
|
|
322
|
+
throw new Error("cannot extend a deleted identity");
|
|
323
|
+
}
|
|
324
|
+
const decoded = decodeJwsUnsafe(newOp);
|
|
325
|
+
if (!decoded) throw new Error("failed to decode JWS");
|
|
326
|
+
const result = IdentityOperation.safeParse(decoded.payload);
|
|
327
|
+
if (!result.success) {
|
|
328
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
329
|
+
throw new Error(messages);
|
|
330
|
+
}
|
|
331
|
+
const op = result.data;
|
|
332
|
+
if (decoded.header.typ !== "did:dfos:identity-op") {
|
|
333
|
+
throw new Error(`invalid typ: ${decoded.header.typ}`);
|
|
334
|
+
}
|
|
335
|
+
if (op.type === "create") {
|
|
336
|
+
throw new Error("extension cannot be a create operation");
|
|
337
|
+
}
|
|
338
|
+
if (op.previousOperationCID !== headCID) {
|
|
339
|
+
throw new Error("previousCID is incorrect");
|
|
340
|
+
}
|
|
341
|
+
if (op.createdAt <= lastCreatedAt) {
|
|
342
|
+
throw new Error("createdAt must be after last op");
|
|
343
|
+
}
|
|
344
|
+
const encoded = await dagCborCanonicalEncode(op);
|
|
345
|
+
const operationCID = encoded.cid.toString();
|
|
346
|
+
if (!decoded.header.cid) throw new Error("missing cid in protected header");
|
|
347
|
+
if (decoded.header.cid !== operationCID) throw new Error("cid mismatch in protected header");
|
|
348
|
+
const kid = decoded.header.kid;
|
|
349
|
+
if (!kid.includes("#")) {
|
|
350
|
+
throw new Error("non-genesis op kid must be DID URL, got bare key ID");
|
|
351
|
+
}
|
|
352
|
+
const hashIdx = kid.indexOf("#");
|
|
353
|
+
const signingKeyId = kid.substring(hashIdx + 1);
|
|
354
|
+
const kidDid = kid.substring(0, hashIdx);
|
|
355
|
+
if (kidDid !== currentState.did) {
|
|
356
|
+
throw new Error("kid DID does not match identity DID");
|
|
357
|
+
}
|
|
358
|
+
const signingKey = currentState.controllerKeys.find((k) => k.id === signingKeyId);
|
|
359
|
+
if (!signingKey) {
|
|
360
|
+
throw new Error(`kid references unknown key: ${signingKeyId}`);
|
|
361
|
+
}
|
|
362
|
+
const { keyBytes } = decodeMultikey(signingKey.publicKeyMultibase);
|
|
363
|
+
try {
|
|
364
|
+
verifyJws({ token: newOp, publicKey: keyBytes });
|
|
365
|
+
} catch {
|
|
366
|
+
throw new Error("invalid signature");
|
|
367
|
+
}
|
|
368
|
+
if (op.type === "update") {
|
|
369
|
+
[op.authKeys, op.assertKeys, op.controllerKeys].forEach((keys) => {
|
|
370
|
+
const set = new Set(keys.map((k) => k.id));
|
|
371
|
+
if (set.size !== keys.length) {
|
|
372
|
+
throw new Error("cannot repeat key ids in same usage");
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const newState = op.type === "update" ? {
|
|
377
|
+
did: currentState.did,
|
|
378
|
+
isDeleted: false,
|
|
379
|
+
authKeys: op.authKeys,
|
|
380
|
+
assertKeys: op.assertKeys,
|
|
381
|
+
controllerKeys: op.controllerKeys
|
|
382
|
+
} : {
|
|
383
|
+
did: currentState.did,
|
|
384
|
+
isDeleted: true,
|
|
385
|
+
authKeys: currentState.authKeys,
|
|
386
|
+
assertKeys: currentState.assertKeys,
|
|
387
|
+
controllerKeys: currentState.controllerKeys
|
|
388
|
+
};
|
|
389
|
+
return { state: newState, operationCID, createdAt: op.createdAt };
|
|
390
|
+
};
|
|
302
391
|
|
|
303
392
|
// src/chain/content-chain.ts
|
|
304
393
|
var signContentOperation = async (input) => {
|
|
@@ -447,6 +536,98 @@ var verifyContentChain = async (input) => {
|
|
|
447
536
|
creatorDID: state.creatorDID
|
|
448
537
|
};
|
|
449
538
|
};
|
|
539
|
+
var verifyContentExtensionFromTrustedState = async (input) => {
|
|
540
|
+
const { currentState, lastCreatedAt, newOp, resolveKey } = input;
|
|
541
|
+
if (currentState.isDeleted) {
|
|
542
|
+
throw new Error("cannot extend a deleted chain");
|
|
543
|
+
}
|
|
544
|
+
const decoded = decodeJwsUnsafe(newOp);
|
|
545
|
+
if (!decoded) throw new Error("failed to decode JWS");
|
|
546
|
+
const result = ContentOperation.safeParse(decoded.payload);
|
|
547
|
+
if (!result.success) {
|
|
548
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
549
|
+
throw new Error(messages);
|
|
550
|
+
}
|
|
551
|
+
const op = result.data;
|
|
552
|
+
if (decoded.header.typ !== "did:dfos:content-op") {
|
|
553
|
+
throw new Error(`invalid typ: ${decoded.header.typ}`);
|
|
554
|
+
}
|
|
555
|
+
if (op.type === "create") {
|
|
556
|
+
throw new Error("extension cannot be a create operation");
|
|
557
|
+
}
|
|
558
|
+
if (op.previousOperationCID !== currentState.headCID) {
|
|
559
|
+
throw new Error("previousOperationCID is incorrect");
|
|
560
|
+
}
|
|
561
|
+
if (op.createdAt <= lastCreatedAt) {
|
|
562
|
+
throw new Error("createdAt must be after last op");
|
|
563
|
+
}
|
|
564
|
+
const kid = decoded.header.kid;
|
|
565
|
+
const hashIdx = kid.indexOf("#");
|
|
566
|
+
if (hashIdx < 0) throw new Error("kid must be a DID URL");
|
|
567
|
+
const kidDid = kid.substring(0, hashIdx);
|
|
568
|
+
if (kidDid !== op.did) {
|
|
569
|
+
throw new Error("kid DID does not match operation did");
|
|
570
|
+
}
|
|
571
|
+
const publicKey = await resolveKey(kid);
|
|
572
|
+
try {
|
|
573
|
+
verifyJws({ token: newOp, publicKey });
|
|
574
|
+
} catch {
|
|
575
|
+
throw new Error("invalid signature");
|
|
576
|
+
}
|
|
577
|
+
if (op.did !== currentState.creatorDID && input.enforceAuthorization) {
|
|
578
|
+
const authorization = op.authorization;
|
|
579
|
+
if (!authorization) {
|
|
580
|
+
throw new Error(`signer ${op.did} is not the chain creator \u2014 authorization VC required`);
|
|
581
|
+
}
|
|
582
|
+
const vcDecoded = decodeCredentialUnsafe(authorization);
|
|
583
|
+
if (!vcDecoded) throw new Error("failed to decode authorization VC");
|
|
584
|
+
const vcKid = vcDecoded.header.kid;
|
|
585
|
+
if (!vcKid || !vcKid.includes("#")) {
|
|
586
|
+
throw new Error("authorization VC kid must be a DID URL");
|
|
587
|
+
}
|
|
588
|
+
let creatorPublicKey;
|
|
589
|
+
try {
|
|
590
|
+
creatorPublicKey = await resolveKey(vcKid);
|
|
591
|
+
} catch {
|
|
592
|
+
throw new Error("cannot resolve creator key for authorization verification");
|
|
593
|
+
}
|
|
594
|
+
const opCreatedAtUnix = Math.floor(new Date(op.createdAt).getTime() / 1e3);
|
|
595
|
+
try {
|
|
596
|
+
const credential = verifyCredential({
|
|
597
|
+
token: authorization,
|
|
598
|
+
publicKey: creatorPublicKey,
|
|
599
|
+
subject: op.did,
|
|
600
|
+
expectedType: VC_TYPE_CONTENT_WRITE,
|
|
601
|
+
currentTime: opCreatedAtUnix
|
|
602
|
+
});
|
|
603
|
+
if (credential.iss !== currentState.creatorDID) {
|
|
604
|
+
throw new Error("VC issuer is not the chain creator");
|
|
605
|
+
}
|
|
606
|
+
if (credential.contentId && credential.contentId !== currentState.contentId) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`VC contentId ${credential.contentId} does not match chain ${currentState.contentId}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
613
|
+
throw new Error(`authorization verification failed: ${message}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const encoded = await dagCborCanonicalEncode(op);
|
|
617
|
+
const operationCID = encoded.cid.toString();
|
|
618
|
+
if (!decoded.header.cid) throw new Error("missing cid in protected header");
|
|
619
|
+
if (decoded.header.cid !== operationCID) throw new Error("cid mismatch in protected header");
|
|
620
|
+
const newState = {
|
|
621
|
+
contentId: currentState.contentId,
|
|
622
|
+
genesisCID: currentState.genesisCID,
|
|
623
|
+
headCID: operationCID,
|
|
624
|
+
isDeleted: op.type === "delete",
|
|
625
|
+
currentDocumentCID: op.type === "update" ? op.documentCID : null,
|
|
626
|
+
length: currentState.length + 1,
|
|
627
|
+
creatorDID: currentState.creatorDID
|
|
628
|
+
};
|
|
629
|
+
return { state: newState, operationCID, createdAt: op.createdAt };
|
|
630
|
+
};
|
|
450
631
|
|
|
451
632
|
// src/chain/beacon.ts
|
|
452
633
|
var signBeacon = async (input) => {
|
|
@@ -499,92 +680,102 @@ var verifyBeacon = async (input) => {
|
|
|
499
680
|
|
|
500
681
|
// src/chain/countersign.ts
|
|
501
682
|
var signCountersignature = async (input) => {
|
|
502
|
-
const encoded = await dagCborCanonicalEncode(input.
|
|
503
|
-
const
|
|
683
|
+
const encoded = await dagCborCanonicalEncode(input.payload);
|
|
684
|
+
const countersignCID = encoded.cid.toString();
|
|
504
685
|
const jwsToken = await createJws({
|
|
505
|
-
header: { alg: "EdDSA", typ: "did:dfos:
|
|
506
|
-
payload: input.
|
|
686
|
+
header: { alg: "EdDSA", typ: "did:dfos:countersign", kid: input.kid, cid: countersignCID },
|
|
687
|
+
payload: input.payload,
|
|
507
688
|
sign: input.signer
|
|
508
689
|
});
|
|
509
|
-
return { jwsToken,
|
|
690
|
+
return { jwsToken, countersignCID };
|
|
510
691
|
};
|
|
511
692
|
var verifyCountersignature = async (input) => {
|
|
512
693
|
const decoded = decodeJwsUnsafe(input.jwsToken);
|
|
513
694
|
if (!decoded) throw new Error("failed to decode countersignature JWS");
|
|
514
|
-
if (decoded.header.typ !== "did:dfos:
|
|
695
|
+
if (decoded.header.typ !== "did:dfos:countersign") {
|
|
515
696
|
throw new Error(`invalid countersignature typ: ${decoded.header.typ}`);
|
|
516
697
|
}
|
|
517
|
-
const result =
|
|
698
|
+
const result = CountersignPayload.safeParse(decoded.payload);
|
|
518
699
|
if (!result.success) {
|
|
519
700
|
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
520
|
-
throw new Error(`invalid
|
|
521
|
-
}
|
|
522
|
-
const op = result.data;
|
|
523
|
-
const encoded = await dagCborCanonicalEncode(op);
|
|
524
|
-
const operationCID = encoded.cid.toString();
|
|
525
|
-
if (operationCID !== input.expectedCID) {
|
|
526
|
-
throw new Error("countersignature CID does not match expected CID");
|
|
527
|
-
}
|
|
528
|
-
if (decoded.header.cid !== operationCID) {
|
|
529
|
-
throw new Error("countersignature header cid mismatch");
|
|
701
|
+
throw new Error(`invalid countersignature payload: ${messages}`);
|
|
530
702
|
}
|
|
703
|
+
const payload = result.data;
|
|
531
704
|
const kid = decoded.header.kid;
|
|
705
|
+
const hashIdx = kid.indexOf("#");
|
|
706
|
+
if (hashIdx < 0) throw new Error("countersignature kid must be a DID URL");
|
|
707
|
+
const kidDid = kid.substring(0, hashIdx);
|
|
708
|
+
if (kidDid !== payload.did) {
|
|
709
|
+
throw new Error("countersignature kid DID does not match payload did");
|
|
710
|
+
}
|
|
532
711
|
const publicKey = await input.resolveKey(kid);
|
|
533
712
|
try {
|
|
534
713
|
verifyJws({ token: input.jwsToken, publicKey });
|
|
535
714
|
} catch {
|
|
536
|
-
throw new Error("invalid countersignature");
|
|
537
|
-
}
|
|
538
|
-
const hashIdx = kid.indexOf("#");
|
|
539
|
-
if (hashIdx < 0) throw new Error("countersignature kid must be a DID URL");
|
|
540
|
-
const witnessDID = kid.substring(0, hashIdx);
|
|
541
|
-
if (witnessDID === op.did) {
|
|
542
|
-
throw new Error("countersignature kid DID must differ from operation did (not a witness)");
|
|
715
|
+
throw new Error("invalid countersignature signature");
|
|
543
716
|
}
|
|
717
|
+
const encoded = await dagCborCanonicalEncode(decoded.payload);
|
|
718
|
+
const countersignCID = encoded.cid.toString();
|
|
719
|
+
if (!decoded.header.cid) throw new Error("missing cid in countersignature header");
|
|
720
|
+
if (decoded.header.cid !== countersignCID) throw new Error("countersignature cid mismatch");
|
|
544
721
|
return {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
722
|
+
countersignCID,
|
|
723
|
+
witnessDID: payload.did,
|
|
724
|
+
targetCID: payload.targetCID
|
|
548
725
|
};
|
|
549
726
|
};
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
727
|
+
|
|
728
|
+
// src/chain/artifact.ts
|
|
729
|
+
var signArtifact = async (input) => {
|
|
730
|
+
const encoded = await dagCborCanonicalEncode(input.payload);
|
|
731
|
+
const artifactCID = encoded.cid.toString();
|
|
732
|
+
if (encoded.bytes.length > MAX_ARTIFACT_PAYLOAD_SIZE) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`artifact payload exceeds max size: ${encoded.bytes.length} > ${MAX_ARTIFACT_PAYLOAD_SIZE}`
|
|
735
|
+
);
|
|
555
736
|
}
|
|
556
|
-
const
|
|
737
|
+
const jwsToken = await createJws({
|
|
738
|
+
header: { alg: "EdDSA", typ: "did:dfos:artifact", kid: input.kid, cid: artifactCID },
|
|
739
|
+
payload: input.payload,
|
|
740
|
+
sign: input.signer
|
|
741
|
+
});
|
|
742
|
+
return { jwsToken, artifactCID };
|
|
743
|
+
};
|
|
744
|
+
var verifyArtifact = async (input) => {
|
|
745
|
+
const decoded = decodeJwsUnsafe(input.jwsToken);
|
|
746
|
+
if (!decoded) throw new Error("failed to decode artifact JWS");
|
|
747
|
+
const result = ArtifactPayload.safeParse(decoded.payload);
|
|
557
748
|
if (!result.success) {
|
|
558
749
|
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
559
|
-
throw new Error(`invalid
|
|
750
|
+
throw new Error(`invalid artifact payload: ${messages}`);
|
|
560
751
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (beaconCID !== input.expectedCID) {
|
|
565
|
-
throw new Error("beacon countersignature CID does not match expected CID");
|
|
566
|
-
}
|
|
567
|
-
if (decoded.header.cid !== beaconCID) {
|
|
568
|
-
throw new Error("beacon countersignature header cid mismatch");
|
|
752
|
+
const payload = result.data;
|
|
753
|
+
if (decoded.header.typ !== "did:dfos:artifact") {
|
|
754
|
+
throw new Error(`invalid artifact typ: ${decoded.header.typ}`);
|
|
569
755
|
}
|
|
570
756
|
const kid = decoded.header.kid;
|
|
757
|
+
const hashIdx = kid.indexOf("#");
|
|
758
|
+
if (hashIdx < 0) throw new Error("artifact kid must be a DID URL");
|
|
759
|
+
const kidDid = kid.substring(0, hashIdx);
|
|
760
|
+
if (kidDid !== payload.did) {
|
|
761
|
+
throw new Error("artifact kid DID does not match payload did");
|
|
762
|
+
}
|
|
571
763
|
const publicKey = await input.resolveKey(kid);
|
|
572
764
|
try {
|
|
573
765
|
verifyJws({ token: input.jwsToken, publicKey });
|
|
574
766
|
} catch {
|
|
575
|
-
throw new Error("invalid
|
|
767
|
+
throw new Error("invalid artifact signature");
|
|
576
768
|
}
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
769
|
+
const encoded = await dagCborCanonicalEncode(decoded.payload);
|
|
770
|
+
const artifactCID = encoded.cid.toString();
|
|
771
|
+
if (!decoded.header.cid) throw new Error("missing cid in artifact header");
|
|
772
|
+
if (decoded.header.cid !== artifactCID) throw new Error("artifact cid mismatch");
|
|
773
|
+
if (encoded.bytes.length > MAX_ARTIFACT_PAYLOAD_SIZE) {
|
|
774
|
+
throw new Error(
|
|
775
|
+
`artifact payload exceeds max size: ${encoded.bytes.length} > ${MAX_ARTIFACT_PAYLOAD_SIZE}`
|
|
776
|
+
);
|
|
582
777
|
}
|
|
583
|
-
return {
|
|
584
|
-
beaconCID,
|
|
585
|
-
controllerDID: beacon.did,
|
|
586
|
-
witnessDID
|
|
587
|
-
};
|
|
778
|
+
return { payload, artifactCID };
|
|
588
779
|
};
|
|
589
780
|
|
|
590
781
|
export {
|
|
@@ -593,6 +784,9 @@ export {
|
|
|
593
784
|
VerifiedIdentity,
|
|
594
785
|
ContentOperation,
|
|
595
786
|
BeaconPayload,
|
|
787
|
+
MAX_ARTIFACT_PAYLOAD_SIZE,
|
|
788
|
+
ArtifactPayload,
|
|
789
|
+
CountersignPayload,
|
|
596
790
|
ED25519_PUB_MULTICODEC,
|
|
597
791
|
ED25519_PRIV_MULTICODEC,
|
|
598
792
|
encodeEd25519Multikey,
|
|
@@ -601,11 +795,14 @@ export {
|
|
|
601
795
|
deriveContentId,
|
|
602
796
|
signIdentityOperation,
|
|
603
797
|
verifyIdentityChain,
|
|
798
|
+
verifyIdentityExtensionFromTrustedState,
|
|
604
799
|
signContentOperation,
|
|
605
800
|
verifyContentChain,
|
|
801
|
+
verifyContentExtensionFromTrustedState,
|
|
606
802
|
signBeacon,
|
|
607
803
|
verifyBeacon,
|
|
608
804
|
signCountersignature,
|
|
609
805
|
verifyCountersignature,
|
|
610
|
-
|
|
806
|
+
signArtifact,
|
|
807
|
+
verifyArtifact
|
|
611
808
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { JwsHeader, JwsVerificationError, JwtClaims, JwtCreateOptions, JwtHeader, JwtVerificationError, JwtVerifyOptions, PrefixedID, base64urlDecode, base64urlEncode, createJws, createJwt, createNewEd25519Keypair, dagCborCanonicalEncode, decodeJwsUnsafe, decodeJwtUnsafe, generateId, generateIdNoPrefix, importEd25519Keypair, isCanonicallyEqual, isValidEd25519Signature, isValidId, normalizedId, parseDagCborCID, signPayloadEd25519, verifyJws, verifyJwt } from './crypto/index.js';
|
|
2
|
-
export { BeaconPayload, ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, Signer,
|
|
2
|
+
export { ArtifactPayload, BeaconPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MAX_ARTIFACT_PAYLOAD_SIZE, MultikeyPublicKey, Signer, VerifiedArtifact, VerifiedBeacon, VerifiedContentChain, VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signArtifact, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyArtifact, verifyBeacon, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState } from './chain/index.js';
|
|
3
3
|
export { MerkleProof, buildMerkleTree, generateMerkleProof, hashLeaf, hexToBytes, verifyMerkleProof } from './merkle/index.js';
|
|
4
4
|
export { AuthTokenClaims, AuthTokenCreateOptions, AuthTokenVerificationError, AuthTokenVerifyOptions, ContentReadSubject, ContentWriteSubject, CredentialClaims, CredentialCreateOptions, CredentialVerificationError, CredentialVerifyOptions, DFOSCredentialType, VCClaim, VC_TYPE_CONTENT_READ, VC_TYPE_CONTENT_WRITE, VerifiedAuthToken, VerifiedCredential, createAuthToken, createCredential, decodeCredentialUnsafe, verifyAuthToken, verifyCredential } from './credentials/index.js';
|
|
5
5
|
import 'multiformats';
|