2022-03-30 21:18:54 +00:00
|
|
|
"""
|
|
|
|
Python interface to EasyRSA CA.
|
|
|
|
"""
|
|
|
|
|
2022-03-22 00:34:06 +00:00
|
|
|
import subprocess
|
|
|
|
from datetime import datetime
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from OpenSSL import crypto
|
|
|
|
from passlib import pwd
|
|
|
|
|
2022-03-30 22:27:17 +00:00
|
|
|
from .config import CertificateAlgo, Config, Settings
|
2022-03-22 00:34:06 +00:00
|
|
|
|
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
class EasyRSA:
|
|
|
|
"""
|
|
|
|
Represents an EasyRSA PKI.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@property
|
|
|
|
def pki_directory(self) -> Path:
|
|
|
|
return Settings._.data_dir.joinpath("pki")
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ca_password(self) -> str:
|
|
|
|
config = Config._
|
|
|
|
|
|
|
|
if (ca_password := config.crypto.ca_password) is None:
|
|
|
|
ca_password = pwd.genword(
|
|
|
|
length=32,
|
|
|
|
charset="ascii_62",
|
|
|
|
)
|
2022-03-22 00:34:06 +00:00
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
config.crypto.ca_password = ca_password
|
|
|
|
config.save()
|
2022-03-22 00:34:06 +00:00
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
return config.crypto.ca_password
|
2022-03-22 00:34:06 +00:00
|
|
|
|
|
|
|
def __easyrsa(
|
|
|
|
self,
|
|
|
|
*easyrsa_args: str,
|
|
|
|
) -> subprocess.CompletedProcess:
|
|
|
|
return subprocess.run(
|
|
|
|
[
|
|
|
|
"easyrsa", "--batch",
|
2022-03-30 21:18:54 +00:00
|
|
|
f"--pki-dir={self.pki_directory}",
|
2022-03-22 00:34:06 +00:00
|
|
|
*easyrsa_args,
|
|
|
|
],
|
|
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
def __build_cert(
|
|
|
|
self,
|
|
|
|
cert_filename: Path,
|
2022-03-30 22:27:17 +00:00
|
|
|
expiry_days: int | None,
|
2022-03-22 00:34:06 +00:00
|
|
|
*easyrsa_args: str,
|
|
|
|
) -> crypto.X509:
|
2022-03-30 22:27:17 +00:00
|
|
|
config = Config._
|
|
|
|
|
|
|
|
extra_args: tuple[str] = tuple()
|
|
|
|
|
|
|
|
if expiry_days is not None:
|
|
|
|
extra_args += tuple([f"--days={expiry_days}"])
|
|
|
|
|
|
|
|
if (algo := config.crypto.cert_algo) is not None:
|
|
|
|
if algo is CertificateAlgo.rsa2048:
|
|
|
|
extra_args += ("--use-algo=rsa", "--keysize=2048")
|
|
|
|
|
|
|
|
elif algo is CertificateAlgo.rsa4096:
|
|
|
|
extra_args += ("--use-algo=rsa", "--keysize=4096")
|
|
|
|
|
|
|
|
elif algo is CertificateAlgo.secp256r1:
|
|
|
|
extra_args += ("--use-algo=ec", "--curve=secp256r1")
|
|
|
|
|
|
|
|
elif algo is CertificateAlgo.secp384r1:
|
|
|
|
extra_args += ("--use-algo=ec", "--curve=secp384r1")
|
|
|
|
|
|
|
|
elif algo is CertificateAlgo.ed25519:
|
|
|
|
extra_args += ("--use-algo=ed", "--curve=ed25519")
|
|
|
|
|
|
|
|
else:
|
|
|
|
raise ValueError(f"Unexpected algorithm: {algo}")
|
|
|
|
|
|
|
|
self.__easyrsa(
|
|
|
|
*extra_args,
|
|
|
|
*easyrsa_args
|
|
|
|
)
|
2022-03-22 00:34:06 +00:00
|
|
|
|
|
|
|
with open(
|
2022-03-30 21:18:54 +00:00
|
|
|
self.pki_directory.joinpath(cert_filename), "r"
|
2022-03-22 00:34:06 +00:00
|
|
|
) as cert_file:
|
|
|
|
return crypto.load_certificate(
|
|
|
|
crypto.FILETYPE_PEM, cert_file.read()
|
|
|
|
)
|
|
|
|
|
|
|
|
def init_pki(self) -> bool:
|
|
|
|
self.__easyrsa("init-pki")
|
|
|
|
|
2022-03-30 22:27:17 +00:00
|
|
|
def build_ca(self) -> crypto.X509:
|
2022-03-30 21:18:54 +00:00
|
|
|
config = Config._
|
2022-03-30 22:27:17 +00:00
|
|
|
server_dn = config.server_dn
|
2022-03-30 21:18:54 +00:00
|
|
|
|
2022-03-24 23:27:35 +00:00
|
|
|
cert = self.__build_cert(
|
2022-03-22 00:34:06 +00:00
|
|
|
Path("ca.crt"),
|
2022-03-30 22:27:17 +00:00
|
|
|
config.crypto.ca_expiry_days,
|
2022-03-22 00:34:06 +00:00
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
f"--passout=pass:{self.ca_password}",
|
|
|
|
f"--passin=pass:{self.ca_password}",
|
2022-03-22 00:34:06 +00:00
|
|
|
|
2022-03-30 22:27:17 +00:00
|
|
|
"--dn-mode=org",
|
|
|
|
f"--req-c={server_dn.country.value}",
|
|
|
|
f"--req-st={server_dn.state.value}",
|
|
|
|
f"--req-city={server_dn.city.value}",
|
|
|
|
f"--req-org={server_dn.organization.value}",
|
|
|
|
f"--req-ou={server_dn.organizational_unit.value}",
|
|
|
|
f"--req-email={server_dn.email.value}",
|
|
|
|
f"--req-cn={server_dn.common_name}",
|
2022-03-22 00:57:09 +00:00
|
|
|
|
2022-03-22 00:34:06 +00:00
|
|
|
"build-ca",
|
|
|
|
)
|
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
# self.__easyrsa("gen-dh")
|
2022-03-24 23:27:35 +00:00
|
|
|
return cert
|
|
|
|
|
2022-03-22 00:34:06 +00:00
|
|
|
def issue(
|
|
|
|
self,
|
2022-03-30 22:27:17 +00:00
|
|
|
cert_type: str = "client",
|
2022-03-22 00:57:09 +00:00
|
|
|
cn: str = "kiwi-vpn-client",
|
2022-03-22 00:34:06 +00:00
|
|
|
) -> crypto.X509:
|
2022-03-30 21:18:54 +00:00
|
|
|
config = Config._
|
|
|
|
|
2022-03-22 00:34:06 +00:00
|
|
|
return self.__build_cert(
|
|
|
|
Path(f"issued/{cn}.crt"),
|
2022-03-30 22:27:17 +00:00
|
|
|
config.crypto.cert_expiry_days,
|
2022-03-22 00:34:06 +00:00
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
f"--passin=pass:{self.ca_password}",
|
2022-03-22 00:34:06 +00:00
|
|
|
|
|
|
|
f"build-{cert_type}-full",
|
|
|
|
cn,
|
|
|
|
"nopass",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2022-03-30 21:18:54 +00:00
|
|
|
easy_rsa = EasyRSA()
|
2022-03-22 00:57:09 +00:00
|
|
|
easy_rsa.init_pki()
|
2022-03-22 00:34:06 +00:00
|
|
|
|
2022-03-30 21:18:54 +00:00
|
|
|
ca = easy_rsa.build_ca()
|
2022-03-22 00:57:09 +00:00
|
|
|
server = easy_rsa.issue(cert_type="server", cn="kiwi-vpn-server")
|
|
|
|
client = easy_rsa.issue(cert_type="client", cn="kiwi-vpn-client")
|
2022-03-22 00:34:06 +00:00
|
|
|
|
|
|
|
date_format, encoding = "%Y%m%d%H%M%SZ", "ascii"
|
2022-03-22 00:57:09 +00:00
|
|
|
|
|
|
|
for cert in [ca, server, client]:
|
|
|
|
print(cert.get_subject().CN)
|
|
|
|
print(cert.get_signature_algorithm().decode(encoding))
|
|
|
|
print(datetime.strptime(
|
|
|
|
cert.get_notAfter().decode(encoding), date_format))
|