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.
72 lines
1.6 KiB
Go
72 lines
1.6 KiB
Go
package acme
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const nonceLifetime = 10 * time.Minute
|
|
|
|
// NonceStore is a thread-safe single-use nonce store with expiry.
|
|
// Nonces are short-lived per RFC 8555 §7.2.
|
|
type NonceStore struct {
|
|
mu sync.Mutex
|
|
nonces map[string]time.Time
|
|
issued int
|
|
}
|
|
|
|
// NewNonceStore creates a new nonce store.
|
|
func NewNonceStore() *NonceStore {
|
|
return &NonceStore{
|
|
nonces: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
// Issue generates, stores, and returns a new base64url-encoded nonce.
|
|
func (s *NonceStore) Issue() (string, error) {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
nonce := base64.RawURLEncoding.EncodeToString(b)
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.nonces[nonce] = time.Now().Add(nonceLifetime)
|
|
s.issued++
|
|
// Purge expired nonces every 100 issues to bound memory.
|
|
if s.issued%100 == 0 {
|
|
s.purgeExpiredLocked()
|
|
}
|
|
return nonce, nil
|
|
}
|
|
|
|
// Consume validates that the nonce exists and has not expired, then removes it.
|
|
// Returns an error if the nonce is unknown, expired, or already consumed.
|
|
func (s *NonceStore) Consume(nonce string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
exp, ok := s.nonces[nonce]
|
|
if !ok {
|
|
return errors.New("unknown or already-consumed nonce")
|
|
}
|
|
delete(s.nonces, nonce)
|
|
if time.Now().After(exp) {
|
|
return errors.New("nonce expired")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// purgeExpiredLocked removes all expired nonces. Caller must hold s.mu.
|
|
func (s *NonceStore) purgeExpiredLocked() {
|
|
now := time.Now()
|
|
for n, exp := range s.nonces {
|
|
if now.After(exp) {
|
|
delete(s.nonces, n)
|
|
}
|
|
}
|
|
}
|