Ed25519 Device Authentication

OpenClaw uses Ed25519 public-key cryptography for device authentication across Gateway, Control UI, and Native Apps.

Key Files

  • src/infra/device-identity.ts (183 lines): Core identity management
  • src/shared/device-auth.ts (31 lines): Auth data structures
  • src/pairing/pairing-store.ts (621 lines): Pairing flow implementation

Authentication Flow

Key Generation and Derivation

Device Identity

// From src/infra/device-identity.ts
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
const deviceId = crypto.createHash("sha256")
  .update(derivePublicKeyRaw(publicKeyPem))
  .digest("hex");

Key Format Constant

// ED25519 SPKI prefix for key format detection
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");

This prefix is used to extract the raw 32-byte Ed25519 public key from the SPKI format.

Pairing Flow

Pairing Code Generation

  • Code length: 8 characters
  • Alphabet: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (excludes 0, O, 1, I to avoid confusion)
  • TTL: 60 minutes (3600000 ms)
  • Max pending: 3 codes per channel/account

From src/pairing/pairing-store.ts:

const PAIRING_CODE_LENGTH = 8;
const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
const PAIRING_PENDING_TTL_MS = 60 * 60 * 1000;
const PAIRING_PENDING_MAX = 3;

Pairing Process

  1. Request Pairing:

    • Device calls upsertChannelPairingRequest({ channel, id, accountId, meta })
    • System generates unique 8-character code
    • Code stored with 60-minute expiration
  2. User Approval:

    • User enters code via control UI
    • System calls approveChannelPairingCode({ channel, code, accountId })
    • On match: adds device to allowlist, removes code from pending
  3. Token Issuance:

    • Gateway issues token with role and scopes
    • Token stored in device auth store

Challenge-Response Signing

Signing

// Device signs challenge payload
function signDevicePayload(privateKeyPem: string, payload: string): string {
  const key = crypto.createPrivateKey(privateKeyPem);
  const sig = crypto.sign(null, Buffer.from(payload, "utf8"), key);
  return base64UrlEncode(sig);
}

Verification

// Gateway verifies signature
function verifyDeviceSignature(
  publicKey: string,
  payload: string,
  signatureBase64Url: string
): boolean {
  const key = crypto.createPublicKey(publicKey);
  const sig = base64UrlDecode(signatureBase64Url);
  return crypto.verify(null, Buffer.from(payload, "utf8"), key, sig);
}

Security Properties

  • Device identity is deterministic: deviceId = SHA256(publicKey) ensures same device always has same ID
  • Private key never leaves device: Only signatures are transmitted
  • Challenge-response prevents replay: Each authentication uses a fresh challenge
  • Time-limited pairing: Codes expire after 60 minutes
  • Limited pending codes: Max 3 pending codes per channel/account prevents abuse

Cross-References