"""Per-Agent authentication with bounded legacy compatibility.""" import hashlib import hmac from app.agent_auth.exceptions import ( AgentAuthenticationFailed, AgentAuthenticationUnavailable, AgentCredentialRevoked, AgentIdentityUnauthorized, ) from app.config import settings def hash_agent_secret(secret: str) -> str: """Hash a high-entropy machine-generated Agent secret for persistence.""" return hashlib.sha256(secret.encode("utf-8")).hexdigest() class AgentAuthService: def __init__(self, repository): self.repository = repository def authenticate(self, agent_id: str | None, provided_secret: str | None) -> str: identity = (agent_id or "").strip() secret = (provided_secret or "").strip() if not identity: raise AgentAuthenticationFailed("Missing Agent identity") if not secret: raise AgentAuthenticationFailed("Invalid or missing Agent secret") credential = self.repository.get_credential(identity) # Any Agent with a per-Agent credential is permanently on the new # authentication path. The legacy shared secret must never authenticate # that identity, even if the Agent later becomes revoked. if credential and credential.get("agent_secret_hash"): if credential.get("credential_revoked_at") is not None: raise AgentCredentialRevoked("Agent credential has been revoked") supplied_hash = hash_agent_secret(secret) if not hmac.compare_digest( supplied_hash, str(credential["agent_secret_hash"]), ): raise AgentAuthenticationFailed("Invalid or missing Agent secret") return identity # Temporary compatibility for the single pre-enrollment Agent. legacy_id = settings.minerops_agent_id.strip() legacy_secret = settings.minerops_agent_shared_secret.strip() if not legacy_id or not legacy_secret: raise AgentAuthenticationUnavailable( "Legacy Agent authentication is not configured" ) if not hmac.compare_digest(identity, legacy_id): raise AgentIdentityUnauthorized("Agent identity is not authorized") if not hmac.compare_digest(secret, legacy_secret): raise AgentAuthenticationFailed("Invalid or missing Agent secret") return identity