mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 21:33:00 +00:00
Support otherName in subAltName in CSR for UTF8 strings (#53)
* Support otherName in subAltName in CSR for UTF8 strings * Remove uneeded docs and added changelog fragment * Missed a merge conflict * Fix up sanity issues and added test expectation * Rename function
This commit is contained in:
153
plugins/module_utils/crypto/_asn1.py
Normal file
153
plugins/module_utils/crypto/_asn1.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2020, Jordan Borean <jborean93@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
|
||||
"""
|
||||
An ASN.1 serialized as a string in the OpenSSL format:
|
||||
[modifier,]type[:value]
|
||||
|
||||
modifier:
|
||||
The modifier can be 'IMPLICIT:<tag_number><tag_class>,' or 'EXPLICIT:<tag_number><tag_class>' where IMPLICIT
|
||||
changes the tag of the universal value to encode and EXPLICIT prefixes its tag to the existing universal value.
|
||||
The tag_number must be set while the tag_class can be 'U', 'A', 'P', or 'C" for 'Universal', 'Application',
|
||||
'Private', or 'Context Specific' with C being the default.
|
||||
|
||||
type:
|
||||
The underlying ASN.1 type of the value specified. Currently only the following have been implemented:
|
||||
UTF8: The value must be a UTF-8 encoded string.
|
||||
|
||||
value:
|
||||
The value to encode, the format of this value depends on the <type> specified.
|
||||
"""
|
||||
ASN1_STRING_REGEX = re.compile(r'^((?P<tag_type>IMPLICIT|EXPLICIT):(?P<tag_number>\d+)(?P<tag_class>U|A|P|C)?,)?'
|
||||
r'(?P<value_type>[\w\d]+):(?P<value>.*)')
|
||||
|
||||
|
||||
class TagClass:
|
||||
universal = 0
|
||||
application = 1
|
||||
context_specific = 2
|
||||
private = 3
|
||||
|
||||
|
||||
# Universal tag numbers that can be encoded.
|
||||
class TagNumber:
|
||||
utf8_string = 12
|
||||
|
||||
|
||||
def _pack_octet_integer(value):
|
||||
""" Packs an integer value into 1 or multiple octets. """
|
||||
# NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value.
|
||||
octets = bytearray()
|
||||
|
||||
# Continue to shift the number by 7 bits and pack into an octet until the
|
||||
# value is fully packed.
|
||||
while value:
|
||||
octet_value = value & 0b01111111
|
||||
|
||||
# First round (last octet) must have the MSB set.
|
||||
if len(octets):
|
||||
octet_value |= 0b10000000
|
||||
|
||||
octets.append(octet_value)
|
||||
value >>= 7
|
||||
|
||||
# Reverse to ensure the higher order octets are first.
|
||||
octets.reverse()
|
||||
return bytes(octets)
|
||||
|
||||
|
||||
def serialize_asn1_string_as_der(value):
|
||||
""" Deserializes an ASN.1 string to a DER encoded byte string. """
|
||||
asn1_match = ASN1_STRING_REGEX.match(value)
|
||||
if not asn1_match:
|
||||
raise ValueError("The ASN.1 serialized string must be in the format [modifier,]type[:value]")
|
||||
|
||||
tag_type = asn1_match.group('tag_type')
|
||||
tag_number = asn1_match.group('tag_number')
|
||||
tag_class = asn1_match.group('tag_class') or 'C'
|
||||
value_type = asn1_match.group('value_type')
|
||||
asn1_value = asn1_match.group('value')
|
||||
|
||||
if value_type != 'UTF8':
|
||||
raise ValueError('The ASN.1 serialized string is not a known type "{0}", only UTF8 types are '
|
||||
'supported'.format(value_type))
|
||||
|
||||
b_value = to_bytes(asn1_value, encoding='utf-8', errors='surrogate_or_strict')
|
||||
|
||||
# We should only do a universal type tag if not IMPLICITLY tagged or the tag class is not universal.
|
||||
if not tag_type or (tag_type == 'EXPLICIT' and tag_class != 'U'):
|
||||
b_value = pack_asn1(TagClass.universal, False, TagNumber.utf8_string, b_value)
|
||||
|
||||
if tag_type:
|
||||
tag_class = {
|
||||
'U': TagClass.universal,
|
||||
'A': TagClass.application,
|
||||
'P': TagClass.private,
|
||||
'C': TagClass.context_specific,
|
||||
}[tag_class]
|
||||
|
||||
# When adding support for more types this should be looked into further. For now it works with UTF8Strings.
|
||||
constructed = tag_type == 'EXPLICIT' and tag_class != TagClass.universal
|
||||
b_value = pack_asn1(tag_class, constructed, int(tag_number), b_value)
|
||||
|
||||
return b_value
|
||||
|
||||
|
||||
def pack_asn1(tag_class, constructed, tag_number, b_data):
|
||||
"""Pack the value into an ASN.1 data structure.
|
||||
|
||||
The structure for an ASN.1 element is
|
||||
|
||||
| Identifier Octet(s) | Length Octet(s) | Data Octet(s) |
|
||||
"""
|
||||
b_asn1_data = bytearray()
|
||||
|
||||
if tag_class < 0 or tag_class > 3:
|
||||
raise ValueError("tag_class must be between 0 and 3 not %s" % tag_class)
|
||||
|
||||
# Bit 8 and 7 denotes the class.
|
||||
identifier_octets = tag_class << 6
|
||||
# Bit 6 denotes whether the value is primitive or constructed.
|
||||
identifier_octets |= ((1 if constructed else 0) << 5)
|
||||
|
||||
# Bits 5-1 contain the tag number, if it cannot be encoded in these 5 bits
|
||||
# then they are set and another octet(s) is used to denote the tag number.
|
||||
if tag_number < 31:
|
||||
identifier_octets |= tag_number
|
||||
b_asn1_data.append(identifier_octets)
|
||||
else:
|
||||
identifier_octets |= 31
|
||||
b_asn1_data.append(identifier_octets)
|
||||
b_asn1_data.extend(_pack_octet_integer(tag_number))
|
||||
|
||||
length = len(b_data)
|
||||
|
||||
# If the length can be encoded in 7 bits only 1 octet is required.
|
||||
if length < 128:
|
||||
b_asn1_data.append(length)
|
||||
|
||||
else:
|
||||
# Otherwise the length must be encoded across multiple octets
|
||||
length_octets = bytearray()
|
||||
while length:
|
||||
length_octets.append(length & 0b11111111)
|
||||
length >>= 8
|
||||
|
||||
length_octets.reverse() # Reverse to make the higher octets first.
|
||||
|
||||
# The first length octet must have the MSB set alongside the number of
|
||||
# octets the length was encoded in.
|
||||
b_asn1_data.append(len(length_octets) | 0b10000000)
|
||||
b_asn1_data.extend(length_octets)
|
||||
|
||||
return bytes(b_asn1_data) + b_data
|
||||
@@ -24,6 +24,7 @@ import binascii
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ._asn1 import serialize_asn1_string_as_der
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
@@ -205,10 +206,22 @@ def cryptography_get_name(name):
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}"'.format(name))
|
||||
return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1)))
|
||||
if name.startswith('otherName:'):
|
||||
# otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with.
|
||||
m = re.match(r'^([0-9]+(?:\.[0-9]+)*);([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2})*)$', to_text(name[10:]))
|
||||
if not m:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}"'.format(name))
|
||||
return x509.OtherName(x509.oid.ObjectIdentifier(m.group(1)), _parse_hex(m.group(2)))
|
||||
if m:
|
||||
return x509.OtherName(x509.oid.ObjectIdentifier(m.group(1)), _parse_hex(m.group(2)))
|
||||
|
||||
# See https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html - Subject Alternative Name for more
|
||||
# defailts on the format expected.
|
||||
name = to_text(name[10:], errors='surrogate_or_strict')
|
||||
if ';' not in name:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name otherName "{0}", must be in the '
|
||||
'format "otherName:<OID>;<ASN.1 OpenSSL Encoded String>" or '
|
||||
'"otherName:<OID>;<hex string>"'.format(name))
|
||||
|
||||
oid, value = name.split(';', 1)
|
||||
b_value = serialize_asn1_string_as_der(value)
|
||||
return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
|
||||
if name.startswith('dirName:'):
|
||||
return x509.DirectoryName(x509.Name(_parse_dn(to_text(name[8:]))))
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user