-
Notifications
You must be signed in to change notification settings - Fork 5
Device Identity and Signing
Every TitanX install has a unique cryptographic identity — an Ed25519 keypair minted on first launch, stored encrypted in the OS keychain. This identity is used for:
- Per-row signing of audit log entries
- JWT signing for fleet enrollment (slave side)
- Envelope signing for
agent.executeand destructive commands (master side) - Cross-device trust verification
On the very first app launch after install:
- TitanX generates a fresh Ed25519 keypair using Node's
crypto.generateKeyPairSync('ed25519') - Private key is wrapped with the OS master key (derived from keychain/secret service/DPAPI)
- Wrapped private key + public key stored in SQLite's
device_identitytable - Row is marked
is_primary = 1 - Device ID (UUID) + public key fingerprint also surfaced in the About dialog
Every use:
- TitanX reads wrapped private key from SQLite
- Requests OS master key from keychain (
Security.frameworkon macOS, libsecret on Linux, DPAPI on Windows) - Unwraps private key in memory
- Uses for the signing operation
- Discards the unwrapped key immediately
The unwrapped private key is never persisted in memory between uses. Typical lifetime: microseconds.
Settings → Advanced → Rotate device identity (admin reauth required):
- Generate new Ed25519 keypair
- Mark old key as
is_primary = 0 - Set new key as
is_primary = 1 - Write audit entry
device.identity.rotatedsigned by both old and new keys (the last row signed by old key is the handoff point)
After rotation:
- New audit rows use new key
- Old audit rows are still verifiable against old key (it's still in SQLite, just non-primary)
- If you're a slave: fleet re-enrollment required — master still trusts the old key; you need to tell master about the new one
- Suspected compromise
- Compliance-required periodic rotation
- Key destruction on device decommission
CREATE TABLE device_identity (
id TEXT PRIMARY KEY,
public_key TEXT NOT NULL, -- base64 Ed25519 public key
private_key_wrapped TEXT NOT NULL, -- AES-256-GCM-wrapped private key
is_primary INTEGER NOT NULL, -- 1 = active, 0 = archived
created_at INTEGER NOT NULL,
rotated_at INTEGER
);Only one row has is_primary = 1 at a time. Rotations preserve old rows for verification.
The master key that unwraps the private key lives in the OS keychain:
| OS | Storage mechanism |
|---|---|
| macOS |
Security.framework — Keychain entry titanx-master-<deviceId>
|
| Linux | libsecret — application entry |
| Windows | DPAPI — user profile-scoped |
The app uses electron's safeStorage abstraction, which picks the right mechanism per OS.
Every activity_log row is signed:
const signature = ed25519.sign(rowHash, privateKey);
row.device_signature = base64(signature);Verification (master side or forensic):
const ok = ed25519.verify(row.device_signature, rowHash, device.public_key);Any third-party with the device's public key can verify. See Audit Logging.
When master dispatches a destructive command:
const envelope = { targetDeviceId, commandType, params, nonce, createdAt, ttl };
envelope.signature = base64(ed25519.sign(canonicalJson(envelope), masterPrivateKey));Slave verifies using the master public key it learned during enrollment:
const ok = ed25519.verify(envelope.signature, canonicalJson(envelope), masterPublicKey);When slave replies to a command or pushes telemetry:
const ack = { commandId, deviceId, ackStatus, ackedAt };
ack.signature = base64(ed25519.sign(canonicalJson(ack), slavePrivateKey));Master verifies using the slave's public key (stored at enrollment).
When a slave enrolls, master issues a JWT using EdDSA (Ed25519 JWT):
{
"alg": "EdDSA",
"typ": "JWT"
}
.
{
"iss": "titanx-master",
"sub": "<deviceId>",
"role": "workforce|farm",
"iat": ...,
"exp": ... (1 year default)
}
.
ed25519.sign(header + "." + payload, masterPrivateKey)
Slave includes as Authorization: Bearer <jwt> on every request. Master verifies on every request using its own public key.
The enrollment handshake is a mutual Ed25519 public-key exchange:
Slave Master
----- ------
Generate keypair (if not existing)
POST /enroll {
token, deviceId, slavePublicKey
} →
Verify token
Store slavePublicKey against deviceId
Issue JWT signed with masterPrivateKey
← { jwt, masterPublicKey, ... }
Store masterPublicKey
Store JWT
After enrollment:
- Master knows
slave.publicKey(can verify slave's signed acks + telemetry) - Slave knows
master.publicKey(can verify master's signed bundles + commands) - Both have JWT for transport-level auth
No shared secrets. Compromise of one doesn't compromise the other.
- About dialog → device ID + public key fingerprint displayed
- Governance → Audit Log → Verify signatures → verify recent rows
TitanX ships a tiny verification utility:
./titanx verify-signature --row-id <uuid> --public-key <base64>Useful for external audits — export a row, verify on an isolated machine with the public key from a known-good source.
Master's Dashboard → [device] → verify signatures on exported audit log from that device. Confirms rows actually originated from the device.
Hardware-backed attestation (Secure Enclave on macOS, TPM on Linux/Windows) is roadmap for v2.7+. Today TitanX uses software-backed keys wrapped with OS keychain — strong, but not TPM-grade.
For HSM-level requirements, the SDK allows pluggable key backends; contact maintainers for enterprise integration.
- Offline audit-log tampering — HMAC chain + signatures detect
- Forged fleet commands — signature verification fails
- Command replay — nonce registry rejects duplicates
- Slave impersonation — master's public-key bound to deviceId at enrollment
- OS compromise — if the keychain is unlocked and the attacker can read process memory, they can extract the unwrapped private key during its brief in-memory lifetime
- Cold-boot memory dumps — possible attack surface on specific hardware
- Quantum computers — Ed25519 is not post-quantum-safe. Roadmap item for v3.x.
If the OS keychain master key is lost (reinstall OS, migrate user, keychain corruption):
-
device_identity.private_key_wrappedcan't be unwrapped - Device effectively loses its identity
Recovery paths:
- Backup OS keychain before migration — standard macOS/Linux practice
- Re-enroll as a new device — generate new identity; old audit log rows are still verifiable against the archived public key
No "master recovery key" exists by design — recovery would imply a backdoor.
- Back up master's DB encrypted — contains all slave public keys
- Rotate master's identity annually — or on any suspicion
- Document which device enrolled when — match against the device roster during audits
- Verify signatures during incident review — don't trust audit rows at face value
- Security Model — where identity fits in the bigger picture
- Audit Logging — per-row signing mechanism
- Slave Enrollment — where key exchange happens
- Secrets Management — related but separate vault mechanism
TitanX · Enterprise AI Agent Orchestration · Apache-2.0
Docs: Wiki · Technical docs · Releases · Security
Last updated for v2.5.1 — report doc issue · contribute to the wiki
📖 Getting Started
🧩 Core Concepts
- Architecture Overview
- Agents and Teams
- Agent Gallery and Templates
- ACP Runtimes
- MCP Servers
- Workspaces
- Reasoning Bank
👤 End-User Guides
- Hiring Agents from the Gallery
- The Sprint Board
- Conversations and Chat UI
- Using Custom Assistants
- Skills Hub
- Cron and Scheduled Tasks
- Observability
- Caveman Mode
🌐 Fleet Mode
- Fleet Mode Overview
- Master Setup Guide
- Slave Enrollment
- Agent Farm Setup
- Publishing Agent Templates
- Command Center
- Device Forensics and Revocation
🌙 Dream Mode
- Dream Mode Overview
- Enabling Dream Mode
- Dream Pass Internals
- Consolidated Learnings Dashboard
- Privacy and Redaction
🔒 Security
- Security Model
- IAM Policies
- Audit Logging
- Device Identity and Signing
- Secrets Management
- Compliance and Data Residency
🛠 Developer
- Development Setup
- Project Structure
- Code Conventions
- Testing
- Adding an ACP Runtime
- Adding an MCP Server
- Pull Request Workflow
📘 Reference
- Configuration Keys
- Environment Variables
- IPC Channels
- Database Schema
- Fleet Command Types
- Telemetry Shape
- CLI and Keyboard Shortcuts
❓ Help
🔗 Outside the wiki
v2.5.1 · 50+ pages · Contribute