did:cow, a proposal for an ID resolution method with most of the convenience of did:plc/did:web and the robustness of a public blockchain
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial spec mostly by claude

Edmund Edgar be49e18a

+533
+533
spec.md
··· 1 + # The did:cow Method Specification v0.1 2 + 3 + **Status:** Draft Specification 4 + **Date:** February 16, 2026 5 + 6 + ## Abstract 7 + 8 + The `did:cow` method (Consensus Origin Wrapper) provides persistent wrappers around other DID methods, enabling rotation and migration without breaking existing references. Uses Ethereum for state transitions, content-addressable storage for zero-cost creation. 9 + 10 + ## Status of This Document 11 + 12 + This is a draft specification and may be updated, replaced, or obsoleted at any time. It is inappropriate to cite this document as anything other than work in progress. 13 + 14 + ## 1. Introduction 15 + 16 + ### 1.1 Motivation 17 + 18 + Existing DID methods have tradeoffs: 19 + - **did:key** - No rotation or recovery 20 + - **did:web** - Domain dependency 21 + - **did:plc** - Centralized sequencer (Bluesky's PLC server) 22 + 23 + Migrating between methods breaks all existing references. `did:cow` provides a stable wrapper. 24 + 25 + **Key advantage:** Ethereum sequencing eliminates did:plc's centralized reorg risk: 26 + - **Reorg protection** - Ethereum finality prevents operation log rewrites 27 + - **Operational independence** - Rotate away from did:plc without permission 28 + - **Censorship resistance** - Bonus: no single entity can block updates 29 + 30 + **Solving the did:plc Centralization Problem:** 31 + 32 + The primary centralization risk in did:plc is that the PLC directory controls operation sequencing and can censor updates. By wrapping a did:plc in did:cow, key rotation and recovery operations are secured by Ethereum's blockchain instead of the PLC directory: 33 + 34 + - **Without did:cow:** `did:plc:abc` rotation requires PLC directory to sequence and publish updates 35 + - **With did:cow:** `did:cow:xyz → did:plc:abc` rotation happens via Ethereum transaction, creating a new did:plc:def and updating the wrapper atomically 36 + 37 + If the PLC directory becomes unavailable, censors updates, or acts maliciously, the wrapper controller can rotate to a different DID method entirely (did:web, did:key, or another did:plc instance) without losing their persistent identifier. 38 + 39 + ### 1.2 Design Goals 40 + 41 + 1. **Persistent** - Wrapper DID never changes 42 + 2. **Zero-cost creation** - No blockchain transaction to create 43 + 3. **Method agnostic** - Wraps any DID method 44 + 4. **Self-certifying** - Hash-based construction 45 + 5. **Decentralized** - No central registry dependency 46 + 6. **Transferable** - Controller can be changed 47 + 7. **Rotation Independence** - Key rotation secured by Ethereum, not the wrapped DID's infrastructure 48 + 49 + ## 2. DID Method Name 50 + 51 + Method name: `cow` (Consensus Origin Wrapper) 52 + 53 + DID prefix: `did:cow:` (lowercase) 54 + 55 + ## 3. Method Specific Identifier 56 + 57 + Format: `did:cow:<hash>` 58 + 59 + Hash computation: 60 + ``` 61 + hash = SHA256(controller_address || wrapped_did) 62 + ``` 63 + 64 + **Parameters:** 65 + - `controller_address` - Ethereum address (20 bytes, no "0x" prefix) 66 + - `wrapped_did` - UTF-8 encoded DID string 67 + - `||` - Binary concatenation 68 + 69 + ### 3.1 Example 70 + 71 + ``` 72 + controller_address = "742d35Cc6634C0532925a3b844Bc9e7595f0bEb" (20 bytes, no 0x prefix) 73 + wrapped_did = "did:web:example.com" 74 + 75 + preimage = bytes.fromhex("742d35Cc6634C0532925a3b844Bc9e7595f0bEb") + 76 + "did:web:example.com".encode('utf-8') 77 + 78 + hash = SHA256(preimage) 79 + = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52" 80 + 81 + DID = did:cow:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52 82 + ``` 83 + 84 + **Parsing:** 85 + 1. First 20 bytes → controller_address 86 + 2. Remaining bytes (UTF-8) → wrapped_did 87 + 3. Verify: SHA256(controller_address || wrapped_did) == hash 88 + 89 + ## 5. Blockchain Transaction Model 90 + 91 + State mutations (updates/deactivations) are standard Ethereum transactions from the controller address. 92 + 93 + **Flow:** 94 + 1. Controller creates transaction with operation data 95 + 2. Controller signs with Ethereum key 96 + 3. Transaction broadcast to Ethereum 97 + 4. Smart contract validates: `msg.sender == current_controller` 98 + 5. State updated or transaction reverts 99 + 100 + **Smart Contract Implementation:** 101 + ```solidity 102 + // SPDX-License-Identifier: MIT 103 + pragma solidity ^0.8.0; 104 + 105 + contract COWRegistry { 106 + struct State { 107 + address controller; 108 + bytes wrappedDid; 109 + bool deactivated; 110 + uint256 lastUpdated; 111 + } 112 + 113 + mapping(bytes32 => State) public didStates; 114 + 115 + event DIDUpdated(bytes32 indexed didHash, address controller, bytes wrappedDid); 116 + event DIDDeactivated(bytes32 indexed didHash); 117 + 118 + function update( 119 + bytes32 didHash, 120 + address newController, 121 + bytes calldata newWrappedDid 122 + ) external { 123 + State storage state = didStates[didHash]; 124 + 125 + // First update creates the state 126 + if (state.controller == address(0)) { 127 + require(newController != address(0), "Controller required"); 128 + require(newWrappedDid.length > 0, "WrappedDid required"); 129 + state.controller = msg.sender; 130 + } else { 131 + // Subsequent updates require authorization 132 + require(msg.sender == state.controller, "Not authorized"); 133 + require(!state.deactivated, "DID is deactivated"); 134 + } 135 + 136 + // Update state 137 + if (newController != address(0)) { 138 + state.controller = newController; 139 + } 140 + if (newWrappedDid.length > 0) { 141 + state.wrappedDid = newWrappedDid; 142 + } 143 + 144 + state.lastUpdated = block.timestamp; 145 + emit DIDUpdated(didHash, state.controller, state.wrappedDid); 146 + } 147 + 148 + function deactivate(bytes32 didHash) external { 149 + State storage state = didStates[didHash]; 150 + 151 + require(msg.sender == state.controller, "Not authorized"); 152 + require(!state.deactivated, "Already deactivated"); 153 + 154 + state.deactivated = true; 155 + emit DIDDeactivated(didHash); 156 + } 157 + 158 + function resolve(bytes32 didHash) external view returns ( 159 + address controller, 160 + bytes memory wrappedDid, 161 + bool deactivated 162 + ) { 163 + State storage state = didStates[didHash]; 164 + return (state.controller, state.wrappedDid, state.deactivated); 165 + } 166 + } 167 + ``` 168 + 169 + ## 6. CRUD Operations 170 + 171 + ### 6.1 Create 172 + 173 + No blockchain transaction required. 174 + 175 + **Process:** 176 + 1. Choose `wrapped_did` and `controller_address` 177 + 2. Compute `hash = SHA256(controller_address || wrapped_did)` 178 + 3. Construct DID: `did:cow:<hash>` 179 + 4. Create initial state: `[20 bytes: controller][N bytes: wrapped_did]` 180 + 5. Store at content-addressable location (IPFS, Arweave, etc.) 181 + 182 + **Verification:** Anyone can fetch the state, recompute hash, confirm it matches the DID. 183 + 184 + ### 6.2 Read (Resolution) 185 + 186 + **Algorithm:** 187 + 1. Extract `<hash>` from `did:cow:<hash>` 188 + 2. Query blockchain for state matching `<hash>` 189 + 3. **If on-chain state exists:** Parse state, resolve wrapped_did, add wrapper metadata 190 + 4. **If no on-chain state:** Fetch from content-addressable storage, verify hash, resolve wrapped_did, add metadata 191 + 192 + Resolved DID document includes wrapped DID's content plus wrapper metadata in `service` endpoint. 193 + 194 + ### 6.3 Update 195 + 196 + On-chain transaction from current controller. 197 + 198 + **Binary format:** 199 + ``` 200 + [32 bytes: did_hash] 201 + [1 byte: operation_type (0x01)] 202 + [20 bytes: new_controller (0x00...00 if unchanged)] 203 + [N bytes: new_wrapped_did UTF-8] 204 + ``` 205 + 206 + **Validation:** 207 + - Transaction from current controller address 208 + - At least one field must change 209 + - New wrapped_did must be valid 210 + 211 + **Use cases:** DID rotation, key recovery, controller transfer, method migration 212 + 213 + ### 6.4 Deactivate 214 + 215 + Permanent. On-chain transaction from current controller. 216 + 217 + **Binary format:** 218 + ``` 219 + [32 bytes: did_hash] 220 + [1 byte: operation_type (0x02)] 221 + ``` 222 + 223 + After deactivation, DID resolves to deactivated status. Cannot be reactivated. 224 + 225 + ## 7. Security Considerations 226 + 227 + ### 7.1 Hash Collision Resistance 228 + 229 + SHA-256 provides 256-bit collision resistance. Accidental collision is negligible. Preimage attacks are computationally infeasible. 230 + 231 + ### 7.2 Controller Key Security 232 + 233 + Security depends on: 234 + 1. Controller's Ethereum private key (secp256k1) 235 + 2. Wrapped DID's keys 236 + 237 + **Authorization:** Ethereum transactions validated by smart contract (`msg.sender == controller`). Replay protection via nonces. Gas costs prevent spam. 238 + 239 + Compromised controller key → attacker can update wrapper. 240 + Compromised wrapped DID keys → attacker can impersonate within that method. 241 + 242 + **Mitigation:** Hardware wallets, multisig contracts (Gnosis Safe), account abstraction (ERC-4337). 243 + 244 + ### 7.3 Wrapped DID Dependence 245 + 246 + Inherits ALL security properties of wrapped DID: 247 + - did:web → DNS hijacking risk 248 + - did:key → no rotation 249 + - did:plc → trust in Bluesky's directory 250 + 251 + **Wrapper only provides portability, not security.** 252 + 253 + **did:plc special case:** Wrapping did:plc adds exit capability. Day-to-day trusts PLC directory; crisis allows rotation via Ethereum. Primary concern is subtle reorgs, not censorship. Ethereum anchoring makes canonical state independent of PLC's operation log. 254 + 255 + **Exception - did:plc Rotation:** 256 + 257 + While daily operations with a wrapped did:plc still depend on the PLC directory for resolution, **key rotation and recovery are secured by Ethereum instead of PLC**. This means: 258 + 259 + - PLC directory can censor or fail → rotate to new DID method via Ethereum 260 + - PLC directory compromised → maintain control through Ethereum-based wrapper updates 261 + - No dependency on PLC for the critical security operation of key rotation 262 + 263 + This significantly reduces the centralization risk compared to using did:plc directly. 264 + 265 + ### 7.4 Registration Race Conditions 266 + 267 + Multiple parties can create wrappers with same `wrapped_did` but different `controller_address` (different hashes, different DIDs). 268 + 269 + Not a security issue: 270 + - Each wrapper independently controlled 271 + - Wrapper doesn't grant authority over wrapped DID 272 + - Social/technical adoption determines authoritative wrapper 273 + - Identity verified through wrapped DID's cryptographic proofs 274 + 275 + ### 7.5 Blockchain Dependencies 276 + 277 + Uses Ethereum mainnet for state transitions, immutable audit trail, authorization, and spam prevention (gas costs). 278 + 279 + **Why Ethereum:** High security, established ecosystem, ECDSA signing, native smart contracts, deterministic finality. 280 + 281 + **Tradeoffs:** Gas costs (~50-100k gas per update), ~12 second confirmation, MEV/front-running (mitigated by validation), L2 not supported in v0.1. 282 + 283 + ## 8. Privacy Considerations 284 + 285 + ### 8.1 Correlation Risk 286 + 287 + `wrapped_did` visible in initial state, on-chain transactions, and resolution responses. Same wrapped DID in multiple contexts enables trivial correlation. 288 + 289 + **Mitigation:** Use pairwise wrapped DIDs. 290 + 291 + ### 8.2 Controller Address Linkability 292 + 293 + `controller_address` appears in hash computation and on-chain. Reusing controller links all DIDs. 294 + 295 + **Mitigation:** Different controllers per context or privacy-preserving addresses. 296 + 297 + ### 8.3 On-Chain Metadata 298 + 299 + All updates permanently public with timestamps. Creates audit trail of updates, previous/new wrapped DIDs, and controller history. 300 + 301 + Unavoidable in current design. 302 + 303 + ## 9. Reference Implementation 304 + 305 + Available at: [To be provided] 306 + 307 + **Key functions:** 308 + - `createDID(controllerHex, wrappedDid)` - Generate did:cow 309 + - `parseInitialState(stateBytes)` - Parse binary state 310 + - `resolveDID(didCow)` - Resolve to DID document 311 + - `updateDID(didCow, newWrappedDid, newController)` - Build update transaction 312 + - `deactivateDID(didCow)` - Build deactivation transaction 313 + 314 + **Benefits:** No signature in payload (blockchain handles auth), ~50% smaller than JSON, deterministic parsing, fixed-length addressing enables delimiter-free concatenation, native replay protection. 315 + 316 + ## 10. Example DID Document 317 + 318 + Given: 319 + ``` 320 + did:cow:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52 321 + ``` 322 + 323 + Created from binary state: 324 + ``` 325 + [742d35Cc6634C0532925a3b844Bc9e7595f0bEb (20 bytes)] 326 + [did:web:example.com (UTF-8)] 327 + ``` 328 + 329 + Wrapping: 330 + ``` 331 + did:web:example.com 332 + ``` 333 + 334 + Resolved DID Document: 335 + ```json 336 + { 337 + "@context": [ 338 + "https://www.w3.org/ns/did/v1", 339 + "https://w3id.org/security/suites/jws-2020/v1" 340 + ], 341 + "id": "did:cow:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52", 342 + "controller": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", 343 + "verificationMethod": [ 344 + { 345 + "id": "did:web:example.com#key-1", 346 + "type": "JsonWebKey2020", 347 + "controller": "did:cow:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52", 348 + "publicKeyJwk": { 349 + "kty": "EC", 350 + "crv": "secp256k1", 351 + "x": "...", 352 + "y": "..." 353 + } 354 + } 355 + ], 356 + "authentication": [ 357 + "did:web:example.com#key-1" 358 + ], 359 + "service": [ 360 + { 361 + "id": "#wrapper-metadata", 362 + "type": "COWWrapper", 363 + "serviceEndpoint": { 364 + "wrapped_did": "did:web:example.com", 365 + "wrapper_controller": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", 366 + "on_chain_state": true, 367 + "last_updated": "2026-02-16T10:30:00Z" 368 + } 369 + } 370 + ] 371 + } 372 + ``` 373 + 374 + ## 11. Comparison 375 + 376 + | Feature | did:cow | did:key | did:web | did:plc | 377 + |---------|---------|---------|---------|---------| 378 + | Rotation Support | ✓ | ✗ | ✓ | ✓ | 379 + | Recovery | ✓ | ✗ | Limited | ✓ | 380 + | Zero-cost Creation | ✓ | ✓ | ✓ | ✗ | 381 + | Decentralized | ✓ | ✓ | ✗ | ✗ | 382 + | Method Migration | ✓ | ✗ | ✗ | ✗ | 383 + | Self-verifying | Partial | ✓ | ✗ | ✗ | 384 + | Blockchain Required | Ethereum | None | None | None | 385 + | Rotation Authority | Ethereum | N/A | DNS | PLC Directory | 386 + | Censorship Resistant | ✓ | N/A | ✗ | ✗ | 387 + 388 + ## 12. Use Cases 389 + 390 + ### 12.1 Solving did:plc Reorg Risk 391 + 392 + did:plc weakness: Bluesky controls operation log sequencing. Can reorder/reorg history. 393 + 394 + **Risks:** Operation log rewrites, sequence ambiguity, directory downtime. 395 + 396 + **Solution with did:cow wrapper:** 397 + ``` 398 + did:cow:abc123 → did:plc:z72i7hdynmk6r22z27h6tvur 399 + ``` 400 + 401 + If PLC has issues, execute Ethereum transaction to rotate: 402 + ``` 403 + did:cow:abc123 → did:web:yoursite.com 404 + ``` 405 + 406 + **Result:** PLC becomes an optional convenience layer, not a single point of failure. Ethereum provides immutable, censorship-resistant record of canonical DID. 407 + 408 + ### 12.2 Progressive Decentralization 409 + 410 + Start simple, upgrade later: 411 + ``` 412 + T0: did:cow:abc123 → did:web:example.com 413 + T1: did:cow:abc123 → did:plc:z72i7hdynmk6r22z27h6tvur 414 + ``` 415 + 416 + ### 12.3 Key Compromise Recovery 417 + 418 + Rotate to fresh keys: 419 + ``` 420 + did:cow:abc123 → did:plc:compromised-id [compromised] 421 + did:cow:abc123 → did:plc:fresh-keys-id [Ethereum tx] 422 + ``` 423 + 424 + ### 12.4 Domain Loss Recovery 425 + 426 + Domain expired? Rotate: 427 + ``` 428 + did:cow:abc123 → did:web:lost-domain.com [expired] 429 + did:cow:abc123 → did:plc:recovered-id [Ethereum tx] 430 + ``` 431 + 432 + ### 12.5 Multi-Identity Aggregation 433 + 434 + Rotate between multiple DIDs for different contexts while maintaining one persistent identifier. 435 + 436 + ## 13. Open Questions 437 + 438 + ### 13.1 Content-Addressable Storage 439 + 440 + IPFS only? Any system? Multiple fallbacks? 441 + 442 + Current: Any system with hash-based retrieval. 443 + 444 + ### 13.2 Smart Contract Deployment 445 + 446 + Canonical contract address? Multiple deployments? ENS registration? 447 + 448 + Current: Single canonical contract at well-known address. 449 + 450 + ### 13.3 Layer 2 Scaling 451 + 452 + Support Optimism/Arbitrum? zkSync/StarkNet? Cross-L2 resolution? 453 + 454 + Current: Ethereum mainnet only for v0.1. 455 + 456 + ## 14. References 457 + 458 + - [DID Core Specification](https://www.w3.org/TR/did-core/) 459 + - [DID Method Rubric](https://w3c.github.io/did-rubric/) 460 + - [did:key Method](https://w3c-ccg.github.io/did-method-key/) 461 + - [did:web Method](https://w3c-ccg.github.io/did-method-web/) 462 + - [did:plc Method](https://github.com/did-method-plc/did-method-plc) 463 + 464 + ## Appendix A: Hash Computation Example 465 + 466 + Python implementation: 467 + 468 + ```python 469 + import hashlib 470 + 471 + def create_did_tlc(controller_hex: str, wrapped_did: str) -> str: 472 + """ 473 + Create a did:cow identifier. 474 + 475 + Args: 476 + controller_hex: Ethereum address as hex string (no 0x prefix), e.g., "742d35..." 477 + wrapped_did: The DID to wrap, e.g., "did:ion:..." 478 + """ 479 + # Convert controller from hex to bytes 480 + controller_bytes = bytes.fromhex(controller_hex) 481 + 482 + # Convert wrapped_did to UTF-8 bytes 483 + wrapped_did_bytes = wrapped_did.encode('utf-8') 484 + 485 + # Concatenate: controller || wrapped_did 486 + preimage = controller_bytes + wrapped_did_bytes 487 + 488 + # Compute SHA-256 hash 489 + hash_bytes = hashlib.sha256(preimage).digest() 490 + 491 + # Convert to hex string 492 + hash_hex = hash_bytes.hex() 493 + 494 + # Construct DID 495 + return f"did:cow:{hash_hex}" 496 + 497 + def parse_initial_state(state_bytes: bytes) -> tuple[str, str]: 498 + """ 499 + Parse initial state blob. 500 + 501 + Returns: (controller_hex, wrapped_did) 502 + """ 503 + controller_bytes = state_bytes[:20] 504 + wrapped_did_bytes = state_bytes[20:] 505 + 506 + controller_hex = controller_bytes.hex() 507 + wrapped_did = wrapped_did_bytes.decode('utf-8') 508 + 509 + return controller_hex, wrapped_did 510 + 511 + # Example 512 + controller = "742d35Cc6634C0532925a3b844Bc9e7595f0bEb" 513 + wrapped = "did:web:example.com" 514 + 515 + did = create_did_tlc(controller, wrapped) 516 + print(did) 517 + # Output: did:cow:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52 518 + 519 + # Verify 520 + state_blob = bytes.fromhex(controller) + wrapped.encode('utf-8') 521 + parsed_controller, parsed_wrapped = parse_initial_state(state_blob) 522 + verified_did = create_did_tlc(parsed_controller, parsed_wrapped) 523 + assert verified_did == did 524 + ``` 525 + 526 + ## Appendix B: Acknowledgments 527 + 528 + This specification was designed in collaboration with Claude (Anthropic) on February 16, 2026. 529 + 530 + --- 531 + 532 + **Version History:** 533 + - v0.1 (2026-02-16) - Initial draft specification