Skip to content
Threats Guide

certificate_pinning.py

Certificate Pinning Implementation This script demonstrates certificate pinning techniques to prevent MITM attacks by validating server certificates against known pins.

  • prevention
  • python
  • 362 lines

Run security tools only on systems you own or have written permission to test.

Run it

From the repository root, after pip install -r tools/requirements.txt
cd 01-network-security/man-in-the-middle/prevention
python3 certificate_pinning.py --help

Source

View on GitHub
#!/usr/bin/env python3"""Certificate Pinning ImplementationThis script demonstrates certificate pinning techniques to prevent MITM attacksby validating server certificates against known pins.""" import sslimport socketimport hashlibimport hmacimport base64import jsonimport requestsfrom cryptography import x509from cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives import hashes, serializationfrom cryptography.hazmat.primitives.asymmetric import rsa, ecimport argparseimport osfrom datetime import datetimeimport sysfrom colorama import Fore, Style, init init(autoreset=True) class CertificatePinner:    """    Certificate Pinning Implementation        Pinning associates a host with their expected certificate or public key.    This prevents MITM attacks using fraudulent certificates.    """        def __init__(self, pins_file="pins.json"):        """        Initialize certificate pinner                Args:            pins_file: JSON file containing pinned certificates/public keys        """        self.pins_file = pins_file        self.pins = self.load_pins()            def load_pins(self):        """Load pinned certificates from file"""        if os.path.exists(self.pins_file):            with open(self.pins_file, 'r') as f:                return json.load(f)        return {}        def save_pins(self):        """Save pins to file"""        with open(self.pins_file, 'w') as f:            json.dump(self.pins, f, indent=2)        # ==================== PIN GENERATION METHODS ====================        def get_certificate(self, hostname, port=443):        """Retrieve certificate from server"""        context = ssl.create_default_context()        context.check_hostname = False        context.verify_mode = ssl.CERT_NONE                try:            with socket.create_connection((hostname, port), timeout=5) as sock:                with context.wrap_socket(sock, server_hostname=hostname) as ssock:                    der_cert = ssock.getpeercert(binary_form=True)                    return x509.load_der_x509_certificate(der_cert, default_backend())        except Exception as e:            print(f"Error getting certificate: {e}")            return None        def pin_certificate(self, hostname, port=443):        """Pin a certificate for a hostname"""        cert = self.get_certificate(hostname, port)        if not cert:            return False                # Calculate certificate fingerprint (SHA-256)        cert_fingerprint = hashlib.sha256(cert.public_bytes(serialization.Encoding.DER)).hexdigest()                # Calculate public key fingerprint        public_key = cert.public_key()        public_key_bytes = public_key.public_bytes(            encoding=serialization.Encoding.DER,            format=serialization.PublicFormat.SubjectPublicKeyInfo        )        public_key_fingerprint = hashlib.sha256(public_key_bytes).hexdigest()                # Store pins        self.pins[hostname] = {            'certificate_fingerprint': cert_fingerprint,            'public_key_fingerprint': public_key_fingerprint,            'subject': str(cert.subject),            'issuer': str(cert.issuer),            'not_before': cert.not_valid_before.isoformat(),            'not_after': cert.not_valid_after.isoformat(),            'pinned_at': datetime.now().isoformat()        }                self.save_pins()                print(f"{Fore.GREEN}[✓] Pinned certificate for {hostname}{Style.RESET_ALL}")        print(f"    Certificate FP: {cert_fingerprint[:20]}...")        print(f"    Public Key FP: {public_key_fingerprint[:20]}...")                return True        def pin_public_key(self, hostname, public_key_pem):        """Pin a specific public key"""        # Parse public key        public_key = serialization.load_pem_public_key(            public_key_pem.encode(),            backend=default_backend()        )                # Calculate fingerprint        public_key_bytes = public_key.public_bytes(            encoding=serialization.Encoding.DER,            format=serialization.PublicFormat.SubjectPublicKeyInfo        )        fingerprint = hashlib.sha256(public_key_bytes).hexdigest()                self.pins[hostname] = {            'public_key_fingerprint': fingerprint,            'pinned_at': datetime.now().isoformat()        }                self.save_pins()        return True        # ==================== VALIDATION METHODS ====================        def validate_certificate(self, hostname, cert):        """Validate certificate against pinned values"""        if hostname not in self.pins:            print(f"{Fore.YELLOW}[!] No pins found for {hostname}{Style.RESET_ALL}")            return False                pins = self.pins[hostname]                # Check certificate fingerprint        cert_fingerprint = hashlib.sha256(            cert.public_bytes(serialization.Encoding.DER)        ).hexdigest()                if 'certificate_fingerprint' in pins:            if cert_fingerprint != pins['certificate_fingerprint']:                print(f"{Fore.RED}[!] Certificate fingerprint mismatch for {hostname}{Style.RESET_ALL}")                print(f"    Expected: {pins['certificate_fingerprint'][:20]}...")                print(f"    Got: {cert_fingerprint[:20]}...")                return False                # Check public key fingerprint        public_key = cert.public_key()        public_key_bytes = public_key.public_bytes(            encoding=serialization.Encoding.DER,            format=serialization.PublicFormat.SubjectPublicKeyInfo        )        public_key_fingerprint = hashlib.sha256(public_key_bytes).hexdigest()                if 'public_key_fingerprint' in pins:            if public_key_fingerprint != pins['public_key_fingerprint']:                print(f"{Fore.RED}[!] Public key fingerprint mismatch for {hostname}{Style.RESET_ALL}")                print(f"    Expected: {pins['public_key_fingerprint'][:20]}...")                print(f"    Got: {public_key_fingerprint[:20]}...")                return False                print(f"{Fore.GREEN}[✓] Certificate validation passed for {hostname}{Style.RESET_ALL}")        return True        def validate_connection(self, hostname, port=443):        """Validate SSL connection with pinning"""        cert = self.get_certificate(hostname, port)        if not cert:            return False                return self.validate_certificate(hostname, cert)        # ==================== REQUEST WRAPPERS ====================        def pinned_session(self):        """Create requests session with certificate pinning"""        class PinnedSession(requests.Session):            def __init__(self, pinner):                super().__init__()                self.pinner = pinner                        def request(self, method, url, **kwargs):                # Extract hostname                from urllib.parse import urlparse                hostname = urlparse(url).hostname                                # Validate certificate before request                if hostname in self.pinner.pins:                    if not self.pinner.validate_connection(hostname):                        raise Exception(f"Certificate validation failed for {hostname}")                                return super().request(method, url, **kwargs)                return PinnedSession(self)        def pinned_socket(self, hostname, port=443):        """Create pinned SSL socket"""        # Validate certificate first        if not self.validate_connection(hostname, port):            raise Exception(f"Certificate validation failed for {hostname}")                # Create SSL socket        context = ssl.create_default_context()        sock = socket.create_connection((hostname, port))        ssock = context.wrap_socket(sock, server_hostname=hostname)                return ssock # ==================== CLIENT EXAMPLES ==================== class PinnedHTTPClient:    """HTTP client with certificate pinning"""        def __init__(self, pinner):        self.pinner = pinner        self.session = pinner.pinned_session()        def get(self, url, **kwargs):        """Make GET request with pinning"""        return self.session.get(url, **kwargs)        def post(self, url, data=None, **kwargs):        """Make POST request with pinning"""        return self.session.post(url, data=data, **kwargs) class PinnedSocketClient:    """Socket client with certificate pinning"""        def __init__(self, pinner):        self.pinner = pinner        def connect(self, hostname, port=443):        """Connect with certificate validation"""        try:            ssock = self.pinner.pinned_socket(hostname, port)            return ssock        except Exception as e:            print(f"{Fore.RED}[!] Connection failed: {e}{Style.RESET_ALL}")            return None        def send_request(self, ssock, request):        """Send HTTP request over pinned socket"""        if ssock:            ssock.send(request.encode())            response = ssock.recv(4096)            return response        return None # ==================== DEMONSTRATION ==================== def demonstrate_pinning():    """Demonstrate certificate pinning"""        pinner = CertificatePinner("demo_pins.json")        print(f"""    {Fore.CYAN}╔═══════════════════════════════════════╗    ║     Certificate Pinning Demo          ║    ║       MITM Prevention Example         ║    ╚═══════════════════════════════════════╝{Style.RESET_ALL}    """)        # Example 1: Pin a certificate    print(f"\n{Fore.GREEN}1. Pinning certificate for example.com{Style.RESET_ALL}")    pinner.pin_certificate("example.com")        # Example 2: Validate connection    print(f"\n{Fore.GREEN}2. Validating connection to example.com{Style.RESET_ALL}")    pinner.validate_connection("example.com")        # Example 3: Simulate MITM attack (with different certificate)    print(f"\n{Fore.GREEN}3. Simulating MITM attack with different certificate{Style.RESET_ALL}")        # Create a fake certificate (simplified)    class FakeCertificate:        def public_bytes(self, encoding):            return b"fake_certificate"                def public_key(self):            class FakeKey:                def public_bytes(self, encoding, format):                    return b"fake_public_key"            return FakeKey()        fake_cert = FakeCertificate()        # This should fail    pinner.validate_certificate("example.com", fake_cert)        # Example 4: Using pinned HTTP client    print(f"\n{Fore.GREEN}4. Using pinned HTTP client{Style.RESET_ALL}")    client = PinnedHTTPClient(pinner)        try:        response = client.get("https://example.com")        print(f"   Status: {response.status_code}")    except Exception as e:        print(f"   Error: {e}")        # Example 5: List all pins    print(f"\n{Fore.GREEN}5. Current pins:{Style.RESET_ALL}")    for hostname, pin_data in pinner.pins.items():        print(f"   {hostname}:")        if 'certificate_fingerprint' in pin_data:            print(f"     Cert FP: {pin_data['certificate_fingerprint'][:20]}...")        if 'public_key_fingerprint' in pin_data:            print(f"     Key FP: {pin_data['public_key_fingerprint'][:20]}...")        if 'not_after' in pin_data:            print(f"     Expires: {pin_data['not_after']}") def main():    parser = argparse.ArgumentParser(description='Certificate Pinning Tool')    parser.add_argument('--pin', metavar='HOSTNAME', help='Pin certificate for hostname')    parser.add_argument('--check', metavar='HOSTNAME', help='Check pinned certificate')    parser.add_argument('--demo', action='store_true', help='Run demonstration')    parser.add_argument('--list', action='store_true', help='List all pins')        args = parser.parse_args()        # Try to import colorama, but don't fail if not available    try:        from colorama import init, Fore, Style        init(autoreset=True)    except ImportError:        # Create dummy colorama replacements        class Fore:            RED = GREEN = YELLOW = CYAN = ''        class Style:            RESET_ALL = ''        pinner = CertificatePinner()        if args.pin:        pinner.pin_certificate(args.pin)        elif args.check:        if pinner.validate_connection(args.check):            print(f"{Fore.GREEN}[✓] Certificate validation passed{Style.RESET_ALL}")        else:            print(f"{Fore.RED}[✗] Certificate validation failed{Style.RESET_ALL}")        elif args.list:        for hostname in pinner.pins:            print(hostname)        elif args.demo:        demonstrate_pinning()        else:        print("Use --demo for demonstration or --help for options") if __name__ == "__main__":    main()