Files
metacrypt/internal/acme/types.go
Kyle Isom 167db48eb4 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.
2026-03-15 08:09:12 -07:00

131 lines
5.3 KiB
Go

package acme
import "time"
// Account represents an ACME account (RFC 8555 §7.1.2).
type Account struct {
ID string `json:"id"`
Status string `json:"status"` // "valid", "deactivated", "revoked"
Contact []string `json:"contact,omitempty"`
JWK []byte `json:"jwk"` // canonical JSON of account public key
CreatedAt time.Time `json:"created_at"`
MCIASUsername string `json:"mcias_username"` // MCIAS user who created via EAB
}
// EABCredential is an External Account Binding credential (RFC 8555 §7.3.4).
type EABCredential struct {
KID string `json:"kid"`
HMACKey []byte `json:"hmac_key"` // raw 32-byte secret
Used bool `json:"used"`
CreatedBy string `json:"created_by"` // MCIAS username
CreatedAt time.Time `json:"created_at"`
}
// Order represents an ACME certificate order (RFC 8555 §7.1.3).
type Order struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
Status string `json:"status"` // "pending","ready","processing","valid","invalid"
Identifiers []Identifier `json:"identifiers"`
AuthzIDs []string `json:"authz_ids"`
CertID string `json:"cert_id,omitempty"`
NotBefore *time.Time `json:"not_before,omitempty"`
NotAfter *time.Time `json:"not_after,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
IssuerName string `json:"issuer_name"` // which CA issuer to sign with
}
// Identifier is a domain name or IP address in an order.
type Identifier struct {
Type string `json:"type"` // "dns" or "ip"
Value string `json:"value"`
}
// Authorization represents an ACME authorization (RFC 8555 §7.1.4).
type Authorization struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
Status string `json:"status"` // "pending","valid","invalid","expired","deactivated","revoked"
Identifier Identifier `json:"identifier"`
ChallengeIDs []string `json:"challenge_ids"`
ExpiresAt time.Time `json:"expires_at"`
}
// Challenge represents an ACME challenge (RFC 8555 §8).
type Challenge struct {
ID string `json:"id"`
AuthzID string `json:"authz_id"`
Type string `json:"type"` // "http-01" or "dns-01"
Status string `json:"status"` // "pending","processing","valid","invalid"
Token string `json:"token"` // base64url, 43 chars (32 random bytes)
Error *ProblemDetail `json:"error,omitempty"`
ValidatedAt *time.Time `json:"validated_at,omitempty"`
}
// ProblemDetail is an RFC 7807 problem detail for ACME errors.
type ProblemDetail struct {
Type string `json:"type"`
Detail string `json:"detail"`
}
// IssuedCert stores the PEM and metadata for a certificate issued via ACME.
type IssuedCert struct {
ID string `json:"id"`
OrderID string `json:"order_id"`
AccountID string `json:"account_id"`
CertPEM string `json:"cert_pem"` // full chain PEM
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked"`
}
// ACMEConfig is per-mount ACME configuration stored in the barrier.
type ACMEConfig struct {
DefaultIssuer string `json:"default_issuer"` // CA issuer name to use for ACME certs
}
// Status constants.
const (
StatusValid = "valid"
StatusPending = "pending"
StatusProcessing = "processing"
StatusReady = "ready"
StatusInvalid = "invalid"
StatusDeactivated = "deactivated"
StatusRevoked = "revoked"
ChallengeHTTP01 = "http-01"
ChallengeDNS01 = "dns-01"
IdentifierDNS = "dns"
IdentifierIP = "ip"
)
// ACME problem type URIs (RFC 8555 §6.7).
const (
ProblemAccountDoesNotExist = "urn:ietf:params:acme:error:accountDoesNotExist"
ProblemAlreadyRevoked = "urn:ietf:params:acme:error:alreadyRevoked"
ProblemBadCSR = "urn:ietf:params:acme:error:badCSR"
ProblemBadNonce = "urn:ietf:params:acme:error:badNonce"
ProblemBadPublicKey = "urn:ietf:params:acme:error:badPublicKey"
ProblemBadRevocationReason = "urn:ietf:params:acme:error:badRevocationReason"
ProblemBadSignatureAlg = "urn:ietf:params:acme:error:badSignatureAlgorithm"
ProblemCAA = "urn:ietf:params:acme:error:caa"
ProblemConnection = "urn:ietf:params:acme:error:connection"
ProblemDNS = "urn:ietf:params:acme:error:dns"
ProblemExternalAccountRequired = "urn:ietf:params:acme:error:externalAccountRequired"
ProblemIncorrectResponse = "urn:ietf:params:acme:error:incorrectResponse"
ProblemInvalidContact = "urn:ietf:params:acme:error:invalidContact"
ProblemMalformed = "urn:ietf:params:acme:error:malformed"
ProblemOrderNotReady = "urn:ietf:params:acme:error:orderNotReady"
ProblemRateLimited = "urn:ietf:params:acme:error:rateLimited"
ProblemRejectedIdentifier = "urn:ietf:params:acme:error:rejectedIdentifier"
ProblemServerInternal = "urn:ietf:params:acme:error:serverInternal"
ProblemTLS = "urn:ietf:params:acme:error:tls"
ProblemUnauthorized = "urn:ietf:params:acme:error:unauthorized"
ProblemUnsupportedContact = "urn:ietf:params:acme:error:unsupportedContact"
ProblemUnsupportedIdentifier = "urn:ietf:params:acme:error:unsupportedIdentifier"
ProblemUserActionRequired = "urn:ietf:params:acme:error:userActionRequired"
)