-
Notifications
You must be signed in to change notification settings - Fork 614
[TESTING][SECURITY]: Encryption and secrets manual test plan (Argon2, Fernet, key derivation) #2405
Description
[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:
- Production Readiness: Security must be validated before release
- Compliance: Required by security standards and audits
- Defense in Depth: Validates multiple protection layers
- Attack Mitigation: Prevents common exploitation techniques
- 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 appliedStory 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 keyStory 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 bundleArchitecture
┌─────────────────────────────────────────────────────────────────────┐
│ 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=trueTest 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/loginExpected 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 distributionsExpected 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 warningExpected 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-633Expected 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/registerExpected 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/clientsendpoint (no direct OAuth client management API) - ❌
/api/secretsendpoint (no generic secrets API) - ❌ Secret rotation procedure (not documented/automated)
Related Files
mcpgateway/services/encryption_service.py- Fernet encryption with Argon2id KDFmcpgateway/services/argon2_service.py- Password hashingmcpgateway/config.py:380-382- argon2id_* settingsmcpgateway/config.py:396-400- password policy settingsmcpgateway/config.py:594-633- secret validation (entropy, weak detection)mcpgateway/routers/email_auth.py- auth endpoints
Related Issues
- None identified