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

Allow using DOMAIN COMPONENT for AD DS certificates #75

Closed
wants to merge 13 commits into from
1 change: 1 addition & 0 deletions ca/django_ca/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class SubjectField(forms.MultiValueField):

def __init__(self, **kwargs: typing.Any) -> None:
fields = (
forms.CharField(required=False), # DC
forms.CharField(required=False), # C
forms.CharField(required=False), # ST
forms.CharField(required=False), # L
Expand Down
18 changes: 14 additions & 4 deletions ca/django_ca/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import typing
from collections import abc
from datetime import datetime
from datetime import timezone
from datetime import timedelta
from ipaddress import ip_address
from ipaddress import ip_network
Expand Down Expand Up @@ -49,6 +50,7 @@
from django.core.validators import URLValidator
from django.utils.encoding import force_bytes
from django.utils.encoding import force_str
from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext_lazy as _

from . import ca_settings
Expand All @@ -65,6 +67,7 @@

# List of possible subject fields, in order
SUBJECT_FIELDS = [
"DC",
"C",
"ST",
"L",
Expand Down Expand Up @@ -104,6 +107,7 @@

#: Map OID objects to IDs used in subject strings
OID_NAME_MAPPINGS: Dict[x509.ObjectIdentifier, str] = {
NameOID.DOMAIN_COMPONENT: "DC",
NameOID.COUNTRY_NAME: "C",
NameOID.STATE_OR_PROVINCE_NAME: "ST",
NameOID.LOCALITY_NAME: "L",
Expand All @@ -124,6 +128,7 @@

# Some OIDs can occur multiple times
MULTIPLE_OIDS = (
NameOID.DOMAIN_COMPONENT,
NameOID.ORGANIZATIONAL_UNIT_NAME,
NameOID.STREET_ADDRESS,
)
Expand Down Expand Up @@ -209,8 +214,11 @@ def getter(self, method): # type: ignore


def sort_name(subject: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
"""Returns the subject in the correct order for a x509 subject."""
return sorted(subject, key=lambda e: SUBJECT_FIELDS.index(e[0]))
"""Returns the subject in the correct order for a x509 subject, while respecting
the original list order for possible subject fields allowing for MULTIPLE_OIDS."""
if SUBJECT_FIELDS.index(subject[0][0]) > SUBJECT_FIELDS.index(subject[-1][0]):
return sorted(subject[::-1], key=lambda k: SUBJECT_FIELDS.index(k[0]))
return sorted(subject, key=lambda k: SUBJECT_FIELDS.index(k[0]))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function here: I think I understand what your updated comment means to express, but the code is far from obvious. And I'm frankly surprised that the old code does not respect sort stability.

  1. I think subject[::-1] is just reversing the list. At least a simple test did that. If yes, then use the more obvious reversed() instead.
  2. What does that if check do?
  3. Definitely add tests for an updated version of this function.

Copy link
Contributor Author

@alfonsrv alfonsrv Sep 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below. I'm not sure how it does its magic exactly anymore, I just know it reliably yields the desired results.

Exemplary test cases as mentioned below:

>>> subject = [('CN', 'John Doe'), ('OU', 'Users'), ('DC', 'contoso'), ('DC', 'loc')] 
>>> sort_name(subject)
[('DC', 'loc'), ('DC', 'contoso'), ('OU', 'Users'), ('CN', 'John Doe')]

>>> subject = [('CN', 'John Doe'), ('OU', 'Marketing'), ('OU', 'Users'), ('OU', 'OtherUnit'), ('DC', 'contoso'), ('DC', 'loc')]
>>> sort_name(subject)
[('DC', 'loc'), ('DC', 'contoso'), ('OU', 'OtherUnit'), ('OU', 'Users'), ('OU', 'Marketing'), ('CN', 'John Doe')]

>>> subject = [('DC', 'loc'), ('DC', 'contoso'), ('DC', 'ad'), ('OU', 'Users'), ('CN', 'John Doe')]
>>> sort_name(subject)
[('DC', 'loc'), ('DC', 'contoso'), ('DC', 'ad'), ('OU', 'Users'), ('CN', 'John Doe')]

Let me know if you feel comfortable with the sort function so we can move forward. Halting any further effort until then.

Copy link
Owner

@mathiasertl mathiasertl Sep 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that's behavior that I don't understand: In testcase two, loc is the last DC in the input and ends up as first element.

In the third test case, loc is the first DC, and still ends up as last. Is that supposed to be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. In the first and second test case, the user input the subject with CN being the first element – this is a classic case of assuming the hierarchy goes from left-to-right with CN being the lowest weighted element.

In test case three, we're looking at the opposite. The user's hierarchical input is correct as per X509 with CN being the last element.

In all three test cases the user respect the general hierarchical-structure, albeit in a reversed manner.



def encode_url(url: str) -> str:
Expand Down Expand Up @@ -959,8 +967,10 @@ def parse_encoding(value: Optional[Union[str, Encoding]] = None) -> Encoding:
def parse_expires(expires: Expires = None) -> datetime:
"""Parse a value specifying an expiry into a concrete datetime."""

now = datetime.utcnow().replace(second=0, microsecond=0)
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)

if not expires.tzinfo:
expires = expires.replace(tzinfo=get_current_timezone())
alfonsrv marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(expires, int):
return now + timedelta(days=expires)
if isinstance(expires, timedelta):
Expand Down Expand Up @@ -1040,7 +1050,7 @@ def get_cert_builder(expires: datetime, serial: Optional[int] = None) -> x509.Ce
to generate such a value. By default, a value will be generated.
"""

now = datetime.utcnow().replace(second=0, microsecond=0)
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)

# NOTE: Explicitly passing a serial is used when creating a CA, where we want to add extensions where the
# value references the serial.
Expand Down