FIPS 140-2/3 Cryptographic Module Testing Guide

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: 365

Summary

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.

Read more