Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change error on empty subjectAltNames #67

Merged
merged 6 commits into from
Jan 14, 2024
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ You can find out backwards-compatibility policy [here](https://github.com/pyca/s

## [Unreleased](https://github.com/pyca/service-identity/compare/23.1.0...HEAD)

### Changed

- If a certificate doesn't contain any `subjectAltName`s, we now raise `service_identity.exceptions.CertificateError` instead of `service_identity.exceptions.VerificationError` to make the problem easier to debug.
[#67](https://github.com/pyca/service-identity/pull/67)


## [23.1.0](https://github.com/pyca/service-identity/compare/21.1.0...23.1.0) - 2023-06-14

### Removed
Expand Down
71 changes: 46 additions & 25 deletions src/service_identity/cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,31 @@
def verify_certificate_hostname(
certificate: Certificate, hostname: str
) -> None:
"""
r"""
Verify whether *certificate* is valid for *hostname*.

.. note:: Nothing is verified about the *authority* of the certificate;
the caller must verify that the certificate chains to an appropriate
trust root themselves.
.. note::
Nothing is verified about the *authority* of the certificate;
the caller must verify that the certificate chains to an appropriate
trust root themselves.

Args:
certificate: A *cryptography* X509 certificate object.

hostname: The hostname that *certificate* should be valid for.

:param certificate: A *cryptography* X509 certificate object.
:param hostname: The hostname that *certificate* should be valid for.
Raises:
service_identity.VerificationError:
If *certificate* is not valid for *hostname*.

:raises service_identity.VerificationError: If *certificate* is not valid
for *hostname*.
:raises service_identity.CertificateError: If *certificate* contains
invalid / unexpected data.
service_identity.CertificateError:
If *certificate* contains invalid / unexpected data. This includes
the case where the certificate contains no `subjectAltName`\ s.

:returns: ``None``
.. versionchanged:: 24.1.0
:exc:`~service_identity.CertificateError` is raised if the certificate
contains no ``subjectAltName``\ s instead of
:exc:`~service_identity.VerificationError`.
"""
verify_service_identity(
cert_patterns=extract_patterns(certificate),
Expand All @@ -67,25 +76,35 @@ def verify_certificate_hostname(
def verify_certificate_ip_address(
certificate: Certificate, ip_address: str
) -> None:
"""
r"""
Verify whether *certificate* is valid for *ip_address*.

.. note:: Nothing is verified about the *authority* of the certificate;
the caller must verify that the certificate chains to an appropriate
trust root themselves.
.. note::
Nothing is verified about the *authority* of the certificate;
the caller must verify that the certificate chains to an appropriate
trust root themselves.

Args:
certificate: A *cryptography* X509 certificate object.

:param certificate: A *cryptography* X509 certificate object.
:param ip_address: The IP address that *connection* should be valid
for. Can be an IPv4 or IPv6 address.
ip_address:
The IP address that *connection* should be valid for. Can be an
IPv4 or IPv6 address.

:raises service_identity.VerificationError: If *certificate* is not valid
for *ip_address*.
:raises service_identity.CertificateError: If *certificate* contains
invalid / unexpected data.
Raises:
service_identity.VerificationError:
If *certificate* is not valid for *ip_address*.

:returns: ``None``
service_identity.CertificateError:
If *certificate* contains invalid / unexpected data. This includes
the case where the certificate contains no ``subjectAltName``\ s.

.. versionadded:: 18.1.0

.. versionchanged:: 24.1.0
:exc:`~service_identity.CertificateError` is raised if the certificate
contains no ``subjectAltName``\ s instead of
:exc:`~service_identity.VerificationError`.
"""
verify_service_identity(
cert_patterns=extract_patterns(certificate),
Expand All @@ -101,9 +120,11 @@ def extract_patterns(cert: Certificate) -> Sequence[CertificatePattern]:
"""
Extract all valid ID patterns from a certificate for service verification.

:param cert: The certificate to be dissected.
Args:
cert: The certificate to be dissected.

:return: List of IDs.
Returns:
List of IDs.

.. versionchanged:: 23.1.0
``commonName`` is not used as a fallback anymore.
Expand Down
5 changes: 5 additions & 0 deletions src/service_identity/hazmat.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def verify_service_identity(
*obligatory_ids* must be both present and match. *optional_ids* must match
if a pattern of the respective type is present.
"""
if not cert_patterns:
hynek marked this conversation as resolved.
Show resolved Hide resolved
raise CertificateError(
"Certificate does not contain any `subjectAltName`s."
)

errors = []
matches = _find_matches(cert_patterns, obligatory_ids) + _find_matches(
cert_patterns, optional_ids
Expand Down
62 changes: 41 additions & 21 deletions src/service_identity/pyopenssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,28 @@


def verify_hostname(connection: Connection, hostname: str) -> None:
"""
r"""
Verify whether the certificate of *connection* is valid for *hostname*.

:param connection: A pyOpenSSL connection object.
:param hostname: The hostname that *connection* should be connected to.
Args:
connection: A pyOpenSSL connection object.

hostname: The hostname that *connection* should be connected to.

Raises:
service_identity.VerificationError:
If *connection* does not provide a certificate that is valid for
*hostname*.

:raises service_identity.VerificationError: If *connection* does not
provide a certificate that is valid for *hostname*.
:raises service_identity.CertificateError: If the certificate chain of
*connection* contains a certificate that contains invalid/unexpected
data.
service_identity.CertificateError:
If certificate provided by *connection* contains invalid /
unexpected data. This includes the case where the certificate
contains no ``subjectAltName``\ s.

:returns: ``None``
.. versionchanged:: 24.1.0
:exc:`~service_identity.CertificateError` is raised if the certificate
contains no ``subjectAltName``\ s instead of
:exc:`~service_identity.VerificationError`.
"""
verify_service_identity(
cert_patterns=extract_patterns(
Expand All @@ -61,22 +70,31 @@ def verify_hostname(connection: Connection, hostname: str) -> None:


def verify_ip_address(connection: Connection, ip_address: str) -> None:
"""
r"""
Verify whether the certificate of *connection* is valid for *ip_address*.

:param connection: A pyOpenSSL connection object.
:param ip_address: The IP address that *connection* should be connected to.
Can be an IPv4 or IPv6 address.
Args:
connection: A pyOpenSSL connection object.

ip_address:
The IP address that *connection* should be connected to. Can be an
IPv4 or IPv6 address.

:raises service_identity.VerificationError: If *connection* does not
provide a certificate that is valid for *ip_address*.
:raises service_identity.CertificateError: If the certificate chain of
*connection* contains a certificate that contains invalid/unexpected
data.
Raises:
service_identity.VerificationError:
If *connection* does not provide a certificate that is valid for
*ip_address*.

:returns: ``None``
service_identity.CertificateError:
If the certificate chain of *connection* contains a certificate
that contains invalid/unexpected data.

.. versionadded:: 18.1.0

.. versionchanged:: 24.1.0
:exc:`~service_identity.CertificateError` is raised if the certificate
contains no ``subjectAltName``\ s instead of
:exc:`~service_identity.VerificationError`.
"""
verify_service_identity(
cert_patterns=extract_patterns(
Expand All @@ -94,9 +112,11 @@ def extract_patterns(cert: X509) -> Sequence[CertificatePattern]:
"""
Extract all valid ID patterns from a certificate for service verification.

:param cert: The certificate to be dissected.
Args:
cert: The certificate to be dissected.

:return: List of IDs.
Returns:
List of IDs.

.. versionchanged:: 23.1.0
``commonName`` is not used as a fallback anymore.
Expand Down
File renamed without changes.
31 changes: 30 additions & 1 deletion tests/test_cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
verify_certificate_ip_address,
)
from service_identity.exceptions import (
CertificateError,
DNSMismatch,
IPAddressMismatch,
VerificationError,
Expand All @@ -24,7 +25,12 @@
URIPattern,
)

from .util import PEM_CN_ONLY, PEM_DNS_ONLY, PEM_EVERYTHING, PEM_OTHER_NAME
from .certificates import (
PEM_CN_ONLY,
PEM_DNS_ONLY,
PEM_EVERYTHING,
PEM_OTHER_NAME,
)


backend = default_backend()
Expand All @@ -35,6 +41,29 @@


class TestPublicAPI:
def test_no_cert_patterns_hostname(self):
"""
A certificate without subjectAltNames raises a helpful
CertificateError.
"""
with pytest.raises(
CertificateError,
match="Certificate does not contain any `subjectAltName`s.",
):
verify_certificate_hostname(X509_CN_ONLY, "example.com")

@pytest.mark.parametrize("ip", ["203.0.113.0", "2001:db8::"])
def test_no_cert_patterns_ip_address(self, ip):
"""
A certificate without subjectAltNames raises a helpful
CertificateError.
"""
with pytest.raises(
CertificateError,
match="Certificate does not contain any `subjectAltName`s.",
):
verify_certificate_ip_address(X509_CN_ONLY, ip)

def test_certificate_verify_hostname_ok(self):
"""
verify_certificate_hostname succeeds if the hostnames match.
Expand Down
14 changes: 13 additions & 1 deletion tests/test_hazmat.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
verify_service_identity,
)

from .certificates import DNS_IDS
from .test_cryptography import CERT_EVERYTHING
from .util import DNS_IDS


try:
Expand All @@ -45,6 +45,18 @@ class TestVerifyServiceIdentity:
Simple integration tests for verify_service_identity.
"""

def test_no_cert_patterns(self):
"""
Empty cert patterns raise a helpful CertificateError.
"""
with pytest.raises(
CertificateError,
match="Certificate does not contain any `subjectAltName`s.",
):
verify_service_identity(
cert_patterns=[], obligatory_ids=[], optional_ids=[]
)

def test_dns_id_success(self):
"""
Return pairs of certificate ids and service ids on matches.
Expand Down
7 changes: 6 additions & 1 deletion tests/test_pyopenssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
verify_ip_address,
)

from .util import PEM_CN_ONLY, PEM_DNS_ONLY, PEM_EVERYTHING, PEM_OTHER_NAME
from .certificates import (
PEM_CN_ONLY,
PEM_DNS_ONLY,
PEM_EVERYTHING,
PEM_OTHER_NAME,
)


if pytest.importorskip("OpenSSL"):
Expand Down