Skip to content

Trust Pairing

Trust pairing establishes cryptographic trust between mesh nodes in Adaptive Sentience. This enables secure communication in untrusted networks, prevents man-in-the-middle attacks, and provides the foundation for execution verification.


Overview

Problem: In distributed edge computing, nodes need to communicate securely without relying on centralized certificate authorities or trusted network infrastructure. Field operations often occur in hostile or untrusted networks.

Solution: Adaptive Sentience supports two pairing modes:

  1. TOFU (Trust On First Use): Automatic pairing on first contact - simple, zero-config
  2. PKI (Public Key Infrastructure): Explicit key exchange via QR codes or certificate authority - secure, verifiable

Core Principles

1. Cryptographic Identity

Every node has a unique cryptographic identity based on Ed25519 public key cryptography:

# Node identity
{
  "node_id": "local:abc123",
  "public_key": "<base64-encoded Ed25519 public key>",
  "public_key_fingerprint": "sha256:a1b2c3d4e5f67890"
}

The public key fingerprint is a SHA256 hash of the public key, providing a human-verifiable identifier.

2. Trust Store

Each node maintains a persistent trust store with:

  • Trusted nodes: Nodes that have been explicitly paired
  • Blocked nodes: Nodes that have been explicitly rejected
{
  "version": 1,
  "trusted": {
    "local:abc123": {
      "public_key": "...",
      "public_key_fingerprint": "sha256:a1b2c3d4",
      "label": "Alice's Phone",
      "added_at": "2024-01-15T10:00:00Z"
    }
  },
  "blocked": {
    "local:def456": {
      "public_key_fingerprint": "sha256:e5f6g7h8",
      "reason": "Compromised device",
      "blocked_at": "2024-01-15T11:00:00Z"
    }
  }
}

3. Zero-Trust Communication

Even after pairing, nodes verify signatures on every message:

Message → Verify sender signature → Check trust store → Process

This prevents: - Message tampering - Impersonation attacks - Replay attacks (via nonces)


TOFU (Trust On First Use)

What is TOFU?

Trust On First Use automatically trusts nodes on first contact, similar to SSH's default behavior.

Advantages: - Zero configuration - Automatic mesh formation - Perfect for development and trusted networks

Disadvantages: - Vulnerable to man-in-the-middle on first contact - No verification of node identity - Not suitable for hostile networks

TOFU Workflow

1. Gateway discovers new node via UDP multicast
2. Gateway connects to node's HTTP endpoint
3. Node presents public key and identity bundle
4. Gateway automatically adds node to trust store
5. Future messages are verified against stored public key

Enabling TOFU Mode

Gateway TOFU mode:

python -m gateway.http_gateway \
  --host 127.0.0.1 \
  --port 8787 \
  --tofu

Edge node TOFU mode:

cd edge_node
TOFU_MODE=true NODE_PORT=8000 python3 node.py

TOFU Security Model

TOFU provides security after first contact:

Threat Protected? Reason
MITM on first contact No verification of initial identity
MITM after pairing Signatures verified against stored key
Message tampering Ed25519 signatures
Replay attacks Nonces and timestamps
Impersonation Cryptographic signatures

TOFU Security

TOFU is vulnerable to man-in-the-middle attacks during initial pairing. Only use TOFU in trusted networks or for development.


PKI-Based Pairing

What is PKI Pairing?

PKI-based pairing requires explicit verification of node identity before trust is granted.

Advantages: - Secure against man-in-the-middle attacks - Explicit user consent - Suitable for hostile networks - Audit trail of pairing decisions

Disadvantages: - Manual pairing step - Requires user interaction - More complex setup

PKI Pairing Workflow

QR Code Pairing

The most common PKI pairing method:

1. Node generates identity bundle with QR code
2. User scans QR code with gateway/other node
3. Gateway verifies bundle and prompts user
4. User approves pairing
5. Node added to trust store

Example QR code pairing:

On Edge Node:

# Start edge node with pairing server
cd edge_node
NODE_PORT=8000 python3 node.py --pairing-server

The node displays a QR code containing:

{
  "node_id": "local:abc123",
  "public_key": "MCowBQYDK2VwAyEA1a2b3c4d5e6f7g8h9i0j...",
  "public_key_fingerprint": "sha256:a1b2c3d4e5f67890",
  "node_type": "android",
  "http_url": "http://192.168.1.100:8000",
  "x25519_pubkey": "...",
  "generated_at": "2024-01-15T10:00:00Z"
}

On Gateway:

# Scan QR code and add to trust store
curl -X POST http://localhost:8787/v1/trust/add \
  -H "Content-Type: application/json" \
  -d '{
    "node_id": "local:abc123",
    "public_key": "MCowBQYDK2VwAyEA1a2b3c4d5e6f7g8h9i0j...",
    "public_key_fingerprint": "sha256:a1b2c3d4e5f67890",
    "label": "Alice Phone"
  }'

Identity Bundle Structure

{
  "node_id": "local:abc123",
  "public_key": "<base64 Ed25519 public key>",
  "public_key_fingerprint": "sha256:<first 16 chars of SHA256 hash>",
  "node_type": "android|macos|raspberry_pi|linux",
  "http_url": "http://192.168.1.100:8000",
  "x25519_pubkey": "<base64 X25519 public key for encryption>",
  "generated_at": "2024-01-15T10:00:00Z"
}

Field descriptions:

Field Description
node_id Unique node identifier
public_key Ed25519 public key for signatures (base64)
public_key_fingerprint SHA256 hash for human verification
node_type Platform type
http_url Node's HTTP endpoint
x25519_pubkey X25519 key for message encryption (optional)
generated_at Bundle creation timestamp

Bundle Validation

When receiving an identity bundle, validate it:

import hashlib
import base64

def validate_bundle(bundle):
    """Validate identity bundle structure and cryptography.

    Raises:
        ValueError: If bundle is invalid
    """
    # Check required fields
    required = ["node_id", "public_key", "public_key_fingerprint", "node_type"]
    for field in required:
        if field not in bundle:
            raise ValueError(f"Missing required field: {field}")

    # Validate public key is base64
    try:
        public_key_bytes = base64.b64decode(bundle["public_key"])
        if len(public_key_bytes) != 32:  # Ed25519 is 32 bytes
            raise ValueError(f"Invalid public key length: {len(public_key_bytes)}")
    except Exception as e:
        raise ValueError(f"Invalid public key encoding: {e}")

    # Validate fingerprint format
    fingerprint = bundle["public_key_fingerprint"]
    if not fingerprint.startswith("sha256:"):
        raise ValueError(f"Invalid fingerprint format: {fingerprint}")

    # Verify fingerprint matches public key
    expected_hash = hashlib.sha256(public_key_bytes).hexdigest()
    expected_fp = f"sha256:{expected_hash[:16]}"
    if fingerprint != expected_fp:
        raise ValueError(
            f"Fingerprint mismatch. Expected: {expected_fp}, Got: {fingerprint}"
        )

    return True

Trust Store Management

Trust Store Location

The trust store is a JSON file stored on disk:

# Default location
./trust_store.json

# Custom location via environment variable
export DWO_TRUST_STORE_PATH=/path/to/trust_store.json

Trust Store Structure

{
  "version": 1,
  "trusted": {
    "<node_id>": {
      "public_key": "<base64>",
      "public_key_fingerprint": "sha256:...",
      "label": "Human-readable label",
      "added_at": "2024-01-15T10:00:00Z",
      "x25519_pubkey": "<base64>",
      "http_url": "http://..."
    }
  },
  "blocked": {
    "<node_id>": {
      "public_key_fingerprint": "sha256:...",
      "reason": "Reason for blocking",
      "blocked_at": "2024-01-15T11:00:00Z"
    }
  }
}

Adding Trusted Nodes

Via API:

curl -X POST http://localhost:8787/v1/trust/add \
  -H "Content-Type: application/json" \
  -d '{
    "node_id": "local:abc123",
    "public_key": "MCowBQYDK2VwAyEA...",
    "public_key_fingerprint": "sha256:a1b2c3d4",
    "label": "Alice Phone"
  }'

Via Python:

from trust.store import TrustStore

store = TrustStore()

store.add_trusted(
    node_id="local:abc123",
    public_key="MCowBQYDK2VwAyEA...",
    public_key_fingerprint="sha256:a1b2c3d4",
    label="Alice Phone"
)

Blocking Nodes

Via API:

curl -X POST http://localhost:8787/v1/trust/block \
  -H "Content-Type: application/json" \
  -d '{
    "node_id": "local:abc123",
    "public_key_fingerprint": "sha256:a1b2c3d4",
    "reason": "Compromised device"
  }'

Via Python:

store.block_node(
    node_id="local:abc123",
    fingerprint="sha256:a1b2c3d4",
    reason="Compromised device"
)

Listing Trust Status

curl http://localhost:8787/v1/trust/list

Response:

{
  "trusted": [
    {
      "node_id": "local:abc123",
      "label": "Alice Phone",
      "public_key_fingerprint": "sha256:a1b2c3d4",
      "added_at": "2024-01-15T10:00:00Z"
    }
  ],
  "blocked": [
    {
      "node_id": "local:def456",
      "public_key_fingerprint": "sha256:e5f6g7h8",
      "reason": "Compromised device",
      "blocked_at": "2024-01-15T11:00:00Z"
    }
  ]
}

Certificate Management

Generating Node Identity

Each node generates an Ed25519 keypair on first boot:

from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import base64

# Generate Ed25519 keypair
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()

# Serialize to bytes
private_bytes = private_key.private_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PrivateFormat.Raw,
    encryption_algorithm=serialization.NoEncryption()
)

public_bytes = public_key.public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)

# Encode as base64
public_key_b64 = base64.b64encode(public_bytes).decode()

# Compute fingerprint
import hashlib
fingerprint_hash = hashlib.sha256(public_bytes).hexdigest()
fingerprint = f"sha256:{fingerprint_hash[:16]}"

print(f"Public Key: {public_key_b64}")
print(f"Fingerprint: {fingerprint}")

Storing Private Keys

Development:

# Stored in plaintext file
./edge_node/identity.json

Production:

  • Android: Android Keystore (hardware-backed)
  • iOS: Secure Enclave
  • Linux/macOS: Encrypted file with OS keychain
  • Raspberry Pi: TPM module (if available)

Key Rotation

To rotate a node's keys:

  1. Generate new keypair
  2. Generate new identity bundle
  3. Re-pair with all trusted nodes
  4. Revoke old public key
# Generate new keypair
new_identity = NodeIdentity.generate()

# Get new bundle
new_bundle = get_node_bundle(
    identity=new_identity,
    node_id="local:abc123",
    node_type="android"
)

# Broadcast new bundle to mesh
# (Nodes will update trust store with new public key)

Cryptographic Operations

Signing Messages

Nodes sign all outgoing messages with their private key:

from cryptography.hazmat.primitives.asymmetric import ed25519
import base64
import json

def sign_message(private_key, message):
    """Sign a message with Ed25519 private key.

    Args:
        private_key: Ed25519PrivateKey
        message: Dict to sign

    Returns:
        Signature (base64)
    """
    # Serialize message to canonical JSON
    message_bytes = json.dumps(message, sort_keys=True).encode()

    # Sign
    signature = private_key.sign(message_bytes)

    # Encode as base64
    return base64.b64encode(signature).decode()

Verifying Signatures

Nodes verify incoming messages against the sender's public key:

def verify_signature(public_key, message, signature):
    """Verify Ed25519 signature.

    Args:
        public_key: Ed25519PublicKey
        message: Dict that was signed
        signature: Signature (base64)

    Returns:
        True if valid, False otherwise
    """
    try:
        # Serialize message to canonical JSON
        message_bytes = json.dumps(message, sort_keys=True).encode()

        # Decode signature
        signature_bytes = base64.b64decode(signature)

        # Verify
        public_key.verify(signature_bytes, message_bytes)
        return True

    except Exception:
        return False

Message Envelope

All mesh messages use a signed envelope:

{
  "message_id": "msg:a1b2c3d4",
  "sender_node_id": "local:abc123",
  "recipient_node_id": "local:def456",
  "timestamp": "2024-01-15T10:00:00Z",
  "payload_type": "TASK_REQUEST",
  "encrypted_payload": "<base64 encrypted payload>",
  "signature": "<base64 Ed25519 signature>"
}

Signature covers:

{
  "message_id": "msg:a1b2c3d4",
  "sender_node_id": "local:abc123",
  "recipient_node_id": "local:def456",
  "timestamp": "2024-01-15T10:00:00Z",
  "payload_type": "TASK_REQUEST",
  "encrypted_payload": "<base64>"
}

The signature does NOT cover itself (obviously), but covers all other fields.

Encryption (Optional)

For message confidentiality, nodes can encrypt payloads using X25519 Diffie-Hellman:

# Sender
shared_secret = sender_x25519_private.exchange(recipient_x25519_public)
encrypted_payload = encrypt_aes_gcm(payload, shared_secret)

# Recipient
shared_secret = recipient_x25519_private.exchange(sender_x25519_public)
decrypted_payload = decrypt_aes_gcm(encrypted_payload, shared_secret)

This is optional - signatures alone provide integrity and authenticity.


Security Considerations

1. Fingerprint Verification

When pairing via QR code, users should verify fingerprints through a second channel:

QR Code: sha256:a1b2c3d4e5f67890
Phone Call: "My fingerprint is alpha-one-bravo-two..."

This prevents QR code tampering.

2. Key Compromise

If a node's private key is compromised:

  1. Block the node:

    curl -X POST http://localhost:8787/v1/trust/block \
      -d '{"node_id": "local:abc123", ...}'
    

  2. Rotate keys on the affected device

  3. Re-pair with new public key

3. Trust Store Backup

Back up the trust store regularly:

cp trust_store.json trust_store.backup.json

4. Trust Store Synchronization

In multi-gateway deployments, synchronize trust stores:

# Export from gateway 1
curl http://gateway1:8787/v1/trust/export > trust_export.json

# Import to gateway 2
curl -X POST http://gateway2:8787/v1/trust/import \
  -d @trust_export.json

5. Audit Trust Changes

Log all trust store changes:

# In TrustStore class
def add_trusted(self, node_id, ...):
    # Add to store
    self._data["trusted"][node_id] = entry
    self._save()

    # Audit log
    audit_logger.log_custom("trust_added", {
        "node_id": node_id,
        "fingerprint": public_key_fingerprint,
        "label": label
    })

Pairing Workflows

Workflow 1: QR Code Pairing (Manual)

Scenario: Pair a new Android phone with the gateway.

Steps:

  1. On Android phone:

    # Start edge node with pairing server
    cd edge_node
    NODE_PORT=8000 python3 node.py --pairing-server
    

  2. Phone displays QR code with identity bundle

  3. On gateway laptop:

  4. Open camera app
  5. Scan QR code
  6. Copy JSON from QR code

  7. Add to trust store:

    curl -X POST http://localhost:8787/v1/trust/add \
      -H "Content-Type: application/json" \
      -d '<JSON from QR code>'
    

  8. Verify pairing:

    curl http://localhost:8787/v1/trust/list
    

Workflow 2: Automatic TOFU Pairing

Scenario: Development environment with trusted network.

Steps:

  1. Start gateway in TOFU mode:

    python -m gateway.http_gateway --tofu
    

  2. Start edge node:

    cd edge_node
    NODE_PORT=8000 python3 node.py
    

  3. Nodes automatically pair on first contact

  4. Verify pairing:

    curl http://localhost:8787/v1/mesh_scan
    

Workflow 3: Certificate Authority (Future)

Scenario: Enterprise deployment with PKI infrastructure.

Steps (not yet implemented):

  1. Generate CSR on edge node
  2. Submit CSR to internal CA
  3. Receive signed certificate
  4. Import certificate to node
  5. Gateway validates against CA root certificate

Trust Status in API Responses

Mesh Scan with Trust Status

curl http://localhost:8787/v1/mesh_scan

Response:

{
  "nodes": [
    {
      "node_id": "local:abc123",
      "node_type": "android",
      "http_url": "http://192.168.1.100:8000",
      "public_key_fingerprint": "sha256:a1b2c3d4",
      "verified": true,
      "trust_status": "trusted"  // trusted | blocked | unknown
    },
    {
      "node_id": "local:def456",
      "trust_status": "unknown"
    },
    {
      "node_id": "local:ghi789",
      "trust_status": "blocked"
    }
  ]
}

Tool Catalog with Trust Status

curl http://localhost:8787/v1/catalog

Response:

{
  "mesh": [
    {
      "node_id": "local:abc123",
      "trust_status": "trusted",
      "tools": [...]
    }
  ]
}

Advanced Topics

Multi-Hop Trust

In some scenarios, nodes need to trust transitively:

Gateway → trusts → Node A → trusts → Node B

Should Gateway trust Node B?

Current behavior: No transitive trust. Each node must be explicitly paired.

Future: Support for trust delegation with constraints.

Reputation Systems

Future versions may include reputation tracking:

{
  "node_id": "local:abc123",
  "trust_status": "trusted",
  "reputation": {
    "uptime": 0.995,
    "successful_tasks": 1000,
    "failed_tasks": 5,
    "avg_latency_ms": 150
  }
}

Hardware Security Modules

For high-security deployments, store private keys in HSMs:

  • YubiKey: FIDO2/U2F for key storage
  • TPM 2.0: Trusted Platform Module on servers
  • Secure Element: On mobile devices

Troubleshooting

Node Not Trusted

Symptom: Gateway rejects requests from edge node.

Solution:

  1. Check trust status:

    curl http://localhost:8787/v1/trust/list
    

  2. Add node to trust store:

    curl -X POST http://localhost:8787/v1/trust/add -d '{...}'
    

  3. Verify fingerprint matches node's fingerprint

Signature Verification Failed

Symptom: "Signature verification failed" error.

Solutions:

  1. Check clock sync: Nodes must have synchronized clocks (use NTP)
  2. Verify public key: Ensure trust store has correct public key
  3. Check for key rotation: Node may have rotated keys

QR Code Won't Scan

Symptom: QR code scanner fails to read bundle.

Solutions:

  1. Increase QR size: Generate larger QR code
  2. Reduce bundle size: Remove optional fields
  3. Use manual entry: Copy/paste JSON instead of scanning

Trust Store Corrupted

Symptom: Cannot load trust store.

Solutions:

  1. Restore from backup:

    cp trust_store.backup.json trust_store.json
    

  2. Reset trust store (loses all pairings):

    rm trust_store.json
    # Restart gateway to create fresh store
    


Best Practices

1. Use PKI in Production

Always use PKI-based pairing in production:

# Good
python -m gateway.http_gateway --host 0.0.0.0 --port 8787

# Bad (for production)
python -m gateway.http_gateway --host 0.0.0.0 --port 8787 --tofu

2. Label Nodes Clearly

Use descriptive labels:

# Good
store.add_trusted(
    node_id="local:abc123",
    label="Alice iPhone - Sales Team"
)

# Bad
store.add_trusted(
    node_id="local:abc123",
    label="node123"
)

3. Regular Trust Audits

Review trust store regularly:

# List all trusted nodes
curl http://localhost:8787/v1/trust/list

# Remove unused nodes
curl -X POST http://localhost:8787/v1/trust/remove \
  -d '{"node_id": "local:old_device"}'

4. Backup Trust Store

Automated backups:

# Cron job
0 2 * * * cp /path/to/trust_store.json /backup/trust_store.$(date +\%Y\%m\%d).json

5. Monitor Pairing Events

Alert on unexpected pairing events:

# Log all pairing events
def add_trusted(self, node_id, ...):
    # ... add to store ...

    # Alert if unexpected
    if node_id not in expected_nodes:
        alert_security_team(f"Unexpected node paired: {node_id}")

Reference

TrustStore Python Class

class TrustStore:
    """Persistent store for trusted and blocked nodes."""

    def __init__(self, store_path: Optional[str] = None)

    def add_trusted(
        self,
        node_id: str,
        public_key: str,
        public_key_fingerprint: str,
        label: Optional[str] = None
    ) -> bool

    def block_node(
        self,
        node_id: str,
        fingerprint: str,
        reason: Optional[str] = None
    ) -> bool

    def is_trusted(
        self,
        node_id: str,
        fingerprint: Optional[str] = None
    ) -> bool

    def is_blocked(self, node_id: str) -> bool

    def get_trust_status(
        self,
        node_id: str,
        fingerprint: Optional[str] = None
    ) -> Literal["trusted", "blocked", "unknown"]

    def list_all(self) -> Dict[str, Any]

Trust API Endpoints

Endpoint Method Description
/v1/trust/bundle GET Get local node identity bundle
/v1/trust/add POST Add trusted node
/v1/trust/remove POST Remove trusted node
/v1/trust/block POST Block node
/v1/trust/unblock POST Unblock node
/v1/trust/list GET List all trusted/blocked nodes
/v1/trust/status/:node_id GET Get trust status for specific node

Next Steps


Further Reading