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