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