init: vaultmesh mcp server
This commit is contained in:
578
packages/vaultmesh_mcp/tools/key_binding.py
Normal file
578
packages/vaultmesh_mcp/tools/key_binding.py
Normal file
@@ -0,0 +1,578 @@
|
||||
"""
|
||||
Key Binding Engine - Authority bound to cryptographic reality
|
||||
|
||||
Every profile has a corresponding key reality:
|
||||
- OBSERVER: Ephemeral (memory only, no signing power)
|
||||
- OPERATOR: Session key (encrypted disk, revocable)
|
||||
- GUARDIAN: Device-bound (secure enclave, non-exportable)
|
||||
- PHOENIX: Time-locked (guardian key + approval artifact)
|
||||
- SOVEREIGN: Offline root (hardware key, never automated)
|
||||
|
||||
Invariant: No profile exists without corresponding key reality.
|
||||
If key cannot be proven → authority collapses downward, never upward.
|
||||
"""
|
||||
|
||||
import json
|
||||
import secrets
|
||||
import hashlib
|
||||
import os
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, List
|
||||
|
||||
import blake3
|
||||
|
||||
# Optional: Ed25519 support
|
||||
try:
|
||||
from nacl.signing import SigningKey, VerifyKey
|
||||
from nacl.encoding import Base64Encoder
|
||||
from nacl.exceptions import BadSignature
|
||||
NACL_AVAILABLE = True
|
||||
except ImportError:
|
||||
NACL_AVAILABLE = False
|
||||
|
||||
# VaultMesh paths
|
||||
VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve()
|
||||
RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts"
|
||||
KEYS_ROOT = VAULTMESH_ROOT / "keys"
|
||||
|
||||
|
||||
def _vmhash_blake3(data: bytes) -> str:
|
||||
return f"blake3:{blake3.blake3(data).hexdigest()}"
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class KeyBindingType(Enum):
|
||||
"""Types of key bindings corresponding to profiles."""
|
||||
EPHEMERAL = "ephemeral" # 👁 OBSERVER - memory only
|
||||
SESSION = "session" # ⚙ OPERATOR - encrypted disk
|
||||
DEVICE = "device" # 🛡 GUARDIAN - secure enclave
|
||||
TIMELOCKED = "timelocked" # 🔥 PHOENIX - guardian + approval
|
||||
HARDWARE = "hardware" # 👑 SOVEREIGN - offline, air-gapped
|
||||
|
||||
|
||||
class KeyStatus(Enum):
|
||||
"""Key lifecycle states."""
|
||||
ACTIVE = "active"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
@dataclass
|
||||
class KeyBinding:
|
||||
"""A cryptographic key bound to a profile."""
|
||||
key_id: str
|
||||
profile: str
|
||||
binding_type: str
|
||||
fingerprint: str
|
||||
|
||||
# Key material (public only stored here)
|
||||
public_key_b64: str
|
||||
|
||||
# Binding constraints
|
||||
created_at: str
|
||||
expires_at: Optional[str]
|
||||
device_id: Optional[str] = None
|
||||
|
||||
# Status
|
||||
status: str = "active"
|
||||
revoked_at: Optional[str] = None
|
||||
revocation_reason: Optional[str] = None
|
||||
|
||||
# Proof
|
||||
binding_receipt_hash: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class KeyAssertion:
|
||||
"""Runtime assertion that a key is valid for a profile."""
|
||||
assertion_id: str
|
||||
key_id: str
|
||||
profile: str
|
||||
binding_type: str
|
||||
fingerprint: str
|
||||
|
||||
# Verification
|
||||
verified_at: str
|
||||
signature_valid: bool
|
||||
binding_valid: bool
|
||||
not_expired: bool
|
||||
not_revoked: bool
|
||||
|
||||
# Result
|
||||
authority_granted: bool
|
||||
collapse_to: Optional[str] = None # If authority denied, collapse to this
|
||||
|
||||
|
||||
# In-memory key store (production would use secure storage)
|
||||
_key_bindings: Dict[str, KeyBinding] = {}
|
||||
_device_keys: Dict[str, str] = {} # device_id -> key_id
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE → KEY BINDING REQUIREMENTS
|
||||
# =============================================================================
|
||||
|
||||
PROFILE_KEY_REQUIREMENTS = {
|
||||
"observer": {
|
||||
"binding_type": KeyBindingType.EPHEMERAL,
|
||||
"requires_signature": False,
|
||||
"requires_device": False,
|
||||
"max_ttl_seconds": 3600, # 1 hour
|
||||
"can_sign_receipts": False,
|
||||
},
|
||||
"operator": {
|
||||
"binding_type": KeyBindingType.SESSION,
|
||||
"requires_signature": True,
|
||||
"requires_device": False,
|
||||
"max_ttl_seconds": 86400, # 24 hours
|
||||
"can_sign_receipts": True,
|
||||
},
|
||||
"guardian": {
|
||||
"binding_type": KeyBindingType.DEVICE,
|
||||
"requires_signature": True,
|
||||
"requires_device": True,
|
||||
"max_ttl_seconds": 604800, # 7 days
|
||||
"can_sign_receipts": True,
|
||||
"device_types": ["secure_enclave", "tpm", "operator_phone"],
|
||||
},
|
||||
"phoenix": {
|
||||
"binding_type": KeyBindingType.TIMELOCKED,
|
||||
"requires_signature": True,
|
||||
"requires_device": True,
|
||||
"requires_approval_artifact": True,
|
||||
"max_ttl_seconds": 86400, # 24 hours (auto-expire)
|
||||
"can_sign_receipts": True,
|
||||
},
|
||||
"sovereign": {
|
||||
"binding_type": KeyBindingType.HARDWARE,
|
||||
"requires_signature": True,
|
||||
"requires_device": True,
|
||||
"requires_human": True,
|
||||
"max_ttl_seconds": None, # No auto-expire
|
||||
"can_sign_receipts": True,
|
||||
"device_types": ["hardware_key", "air_gapped"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# KEY OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def generate_key_pair() -> Dict[str, str]:
|
||||
"""Generate an Ed25519 key pair."""
|
||||
if NACL_AVAILABLE:
|
||||
signing_key = SigningKey.generate()
|
||||
verify_key = signing_key.verify_key
|
||||
return {
|
||||
"private_key_b64": signing_key.encode(Base64Encoder).decode(),
|
||||
"public_key_b64": verify_key.encode(Base64Encoder).decode(),
|
||||
"fingerprint": _vmhash_blake3(verify_key.encode())[:24],
|
||||
}
|
||||
else:
|
||||
# Fallback: generate placeholder for testing
|
||||
fake_key = secrets.token_bytes(32)
|
||||
return {
|
||||
"private_key_b64": "PLACEHOLDER_" + secrets.token_urlsafe(32),
|
||||
"public_key_b64": "PLACEHOLDER_" + secrets.token_urlsafe(32),
|
||||
"fingerprint": _vmhash_blake3(fake_key)[:24],
|
||||
}
|
||||
|
||||
|
||||
def create_key_binding(
|
||||
profile: str,
|
||||
public_key_b64: str,
|
||||
device_id: Optional[str] = None,
|
||||
ttl_seconds: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a key binding for a profile.
|
||||
|
||||
Validates that the binding meets profile requirements.
|
||||
"""
|
||||
requirements = PROFILE_KEY_REQUIREMENTS.get(profile)
|
||||
if not requirements:
|
||||
return {"success": False, "error": f"Unknown profile: {profile}"}
|
||||
|
||||
binding_type = requirements["binding_type"]
|
||||
|
||||
# Validate device requirement
|
||||
if requirements.get("requires_device") and not device_id:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Profile {profile} requires device binding",
|
||||
}
|
||||
|
||||
# Calculate expiry
|
||||
max_ttl = requirements.get("max_ttl_seconds")
|
||||
if ttl_seconds and max_ttl and ttl_seconds > max_ttl:
|
||||
ttl_seconds = max_ttl
|
||||
|
||||
expires_at = None
|
||||
if ttl_seconds:
|
||||
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat()
|
||||
elif max_ttl:
|
||||
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=max_ttl)).isoformat()
|
||||
|
||||
# Generate fingerprint
|
||||
fingerprint = _vmhash_blake3(public_key_b64.encode())[:24]
|
||||
|
||||
key_id = f"key_{secrets.token_hex(12)}"
|
||||
|
||||
binding = KeyBinding(
|
||||
key_id=key_id,
|
||||
profile=profile,
|
||||
binding_type=binding_type.value,
|
||||
fingerprint=fingerprint,
|
||||
public_key_b64=public_key_b64,
|
||||
created_at=_now_iso(),
|
||||
expires_at=expires_at,
|
||||
device_id=device_id,
|
||||
status=KeyStatus.ACTIVE.value,
|
||||
)
|
||||
|
||||
# Emit binding receipt
|
||||
receipt = _emit_key_receipt("key_binding_created", asdict(binding))
|
||||
binding.binding_receipt_hash = receipt["root_hash"]
|
||||
|
||||
# Store
|
||||
_key_bindings[key_id] = binding
|
||||
if device_id:
|
||||
_device_keys[device_id] = key_id
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"key_id": key_id,
|
||||
"profile": profile,
|
||||
"binding_type": binding_type.value,
|
||||
"fingerprint": fingerprint,
|
||||
"expires_at": expires_at,
|
||||
"device_id": device_id,
|
||||
"receipt_hash": binding.binding_receipt_hash,
|
||||
}
|
||||
|
||||
|
||||
def assert_key_authority(
|
||||
key_id: str,
|
||||
required_profile: str,
|
||||
signature: Optional[str] = None,
|
||||
challenge: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Assert that a key has authority for a profile.
|
||||
|
||||
If assertion fails, returns collapse_to indicating the
|
||||
maximum authority the key can claim.
|
||||
"""
|
||||
assertion_id = f"assert_{secrets.token_hex(8)}"
|
||||
|
||||
binding = _key_bindings.get(key_id)
|
||||
if not binding:
|
||||
return _authority_denied(assertion_id, required_profile, "Key not found", "observer")
|
||||
|
||||
# Check status
|
||||
if binding.status == KeyStatus.REVOKED.value:
|
||||
return _authority_denied(assertion_id, required_profile, "Key revoked", "observer")
|
||||
|
||||
# Check expiry
|
||||
not_expired = True
|
||||
if binding.expires_at:
|
||||
expires = datetime.fromisoformat(binding.expires_at.replace('Z', '+00:00'))
|
||||
if datetime.now(timezone.utc) > expires:
|
||||
not_expired = False
|
||||
# Auto-revoke expired keys
|
||||
binding.status = KeyStatus.EXPIRED.value
|
||||
return _authority_denied(assertion_id, required_profile, "Key expired", "observer")
|
||||
|
||||
# Check profile hierarchy
|
||||
profile_order = ["observer", "operator", "guardian", "phoenix", "sovereign"]
|
||||
bound_level = profile_order.index(binding.profile) if binding.profile in profile_order else -1
|
||||
required_level = profile_order.index(required_profile) if required_profile in profile_order else 99
|
||||
|
||||
if bound_level < required_level:
|
||||
# Key is for lower profile - collapse to its actual level
|
||||
return _authority_denied(
|
||||
assertion_id,
|
||||
required_profile,
|
||||
f"Key bound to {binding.profile}, not sufficient for {required_profile}",
|
||||
binding.profile
|
||||
)
|
||||
|
||||
# Check signature if required
|
||||
requirements = PROFILE_KEY_REQUIREMENTS.get(required_profile, {})
|
||||
signature_valid = True
|
||||
|
||||
if requirements.get("requires_signature"):
|
||||
if not signature or not challenge:
|
||||
return _authority_denied(
|
||||
assertion_id,
|
||||
required_profile,
|
||||
"Signature required but not provided",
|
||||
_collapse_profile(required_profile)
|
||||
)
|
||||
|
||||
# Verify signature
|
||||
if NACL_AVAILABLE and not binding.public_key_b64.startswith("PLACEHOLDER"):
|
||||
try:
|
||||
verify_key = VerifyKey(binding.public_key_b64.encode(), Base64Encoder)
|
||||
sig_bytes = Base64Encoder.decode(signature.encode())
|
||||
verify_key.verify(challenge.encode(), sig_bytes)
|
||||
except BadSignature:
|
||||
signature_valid = False
|
||||
return _authority_denied(
|
||||
assertion_id,
|
||||
required_profile,
|
||||
"Invalid signature",
|
||||
_collapse_profile(required_profile)
|
||||
)
|
||||
|
||||
# All checks passed
|
||||
assertion = KeyAssertion(
|
||||
assertion_id=assertion_id,
|
||||
key_id=key_id,
|
||||
profile=required_profile,
|
||||
binding_type=binding.binding_type,
|
||||
fingerprint=binding.fingerprint,
|
||||
verified_at=_now_iso(),
|
||||
signature_valid=signature_valid,
|
||||
binding_valid=True,
|
||||
not_expired=not_expired,
|
||||
not_revoked=binding.status != KeyStatus.REVOKED.value,
|
||||
authority_granted=True,
|
||||
)
|
||||
|
||||
_emit_key_receipt("key_assertion_granted", asdict(assertion))
|
||||
|
||||
return {
|
||||
"authority_granted": True,
|
||||
"assertion_id": assertion_id,
|
||||
"key_id": key_id,
|
||||
"profile": required_profile,
|
||||
"binding_type": binding.binding_type,
|
||||
"fingerprint": binding.fingerprint,
|
||||
"expires_at": binding.expires_at,
|
||||
}
|
||||
|
||||
|
||||
def _authority_denied(
|
||||
assertion_id: str,
|
||||
required_profile: str,
|
||||
reason: str,
|
||||
collapse_to: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Record authority denial and return collapse result."""
|
||||
assertion = KeyAssertion(
|
||||
assertion_id=assertion_id,
|
||||
key_id="unknown",
|
||||
profile=required_profile,
|
||||
binding_type="none",
|
||||
fingerprint="none",
|
||||
verified_at=_now_iso(),
|
||||
signature_valid=False,
|
||||
binding_valid=False,
|
||||
not_expired=False,
|
||||
not_revoked=False,
|
||||
authority_granted=False,
|
||||
collapse_to=collapse_to,
|
||||
)
|
||||
|
||||
_emit_key_receipt("key_assertion_denied", {
|
||||
**asdict(assertion),
|
||||
"reason": reason,
|
||||
})
|
||||
|
||||
return {
|
||||
"authority_granted": False,
|
||||
"assertion_id": assertion_id,
|
||||
"reason": reason,
|
||||
"collapse_to": collapse_to,
|
||||
"message": f"Authority denied. Collapsing to {collapse_to}.",
|
||||
}
|
||||
|
||||
|
||||
def _collapse_profile(profile: str) -> str:
|
||||
"""Determine collapse target when authority is denied."""
|
||||
collapse_map = {
|
||||
"sovereign": "phoenix",
|
||||
"phoenix": "guardian",
|
||||
"guardian": "operator",
|
||||
"operator": "observer",
|
||||
"observer": "observer",
|
||||
}
|
||||
return collapse_map.get(profile, "observer")
|
||||
|
||||
|
||||
def revoke_key(key_id: str, reason: str) -> Dict[str, Any]:
|
||||
"""Revoke a key binding."""
|
||||
binding = _key_bindings.get(key_id)
|
||||
if not binding:
|
||||
return {"success": False, "error": "Key not found"}
|
||||
|
||||
binding.status = KeyStatus.REVOKED.value
|
||||
binding.revoked_at = _now_iso()
|
||||
binding.revocation_reason = reason
|
||||
|
||||
_emit_key_receipt("key_revoked", {
|
||||
"key_id": key_id,
|
||||
"profile": binding.profile,
|
||||
"fingerprint": binding.fingerprint,
|
||||
"reason": reason,
|
||||
"revoked_at": binding.revoked_at,
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"key_id": key_id,
|
||||
"revoked_at": binding.revoked_at,
|
||||
}
|
||||
|
||||
|
||||
def get_device_key(device_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get the key binding for a device."""
|
||||
key_id = _device_keys.get(device_id)
|
||||
if not key_id:
|
||||
return None
|
||||
|
||||
binding = _key_bindings.get(key_id)
|
||||
if not binding:
|
||||
return None
|
||||
|
||||
return {
|
||||
"key_id": key_id,
|
||||
"profile": binding.profile,
|
||||
"binding_type": binding.binding_type,
|
||||
"fingerprint": binding.fingerprint,
|
||||
"device_id": device_id,
|
||||
"status": binding.status,
|
||||
"expires_at": binding.expires_at,
|
||||
}
|
||||
|
||||
|
||||
def list_key_bindings(profile: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List all key bindings, optionally filtered by profile."""
|
||||
bindings = []
|
||||
for key_id, binding in _key_bindings.items():
|
||||
if profile and binding.profile != profile:
|
||||
continue
|
||||
bindings.append({
|
||||
"key_id": key_id,
|
||||
"profile": binding.profile,
|
||||
"binding_type": binding.binding_type,
|
||||
"fingerprint": binding.fingerprint,
|
||||
"status": binding.status,
|
||||
"expires_at": binding.expires_at,
|
||||
"device_id": binding.device_id,
|
||||
})
|
||||
|
||||
return {
|
||||
"count": len(bindings),
|
||||
"bindings": bindings,
|
||||
}
|
||||
|
||||
|
||||
def _emit_key_receipt(receipt_type: str, body: dict) -> dict:
|
||||
"""Emit a receipt for key operations."""
|
||||
scroll_path = RECEIPTS_ROOT / "identity" / "key_events.jsonl"
|
||||
scroll_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
receipt = {
|
||||
"schema_version": "2.0.0",
|
||||
"type": receipt_type,
|
||||
"timestamp": _now_iso(),
|
||||
"scroll": "identity",
|
||||
"tags": ["key", receipt_type],
|
||||
"root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()),
|
||||
"body": body,
|
||||
}
|
||||
|
||||
with open(scroll_path, "a") as f:
|
||||
f.write(json.dumps(receipt) + "\n")
|
||||
|
||||
return receipt
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GUARDIAN DEVICE BINDING
|
||||
# =============================================================================
|
||||
|
||||
def bind_guardian_device(
|
||||
device_id: str,
|
||||
device_type: str,
|
||||
public_key_b64: str,
|
||||
device_attestation: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Bind a device as Guardian-of-record.
|
||||
|
||||
The Guardian device becomes:
|
||||
- Escalation signer
|
||||
- Receipt verifier
|
||||
- Emergency revocation authority
|
||||
"""
|
||||
valid_types = PROFILE_KEY_REQUIREMENTS["guardian"]["device_types"]
|
||||
if device_type not in valid_types:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid device type. Must be one of: {valid_types}",
|
||||
}
|
||||
|
||||
# Create guardian key binding
|
||||
result = create_key_binding(
|
||||
profile="guardian",
|
||||
public_key_b64=public_key_b64,
|
||||
device_id=device_id,
|
||||
ttl_seconds=604800, # 7 days
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
return result
|
||||
|
||||
# Record device binding
|
||||
device_binding = {
|
||||
"device_id": device_id,
|
||||
"device_type": device_type,
|
||||
"key_id": result["key_id"],
|
||||
"fingerprint": result["fingerprint"],
|
||||
"bound_at": _now_iso(),
|
||||
"attestation_hash": _vmhash_blake3(device_attestation.encode()) if device_attestation else None,
|
||||
"capabilities": [
|
||||
"escalation_signer",
|
||||
"receipt_verifier",
|
||||
"emergency_revocation",
|
||||
],
|
||||
}
|
||||
|
||||
_emit_key_receipt("guardian_device_bound", device_binding)
|
||||
|
||||
# Store guardian device info
|
||||
guardian_path = KEYS_ROOT / "guardian_device.json"
|
||||
guardian_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(guardian_path, "w") as f:
|
||||
json.dump(device_binding, f, indent=2)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"device_id": device_id,
|
||||
"device_type": device_type,
|
||||
"key_id": result["key_id"],
|
||||
"fingerprint": result["fingerprint"],
|
||||
"capabilities": device_binding["capabilities"],
|
||||
"message": f"Device {device_id} bound as Guardian-of-record",
|
||||
}
|
||||
|
||||
|
||||
def get_guardian_device() -> Optional[Dict[str, Any]]:
|
||||
"""Get the current Guardian device binding."""
|
||||
guardian_path = KEYS_ROOT / "guardian_device.json"
|
||||
if not guardian_path.exists():
|
||||
return None
|
||||
|
||||
with open(guardian_path, "r") as f:
|
||||
return json.load(f)
|
||||
Reference in New Issue
Block a user