Skip to content

Commit

Permalink
remove several functions related to old OpenSSL style subject format
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiasertl committed Dec 28, 2024
1 parent 8800561 commit a5666ed
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 70 deletions.
17 changes: 16 additions & 1 deletion ca/django_ca/migration_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,29 @@
that they are tested properly.
"""

import shlex
import typing
import warnings
from collections.abc import Iterator
from typing import Optional

from cryptography import x509
from cryptography.x509.oid import AuthorityInformationAccessOID, ExtensionOID

from django_ca.utils import format_general_name, parse_general_name, split_str
from django_ca.utils import format_general_name, parse_general_name


def split_str(val: str, sep: str) -> Iterator[str]:
"""Split a character on the given set of characters.
This function was originally in ``django_ca.utils`` but has since been deprecated/removed. We keep a copy
here so that the function keeps working.
"""
lex = shlex.shlex(val, posix=True)
lex.commenters = ""
lex.whitespace = sep
lex.whitespace_split = True
yield from lex


class Migration0040Helper:
Expand Down
37 changes: 24 additions & 13 deletions ca/django_ca/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import itertools
import os
import typing
import unittest
from collections.abc import Iterable
from datetime import datetime, timedelta, timezone as tz
from pathlib import Path
Expand All @@ -37,7 +36,7 @@

from django_ca import utils
from django_ca.conf import model_settings
from django_ca.tests.base.constants import CRYPTOGRAPHY_VERSION
from django_ca.tests.base.assertions import assert_removed_in_230
from django_ca.tests.base.doctest import doctest_module
from django_ca.tests.base.utils import cn, country, dns
from django_ca.typehints import SerializedObjectIdentifier
Expand Down Expand Up @@ -98,7 +97,8 @@ def test_parse_serialized_name_attributes(
serialized: list[SerializedObjectIdentifier] = [
{"oid": attr[0].dotted_string, "value": attr[1]} for attr in attributes
]
assert parse_serialized_name_attributes(serialized) == expected
with assert_removed_in_230():
assert parse_serialized_name_attributes(serialized) == expected


class GeneratePrivateKeyTestCase(TestCase):
Expand Down Expand Up @@ -147,19 +147,23 @@ class SerializeName(TestCase):

def test_name(self) -> None:
"""Test passing a standard Name."""
assert serialize_name(x509.Name([cn("example.com")])) == [{"oid": "2.5.4.3", "value": "example.com"}]
assert serialize_name(x509.Name([country("AT"), cn("example.com")])) == [
{"oid": "2.5.4.6", "value": "AT"},
{"oid": "2.5.4.3", "value": "example.com"},
]
with assert_removed_in_230():
assert serialize_name(x509.Name([cn("example.com")])) == [
{"oid": "2.5.4.3", "value": "example.com"}
]
with assert_removed_in_230():
assert serialize_name(x509.Name([country("AT"), cn("example.com")])) == [
{"oid": "2.5.4.6", "value": "AT"},
{"oid": "2.5.4.3", "value": "example.com"},
]

@unittest.skipIf(CRYPTOGRAPHY_VERSION < (37, 0), "cg<36 does not yet have bytes.")
def test_bytes(self) -> None:
"""Test names with byte values - probably never happens."""
name = x509.Name(
[x509.NameAttribute(NameOID.X500_UNIQUE_IDENTIFIER, b"example.com", _type=_ASN1Type.BitString)]
)
assert serialize_name(name) == [{"oid": "2.5.4.45", "value": "65:78:61:6D:70:6C:65:2E:63:6F:6D"}]
with assert_removed_in_230():
assert serialize_name(name) == [{"oid": "2.5.4.45", "value": "65:78:61:6D:70:6C:65:2E:63:6F:6D"}]


@pytest.mark.parametrize(
Expand Down Expand Up @@ -354,13 +358,20 @@ def test_str(self) -> None:
("CN", "example.com"),
("emailAddress", "[email protected]"),
]
assert x509_name(subject) == self.name
with assert_removed_in_230():
assert x509_name(subject) == self.name

def test_multiple_other(self) -> None:
"""Test multiple other tokens (only OUs work)."""
with pytest.raises(ValueError, match='^Subject contains multiple "countryName" fields$'):
with (
assert_removed_in_230(),
pytest.raises(ValueError, match='^Subject contains multiple "countryName" fields$'),
):
x509_name([("C", "AT"), ("C", "DE")])
with pytest.raises(ValueError, match='^Subject contains multiple "commonName" fields$'):
with (
assert_removed_in_230(),
pytest.raises(ValueError, match='^Subject contains multiple "commonName" fields$'),
):
x509_name([("CN", "AT"), ("CN", "FOO")])


Expand Down
48 changes: 48 additions & 0 deletions ca/django_ca/tests/utils/test_name_for_display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This file is part of django-ca (https://github.com/mathiasertl/django-ca).
#
# django-ca is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# django-ca is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along with django-ca. If not, see
# <http://www.gnu.org/licenses/>.

"""Test ``django_ca.utils.name_for_display``."""

from cryptography import x509
from cryptography.x509 import NameOID
from cryptography.x509.name import _ASN1Type

import pytest

from django_ca.tests.base.utils import cn
from django_ca.utils import name_for_display


@pytest.mark.parametrize(
("value", "expected"),
(
(x509.Name([cn("example.com")]), [("commonName (CN)", "example.com")]),
(
x509.Name([cn("example.net"), cn("example.com")]),
[("commonName (CN)", "example.net"), ("commonName (CN)", "example.com")],
),
(
x509.Name(
[
x509.NameAttribute(
oid=NameOID.X500_UNIQUE_IDENTIFIER, value=b"example.com", _type=_ASN1Type.BitString
)
]
),
[("x500UniqueIdentifier", "65:78:61:6D:70:6C:65:2E:63:6F:6D")],
),
),
)
def test_name_for_display(value: x509.Name, expected: list[tuple[str, str]]) -> None:
"""Test the function."""
assert name_for_display(value) == expected
6 changes: 4 additions & 2 deletions ca/django_ca/tests/utils/test_parse_name_x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import pytest

from django_ca.tests.base.assertions import assert_removed_in_230
from django_ca.utils import parse_name_x509


Expand Down Expand Up @@ -152,10 +153,11 @@
)
def test_parse_name_x509(value: str, expected: list[tuple[x509.ObjectIdentifier, str]]) -> None:
"""Some basic tests."""
assert parse_name_x509(value) == tuple(x509.NameAttribute(oid, value) for oid, value in expected)
with assert_removed_in_230():
assert parse_name_x509(value) == tuple(x509.NameAttribute(oid, value) for oid, value in expected)


def test_unknown() -> None:
"""Test unknown field."""
with pytest.raises(ValueError, match=r"^Unknown x509 name field: ABC$"):
with assert_removed_in_230(), pytest.raises(ValueError, match=r"^Unknown x509 name field: ABC$"):
parse_name_x509("/ABC=example.com")
6 changes: 4 additions & 2 deletions ca/django_ca/tests/utils/test_split_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import pytest

from django_ca.tests.base.assertions import assert_removed_in_230
from django_ca.utils import split_str


Expand Down Expand Up @@ -85,7 +86,8 @@
)
def test_basic(value: str, seperator: str, expected: list[str]) -> None:
"""Some basic split_str() test cases."""
assert list(split_str(value, seperator)) == expected
with assert_removed_in_230():
assert list(split_str(value, seperator)) == expected


@pytest.mark.parametrize(
Expand All @@ -100,5 +102,5 @@ def test_basic(value: str, seperator: str, expected: list[str]) -> None:
)
def test_quotation_errors(value: str, match: str) -> None:
"""Test quoting."""
with pytest.raises(ValueError, match=match):
with assert_removed_in_230(), pytest.raises(ValueError, match=match):
list(split_str(value, "/"))
2 changes: 1 addition & 1 deletion ca/django_ca/typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class OCSPKeyBackendDict(TypedDict):
hashes.SHA3_512,
]

ParsableName = Union[str, Iterable[tuple[str, str]]]
ParsableName = Union[str, Iterable[tuple[str, str]]] # TODO: remove?

ParsableKeyType = Literal["RSA", "DSA", "EC", "Ed25519", "Ed448"]
ParsableSubject = Union[
Expand Down
60 changes: 24 additions & 36 deletions ca/django_ca/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,14 @@
from django_ca import constants
from django_ca.conf import model_settings
from django_ca.constants import MULTIPLE_OIDS, NAME_OID_DISPLAY_NAMES
from django_ca.deprecation import RemovedInDjangoCA230Warning, deprecate_function
from django_ca.pydantic.validators import (
dns_validator,
email_validator,
is_power_two_validator,
url_validator,
)
from django_ca.typehints import (
AllowedHashTypes,
ParsableGeneralName,
ParsableKeyType,
ParsableName,
SerializedName,
)
from django_ca.typehints import AllowedHashTypes, ParsableGeneralName, ParsableKeyType, SerializedName

#: Regular expression to match general names.
GENERAL_NAME_RE = re.compile("^(email|URI|IP|DNS|RID|dirName|otherName):(.*)", flags=re.I)
Expand Down Expand Up @@ -130,23 +125,18 @@ def _serialize_name_attribute_value(name_attribute: x509.NameAttribute) -> str:
return name_attribute.value


@deprecate_function(RemovedInDjangoCA230Warning)
def serialize_name(name: Union[x509.Name, x509.RelativeDistinguishedName]) -> SerializedName:
"""Serialize a :py:class:`~cg:cryptography.x509.Name`.
.. deprecated:: 2.2.0
This function is deprecated and will be removed in ``django-ca==2.3.0``. Use Pydantic models instead.
The value also accepts a :py:class:`~cg:cryptography.x509.RelativeDistinguishedName`.
The returned value is a list of tuples, each consisting of two strings. If an attribute contains
``bytes``, it is converted using :py:func:`~django_ca.utils.bytes_to_hex`.
Examples::
>>> serialize_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'example.com')]))
[{'oid': '2.5.4.3', 'value': 'example.com'}]
>>> serialize_name(x509.RelativeDistinguishedName([
... x509.NameAttribute(NameOID.COUNTRY_NAME, 'AT'),
... x509.NameAttribute(NameOID.COMMON_NAME, 'example.com'),
... ]))
[{'oid': '2.5.4.6', 'value': 'AT'}, {'oid': '2.5.4.3', 'value': 'example.com'}]
"""
return [{"oid": attr.oid.dotted_string, "value": _serialize_name_attribute_value(attr)} for attr in name]

Expand All @@ -169,16 +159,18 @@ def name_for_display(name: Union[x509.Name, x509.RelativeDistinguishedName]) ->
]


@deprecate_function(RemovedInDjangoCA230Warning)
def parse_serialized_name_attributes(name: SerializedName) -> list[x509.NameAttribute]:
"""Parse a serialized list of name attributes into a list of NameAttributes.
.. deprecated:: 2.2.0
This function is deprecated and will be removed in ``django-ca==2.3.0``. Use Pydantic models instead.
This function takes care of parsing hex-encoded byte values name attributes that are known to use bytes
(currently only :py:attr:`NameOID.X500_UNIQUE_IDENTIFIER
<cg:cryptography.x509.oid.NameOID.X500_UNIQUE_IDENTIFIER>`).
>>> parse_serialized_name_attributes([{"oid": "2.5.4.3", "value": "example.com"}])
[<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='example.com')>]
This function is more or less the inverse of :py:func:`~django_ca.utils.serialize_name`, except that it
returns a list of :py:class:`~cg:cryptography.x509.NameAttribute` instances (``serialize_name()`` takes a
:py:class:`~cg:cryptography.x509.Name` or :py:class:`~cg:cryptography.x509.RelativeDistinguishedName`)
Expand Down Expand Up @@ -308,10 +300,14 @@ def sanitize_serial(value: str) -> str:
return serial


# @deprecate_function(RemovedInDjangoCA200Warning)
def parse_name_x509(name: ParsableName) -> tuple[x509.NameAttribute, ...]:
@deprecate_function(RemovedInDjangoCA230Warning)
def parse_name_x509(name: Union[str, Iterable[tuple[str, str]]]) -> tuple[x509.NameAttribute, ...]:
"""Parses a subject string as used in OpenSSLs command line utilities.
.. deprecated:: 2.2.0
This function is deprecated and will be removed in ``django-ca==2.3.0``. Use Pydantic models instead.
.. versionchanged:: 1.20.0
This function no longer returns the subject in pseudo-sorted order.
Expand All @@ -320,16 +316,6 @@ def parse_name_x509(name: ParsableName) -> tuple[x509.NameAttribute, ...]:
``/C=AT/L=Vienna/CN=example.com/[email protected]``. The function does its best to be lenient
on deviations from the format, object identifiers are case-insensitive, whitespace at the start and end is
stripped and the subject does not have to start with a slash (``/``).
>>> parse_name_x509([("CN", "example.com")])
(<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='example.com')>,)
>>> parse_name_x509(
... [("c", "AT"), ("l", "Vienna"), ("o", "quoting/works"), ("CN", "example.com")]
... ) # doctest: +NORMALIZE_WHITESPACE
(<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.6, name=countryName)>, value='AT')>,
<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.7, name=localityName)>, value='Vienna')>,
<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.10, name=organizationName)>, value='quoting/works')>,
<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='example.com')>)
"""
if isinstance(name, str):
# TYPE NOTE: mypy detects t.split() as Tuple[str, ...] and does not recognize the maxsplit parameter
Expand All @@ -344,12 +330,13 @@ def parse_name_x509(name: ParsableName) -> tuple[x509.NameAttribute, ...]:
return tuple(x509.NameAttribute(oid, value) for oid, value in items)


# @deprecate_function(RemovedInDjangoCA200Warning)
def x509_name(name: ParsableName) -> x509.Name:
@deprecate_function(RemovedInDjangoCA230Warning)
def x509_name(name: Union[str, Iterable[tuple[str, str]]]) -> x509.Name:
"""Parses a string or iterable of two-tuples into a :py:class:`x509.Name <cg:cryptography.x509.Name>`.
>>> x509_name([('C', 'AT'), ('CN', 'example.com')])
<Name(C=AT,CN=example.com)>
.. deprecated:: 2.2.0
This function is deprecated and will be removed in ``django-ca==2.3.0``. Use Pydantic models instead.
"""
return check_name(x509.Name(parse_name_x509(name)))

Expand Down Expand Up @@ -938,6 +925,7 @@ def read_file(path: str) -> bytes:
stream.close()


@deprecate_function(RemovedInDjangoCA230Warning)
def split_str(val: str, sep: str) -> Iterator[str]:
"""Split a character on the given set of characters."""
lex = shlex.shlex(val, posix=True)
Expand Down
Loading

0 comments on commit a5666ed

Please sign in to comment.