Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@ Custom X.509 Vectors
version.
* ``invalid-sct-length.der`` - A certificate with an SCT with an internal
length greater than the amount of data.
* Directory ``has_signature_of``, files
``{rsa,dsa,ecdsa,ed25519,ed448}{issuer,good_leaf,bad_leaf}.pem``
- triplets of (CA certificate, leaf certificate issued by the CA, same leaf
certificate with invalid signature) for the five supported signature
algorithms
* ``has_signature_of/bp-cert.pem`` - self-signed certificate using
``brainpoolP224t1`` curve for signature - one of curves not supported by
Cryptography.

Custom X.509 Request Vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
29 changes: 29 additions & 0 deletions docs/x509/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ X.509 Certificate Object
>>> cert.not_valid_after
datetime.datetime(2030, 12, 31, 8, 30)

.. method:: valid_at_time(time)

.. method:: ca_bit_set(allow_missing)

.. attribute:: issuer

.. versionadded:: 0.8
Expand Down Expand Up @@ -485,6 +489,14 @@ X.509 Certificate Object
:class:`~cryptography.exceptions.InvalidSignature`
exception will be raised if the signature fails to verify.


.. method:: alt_subject_name_matches_issuer(issuer_candidate, allow_missing)

.. method:: is_issued_by(issuer_candidate, is_issued_by_cb, extra_checks_cb)

.. method:: is_issuer_of(leaf_candidate, is_issued_by_cb, extra_checks_cb)


.. method:: public_bytes(encoding)

.. versionadded:: 1.0
Expand Down Expand Up @@ -3317,6 +3329,23 @@ Exceptions
types can be found in `RFC 5280 section 4.2.1.6`_.


.. class:: CertificateNotSuitable


.. class:: InvalidIssuer


.. class:: NotValidYet


.. class:: NotValidAnymore


.. class:: CABitNotSet


.. class:: IssuerAltSubjectNameMismatch

.. _`RFC 5280 section 4.2.1.1`: https://tools.ietf.org/html/rfc5280#section-4.2.1.1
.. _`RFC 5280 section 4.2.1.6`: https://tools.ietf.org/html/rfc5280#section-4.2.1.6
.. _`CABForum Guidelines`: https://cabforum.org/baseline-requirements-documents/
204 changes: 202 additions & 2 deletions src/cryptography/x509/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,31 @@
import typing

from cryptography import utils
from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
from cryptography.hazmat.backends import _get_backend
from cryptography.hazmat.backends.interfaces import Backend
from cryptography.hazmat.bindings._rust import x509 as rust_x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives import hashes, serialization, asymmetric
from cryptography.hazmat.primitives.asymmetric import (
dsa,
ec,
ed25519,
ed448,
rsa,
padding,
)
from cryptography.hazmat.primitives.asymmetric.types import (
PRIVATE_KEY_TYPES,
PUBLIC_KEY_TYPES,
)
from cryptography.x509.extensions import Extension, ExtensionType, Extensions
from cryptography.x509.extensions import (
BasicConstraints,
Extension,
ExtensionType,
ExtensionNotFound,
ExtensionOID,
Extensions,
)
from cryptography.x509.name import Name
from cryptography.x509.oid import ObjectIdentifier

Expand All @@ -38,6 +47,13 @@ def __init__(self, msg: str, oid: ObjectIdentifier) -> None:
self.oid = oid


class InvalidIssuer(Exception):
def __init__(self, own_issuer: Name, ca_subject: Name) -> None:
super(InvalidIssuer, self).__init__()
self.own_issuer = own_issuer
self.ca_subject = ca_subject


def _reject_duplicate_extension(
extension: Extension[ExtensionType],
extensions: typing.List[Extension[ExtensionType]],
Expand Down Expand Up @@ -189,6 +205,190 @@ def public_bytes(self, encoding: serialization.Encoding) -> bytes:
Serializes the certificate to PEM or DER format.
"""

def _has_signature_of(self, signer_candidate: "Certificate") -> bool:
"""
Returns True if the certificate holds a valid signature by
`signer_candidate`.
Raises appropriate exception otherwise.
No other checks, e.g. comparison of issuer and leaf names, is done.
"""
try:
pubkey = signer_candidate.public_key()
except ValueError as e:
# Backend is unable to get the public key
raise UnsupportedAlgorithm(
str(e), _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
)

signature = self.signature
data = self.tbs_certificate_bytes
if isinstance(pubkey, asymmetric.rsa.RSAPublicKeyWithSerialization):
assert isinstance(
self.signature_hash_algorithm, hashes.HashAlgorithm
)
pubkey.verify(
signature,
data,
padding=asymmetric.padding.PKCS1v15(),
algorithm=self.signature_hash_algorithm,
)
elif isinstance(pubkey, asymmetric.dsa.DSAPublicKeyWithSerialization):
assert isinstance(
self.signature_hash_algorithm, hashes.HashAlgorithm
)
pubkey.verify(
signature,
data,
algorithm=self.signature_hash_algorithm,
)
elif isinstance(
pubkey, asymmetric.ec.EllipticCurvePublicKeyWithSerialization
):
assert isinstance(
self.signature_hash_algorithm, hashes.HashAlgorithm
)
pubkey.verify(
signature,
data,
signature_algorithm=asymmetric.ec.ECDSA(
self.signature_hash_algorithm
),
)
elif isinstance(
pubkey,
(
asymmetric.ed25519.Ed25519PublicKey,
asymmetric.ed448.Ed448PublicKey,
),
):
pubkey.verify(signature, data)
else:
# Should not happen, all PUBLIC_KEY_TYPES are tried
raise UnsupportedAlgorithm( # pragma: no cover
"Signature algorithm is not supported",
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
)

return True

def valid_at_time(self, date: datetime.datetime) -> bool:
"""Return True if the `date` is within certificate's validity time.
Raise appropriate exception otherwise."""
if self.not_valid_after < date:
raise NotValidAnymore(self, date)
if self.not_valid_before > date:
raise NotValidYet(self, date)
return True

def ca_bit_set(self, allow_missing=True) -> bool:
try:
basic_constraints = self.extensions.get_extension_for_class(
BasicConstraints
)
except ExtensionNotFound:
if not allow_missing:
raise CABitNotSet(self)
else:
if not basic_constraints.value.ca:
raise CABitNotSet(self)
return True

def alt_subject_name_matches_issuer(
self, ca: "Certificate", allow_missing=True
) -> bool:
try:
subj_alt_name = ca.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
issuer_alt_name = self.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
except ExtensionNotFound:
if allow_missing:
return True
else:
raise
if subj_alt_name != issuer_alt_name:
raise IssuerAltSubjectNameMismatch(self, ca)
return True

@staticmethod
def is_issued_by_default_cb(
leaf: "Certificate", ca: "Certificate"
) -> bool:
"""
Default checks which must hold true to claim that `leaf` is issued by `ca`.
This callback can be overwritten. It must return True or raise an exception otherwise.
"""

# Remains: KeySign EKU

today = datetime.datetime.today()
leaf.valid_at_time(today)
ca.valid_at_time(today)
ca.ca_bit_set()
leaf.alt_subject_name_matches_issuer(ca)
return True

def is_issued_by(
self,
issuer_candidate: "Certificate",
is_issued_by_cb=is_issued_by_default_cb,
extra_checks_cb=None,
) -> bool:
"""
Returns True if the certificate is issued by `issuer_candidate`.
"""
if self.issuer != issuer_candidate.subject:
raise InvalidIssuer(
own_issuer=self.issuer, ca_subject=issuer_candidate.subject
)
self._has_signature_of(issuer_candidate)
is_issued_by_cb(self, issuer_candidate)
if extra_checks_cb is not None:
extra_checks_cb(self, issuer_candidate)
return True

def is_issuer_of(
self,
issued_candidate: "Certificate",
is_issued_by_cb=is_issued_by_default_cb,
extra_checks_cb=None,
) -> bool:
"""
Returns True if the `issued_candidate` is issued by the certificate.
"""
return issued_candidate.is_issued_by(
self, is_issued_by_cb, extra_checks_cb
)


class CertificateNotSuitable(Exception):
def __init__(self, certificate: Certificate):
self.certificate = certificate


class NotValidAnymore(CertificateNotSuitable):
def __init__(self, certificate: Certificate, at_time: datetime.datetime):
super(NotValidAnymore, self).__init__(certificate)
self.at_time = at_time


class NotValidYet(CertificateNotSuitable):
def __init__(self, certificate: Certificate, at_time: datetime.datetime):
super(NotValidYet, self).__init__(certificate)
self.at_time = at_time


class CABitNotSet(CertificateNotSuitable):
pass


class IssuerAltSubjectNameMismatch(CertificateNotSuitable):
def __init__(self, certificate: Certificate, ca: Certificate):
super(IssuerAltSubjectNameMismatch, self).__init__(certificate)
self.ca = ca


# Runtime isinstance checks need this since the rust class is not a subclass.
Certificate.register(rust_x509.Certificate)
Expand Down
41 changes: 40 additions & 1 deletion tests/x509/test_x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import pytz

from cryptography import utils, x509
from cryptography.exceptions import UnsupportedAlgorithm
from cryptography.hazmat.bindings._rust import asn1
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import (
dh,
Expand Down Expand Up @@ -4963,3 +4963,42 @@ def notrandom(size):

assert serial_number == int.from_bytes(sample_data, "big") >> 1
assert serial_number.bit_length() < 160


class TestHasSignatureOf(object):
@staticmethod
def load(backend, filename):
return _load_cert(
os.path.join("x509", "has_signature_of", filename),
x509.load_pem_x509_certificate,
backend,
)

@pytest.mark.parametrize("key_type", ["rsa", "dsa", "ecdsa"])
def test_signature_with_key_type(self, backend, key_type):
issuer = self.load(backend, key_type + "_issuer.pem")
good_leaf = self.load(backend, key_type + "_good_leaf.pem")
assert good_leaf._has_signature_of(issuer)
bad_leaf = self.load(backend, key_type + "_bad_leaf.pem")
with pytest.raises(InvalidSignature):
bad_leaf._has_signature_of(issuer)

@pytest.mark.supported(
only_if=lambda backend: backend.ed25519_supported(),
skip_message="Requires backend with Ed25519 support",
)
def test_ed25519_signature(self, backend):
self.test_signature_with_key_type(backend, "ed25519")

@pytest.mark.supported(
only_if=lambda backend: backend.ed448_supported(),
skip_message="Requires backend with Ed448 support",
)
def test_ed448_signature(self, backend):
self.test_signature_with_key_type(backend, "ed448")

def test_unsupported_curve(self, backend):
# bp-cert.pem uses brainpoolP224t1, which is not in ec._CURVE_TYPES
unsupported_cert = self.load(backend, "bp-cert.pem")
with pytest.raises(UnsupportedAlgorithm):
unsupported_cert._has_signature_of(unsupported_cert)
10 changes: 10 additions & 0 deletions vectors/cryptography_vectors/x509/has_signature_of/bp-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBYTCCARCgAwIBAgIUaCMW+T+3ZPSVxrEhN9GlAiSj52QwCgYIKoZIzj0EAwIw
DjEMMAoGA1UEAwwDZm9vMB4XDTIxMDMyMTE5MjA0NFoXDTIxMDQyMDE5MjA0NFow
DjEMMAoGA1UEAwwDZm9vMFIwFAYHKoZIzj0CAQYJKyQDAwIIAQEGAzoABGAcW7kC
nwkbbZPmBY+oYC575lvUmT+8IdogWnexhPLtDfOPeT+e4NBkucox8qThx5Wzrk65
gb3Co1MwUTAdBgNVHQ4EFgQUElZUaqbrqXCtuoMccyG8PX2Yz9AwHwYDVR0jBBgw
FoAUElZUaqbrqXCtuoMccyG8PX2Yz9AwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjO
PQQDAgM/ADA8Ahx6gdp/bL70RrgMcaaKXOW6OVa9z8KTpngrOZMeAhw0OfDR1LF0
rkC+qBkgueUPWuqGPd1TWL0wAgpf
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC1DCCApQCCQDL8Ub6cdNJPzAJBgcqhkjOOAQDMBkxFzAVBgNVBAMMDkNBIGNl
cnRpZmljYXRlMB4XDTIwMDIwOTEwMTA0OVoXDTIwMDMxMDEwMTA0OVowGzEZMBcG
A1UEAwwQTGVhZiBjZXJ0aWZpY2F0ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBANzNrj5f0qNSeDCHqFqgYS4vn6+IJ/lF3OEPZRM3OqB30ZcTjo8aqNzL
zed7MwcudX+O5yCzkKg4Ix9R+MnfZTLC5fX39cacv1sMZxLmmYPj7HkpUhb6pU62
gJH09LoyeLPWe08e6yUxGHh687UWJFEbupnAs10Kt4oQjvqH2a05ZF8qg+xvreeq
g9aXo0vZhM9vKmDL/vSKvhC4CClGpjRzEcb09RUWUCVC6ODFdrYB6RCHW4vdBX+J
z5Sj0bFlHYSGNU2egc3Fg8Ukl/bccKdkifBrW9vxCj/jHRDcE+7/3Lrc1VWJnOsX
T74IMv75ENBCtJpJob7x2j7Tc0AurqJJaHjqDAkcn85BLKY2G2e6p3FC44rBqTNK
yi/s5sBsjDkrMzKHWE2xQiFjHQb4AgvHASdNvFNUUS/znHDQNsp22zjjuL8JCs23
e5imBKFVDPTdlkO4Mu7IQNzT0M8dRx5Toeudg9XMvlB9zk+FnqX+qQgc1977oyZl
ezqGC0yVOIoF1BjSJ2bE6t3l2dm/lJ/N9s+WUYQjjHgV2zFtOrj/VZsmLZDytHYd
dM8+xXZfeM54Fs19iotSL6IRjNtRiTRqTtsvYeimJonMqEPsr51IJsOAMl++OwlB
p6TsxxjjIMIA+lJ19a1Rv3LE8kWBVYVGT1XvP445j8bXt0mpkwDFAgMBAAEwCQYH
KoZIzjgEAwMvADAsAhRw2AP3fPwkumYNkvAjwk4Nl+I4SgIUFcI3QI70aKth4Rfl
YQ3H28KpoJ4=
-----END CERTIFICATE-----
Loading