get_pki dependable

This commit is contained in:
Jörn-Michael Miehe 2022-04-07 09:43:58 +00:00
parent c76d80bf47
commit ba7d28e931
3 changed files with 74 additions and 21 deletions

View file

@ -199,10 +199,9 @@ class EasyRSA:
def __build_cert( def __build_cert(
self, self,
cert_filename: Path,
*easyrsa_cmd: str, *easyrsa_cmd: str,
**easyrsa_env: str, **easyrsa_env: str,
) -> x509.Certificate | None: ) -> None:
""" """
Create an X.509 certificate Create an X.509 certificate
""" """
@ -230,16 +229,35 @@ class EasyRSA:
**easyrsa_env, **easyrsa_env,
) )
# parse the new certificate except (subprocess.CalledProcessError):
with open( # certificate couldn't be built
self.output_directory.joinpath(cert_filename), "rb" pass
) as cert_file:
return None
def get_certificate(
self,
cert_type: CertificateType | None,
dn: DistinguishedName | None = None,
) -> x509.Certificate | None:
if cert_type is CertificateType.ca:
cert_filename = self.output_directory.joinpath("ca.crt")
else:
if dn is None:
dn = DistinguishedName.build()
cert_filename = (self.output_directory.joinpath("issued")
.joinpath(f"{dn.common_name}.crt"))
try:
# parse the certificate
with open(cert_filename, "rb") as cert_file:
return x509.load_pem_x509_certificate( return x509.load_pem_x509_certificate(
cert_file.read() cert_file.read()
) )
except (subprocess.CalledProcessError, FileNotFoundError): except FileNotFoundError:
# certificate couldn't be built
return None return None
def init_pki(self) -> None: def init_pki(self) -> None:
@ -254,14 +272,17 @@ class EasyRSA:
Build the CA certificate Build the CA certificate
""" """
cert = self.__build_cert( self.__build_cert(
Path("ca.crt"),
"build-ca", "build-ca",
EASYRSA_DN="cn_only", EASYRSA_DN="cn_only",
EASYRSA_REQ_CN="kiwi-vpn-ca", EASYRSA_REQ_CN="kiwi-vpn-ca",
) )
cert = self.get_certificate(
cert_type=CertificateType.ca,
dn=None,
)
assert cert is not None assert cert is not None
# # this takes long! # # this takes long!
@ -284,9 +305,7 @@ class EasyRSA:
or cert_type is CertificateType.server): or cert_type is CertificateType.server):
return None return None
return self.__build_cert( self.__build_cert(
Path("issued").joinpath(f"{dn.common_name}.crt"),
f"build-{cert_type}-full", f"build-{cert_type}-full",
dn.common_name, dn.common_name,
"nopass", "nopass",
@ -294,6 +313,11 @@ class EasyRSA:
**dn.easyrsa_env, **dn.easyrsa_env,
) )
return self.get_certificate(
cert_type=cert_type,
dn=dn,
)
def renew( def renew(
self, self,
dn: DistinguishedName | None = None, dn: DistinguishedName | None = None,
@ -305,9 +329,7 @@ class EasyRSA:
if dn is None: if dn is None:
dn = DistinguishedName.build() dn = DistinguishedName.build()
return self.__build_cert( self.__build_cert(
Path("issued").joinpath(f"{dn.common_name}.crt"),
"renew", "renew",
dn.common_name, dn.common_name,
"nopass", "nopass",
@ -318,6 +340,11 @@ class EasyRSA:
**dn.easyrsa_env, **dn.easyrsa_env,
) )
return self.get_certificate(
cert_type=None,
dn=dn,
)
def revoke( def revoke(
self, self,
dn: DistinguishedName | None = None, dn: DistinguishedName | None = None,

View file

@ -7,6 +7,7 @@ from fastapi.security import OAuth2PasswordBearer
from ..config import SETTINGS, Config from ..config import SETTINGS, Config
from ..db import Device, User from ..db import Device, User
from ..easyrsa import EASYRSA, CertificateType, EasyRSA
oauth2_scheme = OAuth2PasswordBearer( oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{SETTINGS.api_v1_prefix}/user/authenticate" tokenUrl=f"{SETTINGS.api_v1_prefix}/user/authenticate"
@ -35,6 +36,10 @@ class Responses:
"description": "Operation not permitted", "description": "Operation not permitted",
"content": None, "content": None,
} }
NEEDS_PKI = {
"description": "PKI hasn't been initialized",
"content": None,
}
ENTRY_ADDED = { ENTRY_ADDED = {
"description": "Entry added to database", "description": "Entry added to database",
"content": None, "content": None,
@ -129,3 +134,18 @@ async def get_device_by_id(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return device return device
async def get_pki() -> EasyRSA:
"""
Get the EasyRSA object if the CA has been built.
Status:
- 425: EasyRSA not initialized
"""
if EASYRSA.get_certificate(CertificateType.ca) is None:
raise HTTPException(status_code=status.HTTP_425_TOO_EARLY)
return EASYRSA

View file

@ -5,8 +5,8 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from ..db import Device, DeviceCreate, DeviceRead, DeviceStatus, User from ..db import Device, DeviceCreate, DeviceRead, DeviceStatus, User
from ..easyrsa import EASYRSA, DistinguishedName from ..easyrsa import DistinguishedName, EasyRSA
from ._common import (Responses, get_current_user, get_device_by_id, from ._common import (Responses, get_current_user, get_device_by_id, get_pki,
get_user_by_name) get_user_by_name)
router = APIRouter(prefix="/device", tags=["device"]) router = APIRouter(prefix="/device", tags=["device"])
@ -96,12 +96,14 @@ async def remove_device(
status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION, status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION,
status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST, status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST,
status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS, status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS,
status.HTTP_425_TOO_EARLY: Responses.NEEDS_PKI,
}, },
response_model=DeviceRead, response_model=DeviceRead,
) )
async def request_certificate_issuance( async def request_certificate_issuance(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
device: Device = Depends(get_device_by_id), device: Device = Depends(get_device_by_id),
pki: EasyRSA = Depends(get_pki),
) -> Device: ) -> Device:
""" """
POST ./{device_id}/issue: Request certificate issuance for a device. POST ./{device_id}/issue: Request certificate issuance for a device.
@ -124,7 +126,7 @@ async def request_certificate_issuance(
# check if we can issue the certificate immediately # check if we can issue the certificate immediately
if current_user.can_issue: if current_user.can_issue:
if (certificate := EASYRSA.issue( if (certificate := pki.issue(
dn=DistinguishedName.build(device) dn=DistinguishedName.build(device)
)) is not None: )) is not None:
device.set_status(DeviceStatus.certified) device.set_status(DeviceStatus.certified)
@ -144,12 +146,14 @@ async def request_certificate_issuance(
status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION, status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION,
status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST, status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST,
status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS, status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS,
status.HTTP_425_TOO_EARLY: Responses.NEEDS_PKI,
}, },
response_model=DeviceRead, response_model=DeviceRead,
) )
async def request_certificate_renewal( async def request_certificate_renewal(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
device: Device = Depends(get_device_by_id), device: Device = Depends(get_device_by_id),
pki: EasyRSA = Depends(get_pki),
) -> Device: ) -> Device:
""" """
POST ./{device_id}/renew: Request certificate renewal for a device. POST ./{device_id}/renew: Request certificate renewal for a device.
@ -172,7 +176,7 @@ async def request_certificate_renewal(
# check if we can renew the certificate immediately # check if we can renew the certificate immediately
if current_user.can_renew: if current_user.can_renew:
if (certificate := EASYRSA.renew( if (certificate := pki.renew(
dn=DistinguishedName.build(device) dn=DistinguishedName.build(device)
)) is not None: )) is not None:
device.set_status(DeviceStatus.certified) device.set_status(DeviceStatus.certified)
@ -192,12 +196,14 @@ async def request_certificate_renewal(
status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION, status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION,
status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST, status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST,
status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS, status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS,
status.HTTP_425_TOO_EARLY: Responses.NEEDS_PKI,
}, },
response_model=DeviceRead, response_model=DeviceRead,
) )
async def revoke_certificate( async def revoke_certificate(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
device: Device = Depends(get_device_by_id), device: Device = Depends(get_device_by_id),
pki: EasyRSA = Depends(get_pki),
) -> Device: ) -> Device:
""" """
POST ./{device_id}/revoke: Revoke a device certificate. POST ./{device_id}/revoke: Revoke a device certificate.
@ -217,7 +223,7 @@ async def revoke_certificate(
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
# revoke the device certificate # revoke the device certificate
EASYRSA.revoke(dn=DistinguishedName.build(device)) pki.revoke(dn=DistinguishedName.build(device))
# reset the device # reset the device
device.set_status(DeviceStatus.uncertified) device.set_status(DeviceStatus.uncertified)