Certificate Revocation via OCSP

TLS (Transport Layer Security) is fundamentally based on trust. Operating systems ship with a trusted set of certificates known as root certificate authorities (CAs) which are controlled by organizations that operating system vendors trust. These root certificates are self-signed, meaning they are cryptographically signed by themselves, so your trust in them is essentially political and based on the reputation of the issuing organization.

These root CAs are responsible for signing intermediate certificates, which in turn sign other intermediate certificates or end-entity (leaf) certificates, creating a hierarchical chain of trust. When a client connects to a server via TLS, the server presents its certificate chain, and the client must validate both the cryptographic integrity and the trustworthiness of this chain.

Certificates can become compromised or invalid for various reasons:

Without proper revocation mechanisms, compromised certificates would remain valid until their natural expiration date, which could be years away.

Certificate Revocation Methods

There are two primary methods for checking certificate revocation status:

Certificate Revocation Lists (CRL)

CRLs are periodically published lists of revoked certificates. They contain:

Advantages Disadvantages
Simple to implement and cache Can become large and unwieldy
No real-time network requests required Limited update frequency (typically daily or weekly)
Works offline once downloaded No real-time revocation status
Privacy concerns (reveals all revoked certificates)

Online Certificate Status Protocol (OCSP)

OCSP provides real-time certificate status checking. When a client needs to verify a certificate, it sends a request to the OCSP responder (usually operated by the CA) and receives an immediate response.

Advantages Disadvantages
Real-time revocation status Requires network connectivity
Smaller bandwidth usage (only checks specific certificates) Can introduce latency
More privacy-friendly (only reveals the specific certificate being checked) OCSP responders can be unavailable
Immediate revocation capability Privacy concerns (OCSP responder learns which sites users visit)

Understanding OCSP in Detail

OCSP Protocol Overview

OCSP operates over HTTP/HTTPS and uses ASN.1 encoding. The protocol consists of two main message types:

  1. OCSP Request: Contains the certificate to be checked and optional extensions
  2. OCSP Response: Contains the status of the certificate and is signed by the responder

OCSP Request Structure

An OCSP request contains:

OCSP Response Structure

An OCSP response contains:

Certificate Status Values

OCSP responses can indicate one of three statuses:

OCSP Stapling (TLS Certificate Status Request)

OCSP stapling is an optimization that addresses the privacy and performance concerns of traditional OCSP. Instead of clients querying OCSP responders directly, the web server queries the OCSP responder on behalf of its clients and "staples" the response to the TLS handshake.

How OCSP Stapling Works

  1. The web server periodically queries the OCSP responder for its own certificate status
  2. The server caches the signed OCSP response
  3. During TLS handshakes, the server includes the cached OCSP response in the Certificate Status Request extension
  4. Clients can verify the certificate status without making additional network requests

Benefits of OCSP Stapling

OCSP Stapling Requirements

Security Considerations

Privacy Concerns

Traditional OCSP has significant privacy implications:

OCSP stapling addresses these concerns by keeping OCSP queries server-side.

Performance and Reliability

OCSP introduces several performance considerations:

Security Best Practices

Implementation Guide

Let's implement a comprehensive certificate validation system that checks both the chain of trust and revocation status using OCSP. This implementation will be more robust and include proper error handling.

High-Level Design

Our implementation will follow this structure:

def validate_certificate(cert: bytes, check_revocation: bool = True):
"""
Comprehensive certificate validation including chain of trust and OCSP revocation checking.

Args:
cert: Certificate bytes in DER format
check_revocation: Whether to perform OCSP revocation checking

Returns:
dict: Validation results with detailed status information
"""
result = {
'valid': False,
'chain_valid': False,
'revocation_valid': None,
'errors': [],
'warnings': []
}

# Step 1: Validate chain of trust
try:
result['chain_valid'] = verify_chain_of_trust(cert)
if not result['chain_valid']:
    result['errors'].append('Certificate chain validation failed')
except Exception as e:
result['errors'].append(f'Chain validation error: {str(e)}')

# Step 2: Check revocation status if requested
if check_revocation and result['chain_valid']:
try:
    result['revocation_valid'] = verify_ocsp_revocation(cert)
    if not result['revocation_valid']:
        result['errors'].append('Certificate is revoked or OCSP check failed')
except Exception as e:
    result['warnings'].append(f'OCSP check failed: {str(e)}')
    # Soft fail: don't invalidate certificate if OCSP fails

# Overall validation result
result['valid'] = result['chain_valid'] and (result['revocation_valid'] is not False)

return result

Certificate Information Extraction

First, let's create robust functions to extract certificate information:

from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.backends import default_backend
import requests
import logging

def get_certificate_info(cert_bytes: bytes) -> dict:
"""
Extract comprehensive information from a certificate.

Args:
cert_bytes: Certificate in DER format

Returns:
dict: Certificate information including subject, issuer, OCSP URL, etc.
"""
try:
cert = x509.load_der_x509_certificate(cert_bytes, default_backend())

info = {
    'subject': get_common_name(cert.subject),
    'issuer': get_common_name(cert.issuer),
    'serial_number': cert.serial_number,
    'not_before': cert.not_valid_before,
    'not_after': cert.not_valid_after,
    'ocsp_url': get_ocsp_url(cert),
    'crl_url': get_crl_url(cert),
    'is_self_signed': cert.subject == cert.issuer,
    'key_usage': get_key_usage(cert),
    'extended_key_usage': get_extended_key_usage(cert)
}

return info
except Exception as e:
logging.error(f"Error extracting certificate info: {e}")
raise

def get_common_name(name):
"""Extract common name from X509 name."""
try:
return name.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
except IndexError:
return str(name)

def get_ocsp_url(cert):
"""Extract OCSP URL from certificate Authority Information Access extension."""
try:
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
for access_description in aia.value:
    if access_description.access_method._name == 'OCSP':
        url = access_description.access_location.value
        # Convert HTTPS to HTTP for OCSP (common practice)
        return url.replace('https://', 'http://')
except x509.extensions.ExtensionNotFound:
pass

return None

def get_crl_url(cert):
"""Extract CRL URL from certificate CRL Distribution Points extension."""
try:
crl_dp = cert.extensions.get_extension_for_oid(ExtensionOID.CRL_DISTRIBUTION_POINTS)
for dp in crl_dp.value:
    for name in dp.full_name:
        if name.type == x509.DNSName:
            return f"http://{name.value}"
except x509.extensions.ExtensionNotFound:
pass

return None

def get_key_usage(cert):
"""Extract key usage from certificate."""
try:
ku = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE)
return [flag for flag, value in ku.value.__dict__.items() if value]
except x509.extensions.ExtensionNotFound:
return []

def get_extended_key_usage(cert):
"""Extract extended key usage from certificate."""
try:
eku = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE)
return [usage._name for usage in eku.value]
except x509.extensions.ExtensionNotFound:
return []

Chain of Trust Validation

Enhanced chain validation with better error handling:

from OpenSSL import crypto
import tempfile
import os

def verify_chain_of_trust(cert_bytes: bytes, trusted_certs: list = None) -> bool:
"""
Verify the certificate chain of trust.

Args:
cert_bytes: Certificate to validate in DER format
trusted_certs: List of trusted certificates (intermediates and roots)

Returns:
bool: True if chain is valid, False otherwise
"""
try:
# Load the certificate to validate
cert = crypto.load_certificate(crypto.FILETYPE_ASN1, cert_bytes)

# Create certificate store
store = crypto.X509Store()

# Add trusted certificates to store
if trusted_certs:
    for trusted_cert_bytes in trusted_certs:
        try:
            trusted_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, trusted_cert_bytes)
            store.add_cert(trusted_cert)
        except Exception as e:
            logging.warning(f"Failed to add trusted certificate: {e}")

# Create store context and verify
store_ctx = crypto.X509StoreContext(store, cert)
result = store_ctx.verify_certificate()

# None means verification succeeded
return result is None

except Exception as e:
logging.error(f"Chain validation error: {e}")
return False

def load_certificate_chain(cert_file_path: str) -> dict:
"""
Load and parse a certificate chain from a PEM file.

Args:
cert_file_path: Path to PEM file containing certificate chain

Returns:
dict: Dictionary mapping subject names to certificate information
"""
certs = {}

try:
with open(cert_file_path, 'rb') as f:
    pem_data = f.read()

# Parse multiple certificates from PEM
for cert_num, cert_type, cert_bytes in pem.unarmor(pem_data, multiple=True):
    if cert_type == 'CERTIFICATE':
        cert_info = get_certificate_info(cert_bytes)
        certs[cert_info['subject']] = {
            'bytes': cert_bytes,
            'info': cert_info
        }

return certs

except Exception as e:
logging.error(f"Error loading certificate chain: {e}")
raise

OCSP Revocation Checking

Comprehensive OCSP implementation with proper error handling and timeouts:

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
import struct
import time

class OCSPChecker:
def __init__(self, timeout: int = 10, max_retries: int = 3):
self.timeout = timeout
self.max_retries = max_retries
self.session = requests.Session()

def check_certificate_revocation(self, cert_bytes: bytes, issuer_bytes: bytes) -> dict:
"""
Check certificate revocation status via OCSP.

Args:
    cert_bytes: Certificate to check in DER format
    issuer_bytes: Issuer certificate in DER format

Returns:
    dict: OCSP response with status and details
"""
try:
    # Load certificates
    cert = x509.load_der_x509_certificate(cert_bytes, default_backend())
    issuer = x509.load_der_x509_certificate(issuer_bytes, default_backend())
    
    # Get OCSP URL
    ocsp_url = get_ocsp_url(cert)
    if not ocsp_url:
        return {
            'status': 'unknown',
            'reason': 'No OCSP URL found in certificate',
            'valid': False
        }
    
    # Build OCSP request
    ocsp_request = self._build_ocsp_request(cert, issuer)
    
    # Send request with retries
    response = self._send_ocsp_request(ocsp_url, ocsp_request)
    
    # Parse response
    return self._parse_ocsp_response(response)
    
except Exception as e:
    logging.error(f"OCSP check failed: {e}")
    return {
        'status': 'error',
        'reason': str(e),
        'valid': False
    }

def _build_ocsp_request(self, cert: x509.Certificate, issuer: x509.Certificate) -> bytes:
"""Build OCSP request for certificate validation."""
from cryptography.hazmat.primitives.asymmetric import utils

# Create OCSP request
builder = x509.ocsp.OCSPRequestBuilder()
builder = builder.add_certificate(
    cert.public_bytes(serialization.Encoding.DER),
    issuer.public_bytes(serialization.Encoding.DER),
    hashes.SHA256()
)

# Add nonce for replay protection
nonce = os.urandom(16)
builder = builder.add_extension(
    x509.ocsp.OCSPNonce(nonce),
    critical=False
)

return builder.build().public_bytes(serialization.Encoding.DER)

def _send_ocsp_request(self, url: str, request_data: bytes) -> requests.Response:
"""Send OCSP request with retry logic."""
headers = {
    'Content-Type': 'application/ocsp-request',
    'Accept': 'application/ocsp-response'
}

for attempt in range(self.max_retries):
    try:
        response = self.session.post(
            url,
            data=request_data,
            headers=headers,
            timeout=self.timeout
        )
        
        if response.status_code == 200:
            return response
        else:
            logging.warning(f"OCSP request failed with status {response.status_code}")
            
    except requests.exceptions.RequestException as e:
        logging.warning(f"OCSP request attempt {attempt + 1} failed: {e}")
        if attempt == self.max_retries - 1:
            raise

raise Exception(f"OCSP request failed after {self.max_retries} attempts")

def _parse_ocsp_response(self, response: requests.Response) -> dict:
"""Parse OCSP response and extract status information."""
try:
    ocsp_response = x509.ocsp.load_der_ocsp_response(response.content)
    
    # Check response status
    if ocsp_response.response_status != x509.ocsp.OCSPResponseStatus.SUCCESSFUL:
        return {
            'status': 'error',
            'reason': f'OCSP response status: {ocsp_response.response_status}',
            'valid': False
        }
    
    # Get certificate status
    cert_status = ocsp_response.certificate_status
    
    if isinstance(cert_status, x509.ocsp.OCSPCertStatus.GOOD):
        return {
            'status': 'good',
            'reason': 'Certificate is valid and not revoked',
            'valid': True,
            'this_update': ocsp_response.this_update,
            'next_update': ocsp_response.next_update
        }
    elif isinstance(cert_status, x509.ocsp.OCSPCertStatus.REVOKED):
        return {
            'status': 'revoked',
            'reason': f'Certificate revoked at {cert_status.revocation_time}',
            'valid': False,
            'revocation_time': cert_status.revocation_time,
            'revocation_reason': cert_status.revocation_reason
        }
    else:
        return {
            'status': 'unknown',
            'reason': 'Certificate status unknown',
            'valid': False
        }
        
except Exception as e:
    logging.error(f"Error parsing OCSP response: {e}")
    return {
        'status': 'error',
        'reason': f'Failed to parse OCSP response: {e}',
        'valid': False
    }

def verify_ocsp_revocation(cert_bytes: bytes, cert_store: dict = None) -> bool:
"""
Verify certificate revocation status through the entire chain.

Args:
cert_bytes: Certificate to check
cert_store: Dictionary of certificates in the chain

Returns:
bool: True if no certificates are revoked, False otherwise
"""
if not cert_store:
cert_store = load_certificate_chain('./cc_certs/PT.pem')

ocsp_checker = OCSPChecker()
cert_info = get_certificate_info(cert_bytes)

# Check each certificate in the chain
current_cert_bytes = cert_bytes
current_cert_info = cert_info

while True:
# Get issuer certificate
issuer_name = current_cert_info['issuer']

if issuer_name not in cert_store:
    logging.warning(f"Issuer certificate '{issuer_name}' not found in store")
    return False

issuer_cert = cert_store[issuer_name]
issuer_bytes = issuer_cert['bytes']

# Check revocation status
ocsp_result = ocsp_checker.check_certificate_revocation(
    current_cert_bytes, 
    issuer_bytes
)

if not ocsp_result['valid']:
    logging.error(f"Certificate '{current_cert_info['subject']}' revocation check failed: {ocsp_result['reason']}")
    return False

# Stop if we've reached a self-signed certificate
if current_cert_info['is_self_signed']:
    break

# Move to next certificate in chain
current_cert_bytes = issuer_bytes
current_cert_info = issuer_cert['info']

return True

Complete Implementation Example

Here's a complete example showing how to use the enhanced certificate validation system:

def main():
"""Example usage of the certificate validation system."""
import json

# Load certificate store
try:
cert_store = load_certificate_chain('./cc_certs/PT.pem')
print(f"Loaded {len(cert_store)} certificates from store")
except Exception as e:
print(f"Failed to load certificate store: {e}")
return

# Example: Validate a specific certificate
# Replace with actual certificate bytes
example_cert_subject = 'Cartão de Cidadão 001'

if example_cert_subject in cert_store:
cert_bytes = cert_store[example_cert_subject]['bytes']

# Perform comprehensive validation
result = validate_certificate(cert_bytes, check_revocation=True)

# Print results
print("\nCertificate Validation Results:")
print(f"Overall Valid: {result['valid']}")
print(f"Chain Valid: {result['chain_valid']}")
print(f"Revocation Valid: {result['revocation_valid']}")

if result['errors']:
    print("\nErrors:")
    for error in result['errors']:
        print(f"  - {error}")

if result['warnings']:
    print("\nWarnings:")
    for warning in result['warnings']:
        print(f"  - {warning}")

# Print detailed certificate information
cert_info = cert_store[example_cert_subject]['info']
print(f"\nCertificate Details:")
print(f"  Subject: {cert_info['subject']}")
print(f"  Issuer: {cert_info['issuer']}")
print(f"  Valid From: {cert_info['not_before']}")
print(f"  Valid Until: {cert_info['not_after']}")
print(f"  OCSP URL: {cert_info['ocsp_url']}")
print(f"  CRL URL: {cert_info['crl_url']}")
print(f"  Is Self-Signed: {cert_info['is_self_signed']}")
else:
print(f"Certificate '{example_cert_subject}' not found in store")

if __name__ == "__main__":
main()

Real-World Implementation Considerations

Most modern web servers support OCSP stapling. Here are configuration examples:

Apache Configuration

# Enable OCSP stapling
SSLUseStapling on
SSLStaplingCache shmcb:/var/run/ocsp(128000)
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
SSLStaplingResponseMaxAge 86400

Nginx Configuration

# Enable OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/trusted_certs.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

OpenSSL Configuration

# Test OCSP stapling with OpenSSL
openssl s_client -connect example.com:443 -status -servername example.com

Browser Behavior

Different browsers handle OCSP differently:

Conclusion

Certificate revocation via OCSP is a critical component of a secure PKI infrastructure. While it has some limitations and privacy concerns, OCSP stapling addresses many of these issues and provides a robust solution for real-time certificate validation.

When implementing OCSP in production systems, remember to:

The implementation provided in this tutorial serves as a foundation for building robust certificate validation systems. Adapt it to your specific requirements and always test thoroughly in your environment.

Additional Resources