You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

222 lines
6.0 KiB

4 years ago
  1. # coding: utf-8
  2. """
  3. Encoding DER to PEM and decoding PEM to DER. Exports the following items:
  4. - armor()
  5. - detect()
  6. - unarmor()
  7. """
  8. from __future__ import unicode_literals, division, absolute_import, print_function
  9. import base64
  10. import re
  11. import sys
  12. from ._errors import unwrap
  13. from ._types import type_name as _type_name, str_cls, byte_cls
  14. if sys.version_info < (3,):
  15. from cStringIO import StringIO as BytesIO
  16. else:
  17. from io import BytesIO
  18. def detect(byte_string):
  19. """
  20. Detect if a byte string seems to contain a PEM-encoded block
  21. :param byte_string:
  22. A byte string to look through
  23. :return:
  24. A boolean, indicating if a PEM-encoded block is contained in the byte
  25. string
  26. """
  27. if not isinstance(byte_string, byte_cls):
  28. raise TypeError(unwrap(
  29. '''
  30. byte_string must be a byte string, not %s
  31. ''',
  32. _type_name(byte_string)
  33. ))
  34. return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1
  35. def armor(type_name, der_bytes, headers=None):
  36. """
  37. Armors a DER-encoded byte string in PEM
  38. :param type_name:
  39. A unicode string that will be capitalized and placed in the header
  40. and footer of the block. E.g. "CERTIFICATE", "PRIVATE KEY", etc. This
  41. will appear as "-----BEGIN CERTIFICATE-----" and
  42. "-----END CERTIFICATE-----".
  43. :param der_bytes:
  44. A byte string to be armored
  45. :param headers:
  46. An OrderedDict of the header lines to write after the BEGIN line
  47. :return:
  48. A byte string of the PEM block
  49. """
  50. if not isinstance(der_bytes, byte_cls):
  51. raise TypeError(unwrap(
  52. '''
  53. der_bytes must be a byte string, not %s
  54. ''' % _type_name(der_bytes)
  55. ))
  56. if not isinstance(type_name, str_cls):
  57. raise TypeError(unwrap(
  58. '''
  59. type_name must be a unicode string, not %s
  60. ''',
  61. _type_name(type_name)
  62. ))
  63. type_name = type_name.upper().encode('ascii')
  64. output = BytesIO()
  65. output.write(b'-----BEGIN ')
  66. output.write(type_name)
  67. output.write(b'-----\n')
  68. if headers:
  69. for key in headers:
  70. output.write(key.encode('ascii'))
  71. output.write(b': ')
  72. output.write(headers[key].encode('ascii'))
  73. output.write(b'\n')
  74. output.write(b'\n')
  75. b64_bytes = base64.b64encode(der_bytes)
  76. b64_len = len(b64_bytes)
  77. i = 0
  78. while i < b64_len:
  79. output.write(b64_bytes[i:i + 64])
  80. output.write(b'\n')
  81. i += 64
  82. output.write(b'-----END ')
  83. output.write(type_name)
  84. output.write(b'-----\n')
  85. return output.getvalue()
  86. def _unarmor(pem_bytes):
  87. """
  88. Convert a PEM-encoded byte string into one or more DER-encoded byte strings
  89. :param pem_bytes:
  90. A byte string of the PEM-encoded data
  91. :raises:
  92. ValueError - when the pem_bytes do not appear to be PEM-encoded bytes
  93. :return:
  94. A generator of 3-element tuples in the format: (object_type, headers,
  95. der_bytes). The object_type is a unicode string of what is between
  96. "-----BEGIN " and "-----". Examples include: "CERTIFICATE",
  97. "PUBLIC KEY", "PRIVATE KEY". The headers is a dict containing any lines
  98. in the form "Name: Value" that are right after the begin line.
  99. """
  100. if not isinstance(pem_bytes, byte_cls):
  101. raise TypeError(unwrap(
  102. '''
  103. pem_bytes must be a byte string, not %s
  104. ''',
  105. _type_name(pem_bytes)
  106. ))
  107. # Valid states include: "trash", "headers", "body"
  108. state = 'trash'
  109. headers = {}
  110. base64_data = b''
  111. object_type = None
  112. found_start = False
  113. found_end = False
  114. for line in pem_bytes.splitlines(False):
  115. if line == b'':
  116. continue
  117. if state == "trash":
  118. # Look for a starting line since some CA cert bundle show the cert
  119. # into in a parsed format above each PEM block
  120. type_name_match = re.match(b'^(?:---- |-----)BEGIN ([A-Z0-9 ]+)(?: ----|-----)', line)
  121. if not type_name_match:
  122. continue
  123. object_type = type_name_match.group(1).decode('ascii')
  124. found_start = True
  125. state = 'headers'
  126. continue
  127. if state == 'headers':
  128. if line.find(b':') == -1:
  129. state = 'body'
  130. else:
  131. decoded_line = line.decode('ascii')
  132. name, value = decoded_line.split(':', 1)
  133. headers[name] = value.strip()
  134. continue
  135. if state == 'body':
  136. if line[0:5] in (b'-----', b'---- '):
  137. der_bytes = base64.b64decode(base64_data)
  138. yield (object_type, headers, der_bytes)
  139. state = 'trash'
  140. headers = {}
  141. base64_data = b''
  142. object_type = None
  143. found_end = True
  144. continue
  145. base64_data += line
  146. if not found_start or not found_end:
  147. raise ValueError(unwrap(
  148. '''
  149. pem_bytes does not appear to contain PEM-encoded data - no
  150. BEGIN/END combination found
  151. '''
  152. ))
  153. def unarmor(pem_bytes, multiple=False):
  154. """
  155. Convert a PEM-encoded byte string into a DER-encoded byte string
  156. :param pem_bytes:
  157. A byte string of the PEM-encoded data
  158. :param multiple:
  159. If True, function will return a generator
  160. :raises:
  161. ValueError - when the pem_bytes do not appear to be PEM-encoded bytes
  162. :return:
  163. A 3-element tuple (object_name, headers, der_bytes). The object_name is
  164. a unicode string of what is between "-----BEGIN " and "-----". Examples
  165. include: "CERTIFICATE", "PUBLIC KEY", "PRIVATE KEY". The headers is a
  166. dict containing any lines in the form "Name: Value" that are right
  167. after the begin line.
  168. """
  169. generator = _unarmor(pem_bytes)
  170. if not multiple:
  171. return next(generator)
  172. return generator