222 lines
6 KiB
Python
222 lines
6 KiB
Python
# coding: utf-8
|
|
|
|
"""
|
|
Encoding DER to PEM and decoding PEM to DER. Exports the following items:
|
|
|
|
- armor()
|
|
- detect()
|
|
- unarmor()
|
|
|
|
"""
|
|
|
|
from __future__ import unicode_literals, division, absolute_import, print_function
|
|
|
|
import base64
|
|
import re
|
|
import sys
|
|
|
|
from ._errors import unwrap
|
|
from ._types import type_name as _type_name, str_cls, byte_cls
|
|
|
|
if sys.version_info < (3,):
|
|
from cStringIO import StringIO as BytesIO
|
|
else:
|
|
from io import BytesIO
|
|
|
|
|
|
def detect(byte_string):
|
|
"""
|
|
Detect if a byte string seems to contain a PEM-encoded block
|
|
|
|
:param byte_string:
|
|
A byte string to look through
|
|
|
|
:return:
|
|
A boolean, indicating if a PEM-encoded block is contained in the byte
|
|
string
|
|
"""
|
|
|
|
if not isinstance(byte_string, byte_cls):
|
|
raise TypeError(unwrap(
|
|
'''
|
|
byte_string must be a byte string, not %s
|
|
''',
|
|
_type_name(byte_string)
|
|
))
|
|
|
|
return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1
|
|
|
|
|
|
def armor(type_name, der_bytes, headers=None):
|
|
"""
|
|
Armors a DER-encoded byte string in PEM
|
|
|
|
:param type_name:
|
|
A unicode string that will be capitalized and placed in the header
|
|
and footer of the block. E.g. "CERTIFICATE", "PRIVATE KEY", etc. This
|
|
will appear as "-----BEGIN CERTIFICATE-----" and
|
|
"-----END CERTIFICATE-----".
|
|
|
|
:param der_bytes:
|
|
A byte string to be armored
|
|
|
|
:param headers:
|
|
An OrderedDict of the header lines to write after the BEGIN line
|
|
|
|
:return:
|
|
A byte string of the PEM block
|
|
"""
|
|
|
|
if not isinstance(der_bytes, byte_cls):
|
|
raise TypeError(unwrap(
|
|
'''
|
|
der_bytes must be a byte string, not %s
|
|
''' % _type_name(der_bytes)
|
|
))
|
|
|
|
if not isinstance(type_name, str_cls):
|
|
raise TypeError(unwrap(
|
|
'''
|
|
type_name must be a unicode string, not %s
|
|
''',
|
|
_type_name(type_name)
|
|
))
|
|
|
|
type_name = type_name.upper().encode('ascii')
|
|
|
|
output = BytesIO()
|
|
output.write(b'-----BEGIN ')
|
|
output.write(type_name)
|
|
output.write(b'-----\n')
|
|
if headers:
|
|
for key in headers:
|
|
output.write(key.encode('ascii'))
|
|
output.write(b': ')
|
|
output.write(headers[key].encode('ascii'))
|
|
output.write(b'\n')
|
|
output.write(b'\n')
|
|
b64_bytes = base64.b64encode(der_bytes)
|
|
b64_len = len(b64_bytes)
|
|
i = 0
|
|
while i < b64_len:
|
|
output.write(b64_bytes[i:i + 64])
|
|
output.write(b'\n')
|
|
i += 64
|
|
output.write(b'-----END ')
|
|
output.write(type_name)
|
|
output.write(b'-----\n')
|
|
|
|
return output.getvalue()
|
|
|
|
|
|
def _unarmor(pem_bytes):
|
|
"""
|
|
Convert a PEM-encoded byte string into one or more DER-encoded byte strings
|
|
|
|
:param pem_bytes:
|
|
A byte string of the PEM-encoded data
|
|
|
|
:raises:
|
|
ValueError - when the pem_bytes do not appear to be PEM-encoded bytes
|
|
|
|
:return:
|
|
A generator of 3-element tuples in the format: (object_type, headers,
|
|
der_bytes). The object_type is a unicode string of what is between
|
|
"-----BEGIN " and "-----". Examples include: "CERTIFICATE",
|
|
"PUBLIC KEY", "PRIVATE KEY". The headers is a dict containing any lines
|
|
in the form "Name: Value" that are right after the begin line.
|
|
"""
|
|
|
|
if not isinstance(pem_bytes, byte_cls):
|
|
raise TypeError(unwrap(
|
|
'''
|
|
pem_bytes must be a byte string, not %s
|
|
''',
|
|
_type_name(pem_bytes)
|
|
))
|
|
|
|
# Valid states include: "trash", "headers", "body"
|
|
state = 'trash'
|
|
headers = {}
|
|
base64_data = b''
|
|
object_type = None
|
|
|
|
found_start = False
|
|
found_end = False
|
|
|
|
for line in pem_bytes.splitlines(False):
|
|
if line == b'':
|
|
continue
|
|
|
|
if state == "trash":
|
|
# Look for a starting line since some CA cert bundle show the cert
|
|
# into in a parsed format above each PEM block
|
|
type_name_match = re.match(b'^(?:---- |-----)BEGIN ([A-Z0-9 ]+)(?: ----|-----)', line)
|
|
if not type_name_match:
|
|
continue
|
|
object_type = type_name_match.group(1).decode('ascii')
|
|
|
|
found_start = True
|
|
state = 'headers'
|
|
continue
|
|
|
|
if state == 'headers':
|
|
if line.find(b':') == -1:
|
|
state = 'body'
|
|
else:
|
|
decoded_line = line.decode('ascii')
|
|
name, value = decoded_line.split(':', 1)
|
|
headers[name] = value.strip()
|
|
continue
|
|
|
|
if state == 'body':
|
|
if line[0:5] in (b'-----', b'---- '):
|
|
der_bytes = base64.b64decode(base64_data)
|
|
|
|
yield (object_type, headers, der_bytes)
|
|
|
|
state = 'trash'
|
|
headers = {}
|
|
base64_data = b''
|
|
object_type = None
|
|
found_end = True
|
|
continue
|
|
|
|
base64_data += line
|
|
|
|
if not found_start or not found_end:
|
|
raise ValueError(unwrap(
|
|
'''
|
|
pem_bytes does not appear to contain PEM-encoded data - no
|
|
BEGIN/END combination found
|
|
'''
|
|
))
|
|
|
|
|
|
def unarmor(pem_bytes, multiple=False):
|
|
"""
|
|
Convert a PEM-encoded byte string into a DER-encoded byte string
|
|
|
|
:param pem_bytes:
|
|
A byte string of the PEM-encoded data
|
|
|
|
:param multiple:
|
|
If True, function will return a generator
|
|
|
|
:raises:
|
|
ValueError - when the pem_bytes do not appear to be PEM-encoded bytes
|
|
|
|
:return:
|
|
A 3-element tuple (object_name, headers, der_bytes). The object_name is
|
|
a unicode string of what is between "-----BEGIN " and "-----". Examples
|
|
include: "CERTIFICATE", "PUBLIC KEY", "PRIVATE KEY". The headers is a
|
|
dict containing any lines in the form "Name: Value" that are right
|
|
after the begin line.
|
|
"""
|
|
|
|
generator = _unarmor(pem_bytes)
|
|
|
|
if not multiple:
|
|
return next(generator)
|
|
|
|
return generator
|