Exported Keying Material (EKM) channel binding cryptographically links TLS connections to application-layer attestations, preventing man-in-the-middle attacks and quote replay attacks.Umbra implements EKM according to RFC 9266 with an additional HMAC signature layer for defense-in-depth.
The HMAC key is never configured manually. Instead, it’s derived deterministically inside the TEE:
from dstack_sdk import DstackClientdef _get_ekm_hmac_secret() -> str: """Derive EKM HMAC key from TEE, falling back to env var for dev/test.""" try: client = DstackClient() derived = client.get_key("ekm/hmac-key/v1").decode_key().hex() logger.info("EKM HMAC key derived from TEE (dstack)") return derived except Exception as e: logger.warning( f"dstack key derivation failed ({e}), falling back to EKM_SHARED_SECRET env var" ) env_secret = os.getenv("EKM_SHARED_SECRET") if not env_secret: raise RuntimeError("EKM_SHARED_SECRET not set") if len(env_secret) < 32: raise RuntimeError("EKM_SHARED_SECRET is too short") logger.info("EKM validation enabled with shared secret") return env_secret
Deterministic: get_key("ekm/hmac-key/v1") always returns the same key for the same TEE
Derived from TEE identity: Key is based on RTMR2 (docker-compose hash) and bootchain
Operator-invisible: The key never appears in logs, environment variables, or configuration files
Synchronized automatically: Both nginx and attestation service derive the same key
Security Guarantee: The operator who deploys the CVM never sees the HMAC key. This enables zero-trust deployment where even the person running the infrastructure cannot compromise the system.
The attestation service validates EKM headers before trusting them:
import hmacimport hashlibimport secretsdef validate_and_extract_ekm(signed_header: str, secret: str) -> str: """ Validate HMAC signature and extract EKM value. Args: signed_header: Format "{ekm_hex}:{hmac_hex}" (129 chars) secret: Shared secret for HMAC validation Returns: ekm_hex: The validated EKM value (64 hex chars) Raises: ValueError: If validation fails """ # Validate format if len(signed_header) != 129 or signed_header[64] != ":": raise ValueError("Invalid EKM header format (expected: {ekm}:{hmac})") # Extract components ekm_hex = signed_header[:64] ekm_raw = bytes.fromhex(ekm_hex) received_hmac = signed_header[65:] # Compute expected HMAC expected_hmac = hmac.new(secret.encode("utf-8"), ekm_raw, hashlib.sha256).hexdigest() # Constant-time comparison to prevent timing attacks if not secrets.compare_digest(received_hmac, expected_hmac): raise ValueError("HMAC validation failed") return ekm_hex
Timing Attack Protection: The code uses secrets.compare_digest() for constant-time comparison. This prevents attackers from learning information about the HMAC key through timing analysis.
✅ Session Binding: Quote is cryptographically bound to the specific TLS connection✅ Replay Prevention: Attacker cannot reuse a quote on a different TLS session✅ MITM Detection: If attacker terminates TLS and re-establishes it, EKM will differ✅ Freshness: Combined with client nonce, proves quote was generated for THIS request
Umbra’s EKM implementation has multiple security layers:
TLS 1.3 Encryption: EKM derived from TLS master secret
TEE Isolation: Nginx and attestation service run in same TEE
HMAC Signature: Prevents header forgery even if network isolation fails
Constant-Time Validation: Prevents timing attacks
Key Derivation: HMAC key derived from TEE identity, never exposed
Client-Side Verification: Client independently verifies EKM in report_data
Critical: All six layers must be intact for full security. Disabling any layer (e.g., using NO_TDX=true or skipping HMAC validation) significantly weakens security.
# Set shared secret manuallyexport EKM_SHARED_SECRET=$(openssl rand -hex 32)# Both nginx and attestation service must use the same valueecho "EKM_SHARED_SECRET=$EKM_SHARED_SECRET" >> .env# Start servicescd cvmmake dev-up
Never use the same EKM_SHARED_SECRET across multiple deployments. Each deployment should derive a unique key from its TEE measurements.