1# -*- coding: utf-8 -*- 2 3# (c) 2020, Jordan Borean <jborean93@gmail.com> 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9import re 10 11from ansible.module_utils.common.text.converters import to_bytes 12 13 14""" 15An ASN.1 serialized as a string in the OpenSSL format: 16 [modifier,]type[:value] 17 18modifier: 19 The modifier can be 'IMPLICIT:<tag_number><tag_class>,' or 'EXPLICIT:<tag_number><tag_class>' where IMPLICIT 20 changes the tag of the universal value to encode and EXPLICIT prefixes its tag to the existing universal value. 21 The tag_number must be set while the tag_class can be 'U', 'A', 'P', or 'C" for 'Universal', 'Application', 22 'Private', or 'Context Specific' with C being the default. 23 24type: 25 The underlying ASN.1 type of the value specified. Currently only the following have been implemented: 26 UTF8: The value must be a UTF-8 encoded string. 27 28value: 29 The value to encode, the format of this value depends on the <type> specified. 30""" 31ASN1_STRING_REGEX = re.compile(r'^((?P<tag_type>IMPLICIT|EXPLICIT):(?P<tag_number>\d+)(?P<tag_class>U|A|P|C)?,)?' 32 r'(?P<value_type>[\w\d]+):(?P<value>.*)') 33 34 35class TagClass: 36 universal = 0 37 application = 1 38 context_specific = 2 39 private = 3 40 41 42# Universal tag numbers that can be encoded. 43class TagNumber: 44 utf8_string = 12 45 46 47def _pack_octet_integer(value): 48 """ Packs an integer value into 1 or multiple octets. """ 49 # NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value. 50 octets = bytearray() 51 52 # Continue to shift the number by 7 bits and pack into an octet until the 53 # value is fully packed. 54 while value: 55 octet_value = value & 0b01111111 56 57 # First round (last octet) must have the MSB set. 58 if len(octets): 59 octet_value |= 0b10000000 60 61 octets.append(octet_value) 62 value >>= 7 63 64 # Reverse to ensure the higher order octets are first. 65 octets.reverse() 66 return bytes(octets) 67 68 69def serialize_asn1_string_as_der(value): 70 """ Deserializes an ASN.1 string to a DER encoded byte string. """ 71 asn1_match = ASN1_STRING_REGEX.match(value) 72 if not asn1_match: 73 raise ValueError("The ASN.1 serialized string must be in the format [modifier,]type[:value]") 74 75 tag_type = asn1_match.group('tag_type') 76 tag_number = asn1_match.group('tag_number') 77 tag_class = asn1_match.group('tag_class') or 'C' 78 value_type = asn1_match.group('value_type') 79 asn1_value = asn1_match.group('value') 80 81 if value_type != 'UTF8': 82 raise ValueError('The ASN.1 serialized string is not a known type "{0}", only UTF8 types are ' 83 'supported'.format(value_type)) 84 85 b_value = to_bytes(asn1_value, encoding='utf-8', errors='surrogate_or_strict') 86 87 # We should only do a universal type tag if not IMPLICITLY tagged or the tag class is not universal. 88 if not tag_type or (tag_type == 'EXPLICIT' and tag_class != 'U'): 89 b_value = pack_asn1(TagClass.universal, False, TagNumber.utf8_string, b_value) 90 91 if tag_type: 92 tag_class = { 93 'U': TagClass.universal, 94 'A': TagClass.application, 95 'P': TagClass.private, 96 'C': TagClass.context_specific, 97 }[tag_class] 98 99 # When adding support for more types this should be looked into further. For now it works with UTF8Strings. 100 constructed = tag_type == 'EXPLICIT' and tag_class != TagClass.universal 101 b_value = pack_asn1(tag_class, constructed, int(tag_number), b_value) 102 103 return b_value 104 105 106def pack_asn1(tag_class, constructed, tag_number, b_data): 107 """Pack the value into an ASN.1 data structure. 108 109 The structure for an ASN.1 element is 110 111 | Identifier Octet(s) | Length Octet(s) | Data Octet(s) | 112 """ 113 b_asn1_data = bytearray() 114 115 if tag_class < 0 or tag_class > 3: 116 raise ValueError("tag_class must be between 0 and 3 not %s" % tag_class) 117 118 # Bit 8 and 7 denotes the class. 119 identifier_octets = tag_class << 6 120 # Bit 6 denotes whether the value is primitive or constructed. 121 identifier_octets |= ((1 if constructed else 0) << 5) 122 123 # Bits 5-1 contain the tag number, if it cannot be encoded in these 5 bits 124 # then they are set and another octet(s) is used to denote the tag number. 125 if tag_number < 31: 126 identifier_octets |= tag_number 127 b_asn1_data.append(identifier_octets) 128 else: 129 identifier_octets |= 31 130 b_asn1_data.append(identifier_octets) 131 b_asn1_data.extend(_pack_octet_integer(tag_number)) 132 133 length = len(b_data) 134 135 # If the length can be encoded in 7 bits only 1 octet is required. 136 if length < 128: 137 b_asn1_data.append(length) 138 139 else: 140 # Otherwise the length must be encoded across multiple octets 141 length_octets = bytearray() 142 while length: 143 length_octets.append(length & 0b11111111) 144 length >>= 8 145 146 length_octets.reverse() # Reverse to make the higher octets first. 147 148 # The first length octet must have the MSB set alongside the number of 149 # octets the length was encoded in. 150 b_asn1_data.append(len(length_octets) | 0b10000000) 151 b_asn1_data.extend(length_octets) 152 153 return bytes(b_asn1_data) + b_data 154