Loading...
private.me Docs
Get Trust
PRIVATE.ME · Technical White Paper

Trust: Key Verification & Transparency

Key verification, TOFU trust management, key rotation with continuity proofs, and append-only transparency logs. Trust implements the full trust lifecycle for contact public keys: Trust-on-First-Use for initial acceptance, mutual verification via QR codes and Short Authentication Strings, RSA-PSS continuity proofs for rotation, and hash-chained tamper-evident key history. All operations use the Web Crypto API.

v0.1.0 Tests passing Building Block 0 npm deps SHA-256 fingerprints Dual ESM/CJS
Section 01

Executive Summary

Trust solves the critical problem of key management in end-to-end encrypted messaging: how to verify that a contact's public key is authentic and detect when keys change unexpectedly.

Traditional messaging systems fail in two ways. They either trust keys blindly on first use without verification (vulnerable to MITM attacks), or they require out-of-band verification for every contact (poor UX that most users skip). Trust implements a graduated approach: Trust-on-First-Use (TOFU) for initial key acceptance with automatic warnings on unexpected changes, mutual verification ceremonies using QR codes and Short Authentication Strings to upgrade contacts to "verified" status, and cryptographic continuity proofs for authorized key rotations.

An append-only transparency log with SHA-256 hash chaining creates a tamper-evident audit trail. Every key event — registration, rotation, verification, revocation — is logged with a cryptographic link to its predecessor. Log corruption is detectable. Key rotation uses RSA-PSS signatures: the old private key signs the new public key, proving authorization. A configurable grace period (default: 7 days) allows both keys during transition.

Zero configuration out of the box. Zero npm runtime dependencies. Runs anywhere the Web Crypto API is available. Dual ESM and CJS builds ship in a single package.

Section 02

The Problem

Public key cryptography solves confidentiality but introduces a new problem: how do you know this key belongs to the person you think it does?

The Key Distribution Problem

Alice wants to send an encrypted message to Bob. She needs Bob's public key. But where does she get it? If an attacker Mallory intercepts the key exchange, Mallory can substitute their own public key. Alice encrypts to Mallory's key, thinking it's Bob's. The encryption is perfect — but the conversation is compromised from the start. Encryption protects the channel. It doesn't authenticate the endpoints.

Current Approaches Fail

Most messaging apps use Trust-on-First-Use (TOFU): blindly accept the first key seen for each contact. This works until keys change. When Bob rotates his key, Alice has no way to know if the new key is legitimately Bob's or an attacker's. Signal displays a "safety number" for verification, but studies show fewer than 1% of users verify. The UX friction is too high.

Certificate Authorities (CAs) solve this for HTTPS but require centralized trust hierarchies. If a CA is compromised or coerced, the entire trust model collapses. CA-based PKI doesn't work for peer-to-peer messaging where no central authority exists.

What's Missing

There is no standard implementation that combines TOFU's ease of use with opt-in verification for high-value contacts, cryptographic continuity proofs for key rotation, and a tamper-evident transparency log. Trust fills this gap. It implements the full trust lifecycle as a reusable building block.

Security Reality
Getting key verification wrong means every encrypted message that follows is compromised. Encryption is useless if you're encrypting to the wrong key. Trust solves the "whose key is this?" problem before it becomes a breach.
Section 03

Use Cases

Trust is a building block for any application that exchanges cryptographic keys. Primary markets: secure messaging, email clients, M2M communication, enterprise key management.

📧
Secure Messaging
End-to-End Encrypted Email

Email clients implementing end-to-end encryption need to manage recipient public keys. Trust provides TOFU for initial key exchange, verification ceremonies for contacts you meet in person, and automatic rotation handling.

PGP replacement
🔐
Enterprise
Corporate Key Management

Organizations need to manage employee cryptographic identities at scale. Trust's transparency log provides an auditable record of all key events. Continuity proofs prevent unauthorized key replacement.

Zero Trust
🤖
Machine-to-Machine
IoT Device Identity

IoT devices and microservices exchange signed messages. Trust manages device key lifecycles, detects unexpected key changes (indicating device compromise), and logs all rotation events.

ACI identity
⚖️
Legal / Healthcare
High-Compliance Communications

Legal and healthcare communications require cryptographic proof of authenticity. Trust's hash-chained transparency log provides tamper-evident audit trails for compliance and forensic investigation.

HIPAA / Attorney-Client
🏦
Finance
Trading Desk Authentication

Financial institutions need cryptographic authentication for trading counterparties. Trust's verification ceremonies establish high-assurance identity. Rotation grace periods ensure uninterrupted trading during key transitions.

SEC 17a-4
🛡️
Government / Defense
Classified Communications

Government agencies exchange classified information across security domains. Trust's transparency log provides cryptographic proof of key lineage. Continuity proofs prevent unauthorized key substitution.

FISMA / CMMC
Section 04

Architecture

Trust is a client-side TypeScript library with four core components: TofuManager, KeyTransparencyLog, Verification Ceremony, and KeyRotationManager.

Trust-on-First-Use (TOFU)

When a user first sees a contact's key, TofuManager.processKey() accepts it automatically and assigns "tofu" trust level. The key fingerprint is stored. On subsequent messages, if the fingerprint matches, communication proceeds normally. If the fingerprint changes, the system warns the user — potential MITM attack or legitimate rotation. The user must explicitly call acceptKeyChange() to accept the new key. This prevents silent key replacement.

TOFU Example
const log = new KeyTransparencyLog();
const tofu = new TofuManager(log);

// First time seeing alice@example.com
const fingerprint = await computeFingerprint(alicePublicKey);
const result = await tofu.processKey('alice@example.com', fingerprint);
// => { trustLevel: 'tofu', fingerprint: '...', firstSeenAt: ... }

// Later: Alice sends a message from a new key
const newFingerprint = await computeFingerprint(aliceNewKey);
const result2 = await tofu.processKey('alice@example.com', newFingerprint);
// => { ok: false, error: { code: 'FINGERPRINT_CONFLICT', ... } }

// User confirms it's legitimate (Alice rotated her key)
await tofu.acceptKeyChange('alice@example.com', newFingerprint);
// => { ok: true, value: { trustLevel: 'tofu', ... } }

Verification Ceremony

To upgrade from "tofu" to "verified" trust, both parties perform a mutual verification ceremony. createChallenge() generates a QR code and a 6-word Short Authentication String (SAS) derived from both fingerprints via HMAC-SHA256. Both parties compare the SAS phrase (e.g., "amber falcon cedar lotus bridge crimson"). If they match, the keys are authentic. If they don't, there's a MITM attack. After successful verification, upgradeTrust() marks the contact as "verified".

Verification Ceremony Example
// Initiator creates a challenge
const challenge = await createChallenge(
  'me@private.me.io', 'alice@example.com',
  myPublicKey, alicePublicKey
);

// Display QR code and SAS phrase to both users
console.log(challenge.value.shortAuthString);
// => "amber falcon cedar lotus bridge crimson"

// Both users scan QR code or read SAS aloud
// If SAS matches, keys are verified
const verified = verifyChallenge(challenge.value, userEnteredSas);
if (verified.ok) {
  await tofu.upgradeTrust('alice@example.com', fingerprint);
  // => { trustLevel: 'verified', verifiedAt: ... }
}

Transparency Log

Every key event is appended to a hash-chained transparency log. Each entry contains: contact email, public key fingerprint, action (registered/rotated/verified/revoked), timestamp, and a hash of the previous entry. Tampering with any entry breaks the hash chain. verifyChain() detects corruption by recomputing hashes and comparing to stored values.

Transparency Log Example
// Append a key registration event
await log.append('alice@example.com', fingerprint, 'key_registered');

// Verify hash chain integrity
const valid = await log.verifyChain('alice@example.com');
if (!valid.ok) {
  console.error('Log corrupted!', valid.error);
}

// Get complete history for a contact
const history = log.getHistory('alice@example.com');
// => [{ action: 'key_registered', timestamp: ..., previousEntryHash: ... }, ...]

Key Rotation

When a contact rotates their key, they must prove the new key is authorized by signing it with the old private key (RSA-PSS continuity proof). KeyRotationManager.initiateRotation() verifies the signature before accepting the new key. A grace period (default: 7 days) allows both old and new keys to decrypt messages during transition. After the grace period expires, only the new key is accepted.

Key Rotation Example
// Contact generates new key pair and signs it with old key
const newKeyPair = await generateSigningKeyPair();
const signature = await signContinuityProof(oldPrivateKey, newKeyPair.publicKey);

// Rotation manager verifies and logs the rotation
const rotationMgr = new KeyRotationManager(log, 7 * 24 * 60 * 60 * 1000);
const result = await rotationMgr.initiateRotation(
  'alice@example.com', oldPublicKey, newKeyPair.publicKey, signature
);

// During grace period, both keys are accepted
rotationMgr.isAccepted('alice@example.com', oldFingerprint); // true
rotationMgr.isAccepted('alice@example.com', newFingerprint); // true

// After grace period, only new key is accepted
rotationMgr.isAccepted('alice@example.com', oldFingerprint); // false
Section 05

Integration

Trust integrates with any messaging system that uses public key cryptography. It works alongside encryption libraries like @private.me/crypto and transport layers like @private.me/agent-sdk.

Install

npm / pnpm / yarn
pnpm add @private.me/trust

Quick Start

Complete integration example
import {
  KeyTransparencyLog,
  TofuManager,
  computeFingerprint,
  createChallenge,
  verifyChallenge,
} from '@private.me/trust';

// Initialize trust infrastructure
const log = new KeyTransparencyLog();
const tofu = new TofuManager(log);

// When receiving a message from a contact
async function handleIncomingMessage(contact, publicKey, message) {
  const fingerprint = await computeFingerprint(publicKey);
  const trustResult = await tofu.processKey(contact, fingerprint);

  if (!trustResult.ok) {
    // Key changed unexpectedly — warn user
    if (trustResult.error.code === 'FINGERPRINT_CONFLICT') {
      const userConfirmed = await showWarningDialog(contact);
      if (userConfirmed) {
        await tofu.acceptKeyChange(contact, fingerprint);
      } else {
        return; // Reject message
      }
    }
  }

  // Key is trusted — decrypt and display message
  const plaintext = await decrypt(message, myPrivateKey);
  displayMessage(contact, plaintext);
}

// When meeting a contact in person for verification
async function verifyContactInPerson(contact, theirKey) {
  const challenge = await createChallenge(
    myEmail, contact, myPublicKey, theirKey
  );

  // Display QR code and SAS phrase
  showQRCode(challenge.value.qrData);
  showSasPhrase(challenge.value.shortAuthString);

  // User compares SAS with contact
  const userEnteredSas = await promptForSas();
  const verified = verifyChallenge(challenge.value, userEnteredSas);

  if (verified.ok) {
    const fingerprint = await computeFingerprint(theirKey);
    await tofu.upgradeTrust(contact, fingerprint);
    showSuccessMessage('Contact verified!');
  } else {
    showErrorMessage('Verification failed — possible MITM attack');
  }
}

Building Blocks Integration

Trust is designed to work with other PRIVATE.ME building blocks:

  • @private.me/crypto — XorIDA threshold sharing, HMAC primitives
  • @private.me/agent-sdk (Xlink) — DID-based M2M messaging with Trust for key verification
  • @private.me/shared — Shared types (TrustLevel, KeyTransparencyEntry, etc.)
Section 06

Security

Trust is designed to resist MITM attacks, detect log tampering, and prevent unauthorized key replacement.

Threat Model

Attack Vector Defense Status
MITM on first key exchange TOFU accepts first key, warns on changes Mitigated
Silent key replacement TOFU detects fingerprint changes, requires explicit user acceptance Prevented
Forged verification SAS SAS derived via HMAC-SHA256 of both fingerprints — unilateral forgery impossible Prevented
Transparency log tampering Hash chaining — any modification breaks the chain Detectable
Unauthorized key rotation RSA-PSS continuity proof — new key must be signed by old key Prevented
Replay attacks during rotation Rotation entries timestamped and logged — replays detectable Detectable

Cryptographic Primitives

All cryptographic operations use the Web Crypto API:

  • SHA-256 — Key fingerprints (SPKI export digest), transparency log hashing
  • HMAC-SHA256 — SAS derivation (prevents unilateral forgery)
  • RSA-PSS 2048-bit — Continuity proofs for key rotation
  • crypto.getRandomValues() — All random values (challenge IDs, nonces)

Security Properties

Tamper Detection
The transparency log uses hash chaining. Each entry's previousEntryHash field contains the SHA-256 digest of its predecessor. Modifying any historical entry invalidates all subsequent hashes. verifyChain() recomputes the entire chain to detect tampering.
Continuity Proofs
Key rotation requires the old private key to sign the new public key (RSA-PSS signature). An attacker who compromises the current key cannot forge a continuity proof for a previous key they never possessed. This creates a cryptographic chain of custody for key lineage.

Trust Levels

Level Meaning How to Achieve
unknown No key seen for this contact yet Default state
tofu Key accepted on first use, warns on changes Automatic via processKey()
verified Key confirmed via in-person verification ceremony Manual via upgradeTrust() after SAS match
Section 07

Benchmarks

Trust operations are fast. Key fingerprint computation and transparency log operations complete in under 10ms on modern hardware.

<1ms
Fingerprint
<2ms
SAS derivation
<5ms
Log append
~8ms
RSA-PSS sign

Performance Characteristics

Operation Typical Time Notes
computeFingerprint() <1ms SHA-256 of SPKI-exported public key
deriveSas() <2ms HMAC-SHA256 of two fingerprints
log.append() <5ms Hash previous entry + create new entry
log.verifyChain() ~2ms per entry Scales linearly with log size
signContinuityProof() ~8ms RSA-PSS 2048-bit signature
verifyContinuityProof() ~1ms RSA-PSS verification is faster than signing

All benchmarks measured on Node.js 20 (MacBook Pro M1). Web Crypto API performance varies by browser and platform but is generally within 2x of these numbers.

Section 08

Honest Limitations

Trust solves key verification but does not solve all security problems. Here's what it does NOT do.

TOFU is Vulnerable to Active MITM on First Exchange

If an attacker intercepts the very first key exchange, TOFU will accept the attacker's key. This is the fundamental trade-off of Trust-on-First-Use: ease of use vs. absolute security. The only way to eliminate this risk is to verify keys out-of-band (verification ceremony) before sending sensitive messages. For contacts you've never met in person, TOFU is the best you can do without a centralized PKI.

Transparency Log is Client-Side Only

The transparency log in this implementation is stored locally. It provides tamper detection but not tamper resistance — an attacker with filesystem access can delete the entire log. For server-backed transparency (like Certificate Transparency), you need a separate append-only ledger service. Trust provides the log structure but not the distribution mechanism.

No Automatic Key Revocation

If a contact's private key is compromised, they can revoke it (log entry: key_revoked), but there is no mechanism to automatically propagate revocation notices to all parties who trust that key. You need a separate revocation infrastructure (CRLs, OCSP, or blockchain-based revocation). Trust logs the revocation but does not distribute it.

Verification Ceremony Requires Out-of-Band Channel

The verification ceremony assumes both parties can compare SAS phrases via an out-of-band channel (in-person, phone call, video chat). If both parties are remote and have no trusted channel, the ceremony cannot be completed. This is a fundamental limitation of all key verification schemes that don't rely on centralized CAs.

Grace Period is a Policy Decision

The default 7-day grace period for key rotation is arbitrary. Organizations may need shorter (1 day for high-security environments) or longer (30 days for large distributed systems) periods. The KeyRotationManager accepts a configurable grace period, but choosing the right value is an operational decision, not a cryptographic one.

What Trust Does NOT Do
Trust manages key verification, rotation, and transparency logs. It does NOT: encrypt/decrypt messages (use @private.me/crypto), transmit messages (use @private.me/agent-sdk), distribute revocation notices, enforce access control policies, or provide a centralized PKI. Trust is a building block, not a complete messaging system.
Advanced Topics

Full API Surface

Complete reference for all exported functions, classes, and types. Production-ready for integration.

Key Verification

computeFingerprint(publicKey: CryptoKey): Promise<string>
Compute SHA-256 fingerprint of a public key (SPKI export, hex encoded). Returns a 64-character hex string.
deriveSas(fingerprintA: string, fingerprintB: string): Promise<string>
Derive a 6-word Short Authentication String from two fingerprints via HMAC-SHA256. Uses PGP-style 256-word wordlist.
createChallenge(initiatorEmail, responderEmail, initiatorKey, responderKey): Promise<Result<VerificationChallenge, TrustError>>
Create a verification challenge with 15-minute TTL. Returns challenge ID, SAS phrase, QR data, and metadata.
verifyChallenge(challenge: VerificationChallenge, userSas: string): Result<true, TrustError>
Verify that user-entered SAS matches the challenge and that the challenge has not expired.

Transparency Log

KeyTransparencyLog.append(contactEmail, fingerprint, action, metadata?): Promise<Result<KeyTransparencyEntry, TrustError>>
Append a key event to the log with hash chain link. Actions: key_registered, key_rotated, key_verified, key_revoked.
KeyTransparencyLog.verifyChain(contactEmail): Promise<Result<true, TrustError>>
Verify hash chain integrity for all entries belonging to a contact. Returns error if chain is broken.
KeyTransparencyLog.getHistory(contactEmail): readonly KeyTransparencyEntry[]
Get all log entries for a contact in chronological order. Returns empty array if contact not found.

TOFU Manager

TofuManager.processKey(email, fingerprint): Promise<Result<ContactTrust, TrustError>>
Accept new key (TOFU), match existing key, or warn on unexpected change. Returns FINGERPRINT_CONFLICT error if key changed.
TofuManager.acceptKeyChange(email, newFingerprint): Promise<Result<ContactTrust, TrustError>>
Explicitly accept a key change after user confirmation. Resets trust level to TOFU.
TofuManager.upgradeTrust(email, fingerprint): Promise<Result<ContactTrust, TrustError>>
Upgrade contact to "verified" trust level after successful verification ceremony.

Key Rotation

generateSigningKeyPair(): Promise<CryptoKeyPair>
Generate RSA-PSS 2048-bit key pair for signing continuity proofs.
signContinuityProof(oldPrivateKey, newPublicKey): Promise<Uint8Array>
Sign new public key with old private key (RSA-PSS). Proves authorization for key rotation.
verifyContinuityProof(oldPublicKey, newPublicKey, signature): Promise<boolean>
Verify RSA-PSS continuity proof. Returns true if signature is valid.
KeyRotationManager.initiateRotation(contactEmail, oldKey, newKey, signature): Promise<Result<KeyRotationRecord, TrustError>>
Initiate key rotation with continuity proof verification. Logs rotation event and starts grace period.
KeyRotationManager.isAccepted(contactEmail, fingerprint): boolean
Check if fingerprint is current or within grace period for a contact.

Error Taxonomy

Trust uses structured errors with actionable hints. All errors follow the Result<T, E> pattern.

Code Description
VERIFICATION_EXPIRED The verification challenge has exceeded its 15-minute TTL. Create a new challenge.
VERIFICATION_FAILED The SAS phrase entered by the user does not match the expected value.
FINGERPRINT_CONFLICT A different public key fingerprint is already registered for this contact. User must explicitly accept the key change.
CONTACT_NOT_FOUND No trust record exists for the specified email address.
KEY_MISMATCH The provided fingerprint does not match the contact's current key.
CONTINUITY_BROKEN The RSA-PSS continuity proof for a key rotation is invalid. The new key was not signed by the old key.
LOG_CORRUPTED The hash chain in the transparency log is broken. An entry's previousEntryHash does not match the SHA-256 of its predecessor.

Deployment Options

🌐

SaaS Recommended

Fully managed infrastructure. Call our REST API, we handle scaling, updates, and operations.

  • Zero infrastructure setup
  • Automatic updates
  • 99.9% uptime SLA
  • Pay per use
View Pricing →
📦

SDK Integration

Embed directly in your application. Runs in your codebase with full programmatic control.

  • npm install @private.me/trust
  • TypeScript/JavaScript SDK
  • Full source access
  • Enterprise support available
Get Started →
🏢

On-Premise Enterprise

Self-hosted infrastructure for air-gapped, compliance, or data residency requirements.

  • Complete data sovereignty
  • Air-gap capable
  • Docker + Kubernetes ready
  • RBAC + audit logs included
Enterprise CLI →