Implement KCA: web reimplementation of XCA PKI workflows

- kaya backend with OIDC sessions, API tokens, Postgres/Tortoise models
- AES-256-GCM private key storage and full certificate/CSR/CRL domain logic
- Sycamore/WASM SPA for vaults, keys, requests, certificates, CRLs and templates
- imports/exports, XCA .xdb migration, Docker Compose stack and tests
This commit is contained in:
2026-10-23 02:06:55 +00:00
commit 4f3a632f4b
109 changed files with 21048 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
"""Core domain tests: names, extensions, keys, certificates, CRLs and exports."""
from __future__ import annotations
import unittest
from datetime import datetime, timedelta, timezone
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from kca.core import certificates, crls, crypto, exports, imports, keys, names, requests, templates
from kca.core.extensions import (
build_extensions,
default_extension_spec,
extension_specs,
)
def _name(cn: str = "Test CA") -> x509.Name:
return names.name_from_entries(
names.entries_from_json(
[
{"name": "C", "value": "DE"},
{"name": "O", "value": "KCA"},
{"name": "CN", "value": cn},
]
)
)
def _ca_spec() -> dict:
spec = default_extension_spec()
spec["basic_constraints"] = {"critical": True, "ca": True, "path_length": None}
spec["key_usage"] = {"critical": True, "usages": ["key_cert_sign", "crl_sign"]}
spec["subject_key_identifier"] = {"critical": False}
spec["authority_key_identifier"] = {"critical": False}
return spec
class NamesTest(unittest.TestCase):
def test_order_and_duplicates_survive(self) -> None:
name = _name()
entries = names.entries_from_name(name)
self.assertEqual([e.name for e in entries], ["C", "O", "CN"])
self.assertEqual(name.rfc4514_string(), "CN=Test CA,O=KCA,C=DE")
def test_der_roundtrip(self) -> None:
name = _name()
self.assertEqual(names.name_from_der(names.name_to_der(name)), name)
class ExtensionTest(unittest.TestCase):
def test_roundtrip(self) -> None:
key = keys.generate_key("rsa", 2048)
spec = _ca_spec()
spec["subject_alt_name"] = {
"critical": False,
"entries": [
{"type": "dns", "value": "example.com"},
{"type": "ip", "value": "192.0.2.1"},
{"type": "email", "value": "ca@example.com"},
],
}
exts = build_extensions(spec, public_key=key.public_key(), subject_name=_name())
parsed = extension_specs(exts)
self.assertTrue(parsed["basic_constraints"]["ca"])
self.assertEqual(parsed["basic_constraints"]["path_length"], None)
self.assertEqual(len(parsed["subject_alt_name"]["entries"]), 3)
self.assertIn("key_cert_sign", parsed["key_usage"]["usages"])
def test_raw_extension(self) -> None:
key = keys.generate_key("ec", curve="secp256r1")
spec = default_extension_spec()
spec["raw"] = [{"oid": "1.2.3.4", "critical": True, "der_hex": "0c03616263"}]
exts = build_extensions(spec, public_key=key.public_key(), subject_name=_name())
self.assertEqual(exts[0].oid.dotted_string, "1.2.3.4")
self.assertTrue(exts[0].critical)
class KeyTest(unittest.TestCase):
def test_all_types(self) -> None:
for key_type, kwargs in [
("rsa", {"size": 2048}),
("ec", {"curve": "secp256r1"}),
("ed25519", {}),
]:
key = keys.generate_key(key_type, **kwargs)
info = keys.key_info(key)
self.assertTrue(info.is_private)
self.assertEqual(info.key_type.upper() if key_type != "ed25519" else "ED25519", info.key_type)
loaded, private = keys.load_key_bytes(keys.private_pem(key))
self.assertTrue(private)
self.assertEqual(keys.public_der(loaded), info.public_der)
def test_encrypted_export_roundtrip(self) -> None:
key = keys.generate_key("rsa", 2048)
encrypted = keys.private_pem(key, password="secret")
loaded, private = keys.load_key_bytes(encrypted, password="secret")
self.assertTrue(private)
self.assertEqual(keys.public_der(loaded), keys.public_der(key))
def test_ssh_and_pvk(self) -> None:
key = keys.generate_key("rsa", 2048)
public = keys.ssh_public(key, "test")
loaded, private = keys.load_key_bytes(public.encode())
self.assertFalse(private)
pvk = keys.pvk_encode_private(key)
loaded, private = keys.load_key_bytes(pvk)
self.assertTrue(private)
self.assertEqual(keys.public_der(loaded), keys.public_der(key))
class CertificateTest(unittest.TestCase):
def test_issue_and_verify(self) -> None:
ca_key = keys.generate_key("rsa", 2048)
now = datetime.now(timezone.utc)
ca = certificates.build_self_signed(
subject=_name("Root CA"),
private_key=ca_key,
serial=1,
not_before=now,
not_after=now + timedelta(days=3650),
extension_spec=_ca_spec(),
)
self.assertTrue(certificates.is_self_signed(ca))
self.assertTrue(certificates.certificate_info(ca).is_ca)
leaf_key = keys.generate_key("ec", curve="secp256r1")
spec = default_extension_spec()
spec["basic_constraints"] = {"critical": True, "ca": False, "path_length": None}
spec["key_usage"] = {"critical": True, "usages": ["digital_signature", "key_encipherment"]}
spec["extended_key_usage"] = {"critical": False, "usages": ["serverAuth"]}
spec["subject_alt_name"] = {"critical": False, "entries": [{"type": "dns", "value": "example.com"}]}
leaf = certificates.build_signed(
subject=_name("example.com"),
public_key=leaf_key.public_key(),
issuer_cert=ca,
issuer_private_key=ca_key,
serial=2,
not_before=now,
not_after=now + timedelta(days=365),
extension_spec=spec,
)
self.assertTrue(certificates.verify_directly_issued_by(leaf, ca))
self.assertFalse(certificates.verify_directly_issued_by(leaf, leaf))
self.assertIn("SSL server", certificates.purposes(leaf))
parsed = certificates.parse_certificate(certificates.certificate_der(leaf))
self.assertEqual(parsed.serial_number, 2)
self.assertEqual(certificates.find_issuer(parsed, [ca]), ca)
self.assertEqual(certificates.validation_errors(parsed, [parsed, ca]), [])
def test_renew_spec(self) -> None:
key = keys.generate_key("rsa", 2048)
now = datetime.now(timezone.utc)
cert = certificates.build_self_signed(
subject=_name("Renew Me"),
private_key=key,
serial=3,
not_before=now,
not_after=now + timedelta(days=30),
extension_spec=_ca_spec(),
)
spec = certificates.certificate_spec(cert)
self.assertEqual(spec["serial_hex"], "3")
self.assertEqual(spec["subject"][-1]["name"], "CN")
def test_index_and_config(self) -> None:
key = keys.generate_key("rsa", 2048)
now = datetime.now(timezone.utc)
cert = certificates.build_self_signed(
subject=_name("Index"),
private_key=key,
serial=4,
not_before=now,
not_after=now + timedelta(days=30),
extension_spec=_ca_spec(),
)
self.assertIn("V\t", certificates.index_entry(cert))
self.assertIn("basicConstraints", certificates.openssl_config(cert))
self.assertIn("VCALENDAR", certificates.ics_calendar([certificates.ics_event(cert, "Index")]))
class RequestTest(unittest.TestCase):
def test_csr_roundtrip(self) -> None:
key = keys.generate_key("rsa", 2048)
csr = requests.build_csr(
subject=_name("CSR"),
private_key=key,
extension_spec=_ca_spec(),
attributes={requests.CHALLENGE_PASSWORD_OID: "secret"},
)
self.assertTrue(requests.verify_csr(csr))
loaded = requests.parse_csr(requests.csr_der(csr))
self.assertEqual(requests.request_info(loaded).subject, csr.subject)
self.assertEqual(requests.attribute_values(loaded)[requests.CHALLENGE_PASSWORD_OID], "secret")
class CrlTest(unittest.TestCase):
def test_build_and_parse(self) -> None:
key = keys.generate_key("rsa", 2048)
now = datetime.now(timezone.utc)
ca = certificates.build_self_signed(
subject=_name("CRL CA"),
private_key=key,
serial=5,
not_before=now,
not_after=now + timedelta(days=3650),
extension_spec=_ca_spec(),
)
crl = crls.build_crl(
issuer_cert=ca,
issuer_private_key=key,
revocations=[
{
"serial_hex": "10",
"revoked_at": now,
"reason": "keyCompromise",
"invalidity_date": now - timedelta(days=1),
}
],
last_update=now,
next_update=now + timedelta(days=30),
crl_number=7,
)
info = crls.crl_info(crl)
self.assertEqual(info.crl_number, 7)
self.assertEqual(info.revoked_count, 1)
self.assertTrue(crls.verify_crl(crl, ca))
parsed = crls.parse_crl(crls.crl_der(crl))
rows = crls.crl_revocations(parsed)
self.assertEqual(rows[0]["serial_hex"], "10")
self.assertEqual(rows[0]["reason"], "keyCompromise")
class TemplateTest(unittest.TestCase):
def test_xca_roundtrip(self) -> None:
template = templates.builtin_templates()[0].as_dict()
data = templates.template_to_xca_data(template)
parsed = templates.template_from_xca_data(data)
self.assertEqual(parsed["name"], "CA")
self.assertTrue(parsed["settings"]["extensions"]["basic_constraints"]["ca"])
def test_apply(self) -> None:
template = templates.builtin_templates()[1].as_dict()
merged = templates.apply_template(template, {"subject": [], "extensions": {}})
self.assertIn("serverAuth", merged["extensions"]["extended_key_usage"]["usages"])
class ExportImportTest(unittest.TestCase):
def setUp(self) -> None:
self.key = keys.generate_key("rsa", 2048)
now = datetime.now(timezone.utc)
self.cert = certificates.build_self_signed(
subject=_name("Export"),
private_key=self.key,
serial=6,
not_before=now,
not_after=now + timedelta(days=30),
extension_spec=_ca_spec(),
)
def test_certificate_exports(self) -> None:
for fmt, prefix in [
("pem", b"-----BEGIN CERTIFICATE"),
("der", b"0"),
("pkcs7", b"0"),
("pkcs12", b"0"),
]:
result = exports.export_certificate(
self.cert, fmt, name="export", private_key=self.key, password="secret"
)
self.assertTrue(result.data.startswith(prefix), fmt)
pem = exports.export_certificate(self.cert, "pem", name="export")
items = imports.parse_file(pem.data, "export.crt")
self.assertEqual(items[0].kind, "certificate")
def test_key_exports(self) -> None:
for fmt in ("pem-public", "pem-private", "pkcs8", "ssh-public", "jwk-public", "pvk"):
result = exports.export_key(self.key, fmt, name="export")
self.assertTrue(result.data)
encrypted = exports.export_key(self.key, "pkcs8-encrypted", name="export", password="secret")
loaded, private = keys.load_key_bytes(encrypted.data, password="secret")
self.assertTrue(private)
def test_pkcs12_contains_chain(self) -> None:
result = exports.export_certificate(
self.cert, "pkcs12-chain", name="export", chain=[self.cert], private_key=self.key, password="secret"
)
items = imports.parse_file(result.data, "export.pfx", "secret")
self.assertGreaterEqual(len(items), 2)
class CryptoTest(unittest.TestCase):
def test_envelope(self) -> None:
master = crypto.generate_master_key()
blob = crypto.encrypt_private_key(b"secret", "key-id", crypto.decode_master_key(master))
self.assertEqual(crypto.decrypt_private_key(blob, "key-id", crypto.decode_master_key(master)), b"secret")
with self.assertRaises(crypto.DecryptionError):
crypto.decrypt_private_key(blob, "other-id", crypto.decode_master_key(master))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,135 @@
"""XCA .xdb import tests using a synthetic schema-compatible database."""
from __future__ import annotations
import base64
import sqlite3
import struct
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives import serialization
from kca.core import certificates, crls, keys, names, requests, templates
from kca.core.extensions import build_extensions, default_extension_spec
from kca.core.xca_db import import_xca_database
def _name(cn: str) -> object:
return names.name_from_entries(names.entries_from_json([{"name": "CN", "value": cn}]))
class XcaDbTest(unittest.TestCase):
def test_import_synthetic_database(self) -> None:
key = keys.generate_key("rsa", 2048)
now = datetime.now(timezone.utc)
spec = default_extension_spec()
spec["basic_constraints"] = {"critical": True, "ca": True, "path_length": None}
exts = build_extensions(spec, public_key=key.public_key(), subject_name=_name("Synthetic CA"))
cert = certificates.build_certificate(
subject=_name("Synthetic CA"),
public_key=key.public_key(),
issuer_cert=None,
issuer_private_key=key,
serial=42,
not_before=now,
not_after=now + timedelta(days=365),
extensions=exts,
)
csr = requests.build_csr(subject=_name("Synthetic CSR"), private_key=key, extension_spec=spec)
crl = crls.build_crl(
issuer_cert=cert,
issuer_private_key=key,
revocations=[{"serial_hex": "2a", "revoked_at": now, "reason": "keyCompromise"}],
last_update=now,
next_update=now + timedelta(days=30),
crl_number=3,
)
tmpl = templates.builtin_templates()[0].as_dict()
tmpl_pem = templates.template_to_xca_data(tmpl)
body = tmpl_pem.decode().split("-----BEGIN XCA TEMPLATE-----")[1].split("-----END XCA TEMPLATE-----")[0]
payload = base64.b64decode("".join(body.split()))
tmpl_data = payload[8:]
date = now.strftime("%Y%m%d%H%M%SZ")
with tempfile.NamedTemporaryFile(suffix=".xdb", delete=False) as handle:
path = handle.name
try:
db = sqlite3.connect(path)
db.executescript(
"""
CREATE TABLE settings (key_ CHAR(20) UNIQUE, value TEXT);
CREATE TABLE items (id INTEGER PRIMARY KEY, name VARCHAR(128), type INTEGER, source INTEGER, date CHAR(15), comment VARCHAR(2048), stamp INTEGER DEFAULT 0, del SMALLINT DEFAULT 0);
CREATE TABLE public_keys (item INTEGER, type CHAR(4), hash INTEGER, len INTEGER, "public" TEXT);
CREATE TABLE private_keys (item INTEGER, ownPass INTEGER, private TEXT);
CREATE TABLE x509super (item INTEGER, subj_hash INTEGER, pkey INTEGER, key_hash INTEGER);
CREATE TABLE requests (item INTEGER, hash INTEGER, signed INTEGER, request TEXT);
CREATE TABLE certs (item INTEGER, hash INTEGER, iss_hash INTEGER, serial VARCHAR(64), issuer INTEGER, ca INTEGER, cert TEXT);
CREATE TABLE authority (item INTEGER, template INTEGER, crlExpire CHAR(15), crlNo INTEGER, crlDays INTEGER);
CREATE TABLE crls (item INTEGER, hash INTEGER, num INTEGER, iss_hash INTEGER, issuer INTEGER, crl TEXT);
CREATE TABLE revocations (caId INTEGER, serial VARCHAR(64), date CHAR(15), invaldate CHAR(15), crlNo INTEGER, reasonBit INTEGER);
CREATE TABLE templates (item INTEGER, version INTEGER, template TEXT);
CREATE TABLE takeys (item INTEGER UNIQUE, value TEXT);
"""
)
db.execute("INSERT INTO settings VALUES ('schema', '8')")
db.execute("INSERT INTO items VALUES (1, 'Synthetic key', 1, 2, ?, '', 0, 0)", (date,))
db.execute("INSERT INTO items VALUES (2, 'Synthetic CA', 3, 2, ?, '', 0, 0)", (date,))
db.execute("INSERT INTO items VALUES (3, 'Synthetic CSR', 2, 2, ?, '', 0, 0)", (date,))
db.execute("INSERT INTO items VALUES (4, 'Synthetic CRL', 4, 2, ?, '', 0, 0)", (date,))
db.execute("INSERT INTO items VALUES (5, 'Synthetic template', 5, 2, ?, '', 0, 0)", (date,))
encrypted = key.private_bytes(
serialization.Encoding.DER,
serialization.PrivateFormat.PKCS8,
serialization.BestAvailableEncryption(b"dbpass"),
)
db.execute(
"INSERT INTO public_keys VALUES (1, 'RSA', 0, 2048, ?)",
(base64.b64encode(keys.public_der(key)).decode(),),
)
db.execute(
"INSERT INTO private_keys VALUES (1, 0, ?)",
(base64.b64encode(encrypted).decode(),),
)
db.execute("INSERT INTO x509super VALUES (2, 0, 1, 0)")
db.execute("INSERT INTO x509super VALUES (3, 0, 1, 0)")
db.execute(
"INSERT INTO certs VALUES (2, 0, 0, '2a', NULL, 1, ?)",
(base64.b64encode(certificates.certificate_der(cert)).decode(),),
)
db.execute("INSERT INTO authority VALUES (2, NULL, NULL, 0, 30)")
db.execute(
"INSERT INTO revocations VALUES (2, '2a', ?, NULL, 0, 1)",
(date,),
)
db.execute(
"INSERT INTO requests VALUES (3, 0, 0, ?)",
(base64.b64encode(requests.csr_der(csr)).decode(),),
)
db.execute(
"INSERT INTO crls VALUES (4, 0, 1, 0, 2, ?)",
(base64.b64encode(crls.crl_der(crl)).decode(),),
)
db.execute(
"INSERT INTO templates VALUES (5, 10, ?)",
(base64.b64encode(tmpl_data).decode(),),
)
db.commit()
db.close()
with open(path, "rb") as handle:
items = import_xca_database(handle.read(), "synthetic.xdb", "dbpass")
finally:
import os
os.unlink(path)
kinds = [item.kind for item in items]
self.assertEqual(kinds, ["key", "certificate", "request", "crl", "template"])
self.assertTrue(items[0].is_private)
self.assertEqual(items[1].name, "Synthetic CA")
self.assertEqual(items[3].name, "Synthetic CRL")
self.assertEqual(items[4].obj["name"], "Synthetic template")
if __name__ == "__main__":
unittest.main()