Files
goacme/README.md
2025-04-29 09:42:42 +02:00

188 lines
8.4 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](https://datatracker.ietf.org/doc/html/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
```text
┌─────────────┐ 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
```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 `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):
```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 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
```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 (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](https://datatracker.ietf.org/doc/html/rfc7515) 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):
```bash
# 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