Source code for pv080_crypto.utils

"""
This is a utility module containing miscellaneous functions dealing with keys and certificates.

The functions :py:func:`store_private_key <pv080_crypto.utils.store_private_key>` and
:py:func:`store_cert <pv080_crypto.utils.store_cert>` can be used to store keys and certificates
on disk in the `PEM <https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail>`_ format. The dual functions
:py:func:`load_private_key <pv080_crypto.utils.load_private_key>` and
:py:func:`load_cert <pv080_crypto.utils.load_cert>` are used to load such keys/certificates from files.

The function :py:func:`extract_names <pv080_crypto.utils.extract_names>` simplifies obtaining names
from X.509 certificates, and :py:func:`create_csr <pv080_crypto.utils.create_csr>` may be used to prepare
a *Certificate Signing Request* when one wants to obtain a certificate.

"""

import pathlib

from typing import List, Optional, Union, Tuple

from cryptography import x509
from cryptography.x509 import Certificate, CertificateSigningRequest
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes


[docs] def store_private_key( private_key: RSAPrivateKey, filename: str = "private_key.pem", overwrite: bool = False, ) -> bool: """ Stores an RSA private key in a given file. :param private_key: The RSA key to store. :param filename: The name of the file to store the key into. :param overwrite: A boolean flag to determine whether to overwrite an existing file. :returns: True if the `private_key` is written into `filename`, False otherwise. Example: >>> from cryptography.hazmat.primitives.asymmetric import rsa >>> from pv080_crypto import store_private_key >>> private_key = rsa.generate_private_key(65537, 2048) >>> store_private_key(private_key, "private_key.pem") """ pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) if not pathlib.Path(filename).exists() or overwrite: with open(filename, "wb") as handle: handle.write(pem) return True return False
[docs] def load_private_key( filename: str = "private_key.pem", ) -> RSAPrivateKey: """ Loads a private key from a given file. :param filename: The filename to look for the key in. :returns: The private key. Example: >>> from pv080_crypto import load_private_key >>> private_key = load_private_key("private_key.pem") """ filepath = pathlib.Path(filename) with open(filepath, "rb") as key_file: pem_data = key_file.read() private_key = serialization.load_pem_private_key( pem_data, password=None, ) return private_key
[docs] def store_cert( cert: Certificate, filename: str, overwrite: bool = False, ) -> bool: """ Stores an X.509 certificate in a given file. :param cert: The certificate to store. :param filename: The name of the file to store the certificate into. :param overwrite: A boolean flag to determine whether to overwrite an existing file. :returns: True if the `cert` is written into `filename`, False otherwise. Example: >>> from pv080_crypto import fetch_cert, store_cert # doctest: +SKIP >>> cert = fetch_cert(410390) # doctest: +SKIP >>> store_cert(cert, "cert.pem") # doctest: +SKIP """ if not pathlib.Path(filename).exists() or overwrite: with open(filename, "wb") as handle: handle.write(cert.public_bytes(serialization.Encoding.PEM)) return True return False
[docs] def load_cert(filename: str) -> Certificate: """ Loads a certificate from a given file. :param filename: The filename to look for the certificate in. :returns: The certificate itself. Example: >>> from pv080_crypto import load_cert # doctest: +SKIP >>> ca_cert = load_cert("pv080-root.pem") # doctest: +SKIP """ filepath = pathlib.Path(filename) with open(filepath, "rb") as cert_file: pem_data = cert_file.read() cert = x509.load_pem_x509_certificate(pem_data) return cert
[docs] def extract_names(cert: Certificate) -> Tuple[Optional[int], Optional[str]]: """ Extracts UČO and xlogin from a certificate. :param cert: The certificate to extract names from. :returns: A tuple of UČO and xlogin, if present in the certificate. """ common_names = [ name.value for name in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) ] uco: Optional[int] = None # guess and hope for name in common_names: try: uco = int(name) break except: pass xlogin: Optional[str] = None for name in common_names: if name.strip().startswith("x"): xlogin = name break return uco, xlogin
[docs] def create_csr(key: RSAPrivateKey, xlogin: str, uco: int) -> CertificateSigningRequest: """ Creates a Certificate Signing Request with given names. :param key: The key to sign the CSR with. The corresponding public key is the certified one. :param xlogin: The xlogin to insert as a name into the request. :param uco: The UČO to insert as a name into the request. :returns: The CSR itself. Example: >>> from cryptography.hazmat.primitives.asymmetric import rsa >>> from pv080_crypto import request_cert, create_csr >>> private_key = rsa.generate_private_key(65537, 2048) >>> public_key = private_key.public_key() >>> csr = create_csr(private_key, "xzacik", 485305) """ csr = ( x509.CertificateSigningRequestBuilder() .subject_name( x509.Name( [ # Provide various details about who we are. x509.NameAttribute(NameOID.COUNTRY_NAME, "CZ"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "South Moravia"), x509.NameAttribute(NameOID.LOCALITY_NAME, "Brno"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "PV080"), x509.NameAttribute(NameOID.COMMON_NAME, str(uco)), x509.NameAttribute(NameOID.COMMON_NAME, xlogin), ] ) ) .sign(key, hashes.SHA256()) ) return csr