Skip to content

Device Identity and Signing

Ankur Nair edited this page Apr 19, 2026 · 1 revision

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.execute and destructive commands (master side)
  • Cross-device trust verification

Lifecycle

Minting (first launch)

On the very first app launch after install:

  1. TitanX generates a fresh Ed25519 keypair using Node's crypto.generateKeyPairSync('ed25519')
  2. Private key is wrapped with the OS master key (derived from keychain/secret service/DPAPI)
  3. Wrapped private key + public key stored in SQLite's device_identity table
  4. Row is marked is_primary = 1
  5. Device ID (UUID) + public key fingerprint also surfaced in the About dialog

Key access

Every use:

  1. TitanX reads wrapped private key from SQLite
  2. Requests OS master key from keychain (Security.framework on macOS, libsecret on Linux, DPAPI on Windows)
  3. Unwraps private key in memory
  4. Uses for the signing operation
  5. Discards the unwrapped key immediately

The unwrapped private key is never persisted in memory between uses. Typical lifetime: microseconds.

Rotation

Settings → Advanced → Rotate device identity (admin reauth required):

  1. Generate new Ed25519 keypair
  2. Mark old key as is_primary = 0
  3. Set new key as is_primary = 1
  4. Write audit entry device.identity.rotated signed 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

Why rotate?

  • Suspected compromise
  • Compliance-required periodic rotation
  • Key destruction on device decommission

Storage

SQLite table

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.

OS keychain

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.


Using the identity

For audit log signing

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.

For fleet envelopes

Master-side (signing commands)

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);

Slave-side (signing acks + telemetry)

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).

For JWT minting (master)

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.


Key exchange during enrollment

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.


Verification tools

In-app

  • About dialog → device ID + public key fingerprint displayed
  • Governance → Audit Log → Verify signatures → verify recent rows

CLI

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-side

Master's Dashboard → [device] → verify signatures on exported audit log from that device. Confirms rows actually originated from the device.


Attestations (future)

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.


Threat model

Defended

  • 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

Out of scope

  • 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.

Key loss recovery

If the OS keychain master key is lost (reinstall OS, migrate user, keychain corruption):

  • device_identity.private_key_wrapped can't be unwrapped
  • Device effectively loses its identity

Recovery paths:

  1. Backup OS keychain before migration — standard macOS/Linux practice
  2. 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.


Best practices for production

  • 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

Related pages

Clone this wiki locally