Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 105 additions & 1 deletion cl_sii/libs/crypto_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
"""
Crypto utils
============


DER and PEM
-----------

Best answer to the
`StackOverflow question <https://stackoverflow.com/a/22743616>`_
"What are the differences between .pem, .cer and .der?".

Best answer to the
`ServerFault question <https://https://serverfault.com/a/9717>`_.
"What is a Pem file and how does it differ from other OpenSSL Generated Key File Formats?".


DER
--------

DER stands for "Distinguished Encoding Rules".

> A way to encode ASN.1 syntax in binary.

> The parent format of PEM. It's useful to think of it as a binary version
> of the base64-encoded PEM file.

PEM
--------

PEM stands for "Privacy Enhanced Mail".

> A failed method for secure email but the container format it used lives on,
> and is a base64 translation of the x509 ASN.1 keys.

> In the case that it encodes a certificate it would simply contain the
> base64 encoding of the DER certificate [plus the header and footer].

"""
import base64
from typing import Union

import cryptography.x509
Expand All @@ -6,10 +46,35 @@
from cryptography.x509 import Certificate as X509Cert
from OpenSSL.crypto import X509 as _X509CertOpenSsl # noqa: F401

from . import encoding_utils


def load_der_x509_cert(der_value: bytes) -> X509Cert:
"""
Load an X.509 certificate from DER-encoded certificate data.

:raises TypeError:
:raises ValueError:

"""
if not isinstance(der_value, bytes):
raise TypeError("Value must be bytes.")

try:
x509_cert = cryptography.x509.load_der_x509_certificate(
data=der_value,
backend=_crypto_x509_backend)
except ValueError:
# e.g.
# "Unable to load certificate"
raise

return x509_cert


def load_pem_x509_cert(pem_value: Union[str, bytes]) -> X509Cert:
"""
Load an X.509 certificate from a PEM-formatted value.
Load an X.509 certificate from PEM-encoded certificate data.

.. seealso::
https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file
Expand Down Expand Up @@ -39,6 +104,45 @@ def load_pem_x509_cert(pem_value: Union[str, bytes]) -> X509Cert:
return x509_cert


def x509_cert_der_to_pem(der_value: bytes) -> bytes:
"""
Convert an X.509 certificate DER-encoded data to PEM-encoded.

.. warning::
It does not validate that ``der_value`` corresponds to an X.509 cert.

:raises TypeError:

"""
if not isinstance(der_value, bytes):
raise TypeError("Value must be bytes.")

pem_value = base64.standard_b64encode(der_value)
mod_pem_value = add_pem_cert_header_footer(pem_value)

return mod_pem_value.strip()


def x509_cert_pem_to_der(pem_value: bytes) -> bytes:
"""
Convert an X.509 certificate PEM-encoded data to DER-encoded.

.. warning::
It does not validate that ``pem_value`` corresponds to an X.509 cert.

:raises TypeError:
:raises ValueError:

"""
if not isinstance(pem_value, bytes):
raise TypeError("Value must be bytes.")

mod_pem_value = remove_pem_cert_header_footer(pem_value)
der_value = encoding_utils.decode_base64_strict(mod_pem_value)

return der_value.strip()


def add_pem_cert_header_footer(pem_cert: bytes) -> bytes:
"""
Add certificate PEM header and footer (if not already present).
Expand Down
140 changes: 128 additions & 12 deletions tests/test_libs_crypto_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from cryptography.x509 import oid

from cl_sii.libs.crypto_utils import ( # noqa: F401
X509Cert, add_pem_cert_header_footer, load_pem_x509_cert, remove_pem_cert_header_footer,
X509Cert, add_pem_cert_header_footer, load_der_x509_cert, load_pem_x509_cert,
remove_pem_cert_header_footer,
x509_cert_der_to_pem, x509_cert_pem_to_der,
)

from . import utils
Expand Down Expand Up @@ -40,11 +42,11 @@ def test_remove_pem_cert_header_footer(self) -> None:

class LoadPemX509CertTest(unittest.TestCase):

def test_load_pem_x509_cert_ok(self) -> None:
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/crypto/wildcard-google-com-cert.pem')
def test_load_der_x509_cert_ok(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/crypto/wildcard-google-com-cert.der')

x509_cert = load_pem_x509_cert(cert_pem_bytes)
x509_cert = load_der_x509_cert(cert_der_bytes)

self.assertIsInstance(x509_cert, X509Cert)

Expand Down Expand Up @@ -217,11 +219,11 @@ def test_load_pem_x509_cert_ok(self) -> None:
self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None)
self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None)

def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None:
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem')
def test_load_der_x509_cert_ok_cert_real_dte(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der')

x509_cert = load_pem_x509_cert(cert_pem_bytes)
x509_cert = load_der_x509_cert(cert_der_bytes)

self.assertIsInstance(x509_cert, X509Cert)

Expand Down Expand Up @@ -439,10 +441,11 @@ def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None:
self.assertEqual(some_microsoft_ext.critical, False)
self.assertTrue(isinstance(some_microsoft_ext.value.value, bytes))

def test_load_pem_x509_cert_ok_prueba_sii(self) -> None:
cert_pem_bytes = utils.read_test_file_bytes('test_data/sii-crypto/prueba-sii-cert.pem')
def test_load_der_x509_cert_ok_prueba_sii(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/prueba-sii-cert.der')

x509_cert = load_pem_x509_cert(cert_pem_bytes)
x509_cert = load_der_x509_cert(cert_der_bytes)

self.assertIsInstance(x509_cert, X509Cert)

Expand Down Expand Up @@ -615,6 +618,54 @@ def test_load_pem_x509_cert_ok_prueba_sii(self) -> None:
self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None)
self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None)

def test_load_der_x509_cert_fail_type_error(self) -> None:
with self.assertRaises(TypeError) as cm:
load_der_x509_cert(1)
self.assertEqual(cm.exception.args, ("Value must be bytes.", ))

def test_load_der_x509_cert_fail_value_error(self) -> None:
with self.assertRaises(ValueError) as cm:
load_der_x509_cert(b'hello')
self.assertEqual(
cm.exception.args,
("Unable to load certificate", ))

def test_load_pem_x509_cert_ok(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/crypto/wildcard-google-com-cert.der')
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/crypto/wildcard-google-com-cert.pem')

x509_cert_from_der = load_der_x509_cert(cert_der_bytes)
x509_cert_from_pem = load_pem_x509_cert(cert_pem_bytes)

self.assertIsInstance(x509_cert_from_pem, X509Cert)
self.assertEqual(x509_cert_from_der, x509_cert_from_pem)

def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der')
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem')

x509_cert_from_der = load_der_x509_cert(cert_der_bytes)
x509_cert_from_pem = load_pem_x509_cert(cert_pem_bytes)

self.assertIsInstance(x509_cert_from_pem, X509Cert)
self.assertEqual(x509_cert_from_der, x509_cert_from_pem)

def test_load_pem_x509_cert_ok_prueba_sii(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/prueba-sii-cert.der')
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/prueba-sii-cert.pem')

x509_cert_from_der = load_der_x509_cert(cert_der_bytes)
x509_cert_from_pem = load_pem_x509_cert(cert_pem_bytes)

self.assertIsInstance(x509_cert_from_pem, X509Cert)
self.assertEqual(x509_cert_from_der, x509_cert_from_pem)

def test_load_pem_x509_cert_ok_str_ascii(self) -> None:
cert_pem_str_ascii = utils.read_test_file_str_ascii(
'test_data/crypto/wildcard-google-com-cert.pem')
Expand Down Expand Up @@ -642,3 +693,68 @@ def test_load_pem_x509_cert_fail_value_error(self) -> None:
("Unable to load certificate. See "
"https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file "
"for more details.", ))

def test_x509_cert_der_to_pem_pem_to_der_ok_1(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/crypto/wildcard-google-com-cert.der')
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/crypto/wildcard-google-com-cert.pem')

# note: we test the function with a double call because the input PEM data
# may have different line lengths and different line separators.
self.assertEqual(
x509_cert_pem_to_der(x509_cert_der_to_pem(cert_der_bytes)),
x509_cert_pem_to_der(cert_pem_bytes))

def test_x509_cert_der_to_pem_pem_to_der_ok_2(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der')
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem')

# note: we test the function with a double call because the input PEM data
# may have different line lengths and different line separators.
self.assertEqual(
x509_cert_pem_to_der(x509_cert_der_to_pem(cert_der_bytes)),
x509_cert_pem_to_der(cert_pem_bytes))

def test_x509_cert_der_to_pem_pem_to_der_ok_3(self) -> None:
cert_der_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/prueba-sii-cert.der')
cert_pem_bytes = utils.read_test_file_bytes(
'test_data/sii-crypto/prueba-sii-cert.pem')

# note: we test the function with a double call because the input PEM data
# may have different line lengths and different line separators.
self.assertEqual(
x509_cert_pem_to_der(x509_cert_der_to_pem(cert_der_bytes)),
x509_cert_pem_to_der(cert_pem_bytes))

def test_x509_cert_der_to_pem_type_error(self) -> None:
with self.assertRaises(TypeError) as cm:
x509_cert_der_to_pem(1)
self.assertEqual(cm.exception.args, ("Value must be bytes.", ))

def test_x509_cert_pem_to_der_type_error(self) -> None:
with self.assertRaises(TypeError) as cm:
x509_cert_pem_to_der(1)
self.assertEqual(cm.exception.args, ("Value must be bytes.", ))

def test_x509_cert_pem_to_der_valuetype_error(self) -> None:
with self.assertRaises(ValueError) as cm:
x509_cert_pem_to_der(b'hello')
self.assertEqual(
cm.exception.args,
(
"Input is not a valid base64 value.",
"Invalid base64-encoded string: number of data characters (5) cannot be 1 more "
"than a multiple of 4",
))

def test_add_pem_cert_header_footer(self) -> None:
# TODO: implement for 'add_pem_cert_header_footer'
pass

def test_remove_pem_cert_header_footer(self) -> None:
# TODO: implement for 'remove_pem_cert_header_footer'
pass