Root cause
Authentication & access control introduced
feat: Implement post-quantum keyserver system with plugin architecture
How AI contributed: Direct introductionPublished Feb 10, 2026 in
jahlives/openssl_encryptAll 20 findings in this repository
The Bearer dependency accepted any non-empty token, enabling unauthenticated upload of an otherwise-valid self-signed attacker-controlled bundle. Search was intentionally public, and revocation still required a valid ownership signature.
Root cause
feat: Implement post-quantum keyserver system with plugin architecture
Fix
security: Implement proper HMAC token validation in key server (CRIT-7)
AI-assisted fix: Claude Code · Tobi
Case facts
@@ -0,0 +1,334 @@+#!/usr/bin/env python3+"""+API Endpoints - Keys++This module implements the keyserver API endpoints:+- POST /api/v1/keys - Upload key (requires authentication)+- GET /api/v1/keys/search - Search for key (public, no auth)+- POST /api/v1/keys/{fingerprint}/revoke - Revoke key (requires authentication)++SECURITY:+- Upload and revoke require Bearer token authentication+- Search is public (no authentication)+- All bundles verified before storage+- Rate limiting applied+"""++import json+import logging+from datetime import datetime+from typing import Optional++from fastapi import APIRouter, Depends, Header, HTTPException, Query, status+from sqlalchemy.orm import Session++from ...config import settings+from ...database import get_db+from ...models.public_key import PublicKey+from ...schemas.key_bundle import (+ ErrorResponse,+ KeyBundleSchema,+ KeySearchResponse,+ KeyUploadResponse,+ RevocationRequest,+ RevocationResponse,+)+from ...services.verification import (+ FingerprintMismatchError,+ VerificationError,+ verify_bundle_signature,+ verify_revocation_signature,+)++logger = logging.getLogger(__name__)++router = APIRouter(prefix="/api/v1", tags=["keys"])+++def verify_api_token(authorization: Optional[str] = Header(None)) -> bool:+ """+ Verify Bearer token authentication.++ Args:+ authorization: Authorization header (Bearer token)++ Returns:+ True if authenticated++ Raises:+ HTTPException: If authentication fails+ """+ if not authorization:+ raise HTTPException(+ status_code=status.HTTP_401_UNAUTHORIZED,+ detail="Authorization header missing",+ headers={"WWW-Authenticate": "Bearer"},+ )++ if not authorization.startswith("Bearer "):+ raise HTTPException(+ status_code=status.HTTP_401_UNAUTHORIZED,+ detail="Invalid authorization header format. Expected: Bearer <token>",+ headers={"WWW-Authenticate": "Bearer"},+ )++ token = authorization[7:] # Remove "Bearer " prefix++ # In production, validate token against database or JWT+ # For now, simple token check (replace with proper validation)+ if not token:+ raise HTTPException(+ status_code=status.HTTP_401_UNAUTHORIZED,+ detail="Invalid API token",+ headers={"WWW-Authenticate": "Bearer"},+ )++ logger.debug("API token validated successfully")+ return True++@@ -47,7 +47,10 @@ def verify_api_token(authorization: Optional[str] = Header(None)) -> bool: """- Verify Bearer token authentication.+ Verify Bearer token authentication using HMAC-signed tokens.++ Token format: base64(payload_json).base64(hmac_signature)+ Payload must contain: exp (expiration timestamp), iss (issuer) Args: authorization: Authorization header (Bearer token)@@ -58,6 +61,12 @@ def verify_api_token(authorization: Optional[str] = Header(None)) -> bool: Raises: HTTPException: If authentication fails """+ import base64+ import hashlib+ import hmac+ import json+ import time+ if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED,@@ -74,17 +83,67 @@ def verify_api_token(authorization: Optional[str] = Header(None)) -> bool: token = authorization[7:] # Remove "Bearer " prefix - # In production, validate token against database or JWT- # For now, simple token check (replace with proper validation) if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED,- detail="Invalid API token",+ detail="Empty API token", headers={"WWW-Authenticate": "Bearer"}, ) - logger.debug("API token validated successfully")- return True+ # Validate HMAC-signed token+ try:+ parts = token.split(".")+ if len(parts) != 2:+ raise ValueError("Invalid token format")++ payload_b64, signature_b64 = parts++ # Verify HMAC signature+ expected_sig = hmac.new(+ settings.api_token_secret.encode("utf-8"),+ payload_b64.encode("utf-8"),+ hashlib.sha256,+ ).digest()++ provided_sig = base64.urlsafe_b64decode(signature_b64 + "==") # Pad base64++ if not hmac.compare_digest(expected_sig, provided_sig):+ raise ValueError("Invalid token signature")++ # Decode and validate payload+ # Add padding for base64+ padded = payload_b64 + "=" * (4 - len(payload_b64) % 4)+ payload = json.loads(base64.urlsafe_b64decode(padded))++ # Validate expiration+ if "exp" not in payload:+ raise ValueError("Token missing expiration claim")++ if time.time() > payload["exp"]:+ raise ValueError("Token has expired")++ # Validate issuer+ expected_issuer = "openssl_encrypt_keyserver"+ if payload.get("iss") != expected_issuer:+ raise ValueError(f"Invalid token issuer: expected '{expected_issuer}'")++ logger.debug("API token validated successfully")+ return True++ except ValueError as e:+ logger.warning(f"Token validation failed: {e}")+ raise HTTPException(+ status_code=status.HTTP_401_UNAUTHORIZED,+ detail="Invalid or expired API token",+ headers={"WWW-Authenticate": "Bearer"},+ )+ except Exception as e:+ logger.error(f"Unexpected token validation error: {e}")@@ -22,7 +22,7 @@ class Settings(BaseSettings): Server settings with environment variable support. All settings can be overridden via environment variables.- Example: DATABASE_URL, API_TOKEN_SALT, etc.+ Example: DATABASE_URL, API_TOKEN_SECRET, etc. """ # Application@@ -33,8 +33,8 @@ class Settings(BaseSettings): # Database - MUST be set via environment variable database_url: str = os.getenv("DATABASE_URL", "") - # Security- api_token_salt: str = os.getenv("API_TOKEN_SALT", "change-me-in-production")+ # Security - MUST be set via environment variable (min 32 chars)+ api_token_secret: str = os.getenv("API_TOKEN_SECRET", "") cors_origins: List[str] = ["*"] # Configure appropriately in production # Rate limiting@@ -70,6 +70,19 @@ def validate_settings(s: Settings) -> None: "Example: DATABASE_URL=postgresql://user:pass@host/dbname" ) + if not s.api_token_secret or len(s.api_token_secret) < 32:+ if s.debug:+ logger.warning(+ "SECURITY WARNING: API_TOKEN_SECRET not set or too short. "+ "Using insecure default for debug mode only."+ )+ s.api_token_secret = "debug-only-insecure-secret-not-for-production!!"+ else:+ raise ValueError(+ "API_TOKEN_SECRET environment variable is required (min 32 characters). "+ 'Generate with: python -c "import secrets; print(secrets.token_urlsafe(48))"'+ )+ logger = logging.getLogger(__name__) AI-assisted change cf27e1d76666a2f884c37d61439d0644f76c79882b191b80e18ec0ac258024ba · Fix e8d3654960107bbca032a5f1e71856f34633aceaac5e8ec040abb1967713fc79