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
@@ -0,0 +1,287 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "api_token" (
"id" UUID NOT NULL PRIMARY KEY,
"user_sub" VARCHAR(255) NOT NULL,
"name" VARCHAR(100) NOT NULL,
"token_prefix" VARCHAR(16) NOT NULL,
"token_hash" VARCHAR(64) NOT NULL UNIQUE,
"created_at" TIMESTAMPTZ NOT NULL,
"last_used_at" TIMESTAMPTZ,
"expires_at" DATE
);
CREATE INDEX IF NOT EXISTS "idx_api_token_user_su_7c5de7" ON "api_token" ("user_sub");
COMMENT ON TABLE "api_token" IS 'Personal access token for programmatic API access.';
CREATE TABLE IF NOT EXISTS "app_user" (
"id" UUID NOT NULL PRIMARY KEY,
"sub" VARCHAR(255) NOT NULL UNIQUE,
"display_name" VARCHAR(200) NOT NULL,
"name" VARCHAR(255),
"family_name" VARCHAR(255),
"given_name" VARCHAR(255),
"middle_name" VARCHAR(255),
"nickname" VARCHAR(255),
"preferred_username" VARCHAR(255),
"profile" VARCHAR(2048),
"picture" VARCHAR(2048),
"website" VARCHAR(2048),
"gender" VARCHAR(100),
"birthdate" VARCHAR(32),
"zoneinfo" VARCHAR(100),
"locale" VARCHAR(100),
"updated_at" BIGINT,
"created_at" TIMESTAMPTZ NOT NULL,
"last_seen_at" TIMESTAMPTZ NOT NULL
);
COMMENT ON TABLE "app_user" IS 'One OIDC identity that has logged in at least once.';
CREATE TABLE IF NOT EXISTS "vault" (
"id" UUID NOT NULL PRIMARY KEY,
"owner_sub" VARCHAR(255) NOT NULL,
"name" VARCHAR(200) NOT NULL,
"description" TEXT NOT NULL,
"settings" JSONB NOT NULL,
"version" INT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS "idx_vault_owner_s_cdc1f8" ON "vault" ("owner_sub");
COMMENT ON TABLE "vault" IS 'One private XCA database owned by an OIDC user.';
CREATE TABLE IF NOT EXISTS "key" (
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(200) NOT NULL,
"comment" TEXT NOT NULL,
"source" VARCHAR(20) NOT NULL,
"key_type" VARCHAR(20) NOT NULL,
"key_size" INT,
"curve" VARCHAR(100),
"public_der" BYTEA NOT NULL,
"public_key_sha256" VARCHAR(64) NOT NULL,
"private_der" BYTEA,
"private_nonce" BYTEA,
"has_private" BOOL NOT NULL,
"version" INT NOT NULL,
"search_text" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
"vault_id" UUID NOT NULL REFERENCES "vault" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_key_vault_i_e0ffb5" UNIQUE ("vault_id", "public_key_sha256")
);
CREATE INDEX IF NOT EXISTS "idx_key_public__eba47e" ON "key" ("public_key_sha256");
CREATE INDEX IF NOT EXISTS "idx_key_vault_i_a6f27f" ON "key" ("vault_id", "name");
CREATE INDEX IF NOT EXISTS "idx_key_vault_i_c2e997" ON "key" ("vault_id", "key_type");
COMMENT ON TABLE "key" IS 'A public or private key item.';
CREATE TABLE IF NOT EXISTS "request" (
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(200) NOT NULL,
"comment" TEXT NOT NULL,
"source" VARCHAR(20) NOT NULL,
"subject_der" BYTEA NOT NULL,
"req_der" BYTEA NOT NULL,
"public_key_der" BYTEA NOT NULL,
"request_sha256" VARCHAR(64) NOT NULL,
"signed" BOOL NOT NULL,
"signed_at" TIMESTAMPTZ,
"attributes" JSONB NOT NULL,
"version" INT NOT NULL,
"search_text" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
"key_id" UUID REFERENCES "key" ("id") ON DELETE SET NULL,
"vault_id" UUID NOT NULL REFERENCES "vault" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_request_vault_i_b661f4" UNIQUE ("vault_id", "request_sha256")
);
CREATE INDEX IF NOT EXISTS "idx_request_request_fad061" ON "request" ("request_sha256");
CREATE INDEX IF NOT EXISTS "idx_request_vault_i_346e22" ON "request" ("vault_id", "name");
CREATE INDEX IF NOT EXISTS "idx_request_vault_i_2cd9ef" ON "request" ("vault_id", "signed");
COMMENT ON TABLE "request" IS 'A PKCS#10 certificate signing request item.';
CREATE TABLE IF NOT EXISTS "certificate" (
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(200) NOT NULL,
"comment" TEXT NOT NULL,
"source" VARCHAR(20) NOT NULL,
"serial_hex" VARCHAR(128) NOT NULL,
"subject_der" BYTEA NOT NULL,
"issuer_der" BYTEA NOT NULL,
"subject_hash" VARCHAR(64) NOT NULL,
"issuer_hash" VARCHAR(64) NOT NULL,
"not_before" TIMESTAMPTZ NOT NULL,
"not_after" TIMESTAMPTZ NOT NULL,
"is_ca" BOOL NOT NULL,
"path_length" INT,
"cert_der" BYTEA NOT NULL,
"public_key_der" BYTEA NOT NULL,
"signature_algorithm" VARCHAR(100) NOT NULL,
"fingerprint_md5" VARCHAR(32) NOT NULL,
"fingerprint_sha1" VARCHAR(40) NOT NULL,
"fingerprint_sha256" VARCHAR(64) NOT NULL,
"version" INT NOT NULL,
"search_text" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
"issuer_id" UUID REFERENCES "certificate" ("id") ON DELETE SET NULL,
"key_id" UUID REFERENCES "key" ("id") ON DELETE SET NULL,
"request_id" UUID REFERENCES "request" ("id") ON DELETE SET NULL,
"vault_id" UUID NOT NULL REFERENCES "vault" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_certificate_vault_i_842384" UNIQUE ("vault_id", "fingerprint_sha256")
);
CREATE INDEX IF NOT EXISTS "idx_certificate_serial__787fd1" ON "certificate" ("serial_hex");
CREATE INDEX IF NOT EXISTS "idx_certificate_subject_ed411b" ON "certificate" ("subject_hash");
CREATE INDEX IF NOT EXISTS "idx_certificate_issuer__b8daa0" ON "certificate" ("issuer_hash");
CREATE INDEX IF NOT EXISTS "idx_certificate_is_ca_906cf3" ON "certificate" ("is_ca");
CREATE INDEX IF NOT EXISTS "idx_certificate_fingerp_12be2c" ON "certificate" ("fingerprint_sha256");
CREATE INDEX IF NOT EXISTS "idx_certificate_vault_i_18b9a0" ON "certificate" ("vault_id", "name");
CREATE INDEX IF NOT EXISTS "idx_certificate_vault_i_dfb18f" ON "certificate" ("vault_id", "issuer_id");
CREATE INDEX IF NOT EXISTS "idx_certificate_vault_i_eded8f" ON "certificate" ("vault_id", "not_after");
COMMENT ON TABLE "certificate" IS 'An X.509 certificate item.';
CREATE TABLE IF NOT EXISTS "certificate_revocation" (
"id" UUID NOT NULL PRIMARY KEY,
"serial_hex" VARCHAR(128) NOT NULL,
"revoked_at" TIMESTAMPTZ NOT NULL,
"invalidity_date" TIMESTAMPTZ,
"reason" VARCHAR(50) NOT NULL,
"crl_number" INT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"ca_id" UUID NOT NULL REFERENCES "certificate" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_certificate_ca_id_1c9aa5" UNIQUE ("ca_id", "serial_hex")
);
CREATE INDEX IF NOT EXISTS "idx_certificate_serial__8fa20f" ON "certificate_revocation" ("serial_hex");
COMMENT ON TABLE "certificate_revocation" IS 'A serial revoked by a CA (XCA stores revocations on the CA).';
CREATE TABLE IF NOT EXISTS "crl" (
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(200) NOT NULL,
"comment" TEXT NOT NULL,
"source" VARCHAR(20) NOT NULL,
"crl_der" BYTEA NOT NULL,
"crl_hash" VARCHAR(64) NOT NULL,
"crl_number" INT,
"last_update" TIMESTAMPTZ NOT NULL,
"next_update" TIMESTAMPTZ,
"revoked_count" INT NOT NULL,
"signature_algorithm" VARCHAR(100) NOT NULL,
"search_text" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
"issuer_id" UUID REFERENCES "certificate" ("id") ON DELETE SET NULL,
"vault_id" UUID NOT NULL REFERENCES "vault" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_crl_vault_i_e5461a" UNIQUE ("vault_id", "crl_hash")
);
CREATE INDEX IF NOT EXISTS "idx_crl_crl_has_c44d61" ON "crl" ("crl_hash");
CREATE INDEX IF NOT EXISTS "idx_crl_vault_i_6af15c" ON "crl" ("vault_id", "name");
CREATE INDEX IF NOT EXISTS "idx_crl_vault_i_576344" ON "crl" ("vault_id", "issuer_id");
COMMENT ON TABLE "crl" IS 'A certificate revocation list item.';
CREATE TABLE IF NOT EXISTS "template" (
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(200) NOT NULL,
"comment" TEXT NOT NULL,
"source" VARCHAR(20) NOT NULL,
"subject" JSONB NOT NULL,
"settings" JSONB NOT NULL,
"version" INT NOT NULL,
"search_text" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
"vault_id" UUID NOT NULL REFERENCES "vault" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_template_vault_i_18d8f5" ON "template" ("vault_id", "name");
COMMENT ON TABLE "template" IS 'An XCA-style template: subject + extension/validity defaults.';
CREATE TABLE IF NOT EXISTS "authority" (
"id" UUID NOT NULL PRIMARY KEY,
"crl_expire" TIMESTAMPTZ,
"crl_number" INT NOT NULL,
"crl_days" INT NOT NULL,
"tls_auth_key" BYTEA,
"tls_auth_key_nonce" BYTEA,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
"template_id" UUID REFERENCES "template" ("id") ON DELETE SET NULL,
"certificate_id" UUID NOT NULL UNIQUE REFERENCES "certificate" ("id") ON DELETE CASCADE
);
COMMENT ON TABLE "authority" IS 'CA-specific properties, one row per CA certificate.';
CREATE TABLE IF NOT EXISTS "aerich" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
MODELS_STATE = (
"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"
)