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.
131 lines
5.3 KiB
Go
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"
|
|
)
|