This commit is contained in:
2025-04-29 09:42:42 +02:00
parent cdba7f493e
commit 1af95afed5
5 changed files with 398 additions and 1 deletions

187
README.md
View File

@@ -1,2 +1,187 @@
# goacme
# 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