No description
  • Python 76.2%
  • HTML 23.2%
  • Dockerfile 0.6%
Find a file
Johan Selmosson afa5113e01 CA Version display, accurate CRL age%, and orphan reporting via findings
Three related improvements that surfaced from analyzing an AD CS site
(eskilstuna.se) carrying both V0.0 and V1.1 CA artefacts:

* CA Version (MS OID 1.3.6.1.4.1.311.21.1) extracted on certs and CRLs
  and rendered as "<key>.<cert>" alongside other identifiers. Lets
  operators distinguish prior-generation files at a glance — a CRL with
  a different CA Version than its issuing cert was signed by an older
  key, even when the subject DN matches.

* CRLInfo.age_percent computed server-side from full UTC datetimes,
  not from the date-floored this_update/next_update strings the UI used
  to recompute from. Short windows like hourly delta CRLs no longer
  collapse to 0% or 100% at the day boundary; the >90% threshold now
  fires accurately for sub-day "less than 10% left" cases instead of
  yellowing every short-window CRL indiscriminately. Required fixing a
  pre-existing _crl_next_publish bug that ignored UTCTime-encoded
  values (AD CS emits these for pre-2050 dates).

* Prior-generation CRLs now route to orphan_crls instead of attaching
  to a same-named different-key CA via DN fallback. Each orphan carries
  ca_version and a 'key_mismatch' / 'unknown_issuer' reason driving
  tailored remediation text. CRL_ISSUER_ABSENT findings are emitted
  from the orphan path too so they appear in the Issues panel — the
  previous orphan-only routing would have silently demoted them out
  of the at-a-glance findings count. Composes with eb26e0e's
  validate_keys mechanism, which still handles the no-AKI DN-fallback
  case attached under the CA.

AIA_STALE_RENEWAL text rewritten to include CA Version on both the
served and chain certs, explain why same-key reissuance still validates
today, and call out when AIA-built chains will break (the served
cert's expiry date).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:42:05 +02:00
templates CA Version display, accurate CRL age%, and orphan reporting via findings 2026-06-06 20:42:05 +02:00
tests Classify prior-generation CRLs as CRL_ISSUER_ABSENT, not a mismatch cascade 2026-05-31 22:46:37 +02:00
.dockerignore Rebuild as FastAPI web app with CLI, add CLAUDE.md and UI spacing 2026-05-18 09:29:55 +02:00
.gitignore Initial Checkin 2026-05-16 11:38:14 +02:00
.python-version Initial Checkin 2026-05-16 11:38:14 +02:00
analyzer.py CA Version display, accurate CRL age%, and orphan reporting via findings 2026-06-06 20:42:05 +02:00
app.py Docs + use logging in app.py for the diagnostic warnings 2026-05-26 19:24:13 +02:00
CLAUDE.md Docs + use logging in app.py for the diagnostic warnings 2026-05-26 19:24:13 +02:00
Dockerfile Ops: HEALTHCHECK, env-driven DB_PATH, XDG-located JSON cache, CLI flags 2026-05-26 18:17:21 +02:00
pki_check.py CA Version display, accurate CRL age%, and orphan reporting via findings 2026-06-06 20:42:05 +02:00
pyproject.toml Add CSP/security headers, tighten proxy trust, document policy 2026-05-26 07:56:03 +02:00
README.md Docs + use logging in app.py for the diagnostic warnings 2026-05-26 19:24:13 +02:00
scan_kommuner.py Ops: HEALTHCHECK, env-driven DB_PATH, XDG-located JSON cache, CLI flags 2026-05-26 18:17:21 +02:00
uv.lock Add CSP/security headers, tighten proxy trust, document policy 2026-05-26 07:56:03 +02:00

PKI Chain Analyzer

Analyzes certificate chains for public PKI infrastructure using AIA and CDP distribution points. Checks chain integrity, CRL health, and AIA reachability without requiring access to private keys or internal systems.

What it analyzes

Give it a PKI distribution point, an HTTPS server, or a certificate file and it will:

  • Walk the full CA hierarchy from leaf to root via AIA CA Issuers
  • Validate each certificate's expiry, key usage, and AKI/SKI chain linkage
  • Fetch and verify every CRL: reachability, expiry, AKI match, and signature
  • Verify AIA URLs serve the correct issuer certificate
  • Identify orphaned CRLs, end-entity certificates on CA distribution points, and stale renewals
  • Report all issues with severity, detail, and remediation guidance

Usage

Web UI

https://labb.selmosson.cloud/pki-analyzer/

Enter any of:

  • A PKI distribution point: http://pki.example.com/
  • An HTTPS server: https://example.com
  • A direct certificate URL: http://pki.example.com/ca.crt

Results are shareable via the Share button — the URL encodes the analyzed target.

CLI

uv run pki_check.py <url-or-path> [<url-or-path> ...]
uv run pki_check.py --safe <url>   # enable SSRF protection

Exits 0 if no errors, 1 if any error finding is detected. Suitable for monitoring scripts.

Running locally

uv run uvicorn app:app --host 0.0.0.0 --port 8000

Deployment

The Docker image targets linux/amd64. Build from macOS (arm64) using buildx:

docker buildx build --platform linux/amd64 --push -t git.selmosson.cloud/js/pki-analyzer:latest .

First-time buildx setup:

brew install docker-buildx
mkdir -p ~/.docker/cli-plugins
ln -sf $(brew --prefix)/opt/docker-buildx/bin/docker-buildx ~/.docker/cli-plugins/docker-buildx

Served at /pki-analyzer/ behind Caddy:

handle /pki-analyzer/* {
    uri strip_prefix /pki-analyzer
    reverse_proxy pki-analyzer:8000
}

Configuration

Web app environment variables:

Variable Default Description
FORWARDED_ALLOW_IPS 127.0.0.1 Upstreams Uvicorn trusts for X-Forwarded-For. Never set to * — the rate limiter keys on the forwarded IP, so a wildcard lets any client spoof it.
DB_PATH /data/pki_analyzer.db SQLite request log location.
LOG_PATH (unset) URL path for the log viewer, e.g. /audit-abc123. Omit to disable the endpoint entirely.
LOG_USER / LOG_PASSWORD (unset) HTTP Basic Auth for the log viewer. All three log-viewer variables must be set together.

CLI flags for scan_kommuner.py: --clear-cache (delete cache and exit), --no-cache (skip read+write), --cache-file PATH (override). Cache defaults to $XDG_CACHE_HOME/pki-analyzer/scan_kommuner.json.

Security

SSRF protection is enabled by default in the web app. It blocks requests to private IP ranges (RFC 1918, loopback, link-local) and single-label hostnames. The CLI requires --safe to enable it.

The analyzer also:

  • Pins resolved IPs across the entire fetch so DNS rebinding can't sneak a private address in after the safety check
  • Follows redirects manually, re-running the safety check on every hop
  • Strips userinfo (user:pass@host) before the URL reaches the analyzer or the request log
  • Sets Content-Security-Policy, X-Content-Type-Options, X-Frame-Options: DENY, and Referrer-Policy: no-referrer on every response

Certificate validation is intentionally disabled during TLS connections so broken or misconfigured chains can still be analyzed.

Tests

uv run pytest

Covers SSRF protection (localhost block, redirect bypass, DNS rebinding, directory-scrape allowlist), URL-credential stripping, analysis cancellation, AIA depth findings, the validity-display threshold, and security headers.