Dynamic SSL Pinning
Key Registry Server

Go service that continuously tracks TLS certificate public keys for configured domains and serves them as cryptographically signed JSON — enabling pin rotation without releasing a new client version.

Go Docker Redis PostgreSQL BSD-3-Clause

Quick Start

1

Generate RSA signing key pair

Keys are written to build/package/etc/ssl-pinning/tls/ as prv.pem and pub.pem.

task generate-keys
2

Configure domains

Edit build/package/etc/ssl-pinning/config.yaml and list the FQDNs to track:

keys:
  - fqdn: google.com
  - fqdn: mail.google.com
    file: google.com.json      # group into one response
    domainName: mail.google.com

server:
  listen: 0.0.0.0:7500

storage:
  type: memory             # fs | memory | redis | postgres
3

Run with Docker Compose

Starts the server together with Valkey/Redis and PostgreSQL. RSA keys are auto-generated on first run if missing.

cd docker
docker compose up
Ports: server on :7500, metrics on :9090, Redis on :6379, PostgreSQL on :5432.
4

Query a key registry file

curl http://localhost:7500/api/v1/google.com.json

How It Works

One goroutine-worker per configured FQDN polls the live TLS certificate continuously.

#Step
1On startup, one goroutine-worker is launched per configured fqdn
2Each worker dials {fqdn}:443 every second, extracts the leaf certificate, computes SHA-256 of the public key (SPKI)
3Every dump_interval (default 5 s), all current keys are signed and flushed to the configured storage backend
4HTTP handler for GET /api/v1/{file} reads keys from storage, signs the payload, and returns JSON
5On SIGTERM/SIGINT — graceful shutdown of the HTTP server and all workers
Attack resistance — even if an attacker issues a "valid" certificate via CA mis-issuance, they cannot forge a cryptographically signed fingerprint list accepted by clients.

API

Method & PathDescription
GET /api/v1/{file}Signed JSON with current certificate pins. {file} defaults to {fqdn}.json.
GET /health/livenessLiveness probe — returns 200 when the process is running
GET /health/readinessReadiness probe — returns 200 when the service is ready to serve traffic
GET /health/startupStartup probe — returns 200 after initial key collection completes
GET /metricsPrometheus metrics exposed on port 9090

Response Format

Each GET /api/v1/{file} response contains a signed payload with one or more pinned keys.

{
  "payload": {
    "keys": [
      {
        "domainName": "www.google.com",
        "key":        "b8tZqtqfv0RVKfwivfzaXuFWFHkYP4ufBhb5esciCwo=",
        "fqdn":       "google.com",
        "expire":     4736419,
        "date":       "2026-03-24T23:39:22.964574+01:00"
      }
    ]
  },
  "signature": "BASE64_RSA_SHA512_SIGNATURE"
}
FieldDescription
domainNameHostname pattern for SPKI pinning. Defaults to *.{fqdn}. Supports wildcards.
keyBase64-encoded SHA-256 hash of the certificate's Subject Public Key Info (SPKI).
fqdnFully-qualified domain name the server dialed to extract the key.
expireSeconds until certificate expiry at the time of collection.
dateTimestamp when the key was collected.
signatureRSA PKCS#1 v1.5 + SHA-512 over the JCS-canonicalized JSON of the payload field.
Grouping: multiple FQDNs sharing the same file value are returned together in a single signed response.

Configuration

Config is loaded from config.yaml, environment variables (prefix SSL_PINNING_), and CLI flags — in increasing priority order.

Keys

KeyRequiredDescription
fqdnYesDomain to dial at port 443 for TLS certificate extraction
domainNameNoPin pattern returned to clients. Defaults to *.{fqdn}
fileNoResponse filename. Defaults to {fqdn}.json. Shared by multiple FQDNs to group them

Server

KeyDefaultDescription
server.listen127.0.0.1:7500HTTP listen address and port
server.read_timeout5sMaximum duration for reading a request
server.write_timeout5sMaximum duration before timing out a response write

TLS

KeyDefaultDescription
tls.dir{config-path}/tlsDirectory containing prv.pem and pub.pem
tls.dump_interval5sInterval for flushing signed keys to storage
tls.timeout5sTimeout for TLS dial operations

Environment variables

SSL_PINNING_SERVER_LISTEN=0.0.0.0:7500
SSL_PINNING_STORAGE_TYPE=postgres
SSL_PINNING_STORAGE_DSN="postgres://user:pass@localhost:5432/db?sslmode=disable"
SSL_PINNING_TLS_DIR=/opt/ssl-pinning/tls
SSL_PINNING_TLS_DUMP_INTERVAL=30s
SSL_PINNING_LOG_LEVEL=debug
Keys via env: the SSL_PINNING_KEYS variable also accepts a JSON array — useful for container environments where a config file is impractical.

Storage Backends

TypeDSNNotes
memoryEphemeral in-process store. Best for development and testing.
fsWrites signed JSON files to dump_dir. Survives restarts.
redisredis://:password@host:6379/0Shared store for multiple replicas. Key format: file:fqdn:appID.
postgrespostgres://user:pass@host/dbDurable relational storage. Migrations run automatically on startup.

Cryptography

PropertyValue
Signing algorithmRSA PKCS#1 v1.5
Hash functionSHA-512 (signature) · SHA-256 (SPKI pin)
CanonicalizationJSON Canonicalization Scheme (JCS, RFC 8785)
Key sizeRSA-4096
Private key formatPKCS#8 PEM (tls/prv.pem)
Public key formatPEM (tls/pub.pem) — distribute to clients for signature verification
Pin typeSPKI SHA-256 (leaf certificate only)

Prometheus Metrics

MetricTypeLabelsDescription
ssl_pinning_errorsGaugefileNumber of current errors per response file
ssl_pinning_expireGaugekey, fqdnSeconds until certificate expiry per tracked key
Port: metrics are exposed on :9090 separately from the API server.