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:
- TOFU (Trust On First Use): Automatic pairing on first contact - simple, zero-config
- 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:
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:
Edge node TOFU mode:
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:
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¶
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:
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:
- Generate new keypair
- Generate new identity bundle
- Re-pair with all trusted nodes
- 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:
This prevents QR code tampering.
2. Key Compromise¶
If a node's private key is compromised:
-
Block the node:
-
Rotate keys on the affected device
- Re-pair with new public key
3. Trust Store Backup¶
Back up the trust store regularly:
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:
-
On Android phone:
-
Phone displays QR code with identity bundle
-
On gateway laptop:
- Open camera app
- Scan QR code
-
Copy JSON from QR code
-
Add to trust store:
-
Verify pairing:
Workflow 2: Automatic TOFU Pairing¶
Scenario: Development environment with trusted network.
Steps:
-
Start gateway in TOFU mode:
-
Start edge node:
-
Nodes automatically pair on first contact
-
Verify pairing:
Workflow 3: Certificate Authority (Future)¶
Scenario: Enterprise deployment with PKI infrastructure.
Steps (not yet implemented):
- Generate CSR on edge node
- Submit CSR to internal CA
- Receive signed certificate
- Import certificate to node
- Gateway validates against CA root certificate
Trust Status in API Responses¶
Mesh Scan with Trust Status¶
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¶
Response:
Advanced Topics¶
Multi-Hop Trust¶
In some scenarios, nodes need to trust transitively:
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:
-
Check trust status:
-
Add node to trust store:
-
Verify fingerprint matches node's fingerprint
Signature Verification Failed¶
Symptom: "Signature verification failed" error.
Solutions:
- Check clock sync: Nodes must have synchronized clocks (use NTP)
- Verify public key: Ensure trust store has correct public key
- Check for key rotation: Node may have rotated keys
QR Code Won't Scan¶
Symptom: QR code scanner fails to read bundle.
Solutions:
- Increase QR size: Generate larger QR code
- Reduce bundle size: Remove optional fields
- Use manual entry: Copy/paste JSON instead of scanning
Trust Store Corrupted¶
Symptom: Cannot load trust store.
Solutions:
-
Restore from backup:
-
Reset trust store (loses all pairings):
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:
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¶
- Capability Tokens - Authorization with capability tokens
- Execution Evidence - Cryptographic audit trails
- Offline Operation - Secure offline message delivery
- Tool Contracts - Define tools for edge execution