pyhanko-certvalidator

0.30.2 · active · verified Fri Apr 10

pyhanko-certvalidator is a Python library designed for robust validation of X.509 certificates and certificate paths. Originally forked from wbond/certvalidator, it has since diverged significantly, incorporating features and architectural changes tailored for the pyHanko ecosystem, particularly for PDF digital signature validation. It supports advanced features such as revocation checks (CRLs and OCSP), point-in-time validation, policy constraints, and various signature algorithms. The current version is 0.30.2, and it follows a regular release cadence as part of the broader pyHanko project.

Warnings

Install

Imports

Quickstart

This example demonstrates how to perform a basic certificate path validation using `pyhanko-certvalidator`. It generates a synthetic certificate chain (Root CA -> Intermediate CA -> End-Entity) and then uses `ValidationContext` and `CertificateValidator` to verify the end-entity certificate against the trusted root. It showcases the asynchronous `async_validate_tls` method, which is the recommended approach for modern usage.

import asyncio
from datetime import datetime, timedelta

from asn1crypto import x509, pem
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from pyhanko_certvalidator import ValidationContext, CertificateValidator, errors

async def run_validation_example():
    # 1. Generate a self-signed root CA certificate
    root_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())
    root_subject = x509.Name([
        x509.NameAttribute('2.5.4.6', 'US'),
        x509.NameAttribute('2.5.4.10', 'Root CA Inc.'),
        x509.NameAttribute('2.5.4.3', 'Example Root CA')
    ])
    root_cert = x509.CertificateBuilder().
        subject_name(root_subject).
        issuer_name(root_subject).
        public_key(root_key.public_key()).
        serial_number(x509.random_serial_number()).
        not_valid_before(datetime.utcnow() - timedelta(days=1)).
        not_valid_after(datetime.utcnow() + timedelta(days=3650)).
        add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True).
        add_extension(x509.KeyUsage(key_cert_sign=True, crl_sign=True), critical=True).
        sign(root_key, hashes.SHA256(), default_backend())

    # 2. Generate an intermediate CA certificate signed by the root CA
    intermediate_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())
    intermediate_subject = x509.Name([
        x509.NameAttribute('2.5.4.6', 'US'),
        x509.NameAttribute('2.5.4.10', 'Intermediate CA Corp.'),
        x509.NameAttribute('2.5.4.3', 'Example Intermediate CA')
    ])
    intermediate_cert = x509.CertificateBuilder().
        subject_name(intermediate_subject).
        issuer_name(root_subject).
        public_key(intermediate_key.public_key()).
        serial_number(x509.random_serial_number()).
        not_valid_before(datetime.utcnow() - timedelta(days=1)).
        not_valid_after(datetime.utcnow() + timedelta(days=1825)).
        add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True).
        add_extension(x509.KeyUsage(key_cert_sign=True, crl_sign=True), critical=True).
        sign(root_key, hashes.SHA256(), default_backend())

    # 3. Generate an end-entity certificate signed by the intermediate CA
    ee_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())
    ee_subject = x509.Name([
        x509.NameAttribute('2.5.4.6', 'US'),
        x509.NameAttribute('2.5.4.10', 'End-Entity Dept.'),
        x509.NameAttribute('2.5.4.3', 'example.com')
    ])
    ee_cert = x509.CertificateBuilder().
        subject_name(ee_subject).
        issuer_name(intermediate_subject).
        public_key(ee_key.public_key()).
        serial_number(x509.random_serial_number()).
        not_valid_before(datetime.utcnow() - timedelta(days=1)).
        not_valid_after(datetime.utcnow() + timedelta(days=365)).
        add_extension(x509.BasicConstraints(ca=False), critical=True).
        add_extension(x509.KeyUsage(digital_signature=True, key_encipherment=True), critical=True).
        add_extension(x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]), critical=False).
        sign(intermediate_key, hashes.SHA256(), default_backend())

    # Prepare certificates for validation
    root_cert_asn1 = x509.Certificate.load(root_cert.public_bytes(encoding=serialization.Encoding.DER))
    intermediate_cert_asn1 = x509.Certificate.load(intermediate_cert.public_bytes(encoding=serialization.Encoding.DER))
    ee_cert_asn1 = x509.Certificate.load(ee_cert.public_bytes(encoding=serialization.Encoding.DER))

    # 4. Create a ValidationContext with the root CA as trust anchor
    validation_context = ValidationContext(
        trust_roots=[root_cert_asn1],
        # For more complex scenarios, you might add 'intermediate_certs' or enable fetching:
        intermediate_certs=[intermediate_cert_asn1],
        allow_fetching=False # Set to True to allow HTTP fetching of CRLs/OCSP
    )

    # 5. Validate the end-entity certificate path
    try:
        validator = CertificateValidator(ee_cert_asn1, [], validation_context)
        valid_paths = await validator.async_validate_tls()
        print(f"Certificate for {ee_cert_asn1.subject.human_friendly} is VALID.")
        print("Validated paths found:")
        for path in valid_paths:
            print(f" - {len(path.certs)} certificates in path")
    except errors.PathValidationError as e:
        print(f"Certificate validation FAILED: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

if __name__ == '__main__':
    asyncio.run(run_validation_example())

view raw JSON →