Skip to content

Commit

Permalink
fix name constraints to be consistent with created certificate
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiasertl committed Dec 1, 2024
1 parent 695414e commit bc2f6a5
Show file tree
Hide file tree
Showing 79 changed files with 812 additions and 284 deletions.
55 changes: 55 additions & 0 deletions ca/django_ca/extensions/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,59 @@
from django_ca.utils import bytes_to_hex, format_general_name


def _naming_authority_as_text(value: "x509.NamingAuthority") -> str: # pragma: only cryptography>=44
lines = ["* Naming Authority:"]
if value.id is None:
lines.append("* id: None")
else:
lines.append(f"* id: {value.id.dotted_string}")
lines += [f"* URL: {value.url}", f"* text: {value.text}"]
return "\n".join(lines)


def _profession_info_as_text(value: "x509.ProfessionInfo") -> str: # pragma: only cryptography>=44
lines = []

if value.naming_authority is not None:
lines.append(_naming_authority_as_text(value.naming_authority))

lines.append("* Profession items:")
for item in value.profession_items:
lines.append(f" * {item}")

if value.profession_oids:
lines.append("* Profession OIDs:")
for oid in value.profession_oids:
lines.append(f" * {oid.dotted_string}")

if value.registration_number:
lines.append(f"* Registration Number: {value.registration_number}")
if value.add_profession_info:
lines.append(f"* Add Profession Info: {bytes_to_hex(value.add_profession_info)}")
return "\n".join(lines)


def _admissions_as_text(value: "x509.Admissions") -> str: # pragma: only cryptography>=44
lines = []
if value.authority:
lines.append(f"* Authority: {format_general_name(value.authority)}")

lines.append("* Admissions:")
for admission in value._admissions: # pylint: disable=protected-access; only way to get admissions
if admission.admission_authority is not None:
lines.append(f" * Admission Authority: {format_general_name(admission.admission_authority)}")

if admission.naming_authority is not None:
lines.append(textwrap.indent(_naming_authority_as_text(admission.naming_authority), " "))

lines.append(" * ProfessionInfos:")
for info in admission.profession_infos:
lines.append(" * ProfessionInfo:")
lines.append(textwrap.indent(_profession_info_as_text(info), " "))

return "\n".join(lines)


def _authority_information_access_as_text(value: typehints.InformationAccessExtensionType) -> str:
lines = []
issuers = [ad for ad in value if ad.access_method == AuthorityInformationAccessOID.CA_ISSUERS]
Expand Down Expand Up @@ -186,6 +239,8 @@ def extension_as_text(value: x509.ExtensionType) -> str: # noqa: PLR0911
return _signed_certificate_timestamps_as_text(value)
if isinstance(value, (x509.AuthorityInformationAccess, x509.SubjectInformationAccess)):
return _authority_information_access_as_text(value)
if hasattr(x509, "Admissions") and isinstance(value, x509.Admissions): # pragma: only cryptography>=44
return _admissions_as_text(value)
if isinstance(value, x509.AuthorityKeyIdentifier):
return _authority_key_identifier_as_text(value)
if isinstance(value, x509.BasicConstraints):
Expand Down
3 changes: 2 additions & 1 deletion ca/django_ca/pydantic/extension_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ def cryptography(self) -> "x509.Admissions":
@classmethod
def parse_cryptography(cls, data: Any) -> Any:
"""Parse cryptography instance."""
if isinstance(data, x509.Admissions):
# pragma: only cryptography<44 # remove hasattr() call when cg<44 is dropped
if hasattr(x509, "Admissions") and isinstance(data, x509.Admissions):
return {"authority": data.authority, "admissions": data._admissions} # pylint: disable=protected-access
return data

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{% extends "django_ca/admin/extensions/base/base.html" %}
{% load i18n django_ca %}

{# Admissions extension #}
{% block content %}{% spaceless %}
<ul>
{% if value.authority %}<li>Authority: {{ value.authority|format_general_name }}</li>{% endif %}
<li>Admissions:
<ul>
{% for admission in value|admissions %}

{% if admission.admission_authority %}
<li>Admission Authority: {{ admission.admission_authority|format_general_name }}</li>
{% endif %}

{% if admission.naming_authority %}
<li>Naming Authority:
{% if admission.naming_authority.id or admission.naming_authority.url or admission.naming_authority.text %}
<ul>
{% if admission.naming_authority.id %}<li>ID: {{ admission.naming_authority.id.dotted_string }}</li>{% endif %}
{% if admission.naming_authority.url %}<li>URL: {{ admission.naming_authority.url }}</li>{% endif %}
{% if admission.naming_authority.text %}<li>Text: {{ admission.naming_authority.url }}</li>{% endif %}
</ul>
{% else %}
No values.
{% endif %}
</li>
{% endif %}

<li>Profession Infos:
<ul>
{% for info in admission.profession_infos %}
<li>Profession Info:
<ul>
{% if info.naming_authority %}
<li>Naming Authority:
{% if info.naming_authority.id or info.naming_authority.url or info.naming_authority.text %}
<ul>
{% if info.naming_authority.id %}<li>ID: {{ info.naming_authority.id.dotted_string }}</li>{% endif %}
{% if info.naming_authority.url %}<li>URL: {{ info.naming_authority.url }}</li>{% endif %}
{% if info.naming_authority.text %}<li>Text: {{ info.naming_authority.text }}</li>{% endif %}
</ul>
{% else %}
No values.
{% endif %}
</li>
{% endif %}

<li>Profession items:
<ul>
{% for item in info.profession_items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</li>

{% if info.profession_oids %}
<li>Profession OIDs:
<ul>
{% for item in info.profession_oids %}
<li>{{ item.dotted_string }}</li>
{% endfor %}
</ul>
</li>
{% endif %}

{% if info.registration_number %}<li>Registration number: {{ info.registration_number }}</li>{% endif %}
{% if info.add_profession_info %}<li>Add Profession Info: {{ info.add_profession_info|as_hex }}</li>{% endif %}
</ul>
</li>
{% endfor %}
</ul>
</li>

</ul>
{% endfor %}
</li>
</ul>
{% endspaceless %}{% endblock content %}
27 changes: 21 additions & 6 deletions ca/django_ca/templatetags/django_ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@

from django_ca.constants import EXTENDED_KEY_USAGE_NAMES, EXTENSION_CRITICAL_HELP, EXTENSION_RFC_DEFINITION
from django_ca.extensions.utils import key_usage_items, signed_certificate_timestamp_values
from django_ca.utils import add_colons, bytes_to_hex, format_general_name, int_to_hex, name_for_display
from django_ca.utils import (
add_colons,
bytes_to_hex,
format_general_name as _format_general_name,
int_to_hex,
name_for_display,
)

register = template.Library()

Expand All @@ -36,6 +42,12 @@
register.filter("signed_certificate_timestamp_values", signed_certificate_timestamp_values)


@register.filter
def admissions(value: "x509.Admissions") -> list["x509.Admission"]: # pragma: only cryptography>=44
"""Return list of admissions (templates cannot contain underscores in variables)."""
return value._admissions # pylint: disable=protected-access; only way to get admissions


@register.filter
def critical_help(dotted_string: str) -> str:
"""Return help text informing the user if the extension should be marked as critical or not."""
Expand Down Expand Up @@ -85,12 +97,15 @@ def enum(mod: Any, cls_name_and_member: str) -> Any:


@register.filter
def format_general_names(value: Iterable[x509.GeneralName]) -> list[str]:
"""A template tag to format general names.
def format_general_name(value: x509.GeneralName) -> str: # pragma: only cryptography>=44
"""A template tag to format general name."""
return _format_general_name(value)

Note that currently general names always occur as list.
"""
return [format_general_name(v) for v in value]

@register.filter
def format_general_names(value: Iterable[x509.GeneralName]) -> list[str]:
"""A template tag to format general names."""
return [_format_general_name(v) for v in value]


@register.filter
Expand Down
112 changes: 102 additions & 10 deletions ca/django_ca/tests/api/test_sign_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
uri,
)

pytestmark = [pytest.mark.freeze_time(TIMESTAMPS["everything_valid"])]

path = reverse_lazy("django_ca:api:sign_certificate", kwargs={"serial": CERT_DATA["root"]["serial"]})
default_subject = [{"oid": NameOID.COMMON_NAME.dotted_string, "value": "api.example.com"}]
csr = CERT_DATA["root-cert"]["csr"]["parsed"].public_bytes(Encoding.PEM).decode("utf-8")
Expand Down Expand Up @@ -144,7 +146,6 @@ def sign_certificate(
return cert


@freeze_time(TIMESTAMPS["everything_valid"])
def test_sign_ca_values(
api_user: AbstractUser,
api_client: Client,
Expand All @@ -171,7 +172,6 @@ def test_sign_ca_values(
assert extensions[ExtensionOID.CRL_DISTRIBUTION_POINTS] == usable_root.sign_crl_distribution_points


@freeze_time(TIMESTAMPS["everything_valid"])
def test_private_key_unavailable(
api_user: AbstractUser,
api_client: Client,
Expand All @@ -198,7 +198,6 @@ def test_private_key_unavailable(
assert response.json() == expected_response


@freeze_time(TIMESTAMPS["everything_valid"])
def test_key_backend_options(
settings: SettingsWrapper,
api_client: Client,
Expand Down Expand Up @@ -231,7 +230,6 @@ def test_key_backend_options(
assert cert.serial


@freeze_time(TIMESTAMPS["everything_valid"])
def test_key_backend_configuration_not_required_on_frontend(
api_user: AbstractUser,
api_client: Client,
Expand Down Expand Up @@ -263,7 +261,6 @@ def test_key_backend_configuration_not_required_on_frontend(
assert response.json() == expected_response


@freeze_time(TIMESTAMPS["everything_valid"])
def test_sign_certificate_with_parameters(
api_user: AbstractUser,
api_client: Client,
Expand Down Expand Up @@ -293,7 +290,6 @@ def test_sign_certificate_with_parameters(
assert cert.not_after == not_after


@freeze_time(TIMESTAMPS["everything_valid"])
def test_sign_certificate_with_extensions(
api_user: AbstractUser,
api_client: Client,
Expand Down Expand Up @@ -452,7 +448,106 @@ def test_sign_certificate_with_extensions(
assert exts[ExtensionOID.TLS_FEATURE] == tls_feature(x509.TLSFeatureType.status_request)


@freeze_time(TIMESTAMPS["everything_valid"])
if hasattr(x509, "Admissions"):

@pytest.mark.parametrize(
("data", "expected"),
(
({"admissions": []}, x509.Admissions(authority=None, admissions=[])),
(
{
"authority": {"type": "URI", "value": "https://auth.example.com"},
"admissions": [
{
"admission_authority": {
"type": "URI",
"value": "https://auth.admission.example.com",
},
"naming_authority": {
"id": "1.2.3",
"url": "https://url.example.com",
"text": "text example",
},
"profession_infos": [
{"profession_items": ["prof item"]},
{
"naming_authority": {
"id": "1.2.3.4",
"url": "https://url.prof-info.example.com",
"text": "text prof-info example",
},
"profession_items": ["prof item2"],
"profession_oids": ["1.2.3.5"],
"registration_number": "reg number",
"add_profession_info": "Zm9vYmFy",
},
],
}
],
},
x509.Admissions(
authority=uri("https://auth.example.com"),
admissions=[
x509.Admission(
admission_authority=uri("https://auth.admission.example.com"),
naming_authority=x509.NamingAuthority(
id=x509.ObjectIdentifier("1.2.3"),
url="https://url.example.com",
text="text example",
),
profession_infos=[
x509.ProfessionInfo(
naming_authority=None,
profession_items=["prof item"],
profession_oids=None,
registration_number=None,
add_profession_info=None,
),
x509.ProfessionInfo(
naming_authority=x509.NamingAuthority(
id=x509.ObjectIdentifier("1.2.3.4"),
url="https://url.prof-info.example.com",
text="text prof-info example",
),
profession_items=["prof item2"],
profession_oids=[x509.ObjectIdentifier("1.2.3.5")],
registration_number="reg number",
add_profession_info=b"foobar",
),
],
)
],
),
),
),
)
def test_sign_certificate_with_admissions_extension(
api_user: AbstractUser,
api_client: Client,
usable_root: CertificateAuthority,
expected_response: dict[str, Any],
django_capture_on_commit_callbacks: CaptureOnCommitCallbacks,
data: dict[str, Any],
expected: "x509.Admission",
) -> None:
"""Test signing certificates with extensions."""
cert = sign_certificate(
django_capture_on_commit_callbacks,
api_user,
api_client,
ca=usable_root,
data={
"subject": default_subject,
"extensions": [{"type": "admissions", "value": data}],
},
expected_response=expected_response,
)

# Test extensions
exts = cert.extensions
assert exts[ExtensionOID.ADMISSIONS].value == expected


def test_sign_certificate_with_subject_alternative_name(
api_user: AbstractUser,
api_client: Client,
Expand Down Expand Up @@ -519,7 +614,6 @@ def test_invalid_csr_with_valid_headers(api_client: Client) -> None:


@pytest.mark.usefixtures("tmpcadir")
@freeze_time(TIMESTAMPS["everything_valid"])
def test_crldp_with_full_name_and_relative_name(api_client: Client) -> None:
"""Test sending a CRL Distribution point with a full_name and a relative_name."""
response = request(
Expand Down Expand Up @@ -581,7 +675,6 @@ def test_crldp_with_no_full_name_or_relative_name(api_client: Client) -> None:


@pytest.mark.usefixtures("tmpcadir")
@freeze_time(TIMESTAMPS["everything_valid"])
def test_with_invalid_algorithm(api_client: Client) -> None:
"""Test sending an invalid key usage."""
response = request(api_client, {"csr": csr, "subject": default_subject, "algorithm": "foo"})
Expand All @@ -601,7 +694,6 @@ def test_with_invalid_algorithm(api_client: Client) -> None:


@pytest.mark.usefixtures("tmpcadir")
@freeze_time(TIMESTAMPS["everything_valid"])
def test_with_invalid_key_usage(api_client: Client) -> None:
"""Test sending an invalid key usage."""
response = request(
Expand Down
Loading

0 comments on commit bc2f6a5

Please sign in to comment.