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