Skip to content

Identity & Keys

Every xNet user has a Decentralized Identifier (DID) derived from their Ed25519 public key. The format follows the W3C DID:key method:

did:key:z6Mk...

The public key is embedded directly in the DID, making it self-certifying — you can extract the public key and verify signatures without any external resolver or blockchain.

  1. Take the 32-byte Ed25519 public key
  2. Prepend the multicodec prefix 0xed01 (Ed25519 public key)
  3. Encode with base58btc (multibase prefix z)
  4. Prefix with did:key:
import { generateIdentity, isValidDID } from '@xnet/identity'
const { identity, privateKey } = generateIdentity()
// identity.did = "did:key:z6Mk..."
// identity.publicKey = Uint8Array (32 bytes)
// identity.created = Date.now()
isValidDID(identity.did) // true
import { parseDID } from '@xnet/identity'
const publicKey = parseDID('did:key:z6Mk...')
// Returns the 32-byte Ed25519 public key

Each user has two key pairs bundled together:

KeyAlgorithmPurpose
Signing keyEd25519Sign changes, Yjs updates, UCAN tokens
Encryption keyX25519Key exchange, encrypted storage

The DID is derived from the signing public key.

import { generateKeyBundle, deriveKeyBundle } from '@xnet/identity'
// Random generation
const bundle = generateKeyBundle()
// bundle.signingKey — Ed25519 private key
// bundle.encryptionKey — X25519 private key
// bundle.identity — { did, publicKey, created }
// Deterministic from a seed (same seed = same identity)
const bundle2 = deriveKeyBundle(masterSeed)

Key bundles are serialized, encrypted with XChaCha20-Poly1305, and stored via PasskeyStorage. In production, the encryption key should be derived from WebAuthn credentials.

import { BrowserPasskeyStorage } from '@xnet/identity'
const storage = new BrowserPasskeyStorage()
const storedKey = await storage.store(bundle, credentialId)
const recovered = await storage.retrieve(storedKey, credentialId)

The @xnet/crypto package provides all the low-level operations:

import { hash, hashHex } from '@xnet/crypto'
const digest = hash(data) // Uint8Array
const hex = hashHex(data) // hex string
const b64 = hashBase64(data) // base64url string
const sha = hash(data, 'sha256') // SHA-256 fallback
import { generateSigningKeyPair, sign, verify } from '@xnet/crypto'
const { publicKey, privateKey } = generateSigningKeyPair()
const signature = sign(message, privateKey)
const valid = verify(message, signature, publicKey) // boolean
import { generateKey, encrypt, decrypt } from '@xnet/crypto'
const key = generateKey() // 32 bytes
const encrypted = encrypt(plaintext, key) // { nonce, ciphertext }
const decrypted = decrypt(encrypted, key) // Uint8Array
import { generateKeyPair, deriveSharedSecret } from '@xnet/crypto'
const alice = generateKeyPair()
const bob = generateKeyPair()
const shared = deriveSharedSecret(alice.privateKey, bob.publicKey)
// Both sides derive the same 32-byte shared secret

UCANs (User Controlled Authorization Networks) are self-signed capability tokens for decentralized authorization. They replace traditional API keys or OAuth tokens.

interface UCANToken {
iss: string // Issuer DID
aud: string // Audience DID
exp: number // Expiration (Unix seconds)
att: UCANCapability[] // Capabilities granted
prf: string[] // Proof chain (parent tokens)
sig: Uint8Array // Ed25519 signature
}
interface UCANCapability {
with: string // Resource URI: 'xnet://doc/123' or '*'
can: string // Action: 'read', 'write', or '*'
}
import { createUCAN } from '@xnet/identity'
const token = createUCAN({
issuer: alice.did,
issuerKey: alice.privateKey,
audience: bob.did,
capabilities: [
{ with: 'xnet://doc/123', can: 'write' },
{ with: 'xnet://doc/*', can: 'read' }
],
expiration: Date.now() / 1000 + 3600, // 1 hour (default)
proofs: [] // Parent tokens for delegation
})
import { verifyUCAN, hasCapability } from '@xnet/identity'
const result = verifyUCAN(token)
if (result.valid) {
const canWrite = hasCapability(result.payload, 'xnet://doc/123', 'write')
}

Verification extracts the issuer’s public key from their DID (no external resolver), checks expiration, and verifies the Ed25519 signature.

flowchart LR
  A["Alice\n(document owner)"] -->|"grants write\n(signs UCAN)"| B["Bob"]
  B -->|"delegates read\n(cites Alice's UCAN as proof)"| C["Carol"]
  C -->|"verifies chain"| D["Alice → Bob → Carol"]

UCANs support transitive delegation via the prf (proofs) field:

// Alice grants Bob write access
const aliceToBob = createUCAN({
issuer: alice.did,
issuerKey: alice.privateKey,
audience: bob.did,
capabilities: [{ with: 'xnet://doc/123', can: 'write' }]
})
// Bob delegates read access to Carol, citing Alice's grant as proof
const bobToCarol = createUCAN({
issuer: bob.did,
issuerKey: bob.privateKey,
audience: carol.did,
capabilities: [{ with: 'xnet://doc/123', can: 'read' }],
proofs: [aliceToBob]
})

Identity is provided to the app through XNetProvider:

import { XNetProvider } from '@xnet/react'
;<XNetProvider
config={{
authorDID: identity.did,
signingKey: privateKey,
identity: identity
// ...
}}
>
<App />
</XNetProvider>

The useIdentity hook exposes identity state to components:

import { useIdentity } from '@xnet/react'
function Profile() {
const { identity, isAuthenticated, did } = useIdentity()
if (!isAuthenticated) return <LoginScreen />
return <p>Signed in as {did}</p>
}
flowchart TD
  A["XNetProvider\n(authorDID + signingKey)"] --> B["NodeStore"]
  A --> C["SyncManager"]
  B --> D["mutate.create() / update()"]
  D --> E["Ed25519 sign Change‹T›"]
  C --> F["BSM\n(Background Sync Manager)"]
  F --> G["signYjsUpdate()\nSignedYjsEnvelope"]
  F --> H["ClientID attestation\n(clientId → DID binding)"]
  G --> I["Broadcast to peers"]
  I --> J["Remote peer\nverify all signatures"]
  1. XNetProvider creates a NodeStore with authorDID and signingKey
  2. Every mutate.create() / mutate.update() call signs the change with Ed25519
  3. SyncManager receives the signing credentials and passes them to the BSM
  4. Yjs updates are signed as SignedYjsEnvelope before broadcast
  5. ClientID attestations bind the Yjs clientID to the DID
  6. Remote peers verify all signatures before applying changes