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:
130
internal/acme/types.go
Normal file
130
internal/acme/types.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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"
|
||||
)
|
||||
Reference in New Issue
Block a user