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.

spec updates

+34 -251
+34 -251
spec.md
··· 5 5 6 6 ## Abstract 7 7 8 - The `did:cow` method (Consensus Ownership 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. 8 + The `did:cow` method (Consensus Ownership Wrapper) provides persistent wrappers around other DID methods, enabling rotation and migration without breaking existing references. 9 + It stores changes of control (done currently done with `rotationKeys` in DID:PLC) on the Ethereum blockchain. 9 10 10 11 ## Status of This Document 11 12 ··· 17 18 18 19 Existing DID methods have tradeoffs: 19 20 - **did:key** - No rotation or recovery 20 - - **did:web** - Domain dependency 21 - - **did:plc** - Centralized sequencer (Bluesky's PLC server) 21 + - **did:web** - Domain dependency, if you lose control of your domain you lose control of your identity 22 + - **did:plc** - Dependency on a centralized sequencer (Bluesky's PLC server) 22 23 - **did:ethr** - Gas costs for all updates 23 24 24 25 Migrating between methods breaks all existing references. `did:cow` provides a stable wrapper. ··· 28 29 1. **Persistent** - Wrapper DID never changes 29 30 2. **Zero-cost creation** - No blockchain transaction to create 30 31 3. **Method agnostic** - Wraps any DID method 31 - 4. **Self-certifying** - Hash-based construction 32 - 5. **Decentralized** - No central registry dependency 33 - 6. **Transferable** - Controller can be changed 34 - 7. **Rotation Independence** - Key rotation secured by Ethereum, not the wrapped DID's infrastructure 35 - 8. **Composible Control** - Automatic compatibility with multisig and decentralized organization tooling such as Gnosis Safe. 32 + 4. **Decentralized** - No central registry dependency 33 + 5. **Transferable** - Controller can be changed 34 + 6. **Composible Control** - Automatic compatibility with multisig and decentralized organization tooling such as Gnosis Safe. 36 35 37 36 ## 2. DID Method Name 38 37 ··· 62 61 63 62 State mutations (updates/deactivations) are standard Ethereum transactions from the controller address. 64 63 65 - **Flow:** 66 64 1. Controller creates transaction with operation data 67 65 2. Controller signs with Ethereum key 68 66 3. Transaction broadcast to Ethereum 69 67 4. Smart contract validates: `msg.sender == current_controller` 70 68 5. State updated or transaction reverts 71 69 72 - **Smart Contract Implementation:** 73 - ```solidity 74 - // SPDX-License-Identifier: MIT 75 - pragma solidity ^0.8.0; 76 - 77 - contract COWRegistry { 78 - struct COW { 79 - address controller; 80 - bool deactivated; 81 - bytes wrappedDid; 82 - } 83 - 84 - mapping(bytes32 => State) public cows; 85 - 86 - event DIDUpdated(bytes32 indexed didHash, address controller, bytes wrappedDid); 87 - event DIDDeactivated(bytes32 indexed didHash); 88 - 89 - modifier onlyOwner() { 90 - } 91 - 92 - function updateWrappedDID( 93 - bytes32 didHash, 94 - bytes calldata newWrappedDid 95 - ) external onlyOwner { 96 - cows[didHash].wrappedDid = newWrappedDid; 97 - } 98 - 99 - function updateController( 100 - bytes32 didHash, 101 - address newController, 102 - ) external { 103 - State storage state = didStates[didHash]; 104 - 105 - // First update creates the state 106 - if (state.controller == address(0)) { 107 - require(newController != address(0), "Controller required"); 108 - require(newWrappedDid.length > 0, "WrappedDid required"); 109 - state.controller = msg.sender; 110 - } else { 111 - // Subsequent updates require authorization 112 - require(msg.sender == state.controller, "Not authorized"); 113 - require(!state.deactivated, "DID is deactivated"); 114 - } 115 - 116 - // Update state 117 - if (newController != address(0)) { 118 - state.controller = newController; 119 - } 120 - if (newWrappedDid.length > 0) { 121 - state.wrappedDid = newWrappedDid; 122 - } 123 - 124 - state.lastUpdated = block.timestamp; 125 - emit DIDUpdated(didHash, state.controller, state.wrappedDid); 126 - } 127 - 128 - function deactivate(bytes32 didHash) external { 129 - State storage state = didStates[didHash]; 130 - 131 - require(msg.sender == state.controller, "Not authorized"); 132 - require(!state.deactivated, "Already deactivated"); 133 - 134 - state.deactivated = true; 135 - emit DIDDeactivated(didHash); 136 - } 137 - 138 - function resolve(bytes32 didHash) external view returns ( 139 - address controller, 140 - bytes memory wrappedDid, 141 - bool deactivated 142 - ) { 143 - State storage state = didStates[didHash]; 144 - return (state.controller, state.wrappedDid, state.deactivated); 145 - } 146 - } 147 - ``` 148 - 149 70 ## 6. CRUD Operations 150 71 151 72 ### 6.1 Create 152 73 153 - No blockchain transaction required. 154 - 155 - **Process:** 156 - 1. Choose `wrapped_did` and `controller_address` 157 - 2. Construct DID: `did:cow:<controller_address>:wrapped_did` 158 - 159 - **Verification:** Anyone can fetch the state, recompute hash, confirm it matches the DID. 74 + 1. Create the wrapped DID 75 + 2. Choose your controller address 76 + 2. Insert `cow:<controller_address>:` after the initial `did`:. 160 77 161 78 ### 6.2 Read (Resolution) 162 79 163 - **Algorithm:** 164 - 1. Extract `<hash>` from `did:cow:<hash>` 165 - 2. Query blockchain for state matching `<hash>` 166 - 3. **If on-chain state exists:** Parse state, resolve wrapped_did, add wrapper metadata 167 - 4. **If no on-chain state:** Fetch from content-addressable storage, verify hash, resolve wrapped_did, add metadata 80 + 1. Query an Ethereum RPC endpoint to find out the wrapped DID 81 + 2. If it returns a value, resolve that as per that DID's standard 82 + 3. If it is unset, use the DID value originally specified in the ID 168 83 169 - Resolved DID document includes wrapped DID's content plus wrapper metadata in `service` endpoint. 84 + Resolved DID document includes wrapped DID's content plus wrapper metadata. 170 85 171 86 ### 6.3 Update 172 87 173 - On-chain transaction from current controller. 88 + Make an on-chain transaction from the current controller. 174 89 175 - **Binary format:** 176 - ``` 177 - [32 bytes: did_hash] 178 - [1 byte: operation_type (0x01)] 179 - [20 bytes: new_controller (0x00...00 if unchanged)] 180 - [N bytes: new_wrapped_did UTF-8] 181 - ``` 182 - 183 - **Validation:** 184 - - Transaction from current controller address 185 - - At least one field must change 186 - - New wrapped_did must be valid 187 - 188 - **Use cases:** DID rotation, key recovery, controller transfer, method migration 90 + The initial update 189 91 190 92 ### 6.4 Deactivate 191 93 192 94 Permanent. On-chain transaction from current controller. 193 95 194 - **Binary format:** 195 - ``` 196 - [32 bytes: did_hash] 197 - [1 byte: operation_type (0x02)] 198 - ``` 96 + Set the controller address to `0x` and the wrapped DID value to `did:`. 199 97 200 98 After deactivation, DID resolves to deactivated status. Cannot be reactivated. 201 99 202 100 ## 7. Security Considerations 203 101 204 - ### 7.1 Controller Key Security 102 + ### 7.1 Controller 205 103 206 - Security depends on: 207 - 1. Controller's Ethereum private key (secp256k1) 208 - 2. Wrapped DID's keys 209 - 210 - Compromised controller key → attacker can update wrapper. 211 - Compromised wrapped DID keys → attacker can impersonate within that method. 104 + The controller address inherits all the security considerations of any other Ethereum address. 212 105 213 106 ### 7.2 Wrapped DID Dependence 214 107 215 - Inherits ALL security properties of wrapped DID, but is recoverable: 108 + The did:cow address inherits all security properties of wrapped DID. 216 109 - did:web → DNS hijacking risk 217 110 - did:key → no rotation 218 111 - did:plc → trust in Bluesky's directory 219 112 113 + However, since users can switch to another wrapped DID they can recover a compromise of the wrapped DID, and also exit in circumstances where the wrapped DID appears unreliable. 114 + 220 115 ### 7.3 Blockchain Dependencies 221 116 222 - Uses Ethereum mainnet for state transitions, immutable audit trail, authorization, and spam prevention (gas costs). 117 + **Why Ethereum:** 223 118 224 - **Why Ethereum:** High security, established ecosystem, ECDSA signing, native smart contracts, deterministic finality. 119 + High security, established ecosystem, established tooling for multisig and organizational control. Strong social consensus on anti-censorship means we can be confident that the main Ethereum chain, or failing that a viable fork of the Ethereum chain, will accept continue accepting updates without censorship for the foreseeable future. 225 120 226 121 **Tradeoffs:** Gas costs (~50-100k gas per update), ~12 second confirmation 227 122 ··· 229 124 230 125 ### 8.1 Controller Address Linkability 231 126 232 - `controller_address` appears in hash computation and on-chain. Reusing controller links all DIDs. 233 - 234 - **Mitigation:** Different controllers per context or privacy-preserving addresses. 127 + `controller_address` is visible as part of the DID and also on-chain once updates are made. Reusing a controller links all DIDs. 235 128 236 129 ### 8.2 On-Chain Metadata 237 130 ··· 247 140 - `resolveDID(didCow)` - Resolve to DID document 248 141 - `updateDID(didCow, newWrappedDid, newController)` - Build update transaction 249 142 - `deactivateDID(didCow)` - Build deactivation transaction 250 - 251 - **Benefits:** No signature in payload (blockchain handles auth), ~50% smaller than JSON, deterministic parsing, fixed-length addressing enables delimiter-free concatenation, native replay protection. 252 143 253 144 ## 10. Example DID Document 254 145 ··· 307 198 | Feature | did:cow | did:key | did:web | did:plc | 308 199 |---------|---------|---------|---------|---------| 309 200 | Rotation Support | ✓ | ✗ | ✓ | ✓ | 310 - | Recovery | ✓ | ✗ | Limited | ✓ | 311 - | Zero-cost Creation | ✓ | ✓ | ✓ | ✗ | 201 + | Zero-cost Creation | ✓ | ✓ | ✓ | ✓ | 202 + | Zero-cost Controller Updates | ✗ | ✓ | ✓ | ✓ | 312 203 | Decentralized | ✓ | ✓ | ✗ | ✗ | 313 - | Method Migration | ✓ | ✗ | ✗ | ✗ | 314 - | Self-verifying | Partial | ✓ | ✗ | ✗ | 204 + | Zero-cost Controller Updates | ✓ | ✓ | ✓ | ✓ | 205 + | Decentralized | ✓ | ✓ | ✗ | ✗ | 315 206 | Blockchain Required | Ethereum | None | None | None | 316 207 | Rotation Authority | Ethereum | N/A | DNS | PLC Directory | 317 - | Censorship Resistant | ✓ | N/A | ✗ | ✗ | 318 - 319 - ## 12. Use Cases 208 + | Censorship Resistant | ✓ | ✓ | ✗ | ✗ | 320 209 321 - ### 12.1 Solving did:plc Reorg Risk 322 - 323 - did:plc weakness: Bluesky controls operation log sequencing. Can reorder/reorg history. 210 + ## 12. Philosophical considerations 324 211 325 - **Risks:** Operation log rewrites, sequence ambiguity, directory downtime. 326 - 327 - **Solution with did:cow wrapper:** 328 - ``` 329 - did:cow:abc123 → did:plc:z72i7hdynmk6r22z27h6tvur 330 - ``` 331 - 332 - If PLC has issues, execute Ethereum transaction to rotate: 333 - ``` 334 - did:cow:abc123 → did:web:yoursite.com 335 - ``` 336 - 337 - **Result:** PLC becomes an optional convenience layer, not a single point of failure. Ethereum provides immutable, censorship-resistant record of canonical DID. 338 - 339 - ### 12.2 Progressive Decentralization 340 - 341 - Start simple, upgrade later: 342 - ``` 343 - T0: did:cow:abc123 → did:web:example.com 344 - T1: did:cow:abc123 → did:plc:z72i7hdynmk6r22z27h6tvur 345 - ``` 346 - 347 - ### 12.3 Key Compromise Recovery 348 - 349 - Rotate to fresh keys: 350 - ``` 351 - did:cow:abc123 → did:plc:compromised-id [compromised] 352 - did:cow:abc123 → did:plc:fresh-keys-id [Ethereum tx] 353 - ``` 354 - 355 - ### 12.4 Domain Loss Recovery 356 - 357 - Domain expired? Rotate: 358 - ``` 359 - did:cow:abc123 → did:web:lost-domain.com [expired] 360 - did:cow:abc123 → did:plc:recovered-id [Ethereum tx] 361 - ``` 362 - 363 - ### 12.5 Multi-Identity Aggregation 212 + DIDs are intended to be permanent identifiers. Using a wrapper implies that the wrapped DID is not in fact a permanent identifier. 364 213 365 - Rotate between multiple DIDs for different contexts while maintaining one persistent identifier. 214 + We consider this to illuminate a problem with the wrapped DIDs, rather than with this proposal. A permanent wrapper is required because users cannot be sufficiently confident in the permanence of their existing options. 366 215 367 - ## 14. References 216 + ## 13. References 368 217 369 218 - [DID Core Specification](https://www.w3.org/TR/did-core/) 370 219 - [DID Method Rubric](https://w3c.github.io/did-rubric/) 371 220 - [did:key Method](https://w3c-ccg.github.io/did-method-key/) 372 221 - [did:web Method](https://w3c-ccg.github.io/did-method-web/) 373 222 - [did:plc Method](https://github.com/did-method-plc/did-method-plc) 374 - 375 - ## Appendix A: Hash Computation Example 376 - 377 - Python implementation: 378 - 379 - ```python 380 - import hashlib 381 - 382 - def create_did_tlc(controller_hex: str, wrapped_did: str) -> str: 383 - """ 384 - Create a did:cow identifier. 385 - 386 - Args: 387 - controller_hex: Ethereum address as hex string (no 0x prefix), e.g., "742d35..." 388 - wrapped_did: The DID to wrap, e.g., "did:ion:..." 389 - """ 390 - # Convert controller from hex to bytes 391 - controller_bytes = bytes.fromhex(controller_hex) 392 - 393 - # Convert wrapped_did to UTF-8 bytes 394 - wrapped_did_bytes = wrapped_did.encode('utf-8') 395 - 396 - # Concatenate: controller || wrapped_did 397 - preimage = controller_bytes + wrapped_did_bytes 398 - 399 - # Compute SHA-256 hash 400 - hash_bytes = hashlib.sha256(preimage).digest() 401 - 402 - # Convert to hex string 403 - hash_hex = hash_bytes.hex() 404 - 405 - # Construct DID 406 - return f"did:cow:{hash_hex}" 407 - 408 - def parse_initial_state(state_bytes: bytes) -> tuple[str, str]: 409 - """ 410 - Parse initial state blob. 411 - 412 - Returns: (controller_hex, wrapped_did) 413 - """ 414 - controller_bytes = state_bytes[:20] 415 - wrapped_did_bytes = state_bytes[20:] 416 - 417 - controller_hex = controller_bytes.hex() 418 - wrapped_did = wrapped_did_bytes.decode('utf-8') 419 - 420 - return controller_hex, wrapped_did 421 - 422 - # Example 423 - controller = "742d35Cc6634C0532925a3b844Bc9e7595f0bEb" 424 - wrapped = "did:web:example.com" 425 - 426 - did = create_did_tlc(controller, wrapped) 427 - print(did) 428 - # Output: did:cow:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52 429 - 430 - # Verify 431 - state_blob = bytes.fromhex(controller) + wrapped.encode('utf-8') 432 - parsed_controller, parsed_wrapped = parse_initial_state(state_blob) 433 - verified_did = create_did_tlc(parsed_controller, parsed_wrapped) 434 - assert verified_did == did 435 - ``` 436 - 437 - ## Appendix B: Acknowledgments 438 - 439 - This specification was designed in collaboration with Claude (Anthropic) on February 16, 2026. 440 223 441 224 --- 442 225