Skip to content

[TESTING][SECURITY]: Encryption and secrets manual test plan (Argon2, Fernet, key derivation) #2405

@crivetimihai

Description

@crivetimihai

[TESTING][SECURITY]: Encryption and secrets manual test plan (Argon2, Fernet, key derivation)

Goal

Produce a comprehensive manual test plan for validating encryption and secrets handling follows security best practices including Argon2id password hashing, Fernet symmetric encryption, secure key derivation, and proper secret management.

Why Now?

Security testing is critical for GA release:

  1. Production Readiness: Security must be validated before release
  2. Compliance: Required by security standards and audits
  3. Defense in Depth: Validates multiple protection layers
  4. Attack Mitigation: Prevents common exploitation techniques
  5. User Trust: Security issues erode confidence

User Stories

Story 1: Password Hashing with Argon2id

As a security administrator
I want passwords hashed with Argon2id
So that password storage follows OWASP recommendations

Acceptance Criteria

Feature: Argon2id password hashing
  Scenario: Password hashed on creation
    When a user creates an account with a password
    Then the password should be hashed with Argon2id
    And the hash should include salt

  Scenario: Password verification
    When a user logs in with correct password
    Then the password should be verified against hash
    And timing should be constant (no timing attacks)

  Scenario: Configurable Argon2id parameters
    When Argon2id parameters are configured
    Then time_cost, memory_cost, parallelism should be applied

Story 2: Fernet Symmetric Encryption

As a security administrator
I want secrets encrypted with Fernet
So that sensitive data is protected at rest

Acceptance Criteria

Feature: Fernet encryption
  Scenario: Secret encryption
    When a secret (API key, gateway credential) is stored
    Then it should be encrypted with Fernet
    And only encrypted form stored in database

  Scenario: Secret decryption
    When an encrypted secret is needed
    Then it should be decrypted transparently
    And decryption should use Argon2id-derived key

Story 3: Key Derivation

As a security administrator
I want encryption keys derived from master secret
So that key management is simplified and secure

Acceptance Criteria

Feature: Key derivation
  Scenario: Key derived from master secret
    When encryption is needed
    Then key should be derived from AUTH_ENCRYPTION_SECRET
    And derivation should use Argon2id (NOT PBKDF2)

  Scenario: Salted key generation
    When generating derived keys
    Then unique salt should be used
    And salt should be stored with ciphertext in JSON bundle

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                   Encryption Services                                │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ Argon2 Service                                              │    │
│  │ mcpgateway/services/argon2_service.py                       │    │
│  │                                                             │    │
│  │  Algorithm: Argon2id (hybrid, resistant to side-channel)   │    │
│  │  Parameters (configurable via env):                         │    │
│  │    ARGON2ID_TIME_COST:    3 (iterations)                   │    │
│  │    ARGON2ID_MEMORY_COST:  65536 KB (64 MB)                 │    │
│  │    ARGON2ID_PARALLELISM:  1 (threads)                      │    │
│  │    hash_len:     32 bytes                                  │    │
│  │    salt_len:     16 bytes                                  │    │
│  │                                                             │    │
│  │  Output: $argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>     │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ Encryption Service                                          │    │
│  │ mcpgateway/services/encryption_service.py                   │    │
│  │                                                             │    │
│  │  Algorithm: Fernet (AES-128-CBC + HMAC-SHA256)             │    │
│  │  Key Source: Derived from AUTH_ENCRYPTION_SECRET           │    │
│  │  Key Derivation: Argon2id (NOT PBKDF2)                     │    │
│  │                                                             │    │
│  │  Encrypted Format: JSON bundle containing:                  │    │
│  │    {"kdf":"argon2id","t":3,"m":65536,"p":1,                │    │
│  │     "salt":"<base64>","token":"gAAAAA..."}                  │    │
│  │                                                             │    │
│  │  Detection: Supports BOTH formats:                          │    │
│  │    - New: JSON with "kdf" field                            │    │
│  │    - Legacy: base64-wrapped Fernet token                   │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ Secret Validation                                           │    │
│  │ mcpgateway/config.py                                        │    │
│  │                                                             │    │
│  │  JWT_SECRET_KEY: Minimum 32 characters                     │    │
│  │  AUTH_ENCRYPTION_SECRET: Minimum 32 characters             │    │
│  │  Entropy check: Warn if < 10 unique characters             │    │
│  │  Weak secret detection: Checks against known weak values   │    │
│  │    ("changeme", "secret", "password", etc.)                │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

Test Environment Setup

# Build and start
make docker-prod && make compose-down && make testing-up

# Key settings in .env for encryption testing:
AUTH_REQUIRED=true
AUTH_ENCRYPTION_SECRET=your-32-char-encryption-secret!!

# Argon2id parameters (CORRECT env var names with "ID" suffix):
ARGON2ID_TIME_COST=3
ARGON2ID_MEMORY_COST=65536
ARGON2ID_PARALLELISM=1

# Password policy:
PASSWORD_POLICY_ENABLED=true
PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_DIGIT=true
PASSWORD_REQUIRE_SPECIAL=true

Test Cases

TC-EN-001: Argon2id Password Hashing

Objective: Verify passwords are hashed with Argon2id

Steps:

# Step 1: Create user with password (CORRECT endpoint)
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"SecureP@ssw0rd!"}' \
  http://localhost:4444/auth/email/register | jq .

# Step 2: Check stored hash format in database
sqlite3 mcp.db "SELECT password_hash FROM email_users WHERE email='test@example.com';"
# Hash should start with $argon2id$v=19$

Expected Results:

  • Password stored as Argon2id hash
  • Hash format includes version and parameters
  • Salt embedded in hash string

TC-EN-002: Argon2id Parameters Configuration

Objective: Verify Argon2id parameters are configurable

Steps:

# Step 1: Configure custom parameters (CORRECT env var names)
export ARGON2ID_TIME_COST=4
export ARGON2ID_MEMORY_COST=131072
export ARGON2ID_PARALLELISM=2

# Step 2: Restart server and create user
# Hash should reflect new parameters: m=131072,t=4,p=2
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"test2@example.com","password":"SecureP@ssw0rd!"}' \
  http://localhost:4444/auth/email/register | jq .

# Step 3: Verify hash parameters
sqlite3 mcp.db "SELECT password_hash FROM email_users WHERE email='test2@example.com';"

Expected Results:

  • Custom time_cost applied
  • Custom memory_cost applied
  • Custom parallelism applied
  • Parameters visible in hash string: $argon2id$v=19$m=131072,t=4,p=2$

TC-EN-003: Password Verification

Objective: Verify password verification works correctly

Steps:

# Step 1: Login with correct password (CORRECT endpoint)
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"SecureP@ssw0rd!"}' \
  http://localhost:4444/auth/email/login | jq .

# Step 2: Login with wrong password
curl -s -w "\nStatus: %{http_code}\n" \
  -X POST -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"wrongpassword"}' \
  http://localhost:4444/auth/email/login

Expected Results:

  • Correct password: login succeeds with access_token
  • Wrong password: 401 Unauthorized
  • Similar response time (constant-time comparison)

TC-EN-004: Timing Attack Resistance

Objective: Verify password verification is constant-time

Steps:

# Step 1: Time verification with correct password
for i in {1..10}; do
  time curl -s -X POST -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"SecureP@ssw0rd!"}' \
    http://localhost:4444/auth/email/login > /dev/null
done

# Step 2: Time verification with wrong password
for i in {1..10}; do
  time curl -s -X POST -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"wrongpassword"}' \
    http://localhost:4444/auth/email/login > /dev/null
done

# Step 3: Compare timing distributions

Expected Results:

  • Similar timing for correct and incorrect passwords
  • No significant timing difference exploitable
  • Constant-time comparison used

TC-EN-005: Fernet Secret Encryption

Objective: Verify secrets are encrypted with Fernet

Steps:

# Note: No /api/oauth/clients endpoint exists
# The encryption service IS used for:
# - Gateway credentials (services/gateway_service.py)
# - API tokens (services/token_service.py)

# To test encryption manually via Python:
python -c "
from mcpgateway.services.encryption_service import EncryptionService

# MUST provide encryption_secret (required constructor arg)
svc = EncryptionService('my-32-char-encryption-secret!!')

# Methods are encrypt_secret/decrypt_secret (NOT encrypt/decrypt)
encrypted = svc.encrypt_secret('my-secret-value')
print(f'Encrypted: {encrypted}')

# Verify it's a JSON bundle
import json
data = json.loads(encrypted)
print(f'KDF: {data[\"kdf\"]}')  # Should be 'argon2id'
print(f'Has salt: {\"salt\" in data}')
print(f'Has token: {\"token\" in data}')  # Fernet token starts with gAAAAA
"

Expected Results:

  • Secret encrypted as JSON bundle
  • JSON contains: kdf, t, m, p, salt, token
  • Token field contains Fernet-encrypted data (starts with gAAAAA)
  • Original secret not visible

TC-EN-006: Fernet Secret Decryption

Objective: Verify encrypted secrets are decrypted correctly

Steps:

# Test decryption via Python:
python -c "
from mcpgateway.services.encryption_service import EncryptionService

svc = EncryptionService('my-32-char-encryption-secret!!')

# Encrypt then decrypt
original = 'super-secret-value'
encrypted = svc.encrypt_secret(original)
decrypted = svc.decrypt_secret(encrypted)

print(f'Original: {original}')
print(f'Decrypted: {decrypted}')
print(f'Match: {original == decrypted}')
"

Expected Results:

  • Secret decrypted correctly
  • Original value recovered
  • Decryption transparent to application

TC-EN-007: Key Derivation (Argon2id, NOT PBKDF2)

Objective: Verify keys are derived using Argon2id

Steps:

# Key derivation uses Argon2id (NOT PBKDF2 as originally claimed)
# See encryption_service.py:74-96 - derive_key_argon2id()

python -c "
from mcpgateway.services.encryption_service import EncryptionService

# Different secrets produce different keys
svc1 = EncryptionService('master-secret-key-32-chars-min!!')
svc2 = EncryptionService('different-secret-32-chars-min!!')

encrypted1 = svc1.encrypt_secret('test')
encrypted2 = svc2.encrypt_secret('test')

# Same secret can decrypt its own encrypted values
decrypted1 = svc1.decrypt_secret(encrypted1)
print(f'svc1 decrypts own: {decrypted1}')

# But cannot decrypt values from different secret
try:
    svc1.decrypt_secret(encrypted2)
    print('ERROR: Should have failed')
except Exception as e:
    print(f'Correctly failed: {type(e).__name__}')
"

Expected Results:

  • Key derived from AUTH_ENCRYPTION_SECRET using Argon2id
  • Different secrets produce different keys
  • Same master secret + salt = same key

TC-EN-008: Encrypted Secret Detection

Objective: Verify system detects already-encrypted values

Steps:

# Detection supports BOTH formats (encryption_service.py:183):
# 1. New Argon2id JSON bundle: {"kdf":"argon2id",...}
# 2. Legacy PBKDF2 base64-wrapped Fernet format

python -c "
from mcpgateway.services.encryption_service import EncryptionService

svc = EncryptionService('my-32-char-encryption-secret!!')

# Test detection of JSON format
json_encrypted = svc.encrypt_secret('test')
print(f'JSON format detected: {svc.is_encrypted(json_encrypted)}')  # True

# Plain text not detected as encrypted
print(f'Plain text detected: {svc.is_encrypted(\"plain-text\")}')  # False

# Re-encryption prevention
encrypted_twice = svc.encrypt_secret(json_encrypted)
# Should detect and not double-encrypt (or handle gracefully)
"

Expected Results:

  • Encrypted values detected by JSON structure or base64 format
  • Supports both new JSON and legacy formats
  • Double-encryption prevented
  • Decryption only on encrypted values

TC-EN-009: JWT Secret Validation

Objective: Verify JWT_SECRET_KEY is validated

Steps:

# Step 1: Start with short secret (should warn)
export JWT_SECRET_KEY="short"
# Check logs for warning about minimum length

# Step 2: Start with weak/default secret (should warn)
export JWT_SECRET_KEY="changeme"
# Check logs for warning about weak secret

# Step 3: Start with low entropy secret (should warn)
export JWT_SECRET_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
# Check logs for warning about low entropy (< 10 unique chars)

# Step 4: Start with valid secret
export JWT_SECRET_KEY="this-is-a-strong-32-char-secret!"
# Should work without warning

Expected Results:

  • Short secrets trigger warning
  • Weak/default secrets trigger warning (checks against: changeme, secret, password, etc.)
  • Low entropy secrets trigger warning (< 10 unique characters)
  • Valid secrets accepted without warning

TC-EN-010: Encryption Secret Validation

Objective: Verify AUTH_ENCRYPTION_SECRET is validated

Steps:

# Step 1: Missing encryption secret
unset AUTH_ENCRYPTION_SECRET
# Check behavior (may use fallback or error)

# Step 2: Short encryption secret
export AUTH_ENCRYPTION_SECRET="short"
# Should warn

# Step 3: Valid encryption secret
export AUTH_ENCRYPTION_SECRET="valid-32-char-encryption-secret!"
# Should work without warning

# Validation happens in config.py:594-633

Expected Results:

  • Missing secret handled gracefully
  • Short secret triggers warning
  • Valid secret accepted

TC-EN-011: Password Policy Enforcement

Objective: Verify password complexity is enforced

Steps:

# Step 1: Try weak password
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"weak@example.com","password":"123456"}' \
  http://localhost:4444/auth/email/register

# Step 2: Try password without special chars
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"weak2@example.com","password":"Password123"}' \
  http://localhost:4444/auth/email/register

# Step 3: Try valid complex password
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"strong@example.com","password":"SecureP@ssw0rd!"}' \
  http://localhost:4444/auth/email/register

Expected Results:

  • Weak passwords rejected (when PASSWORD_POLICY_ENABLED=true)
  • Minimum length enforced (PASSWORD_MIN_LENGTH)
  • Complexity requirements enforced (if configured)

TC-EN-012: Salted Hash Generation

Objective: Verify unique salts are used for each hash

Steps:

# Create multiple users with same password
for i in 1 2 3; do
  curl -s -X POST -H "Content-Type: application/json" \
    -d "{\"email\":\"salt-test-$i@example.com\",\"password\":\"SamePassword123!\"}" \
    http://localhost:4444/auth/email/register > /dev/null
done

# Check that hashes are different
sqlite3 mcp.db "SELECT email, password_hash FROM email_users WHERE email LIKE 'salt-test%';"

Expected Results:

  • Each hash has unique salt
  • Same password produces different hashes for different users
  • Salt is random and sufficient length (16 bytes)

Test Matrix

Test Case Argon2id Fernet Key Derivation Validation
TC-EN-001
TC-EN-002
TC-EN-003
TC-EN-004
TC-EN-005
TC-EN-006
TC-EN-007
TC-EN-008
TC-EN-009
TC-EN-010
TC-EN-011
TC-EN-012

Success Criteria

  • All 12 test cases pass
  • Argon2id used for password hashing
  • Fernet used for secret encryption
  • Keys derived using Argon2id (NOT PBKDF2)
  • Timing attacks mitigated
  • Secret validation warnings work (entropy, weak secrets)
  • Password policy enforced
  • Unique salts generated

Features NOT Currently Implemented

  • /api/oauth/clients endpoint (no direct OAuth client management API)
  • /api/secrets endpoint (no generic secrets API)
  • ❌ Secret rotation procedure (not documented/automated)

Related Files

  • mcpgateway/services/encryption_service.py - Fernet encryption with Argon2id KDF
  • mcpgateway/services/argon2_service.py - Password hashing
  • mcpgateway/config.py:380-382 - argon2id_* settings
  • mcpgateway/config.py:396-400 - password policy settings
  • mcpgateway/config.py:594-633 - secret validation (entropy, weak detection)
  • mcpgateway/routers/email_auth.py - auth endpoints

Related Issues

  • None identified

Metadata

Metadata

Assignees

Labels

MUSTP1: Non-negotiable, critical requirements without which the product is non-functional or unsafedocumentationImprovements or additions to documentationmanual-testingManual testing / test planning issuessecurityImproves securitytestingTesting (unit, e2e, manual, automated, etc)

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions