forked from workfloworchestrator/nsi-auth
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrfc4514_cmp.py
More file actions
165 lines (137 loc) · 6.54 KB
/
rfc4514_cmp.py
File metadata and controls
165 lines (137 loc) · 6.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#
# Official definition of distinguishedNameMatch
# https://datatracker.ietf.org/doc/html/rfc4517#section-4.2.15
#
import re
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.x509 import load_pem_x509_certificate
# Example: openssl x509 -nameopt RFC2253 -text output:
# "CN=University Corporation For Advanced Internet Development,emailAddress=knewell@internet2.edu,organizationIdentifier=NTRUS\\+MI-801069584,O=University Corporation For Advanced Internet Development,ST=Michigan,C=US"
# According to https://datatracker.ietf.org/doc/html/rfc4514#page-7 'emailAddress' is not a MUST,
# nor is 'organizationIdentifier'. cryptography package doesn't do MAY
# So do a workaround where we pass a complete list of name-to-OID mappings to the parser
# to fix any MAYs
# Official X500 OID-to-name mappings are not available. Use a Mozilla compiled list, converted
# by our own script:
from name2oid import names2oid
# Reverse for cryptography's internal swapping of names for OIDs, which breaks comparisons
_oid2names = {v: k for k, v in names2oid.items()}
# https://datatracker.ietf.org/doc/html/rfc4514#section-2.4
_escaped_chars = [' ', '"', '#', '+', ',', ';', '<', '=', '>','\\' ]
def dn_rfc2253_string_to_rfc4514_name(rfc2253_string:str):
"""
Convert a string with a DistinguishedName in RFC2253 format, such as generated by:
openssl x509 -nameopt RFC2253 -in bla.pem -text
into a cryptography.x509.Name object.
Args:
rfc2253_string (str): RFC2253 formatted DN.
According to openssl CN comes first
Returns:
cryptography.x509.Name: Parsed version of DN.
Raises:
ValueError: If rfc2253_string is not a valid RFC2253 DN.
"""
# Output is big-to-small sorted
return x509.Name.from_rfc4514_string(rfc2253_string,names2oid)
def parse_tag_value_string(text):
"""Parse comma-separated tag=value pairs, returning ordered list.
@blame: confer.to"""
pattern = r'([^,=]+)=((?:[^,\\]|\\.)*)'
def unescape(s):
return s.replace('\\,', ',').replace('\\\\', '\\')
return [(tag, unescape(value)) for tag, value in re.findall(pattern, text)]
def dn_tagvalue_string_to_rfc4514_name(tagvalue_string:str):
"""
Convert a string in "tag1=value1,tag2=value2" format, that should represent
a valid DistinguishedName into a cryptography.x509.Name object, if it can be parsed
by the cryptography code. tags (symbolic names, not oids) are taken from the set
of valid DN tags as defined by cryptography, with e.g. "organizationIdentifier" added,
see .py.
Args:
tagvalue_string (str): comma-separated tag-value DN
Returns:
cryptography.x509.Name: Parsed version of DN.
Raises:
ValueError: If tagvalue_string is not a valid DN.
"""
try:
tvs = parse_tag_value_string(tagvalue_string)
rdns = []
oidlist = []
for tag_string, value_string in tvs:
tag_string = tag_string.strip()
value_string = value_string.strip()
# Check if there are any escapes, and remove them before cryptography adds them again.
for c in _escaped_chars:
esc_str = '\\'+c
if esc_str in value_string:
value_string = value_string.replace(esc_str, c)
if tag_string not in names2oid:
# https://datatracker.ietf.org/doc/html/rfc4512#section-1.4
# "Short names, also known as descriptors, are used as more readable
# aliases for object identifiers. Short names are case insensitive"
# _name2oid is in lower-case.
tag_string = tag_string.lower()
oid = names2oid[tag_string]
oidlist.append(oid)
# Cannot pass symbolic name, so Name with "GN" will not be equal to Name
# with NameOID.GIVEN_NAME (1.xxx) :-( Fix below
na = x509.NameAttribute(oid,value_string)
rdn = x509.RelativeDistinguishedName([na])
rdns.append(rdn)
# cryptography does not fix order, so if input was big-to-small this is not fixed automatically
# note that big-to-small is the internal representation.
firstoid = oidlist[0]
lastoid = oidlist[-1]
bigtosmall = True
# Heuristic to determine order. These two fields MUST be "recognized"
# https://datatracker.ietf.org/doc/html/rfc4514#section-3
if firstoid == NameOID.COMMON_NAME:
bigtosmall = False
elif lastoid == NameOID.COUNTRY_NAME:
bigtosmall = False
if not bigtosmall:
rdns.reverse()
n = x509.Name(rdns)
# Hack to convert tag OIDs to symbolic names
rfc4514_string = n.rfc4514_string(_oid2names)
n = x509.Name.from_rfc4514_string(rfc4514_string,names2oid)
return n
except ValueError as e:
# Repeat ValueErrors from cryptography
raise e
except Exception as e:
# Turn other errors into ValueErrors
raise ValueError(e)
def subject_dn_from_cert_pem(cert_pem_bytes:str):
"""
Parse Subject DN from a PEM-encoded certificate.
Args:
cert_pem_str (bytes): PEM framed as per cryptography's rules
See https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file
Returns:
cryptography.x509.Name: Parsed version of Subject DN.
Raises:
ValueError: If cert_pem_str is not a valid PEM-encoded x.509 certificate.
"""
cert_obj = load_pem_x509_certificate(cert_pem_bytes)
subject_name = x509.Name(cert_obj.subject)
return subject_name
def subject_dn_from_traefik_cert_pem(traefik_cert_str):
""" Convert Traefik minimized, HTTP Header compatible PEM to what
cryptography groks
See https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file
"""
delim_pem_cert_str = '-----BEGIN CERTIFICATE-----\n'
#base64lines = re.findall('.{64}', traefik_cert_str)
n = 64
base64lines = [traefik_cert_str[i:i + n] for i in range(0, len(traefik_cert_str), n)]
for base64line in base64lines:
delim_pem_cert_str += base64line+'\n'
delim_pem_cert_str += '-----END CERTIFICATE-----\n'
# https://www.rfc-editor.org/rfc/rfc9110.html#name-fields says values must be considered
# opaque bytes, but Python cannot do that, so use the old HTTP/1.1 standard header encoding.
delim_pem_cert_bytes = bytes(delim_pem_cert_str, "iso-8859-1")
s = subject_dn_from_cert_pem(delim_pem_cert_bytes)
return s