| # 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 |