2025-04-29 09:42:56 +02:00
2025-04-29 09:42:42 +02:00
2025-04-29 09:42:42 +02:00
2025-04-29 09:42:42 +02:00
2025-04-29 09:42:56 +02:00
2025-04-29 09:42:42 +02:00

ACME MiniCA Documentation

This repository contains a minimalviable ACME Certificate Authority written in Go.
It is not a full replacement for production CAs such as Boulder/PEBBLE, but it demonstrates every buildingblock you need to issue X.509 certificates via RFC8555:

  • ACME endpoints (newAccount, newOrder, challenge, finalize, certificate, revokeCert)
  • Challenge types: http01 (default) and dns01 (optional)
  • JWS request verification  supports both JWK and KID modes
  • OCSP responder and CRL generation (optional)
  • MySQL persistence for all entities (accounts, orders, nonces, certs …)
  • Pluggable DNS provider API to publish/remove _acmechallenge. TXT records

1  Architecture

┌─────────────┐      HTTP        ┌──────────────┐
│  ACME Client│  ──────────────►│  ACME Server │
└─────────────┘                 │  (this repo) │
                                ├─── MySQL      │
                                ├─── OCSP / CRL │
                                └──────────────┘
                                        ▲
                                        │ REST
                                ┌───────┴────────┐
                                │ DNS Provider   │ (optional)
                                └────────────────┘

The server may sit behind a reverseproxy for TLS termination; however http01 challenges are exposed directly by the embedded autocert.Manager.


2  Build & Run

# clone & build
$ go build -o acme-ca .

# set mandatory secrets
export CA_CERT_PATH="./ca_cert.pem"
export CA_KEY_PATH="./ca_key.pem"

# run with sensible defaults
$ ./acme-ca

2.1  Environment variables

Variable Default Description
PORT 8080 HTTP listener port
MYSQL_DSN root:root@tcp(localhost:3306)/acme?parseTime=true&multiStatements=true DSN in Go SQL format
ACME_ALLOWED_DOMAIN example.com Hostname whitelist for autocert HTTP handler
DNS01_ENABLED false When true the server issues additional dns01 challenges
DNS_PROVIDER_URL (empty) Base URL of your DNS providers TXT API
DNS_PROVIDER_TOKEN (empty) Bearer token used in Authorization header
OCSP_ENABLED false Expose GET /ocsp/{serial} endpoint
CRL_ENABLED false Expose GET /crl endpoint
JWS_VERIFY_ENABLED true Toggle signature verification (disable for local dev)

Note

   All optional modules can be enabled/disabled independently.

2.2  Database schema

init() automatically executes the following DDL (simplified):

nonces(id PK, created)
accounts(id PK, jwk JSON)
orders(id PK, payload JSON, status, finalize_url)
authzs(id PK, order_id FK, identifier, status, payload JSON)
challenges(id PK, authz_id FK, type, token, status, payload JSON)
certs(id PK, serial, der LONGBLOB, revoked_at)

Add indices as required (e.g. serial, token).


3  ACME API surface

Method / Path Purpose
HEAD /acme/new-nonce Issue fresh nonce (always)
POST /acme/new-account Register account  payload must be JWSsigned
POST /acme/new-order Create order → returns authz/finalize URLs
GET /acme/authz/{id} Poll authorization status
POST /acme/challenge/{id} Client signals that challenge token is ready
POST /acme/finalize/{order} Upload CSR (base64url DER)
GET /acme/certificate/{id} Retrieve PEM certificate
POST /acme/revoke-cert Revoke certificate (base64url DER)

3.1  Challenge flow

  1. Server → Client newOrder response contains an authz URL.
  2. Client → Server GET /acme/authz/{id} to fetch challenge details.
  3. Client publishes token:
    • http01: http://<DOMAIN>/.well-known/acme-challenge/{token} = keyauth
    • dns01: TXT _acme-challenge.<DOMAIN> = base64url(sha256(token))
    • When DNS01_ENABLED=true the server automatically calls DNS Provider API.
  4. Client → Server POST /acme/challenge/{id} signals readiness.
  5. Server validates (dns01 simply trusts provider) and sets valid.
  6. Client uploads CSR via finalize, server issues certificate.

3.2  DNS Provider contract

POST   <DNS_PROVIDER_URL>
DELETE <DNS_PROVIDER_URL>
Authorization: Bearer <DNS_PROVIDER_TOKEN>
Content-Type:  application/json

{
  "fqdn":  "_acme-challenge.example.com",
  "token": "<txt contents>"
}

Return 200 to acknowledge; all other codes are logged only (nonblocking).


4  OCSP & CRL

  • OCSP ↠ GET /ocsp/{serial} returns DER application/ocsp-response.
    Serial numbers are lookedup in certs.revoked_at. Status =Revoked if timestamp present.
  • CRL  ↠ GET /crl regenerates PEMCRL on each request.

Enable with the respective ENV flags.


5  JWS verification

When JWS_VERIFY_ENABLED=true every ACME POST body must be a JOSE JWS JSON serialization.

  • NewAccount: payload includes a jwk → key is saved under new account.id.
  • Subsequent requests: use kid header pointing to account URL.
  • Noncereuse or invalid signatures return 4xx.

6  Security & Hardening checklist

Area Recommendation
CA key Store in HSM / KMS, never plaintext.
TLS Run behind reverseproxy (Nginx, Traefik) or enable tls.Config on http.Server.
Ratelimits Use middleware (e.g. golang.org/x/time/rate).
Audit Stream structured logs to SIEM; DB triggers for cert changes.
Backup Full MySQL dumps; optionally S3 for issued certs.
Monitoring Export Prometheus metrics: issued / revoked / OCSP hits.

7  Testing

A quick smoketest with Pebble (Lets Encrypt dev CA):

# run Pebble
$ docker run --rm -p 14000:14000 letsencrypt/pebble -config /test/config.json

# point Pebble at your MiniCA
PEBBLE\_ALTERNATE\_ROOTS=
export PEBBLE\_V2\_CA="http://localhost:8080/acme"

# use `lego` client
$ lego -m you@example.com --pem --server http://localhost:8080/acme --accept-tos -d sub.stadt-hilden.de run

8  Future improvements

  • OCSP stapling & cache headers
  • PostgreSQL + GORM migrations
  • Automatic keyrollover for CA
  • ACMEv3 STAR certificates (shortterm reuse)
  • Proper background validator for dns01 via dig TXT lookup

© 2025 — Released under the Apache2.0 License

Description
No description provided
Readme 154 KiB
Languages
Go 98.9%
Dockerfile 1.1%