Skip to content

No certificate chain validation for signature verification #17

@lnagel

Description

@lnagel

Summary

The library verifies cryptographic signatures but does not validate that signing certificates chain to a trusted root CA. A signature from an untrusted or self-signed certificate will pass verification.

Current Verification Flow

When verifying a signature, the flow is:

Container.verify_signatures() (container.py:249-256)
  → XmlSignature.verify() (xmlsig.py:265-271)
    → signature_verifier.verify() (signature_verifier.py:35-76)

signature_verifier.verify() only performs cryptographic verification:

# signature_verifier.py:35-76
def verify(certificate, signature, data, hash_algo="SHA256", prehashed=False):
    """Verify RSA and EC signatures with the cryptography library"""
    if not isinstance(certificate, Certificate):
        certificate = load_der_x509_certificate(certificate, default_backend())
    # ...
    public_key = certificate.public_key()
    # Only verifies the signature is mathematically valid
    public_key.verify(signature, data, padding.PKCS1v15(), chosen_hash)

What's Missing

1. No chain building or path validation

The signing certificate is extracted and used directly. There is no validation that:

  • The certificate was issued by a trusted CA
  • Intermediate certificates form a valid chain to a root
  • Each issuer certificate has CA=True in BasicConstraints

2. No certificate validity period check

certificate.not_valid_before and certificate.not_valid_after are never checked against the signing time.

3. No key usage validation

The signing certificate should have digitalSignature in its key usage extension. This is not verified.

4. Root CA stored but never validated against

XmlSignature.set_root_ca_cert() at xmlsig.py:328-334 stores a root CA certificate, but it's never used to validate the chain:

# xmlsig.py:328-334
def set_root_ca_cert(self, root_ca_cert: bytes):
    """Set the root CA cert that issued the OCSP responder's certificate"""
    el = self._find_element(f".//{XmlSignature.XADES_NS}CertificateValues")
    SubElement(el, f"{XmlSignature.XADES_NS}EncapsulatedX509Certificate").text = (
        b64encode(root_ca_cert).decode("ascii")
    )
    return self

Acknowledged Gap in OCSP

The codebase explicitly acknowledges this issue in ocsp.py:134-137:

"""
Verify the OCSP response signature.

Ideally this should also verify the signer certificate, as does openssl with:

    openssl ocsp -respin ocsp.der
"""

The OCSP responder certificate is extracted and used to verify the response signature (ocsp.py:163-182), but the responder certificate itself is never validated for chain, expiration, or EKU.

Impact

  • Signatures from self-signed certificates pass verification
  • Signatures from expired certificates pass verification
  • Signatures from certificates issued by untrusted CAs pass verification
  • Cannot distinguish qualified signatures (from QTSPs) from unqualified ones

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions