Source code for pv080_crypto.certificates

"""
The :py:mod:`certificates <pv080_crypto.certificates>` module contains functions dealing with X.509 public key certificates.

The functions :py:func:`request_cert <pv080_crypto.certificates.request_cert>` and
:py:func:`verify_challenge <pv080_crypto.certificates.verify_challenge>` are used during the challenge-response protocol when one
wants to obtain a public key certificate from the PV080 Certificate Authority.

The function :py:func:`fetch_cert <pv080_crypto.certificates.fetch_cert>` can be used to obtain a certificate of any user, and
the signature in any certificate can be verified using :py:func:`verify_cert_signature <pv080_crypto.certificates.verify_cert_signature>`
"""

import requests

from typing import Optional, Tuple

from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import cryptography.hazmat.primitives.asymmetric.padding as padding_asymmetric

from pv080_crypto.config import (
    SERVER_CERT_REQUEST_CHALLENGE,
    SERVER_CERT_VERIFY_CHALLENGE,
    SERVER_CERTIFICATES,
)


[docs] def verify_cert_signature( ca_public_key: rsa.RSAPublicKey, cert: x509.Certificate, ) -> bool: """ Verifies whether a given certificate was signed by the Certificate Authority whose public key is provided. :param ca_public_key: The public key of the Certificate Authority, used to verify the signature. :param cert: The certificate in which the signature should be verified. :returns: ``True`` if ``cert`` was signed with the private key corresponding to ``ca_public_key``, ``False`` otherwise. Example: >>> from pv080_crypto import verify_cert_signature, fetch_cert, load_cert # doctest: +SKIP >>> ca_cert = load_cert("pv080-root.pem") # doctest: +SKIP >>> ca_public_key = ca_cert.public_key() # doctest: +SKIP >>> cert = fetch_cert(410390) # doctest: +SKIP >>> print(verify_cert_signature(ca_public_key, cert)) # doctest: +SKIP """ try: ca_public_key.verify( cert.signature, cert.tbs_certificate_bytes, padding_asymmetric.PKCS1v15(), cert.signature_hash_algorithm, ) return True except: return False
# FIXME improve the verification
[docs] def request_cert( csr: x509.CertificateSigningRequest, ) -> Optional[Tuple[str, bytes]]: """ Requests a certificate from the PV080 Server CA using a provided certificate signing request. :param csr: The Certificate Signing Request (created with the :py:func:`create_csr <pv080_crypto.utils.create_csr>` function). :returns: ``None`` if unsuccessful, a challenge sent by the CA otherwise. The challenge is a pair of the following two elements: - ``path`` is a random hexadecimal string specifying a path to a file.\ To prove that you are, indeed, the person with xlogin,\ you must create a file at this path on your aisa web server. (Since only you have the access to your\ `Aisa <https://www.fi.muni.cz/tech/unix/aisa.html.en>`_ web server,\ this counts as a proof of your identity.) - ``nonce`` are random bytes. By signing this nonce, you prove that you\ hold the private key corresponding to the public key in the CSR. 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) >>> path, nonce = request_cert(csr) # doctest: +SKIP """ data = {"csr": csr.public_bytes(serialization.Encoding.PEM).hex()} response = requests.post(SERVER_CERT_REQUEST_CHALLENGE, json=data) if response.json()["status"] != "OK": print(response.json()["status"]) return None path = response.json()["path"] nonce = bytes.fromhex(response.json()["nonce"]) return path, nonce
[docs] def verify_challenge(nonce: bytes, signed_nonce: bytes) -> bool: """ Sends a request to the PV080 Server CA asking to verify the completion of a challenge. The CA verifies 3 things: - That the received ``nonce`` was previously issued as a challenge. - That the received ``signed_nonce`` was signed by the private key of the challenged entity. - That the entity created a file on the Aisa web server according to the challenge. When the verification succeeds, the CA publishes the certificate of the challenged entity which can then be downloaded using :py:func:`fetch_cert <pv080_crypto.certificates.fetch_cert>`. :param nonce: The nonce to be signed with the private key of the challenged entity. :param signed_nonce: The signature of the nonce. :returns: ``True`` if verified successfully, ``False`` otherwise. Example: >>> from pv080_crypto import request_cert, create_signature, verify_challenge # doctest: +SKIP >>> # Assume `private_key` and `csr` are already created # doctest: +SKIP >>> path, nonce = request_cert(csr) # doctest: +SKIP >>> signed_nonce = create_signature(private_key, nonce) # doctest: +SKIP >>> # The `~/public_html/<path>` must be present on Aisa # doctest: +SKIP >>> verify_challenge(nonce, signed_nonce) # doctest: +SKIP """ response = requests.post( SERVER_CERT_VERIFY_CHALLENGE, json={"nonce": nonce.hex(), "signed_nonce": signed_nonce.hex()}, ) print(response.json()["status"]) return response.json()["status"] == "OK"
[docs] def fetch_cert(uco: int) -> Optional[x509.Certificate]: """ Fetches a certificate for a given UCO from the PV080 server. :param uco: The UČO of the person whose certificate is going to be fetched. :returns: The certificate from of the person with ``uco``, ``None`` if no such certificate is found. Example: >>> from pv080_crypto import fetch_cert # doctest: +SKIP >>> cert = fetch_cert(410390) # doctest: +SKIP """ response = requests.get(SERVER_CERTIFICATES, params={"uco": uco}) if response.json()["status"] != "OK": print(response.json()["status"]) return None cert_pem = response.json()["certificate"] cert = x509.load_pem_x509_certificate(cert_pem.encode()) return cert