188 lines
8.4 KiB
Markdown
188 lines
8.4 KiB
Markdown
# ACME Mini‑CA – Documentation
|
||
|
||
This repository contains a **minimal‑viable ACME Certificate Authority** written in Go.\
|
||
It is *not* a full replacement for production CAs such as Boulder/PEBBLE, but it demonstrates every building‑block you need to issue X.509 certificates via [RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555):
|
||
|
||
- **ACME endpoints** (`newAccount`, `newOrder`, `challenge`, `finalize`, `certificate`, `revokeCert`)
|
||
- **Challenge types**: `http‑01` *(default)* and `dns‑01` *(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 `_acme‑challenge.` TXT records
|
||
|
||
---
|
||
|
||
## 1 Architecture
|
||
|
||
```text
|
||
┌─────────────┐ HTTP ┌──────────────┐
|
||
│ ACME Client│ ──────────────►│ ACME Server │
|
||
└─────────────┘ │ (this repo) │
|
||
├─── MySQL │
|
||
├─── OCSP / CRL │
|
||
└──────────────┘
|
||
▲
|
||
│ REST
|
||
┌───────┴────────┐
|
||
│ DNS Provider │ (optional)
|
||
└────────────────┘
|
||
```
|
||
|
||
*The server may sit behind a reverse‑proxy for TLS termination; however **`http‑01`** challenges are exposed directly by the embedded **`autocert.Manager`**.*
|
||
|
||
---
|
||
|
||
## 2 Build & Run
|
||
|
||
```bash
|
||
# 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 `dns‑01` challenges |
|
||
| `DNS_PROVIDER_URL` | *(empty)* | Base URL of your DNS provider’s 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):
|
||
|
||
```sql
|
||
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 JWS‑signed |
|
||
| `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:
|
||
- **http‑01**: `http://<DOMAIN>/.well-known/acme-challenge/{token}` = key‑auth
|
||
- **dns‑01**: 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 (**dns‑01** simply trusts provider) and sets *valid*.
|
||
6. Client uploads CSR via `finalize`, server issues certificate.
|
||
|
||
### 3.2 DNS Provider contract
|
||
|
||
```http
|
||
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 (non‑blocking).
|
||
|
||
---
|
||
|
||
## 4 OCSP & CRL
|
||
|
||
- **OCSP** ↠ `GET /ocsp/{serial}` – returns DER `application/ocsp-response`.\
|
||
Serial numbers are looked‑up in `certs.revoked_at`. Status =`Revoked` if timestamp present.
|
||
- **CRL** ↠ `GET /crl` – regenerates PEM‑CRL 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](https://datatracker.ietf.org/doc/html/rfc7515) JSON serialization.
|
||
|
||
- **New‑Account**: payload includes a `jwk` → key is saved under new `account.id`.
|
||
- **Subsequent requests**: use `kid` header pointing to `account` URL.
|
||
- Nonce‑reuse or invalid signatures return `4xx`.
|
||
|
||
---
|
||
|
||
## 6 Security & Hardening checklist
|
||
|
||
| Area | Recommendation |
|
||
| --------------- | ---------------------------------------------------------------------------------- |
|
||
| **CA key** | Store in HSM / KMS, *never* plaintext. |
|
||
| **TLS** | Run behind reverse‑proxy (Nginx, Traefik) or enable `tls.Config` on `http.Server`. |
|
||
| **Rate‑limits** | 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 smoke‑test with **Pebble** (Let’s Encrypt dev CA):
|
||
|
||
```bash
|
||
# run Pebble
|
||
$ docker run --rm -p 14000:14000 letsencrypt/pebble -config /test/config.json
|
||
|
||
# point Pebble at your Mini‑CA
|
||
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 key‑rollover for CA
|
||
- ACME‑v3 *STAR* certificates (short‑term re‑use)
|
||
- Proper background validator for `dns‑01` via `dig TXT` lookup
|
||
|
||
---
|
||
|
||
© 2025 — Released under the **Apache‑2.0** License
|
||
|