diff --git a/README.md b/README.md index 8c04801..5653c61 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,8 @@ Rust/Axum responder on port `2560`. - KCA publishes `ocsp:` whenever certificates are issued, renewed, revoked, deleted or imported. Revocation reasons are included; this is a full - status snapshot, not a revoked-only list. OCSP keys are separate from session - keys through their prefix. + CBOR-encoded status snapshot, not a revoked-only list. OCSP keys are separate + from session keys through their prefix. - The responder has no Postgres or KCA master-key access. It reads only OCSP snapshots from Redis and uses a delegated OCSP signing certificate/key per CA. Where Redis ACLs are available, restrict the responder to `ocsp:*` so it diff --git a/ocsp-responder/Cargo.lock b/ocsp-responder/Cargo.lock index ae0a3e0..3dba814 100644 --- a/ocsp-responder/Cargo.lock +++ b/ocsp-responder/Cargo.lock @@ -175,6 +175,15 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cbor2" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf90b4df9f396c3b5dad07dcdaf0627f2b8ae533ea9be9937f9d6b8713cb888" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.9.0" @@ -752,6 +761,7 @@ dependencies = [ "axum", "base64", "bytes", + "cbor2", "chrono", "const-oid", "der", @@ -763,7 +773,6 @@ dependencies = [ "rsa", "sec1", "serde", - "serde_json", "serde_yaml", "sha1", "sha2", diff --git a/ocsp-responder/Cargo.toml b/ocsp-responder/Cargo.toml index a8eb0cc..bbfe401 100644 --- a/ocsp-responder/Cargo.toml +++ b/ocsp-responder/Cargo.toml @@ -11,7 +11,7 @@ tower-http = { version = "0.6", features = ["limit", "trace", "timeout"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } serde = { version = "1", features = ["derive"] } -serde_json = "1" +cbor2 = "1" serde_yaml = "0.9" chrono = { version = "0.4", features = ["serde"] } thiserror = "2" diff --git a/ocsp-responder/README.md b/ocsp-responder/README.md index 722093b..49f1335 100644 --- a/ocsp-responder/README.md +++ b/ocsp-responder/README.md @@ -1,6 +1,6 @@ # KCA OCSP responder -Standalone Rust/Axum responder. It reads one full JSON status snapshot per CA +Standalone Rust/Axum responder. It reads one full CBOR status snapshot per CA from Redis, caches it for one second with a single-flight refresh, and signs OCSP responses with a delegated responder certificate. @@ -31,10 +31,11 @@ certificate exists in multiple vaults. The responder validates both this ID and the CA certificate fingerprint against every downloaded snapshot. KCA publishes these keys when `REDIS_URL` is configured. The -`kca-ocsp-status/v2` JSON contains all certificates issued by that CA, both -valid and revoked, without a generation timestamp. Freshness is carried by the -Redis key TTL. The responder never infers status from a partial revoked-only -list. +`kca-ocsp-status/v2` payload is encoded with CBOR and contains all certificates +issued by that CA, both valid and revoked, without a generation timestamp. +Freshness is carried by the Redis key TTL. The responder never infers status +from a partial revoked-only list. Publisher and responder upgrades must be +coordinated because legacy JSON payloads are rejected until republished. ## Run locally diff --git a/ocsp-responder/src/cache.rs b/ocsp-responder/src/cache.rs index e45447e..a4871a3 100644 --- a/ocsp-responder/src/cache.rs +++ b/ocsp-responder/src/cache.rs @@ -135,11 +135,11 @@ mod tests { use bytes::Bytes; use super::*; - use crate::snapshot::sha256_hex; + use crate::snapshot::{sha256_hex, test_body as body}; use crate::store::{ObjectStore, StoreError, StoredSnapshot}; struct FakeStore { - body: String, + body: Vec, expires_at: SystemTime, calls: AtomicUsize, fail: AtomicBool, @@ -147,7 +147,7 @@ mod tests { } impl FakeStore { - fn new(body: String, lifetime: Duration) -> Self { + fn new(body: Vec, lifetime: Duration) -> Self { Self { body, expires_at: SystemTime::now() + lifetime, @@ -157,7 +157,7 @@ mod tests { } } - fn expired(body: String) -> Self { + fn expired(body: Vec) -> Self { Self { body, expires_at: SystemTime::now() - Duration::from_secs(1), @@ -185,12 +185,6 @@ mod tests { } } - fn body(sha: &str) -> String { - format!( - r#"{{"schema":"kca-ocsp-status/v2","issuer":{{"id":"ca","sha256":"{sha}"}},"entries":[{{"serial_hex":"01","status":"valid"}}]}}"# - ) - } - #[tokio::test] async fn single_flight_refreshes_once() { let sha = sha256_hex(b"ca"); diff --git a/ocsp-responder/src/snapshot.rs b/ocsp-responder/src/snapshot.rs index 44db107..1a95ee5 100644 --- a/ocsp-responder/src/snapshot.rs +++ b/ocsp-responder/src/snapshot.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sha1::{Digest as _, Sha1}; use sha2::Sha256; use x509_cert::ext::pkix::CrlReason; @@ -28,7 +28,7 @@ pub struct Snapshot { pub entries: HashMap, StatusEntry>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] struct RawSnapshot { schema: String, issuer: RawIssuer, @@ -36,7 +36,7 @@ struct RawSnapshot { entries: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] struct RawIssuer { #[serde(default)] id: String, @@ -44,7 +44,7 @@ struct RawIssuer { sha256: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] struct RawEntry { serial_hex: String, status: String, @@ -61,7 +61,9 @@ impl Snapshot { expected_sha256: &str, expected_ca_id: &str, ) -> Result { - let raw: RawSnapshot = serde_json::from_slice(bytes)?; + cbor2::validate_slice(bytes).map_err(|error| SnapshotError::Cbor(error.to_string()))?; + let raw: RawSnapshot = + cbor2::from_slice(bytes).map_err(|error| SnapshotError::Cbor(error.to_string()))?; if raw.schema != SCHEMA { return Err(SnapshotError::Schema(raw.schema)); } @@ -166,8 +168,8 @@ pub fn hash_sha256(bytes: &[u8]) -> Vec { #[derive(Debug, thiserror::Error)] pub enum SnapshotError { - #[error("invalid JSON: {0}")] - Json(#[from] serde_json::Error), + #[error("invalid CBOR: {0}")] + Cbor(String), #[error("unsupported snapshot schema {0:?}")] Schema(String), #[error("snapshot issuer ID {actual} does not match configured CA ID {expected}")] @@ -184,27 +186,64 @@ pub enum SnapshotError { DuplicateSerial(String), } +#[cfg(test)] +pub(crate) fn test_body(sha: &str) -> Vec { + let raw = RawSnapshot { + schema: SCHEMA.to_string(), + issuer: RawIssuer { + id: "ca".to_string(), + sha256: sha.to_string(), + }, + entries: vec![RawEntry { + serial_hex: "01".to_string(), + status: "valid".to_string(), + revoked_at: None, + reason: None, + }], + }; + cbor2::to_vec(&raw).unwrap() +} + #[cfg(test)] mod tests { use super::*; - fn body(sha: &str) -> String { - format!( - r#"{{ - "schema": "kca-ocsp-status/v2", - "issuer": {{"id": "ca", "sha256": "{sha}", "subject": "CN=CA"}}, - "entries": [ - {{"serial_hex": "01ab", "status": "valid", "not_after": "2027-01-01T00:00:00Z"}}, - {{"serial_hex": "00ff", "status": "revoked", "revoked_at": "2026-02-03T04:05:06Z", "reason": "keyCompromise"}} - ] - }}"# - ) + fn raw_body(sha: &str, schema: &str) -> RawSnapshot { + RawSnapshot { + schema: schema.to_string(), + issuer: RawIssuer { + id: "ca".to_string(), + sha256: sha.to_string(), + }, + entries: vec![ + RawEntry { + serial_hex: "01ab".to_string(), + status: "valid".to_string(), + revoked_at: None, + reason: None, + }, + RawEntry { + serial_hex: "00ff".to_string(), + status: "revoked".to_string(), + revoked_at: Some( + DateTime::parse_from_rfc3339("2026-02-03T04:05:06Z") + .unwrap() + .with_timezone(&Utc), + ), + reason: Some("keyCompromise".to_string()), + }, + ], + } + } + + fn body(sha: &str) -> Vec { + cbor2::to_vec(&raw_body(sha, SCHEMA)).unwrap() } #[test] fn parses_statuses_and_reasons() { let sha = sha256_hex(b"ca"); - let snapshot = Snapshot::parse(body(&sha).as_bytes(), &sha, "ca").unwrap(); + let snapshot = Snapshot::parse(&body(&sha), &sha, "ca").unwrap(); let good = snapshot.status_for_serial(&[0x01, 0xab]).unwrap(); assert_eq!(good.status, Status::Good); let revoked = snapshot.status_for_serial(&[0xff]).unwrap(); @@ -216,15 +255,28 @@ mod tests { #[test] fn rejects_wrong_issuer_schema_and_duplicates() { let sha = sha256_hex(b"ca"); - assert!(Snapshot::parse(body(&sha).as_bytes(), "wrong", "ca").is_err()); + assert!(Snapshot::parse(&body(&sha), "wrong", "ca").is_err()); assert!(matches!( - Snapshot::parse(body(&sha).as_bytes(), &sha, "other"), + Snapshot::parse(&body(&sha), &sha, "other"), Err(SnapshotError::IssuerIdMismatch { .. }) )); - let wrong_schema = body(&sha).replace(SCHEMA, "other"); - assert!(Snapshot::parse(wrong_schema.as_bytes(), &sha, "ca").is_err()); - let duplicate = body(&sha).replace("\"00ff\"", "\"01ab\""); - assert!(Snapshot::parse(duplicate.as_bytes(), &sha, "ca").is_err()); + let wrong_schema = cbor2::to_vec(&raw_body(&sha, "other")).unwrap(); + assert!(Snapshot::parse(&wrong_schema, &sha, "ca").is_err()); + let mut duplicate = raw_body(&sha, SCHEMA); + duplicate.entries[1].serial_hex = duplicate.entries[0].serial_hex.clone(); + let duplicate = cbor2::to_vec(&duplicate).unwrap(); + assert!(Snapshot::parse(&duplicate, &sha, "ca").is_err()); + } + + #[test] + fn rejects_legacy_json() { + let sha = sha256_hex(b"ca"); + let legacy = + br#"{"schema":"kca-ocsp-status/v2","issuer":{"id":"ca","sha256":"sha"},"entries":[]}"#; + assert!(matches!( + Snapshot::parse(legacy, &sha, "ca"), + Err(SnapshotError::Cbor(_)) + )); } #[test] diff --git a/ocsp-responder/src/store.rs b/ocsp-responder/src/store.rs index b957e2d..1ff34a5 100644 --- a/ocsp-responder/src/store.rs +++ b/ocsp-responder/src/store.rs @@ -133,13 +133,14 @@ mod tests { prefix: format!("ocsp:test:{}", std::process::id()), ..Default::default() }; - let key = config.key("round-trip"); + let key = config.key("ca"); + let sha = crate::snapshot::sha256_hex(b"ca"); let store = StatusStore::new(&config).await.unwrap(); let client = redis::Client::open(config.url.as_str()).unwrap(); let mut connection = client.get_connection_manager().await.unwrap(); let _: () = redis::cmd("SET") .arg(&key) - .arg("snapshot") + .arg(crate::snapshot::test_body(&sha)) .arg("EX") .arg(30) .query_async(&mut connection) @@ -149,7 +150,8 @@ mod tests { let stored = store.get(&key).await.unwrap(); match stored { StoredSnapshot::Present { body, expires_at } => { - assert_eq!(body, Bytes::from_static(b"snapshot")); + let snapshot = crate::snapshot::Snapshot::parse(&body, &sha, "ca").unwrap(); + assert_eq!(snapshot.entries.len(), 1); assert!(expires_at > SystemTime::now()); } StoredSnapshot::Missing => panic!("snapshot should be present"), diff --git a/server/.env.example b/server/.env.example index 3239e4b..f90cef4 100644 --- a/server/.env.example +++ b/server/.env.example @@ -66,7 +66,7 @@ OTEL_EXCLUDED_PATHS=/api/health #OTEL_METRICS_POLL_INTERVAL_SECONDS=15 # --- OCSP status publisher (optional) -------------------------------------- -# When REDIS_URL is configured, KCA publishes one full JSON status snapshot +# When REDIS_URL is configured, KCA publishes one full CBOR status snapshot # per CA to the same Redis instance and logical database used for sessions. # Keys use a separate prefix and expire unless refreshed by the heartbeat. KCA_OCSP_REDIS_PREFIX=ocsp diff --git a/server/packages/kca-app/pyproject.toml b/server/packages/kca-app/pyproject.toml index 41b8535..ee2241f 100644 --- a/server/packages/kca-app/pyproject.toml +++ b/server/packages/kca-app/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "PyYAML", "redis", "cryptography>=44", + "cbor2", "pyOpenSSL>=25", "paramiko>=3.5", ] diff --git a/server/packages/kca-app/src/kca/ocsp.py b/server/packages/kca-app/src/kca/ocsp.py index e42a35a..ca76f45 100644 --- a/server/packages/kca-app/src/kca/ocsp.py +++ b/server/packages/kca-app/src/kca/ocsp.py @@ -1,19 +1,19 @@ """Per-CA OCSP status snapshots and Redis publication. KCA is the source of truth for certificate status. This module builds a -compact JSON document per CA and stores it in the same Redis instance used for +compact CBOR document per CA and stores it in the same Redis instance used for sessions, so the standalone OCSP responder never needs database access or a CA private key. """ from __future__ import annotations import asyncio -import json import logging from datetime import datetime, timedelta, timezone from typing import Any, Awaitable, Callable, Dict, Optional from uuid import UUID +import cbor2 from kaya.core import KayaApp, KayaMixin from redis.asyncio import Redis from tortoise.expressions import Q @@ -92,7 +92,7 @@ async def publish_snapshot( """Store one CA snapshot with an expiry and return its Redis key.""" if snapshot is None: snapshot = await snapshot_for_ca(ca) - body = json.dumps(snapshot, indent=2, sort_keys=True).encode("utf-8") + body = cbor2.dumps(snapshot) key = redis_key(str(ca.id)) owned_client = None diff --git a/server/tests/test_routes_extra.py b/server/tests/test_routes_extra.py index ab7e707..ec65615 100644 --- a/server/tests/test_routes_extra.py +++ b/server/tests/test_routes_extra.py @@ -1216,6 +1216,7 @@ class OcspSnapshotTests(unittest.TestCase): @async_test async def test_publisher_marks_dirty_and_uploads(self) -> None: + import cbor2 from dataclasses import replace from unittest.mock import patch @@ -1252,8 +1253,9 @@ class OcspSnapshotTests(unittest.TestCase): assert len(fake.calls) == 1 call = fake.calls[0] assert call["name"] == f"ocsp:{ca['id']}" - assert b"kca-ocsp-status/v2" in call["value"] - assert b"generated_at" not in call["value"] + published = cbor2.loads(call["value"]) + assert published["schema"] == "kca-ocsp-status/v2" + assert "generated_at" not in published assert call["ex"] == 60 row = await OcspSnapshot.get(ca_id=ca["id"]) assert row.dirty is False @@ -1334,7 +1336,7 @@ class OcspSnapshotTests(unittest.TestCase): @async_test async def test_imported_ca_cannot_overwrite_other_vault_snapshot(self) -> None: - import json + import cbor2 from dataclasses import replace from unittest.mock import patch @@ -1414,9 +1416,9 @@ class OcspSnapshotTests(unittest.TestCase): assert imported_key != target_key assert set(fake.objects) == {target_key, imported_key} assert fake.expiries == {target_key: 60, imported_key: 60} - target_snapshot = json.loads(fake.objects[target_key]) + target_snapshot = cbor2.loads(fake.objects[target_key]) target_entries = {entry["serial_hex"]: entry for entry in target_snapshot["entries"]} assert target_entries[leaf["serial_hex"].lower()]["status"] == "revoked" - imported_snapshot = json.loads(fake.objects[imported_key]) + imported_snapshot = cbor2.loads(fake.objects[imported_key]) imported_serials = {entry["serial_hex"] for entry in imported_snapshot["entries"]} assert leaf["serial_hex"].lower() not in imported_serials diff --git a/server/uv.lock b/server/uv.lock index 8b0444f..49260be 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -309,6 +309,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, ] +[[package]] +name = "cbor2" +version = "6.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/34/d443914ea562a985ccb357682e17b7190d5d58eff797c741379be47a8f31/cbor2-6.1.5.tar.gz", hash = "sha256:6eb06160c42315ac0c4ded461c7d84d92fa18c69d13d17fc1dfc1fae96580c95", size = 94232, upload-time = "2026-10-01T18:09:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/08/8bb3abca3820c20cd5efa51f0f37033f8bc514b4d6f38afb559257a4b17d/cbor2-6.1.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:519f3f0d0d9467091c678f4a19a31e1b8756c10bbd6294cb3f906092f3da1597", size = 416900, upload-time = "2026-10-01T18:07:47.646Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/39d6a60076cd9ffda49cb6cfa87cb57fc8bb9fdc2bec1b7eb4e934bb2ab2/cbor2-6.1.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fe81e4ff1b6bab72856d020dab89d86d4dcfbe18af4ff3fe2f391e1b03d0793c", size = 460595, upload-time = "2026-10-01T18:07:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/40618405d7925149c59e4e2874c7247670ccb562ede141b4c3b46f826d02/cbor2-6.1.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1ebbc6e2d5ea8acf44cc2247d48ca4ccae724fcdb97eaa673903e2d87f0ffc5d", size = 468860, upload-time = "2026-10-01T18:07:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3d/e9dfa478e4964e741cf6a9c5a098644264d4e0f5bef18a51d3ec4e2610d3/cbor2-6.1.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4db32eefe9fc173939d114fb78e09f967e69627714ad2e3bca807d0ea9d386ad", size = 527958, upload-time = "2026-10-01T18:07:53.916Z" }, + { url = "https://files.pythonhosted.org/packages/e0/39/13fa54e47a466414ea4a7b9d384b188539e869f6c2b57771b7e2f7429413/cbor2-6.1.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0fa113902a302c22429b32e2454251a8fd14b18204fdff647c869a54114c3ed1", size = 536415, upload-time = "2026-10-01T18:07:55.668Z" }, + { url = "https://files.pythonhosted.org/packages/51/b7/f12c7b555ab56633c0285e10294d5ea8a1d6c3aba3699ca9a44b0d2d267b/cbor2-6.1.5-cp310-cp310-win32.whl", hash = "sha256:c87272763122be24213c7bb3d47750a3af034da8755fbd3fcb0694c1efb6c3e8", size = 285715, upload-time = "2026-10-01T18:07:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/fcf9348216a376bd3607fdd15f46aec50e665deff677b936fccc77d931b7/cbor2-6.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:994b09c578e9dd7c5687a9f151f545bde705d12e47427b5a78c9d6cc970187f5", size = 308357, upload-time = "2026-10-01T18:07:58.892Z" }, + { url = "https://files.pythonhosted.org/packages/ed/15/4f3f573eb75cd7f2b709983bf567021d3d1018f101b6fb62f2e3d4d917c0/cbor2-6.1.5-cp310-cp310-win_arm64.whl", hash = "sha256:eba54489d82683e8cdb9af80a2e55c2089e439e76b60cdb9fd4dfdc62ecfee3c", size = 300702, upload-time = "2026-10-01T18:08:00.439Z" }, + { url = "https://files.pythonhosted.org/packages/84/62/6bd7ab55dda27ce4c0eefdf31a05b647c74a46e794bbf8ad5c3c26928e5b/cbor2-6.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5a5859d1f82dce094a1bdd6a5b318411b750262070bf5d37fbc9607d185f0b1b", size = 416295, upload-time = "2026-10-01T18:08:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/b2/22/9151b86062cc63d7155c86968971013dd6b01aeabd252a6dea015b16cfd9/cbor2-6.1.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7de5383eb059498291415f5b07f99e54dac4603dc99960eb0e2307c9cb2dc352", size = 458485, upload-time = "2026-10-01T18:08:03.502Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/9aecf0948c50e54302ae8859c85358a82310331ca00e210f8984760a2e3c/cbor2-6.1.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:dd3e4f08aaf25bca5db6274ac40e4d138b0e09890510c1fda20d5b7840e505fa", size = 467049, upload-time = "2026-10-01T18:08:05.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/13/bf133682c99f162662395dafe3b2525ed0bdafa558e52ac840e7a134d5bc/cbor2-6.1.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bb58549a45e3f6355338345a2df449f42f45d55e4a20af24d4302d76a1578650", size = 526819, upload-time = "2026-10-01T18:08:06.758Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7dda5b13258f740d529c9b3f5ed418d2c1aa4dbcbf886a35fb2f3f41970b/cbor2-6.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4956f498cbf5eab192e0f838cc787e09bef4caab57f05ccbf00451935cacb8b", size = 535239, upload-time = "2026-10-01T18:08:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/b72cb7b71c25b506a181ea9ec5bf634783c38e284873847ae6cb610c0f45/cbor2-6.1.5-cp311-cp311-win32.whl", hash = "sha256:f02c339ab9942578b63a5d54c8956191f6e88f3d8b2c918024ff565f7faa1bde", size = 285268, upload-time = "2026-10-01T18:08:10.591Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/9e51e3e43d6d42e71e93781d50b2f28cdcacc7f647681e07cbdaaf670e03/cbor2-6.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:015ed73f10e1f7b67306d41e36e0d7dc40e4a2100bc5c29b7a7f039ad3dc9061", size = 307786, upload-time = "2026-10-01T18:08:12.034Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7c/8514bf3a7a8af8347b8ba33cb9b3a9943200b37d81103b783543ab831ecb/cbor2-6.1.5-cp311-cp311-win_arm64.whl", hash = "sha256:f0bd6334302a5016a2b0f5530b7aea3ff588b6894523fd8491b49f7ce9e67f11", size = 300234, upload-time = "2026-10-01T18:08:13.579Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d6/8278f1abd5b6b5bcfc94158226a737b62fa0e50ba1d8d0b77f42edbf74f8/cbor2-6.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c1565bcd74a389b581e292592ccab0ed9c46286c6e986256820bc68c9ad7e8c", size = 407737, upload-time = "2026-10-01T18:08:14.982Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1b/a58d72ecbe15273e4e4842ac2149361e2bc0ad75fcab117c06da3c31782f/cbor2-6.1.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f8f85a49db66df77546d278de4d249772a4557d715df07ba8ae155cfa6a7fb31", size = 451924, upload-time = "2026-10-01T18:08:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/72/28/72c76aee7aa74e5dc53b79505dc6c168805d20c8e75166143076c5b61906/cbor2-6.1.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b70d7c47ea84d456034d2be02e89d92eef7044cfcedf6f05058e21d4452f0fef", size = 463316, upload-time = "2026-10-01T18:08:18.293Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a4/d81e9351c9ad37da4d999edcd05c6542a24e8899bb0ee8f91990e9e52981/cbor2-6.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:694f75fdcdb8c6b9a71ab77f789f56be1deab20bbdbf948d5ff53cd7c2543dfc", size = 519564, upload-time = "2026-10-01T18:08:20.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/c7/f7da3d0d46022a1c802074e13966863972d68f29cf07301cce2c8e98febc/cbor2-6.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:09eeb76177758a0fdf1627a9428b384756872b048c6c0d7d158106b29b207d2c", size = 530974, upload-time = "2026-10-01T18:08:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e3/74fddce015b171ee087a6e0185a233f3d29c7fda80cfa3041c796a67d100/cbor2-6.1.5-cp312-cp312-win32.whl", hash = "sha256:789ef813f416d353aecd5c8824860ee4be94e0f1179a385eb2beccfbeb615e4f", size = 281010, upload-time = "2026-10-01T18:08:23.614Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/ecc8d6a9ff9322405b23a4d3226504e7d7a44424e0d831a02b49bac8e605/cbor2-6.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:9677ce1c3c0cb1fa5a4f721a127fc2cc06e8efc43ee8e5f94e292186d6b51953", size = 304308, upload-time = "2026-10-01T18:08:25.077Z" }, + { url = "https://files.pythonhosted.org/packages/a8/90/23b702147b0858dbbc8a3136f288248118bb32f2785cc35c470a3b3f5571/cbor2-6.1.5-cp312-cp312-win_arm64.whl", hash = "sha256:b73d982e35a60e602a200feb2a9d272e850efdc9ff767b0f4887bdbc16d23e52", size = 293958, upload-time = "2026-10-01T18:08:26.493Z" }, + { url = "https://files.pythonhosted.org/packages/f9/db/a40752361f48c5b369f7e39ad80d8c67dfebe021f06042fadb5425592084/cbor2-6.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f850860e43d47312cb962bfdfe1cd879b180a04d0e7352f80e426b3852be8b79", size = 406941, upload-time = "2026-10-01T18:08:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f3/1bd052177e63fc5114a105c210ddef6d1132006f421b2577f51abf6fbecc/cbor2-6.1.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:65a677ff460f5c31f060a4bf8518f3e8184c321fddc0223a5ac2fac59a7f9f30", size = 450578, upload-time = "2026-10-01T18:08:29.881Z" }, + { url = "https://files.pythonhosted.org/packages/82/92/9d20136a9e3ba31fd2a9073955409b9f9001c86b4149cae4900ac737a820/cbor2-6.1.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:833db11fbea9808b080e5340d5f96615e28a6a6617618a4331e60082d0dc1ca4", size = 462522, upload-time = "2026-10-01T18:08:31.486Z" }, + { url = "https://files.pythonhosted.org/packages/35/5c/094b4194e64437252bea8c009f5094a6b1d7c2308e9f9e7edd56062209a8/cbor2-6.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb30032171afc7ab95e524f13eee0c9a79af356b0414fa3a3736b3febca7d641", size = 518793, upload-time = "2026-10-01T18:08:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/88/d7/cdd8581472c8bdeb3fb6077612535eb81e5b50b1efc8c98944a5b85f9e65/cbor2-6.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c916d7af4edcbf5dba157e9a8dd927bbf1fd66d3f137618226f7ad8b54bd944a", size = 530301, upload-time = "2026-10-01T18:08:34.828Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/018fbb0d4a1ef41384fe00454f5d8cc773b9a7242a54aed24a7cf1171427/cbor2-6.1.5-cp313-cp313-win32.whl", hash = "sha256:773ef85feea8beb5666a525e88197e3ef1c6629c6b6cf721e31b228c97cf6555", size = 280312, upload-time = "2026-10-01T18:08:36.288Z" }, + { url = "https://files.pythonhosted.org/packages/da/98/b157eced6c24d6edf38ec29aa21023e01f3f49a1b1da8b3b05ef83bfdca5/cbor2-6.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:af14089f5fb36f89b3f766acc7d4990cdfba7487ec0249d51bfa3a8caad25f0a", size = 303367, upload-time = "2026-10-01T18:08:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/a8/24/9482a7ade6cc017f29c420b92a5aed1d2affe76d4ec337eff01af5799246/cbor2-6.1.5-cp313-cp313-win_arm64.whl", hash = "sha256:9b3ba6f694ec196ebefc9c67ebc862b0fecdd3d6f85d5557378cf20ff8b1fb31", size = 293095, upload-time = "2026-10-01T18:08:39.482Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/d2fdf618c87d9b2964cd76550b93a6cfd0918303ac7f3b9b9f0c36fff9be/cbor2-6.1.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a14edbdc9e02d9daa72c3b8805edb297a6025a35e708f7dd8ccbdf1b18adb40f", size = 409682, upload-time = "2026-10-01T18:08:40.891Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7d/8ad5d4e6088b292ecea337726c6ca602bb9abffeae39998f4b072731aec3/cbor2-6.1.5-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e1028f34af9158ee810c705a1c6c0b7c71f1e0a3c890fb343afd75725a80c191", size = 454408, upload-time = "2026-10-01T18:08:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fa/5f9baeecf35db1d35ca5415dfa1e8656d656ccbbaca875e65d72df849f4e/cbor2-6.1.5-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:73b97d92ce64a344015909f1888de0abec76211b9c1f33b075563a05512f3a98", size = 464560, upload-time = "2026-10-01T18:08:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/d4/63/260e882e1055f48f88dc7e13ceaeff0f700e84d9c6d3683ac4d6350ee551/cbor2-6.1.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9907225060f8afcf31b5c97711cd057272160056a6b1b488313cc2b20c0afe74", size = 521581, upload-time = "2026-10-01T18:08:45.705Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c7/f2976097933583b48109d76c30e9df7503f7001fb78abc77af0db87516f8/cbor2-6.1.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4c824355799799ab065686a05f65398319109955544db35cc797c60ad208b174", size = 532971, upload-time = "2026-10-01T18:08:47.352Z" }, + { url = "https://files.pythonhosted.org/packages/c8/56/e99d5f265e4647f7a5ba4fe82888bb4434f10ef80bbbce82b72f2e34a8ce/cbor2-6.1.5-cp314-cp314-win32.whl", hash = "sha256:8665b7970e563fb807cca5c42815fe0741192a899b74bf9052557486a46f9188", size = 287411, upload-time = "2026-10-01T18:08:48.841Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/6e501c663e1c682d023abbf072bc2866b0ebf4143332a228b2b16c2914f2/cbor2-6.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:0529a95c1330c9c381286650dd65ff5b4ef136dcee06474ad30c028b5ae99a50", size = 317179, upload-time = "2026-10-01T18:08:50.326Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b8dc9768097d9d6eb9d3598b35011caecc53911e2a41b164035fc6d80872/cbor2-6.1.5-cp314-cp314-win_arm64.whl", hash = "sha256:547c58e758462f06ba542b0af21afb150ee64c4c81d7ca6d1ecae0655c6a283d", size = 307114, upload-time = "2026-10-01T18:08:51.825Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/7f4654f26ed2d6ca7c17485d4a87ccfe023798ffd6e979aa0ed007e9d86e/cbor2-6.1.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2634a4e8dbd86cfbdace0a546a1ded1fb024ebc4fbbeaea0232cc76721e6bc91", size = 405647, upload-time = "2026-10-01T18:08:53.529Z" }, + { url = "https://files.pythonhosted.org/packages/db/f3/01893ff4f379109a156c7d356968b966fb9155ec18283926891ef9f1fb6e/cbor2-6.1.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db607ae2b12c7eb85d463fe502a2f50111125bee69e70f85f793f0b7da7896e7", size = 447164, upload-time = "2026-10-01T18:08:55.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b8ffb30546b1c06d98424b9eb02ae6267b16e2323c3e73404bf807faedd9/cbor2-6.1.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:68bcabc5b36a7c7c8825625b7b331a74098a4839d5d38b5cc29cb30a7acfee49", size = 462895, upload-time = "2026-10-01T18:08:56.953Z" }, + { url = "https://files.pythonhosted.org/packages/1a/32/8eaea4e9e46c8b8e7e1e94b6c43807a2897f0cc36c0b0fab0a488e345dcf/cbor2-6.1.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:10d5237100190133d6a770181a63d93752cb67a2849c18484d196b5f8880784e", size = 514829, upload-time = "2026-10-01T18:08:58.762Z" }, + { url = "https://files.pythonhosted.org/packages/02/27/12e4427d256a02f6124426251c6ae1d37c2a90cae1f2d09d0424eecd01a2/cbor2-6.1.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4144e2ba881534f62968cdb4a4f134e07a351e75c997d8debca65fcb2edd61c8", size = 530055, upload-time = "2026-10-01T18:09:00.747Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/074eb7c1a4a41a9ddf930ec911888dda7ea3c88dca85df316e5b7aeb53c7/cbor2-6.1.5-cp314-cp314t-win32.whl", hash = "sha256:7dfb68b65d6b0d0d90512626247bfa4993354f1e2b2d83b28b51785e63853422", size = 284236, upload-time = "2026-10-01T18:09:02.335Z" }, + { url = "https://files.pythonhosted.org/packages/04/97/687b31a25f4755d71912682587f6d909f751a06cf8d2e68dc8737ac20537/cbor2-6.1.5-cp314-cp314t-win_amd64.whl", hash = "sha256:e1e8a6a72c7ab2f82579497cb1d5564987b02559ab980fe6a5f82a7d65031d19", size = 313558, upload-time = "2026-10-01T18:09:03.916Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/6a3fe78c3d79385bedb1a40b8d1554bbcb03b8762ed5847e77ec9b86b777/cbor2-6.1.5-cp314-cp314t-win_arm64.whl", hash = "sha256:edc4a4dfa313b2cd78d7562cb99b51615e06c89832b78c0c02e2b5c2e27906ae", size = 301775, upload-time = "2026-10-01T18:09:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/97/98c7c04aa255a9f6b2d1d3c35d210d0363fc7fa7c67963d6886086238748/cbor2-6.1.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f340682e2481ab729c399f8b81147476c5a179cfef65d02402702aeb9429088", size = 402161, upload-time = "2026-10-01T18:09:07.143Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/8c209c49a7a1cefe7d6aa35211523ca5c25b3cf35e1b281cfdea2a42ec81/cbor2-6.1.5-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:30f88d1aff6c8c58ffec56591468f820d5ce6aee0bd64ae7443c0d7ef653eaf8", size = 446558, upload-time = "2026-10-01T18:09:08.964Z" }, + { url = "https://files.pythonhosted.org/packages/eb/65/c6836f9bb9f14a01696c5d90fee07585ae595b6b466ae1c7885405f7317d/cbor2-6.1.5-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:f294e65db28424fe89985faf74648622e04da7977ca5401ac65c7d1b6538d08a", size = 460016, upload-time = "2026-10-01T18:09:10.694Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/f58879254c9e5478f05bc9d5aaad9310b190d8a942f992980c877ba8795b/cbor2-6.1.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b586912cdb086dbad12052250acd5922fbe66a341ebee7031039eedf90fe84b1", size = 513758, upload-time = "2026-10-01T18:09:12.374Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ec/7ad474e9f79f8f7047754d4be6cc55b58f774ad3990631420dcd2f429197/cbor2-6.1.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e6d54e11887e649345b2ecb491a8e2866f4abdb6d83abc2a1a52d5ee23785ff8", size = 527606, upload-time = "2026-10-01T18:09:13.957Z" }, + { url = "https://files.pythonhosted.org/packages/01/90/df3e21b7d71ab6bf61f8fd8a0c87ad1de129dbbc5bc5dc2b01b1a1437e2d/cbor2-6.1.5-cp315-cp315-win32.whl", hash = "sha256:4e298c8a88488ebbf5475e51273b8d80da08f7b47aebfa79eb904fc82da49474", size = 281140, upload-time = "2026-10-01T18:09:15.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/58/d31f4eb982a87a71b469b16d1579ec703ba0fcd7f748907b89e84b6c1120/cbor2-6.1.5-cp315-cp315-win_amd64.whl", hash = "sha256:a9a154e010044662ce2e433f7c49e9c0f89ad7b86cb20e5d2e5afe6fd1753162", size = 308898, upload-time = "2026-10-01T18:09:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/e9/55/016955040b4193a50440116c4ccc827df15860c9a192476cd178671270c9/cbor2-6.1.5-cp315-cp315-win_arm64.whl", hash = "sha256:cf89dd755e9781bea60bb67c1569d32ca10c38412126ab58bbc0235c697d98fc", size = 299711, upload-time = "2026-10-01T18:09:18.996Z" }, + { url = "https://files.pythonhosted.org/packages/7a/09/e7895f5388f243e6224581c77133d0404e9c8d302e72ec9179cdd8bdc007/cbor2-6.1.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:42217c9de0ead6c5a6c1a6ca6b836204ac46b5bf4f57c758f522f308d7784bf0", size = 397947, upload-time = "2026-10-01T18:09:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6e/983bbf4850acb3ec3e99b039331e568fca0fd10bcd2c55746374d24e5875/cbor2-6.1.5-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:40754de6aef3f3d37f2ab36bb431da145359d0e28fce739683f8717ad2e97280", size = 441234, upload-time = "2026-10-01T18:09:22.584Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0c/a19e7b8627dfc291c1004e67e0594ce687a5ccfc32321748b27cefca76a1/cbor2-6.1.5-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:9140388e9a732f3748641abb91d257d30cc466a7ed13c2c5a3d1aaa6af37bd66", size = 457317, upload-time = "2026-10-01T18:09:24.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/4e/2fa0a755436323155b574ded8d6fa840bec8f153ba7a47c2363d316e0df9/cbor2-6.1.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:040cf628af473fe18cb6f56bdac556d2398102e56852aab5206fbeb3dbde6b52", size = 507155, upload-time = "2026-10-01T18:09:25.61Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b8/6fbe00ebaa935ab0683f5d9eb7b6f67097e0398a1e8e4120eb1298968f07/cbor2-6.1.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:151f624186a6b607d14074dfffe7b601f403445ab430554e3d920390c3068b05", size = 524789, upload-time = "2026-10-01T18:09:27.451Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/f10f5a273a680ef9beb36e6c22f92461d1d9c19bea6cb1bd876a1eb26d3b/cbor2-6.1.5-cp315-cp315t-win32.whl", hash = "sha256:1538e87b4b32764bc4940a37b6aa72e3bc6855033aac18d392d70daa89113a2b", size = 277303, upload-time = "2026-10-01T18:09:29.102Z" }, + { url = "https://files.pythonhosted.org/packages/78/33/c8c958ee8bb1a0931d1f863fa2b8ab9526e29c841c86f7a428feb7cb9a76/cbor2-6.1.5-cp315-cp315t-win_amd64.whl", hash = "sha256:0b1fa210f23b1f822ee0c9157c99b0e851fce93c6da1dc8441aa7fb3c4089d70", size = 305311, upload-time = "2026-10-01T18:09:30.645Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c0/e27a1e516a89af7194fc497f4b96d9601771ca41bb66fd5738113df80282/cbor2-6.1.5-cp315-cp315t-win_arm64.whl", hash = "sha256:fd34b35b0a2b366f5b4bd53489ccd10d7576b0d4dd68db38ef64b4e617ea8f76", size = 294495, upload-time = "2026-10-01T18:09:32.192Z" }, +] + [[package]] name = "certifi" version = "2026.10.19" @@ -989,6 +1061,7 @@ source = { editable = "packages/kca-app" } dependencies = [ { name = "aerich" }, { name = "asyncpg" }, + { name = "cbor2" }, { name = "cryptography" }, { name = "granian" }, { name = "httpx" }, @@ -1026,6 +1099,7 @@ dev = [ requires-dist = [ { name = "aerich" }, { name = "asyncpg" }, + { name = "cbor2" }, { name = "cryptography", specifier = ">=44" }, { name = "granian", specifier = ">=2.0" }, { name = "httpx" },