pyhanko-certvalidator
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
- breaking Starting with version 0.17.0, the library underwent significant refactoring to favour asynchronous I/O. While most high-level API entrypoints can still be used synchronously, their `asyncio` equivalents are now preferred and many synchronous methods have been deprecated.
- breaking The package was renamed from `certvalidator` to `pyhanko_certvalidator` to prevent namespace collisions, as the library significantly diverged from the original `wbond/certvalidator` project.
- gotcha This library is a fork of `wbond/certvalidator` and has diverged considerably. While basic usage might be similar, specific features, internal workings, and advanced configurations may differ. Directly swapping between the two without review may lead to unexpected behaviour.
- gotcha GitHub issues are disabled on the `pyhanko-certvalidator` repository. Bug reports and usage questions should be submitted to the main `pyHanko` issue tracker or discussion forum.
- gotcha While `pyhanko-certvalidator` previously supported Python 3.7+, the broader `pyHanko` project and its latest releases now require Python 3.10 or later for full compatibility and intended functionality.
Install
-
pip install pyhanko-certvalidator
Imports
- ValidationContext
from pyhanko_certvalidator import ValidationContext
- CertificateValidator
from pyhanko_certvalidator import CertificateValidator
- errors
from pyhanko_certvalidator import errors
- ValidationContext
Quickstart
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())