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:
@@ -0,0 +1,210 @@
|
||||
# KCA — web PKI management
|
||||
|
||||
KCA is a browser-based reimplementation of the software-key workflows of
|
||||
[XCA](https://github.com/chris2511/xca): private keys, PKCS#10 requests,
|
||||
X.509 certificates, certificate revocation lists and reusable templates,
|
||||
organised in private **vaults** (each vault is the web equivalent of one XCA
|
||||
database).
|
||||
|
||||
- **`server/`** — Python + [kaya](https://github.com/woggioni/kaya) backend:
|
||||
OIDC login, Redis sessions, personal API tokens, Postgres (Tortoise ORM +
|
||||
aerich migrations), AES-256-GCM private-key encryption, and a complete REST
|
||||
API. The pure PKI domain logic lives in `packages/kca-core`; the deployable
|
||||
app is `packages/kca-app`.
|
||||
- **`web/`** — Rust + [Sycamore](https://sycamore.dev) compiled to WebAssembly
|
||||
by [Trunk](https://trunkrs.dev), styled with Bootstrap 5.3 (vendored under
|
||||
`web/vendor/`) and a dark PKI-console theme.
|
||||
|
||||
## Run the whole stack
|
||||
|
||||
```sh
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Postgres, Redis, a mock OIDC provider (test users `alice`, `bob`, `carol`,
|
||||
`dave`; any username works), the database migrator and the app all start
|
||||
together. The app listens on `http://127.0.0.1:8080`.
|
||||
|
||||
The browser and the app both use the OIDC issuer
|
||||
`http://mockoauth:8180/kca`, so add a host entry once:
|
||||
|
||||
```sh
|
||||
echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts
|
||||
```
|
||||
|
||||
Then open <http://localhost:8080>, click **Sign in with default**, and type
|
||||
`alice` (no password). The SPA and API are served from the same origin; the
|
||||
session cookie authenticates both.
|
||||
|
||||
For a real deployment set at least:
|
||||
|
||||
```sh
|
||||
# 32 random bytes, base64 or hex:
|
||||
KCA_MASTER_KEY="$(openssl rand -base64 32)"
|
||||
# Point these at your IdP and public callback URL:
|
||||
OIDC_0_ISSUER=https://idp.example.com
|
||||
OIDC_0_CLIENT_ID=kca
|
||||
OIDC_0_CLIENT_SECRET=...
|
||||
OIDC_0_REDIRECT_URI=https://kca.example.com/auth/default/callback
|
||||
```
|
||||
|
||||
`server/.env.example` documents every setting.
|
||||
|
||||
## What it does
|
||||
|
||||
### Vaults
|
||||
|
||||
Every user owns a private set of vaults. A vault contains keys, certificates,
|
||||
requests, CRLs and templates; non-owners get `404`. Vaults can be renamed,
|
||||
deleted and configured from **Settings** (default hash, serial length,
|
||||
explicit/mandatory DN entries, default key type/size, default validity).
|
||||
|
||||
### Keys
|
||||
|
||||
- Generate RSA (1024–8192), DSA (1024–3072), EC (P-192/P-224/P-256/P-384/P-521,
|
||||
secp256k1, brainpool) and Ed25519 keys.
|
||||
- Import PEM/DER public or private keys, PKCS#8, OpenSSH, PKCS#12 and PVK.
|
||||
- Export PEM public/private, traditional encrypted PEM, PKCS#8 (plain or
|
||||
encrypted), DER public/private, SSH2 public/private, JWK public/private and
|
||||
PVK.
|
||||
- Private keys are stored encrypted with AES-256-GCM under `KCA_MASTER_KEY`
|
||||
(AAD bound to the key row id). They are never included in normal API
|
||||
responses; only an explicit export decrypts them.
|
||||
|
||||
### Requests and certificates
|
||||
|
||||
- Create PKCS#10 requests with subject, key, extensions and attributes
|
||||
(challenge password, unstructured name); verify imported requests.
|
||||
- Issue self-signed certificates or certificates signed by a CA in the same
|
||||
vault, from scratch or from a request. Imported CSRs are marked **signed**
|
||||
once a certificate is issued from them.
|
||||
- The certificate wizard covers subject entries, validity, basic constraints,
|
||||
key usage, extended key usage, SAN/IAN, subject/authority key identifiers,
|
||||
CRL distribution points, authority information access, name constraints,
|
||||
certificate policies, policy constraints, inhibit-any-policy, OCSP no-check,
|
||||
TLS feature (OCSP stapling), Netscape legacy extensions and raw
|
||||
OID/DER extensions. OpenSSL config text is available on export.
|
||||
- Certificate details show subject/issuer, serial, validity, signatures,
|
||||
MD5/SHA-1/SHA-256 fingerprints, parsed extensions, OpenSSL validation
|
||||
errors, purposes and the issuer chain.
|
||||
- Renew with a new or kept serial, optionally revoke/replace the old
|
||||
certificate. Revoke with a reason; manage the CA's revocation list manually.
|
||||
- Transform certificates to a public key, a CSR or a template.
|
||||
- Export PEM, PEM chain, PEM+key, PEM+encrypted PKCS#8, PKCS#7, PKCS#7 chain,
|
||||
DER, PKCS#12, PKCS#12 chain, OpenVPN, OpenSSL config, certificate index,
|
||||
vCalendar, JWK/JWK chain and the CA's OpenVPN tls-auth key.
|
||||
|
||||
### CRLs
|
||||
|
||||
Generate a CRL from a CA's revocation entries with configurable hash, dates,
|
||||
reason inclusion, AKI and issuer alternative name. Imported CRLs are linked
|
||||
to their CA and their serials are merged into its revocation list. Export PEM,
|
||||
DER or vCalendar.
|
||||
|
||||
### Templates
|
||||
|
||||
Built-in **CA**, **TLS server** and **TLS client** templates mirror XCA's
|
||||
presets. User templates store subject defaults, extension defaults and a
|
||||
relative validity range. XCA's binary template format is supported for import
|
||||
and export, and templates can be applied to a certificate/request spec
|
||||
server-side.
|
||||
|
||||
### Import and migration
|
||||
|
||||
- Paste PEM or upload files: PEM bundles, DER certificates/requests/CRLs/keys,
|
||||
PKCS#7, PKCS#12, SSH keys and XCA templates.
|
||||
- Preview imports before committing; duplicates are skipped.
|
||||
- Import XCA SQLite `.xdb` databases (schema 7/8): software keys, certificates,
|
||||
requests, CRLs, revocations, authorities and templates. PKCS#11 token items
|
||||
are skipped, and private keys encrypted with individual passwords are only
|
||||
imported when the supplied database password decrypts them.
|
||||
|
||||
### Search and API tokens
|
||||
|
||||
Global search covers internal names, comments, subjects, issuers, serials,
|
||||
fingerprints and extension text. Personal API tokens (`Authorization: Bearer
|
||||
kca_…`) let scripts use the REST API; tokens act as their owner, are stored
|
||||
only as SHA-256 digests, and cannot mint/revoke tokens.
|
||||
|
||||
## REST API
|
||||
|
||||
OpenAPI JSON is at `/api/openapi.json`; Swagger UI at `/api/docs`.
|
||||
|
||||
| Area | Endpoints |
|
||||
|---|---|
|
||||
| Identity | `GET /api/health`, `GET /api/me`, `GET /api/auth/providers`, `GET/POST/DELETE /api/tokens` |
|
||||
| Vaults | `GET/POST /api/vaults`, `GET/PATCH/DELETE /api/vaults/{id}`, `GET/PATCH /api/vaults/{id}/settings`, `GET /api/vaults/{id}/summary`, `GET /api/vaults/{id}/options`, `GET /api/vaults/{id}/search` |
|
||||
| Keys | `GET/POST /api/vaults/{id}/keys`, `GET/PATCH/DELETE .../keys/{key_id}`, `POST .../keys/{key_id}/export` |
|
||||
| Requests | `GET/POST /api/vaults/{id}/requests`, `GET/PATCH/DELETE .../{id}`, `POST .../{id}/export`, `POST .../{id}/transform/open-key`, `POST .../{id}/transform/template` |
|
||||
| Certificates | `GET/POST /api/vaults/{id}/certificates`, `GET/PATCH/DELETE .../{id}`, `POST .../{id}/export`, `POST .../{id}/renew`, `POST .../{id}/revoke`, `GET/PATCH .../{id}/ca`, `GET/POST .../{id}/revocations`, `PATCH/DELETE .../revocations/{rev_id}`, `POST .../{id}/crl`, `POST .../{id}/tls-auth`, transforms |
|
||||
| CRLs | `GET /api/vaults/{id}/crls`, `GET/DELETE .../{crl_id}`, `POST .../{crl_id}/export` |
|
||||
| Templates | `GET/POST /api/vaults/{id}/templates`, `GET/PATCH/DELETE .../{template_id}`, `POST .../{template_id}/apply`, `POST .../{template_id}/export` |
|
||||
| Import | `POST /api/vaults/{id}/import` (`dry_run`, `selected`, `xca_db`) |
|
||||
|
||||
Errors are always `{"error": "..."}`. Mutating resources carry a `version`
|
||||
for optimistic concurrency (`409` on stale writes). Export endpoints return
|
||||
JSON with `content_base64` so the SPA can download or copy the bytes.
|
||||
|
||||
## Development
|
||||
|
||||
Backend (from `server/`, using [uv](https://docs.astral.sh/uv/)):
|
||||
|
||||
```sh
|
||||
uv sync --package kca-app
|
||||
uv run python -m unittest discover -s tests -t .
|
||||
uv run python -m unittest discover -s packages/kca-core/tests
|
||||
uv run mypy -p kca.core -p kca
|
||||
```
|
||||
|
||||
Tests run in-process with in-memory sqlite, the in-memory session store and a
|
||||
fake OIDC user, exactly like the spendwise reference app.
|
||||
|
||||
Frontend (from `web/`, with the backend running on `:8080`):
|
||||
|
||||
```sh
|
||||
trunk serve # SPA on http://localhost:8000, /api and /auth proxied
|
||||
cargo test
|
||||
```
|
||||
|
||||
Migrations live in `server/migrations/`. After changing `models.py`:
|
||||
|
||||
```sh
|
||||
DATABASE_URL=postgres://kca:password@localhost:5432/kca uv run aerich migrate --name describe_change
|
||||
DATABASE_URL=postgres://kca:password@localhost:5432/kca uv run aerich upgrade
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
server/
|
||||
packages/kca-core/ # pure PKI domain (keys, names, extensions, certs, CSRs,
|
||||
# CRLs, templates, imports/exports, .xdb, AES-GCM)
|
||||
packages/kca-app/ # kaya app, config, Tortoise models, routes, static
|
||||
migrations/ # aerich schema history
|
||||
tests/ # app integration suite
|
||||
dev/mockoauth/ # dev OIDC provider image + test users
|
||||
web/
|
||||
src/pages/ # dashboard, vault, keys, requests, certificates,
|
||||
# wizard, CRLs, templates, import, search, settings, tokens
|
||||
src/components/ # nav, toast, shared UI
|
||||
src/{api,auth,model,dom}.rs
|
||||
pwa/ # manifest + app-shell assets
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
- **Smart cards / PKCS#11 / HSMs are not supported.** A browser app cannot use
|
||||
a local card reader; server-side PKCS#11 would require a separate HSM
|
||||
deployment and is deliberately out of scope.
|
||||
- The desktop command-line interface is replaced by the REST API and personal
|
||||
access tokens.
|
||||
- PKCS#12 export uses PBES2/AES via `cryptography`; legacy RC2/3DES PKCS#12
|
||||
output is not available. Legacy inputs are imported when the library
|
||||
supports them.
|
||||
- XCA's OpenSSL-config "Advanced" text editor is replaced by typed extension
|
||||
fields plus raw OID/DER rows. OpenSSL config remains an export format.
|
||||
- The PWA service worker is intentionally not registered: certificate and key
|
||||
data must never be served stale from a cache. The manifest and icons are
|
||||
still available for installation.
|
||||
- Column layouts, window geometry, language packs and other desktop-only XCA
|
||||
preferences are not part of the web UI.
|
||||
Reference in New Issue
Block a user