FIPS 140-2/3 Cryptographic Module Testing Guide
FIPS 140-2 (and its successor FIPS 140-3) defines security requirements for cryptographic modules used in federal systems. If your application handles sensitive government data or pursues FedRAMP authorization, you need to use FIPS-validated cryptographic modules. Testing verifies your code actually uses them.
What FIPS 140-2/3 Requires
FIPS 140-2 defines four security levels (1–4). Most commercial software requires Level 1 or Level 2:
- Level 1: Software cryptographic module, no physical security requirements
- Level 2: Adds evidence of tampering with physical security mechanisms
- Level 3: Adds identity-based authentication and physical protection
- Level 4: Highest physical security, extreme environments
The critical requirement: all cryptographic operations must use FIPS 140-validated modules. Using a non-validated library — even one implementing approved algorithms — is non-compliant.
Approved Algorithms
FIPS 140 approves specific algorithms. Non-approved algorithms cannot be used for security functions:
Approved symmetric encryption:
- AES-128, AES-192, AES-256
- 3DES (legacy, approved but not recommended)
Approved hash functions:
- SHA-256, SHA-384, SHA-512
- SHA-3 variants
Approved asymmetric:
- RSA (2048-bit minimum)
- ECDSA with NIST curves (P-256, P-384, P-521)
- ECDH with NIST curves
Not approved:
- MD5 (any purpose in security functions)
- SHA-1 (for digital signatures and key derivation)
- RC4, DES
- Blowfish, Twofish
- Elliptic curves not on NIST-approved list (Curve25519, Curve448)
Note: Curve25519/Ed25519 are not FIPS-approved, even though they're cryptographically strong. This is a significant constraint for teams using modern TLS or SSH tooling.
Testing FIPS Compliance in Python
Python's cryptography library supports FIPS mode when the underlying OpenSSL is built in FIPS mode.
# test/fips/test_cryptographic_operations.py
import pytest
import subprocess
import hashlib
import ssl
class TestFIPS140Compliance:
"""Verify application uses only FIPS-approved cryptographic operations"""
def test_openssl_fips_mode_enabled(self):
"""Verify the system OpenSSL is in FIPS mode"""
result = subprocess.run(
['openssl', 'version', '-f'],
capture_output=True, text=True
)
# FIPS builds include FIPS in the output
# Alternatively check /proc/sys/crypto/fips_enabled on Linux
try:
with open('/proc/sys/crypto/fips_enabled') as f:
fips_enabled = f.read().strip()
assert fips_enabled == '1', "FIPS mode not enabled at OS level"
except FileNotFoundError:
pytest.skip("Not on Linux — FIPS mode check skipped")
def test_no_md5_in_application(self):
"""MD5 is not used for any security function"""
result = subprocess.run(
['grep', '-rn', '--include=*.py',
'-e', 'hashlib.md5',
'-e', "new('md5')",
'-e', 'MD5',
'src/'],
capture_output=True, text=True
)
# Allow MD5 only in non-security contexts (checksums, ETags)
# Filter out approved usages
lines = [l for l in result.stdout.split('\n')
if l and 'etag' not in l.lower() and 'checksum' not in l.lower()]
assert len(lines) == 0, \
f"FIPS: MD5 used in security context:\n" + '\n'.join(lines)
def test_no_sha1_for_signatures(self):
"""SHA-1 not used for digital signatures or key derivation"""
result = subprocess.run(
['grep', '-rn', '--include=*.py',
'-e', 'SHA1',
'-e', 'sha1',
'src/crypto/', 'src/auth/', 'src/signing/'],
capture_output=True, text=True
)
assert result.returncode != 0 or not result.stdout, \
f"FIPS: SHA-1 used in security context:\n{result.stdout}"
def test_aes_key_size_compliance(self):
"""AES keys must be 128, 192, or 256 bits"""
from src.crypto.encryption import get_encryption_config
config = get_encryption_config()
key_size = config.get('key_size_bits')
assert key_size in (128, 192, 256), \
f"FIPS: AES key size {key_size} bits is not approved (must be 128, 192, or 256)"
def test_rsa_minimum_key_size(self):
"""RSA keys must be at least 2048 bits"""
from src.crypto.keys import get_rsa_key_config
config = get_rsa_key_config()
key_size = config.get('key_size_bits')
assert key_size >= 2048, \
f"FIPS: RSA key size {key_size} bits is below minimum 2048"
def test_ecdsa_uses_approved_curves(self):
"""EC keys must use NIST-approved curves"""
from src.crypto.keys import get_ec_key_config
config = get_ec_key_config()
curve = config.get('curve')
approved_curves = {'P-256', 'P-384', 'P-521', 'secp256r1', 'secp384r1', 'secp521r1'}
assert curve in approved_curves, \
f"FIPS: EC curve '{curve}' is not NIST-approved. " \
f"Note: Curve25519/Ed25519 are NOT FIPS-approved."
def test_no_rc4_or_des(self):
"""Legacy ciphers not used"""
result = subprocess.run(
['grep', '-rni', '--include=*.py',
'-e', 'rc4',
'-e', "'des'",
'-e', 'blowfish',
'src/'],
capture_output=True, text=True
)
assert result.returncode != 0 or not result.stdout, \
f"FIPS: Non-approved cipher found:\n{result.stdout}"Testing FIPS Compliance in Java
Java's FIPS compliance goes through the JCE (Java Cryptography Extension) provider. Bouncy Castle FIPS or IBM JSSE provide FIPS-validated JCE implementations.
// src/test/java/com/example/FIPSComplianceTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
public class FIPSComplianceTest {
@Test
void fipsProviderIsInstalled() {
// Verify a FIPS-validated provider is registered
Provider[] providers = Security.getProviders();
boolean hasFipsProvider = false;
for (Provider p : providers) {
String name = p.getName();
if (name.contains("BCFIPS") || name.contains("IBMJCEPlus") ||
name.contains("SunPKCS11-NSS-FIPS")) {
hasFipsProvider = true;
break;
}
}
assertTrue(hasFipsProvider, "FIPS-validated JCE provider not installed");
}
@Test
void aesKeyGenerationUsesApprovedSize() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256); // 128, 192, or 256
SecretKey key = keyGen.generateKey();
assertEquals(256, key.getEncoded().length * 8,
"FIPS: AES key must be 256 bits");
}
@Test
void md5IsNotAvailableForSecurityUse() {
// In strict FIPS mode, MD5 should throw an exception
// This test verifies the environment enforces the restriction
assertThrows(NoSuchAlgorithmException.class, () -> {
MessageDigest md = MessageDigest.getInstance("MD5");
}, "FIPS: MD5 should not be available in FIPS mode");
}
@Test
void rsaKeyMinimumSize() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048); // Minimum for FIPS
var keyPair = kpg.generateKeyPair();
// Extract key size via reflection or use provider-specific method
int keySize = ((java.security.interfaces.RSAPublicKey)
keyPair.getPublic()).getModulus().bitLength();
assertTrue(keySize >= 2048,
"FIPS: RSA key must be at least 2048 bits, got " + keySize);
}
}Testing FIPS Compliance in Go
Go's standard library supports FIPS mode through the GOEXPERIMENT=boringcrypto build tag (using BoringSSL, which is FIPS-validated):
// crypto_test.go
//go:build boringcrypto
package crypto_test
import (
"crypto/aes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"testing"
"crypto/internal/boring"
)
func TestFIPSModeEnabled(t *testing.T) {
// BoringCrypto build tag enforces FIPS-approved algorithms
if !boring.Enabled() {
t.Fatal("FIPS: BoringCrypto not enabled — build with GOEXPERIMENT=boringcrypto")
}
}
func TestAESKeySize(t *testing.T) {
// AES-256 with FIPS-approved key
key := make([]byte, 32) // 256 bits
rand.Read(key)
_, err := aes.NewCipher(key)
if err != nil {
t.Fatalf("FIPS: AES-256 cipher creation failed: %v", err)
}
}
func TestRSAMinimumKeySize(t *testing.T) {
// Should succeed with 2048-bit key
_, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("FIPS: RSA-2048 key generation failed: %v", err)
}
// 1024-bit key should fail in FIPS mode
_, err = rsa.GenerateKey(rand.Reader, 1024)
if err == nil {
t.Fatal("FIPS: RSA-1024 should be rejected in FIPS mode")
}
}Build and test with FIPS mode:
GOEXPERIMENT=boringcrypto go test ./...Scanning for Non-Approved Algorithm Usage
Static analysis to find algorithm issues before they reach testing:
# scripts/scan-crypto-usage.py
"""Scans codebase for non-FIPS-approved cryptographic algorithm usage"""
import re
import sys
from pathlib import Path
NON_APPROVED = {
'md5': 'MD5 is not approved for security functions (FIPS 140-2)',
'sha1': 'SHA-1 is not approved for digital signatures (FIPS 140-2)',
'rc4': 'RC4 is not approved (FIPS 140-2)',
'des': 'DES is not approved (FIPS 140-2)',
'blowfish': 'Blowfish is not approved (FIPS 140-2)',
'curve25519': 'Curve25519 is not NIST-approved (FIPS 140-2)',
'ed25519': 'Ed25519 is not NIST-approved — use ECDSA P-256 (FIPS 140-2)',
}
def scan_file(path):
issues = []
with open(path) as f:
for i, line in enumerate(f, 1):
line_lower = line.lower()
for pattern, message in NON_APPROVED.items():
if pattern in line_lower:
issues.append({
'file': str(path),
'line': i,
'pattern': pattern,
'message': message,
'code': line.strip()
})
return issues
def main():
src_dir = Path('src')
all_issues = []
for ext in ['*.py', '*.go', '*.java', '*.js', '*.ts']:
for path in src_dir.glob(f'**/{ext}'):
all_issues.extend(scan_file(path))
if all_issues:
print(f"FIPS COMPLIANCE ISSUES ({len(all_issues)}):")
for issue in all_issues:
print(f" {issue['file']}:{issue['line']}: {issue['message']}")
print(f" Code: {issue['code']}")
sys.exit(1)
else:
print("No FIPS compliance issues found.")
if __name__ == '__main__':
main()CI Integration
name: FIPS Compliance Check
on: [push, pull_request]
jobs:
fips-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Scan for non-approved algorithms
run: python3 scripts/scan-crypto-usage.py
- name: Run FIPS compliance tests
run: |
# Enable FIPS mode for the test environment
python3 -m pytest test/fips/ -v \
--tb=short \
-m "fips" \
--junit-xml=fips-test-results.xml
- name: Upload FIPS test results
uses: actions/upload-artifact@v3
with:
name: fips-compliance-${{ github.sha }}
path: fips-test-results.xml
retention-days: 365Summary
FIPS 140-2/3 testing enforces that your application uses only cryptographically validated implementations of approved algorithms:
- Algorithm approval — AES, SHA-256+, RSA-2048+, ECDSA P-256/P-384/P-521
- Non-approval — MD5, SHA-1 (signatures), RC4, DES, Curve25519, Ed25519
- Validated modules — BoringSSL (Go), Bouncy Castle FIPS (Java), OpenSSL FIPS (Python/C)
- Static scanning — catch non-approved usage in CI before it reaches testing
- Runtime testing — verify the validated provider is actually in use
The most common mistake: using a well-known algorithm like SHA-256 but through a non-validated library. FIPS requires both the algorithm AND the implementation to be validated.