@metalabel/dfos-protocol 0.1.0 → 0.3.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/PROTOCOL.md +259 -27
- package/README.md +14 -6
- package/dist/chain/index.d.ts +22 -1
- package/dist/chain/index.js +2 -1
- package/dist/chunk-CZSEEZLL.js +258 -0
- package/dist/{chunk-ASGEXSVT.js → chunk-GEVJ3SEV.js} +60 -4
- package/dist/credentials/index.d.ts +206 -0
- package/dist/credentials/index.js +35 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +40 -8
- package/examples/beacon.json +14 -0
- package/examples/content-delegated.json +44 -0
- package/examples/credential-read.json +12 -0
- package/examples/credential-write.json +14 -0
- package/examples/merkle-tree.json +28 -0
- package/package.json +5 -1
package/PROTOCOL.md
CHANGED
|
@@ -20,11 +20,12 @@ The protocol is not coupled to the DFOS platform. Any system implementing the sa
|
|
|
20
20
|
|
|
21
21
|
## Protocol Overview
|
|
22
22
|
|
|
23
|
-
The DFOS protocol has
|
|
23
|
+
The DFOS protocol has five components:
|
|
24
24
|
|
|
25
25
|
| Component | Concern |
|
|
26
26
|
| --------------------- | ---------------------------------------------------------------------------- |
|
|
27
27
|
| **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
|
|
28
|
+
| **Credentials** | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
|
|
28
29
|
| **Beacons** | Signed merkle root announcements — periodic commitment over content sets |
|
|
29
30
|
| **Countersignatures** | Witness attestation — third-party signatures over existing chain operations |
|
|
30
31
|
| **Merkle trees** | SHA-256 binary trees over content IDs — inclusion proofs for beacon roots |
|
|
@@ -92,7 +93,7 @@ This is a self-sovereign invariant: the identity chain defines its own valid sig
|
|
|
92
93
|
|
|
93
94
|
Content chain verification requires a **valid EdDSA signature** and delegates key resolution to the caller. The `kid` in each operation's JWS header is a DID URL (`did:dfos:<id>#<keyId>`). The verifier calls `resolveKey(kid)` to obtain the raw Ed25519 public key bytes for that key on that identity. How the resolver obtains and validates the identity's key state is application-defined.
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
**Creator sovereignty**: The DID that signs the genesis (create) operation is the **chain creator** and permanently owns the chain. The creator can sign subsequent operations directly — no credential needed. Other DIDs require a **DFOSContentWrite** VC-JWT credential in the operation's `authorization` field, issued by the creator DID. See [Credentials](#credentials) for the VC-JWT format.
|
|
96
97
|
|
|
97
98
|
**Signer-payload consistency**: The `kid` DID in the JWS header MUST match the `did` field in the content operation payload. This enables discrimination between author operations and countersignatures — if the kid DID differs from the payload `did`, it is a countersignature (witness attestation), not a chain operation.
|
|
98
99
|
|
|
@@ -101,13 +102,12 @@ Identity chains are self-sovereign — they define their own valid signers via `
|
|
|
101
102
|
- The EdDSA signature on each operation is valid against the key returned by `resolveKey(kid)`
|
|
102
103
|
- Chain integrity (CID links, timestamp ordering, terminal state)
|
|
103
104
|
- The `kid` DID matches the payload `did` for chain operations
|
|
105
|
+
- Creator-sovereignty authorization (when `enforceAuthorization` is enabled): non-creator signers must present a valid DFOSContentWrite VC-JWT issued by the creator
|
|
104
106
|
|
|
105
107
|
**What the protocol does NOT enforce (application concerns):**
|
|
106
108
|
|
|
107
|
-
- Which identities are authorized to sign operations on a given chain
|
|
108
109
|
- Which key role (auth, assert, controller) the signing key must have
|
|
109
|
-
-
|
|
110
|
-
- Ownership or attribution semantics between signers and content chains
|
|
110
|
+
- Ownership or attribution semantics beyond creator sovereignty
|
|
111
111
|
|
|
112
112
|
### Terminal States and Special Operations
|
|
113
113
|
|
|
@@ -121,14 +121,15 @@ Identity chains are self-sovereign — they define their own valid signers via `
|
|
|
121
121
|
|
|
122
122
|
The JWS `typ` header uses protocol-specific values (not IANA media types):
|
|
123
123
|
|
|
124
|
-
| `typ` value | Usage
|
|
125
|
-
| ---------------------- |
|
|
126
|
-
| `did:dfos:identity-op` | Identity chain operations
|
|
127
|
-
| `did:dfos:content-op` | Content chain operations
|
|
128
|
-
| `did:dfos:beacon` | Beacon announcements
|
|
129
|
-
| `JWT` |
|
|
124
|
+
| `typ` value | Usage |
|
|
125
|
+
| ---------------------- | --------------------------------------------- |
|
|
126
|
+
| `did:dfos:identity-op` | Identity chain operations |
|
|
127
|
+
| `did:dfos:content-op` | Content chain operations |
|
|
128
|
+
| `did:dfos:beacon` | Beacon announcements |
|
|
129
|
+
| `JWT` | Auth tokens (DID-signed relay authentication) |
|
|
130
|
+
| `vc+jwt` | VC-JWT credentials (W3C VC Data Model v2) |
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
Protocol-specific `typ` values are non-standard per JOSE convention, documented intentionally. `JWT` and `vc+jwt` follow IANA conventions. The `typ` header aids routing but is not security-critical. Implementations SHOULD validate it but MUST NOT rely on it for security decisions.
|
|
132
133
|
|
|
133
134
|
### Operation Field Limits
|
|
134
135
|
|
|
@@ -303,14 +304,16 @@ DID: did:dfos:e3vvtck42d4eacdnzvtrn6
|
|
|
303
304
|
documentCID: string | null,
|
|
304
305
|
baseDocumentCID: string | null,
|
|
305
306
|
createdAt: string,
|
|
306
|
-
note: string | null
|
|
307
|
+
note: string | null,
|
|
308
|
+
authorization?: string } // VC-JWT for delegated operations
|
|
307
309
|
|
|
308
310
|
// Permanent destruction
|
|
309
311
|
{ version: 1, type: "delete",
|
|
310
312
|
did: string, // author DID
|
|
311
313
|
previousOperationCID: string,
|
|
312
314
|
createdAt: string,
|
|
313
|
-
note: string | null
|
|
315
|
+
note: string | null,
|
|
316
|
+
authorization?: string } // VC-JWT for delegated operations
|
|
314
317
|
```
|
|
315
318
|
|
|
316
319
|
### MultikeyPublicKey
|
|
@@ -366,7 +369,7 @@ Every operation JWS (identity-op and content-op) includes a `cid` field in the p
|
|
|
366
369
|
|
|
367
370
|
A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations.
|
|
368
371
|
|
|
369
|
-
Note: JWT tokens
|
|
372
|
+
Note: JWT auth tokens and VC-JWT credentials do NOT include a `cid` header — this field is specific to operation JWS tokens and beacons.
|
|
370
373
|
|
|
371
374
|
### CID Derivation
|
|
372
375
|
|
|
@@ -386,6 +389,122 @@ Where `idEncode` is the 19-char alphabet encoding described above.
|
|
|
386
389
|
|
|
387
390
|
---
|
|
388
391
|
|
|
392
|
+
## Credentials
|
|
393
|
+
|
|
394
|
+
Two credential types handle authentication and authorization. Both are DID-signed JWTs using Ed25519 (`alg: "EdDSA"`).
|
|
395
|
+
|
|
396
|
+
### Auth Tokens (Relay Authentication)
|
|
397
|
+
|
|
398
|
+
A DID-signed JWT proving the caller controls a DID. Short-lived, scoped to a specific relay via the `aud` (audience) claim. Used for relay AuthN — establishing identity before making requests.
|
|
399
|
+
|
|
400
|
+
**JWT Header:**
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"alg": "EdDSA",
|
|
405
|
+
"typ": "JWT",
|
|
406
|
+
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**JWT Payload:**
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
{
|
|
414
|
+
"iss": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
415
|
+
"sub": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
416
|
+
"aud": "relay.example.com",
|
|
417
|
+
"exp": 1772845200,
|
|
418
|
+
"iat": 1772841600
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
| Field | Type | Description |
|
|
423
|
+
| ----- | ------ | ---------------------------------------------------------- |
|
|
424
|
+
| `iss` | string | DID proving identity (the signer) |
|
|
425
|
+
| `sub` | string | Same as `iss` for auth tokens |
|
|
426
|
+
| `aud` | string | Target relay hostname (prevents cross-relay replay) |
|
|
427
|
+
| `exp` | number | Expiration — unix seconds (short-lived, typically minutes) |
|
|
428
|
+
| `iat` | number | Issued-at — unix seconds |
|
|
429
|
+
|
|
430
|
+
**Verification:** Standard JWT verification — EdDSA signature check, temporal validity (`iat` must not be in the future, `exp` must be after current time), audience match. The `kid` MUST be a DID URL (`did:dfos:xxx#key_yyy`) and the `kid` DID MUST match `iss`.
|
|
431
|
+
|
|
432
|
+
Auth tokens do NOT include a `cid` header — they are ephemeral session tokens, not content-addressed artifacts.
|
|
433
|
+
|
|
434
|
+
### VC-JWT Credentials (Authorization)
|
|
435
|
+
|
|
436
|
+
W3C Verifiable Credential Data Model v2 credentials encoded as JWT (`typ: "vc+jwt"`). Two credential types:
|
|
437
|
+
|
|
438
|
+
| Credential Type | Purpose |
|
|
439
|
+
| ------------------ | ------------------------------------------------------------ |
|
|
440
|
+
| `DFOSContentWrite` | Authorize extending a content chain (embedded in operations) |
|
|
441
|
+
| `DFOSContentRead` | Authorize reading content plane data (presented to relay) |
|
|
442
|
+
|
|
443
|
+
**VC-JWT Header:**
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"alg": "EdDSA",
|
|
448
|
+
"typ": "vc+jwt",
|
|
449
|
+
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**VC-JWT Payload:**
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"iss": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
458
|
+
"sub": "did:dfos:nzkf838efr424433rn2rzk",
|
|
459
|
+
"exp": 1798761600,
|
|
460
|
+
"iat": 1772841600,
|
|
461
|
+
"vc": {
|
|
462
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
463
|
+
"type": ["VerifiableCredential", "DFOSContentWrite"],
|
|
464
|
+
"credentialSubject": {}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
| Field | Type | Description |
|
|
470
|
+
| ---------------------- | -------- | -------------------------------------------------------- |
|
|
471
|
+
| `iss` | string | DID granting the credential (content creator/controller) |
|
|
472
|
+
| `sub` | string | DID receiving the credential (collaborator/reader) |
|
|
473
|
+
| `exp` | number | Expiration — unix seconds |
|
|
474
|
+
| `iat` | number | Issued-at — unix seconds |
|
|
475
|
+
| `vc.@context` | string[] | Must be `["https://www.w3.org/ns/credentials/v2"]` |
|
|
476
|
+
| `vc.type` | string[] | `["VerifiableCredential", "<DFOSCredentialType>"]` |
|
|
477
|
+
| `vc.credentialSubject` | object | Optional narrowing — see scope narrowing below |
|
|
478
|
+
|
|
479
|
+
**Scope narrowing:** The `credentialSubject` object may contain a `contentId` field. If absent, the credential grants broad access to all content by the issuer. If present, the credential is narrowed to the specific content chain.
|
|
480
|
+
|
|
481
|
+
```json
|
|
482
|
+
// Broad — all content by this DID
|
|
483
|
+
{ "credentialSubject": {} }
|
|
484
|
+
|
|
485
|
+
// Narrow — specific content chain only
|
|
486
|
+
{ "credentialSubject": { "contentId": "a82z92a3hndk6c97thcrn8" } }
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Verification:** EdDSA signature check, temporal validity (`iat` must not be in the future, `exp` must be after current time — using operation `createdAt` for chain-embedded VCs, wall clock for relay-presented VCs), `kid` DID URL format, `kid` DID matches `iss`, payload structure via Zod schema. Optionally verify `sub` and credential type match expectations.
|
|
490
|
+
|
|
491
|
+
### Content Chain Authorization
|
|
492
|
+
|
|
493
|
+
When `enforceAuthorization` is enabled on content chain verification:
|
|
494
|
+
|
|
495
|
+
1. **Genesis operation**: The signer is the chain creator, always authorized
|
|
496
|
+
2. **Creator signs subsequent ops**: Authorized directly — no credential needed
|
|
497
|
+
3. **Different DID signs**: Must include an `authorization` field containing a valid `DFOSContentWrite` VC-JWT where:
|
|
498
|
+
- `iss` matches the chain creator DID
|
|
499
|
+
- `sub` matches the signing DID
|
|
500
|
+
- The credential is temporally valid (`iat <= op.createdAt < exp`, not wall clock)
|
|
501
|
+
- If `contentId` is present in `credentialSubject`, it must match this chain's contentId
|
|
502
|
+
- The credential type is `DFOSContentWrite`
|
|
503
|
+
|
|
504
|
+
The `authorization` field is available on `update` and `delete` content operations. It is absent for creator-signed operations.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
389
508
|
## Beacons
|
|
390
509
|
|
|
391
510
|
A beacon is a signed announcement of a merkle root — a periodic commitment over a set of content IDs. Beacons are floating signed artifacts, not chained. They provide a compact, verifiable snapshot of an identity's content set at a point in time.
|
|
@@ -397,8 +516,8 @@ A beacon is a signed announcement of a merkle root — a periodic commitment ove
|
|
|
397
516
|
"version": 1,
|
|
398
517
|
"type": "beacon",
|
|
399
518
|
"did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
400
|
-
"merkleRoot": "
|
|
401
|
-
"createdAt": "2026-03-07T00:
|
|
519
|
+
"merkleRoot": "7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e",
|
|
520
|
+
"createdAt": "2026-03-07T00:05:00.000Z"
|
|
402
521
|
}
|
|
403
522
|
```
|
|
404
523
|
|
|
@@ -416,11 +535,41 @@ A beacon is a signed announcement of a merkle root — a periodic commitment ove
|
|
|
416
535
|
{
|
|
417
536
|
"alg": "EdDSA",
|
|
418
537
|
"typ": "did:dfos:beacon",
|
|
419
|
-
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#
|
|
420
|
-
"cid": "
|
|
538
|
+
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8",
|
|
539
|
+
"cid": "bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu"
|
|
421
540
|
}
|
|
422
541
|
```
|
|
423
542
|
|
|
543
|
+
### Worked Example: Beacon
|
|
544
|
+
|
|
545
|
+
Using the reference identity (`did:dfos:e3vvtck42d4eacdnzvtrn6`) and key 1 from the identity chain examples. The beacon commits to a merkle root over 5 content IDs (see Merkle Tree worked example below).
|
|
546
|
+
|
|
547
|
+
**Beacon CID** (dag-cbor canonical encode → CIDv1):
|
|
548
|
+
|
|
549
|
+
```
|
|
550
|
+
bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Controller JWS** (key 1 signs):
|
|
554
|
+
|
|
555
|
+
```
|
|
556
|
+
kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
|
|
557
|
+
typ: did:dfos:beacon
|
|
558
|
+
cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Witness countersignature** (key 2 signs the same payload — same CID, different kid):
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
|
|
565
|
+
typ: did:dfos:beacon
|
|
566
|
+
cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Both JWS tokens commit to identical bytes (same CID). The controller/witness distinction is determined at verification time by comparing the `kid` DID to the payload `did`.
|
|
570
|
+
|
|
571
|
+
Full JWS tokens are in [`examples/beacon.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/beacon.json).
|
|
572
|
+
|
|
424
573
|
### Beacon Semantics
|
|
425
574
|
|
|
426
575
|
Beacons are not chained — there is no `previousOperationCID`. For a given DID, the latest beacon with a strictly-greater `createdAt` timestamp wins. Beacons replace, not accumulate.
|
|
@@ -445,10 +594,84 @@ Beacons commit to a set of content IDs via a pure SHA-256 binary Merkle tree. Th
|
|
|
445
594
|
|
|
446
595
|
An empty set of content IDs produces a `null` root. A single content ID produces a root equal to `hex(SHA-256(UTF-8(contentId)))`.
|
|
447
596
|
|
|
597
|
+
### Worked Example: Merkle Tree
|
|
598
|
+
|
|
599
|
+
5 content IDs: `["alpha", "bravo", "charlie", "delta", "echo"]`
|
|
600
|
+
|
|
601
|
+
Already sorted lexicographically. Hash each leaf:
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
alpha → SHA-256("alpha") → 8ed3f6ad685b959ead7022518e1af76cd816f8e8ec7ccdda1ed4018e8f2223f8
|
|
605
|
+
bravo → SHA-256("bravo") → 4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398
|
|
606
|
+
charlie → SHA-256("charlie") → 36ef585cd42d49706cd2827a77d86c91bfdaf87a3f22b8f0e0308bd2c16cf85f
|
|
607
|
+
delta → SHA-256("delta") → 18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4
|
|
608
|
+
echo → SHA-256("echo") → 092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Build tree bottom-up, pairing left-to-right. Odd nodes promote unpaired:
|
|
612
|
+
|
|
613
|
+
```
|
|
614
|
+
Level 0 (leaves): [alpha] [bravo] [charlie] [delta] [echo]
|
|
615
|
+
Level 1: [alpha‖bravo] [charlie‖delta] [echo] ← promoted
|
|
616
|
+
Level 2: [L1-left‖L1-mid] [echo] ← promoted
|
|
617
|
+
Level 3 (root): [L2-left‖echo]
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Interior hashes:
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
SHA-256(alpha‖bravo) → 90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3
|
|
624
|
+
SHA-256(charlie‖delta) → 6b55e77bef32937d9ccce2bd4b18127b0483f0be8e5b63c30bcc2b0d09f7dd44
|
|
625
|
+
SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb862e3b6a86eb2dfa0ea8ba0edcf1c3f3b8f14abc5eb9d72eab2edc2f7
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Root** (level 3):
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
SHA-256(23c83c...edc2f7 ‖ 092c79...f7431d) → 7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e
|
|
632
|
+
```
|
|
633
|
+
|
|
448
634
|
### Inclusion Proofs
|
|
449
635
|
|
|
450
636
|
A Merkle inclusion proof demonstrates that a specific content ID is part of the committed set without revealing the full set. The proof consists of sibling hashes along the path from leaf to root, plus a direction (left/right) for each step.
|
|
451
637
|
|
|
638
|
+
### Worked Example: Inclusion Proof for "charlie"
|
|
639
|
+
|
|
640
|
+
Starting from the leaf hash of "charlie" (`36ef58...`), walk to the root using sibling hashes:
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
Step 1: charlie (index 2) paired with delta (index 3)
|
|
644
|
+
sibling: 4f4a9410...045398 (delta leaf) position: right
|
|
645
|
+
→ SHA-256(charlie ‖ delta) → 6b55e77b...f7dd44
|
|
646
|
+
|
|
647
|
+
Step 2: charlie‖delta paired with alpha‖bravo
|
|
648
|
+
sibling: 90d39555...a9b4f3 (alpha‖bravo) position: left
|
|
649
|
+
→ SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb8...edc2f7
|
|
650
|
+
|
|
651
|
+
Step 3: L2-left paired with echo (promoted)
|
|
652
|
+
sibling: 092c79e8...f7431d (echo leaf) position: right
|
|
653
|
+
→ SHA-256(L2-left ‖ echo) → 7e80d478...fda28e ✓ matches root
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Proof path (from [`examples/merkle-tree.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/merkle-tree.json)):
|
|
657
|
+
|
|
658
|
+
```json
|
|
659
|
+
[
|
|
660
|
+
{
|
|
661
|
+
"hash": "4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398",
|
|
662
|
+
"position": "right"
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
"hash": "90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3",
|
|
666
|
+
"position": "left"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
"hash": "092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d",
|
|
670
|
+
"position": "right"
|
|
671
|
+
}
|
|
672
|
+
]
|
|
673
|
+
```
|
|
674
|
+
|
|
452
675
|
---
|
|
453
676
|
|
|
454
677
|
## Countersignatures
|
|
@@ -489,13 +712,14 @@ Countersignatures are not part of the chain — they do not have `previousOperat
|
|
|
489
712
|
### Content Chain
|
|
490
713
|
|
|
491
714
|
1. Decode each JWS, parse payload as ContentOperation
|
|
492
|
-
2. First op must be `type: "create"`
|
|
715
|
+
2. First op must be `type: "create"` — the signer is the chain creator
|
|
493
716
|
3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing
|
|
494
717
|
4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID.
|
|
495
718
|
5. Verify the `kid` DID matches the payload `did` field (mismatches indicate a countersignature, not a chain operation)
|
|
496
719
|
6. Resolve `kid` via external key resolver (caller provides)
|
|
497
720
|
7. Verify EdDSA JWS signature
|
|
498
|
-
8.
|
|
721
|
+
8. If `enforceAuthorization` is enabled and the signer DID differs from the chain creator: verify the `authorization` field contains a valid `DFOSContentWrite` VC-JWT issued by the creator DID, with `sub` matching the signer, not expired at `op.createdAt`, and `contentId` (if present) matching this chain
|
|
722
|
+
9. Apply state change (set document, clear, or delete)
|
|
499
723
|
|
|
500
724
|
---
|
|
501
725
|
|
|
@@ -813,14 +1037,19 @@ Given the artifacts above, verify:
|
|
|
813
1037
|
|
|
814
1038
|
11. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end.
|
|
815
1039
|
|
|
1040
|
+
12. **VC-JWT credential verify**: using the issuer's public key, verify a `DFOSContentWrite` or `DFOSContentRead` credential: check EdDSA signature, `typ: "vc+jwt"`, expiration, `kid` DID URL format, `kid` DID matches `iss`, `vc` claim structure matches W3C VC Data Model v2, credential type matches expected DFOS type. Test vectors in [`examples/credential-write.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/credential-write.json) and [`examples/credential-read.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/credential-read.json).
|
|
1041
|
+
|
|
1042
|
+
13. **Delegated content chain verify**: using [`examples/content-delegated.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/content-delegated.json), verify a content chain where the genesis is signed by the creator and a subsequent update is signed by a delegate with an embedded `DFOSContentWrite` VC-JWT in the `authorization` field. The VC must be issued by the creator DID, with `sub` matching the delegate DID.
|
|
1043
|
+
|
|
816
1044
|
---
|
|
817
1045
|
|
|
818
1046
|
## Source and Verification
|
|
819
1047
|
|
|
820
|
-
All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies.
|
|
1048
|
+
All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 293 checks across 5 languages.
|
|
821
1049
|
|
|
822
1050
|
- [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature`
|
|
823
1051
|
- [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe`
|
|
1052
|
+
- [`crypto/jwt`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jwt.ts) — `createJwt`, `verifyJwt`
|
|
824
1053
|
- [`crypto/base64url`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/base64url.ts) — `base64urlEncode`, `base64urlDecode`
|
|
825
1054
|
- [`crypto/multiformats`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/multiformats.ts) — `dagCborCanonicalEncode`, `dagCborCanonicalEqual`
|
|
826
1055
|
- [`crypto/id`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/id.ts) — `generateId`, `generateIdNoPrefix`, `isValidId`
|
|
@@ -831,6 +1060,9 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
|
|
|
831
1060
|
- [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId`
|
|
832
1061
|
- [`chain/beacon`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/beacon.ts) — `signBeacon`, `verifyBeacon`
|
|
833
1062
|
- [`chain/countersign`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/countersign.ts) — `signCountersignature`, `verifyCountersignature`
|
|
1063
|
+
- [`credentials/auth-token`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/auth-token.ts) — `createAuthToken`, `verifyAuthToken`
|
|
1064
|
+
- [`credentials/credential`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/credential.ts) — `createCredential`, `verifyCredential`, `decodeCredentialUnsafe`
|
|
1065
|
+
- [`credentials/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/schemas.ts) — `AuthTokenClaims`, `CredentialClaims`, `VCClaim`, `DFOSCredentialType`
|
|
834
1066
|
- [`merkle/tree`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/tree.ts) — `buildMerkleTree`, `hashLeaf`
|
|
835
1067
|
- [`merkle/proof`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/proof.ts) — `generateMerkleProof`, `verifyMerkleProof`
|
|
836
1068
|
|
|
@@ -843,11 +1075,11 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
|
|
|
843
1075
|
|
|
844
1076
|
| Language | Tests | Source |
|
|
845
1077
|
| ---------- | ----- | ---------------------------------------------------------------------------------------------------- |
|
|
846
|
-
| TypeScript |
|
|
847
|
-
| Python |
|
|
848
|
-
| Go |
|
|
849
|
-
| Rust |
|
|
850
|
-
| Swift |
|
|
1078
|
+
| TypeScript | 190 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
|
|
1079
|
+
| Python | 59 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
|
|
1080
|
+
| Go | 15 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
|
|
1081
|
+
| Rust | 15 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
|
|
1082
|
+
| Swift | 14 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
|
|
851
1083
|
|
|
852
1084
|
---
|
|
853
1085
|
|
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ npm install @metalabel/dfos-protocol
|
|
|
13
13
|
```ts
|
|
14
14
|
// Chain verification
|
|
15
15
|
import { verifyContentChain, verifyIdentityChain } from '@metalabel/dfos-protocol/chain';
|
|
16
|
+
// Credentials (auth tokens + VC-JWT)
|
|
17
|
+
import { createAuthToken, verifyCredential } from '@metalabel/dfos-protocol/credentials';
|
|
16
18
|
// Crypto primitives
|
|
17
19
|
import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-protocol/crypto';
|
|
18
20
|
// Merkle trees
|
|
@@ -21,11 +23,12 @@ import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/mer
|
|
|
21
23
|
|
|
22
24
|
## Subpath Exports
|
|
23
25
|
|
|
24
|
-
| Export
|
|
25
|
-
|
|
|
26
|
-
| `@metalabel/dfos-protocol/chain`
|
|
27
|
-
| `@metalabel/dfos-protocol/
|
|
28
|
-
| `@metalabel/dfos-protocol/
|
|
26
|
+
| Export | Description |
|
|
27
|
+
| -------------------------------------- | ----------------------------------------------------------------------- |
|
|
28
|
+
| `@metalabel/dfos-protocol/chain` | Identity and content chain signing, verification, beacons, countersigns |
|
|
29
|
+
| `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
|
|
30
|
+
| `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
|
|
31
|
+
| `@metalabel/dfos-protocol/merkle` | SHA-256 binary merkle tree, inclusion proofs |
|
|
29
32
|
|
|
30
33
|
## Specifications
|
|
31
34
|
|
|
@@ -37,13 +40,18 @@ import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/mer
|
|
|
37
40
|
|
|
38
41
|
## Examples
|
|
39
42
|
|
|
40
|
-
The `examples/` directory contains deterministic reference
|
|
43
|
+
The `examples/` directory contains deterministic reference fixtures that can be independently verified by any Ed25519 + dag-cbor implementation:
|
|
41
44
|
|
|
42
45
|
- `identity-genesis.json` — single create operation
|
|
43
46
|
- `identity-rotation.json` — genesis + key rotation
|
|
44
47
|
- `identity-delete.json` — genesis + delete (terminal)
|
|
45
48
|
- `content-lifecycle.json` — create + update (with both documents)
|
|
46
49
|
- `content-delete.json` — create + delete
|
|
50
|
+
- `content-delegated.json` — creator genesis + delegated update with DFOSContentWrite VC-JWT
|
|
51
|
+
- `credential-write.json` — DFOSContentWrite VC-JWT (broad + content-narrowed)
|
|
52
|
+
- `credential-read.json` — DFOSContentRead VC-JWT
|
|
53
|
+
- `merkle-tree.json` — 5 content IDs → sorted tree → root, with inclusion proof
|
|
54
|
+
- `beacon.json` — signed merkle root announcement with witness countersignature
|
|
47
55
|
|
|
48
56
|
## License
|
|
49
57
|
|
package/dist/chain/index.d.ts
CHANGED
|
@@ -91,6 +91,8 @@ declare const ContentOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
91
91
|
baseDocumentCID: z.ZodNullable<z.ZodString>;
|
|
92
92
|
createdAt: z.ZodISODateTime;
|
|
93
93
|
note: z.ZodNullable<z.ZodString>;
|
|
94
|
+
/** VC-JWT authorizing this operation when signer is not the chain creator */
|
|
95
|
+
authorization: z.ZodOptional<z.ZodString>;
|
|
94
96
|
}, z.core.$strict>, z.ZodObject<{
|
|
95
97
|
version: z.ZodLiteral<1>;
|
|
96
98
|
type: z.ZodLiteral<"delete">;
|
|
@@ -98,6 +100,8 @@ declare const ContentOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
98
100
|
previousOperationCID: z.ZodString;
|
|
99
101
|
createdAt: z.ZodISODateTime;
|
|
100
102
|
note: z.ZodNullable<z.ZodString>;
|
|
103
|
+
/** VC-JWT authorizing this operation when signer is not the chain creator */
|
|
104
|
+
authorization: z.ZodOptional<z.ZodString>;
|
|
101
105
|
}, z.core.$strict>], "type">;
|
|
102
106
|
type ContentOperation = z.infer<typeof ContentOperation>;
|
|
103
107
|
/** Beacon: floating signed merkle root announcement */
|
|
@@ -182,6 +186,8 @@ interface VerifiedContentChain {
|
|
|
182
186
|
currentDocumentCID: string | null;
|
|
183
187
|
/** Number of operations in the chain */
|
|
184
188
|
length: number;
|
|
189
|
+
/** The DID that created the chain (signer of genesis operation) */
|
|
190
|
+
creatorDID: string;
|
|
185
191
|
}
|
|
186
192
|
/**
|
|
187
193
|
* Sign a content chain operation as a JWS and derive the operation CID
|
|
@@ -196,15 +202,30 @@ declare const signContentOperation: (input: {
|
|
|
196
202
|
operationCID: string;
|
|
197
203
|
}>;
|
|
198
204
|
/**
|
|
199
|
-
* Verify a content chain's structural integrity and
|
|
205
|
+
* Verify a content chain's structural integrity, signatures, and authorization
|
|
200
206
|
*
|
|
201
207
|
* The caller provides a key resolver to look up public keys from kid values.
|
|
202
208
|
* This keeps the content chain protocol independent of identity resolution.
|
|
209
|
+
*
|
|
210
|
+
* Authorization rules:
|
|
211
|
+
* - Genesis (create) operation: the signer is the chain creator, always authorized
|
|
212
|
+
* - Subsequent operations signed by the creator DID: authorized (no credential needed)
|
|
213
|
+
* - Subsequent operations signed by a different DID: must include an `authorization`
|
|
214
|
+
* field containing a valid DFOSContentWrite VC-JWT issued by the creator DID
|
|
203
215
|
*/
|
|
204
216
|
declare const verifyContentChain: (input: {
|
|
205
217
|
log: string[];
|
|
206
218
|
/** Resolve a kid (DID URL) to the raw Ed25519 public key bytes */
|
|
207
219
|
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
220
|
+
/**
|
|
221
|
+
* Enforce creator-sovereignty authorization. When true, non-creator signers
|
|
222
|
+
* must include a DFOSContentWrite VC-JWT in the operation's `authorization`
|
|
223
|
+
* field. When false (default), any signer with a valid signature is accepted.
|
|
224
|
+
*
|
|
225
|
+
* Web relays should set this to true. Applications migrating to VC-based
|
|
226
|
+
* authorization can enable this once all chains include authorization fields.
|
|
227
|
+
*/
|
|
228
|
+
enforceAuthorization?: boolean;
|
|
208
229
|
}) => Promise<VerifiedContentChain>;
|
|
209
230
|
|
|
210
231
|
/**
|
package/dist/chain/index.js
CHANGED