Add ACME (RFC 8555) server and Go client library

Implements full ACME protocol support in Metacrypt:

- internal/acme: core types, JWS verification (ES256/384/512 + RS256),
  nonce store, per-mount handler, all RFC 8555 protocol endpoints,
  HTTP-01 and DNS-01 challenge validation, EAB management
- internal/server/acme.go: management REST routes (EAB create, config,
  list accounts/orders) + ACME protocol route dispatch
- proto/metacrypt/v1/acme.proto: ACMEService (CreateEAB, SetConfig,
  ListAccounts, ListOrders) — protocol endpoints are HTTP-only per RFC
- clients/go: new Go module with MCIAS-auth bootstrap, ACME account
  registration, certificate issuance/renewal, HTTP-01 and DNS-01
  challenge providers
- .claude/launch.json: dev server configuration

EAB is required for all account creation; MCIAS-authenticated users
obtain a single-use KID + HMAC-SHA256 key via POST /v1/acme/{mount}/eab.
This commit is contained in:
2026-03-15 01:31:52 -07:00
parent aa9a378685
commit 167db48eb4
19 changed files with 2743 additions and 5 deletions

118
internal/acme/eab.go Normal file
View File

@@ -0,0 +1,118 @@
package acme
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
// CreateEAB generates a new EAB credential for the given MCIAS user.
// The credential is stored in the barrier and must be consumed on first use.
func (h *Handler) CreateEAB(ctx context.Context, mciasUsername string) (*EABCredential, error) {
kidBytes := make([]byte, 16)
if _, err := rand.Read(kidBytes); err != nil {
return nil, fmt.Errorf("acme: generate EAB kid: %w", err)
}
hmacKey := make([]byte, 32)
if _, err := rand.Read(hmacKey); err != nil {
return nil, fmt.Errorf("acme: generate EAB key: %w", err)
}
cred := &EABCredential{
KID: base64.RawURLEncoding.EncodeToString(kidBytes),
HMACKey: hmacKey,
Used: false,
CreatedBy: mciasUsername,
CreatedAt: time.Now(),
}
data, err := json.Marshal(cred)
if err != nil {
return nil, fmt.Errorf("acme: marshal EAB: %w", err)
}
path := h.barrierPrefix() + "eab/" + cred.KID + ".json"
if err := h.barrier.Put(ctx, path, data); err != nil {
return nil, fmt.Errorf("acme: store EAB: %w", err)
}
return cred, nil
}
// GetEAB retrieves an EAB credential by KID.
func (h *Handler) GetEAB(ctx context.Context, kid string) (*EABCredential, error) {
path := h.barrierPrefix() + "eab/" + kid + ".json"
data, err := h.barrier.Get(ctx, path)
if err != nil || data == nil {
return nil, fmt.Errorf("acme: EAB not found")
}
var cred EABCredential
if err := json.Unmarshal(data, &cred); err != nil {
return nil, fmt.Errorf("acme: unmarshal EAB: %w", err)
}
return &cred, nil
}
// MarkEABUsed marks an EAB credential as consumed so it cannot be reused.
func (h *Handler) MarkEABUsed(ctx context.Context, kid string) error {
cred, err := h.GetEAB(ctx, kid)
if err != nil {
return err
}
cred.Used = true
data, err := json.Marshal(cred)
if err != nil {
return fmt.Errorf("acme: marshal EAB: %w", err)
}
return h.barrier.Put(ctx, h.barrierPrefix()+"eab/"+kid+".json", data)
}
// ListAccounts returns all ACME accounts for this mount.
func (h *Handler) ListAccounts(ctx context.Context) ([]*Account, error) {
paths, err := h.barrier.List(ctx, h.barrierPrefix()+"accounts/")
if err != nil {
return nil, err
}
var accounts []*Account
for _, p := range paths {
if !strings.HasSuffix(p, ".json") {
continue
}
data, err := h.barrier.Get(ctx, h.barrierPrefix()+"accounts/"+p)
if err != nil || data == nil {
continue
}
var acc Account
if err := json.Unmarshal(data, &acc); err != nil {
continue
}
accounts = append(accounts, &acc)
}
return accounts, nil
}
// ListOrders returns all ACME orders for this mount.
func (h *Handler) ListOrders(ctx context.Context) ([]*Order, error) {
paths, err := h.barrier.List(ctx, h.barrierPrefix()+"orders/")
if err != nil {
return nil, err
}
var orders []*Order
for _, p := range paths {
if !strings.HasSuffix(p, ".json") {
continue
}
data, err := h.barrier.Get(ctx, h.barrierPrefix()+"orders/"+p)
if err != nil || data == nil {
continue
}
var order Order
if err := json.Unmarshal(data, &order); err != nil {
continue
}
orders = append(orders, &order)
}
return orders, nil
}