diff --git a/.dockerignore b/.dockerignore index b6264b0..7bcbbcd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,3 +12,8 @@ web/.trunk e2e/node_modules e2e/test-results e2e/playwright-report + +ocsp-responder/target +ocsp-responder/ca +ocsp-responder/signers +ocsp-responder/responder.yaml diff --git a/.gitignore b/.gitignore index 2454177..6c121be 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ __pycache__/ web/target/ web/dist/ web/.trunk/ +ocsp-responder/target/ +ocsp-responder/responder.yaml +ocsp-responder/ca/ +ocsp-responder/signers/ # Environment and local data .env diff --git a/README.md b/README.md index ce1285e..c609a1d 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,34 @@ 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. +## OCSP responder + +KCA can publish full per-CA certificate-status snapshots to any S3-compatible +bucket and ships a standalone Rust/Axum responder on port `2560`. + +- KCA publishes `ocsp/.json` whenever certificates are + issued, renewed, revoked, deleted or imported. Revocation reasons are + included; this is a full status snapshot, not a revoked-only list. +- The responder has no Postgres/Redis/KCA master-key access. It only reads the + configured S3 prefix and uses a delegated OCSP signing certificate/key per + CA. +- Each CA snapshot is cached for one second with a single-flight refresh, so + concurrent requests cause at most one S3 `GetObject` per CA per second. +- If S3 is unavailable, a stale snapshot is served within the configured grace + period; otherwise the responder returns `tryLater`. + +Enable publishing in KCA with `KCA_OCSP_S3_BUCKET` and the other +`KCA_OCSP_*` variables in `server/.env.example`. Run the responder with a +configuration derived from `ocsp-responder/responder.example.yaml`, or with +Compose: + +```sh +docker compose --profile ocsp up --build +``` + +See `ocsp-responder/README.md` for the JSON layout, signer requirements and +`openssl ocsp` verification. + ## REST API OpenAPI JSON is at `/api/openapi.json`; Swagger UI at `/api/docs`. @@ -155,7 +183,7 @@ OpenAPI JSON is at `/api/openapi.json`; Swagger UI at `/api/docs`. | 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`, `POST /api/vaults/{id}/export` | | 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`, `POST /api/vaults/{id}/certificates/preview`, `GET/PATCH/DELETE .../{id}`, `POST .../{id}/export`, `POST .../{id}/renew`, `POST .../{id}/revoke`, `POST /api/vaults/{id}/certificates/bulk-revoke`, `GET/PATCH .../{id}/ca`, `GET/POST .../{id}/revocations`, `PATCH/DELETE .../revocations/{rev_id}`, `POST .../{id}/crl`, `POST .../{id}/tls-auth`, transforms | +| Certificates | `GET/POST /api/vaults/{id}/certificates`, `POST /api/vaults/{id}/certificates/preview`, `GET /api/vaults/{id}/certificates/{ca_id}/ocsp-status`, `GET/PATCH/DELETE .../{id}`, `POST .../{id}/export`, `POST .../{id}/renew`, `POST .../{id}/revoke`, `POST /api/vaults/{id}/certificates/bulk-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`) | @@ -190,6 +218,13 @@ npm test They use Playwright Chromium. Set `KCA_BASE_URL` to target another port, e.g. `KCA_BASE_URL=http://localhost:8083 npm test`. See `e2e/README.md`. +OCSP responder (from `ocsp-responder/`): + +```sh +cargo test +cargo run # uses OCSP_CONFIG or /etc/ocsp/responder.yaml +``` + Frontend (from `web/`, with the backend running on `:8080`): ```sh @@ -255,6 +290,7 @@ web/ src/{api,auth,model,dom}.rs pwa/ # manifest + app-shell assets e2e/ # Playwright browser suite (Node) +ocsp-responder/ # standalone Rust/Axum OCSP responder ``` ## Limits diff --git a/docker-compose.yml b/docker-compose.yml index 310cdc3..e1afe99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,8 +105,44 @@ services: OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-} OTEL_EXCLUDED_PATHS: ${OTEL_EXCLUDED_PATHS:-/api/health} + # OCSP status publisher (optional). The bucket must be writable by + # KCA; the responder later reads the same prefix with read-only access. + KCA_OCSP_S3_BUCKET: ${KCA_OCSP_S3_BUCKET:-} + KCA_OCSP_S3_PREFIX: ${KCA_OCSP_S3_PREFIX:-ocsp} + KCA_OCSP_S3_REGION: ${KCA_OCSP_S3_REGION:-} + KCA_OCSP_S3_ENDPOINT: ${KCA_OCSP_S3_ENDPOINT:-} + KCA_OCSP_S3_FORCE_PATH_STYLE: ${KCA_OCSP_S3_FORCE_PATH_STYLE:-false} + KCA_OCSP_PUBLISH_INTERVAL_SECONDS: ${KCA_OCSP_PUBLISH_INTERVAL_SECONDS:-1} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-} + AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN:-} + AWS_REGION: ${AWS_REGION:-} ports: - "127.0.0.1:${APP_PORT:-8080}:8080" + # Optional standalone OCSP responder. It deliberately has no database, + # Redis or KCA master-key environment. Enable with: + # docker compose --profile ocsp up --build + ocsp: + profiles: ["ocsp"] + build: + context: . + dockerfile: ocsp-responder/Dockerfile + environment: + OCSP_CONFIG: /etc/ocsp/responder.yaml + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-} + AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN:-} + AWS_REGION: ${AWS_REGION:-} + volumes: + - ${OCSP_CONFIG_FILE:-./ocsp-responder/responder.example.yaml}:/etc/ocsp/responder.yaml:ro + - ${OCSP_CA_DIR:-./ocsp-responder/ca}:/etc/ocsp/ca:ro + - ${OCSP_SIGNER_DIR:-./ocsp-responder/signers}:/etc/ocsp/signers:ro + ports: + - "127.0.0.1:${OCSP_PORT:-2560}:2560" + read_only: true + security_opt: + - no-new-privileges:true + volumes: pgdata: diff --git a/ocsp-responder/Cargo.lock b/ocsp-responder/Cargo.lock new file mode 100644 index 0000000..873e688 --- /dev/null +++ b/ocsp-responder/Cargo.lock @@ -0,0 +1,3309 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arc-swap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939e256cfe0ed68e96d32f194339d88210d5c10685cff0787400158f852c6819" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.5.0", + "sha1 0.10.7", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576e8da35751dc85409ec1cd15999f692c277a1fbe9336780fe6f15e49dd637d" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a0cc2144962cc5e358467b16e3d82981b1d424a7c3d34602be0036103814a1a" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.155.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a542f0e9261b9ce56708d591a62062079fa3c58a362ffbf49a2e48a4f6ced16" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "http-body 1.1.0", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.1", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.117.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316813498fa9c403877007934ae448aa44b18136f58051f8fa3c5b79b44bdc6" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.119.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31db35e5662787b786daba15f431e4393c7ea512b70fa112bd048d1f3ecf877a" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.122.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10491638d9e560db90c484dbc77d73b8ad202780cdc9b9e92180603acb07ada" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3d48355cb86b15ccbb38b8bc6b87500c824ee4ed8cbc10a961b435242b5e16" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "ecdsa 0.17.0", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "p256 0.14.0", + "percent-encoding", + "sha2 0.11.1", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de681a9df7ff79823b759ee17061076ec049cd61a8fa28ece4293d89c9f6964e" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.1", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c8a643d0e114cae39e45415e3225793a6396698b7779a0c9e3ede731d8fc71" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8960956e0ee0ca3f3161e62913a61bc65a7ef638ca3ef1d4c7159f8443e398" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeca69f65362f7375891008ea525fd440ad6db1836458ac188c87b126804114e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.20", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.12.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.10", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.46", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.6", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f6835fb38bc6fc8c91e584b0366eb4ff595e5c485134dbf4f0bdeaa18856666" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165f6265342a313ad6f7a1ac17a0cdba30c522039d3f41d8d6871ce8a2b34ecd" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cef0c41e8ca40f74f68ed8ee4d1ddbde12bb847b95ef9b3c025681ddfae58a" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06788004dea79018413f381891da6dcca23196acd79c4d51b767fbdacc76eae4" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec8dae1015a6d959a5faf86fbdf77486cfe19e4ccb6ee009d8a46efc45028a4" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.5.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55752025c39c0e094962fd73f1c57014b191f678612e517f219468e575af9822" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6581618a2fd006159016d1c36881d94d61b4a017f4a8d631310875e18252413" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0d970d3947bb064d2ae1d823efa186c1dbde88f88afa8f6549946e5d8deee" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.12.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.1", +] + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der 0.8.2", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.1", + "generic-array", + "group 0.13.0", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.5.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.20", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http 1.5.0", + "hyper 1.12.0", + "hyper-util", + "rustls 0.23.46", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.6", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "hyper 1.12.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kca-ocsp-responder" +version = "0.1.0" +dependencies = [ + "async-trait", + "aws-config", + "aws-sdk-s3", + "axum", + "base64 0.22.1", + "bytes", + "chrono", + "const-oid 0.9.6", + "der 0.7.10", + "ecdsa 0.16.9", + "hex", + "p256 0.13.2", + "pkcs8 0.10.2", + "rsa", + "sec1 0.7.3", + "serde", + "serde_json", + "serde_yaml", + "sha1 0.10.7", + "sha2 0.10.9", + "thiserror", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "x509-cert", + "x509-ocsp", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.1", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve 0.14.1", + "once_cell", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.16", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.2", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls 0.23.46", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "primefield", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + +[[package]] +name = "x509-ocsp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e54e695a31f0fecb826cf59ae2093c941d7ef932a1f8508185dd23b29ce2e2e" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "digest 0.10.7", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "x509-cert", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/ocsp-responder/Cargo.toml b/ocsp-responder/Cargo.toml new file mode 100644 index 0000000..996ed60 --- /dev/null +++ b/ocsp-responder/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "kca-ocsp-responder" +version = "0.1.0" +edition = "2021" +description = "Standalone OCSP responder for KCA status snapshots" + +[dependencies] +axum = "0.8" +tokio = { version = "1", features = ["full"] } +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" +serde_yaml = "0.9" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "2" +bytes = "1" +async-trait = "0.1" +base64 = "0.22" +hex = "0.4" +sha1 = "0.10" +sha2 = "0.10" +der = { version = "0.7", features = ["std"] } +const-oid = "0.9" +rsa = { version = "0.9", features = ["sha2"] } +p256 = { version = "0.13", features = ["ecdsa"] } +sec1 = { version = "0.7", features = ["pem"] } +ecdsa = { version = "0.16", features = ["der"] } +pkcs8 = { version = "0.10", features = ["pem"] } +x509-ocsp = { version = "0.2.1", features = ["builder", "std"] } +x509-cert = { version = "0.2", features = ["std"] } +aws-config = "1" +aws-sdk-s3 = "1" diff --git a/ocsp-responder/Dockerfile b/ocsp-responder/Dockerfile new file mode 100644 index 0000000..a3b32ea --- /dev/null +++ b/ocsp-responder/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 +# Standalone Rust/Axum OCSP responder. It has no database or KCA master-key +# access; configuration and signer material are mounted at runtime. + +FROM rust:1-slim-bookworm AS builder +WORKDIR /app +COPY ocsp-responder/Cargo.toml ocsp-responder/Cargo.lock ./ +COPY ocsp-responder/src ./src +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo build --release --locked \ + && cp /app/target/release/kca-ocsp-responder /usr/local/bin/kca-ocsp-responder + +FROM debian:bookworm-slim +RUN addgroup --system ocsp \ + && adduser --system --ingroup ocsp --no-create-home ocsp \ + && mkdir -p /etc/ssl/certs +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +COPY --from=builder /usr/local/bin/kca-ocsp-responder /usr/local/bin/kca-ocsp-responder + +ENV OCSP_CONFIG=/etc/ocsp/responder.yaml +USER ocsp +EXPOSE 2560 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD ["/usr/local/bin/kca-ocsp-responder", "--healthcheck"] + +CMD ["/usr/local/bin/kca-ocsp-responder"] diff --git a/ocsp-responder/README.md b/ocsp-responder/README.md new file mode 100644 index 0000000..ce69740 --- /dev/null +++ b/ocsp-responder/README.md @@ -0,0 +1,74 @@ +# KCA OCSP responder + +Standalone Rust/Axum responder. It reads one full JSON status snapshot per CA +from S3-compatible object storage, caches it for one second with a single-flight +refresh, and signs OCSP responses with a delegated responder certificate. + +It has **no KCA database access**, no Redis access, and no KCA master key. It +only needs: + +- `s3:GetObject` on the configured bucket/prefix; +- the CA certificate for each issuer; +- a delegated OCSP signer certificate and private key for each CA. The + responder currently accepts RSA-SHA256 and ECDSA P-256 signers in PKCS#8, + PKCS#1 RSA, or SEC1 EC PEM form. + +## Snapshot layout + +KCA publishes objects as: + +``` +/.json +``` + +KCA publishes these objects when `KCA_OCSP_S3_BUCKET` is configured. The JSON +contains all certificates issued by that CA, both valid and revoked. The +responder never infers status from a partial revoked-only list. + +## Run locally + +1. Create a config based on `responder.example.yaml`. +2. Put your CA certificates and delegated signer files where the config points. +3. Export standard AWS credentials / endpoint variables as needed. +4. Make the signer key readable by the container user (`ocsp`, uid/gid + `100:101` in the provided image), or mount it via a Docker secret. +5. Run: + +```sh +OCSP_CONFIG=./responder.yaml cargo run +``` + +The default listen address is `0.0.0.0:2560`. + +## Verify with OpenSSL + +For a CA certificate `ca.crt` and leaf certificate `leaf.crt`: + +```sh +openssl ocsp \ + -issuer ca.crt \ + -cert leaf.crt \ + -url http://127.0.0.1:2560/ocsp \ + -resp_text \ + -noverify +``` + +`-noverify` skips local trust setup; the response still shows the signature +algorithm and included responder certificate. For production verification, +build a trust store containing the CA or validate with your normal PKI tooling. + +## Endpoints + +- `POST /ocsp` — `application/ocsp-request` +- `GET /ocsp/` — optional GET form +- `GET /healthz` +- `GET /readyz` + +## Cache and availability + +- Per-CA cache TTL: `s3.cache_ttl_ms` (default 1000 ms). +- Concurrent requests for the same CA share one S3 `GetObject`. +- If S3 fails, a previously cached snapshot is served for + `s3.stale_grace_seconds`; otherwise the OCSP response is `tryLater`. +- With multiple CAs, the TTL is per CA. If you need a global one-request-per- + second limit across all CAs, that requires a shared rate limiter. diff --git a/ocsp-responder/responder.example.yaml b/ocsp-responder/responder.example.yaml new file mode 100644 index 0000000..cdf8792 --- /dev/null +++ b/ocsp-responder/responder.example.yaml @@ -0,0 +1,24 @@ +# Copy this file, provide your own CA certificates and delegated OCSP +# signer certificate/key pairs, then mount it at /etc/ocsp/responder.yaml. +listen: 0.0.0.0:2560 +next_update_seconds: 10 + +s3: + bucket: kca-ocsp + prefix: ocsp + region: eu-central-1 + # Optional for MinIO / other S3-compatible stores: + # endpoint: https://s3.example.com + # force_path_style: true + cache_ttl_ms: 1000 + stale_grace_seconds: 60 + +issuers: + - name: Example Root CA + ca_cert: /etc/ocsp/ca/root.pem + signer_cert: /etc/ocsp/signers/root.pem + signer_key: /etc/ocsp/signers/root.key + # - name: Example Intermediate CA + # ca_cert: /etc/ocsp/ca/intermediate.pem + # signer_cert: /etc/ocsp/signers/intermediate.pem + # signer_key: /etc/ocsp/signers/intermediate.key diff --git a/ocsp-responder/src/cache.rs b/ocsp-responder/src/cache.rs new file mode 100644 index 0000000..a6277ef --- /dev/null +++ b/ocsp-responder/src/cache.rs @@ -0,0 +1,198 @@ +//! One-second, single-flight cache of per-CA status snapshots. + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use tokio::sync::{Mutex, RwLock}; + +use crate::snapshot::Snapshot; +use crate::store::ObjectStore; + +#[derive(Clone)] +pub struct CachedSnapshot { + snapshot: Arc, + fetched_at: Instant, +} + +#[derive(Default)] +pub struct SnapshotCache { + cached: RwLock>, + refresh: Mutex<()>, +} + +impl SnapshotCache { + pub fn new() -> Self { + Self::default() + } + + /// Return a snapshot, refreshing from S3 at most once per `ttl`. + /// + /// On refresh failure, data up to `ttl + stale_grace` old is still used. + pub async fn get( + &self, + store: &S, + key: &str, + expected_sha256: &str, + ttl: Duration, + stale_grace: Duration, + ) -> Result, CacheError> { + if let Some(value) = self.cached.read().await.as_ref() { + if value.fetched_at.elapsed() < ttl { + return Ok(value.snapshot.clone()); + } + } + + // Only one request refreshes a given CA. Everybody else waits here + // and then observes the refreshed value below. + let _guard = self.refresh.lock().await; + let stale = { + let cache = self.cached.read().await; + if let Some(value) = cache.as_ref() { + if value.fetched_at.elapsed() < ttl { + return Ok(value.snapshot.clone()); + } + Some(value.clone()) + } else { + None + } + }; + + match store.get(key).await { + Ok(bytes) => match Snapshot::parse(&bytes, expected_sha256) { + Ok(snapshot) => { + let snapshot = Arc::new(snapshot); + *self.cached.write().await = Some(CachedSnapshot { + snapshot: snapshot.clone(), + fetched_at: Instant::now(), + }); + Ok(snapshot) + } + Err(error) => self.fallback(stale, error.to_string(), stale_grace), + }, + Err(error) => self.fallback(stale, error.to_string(), stale_grace), + } + } + + fn fallback( + &self, + stale: Option, + error: String, + stale_grace: Duration, + ) -> Result, CacheError> { + if let Some(value) = stale { + if value.fetched_at.elapsed() <= stale_grace { + tracing::warn!(error = %error, "serving stale OCSP snapshot"); + return Ok(value.snapshot); + } + } + Err(CacheError::Unavailable(error)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CacheError { + #[error("OCSP snapshot unavailable: {0}")] + Unavailable(String), +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use async_trait::async_trait; + use bytes::Bytes; + + use super::*; + use crate::snapshot::sha256_hex; + use crate::store::{ObjectStore, StoreError}; + + struct FakeStore { + body: String, + calls: AtomicUsize, + fail: AtomicBool, + } + + impl FakeStore { + fn new(body: String) -> Self { + Self { + body, + calls: AtomicUsize::new(0), + fail: AtomicBool::new(false), + } + } + } + + #[async_trait] + impl ObjectStore for FakeStore { + async fn get(&self, _key: &str) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + if self.fail.load(Ordering::SeqCst) { + return Err(StoreError::S3("offline".to_string())); + } + Ok(Bytes::from(self.body.clone())) + } + } + + fn body(sha: &str) -> String { + format!( + r#"{{"schema":"kca-ocsp-status/v1","issuer":{{"sha256":"{sha}"}},"entries":[{{"serial_hex":"01","status":"valid"}}]}}"# + ) + } + + #[tokio::test] + async fn single_flight_refreshes_once() { + let sha = sha256_hex(b"ca"); + let store = FakeStore::new(body(&sha)); + let cache = SnapshotCache::new(); + let (a, b) = tokio::join!( + cache.get( + &store, + "key", + &sha, + Duration::from_millis(100), + Duration::from_secs(1) + ), + cache.get( + &store, + "key", + &sha, + Duration::from_millis(100), + Duration::from_secs(1) + ) + ); + assert!(a.is_ok()); + assert!(b.is_ok()); + assert_eq!(store.calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn refreshes_after_ttl_and_serves_stale_on_error() { + let sha = sha256_hex(b"ca"); + let store = FakeStore::new(body(&sha)); + let cache = SnapshotCache::new(); + cache + .get( + &store, + "key", + &sha, + Duration::from_millis(20), + Duration::from_secs(1), + ) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(30)).await; + store.fail.store(true, Ordering::SeqCst); + let stale = cache + .get( + &store, + "key", + &sha, + Duration::from_millis(20), + Duration::from_secs(1), + ) + .await + .unwrap(); + assert_eq!(stale.entries.len(), 1); + assert_eq!(store.calls.load(Ordering::SeqCst), 2); + } +} diff --git a/ocsp-responder/src/config.rs b/ocsp-responder/src/config.rs new file mode 100644 index 0000000..bb8be89 --- /dev/null +++ b/ocsp-responder/src/config.rs @@ -0,0 +1,106 @@ +//! OCSP responder configuration. + +use std::net::SocketAddr; +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + #[serde(default = "default_listen")] + pub listen: SocketAddr, + #[serde(default)] + pub s3: S3Config, + #[serde(default)] + pub issuers: Vec, + #[serde(default = "default_ocsp_next_update_seconds")] + pub next_update_seconds: u64, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct IssuerConfig { + pub name: String, + pub ca_cert: PathBuf, + pub signer_cert: PathBuf, + pub signer_key: PathBuf, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct S3Config { + pub bucket: String, + #[serde(default = "default_prefix")] + pub prefix: String, + pub region: Option, + pub endpoint: Option, + #[serde(default)] + pub force_path_style: bool, + #[serde(default = "default_cache_ttl_ms")] + pub cache_ttl_ms: u64, + #[serde(default = "default_stale_grace_seconds")] + pub stale_grace_seconds: u64, +} + +impl Default for S3Config { + fn default() -> Self { + Self { + bucket: String::new(), + prefix: default_prefix(), + region: None, + endpoint: None, + force_path_style: false, + cache_ttl_ms: default_cache_ttl_ms(), + stale_grace_seconds: default_stale_grace_seconds(), + } + } +} + +impl S3Config { + pub fn object_key(&self, ca_fingerprint_sha256: &str) -> String { + let prefix = self.prefix.trim_matches('/'); + let name = format!("{}.json", ca_fingerprint_sha256.to_lowercase()); + if prefix.is_empty() { + name + } else { + format!("{prefix}/{name}") + } + } +} + +fn default_listen() -> SocketAddr { + "0.0.0.0:2560" + .parse() + .expect("valid default listen address") +} + +fn default_prefix() -> String { + "ocsp".to_string() +} + +fn default_cache_ttl_ms() -> u64 { + 1000 +} + +fn default_stale_grace_seconds() -> u64 { + 60 +} + +fn default_ocsp_next_update_seconds() -> u64 { + 10 +} + +pub fn load_config() -> Result> { + let path = + std::env::var("OCSP_CONFIG").unwrap_or_else(|_| "/etc/ocsp/responder.yaml".to_string()); + let text = std::fs::read_to_string(&path)?; + let mut config: Config = serde_yaml::from_str(&text)?; + if config.issuers.is_empty() { + return Err("configuration must contain at least one issuer".into()); + } + if config.s3.bucket.trim().is_empty() { + return Err("s3.bucket must be configured".into()); + } + if let Ok(listen) = std::env::var("OCSP_LISTEN") { + config.listen = listen.parse()?; + } + Ok(config) +} diff --git a/ocsp-responder/src/issuer.rs b/ocsp-responder/src/issuer.rs new file mode 100644 index 0000000..fc675c4 --- /dev/null +++ b/ocsp-responder/src/issuer.rs @@ -0,0 +1,158 @@ +//! Configured CA + delegated signer + snapshot cache. + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; + +use const_oid::db::rfc5280::{ID_CE_EXT_KEY_USAGE, ID_KP_OCSP_SIGNING}; +use der::{Decode, Encode}; +use tokio::sync::Mutex; +use x509_cert::ext::pkix::ExtendedKeyUsage; +use x509_cert::Certificate; +use x509_ocsp::CertId; + +use crate::cache::SnapshotCache; +use crate::config::IssuerConfig; +use crate::signer::{read_certificate, SignerError, SignerKey}; +use crate::snapshot::{hash_sha1, hash_sha256, sha256_hex}; + +pub struct Issuer { + pub name: String, + pub signer_cert: Certificate, + pub signer: Mutex, + pub fingerprint: String, + name_hashes: HashMap>, + key_hashes: HashMap>, + pub cache: SnapshotCache, +} + +impl Issuer { + pub fn load(config: &IssuerConfig) -> Result, IssuerError> { + let ca_cert = read_certificate(&config.ca_cert)?; + let signer_cert = read_certificate(&config.signer_cert)?; + let signer = SignerKey::load(&config.signer_key)?; + if !signer.matches_certificate(&signer_cert)? { + return Err(IssuerError::KeyMismatch(config.name.clone())); + } + if signer_cert.tbs_certificate.issuer != ca_cert.tbs_certificate.subject { + return Err(IssuerError::SignerIssuer(config.name.clone())); + } + check_ocsp_signing_eku(&signer_cert)?; + let now = SystemTime::now(); + if signer_cert + .tbs_certificate + .validity + .not_before + .to_system_time() + > now + || signer_cert + .tbs_certificate + .validity + .not_after + .to_system_time() + < now + { + return Err(IssuerError::SignerExpired(config.name.clone())); + } + + let ca_der = ca_cert + .to_der() + .map_err(|error| IssuerError::Certificate(error.to_string()))?; + let fingerprint = sha256_hex(&ca_der); + let name_der = ca_cert + .tbs_certificate + .subject + .to_der() + .map_err(|error| IssuerError::Certificate(error.to_string()))?; + let key_bytes = ca_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + let mut name_hashes = HashMap::new(); + let mut key_hashes = HashMap::new(); + for (oid, hash_name, hash_key) in [ + ("1.3.14.3.2.26", hash_sha1(&name_der), hash_sha1(key_bytes)), + ( + "2.16.840.1.101.3.4.2.1", + hash_sha256(&name_der), + hash_sha256(key_bytes), + ), + ] { + name_hashes.insert(oid.to_string(), hash_name); + key_hashes.insert(oid.to_string(), hash_key); + } + + Ok(Arc::new(Self { + name: config.name.clone(), + signer_cert, + signer: Mutex::new(signer), + fingerprint, + name_hashes, + key_hashes, + cache: SnapshotCache::new(), + })) + } + + pub fn matches(&self, cert_id: &CertId) -> bool { + let algorithm = cert_id.hash_algorithm.oid.to_string(); + let Some(expected_name) = self.name_hashes.get(&algorithm) else { + return false; + }; + let Some(expected_key) = self.key_hashes.get(&algorithm) else { + return false; + }; + cert_id.issuer_name_hash.as_bytes() == expected_name.as_slice() + && cert_id.issuer_key_hash.as_bytes() == expected_key.as_slice() + } + + pub async fn snapshot( + &self, + store: &S, + config: &crate::config::S3Config, + ) -> Result, crate::cache::CacheError> { + let key = config.object_key(&self.fingerprint); + self.cache + .get( + store, + &key, + &self.fingerprint, + Duration::from_millis(config.cache_ttl_ms), + Duration::from_secs(config.stale_grace_seconds), + ) + .await + } +} + +fn check_ocsp_signing_eku(cert: &Certificate) -> Result<(), IssuerError> { + let extensions = match &cert.tbs_certificate.extensions { + Some(extensions) => extensions, + None => return Err(IssuerError::MissingEku), + }; + for extension in extensions { + if extension.extn_id == ID_CE_EXT_KEY_USAGE { + let eku = ExtendedKeyUsage::from_der(extension.extn_value.as_bytes()) + .map_err(|error| IssuerError::Certificate(error.to_string()))?; + if eku.0.contains(&ID_KP_OCSP_SIGNING) { + return Ok(()); + } + } + } + Err(IssuerError::MissingEku) +} + +#[derive(Debug, thiserror::Error)] +pub enum IssuerError { + #[error("signer error: {0}")] + Signer(#[from] SignerError), + #[error("invalid CA certificate: {0}")] + Certificate(String), + #[error("signer certificate for {0} is not issued by its CA")] + SignerIssuer(String), + #[error("signer certificate for {0} is not currently valid")] + SignerExpired(String), + #[error("signer key does not match its certificate for {0}")] + KeyMismatch(String), + #[error("signer certificate has no OCSPSigning extended key usage")] + MissingEku, +} diff --git a/ocsp-responder/src/main.rs b/ocsp-responder/src/main.rs new file mode 100644 index 0000000..29b70d1 --- /dev/null +++ b/ocsp-responder/src/main.rs @@ -0,0 +1,91 @@ +//! Standalone KCA OCSP responder. + +mod cache; +mod config; +mod issuer; +mod ocsp; +mod signer; +mod snapshot; +mod store; + +use std::sync::Arc; + +use axum::extract::DefaultBodyLimit; +use axum::routing::{get, post}; +use axum::Router; +use tower_http::timeout::TimeoutLayer; +use tracing_subscriber::EnvFilter; + +use crate::issuer::Issuer; +use crate::ocsp::AppState; +use crate::store::StatusStore; + +#[tokio::main] +async fn main() -> Result<(), Box> { + if std::env::args().any(|argument| argument == "--healthcheck") { + return healthcheck(); + } + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let config = config::load_config()?; + let store = StatusStore::new(&config.s3).await; + let mut issuers = Vec::with_capacity(config.issuers.len()); + for issuer_config in &config.issuers { + let issuer = Issuer::load(issuer_config)?; + tracing::info!( + issuer = %issuer.name, + fingerprint = %issuer.fingerprint, + "loaded OCSP issuer" + ); + issuers.push(issuer); + } + let state = AppState { + issuers, + store, + config: Arc::new(config.clone()), + }; + + let app = Router::new() + .route("/ocsp", post(ocsp::ocsp_post)) + .route("/ocsp/{request}", get(ocsp::ocsp_get)) + .route("/healthz", get(health)) + .route("/readyz", get(health)) + .layer(DefaultBodyLimit::max(16 * 1024)) + .layer(TimeoutLayer::with_status_code( + axum::http::StatusCode::REQUEST_TIMEOUT, + std::time::Duration::from_secs(15), + )) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(config.listen).await?; + tracing::info!(listen = %config.listen, "OCSP responder listening"); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + Ok(()) +} + +async fn health() -> &'static str { + "ok" +} + +fn healthcheck() -> Result<(), Box> { + use std::io::{Read, Write}; + let mut stream = std::net::TcpStream::connect("127.0.0.1:2560")?; + stream.write_all(b"GET /healthz HTTP/1.0\r\nHost: localhost\r\n\r\n")?; + let mut response = String::new(); + stream.read_to_string(&mut response)?; + if response.starts_with("HTTP/1.") && response.contains(" 200 ") { + Ok(()) + } else { + Err("health endpoint did not return HTTP 200".into()) + } +} + +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; +} diff --git a/ocsp-responder/src/ocsp.rs b/ocsp-responder/src/ocsp.rs new file mode 100644 index 0000000..b6e1394 --- /dev/null +++ b/ocsp-responder/src/ocsp.rs @@ -0,0 +1,151 @@ +//! OCSP request handling. + +use std::sync::Arc; +use std::time::{Duration, SystemTime}; + +use axum::body::Bytes; +use axum::extract::{Path, State}; +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; +use base64::Engine; +use der::{Decode, Encode}; +use x509_cert::ext::pkix::CrlReason; +use x509_ocsp::builder::OcspResponseBuilder; +use x509_ocsp::{ + CertStatus, OcspGeneralizedTime, OcspRequest, OcspResponse, RevokedInfo, SingleResponse, +}; + +use crate::config::Config; +use crate::issuer::Issuer; +use crate::signer::{now_time, system_time}; +use crate::store::StatusStore; + +#[derive(Clone)] +pub struct AppState { + pub issuers: Vec>, + pub store: StatusStore, + pub config: Arc, +} + +pub async fn ocsp_post(State(state): State, body: Bytes) -> Response { + handle_request(&state, &body).await +} + +pub async fn ocsp_get(State(state): State, Path(encoded): Path) -> Response { + let decoded = base64::engine::general_purpose::STANDARD + .decode(encoded.as_bytes()) + .or_else(|_| base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes())); + match decoded { + Ok(data) => handle_request(&state, &data).await, + Err(_) => ocsp_response(OcspResponse::malformed_request()), + } +} + +async fn handle_request(state: &AppState, body: &[u8]) -> Response { + let request = match OcspRequest::from_der(body) { + Ok(request) => request, + Err(_) => return ocsp_response(OcspResponse::malformed_request()), + }; + if request.tbs_request.request_list.is_empty() { + return ocsp_response(OcspResponse::malformed_request()); + } + + let first_id = &request.tbs_request.request_list[0].req_cert; + let issuer = match state.issuers.iter().find(|issuer| issuer.matches(first_id)) { + Some(issuer) => issuer, + None => return ocsp_response(OcspResponse::unauthorized()), + }; + // One BasicOCSPResponse has one signer, so all CertIDs must belong to + // the same CA. + if request + .tbs_request + .request_list + .iter() + .any(|item| !issuer.matches(&item.req_cert)) + { + return ocsp_response(OcspResponse::unauthorized()); + } + + let snapshot = match issuer.snapshot(&state.store, &state.config.s3).await { + Ok(snapshot) => snapshot, + Err(error) => { + tracing::warn!(issuer = %issuer.name, error = %error, "OCSP snapshot unavailable"); + return ocsp_response(OcspResponse::try_later()); + } + }; + + let now = match now_time() { + Ok(now) => now, + Err(error) => { + tracing::error!(error = %error, "could not create OCSP time"); + return ocsp_response(OcspResponse::internal_error()); + } + }; + let next_update = now_time() + .ok() + .and_then(|_| { + SystemTime::now().checked_add(Duration::from_secs(state.config.next_update_seconds)) + }) + .and_then(|value| OcspGeneralizedTime::try_from(value).ok()); + + let mut builder = OcspResponseBuilder::new(issuer.signer_cert.tbs_certificate.subject.clone()); + for item in &request.tbs_request.request_list { + let serial = item.req_cert.serial_number.as_bytes(); + let status = match snapshot.status_for_serial(serial) { + Some(entry) if entry.status == crate::snapshot::Status::Revoked => { + let revocation_time = match entry.revoked_at { + Some(value) => match OcspGeneralizedTime::try_from(system_time(value)) { + Ok(value) => value, + Err(_) => return ocsp_response(OcspResponse::internal_error()), + }, + None => return ocsp_response(OcspResponse::internal_error()), + }; + CertStatus::revoked(RevokedInfo { + revocation_time, + revocation_reason: Some(entry.reason.unwrap_or(CrlReason::Unspecified)), + }) + } + Some(_) => CertStatus::good(), + None => CertStatus::unknown(), + }; + let mut single = SingleResponse::new(item.req_cert.clone(), status, now); + if let Some(next_update) = next_update { + single = single.with_next_update(next_update); + } + builder = builder.with_single_response(single); + } + if let Some(nonce) = request.nonce() { + if let Ok(updated) = builder.clone().with_extension(nonce) { + builder = updated; + } else { + tracing::warn!("could not echo OCSP nonce"); + } + } + + let mut signer = issuer.signer.lock().await; + match signer.sign(builder, vec![issuer.signer_cert.clone()], now) { + Ok(response) => ocsp_response(response), + Err(error) => { + tracing::error!(issuer = %issuer.name, error = %error, "OCSP signing failed"); + ocsp_response(OcspResponse::internal_error()) + } + } +} + +fn ocsp_response(response: OcspResponse) -> Response { + match response.to_der() { + Ok(bytes) => ( + StatusCode::OK, + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/ocsp-response"), + )], + bytes, + ) + .into_response(), + Err(error) => { + tracing::error!(error = %error, "could not encode OCSP response"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } +} diff --git a/ocsp-responder/src/signer.rs b/ocsp-responder/src/signer.rs new file mode 100644 index 0000000..d515423 --- /dev/null +++ b/ocsp-responder/src/signer.rs @@ -0,0 +1,112 @@ +//! Delegated OCSP response signers. + +use std::fs; +use std::path::Path; +use std::time::SystemTime; + +use der::{Decode, DecodePem, Encode}; +use pkcs8::{DecodePrivateKey, DecodePublicKey}; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs1v15::SigningKey as RsaSigningKey; +use rsa::RsaPrivateKey; +use sha2::Sha256; +use x509_cert::Certificate; +use x509_ocsp::builder::OcspResponseBuilder; +use x509_ocsp::{OcspGeneralizedTime, OcspResponse}; + +pub enum SignerKey { + Rsa(Box>), + P256(p256::ecdsa::SigningKey), +} + +impl SignerKey { + pub fn load(path: &Path) -> Result { + let text = fs::read_to_string(path)?; + if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(&text) { + return Ok(Self::Rsa(Box::new(RsaSigningKey::::new(key)))); + } + if let Ok(key) = RsaPrivateKey::from_pkcs1_pem(&text) { + return Ok(Self::Rsa(Box::new(RsaSigningKey::::new(key)))); + } + if let Ok(secret) = p256::SecretKey::from_pkcs8_pem(&text) { + return Ok(Self::P256(p256::ecdsa::SigningKey::from(secret))); + } + if let Ok(secret) = p256::SecretKey::from_sec1_pem(&text) { + return Ok(Self::P256(p256::ecdsa::SigningKey::from(secret))); + } + Err(SignerError::KeyFormat) + } + + pub fn matches_certificate(&self, cert: &Certificate) -> Result { + let spki = cert + .tbs_certificate + .subject_public_key_info + .to_der() + .map_err(|error| SignerError::Certificate(error.to_string()))?; + match self { + Self::Rsa(key) => { + let public = rsa::RsaPublicKey::from_public_key_der(&spki) + .map_err(|error| SignerError::Certificate(error.to_string()))?; + Ok(key.as_ref().as_ref().to_public_key() == public) + } + Self::P256(key) => { + let public = p256::PublicKey::from_public_key_der(&spki) + .map_err(|error| SignerError::Certificate(error.to_string()))?; + Ok(key.verifying_key().to_sec1_bytes().as_ref() == public.to_sec1_bytes().as_ref()) + } + } + } + + pub fn sign( + &mut self, + builder: OcspResponseBuilder, + chain: Vec, + produced_at: OcspGeneralizedTime, + ) -> Result { + let response = match self { + Self::Rsa(key) => builder + .sign::<_, rsa::pkcs1v15::Signature>(key.as_mut(), Some(chain), produced_at) + .map_err(|error| SignerError::Sign(error.to_string()))?, + Self::P256(key) => builder + .sign::<_, ecdsa::der::Signature>(key, Some(chain), produced_at) + .map_err(|error| SignerError::Sign(error.to_string()))?, + }; + Ok(response) + } +} + +pub fn read_certificate(path: &Path) -> Result { + let data = fs::read(path)?; + if let Ok(cert) = Certificate::from_pem(&data) { + return Ok(cert); + } + Certificate::from_der(&data).map_err(|error| SignerError::Certificate(error.to_string())) +} + +pub fn now_time() -> Result { + Ok(OcspGeneralizedTime::try_from(SystemTime::now())?) +} + +pub fn system_time(value: chrono::DateTime) -> SystemTime { + if value.timestamp() >= 0 { + SystemTime::UNIX_EPOCH + + std::time::Duration::new(value.timestamp() as u64, value.timestamp_subsec_nanos()) + } else { + SystemTime::UNIX_EPOCH + - std::time::Duration::new((-value.timestamp()) as u64, value.timestamp_subsec_nanos()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SignerError { + #[error("could not read signer file: {0}")] + Io(#[from] std::io::Error), + #[error("unsupported signer private-key format")] + KeyFormat, + #[error("invalid signer certificate: {0}")] + Certificate(String), + #[error("OCSP signing failed: {0}")] + Sign(String), + #[error("time conversion failed: {0}")] + Time(#[from] der::Error), +} diff --git a/ocsp-responder/src/snapshot.rs b/ocsp-responder/src/snapshot.rs new file mode 100644 index 0000000..9222960 --- /dev/null +++ b/ocsp-responder/src/snapshot.rs @@ -0,0 +1,218 @@ +//! Parsed KCA OCSP status snapshots. + +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use sha1::{Digest as _, Sha1}; +use sha2::Sha256; +use x509_cert::ext::pkix::CrlReason; + +pub const SCHEMA: &str = "kca-ocsp-status/v1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + Good, + Revoked, +} + +#[derive(Debug, Clone)] +pub struct StatusEntry { + pub status: Status, + pub revoked_at: Option>, + pub reason: Option, +} + +#[derive(Debug, Clone)] +pub struct Snapshot { + pub entries: HashMap, StatusEntry>, +} + +#[derive(Debug, Deserialize)] +struct RawSnapshot { + schema: String, + issuer: RawIssuer, + #[serde(default)] + entries: Vec, +} + +#[derive(Debug, Deserialize)] +struct RawIssuer { + #[serde(default)] + sha256: String, +} + +#[derive(Debug, Deserialize)] +struct RawEntry { + serial_hex: String, + status: String, + #[serde(default)] + revoked_at: Option>, + #[serde(default)] + reason: Option, +} + +impl Snapshot { + /// Parse and validate a snapshot against the configured CA fingerprint. + pub fn parse(bytes: &[u8], expected_sha256: &str) -> Result { + let raw: RawSnapshot = serde_json::from_slice(bytes)?; + if raw.schema != SCHEMA { + return Err(SnapshotError::Schema(raw.schema)); + } + if !raw.issuer.sha256.eq_ignore_ascii_case(expected_sha256) { + return Err(SnapshotError::IssuerMismatch { + expected: expected_sha256.to_string(), + actual: raw.issuer.sha256, + }); + } + let mut entries = HashMap::with_capacity(raw.entries.len()); + for entry in raw.entries { + let serial = parse_serial(&entry.serial_hex)?; + let status = match entry.status.as_str() { + "valid" => Status::Good, + "revoked" => Status::Revoked, + other => return Err(SnapshotError::Status(other.to_string())), + }; + if status == Status::Revoked && entry.revoked_at.is_none() { + return Err(SnapshotError::MissingRevocationTime(entry.serial_hex)); + } + let reason = if status == Status::Revoked { + Some(parse_reason( + entry.reason.as_deref().unwrap_or("unspecified"), + )) + } else { + None + }; + if entries + .insert( + serial, + StatusEntry { + status, + revoked_at: entry.revoked_at, + reason, + }, + ) + .is_some() + { + return Err(SnapshotError::DuplicateSerial(entry.serial_hex)); + } + } + let _ = raw.issuer.sha256; + Ok(Self { entries }) + } + + pub fn status_for_serial(&self, serial: &[u8]) -> Option<&StatusEntry> { + self.entries.get(&normalize_serial(serial)) + } +} + +pub fn normalize_serial(serial: &[u8]) -> Vec { + let first = serial + .iter() + .position(|byte| *byte != 0) + .unwrap_or(serial.len()); + serial[first..].to_vec() +} + +fn parse_serial(value: &str) -> Result, SnapshotError> { + let text = value.trim(); + let text = text.strip_prefix("0x").unwrap_or(text); + let decoded = hex::decode(text).map_err(|_| SnapshotError::Serial(value.to_string()))?; + Ok(normalize_serial(&decoded)) +} + +fn parse_reason(value: &str) -> CrlReason { + match value.to_ascii_lowercase().as_str() { + "keycompromise" => CrlReason::KeyCompromise, + "cacompromise" => CrlReason::CaCompromise, + "affiliationchanged" => CrlReason::AffiliationChanged, + "superseded" => CrlReason::Superseded, + "cessationofoperation" => CrlReason::CessationOfOperation, + "certificatehold" => CrlReason::CertificateHold, + "removefromcrl" => CrlReason::RemoveFromCRL, + "privilegewithdrawn" => CrlReason::PrivilegeWithdrawn, + "aacompromise" => CrlReason::AaCompromise, + _ => CrlReason::Unspecified, + } +} + +pub fn sha256_hex(bytes: &[u8]) -> String { + use sha2::Digest as _; + hex::encode(Sha256::digest(bytes)) +} + +/// OCSP CertID hashes are SHA-1 in the common case, with SHA-256 as an +/// alternative. The responder supports both. +pub fn hash_sha1(bytes: &[u8]) -> Vec { + Sha1::digest(bytes).to_vec() +} + +pub fn hash_sha256(bytes: &[u8]) -> Vec { + use sha2::Digest as _; + Sha256::digest(bytes).to_vec() +} + +#[derive(Debug, thiserror::Error)] +pub enum SnapshotError { + #[error("invalid JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("unsupported snapshot schema {0:?}")] + Schema(String), + #[error("snapshot issuer {actual} does not match configured CA {expected}")] + IssuerMismatch { expected: String, actual: String }, + #[error("invalid serial {0:?}")] + Serial(String), + #[error("unknown status {0:?}")] + Status(String), + #[error("revoked entry {0:?} has no revocation time")] + MissingRevocationTime(String), + #[error("duplicate serial {0:?}")] + DuplicateSerial(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + fn body(sha: &str) -> String { + format!( + r#"{{ + "schema": "kca-ocsp-status/v1", + "issuer": {{"id": "ca", "sha256": "{sha}", "subject": "CN=CA"}}, + "generated_at": "2026-01-01T00:00:00Z", + "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"}} + ] + }}"# + ) + } + + #[test] + fn parses_statuses_and_reasons() { + let sha = sha256_hex(b"ca"); + let snapshot = Snapshot::parse(body(&sha).as_bytes(), &sha).unwrap(); + let good = snapshot.status_for_serial(&[0x01, 0xab]).unwrap(); + assert_eq!(good.status, Status::Good); + let revoked = snapshot.status_for_serial(&[0xff]).unwrap(); + assert_eq!(revoked.status, Status::Revoked); + assert_eq!(revoked.reason, Some(CrlReason::KeyCompromise)); + assert!(revoked.revoked_at.is_some()); + } + + #[test] + fn rejects_wrong_issuer_schema_and_duplicates() { + let sha = sha256_hex(b"ca"); + assert!(Snapshot::parse(body(&sha).as_bytes(), "wrong").is_err()); + let wrong_schema = body(&sha).replace(SCHEMA, "other"); + assert!(Snapshot::parse(wrong_schema.as_bytes(), &sha).is_err()); + let duplicate = body(&sha).replace("\"00ff\"", "\"01ab\""); + assert!(Snapshot::parse(duplicate.as_bytes(), &sha).is_err()); + } + + #[test] + fn normalizes_serials() { + assert_eq!(normalize_serial(&[0, 0, 1, 2]), vec![1, 2]); + assert_eq!(normalize_serial(&[0, 0]), Vec::::new()); + } +} diff --git a/ocsp-responder/src/store.rs b/ocsp-responder/src/store.rs new file mode 100644 index 0000000..42ff1e9 --- /dev/null +++ b/ocsp-responder/src/store.rs @@ -0,0 +1,64 @@ +//! S3-compatible object store access. + +use async_trait::async_trait; +use aws_sdk_s3::Client; +use bytes::Bytes; + +use crate::config::S3Config; + +#[derive(Clone)] +pub struct StatusStore { + client: Client, + bucket: String, +} + +impl StatusStore { + pub async fn new(config: &S3Config) -> Self { + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); + if let Some(region) = config.region.as_ref() { + loader = loader.region(aws_config::Region::new(region.clone())); + } + let shared = loader.load().await; + let mut builder = aws_sdk_s3::config::Builder::from(&shared); + if let Some(endpoint) = config.endpoint.as_ref() { + builder = builder.endpoint_url(endpoint); + } + builder = builder.force_path_style(config.force_path_style); + Self { + client: Client::from_conf(builder.build()), + bucket: config.bucket.clone(), + } + } +} + +#[async_trait] +pub trait ObjectStore: Send + Sync { + async fn get(&self, key: &str) -> Result; +} + +#[async_trait] +impl ObjectStore for StatusStore { + async fn get(&self, key: &str) -> Result { + let response = self + .client + .get_object() + .bucket(&self.bucket) + .key(key) + .send() + .await + .map_err(|error| StoreError::S3(error.to_string()))?; + let data = response + .body + .collect() + .await + .map_err(|error| StoreError::S3(error.to_string()))? + .into_bytes(); + Ok(data) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum StoreError { + #[error("S3 GetObject failed: {0}")] + S3(String), +} diff --git a/server/.env.example b/server/.env.example index 2700e09..99c0b9f 100644 --- a/server/.env.example +++ b/server/.env.example @@ -51,6 +51,18 @@ OTEL_EXCLUDED_PATHS=/api/health #OTEL_METRIC_EXPORT_INTERVAL_MILLIS=60000 #OTEL_METRICS_POLL_INTERVAL_SECONDS=15 +# --- OCSP status publisher (optional) -------------------------------------- +# When a bucket is configured, KCA publishes one full JSON status snapshot +# per CA. The standalone OCSP responder reads these objects. Uses the normal +# AWS credential chain (AWS_ACCESS_KEY_ID / +# AWS_SECRET_ACCESS_KEY, role, instance profile, ...). +#KCA_OCSP_S3_BUCKET=kca-ocsp +KCA_OCSP_S3_PREFIX=ocsp +#KCA_OCSP_S3_REGION=eu-central-1 +#KCA_OCSP_S3_ENDPOINT=http://localhost:9000 +KCA_OCSP_S3_FORCE_PATH_STYLE=false +KCA_OCSP_PUBLISH_INTERVAL_SECONDS=1 + # --- CORS (optional) ------------------------------------------------------- #CORS_ALLOW_ORIGINS= #CORS_ALLOW_ORIGIN_REGEX= diff --git a/server/Dockerfile b/server/Dockerfile index ccfdbab..492475c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -46,11 +46,11 @@ COPY server/uv.lock server/pyproject.toml ./ COPY server/packages/kca-core/pyproject.toml packages/kca-core/ COPY server/packages/kca-app/pyproject.toml packages/kca-app/ RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen --no-install-workspace --package kca-app --extra otel --no-dev + uv sync --frozen --no-install-workspace --package kca-app --extra otel --extra s3 --no-dev COPY server/packages/ ./packages/ RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-editable --package kca-app --extra otel --no-dev \ + uv sync --locked --no-editable --package kca-app --extra otel --extra s3 --no-dev \ --reinstall-package kca-core --reinstall-package kca-app COPY server/migrations ./migrations diff --git a/server/migrations/models/3_20261023083400_ocsp_snapshot.py b/server/migrations/models/3_20261023083400_ocsp_snapshot.py new file mode 100644 index 0000000..6601216 --- /dev/null +++ b/server/migrations/models/3_20261023083400_ocsp_snapshot.py @@ -0,0 +1,97 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "ocsp_snapshot" ( + "ca_id" UUID NOT NULL PRIMARY KEY, + "dirty" BOOL NOT NULL, + "published_at" TIMESTAMPTZ, + "last_error" TEXT, + "updated_at" TIMESTAMPTZ NOT NULL +); +CREATE INDEX IF NOT EXISTS "idx_ocsp_snapsh_dirty_a8d440" ON "ocsp_snapshot" ("dirty"); +COMMENT ON TABLE "ocsp_snapshot" IS 'Publication state for one CA''s OCSP status JSON object.';""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + DROP TABLE IF EXISTS "ocsp_snapshot";""" + + +MODELS_STATE = ( + "eJztXf9zmzgW/1c0vh+uO5ftpU6cZndubsZJs7e5tnGncfe6u7PDyCDbXDCwCNJ69/q/n4" + "TBgBAYYeMAfb/0i9BHxp8nC73Pe3r8OVg5BrHo87FrTp0HYg++R38ObLwi7B+5aydogF03" + "ucIbfDyzws7YNTV/221GfQ/rPrswxxYlrMkgVPdM1zcd/iGDd8Sjjo0thHWdUIpCKJo7Hn" + "I9Z+Hh1Qr7po7G726jHs/5uIajs4FNe1F/iMA2fw8Iu9UF8ZfEYwP9+htrNm2DfCY0/q/7" + "oM1NYhkZPkyDDxC2a/7aDds+fLh99UPYk9/eTNMdK1jZSW937S8de9s9CEzjOcfwawtiEw" + "/7xEjRZAeWFVEaN23umDX4XkC2t2okDQaZ48DiZA/+MQ9snXOMwk/if5z/M7q1VDdNu5tM" + "tfubqaYNcrbhtyBwHTXpjs3tato+J+rPL5txE0LC1gH/gOsfx++fnV18E1LgUH/hhRdDug" + "ZfQiD28QYakp6wHFDiaTSY5bm+XmJPznUaIzDObrgO13FDCdkxTw0wO1jhz5pF7IW/ZP8d" + "jkYlVP80fh+yzXqFdDvsh7f5Sd5Fl4aba5z2hObwbwWK4/5N0ZusE0fn98XpaQV+Wa9Cfs" + "NrWX7D1UhzPTI3P6vwLOL6yPdFFbovitm+kJO9xHSpTnWMOgzRu9foo9F8cV6B5ovzQpr5" + "pSzNukc4FRr28zS/Yld8c0XkVGeRAtVGBH0e/6OLM5x9QWNiW+vI5CXUT2/f3txPx2/f8Y" + "9bUfq7FfI3nt7wK8OwdS20PhN/DdtB0H9upz8i/l/0y+TuRnzgbvtNfxnwe8KB72i280nD" + "Rmp2xq0xaxmrW5j6GnvC1rG7iD2A5aNbbovhO2LomJNSS5PPrsmGK7Sz3MZZVJmFe2Vdbj" + "G2PnJvYf6Q2snyhhnWHz5hz9ByV5yhU9Q3f2k1XIkt2MaLkAZ+v/zuYictYLR7pr8eyDy4" + "7cWTUhcu022nC3c9/pa6RDfnzMliHpdLPN8k9AQ5NkGe8wmxBnQ9RjpvZ32YzfM+XM0xwI" + "lruRPnk5VrMVa0B7JW2pIJuFqbspatIc3vyixtswar78rSSHg2t/nZzG1lB6sZ8fJWvrX9" + "YgMnIMHAjKWmNtqnTf2UFvwOvh2+OH95fnl2cX7JuoR3uW15WWLz27up5Kdj4DVVpDSGHI" + "/Qs64w6ltsI8j2EfKF/8q0sbcuWPoFpEDubO0T2qsl6OrN5Cqz+rA1/ur2bvz+52dvxx+/" + "ySxBbyZ3/4r7p1eWq5+nN+MSE7DlxNYlz4WqhkjwYI6a5gDRpHjx6a9oErhGTatnkWD1tl" + "hdskuL7l7i9qh5mQJsD3ezO8vtTucytYIm3r8isXnkcV35VlKbU6zy8zfP8Q+OR8yF/Zps" + "Ng237F6wfGcQaU7T1FCdnLRJa3KLHv60lZTEXy2jgH1x4ocksHtAdx/evBl8UZEDpVM+b4" + "uJTaYO+6OiJa6zY3Vxlu+0Rf6HnjHH9fj+evzqRrDGXoJrmlWJ5CqQXiy6CpbeLbuObfTx" + "+ej0u7Qoikw2GfPqanlXiYj66+AxtF845ewF8Vw2lK/RJR6OLga/CRprqnf47dnlVJNJac" + "BGzTbajq/huc/bQaF9UoUW8j+S/JpK+R/DkvyPYT7/Q3dWK2JLdv9T8rlIWkogx2N58ATT" + "dnrzcZrZ0cdMyj3tuHuK+WvufGcJp07gyaSO4imdII5IdzLokeZ2paldMrPFiU2JZ2JLWx" + "KltKYsqn85ei+Gl1WSmoaXxVlN/JpAdTD7L9F9du8S0b9MwhOANbW7tq3aTyDebTYx6gbI" + "4oD/uvzHE1k1tU/E9W/BOXwcOZqzqlQLMGB6N9PcC5qRuaMesc8ieygOd1cMzscAEme3hp" + "W3QDByy428SaHBIQn5TYLjWATbRXYWsOJGgYGbMu+25cgbhcnkTca0V7eib/fh7dUN2yqH" + "NmWdzI2El081MKmmY0XGt5jGmM49p7pPtIv9Zfw4zNFdmCgjoGrlyrQsMnDw9CPi1XDu0i" + "jwLOp6Fm4ws0w9zHFRtkAeC3aoawdqLmzsBx7RsLXg2efLlZKjJ4f3QD89whnFdIxnZYxU" + "eJdAe8f52bAC5WfDQsb5pWLC6RK/qMt4jO0d5edVZvl58SQ/L53jURyzPukRGjSO3RrHI/" + "Go1CMq3DCmEMdLrH7Rka0iJdjTl5pPPiuFFAVYD9aLY4cVIX23eI53M5ET0ne/RqtXSd+N" + "QhmKCUhpEKTuZldP7iKr0ZkggMsslx5hX4b6inxmUcCpsEvlN6XIaBpzyLzDzhJaklCeSi" + "fdK5v8p3icbpK6M385PafkmcvCqnoATl9vRunkz34nocljpCAvP//cPwClaqn2HaU2s+Gp" + "wG70BDoAve+TkfpJbfZhrXyiJOW1Lk3L8IgsIB0hf3j9nlhFgec+T2dhbj46esiCpAZATa" + "reb8fs0QNLLJuwL11elXB7V2aUUmGnhMZMvSU5l/FRr92MZmo89YjXekeyUr/B8sNZ2R9r" + "pWNampfF7D6xhTbZ74gDH4iBZmuEeVmrZx/ZH9Rnj0GKUisRcmzkLwnr8Y3kTNeeg0lPfW" + "1yYlI5+uJRLzis9aSHteDMxdHOXEQ/qxrKbxbZQ+W3u0pvXuA37UdsmQZ7XmpxZUoVW0vg" + "UD6tzfb2CKayaHTx6pkgjhg1DeyoHOaxzgeOqiRcjIoTLkb5g69QqK6JQnUQhe5XPLJKFF" + "rHqvVuMMQlKsUlZAcHGlZ7W8rr7tIquHpFlaOVu+b6kcyx38hKJW581KGCz56umZL408gy" + "qV9UbqUaprzuCn8Shmcp61VbAUcdqqpAVRWoqpJmEqqqtL+qSlhWWvkkWAKCA0ji/K5en9" + "dSrnmQxvRPV23mFQVPoQm0TFA7tCqwebuSW0fAFKA91AU6ogNUUi9ttrupaWgBCip1m+0c" + "h5B0J5BtcguXyhwOFNTcASo4Z1wpptrEOWM4vAaH19q2Pehx2AAOr/XN6pLNAxxeg8NBXS" + "AUDgftIvXAh4PgMIsKt1UOszxNiJMfz5KEOKNTW8UhzuhwWJUQ56aOFXI85HrmIw9aMnBh" + "cHNH7/KwZqpkVq23SXBgOD8hwgkRTohwQoQTIpwdi3BuV3AFptOYXi4gTZBMzT8kJBdqx2" + "kIBNkkqbeB96g0abeA/r28vhF1ONoa1q3CCgkQh6uEq14gUQqGlIjdKRGRD1Vj2meB8F7u" + "utM+4rHGG9JzUDBCTSMsMdUiNiUmKKvkLyDhzQkKBf2hNutBd4gQ3obwdtv2kR0NdEJ4+2" + "u0epXwNgRjIRjbhoDh7mBspfpsSWz1cIXHOutx7KrRFhbF25On3tUOVKs6phh8nujUvbex" + "S5eOP5BEoTPXT8rC0ez7uRpNd90ZmH4Xakqbo7NsufEJmjsecmxexOqvFE2u79+F7QFF/7" + "6f3CEnfNdrPmS9xziSYHbViljtOLT/dQSjDdOTVs8rEw62mOO9AjBu6KxgEKq8dFlr0y1i" + "4QRKe3bZeecqPBdGPM+RqNLFUkYW1Ye417GlDHBqC03RM6c254M9TXphvB+WbO5SW+XifV" + "2qqneVVMN3r6/v//LiNFMhhZ+pYoZH0VCFiYdK2PI0xLjAdq0cRP6ZjErIQIQMxJbkY0AG" + "Yh+e/ZCBeJzkOBqEDr56yoUAhFQjcZ5XDfezx686+ykQMF+XeXjdeTvsIGxAFdb8PBLS7H" + "an2UV79vx8L5MIExCkFSmohBvaakgYGSDog+0RLPL6IPbZd50F0rAlD6LIDZxFiRY2dR/9" + "LyyS2tjeNXF7Z4Fp+aZNn/PPewrPl7NU7kKI3oJgRj6A6EJARt8AMvp64ApDRl/xHO+mDA" + "4ZfV+j1atk9MG7wXessFCqBrIj20UqvMe6de+xhmzTdmdSTsnKtXBotVygfXvtpCzS7qd7" + "7Q612+jj9fhb6q8tgmLo9ygKYaC/IeacEJu7fn+PX2GHIqapJPi+72i7EylzMXcIr0N4Hc" + "LrEF5PMwnh9c6E11V04RQEROGDi8KU+OxLLZSU+jSmHSbhH9sbk4BOPwCdvgfPVNDpi+d4" + "NxVb0Om/RqtX0elBWwZtuQ1S6GFO3rOZvuTv7zH3lULH0UA9Ep8bFUI/0FD+y4mgYftJmQ" + "DKrmhB3GunADqxCZrcvrpGpkFsnwuS/hL7aIkpspwFuzVk2og1WARTH/EKbnnZs+YYe5wa" + "B23zGNomDWZKOtCm+2H8g90MH0/XHI2qiD+jUbH6w69lXQLDpK6F15qqfiziQEeupiMfVa" + "dv2QPsCNN5jlempT6bBRiQXYnshflIbGWusyiguhLVK9MwLKLMtQADsiuRbZv6g/I6ncIA" + "zZVodj0yJ55HjNBNUCVcjgbqK1LvzE1Lke8tpIckn55fVtrrnV+WbPb4RYFnU+dvH1biOY" + "EAz1V5/kRm1JQVIy/mOQUBnqvyzDiRnkMu2e1tEf1juZH3qsxMz18a0sr6xSxnQP0j+mxY" + "geezYSHN/FKW5T8cm5j23FEhOY3pH8eNTGbL0bHaNiNBAMWVKC6L7l6Zi8IMldLYbpdfIf" + "bdcHh29nJ4enZxOTp/+XJ0ebrNV8lfKktcubr9F89dyZhC8qIxyKnoWXS9Sk5FWDqTEmLX" + "sLuIBcu3xfJFeRW5NIDikHWTsdifIiPlgrHbHILiaOw2XaFaKDZ+V/TH6zHicbkZpgQ5n2" + "xioNkaYXsTZ+XKizwMq4iHEOzBfnrNhGC56TxNMRCbAfWv7FEz2jOc42k0/pq+sxzNxbnH" + "Agxyj9XP88CxBjjW0OtjDeAJFlPdTX8Asuu/RqsfwAt8+iIObZsLhcnL2RXU2pckr7FKn0" + "9NzgNZ70lOtZIrnSTn2G+b6yRJcSWQPVlK1z/pC01NynZj4pn6ciDR7aIrJ6XHKJI+u5S7" + "YhoOrK8V+gdSeU3iGkQbq/Ym3h/ENSiW0wo9r2Khp9j16oXW04SWxn9UCgxH3XvIbiOBZ/" + "aJvrQiTrGek4IcX85p/rHSqHDzpDGoL/8Hu5mclQ==" +) diff --git a/server/packages/kca-app/pyproject.toml b/server/packages/kca-app/pyproject.toml index 41b8535..860a2c8 100644 --- a/server/packages/kca-app/pyproject.toml +++ b/server/packages/kca-app/pyproject.toml @@ -31,6 +31,9 @@ dependencies = [ ] [project.optional-dependencies] +s3 = [ + "boto3>=1.34", +] otel = [ "kaya-otel>=0.0.7", "opentelemetry-instrumentation-asyncpg>=0.66b0", diff --git a/server/packages/kca-app/src/kca/app.py b/server/packages/kca-app/src/kca/app.py index 00a42cd..106e062 100644 --- a/server/packages/kca-app/src/kca/app.py +++ b/server/packages/kca-app/src/kca/app.py @@ -20,6 +20,7 @@ from redis.asyncio import Redis from .config import Settings, settings from .logging_config import configure_logging from .mixin import Kca, KcaMixin +from .ocsp import publisher_mixin_from_settings from .openapi import SCHEMAS, SECURITY_SCHEMES, KcaOpenAPI from .telemetry import ( CountableSessionStore, @@ -143,6 +144,9 @@ mixins: list[KayaMixin] = [ openapi_mixin, KcaMixin(kca), ] +ocsp_publisher = publisher_mixin_from_settings(settings, tortoise_mixin.ensure_context) +if ocsp_publisher is not None: + mixins.append(ocsp_publisher) telemetry = telemetry_from_settings(settings, session_store) if telemetry is not None: diff --git a/server/packages/kca-app/src/kca/config.py b/server/packages/kca-app/src/kca/config.py index de77f03..7849176 100644 --- a/server/packages/kca-app/src/kca/config.py +++ b/server/packages/kca-app/src/kca/config.py @@ -185,6 +185,12 @@ class Settings: otel_excluded_paths: Tuple[str, ...] otel_metric_export_interval_millis: int otel_metrics_poll_interval_seconds: int + ocsp_s3_bucket: Optional[str] + ocsp_s3_prefix: str + ocsp_s3_region: Optional[str] + ocsp_s3_endpoint: Optional[str] + ocsp_s3_force_path_style: bool + ocsp_publish_interval_seconds: int @staticmethod def from_env() -> "Settings": @@ -231,6 +237,14 @@ class Settings: otel_metrics_poll_interval_seconds=int( _env("OTEL_METRICS_POLL_INTERVAL_SECONDS", "15") ), + ocsp_s3_bucket=os.environ.get("KCA_OCSP_S3_BUCKET") or None, + ocsp_s3_prefix=_env("KCA_OCSP_S3_PREFIX", "ocsp").strip("/"), + ocsp_s3_region=os.environ.get("KCA_OCSP_S3_REGION") or None, + ocsp_s3_endpoint=os.environ.get("KCA_OCSP_S3_ENDPOINT") or None, + ocsp_s3_force_path_style=_env_bool("KCA_OCSP_S3_FORCE_PATH_STYLE"), + ocsp_publish_interval_seconds=max( + 1, int(_env("KCA_OCSP_PUBLISH_INTERVAL_SECONDS", "1")) + ), ) diff --git a/server/packages/kca-app/src/kca/models.py b/server/packages/kca-app/src/kca/models.py index bdb37f0..9d1debd 100644 --- a/server/packages/kca-app/src/kca/models.py +++ b/server/packages/kca-app/src/kca/models.py @@ -298,6 +298,19 @@ class Crl(Model): return f"Crl(name={self.name!r}, number={self.crl_number})" +class OcspSnapshot(Model): + """Publication state for one CA's OCSP status JSON object.""" + + ca_id = fields.UUIDField(pk=True) + dirty = fields.BooleanField(default=True, db_index=True) + published_at = fields.DatetimeField(null=True) + last_error = fields.TextField(null=True) + updated_at = fields.DatetimeField(auto_now=True) + + class Meta: + table = "ocsp_snapshot" + + class Template(Model): """An XCA-style template: subject + extension/validity defaults.""" diff --git a/server/packages/kca-app/src/kca/ocsp.py b/server/packages/kca-app/src/kca/ocsp.py new file mode 100644 index 0000000..9e2a43f --- /dev/null +++ b/server/packages/kca-app/src/kca/ocsp.py @@ -0,0 +1,258 @@ +"""Per-CA OCSP status snapshots and S3 publication. + +KCA is the source of truth for certificate status. This module builds a +compact JSON document per CA and uploads it to an S3-compatible bucket so +that 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, timezone +from typing import Any, Awaitable, Callable, Dict, Optional +from uuid import UUID + +from kaya.core import KayaApp, KayaMixin + +from .config import Settings, settings +from .models import Certificate, CertificateRevocation, OcspSnapshot, Vault + +log = logging.getLogger(__name__) + +SCHEMA = "kca-ocsp-status/v1" + + +def _iso(value: Optional[datetime]) -> Optional[str]: + if value is None: + return None + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + return value.astimezone(timezone.utc).isoformat() + + +def object_key(ca_fingerprint_sha256: str) -> str: + """The S3 key for one CA; the responder derives the same key.""" + prefix = settings.ocsp_s3_prefix.strip("/") + name = f"{ca_fingerprint_sha256.lower()}.json" + return f"{prefix}/{name}" if prefix else name + + +async def snapshot_for_ca(ca: Certificate) -> Dict[str, Any]: + """Build the full OCSP status snapshot for certificates issued by ``ca``.""" + certs = await Certificate.filter(vault_id=ca.vault_id, issuer_id=ca.id).order_by("serial_hex") # type: ignore[attr-defined] + revocations = { + row.serial_hex.lower(): row + for row in await CertificateRevocation.filter(ca_id=ca.id) + } + entries = [] + for cert in certs: + revocation = revocations.get(cert.serial_hex.lower()) + entry: Dict[str, Any] = { + "serial_hex": cert.serial_hex.lower(), + "status": "revoked" if revocation is not None else "valid", + "not_after": _iso(cert.not_after), + } + if revocation is not None: + entry.update( + { + "revoked_at": _iso(revocation.revoked_at), + "reason": revocation.reason, + "invalidity_date": _iso(revocation.invalidity_date), + } + ) + entries.append(entry) + return { + "schema": SCHEMA, + "issuer": { + "id": str(ca.id), + "sha256": ca.fingerprint_sha256.lower(), + "subject": cert_subject(ca), + }, + "generated_at": _iso(datetime.now(timezone.utc)), + "entries": entries, + } + + +def cert_subject(ca: Certificate) -> str: + from kca.core import certificates as cert_core + + return cert_core.parse_certificate(bytes(ca.cert_der)).subject.rfc4514_string() + + +def _s3_client() -> Any: + import boto3 + from botocore.config import Config + + kwargs: Dict[str, Any] = { + "region_name": settings.ocsp_s3_region, + "endpoint_url": settings.ocsp_s3_endpoint, + } + if settings.ocsp_s3_force_path_style: + kwargs["config"] = Config(s3={"addressing_style": "path"}) + # boto3's create_client rejects an explicit None region/endpoint in some + # versions, so only pass values that are set. + kwargs = {key: value for key, value in kwargs.items() if value is not None} + return boto3.client("s3", **kwargs) + + +async def publish_snapshot(ca: Certificate, snapshot: Optional[Dict[str, Any]] = None) -> str: + """Upload one CA snapshot and return its S3 key.""" + if not settings.ocsp_s3_bucket: + raise RuntimeError("KCA_OCSP_S3_BUCKET is not configured") + if snapshot is None: + snapshot = await snapshot_for_ca(ca) + body = json.dumps(snapshot, indent=2, sort_keys=True).encode("utf-8") + key = object_key(ca.fingerprint_sha256) + + # boto3 is synchronous and credential resolution can block; do both the + # client creation and the PUT outside the event loop. + def upload() -> None: + client = _s3_client() + client.put_object( + Bucket=settings.ocsp_s3_bucket, + Key=key, + Body=body, + ContentType="application/json", + CacheControl="no-store", + ) + + await asyncio.to_thread(upload) + return key + + +async def mark_ocsp_dirty(ca_id: UUID) -> None: + """Mark one issuing CA's status object as needing a republish.""" + if not settings.ocsp_s3_bucket: + return + row = await OcspSnapshot.get_or_none(ca_id=ca_id) + if row is None: + await OcspSnapshot.create(ca_id=ca_id, dirty=True) + return + if not row.dirty: + row.dirty = True + row.last_error = None # type: ignore[assignment] + await row.save(update_fields=["dirty", "last_error", "updated_at"]) + + +async def mark_all_cas_dirty(vault: Vault) -> None: + if not settings.ocsp_s3_bucket: + return + for ca in await Certificate.filter(vault_id=vault.id, is_ca=True): + await mark_ocsp_dirty(ca.id) + + +class OcspPublisherMixin(KayaMixin): + """Background publisher for dirty OCSP snapshots.""" + + def __init__( + self, + interval_seconds: int, + ensure_context: Callable[[], Awaitable[None]], + ) -> None: + self._interval = max(1, interval_seconds) + self._ensure_context = ensure_context + self._task: Optional[asyncio.Task] = None + self._bootstrapped = False + + def apply(self, app: KayaApp) -> None: + # This mixin only owns a background task; no request hooks. + return None + + def setup(self, loop: asyncio.AbstractEventLoop) -> None: + if not settings.ocsp_s3_bucket: + log.info("OCSP S3 publication disabled (KCA_OCSP_S3_BUCKET unset)") + return + try: + import boto3 # noqa: F401 + except ImportError as exc: + raise RuntimeError( + "KCA_OCSP_S3_BUCKET is set but boto3 is not installed; " + "install kca-app with the 's3' extra" + ) from exc + self._task = loop.create_task(self._poll()) + log.info( + "OCSP S3 publication enabled: bucket=%s prefix=%s interval=%ss", + settings.ocsp_s3_bucket, + settings.ocsp_s3_prefix or "(root)", + self._interval, + ) + + def shutdown(self, loop: asyncio.AbstractEventLoop) -> None: + if self._task is not None: + self._task.cancel() + self._task = None + + async def _poll(self) -> None: + # Let the Tortoise mixin establish its database context before the + # first query. Subsequent failures are retried on the next tick. + await asyncio.sleep(self._interval) + while True: + try: + if not self._bootstrapped: + # Existing CAs may predate the publisher or have lost + # their S3 objects; publish one snapshot for every CA on + # the first successful tick. + await self._ensure_context() + for ca in await Certificate.filter(is_ca=True): + await mark_ocsp_dirty(ca.id) + self._bootstrapped = True + await self.run_once() + except asyncio.CancelledError: + raise + except Exception: + log.exception("OCSP snapshot publisher iteration failed") + await asyncio.sleep(self._interval) + + async def run_once(self) -> None: + # The publisher runs outside an HTTP request task, so it must bind + # Tortoise's per-loop context before querying models. + await self._ensure_context() + rows = await OcspSnapshot.filter(dirty=True).limit(25) + for row in rows: + ca = await Certificate.get_or_none(id=row.ca_id) + if ca is None: + row.dirty = False + row.last_error = "CA certificate not found" + await row.save(update_fields=["dirty", "last_error", "updated_at"]) + continue + try: + snapshot = await snapshot_for_ca(ca) + key = await publish_snapshot(ca, snapshot) + row.dirty = False + row.published_at = datetime.now(timezone.utc) + row.last_error = None # type: ignore[assignment] + await row.save( + update_fields=["dirty", "published_at", "last_error", "updated_at"] + ) + log.info("published OCSP snapshot ca=%s key=%s", ca.id, key) + except Exception as exc: # noqa: BLE001 - keep retrying the same row + row.dirty = True + row.last_error = str(exc)[:2000] + await row.save(update_fields=["dirty", "last_error", "updated_at"]) + log.warning("OCSP snapshot publish failed ca=%s: %s", ca.id, exc) + + +def publisher_mixin_from_settings( + settings_obj: Settings, + ensure_context: Callable[[], Awaitable[None]], +) -> Optional[OcspPublisherMixin]: + if not settings_obj.ocsp_s3_bucket: + return None + return OcspPublisherMixin( + settings_obj.ocsp_publish_interval_seconds, + ensure_context, + ) + + +__all__ = [ + "OcspPublisherMixin", + "SCHEMA", + "mark_all_cas_dirty", + "mark_ocsp_dirty", + "object_key", + "publish_snapshot", + "publisher_mixin_from_settings", + "snapshot_for_ca", +] diff --git a/server/packages/kca-app/src/kca/routes/certificates.py b/server/packages/kca-app/src/kca/routes/certificates.py index 5512257..b9948f4 100644 --- a/server/packages/kca-app/src/kca/routes/certificates.py +++ b/server/packages/kca-app/src/kca/routes/certificates.py @@ -19,7 +19,8 @@ from kca.core.crypto import EncryptedBlob, decrypt_blob, encrypt_blob from ..auth import authenticated_user, require_auth from ..config import settings from ..http import JsonRequestError, read_json, send_error, send_json -from ..models import Authority, Certificate, CertificateRevocation, Crl, Key, Request, Template +from ..models import Authority, Certificate, CertificateRevocation, Crl, Key, OcspSnapshot, Request, Template +from ..ocsp import snapshot_for_ca from ..openapi import error_response, json_response, schema_ref from ..service import ( ValidationError, @@ -136,6 +137,32 @@ def register(app: KayaApp, kca: "Kca") -> None: return await send_json(ctx, 200, result) + @app.GET("/api/vaults/${vault_id}/certificates/${cert_id}/ocsp-status") + @operation(summary="OCSP status snapshot for a CA", tags=["certificates"]) + @require_auth(kca.oidc) + async def ocsp_status(ctx: HttpContext, vault_id: str, cert_id: str) -> None: + vault = await _vault(kca, ctx, vault_id) + if vault is None: + return + cert = await _cert(vault, cert_id) + if cert is None: + await send_error(ctx, 404, "certificate not found") + return + if not cert.is_ca: + await send_error(ctx, 400, "the certificate is not a CA") + return + row = await OcspSnapshot.get_or_none(ca_id=cert.id) + snapshot = await snapshot_for_ca(cert) + await send_json(ctx, 200, { + "enabled": bool(settings.ocsp_s3_bucket), + "bucket": settings.ocsp_s3_bucket, + "prefix": settings.ocsp_s3_prefix, + "dirty": row.dirty if row else bool(settings.ocsp_s3_bucket), + "published_at": row.published_at.isoformat() if row and row.published_at else None, + "last_error": row.last_error if row else None, + "snapshot": snapshot, + }) + @app.GET("/api/vaults/${vault_id}/certificates/${cert_id}") @operation(summary="Certificate detail", tags=["certificates"]) @require_auth(kca.oidc) diff --git a/server/packages/kca-app/src/kca/service.py b/server/packages/kca-app/src/kca/service.py index f3a6b4c..bad62f8 100644 --- a/server/packages/kca-app/src/kca/service.py +++ b/server/packages/kca-app/src/kca/service.py @@ -22,6 +22,7 @@ from kca.core.imports import ParsedItem from kca.core.names import DnEntry from .config import settings +from .ocsp import mark_all_cas_dirty, mark_ocsp_dirty from .models import ( ApiToken, Authority, @@ -807,14 +808,19 @@ async def issue_certificate( request_obj.signed_at = datetime.now(timezone.utc) await request_obj.save(update_fields=["signed", "signed_at"]) await relink_certificates(vault) + if signer is not None: + await mark_ocsp_dirty(signer.id) return row async def delete_certificate_cascade(vault: Vault, cert: Certificate) -> None: + issuer_id = cert.issuer_id await Certificate.filter(issuer_id=cert.id).update(issuer_id=None) await Crl.filter(issuer_id=cert.id).update(issuer_id=None) await CertificateRevocation.filter(ca_id=cert.id).delete() await cert.delete() + if issuer_id is not None: + await mark_ocsp_dirty(issuer_id) async def preview_certificate(vault: Vault, payload: Mapping[str, Any]) -> Dict[str, Any]: @@ -946,6 +952,8 @@ async def renew_certificate( if payload.get("replace_old"): await delete_certificate_cascade(vault, cert) await relink_certificates(vault) + if signer.id != cert.id: + await mark_ocsp_dirty(signer.id) return row @@ -973,6 +981,7 @@ async def revoke_certificate(vault: Vault, cert: Certificate, payload: Mapping[s invalidity_date=parse_datetime(invalidity, "invalidity_date") if invalidity else None, reason=reason, ) + await mark_ocsp_dirty(ca.id) return row @@ -1369,6 +1378,7 @@ async def import_parsed_items( "crl_number": int(crl.crl_number or 0), }, ) + await mark_all_cas_dirty(vault) return {"imported": imported, "skipped": skipped} diff --git a/server/tests/test_routes_extra.py b/server/tests/test_routes_extra.py index 6156a4c..386346d 100644 --- a/server/tests/test_routes_extra.py +++ b/server/tests/test_routes_extra.py @@ -401,3 +401,103 @@ class BulkRevocationTests(unittest.TestCase): json={"certificate_ids": ["00000000-0000-0000-0000-000000000000"], "reason": "superseded"}, ) assert response.status_code == 404 + + +class OcspSnapshotTests(unittest.TestCase): + @async_test + async def test_snapshot_contains_status_and_reason(self) -> None: + async with api_client() as client: + with oidc_user("alice"): + vault_id = await _vault(client) + ca_key = await _key(client, vault_id, "ocsp ca") + ca = await _cert(client, vault_id, ca_key["id"], "OCSP CA", ca=True) + leaf_key = await _key(client, vault_id, "ocsp leaf") + leaf = await _cert(client, vault_id, leaf_key["id"], "OCSP Leaf", signer_id=ca["id"]) + await client.post( + f"/api/vaults/{vault_id}/certificates/{leaf['id']}/revoke", + json={"reason": "keyCompromise"}, + ) + response = await client.get( + f"/api/vaults/{vault_id}/certificates/{ca['id']}/ocsp-status" + ) + assert response.status_code == 200, response.text + payload = response.json() + assert payload["enabled"] is False + snapshot = payload["snapshot"] + assert snapshot["schema"] == "kca-ocsp-status/v1" + assert snapshot["issuer"]["sha256"] == ca["fingerprint_sha256"] + entries = {entry["serial_hex"]: entry for entry in snapshot["entries"]} + assert entries[leaf["serial_hex"]]["status"] == "revoked" + assert entries[leaf["serial_hex"]]["reason"] == "keyCompromise" + assert entries[leaf["serial_hex"]]["revoked_at"] + + @async_test + async def test_revocation_marks_snapshot_dirty(self) -> None: + from dataclasses import replace + from unittest.mock import patch + + from kca import ocsp + from kca.models import OcspSnapshot + + async with api_client() as client: + with oidc_user("alice"): + vault_id = await _vault(client) + ca_key = await _key(client, vault_id, "dirty ca") + ca = await _cert(client, vault_id, ca_key["id"], "Dirty CA", ca=True) + leaf_key = await _key(client, vault_id, "dirty leaf") + leaf = await _cert(client, vault_id, leaf_key["id"], "Dirty Leaf", signer_id=ca["id"]) + configured = replace(ocsp.settings, ocsp_s3_bucket="test-bucket") + with patch.object(ocsp, "settings", configured): + response = await client.post( + f"/api/vaults/{vault_id}/certificates/{leaf['id']}/revoke", + json={"reason": "superseded"}, + ) + assert response.status_code == 200, response.text + row = await OcspSnapshot.get(ca_id=ca["id"]) + assert row.dirty is True + + @async_test + async def test_publisher_marks_dirty_and_uploads(self) -> None: + from dataclasses import replace + from unittest.mock import patch + + from kca import ocsp + from kca.models import OcspSnapshot + + class FakeS3: + def __init__(self): + self.calls = [] + + def put_object(self, **kwargs): + self.calls.append(kwargs) + + fake = FakeS3() + async with api_client() as client: + with oidc_user("alice"): + vault_id = await _vault(client) + ca_key = await _key(client, vault_id, "ocsp publish ca") + ca = await _cert(client, vault_id, ca_key["id"], "OCSP Publish CA", ca=True) + configured = replace( + ocsp.settings, + ocsp_s3_bucket="test-bucket", + ocsp_s3_prefix="ocsp", + ) + with patch.object(ocsp, "settings", configured), patch.object( + ocsp, "_s3_client", lambda: fake + ): + await ocsp.mark_ocsp_dirty(ca["id"]) + row = await OcspSnapshot.get(ca_id=ca["id"]) + assert row.dirty is True + async def ensure_context(): + return None + + mixin = ocsp.OcspPublisherMixin(1, ensure_context) + await mixin.run_once() + assert len(fake.calls) == 1 + call = fake.calls[0] + assert call["Bucket"] == "test-bucket" + assert call["Key"] == f"ocsp/{ca['fingerprint_sha256']}.json" + assert b"kca-ocsp-status/v1" in call["Body"] + row = await OcspSnapshot.get(ca_id=ca["id"]) + assert row.dirty is False + assert row.published_at is not None diff --git a/server/uv.lock b/server/uv.lock index efae735..4f6ebef 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -309,6 +309,34 @@ 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 = "boto3" +version = "1.43.118" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/8b/5694c8433b5202f34a82def4edee274f7df1eba2919c66d9d389d2202ec8/boto3-1.43.118.tar.gz", hash = "sha256:c0620f799e0f0c222ba42c337b65cdbcd3da38a3121187f634d8926cbbb28a9c", size = 112649, upload-time = "2026-10-22T19:22:09.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/a3/65bb3d64928f3c2da17640b92044084150317f02adf22442cb0c52af2a8b/boto3-1.43.118-py3-none-any.whl", hash = "sha256:ab8f3d2c622ef039338b4f228d97ea55f1abd573d84bb9b9a5ee8cb6780c0ccd", size = 140045, upload-time = "2026-10-22T19:22:08.353Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.118" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/b8/3e55154ddaf1a6d1d4ddda85c0e5983205a3b7657edacd8ebba8c6fd774c/botocore-1.43.118.tar.gz", hash = "sha256:7f3f9d88cc9e847587690f82a42e3c3c292c3026c92c1e60d68a9310018e003f", size = 16420126, upload-time = "2026-10-22T19:22:04.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/ce/61b39a8e7a69af5ea43c1c3e1b0065a4866b42ccddf107e16686a3143614/botocore-1.43.118-py3-none-any.whl", hash = "sha256:a45d7ae570674161e4a3542d8c68633bf7503f92d1de781ef7361b79488eda6f", size = 16122016, upload-time = "2026-10-22T19:22:00.414Z" }, +] + [[package]] name = "certifi" version = "2026.10.19" @@ -876,6 +904,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "kaya-core" version = "0.0.8" @@ -1015,6 +1052,9 @@ otel = [ { name = "opentelemetry-instrumentation-redis" }, { name = "opentelemetry-instrumentation-system-metrics" }, ] +s3 = [ + { name = "boto3" }, +] [package.dev-dependencies] dev = [ @@ -1026,6 +1066,7 @@ dev = [ requires-dist = [ { name = "aerich" }, { name = "asyncpg" }, + { name = "boto3", marker = "extra == 's3'", specifier = ">=1.34" }, { name = "cryptography", specifier = ">=44" }, { name = "granian", specifier = ">=2.0" }, { name = "httpx" }, @@ -1048,7 +1089,7 @@ requires-dist = [ { name = "redis" }, { name = "tortoise-orm", specifier = ">=0.25" }, ] -provides-extras = ["otel"] +provides-extras = ["s3", "otel"] [package.metadata.requires-dev] dev = [ @@ -1635,6 +1676,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/b8/502910eb8b315f719d8f6a6509f13a38b6c4c05378f14ac151ff347bff0a/pypika_tortoise-0.6.5-py3-none-any.whl", hash = "sha256:9194ac6ce6ac9bdfc6e959c831c5788ef05ee1371e82ba281b0eb75f4a2bd4f1", size = 47936, upload-time = "2026-03-13T20:44:53.541Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1726,6 +1779,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/c5/0a5ce655109cb96b8dfad36908a4e108d15f5dc2eff8fbc3146c4312e704/s3transfer-0.19.3.tar.gz", hash = "sha256:8326acc3d056d7f7b8a37110c7d2aea41f67e83f6ae79cd3b5fbcca34c726406", size = 166087, upload-time = "2026-10-19T19:19:49.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a4/88f9e10498759121060b8050a1554753dfc04df39bbbcc7aad9ecc8d9440/s3transfer-0.19.3-py3-none-any.whl", hash = "sha256:24eb7535804306355e31c952a90022bf5b2f9d760f9a11062c7493a217c08553", size = 90252, upload-time = "2026-10-19T19:19:48.309Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "tomli" version = "2.5.0" diff --git a/web/src/api.rs b/web/src/api.rs index 48f6fb2..eae3d75 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -342,6 +342,14 @@ pub async fn ca_info(vault_id: &str, cert_id: &str) -> Result Result { + let response = Request::get(&format!("/api/vaults/{vault_id}/certificates/{cert_id}/ocsp-status")) + .send() + .await + .map_err(|e| e.to_string())?; + check(response).await +} + pub async fn update_ca(vault_id: &str, cert_id: &str, payload: &serde_json::Value) -> Result { let response = send("PATCH", &format!("/api/vaults/{vault_id}/certificates/{cert_id}/ca"), Some(payload)).await?; check(response).await