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:
118
internal/acme/eab.go
Normal file
118
internal/acme/eab.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user