Documentation
¶
Overview ¶
Package acmeserver provides the ACME protocol layer for embedding an ACME server into CA and PKI applications. Certificate issuance, persistence and policy decisions are supplied by the host application through interfaces, so the package depends on no particular CA, database or deployment.
Embedding ¶
New validates a Config and returns a Server that implements http.Handler. Mount it at the path of Config.BaseURL behind the HTTPS origin that URL names, because signed request URLs must match it exactly. Run processes the persisted validation and issuance work and must run in this or another process on the same Store, otherwise challenges are never validated and orders are never issued. Ready reports work that has waited too long for a worker. The package example shows a complete host.
Host interfaces ¶
A Store persists accounts, orders, authorizations, challenges, certificates and background tasks. Its operations are atomic and guarded by resource revisions and task fences, which is what lets several workers share one store. The memstore package is the in-memory implementation for tests and examples and the storetest package checks a host adapter against the contract.
An Issuer signs certificates and a Revoker revokes them. Both receive a durable OperationID that is stored before the first call and repeated on every retry, so the host CA deduplicates. An Issuer must enforce IssueRequest.Deadline and RecoveryOnly. A chain that fails publication checks is kept in Order.UnpublishedResult for host reconciliation.
A Validator checks one challenge type. The challenge package implements HTTP-01, DNS-01, TLS-ALPN-01 and tkauth-01. A GrantingValidator reports what a proof authorizes beyond control of the identifier, which tkauth-01 uses for CA certificates and validity bounds.
Policy reviews new accounts and orders, IssuancePolicy reviews the first issuance dispatch and ExternalAccountKeys verifies external account bindings. A RefusalRecorder learns about the finalize and revokeCert requests the server refuses itself.
Extensions ¶
RFC 8738 IP identifiers, RFC 9448 TNAuthList identifiers with the RFC 9447 tkauth-01 challenge and RFC 9773 renewal information are off until the matching Config field enables them.
Documentation ¶
Step-by-step guides for hosts are at https://misiektoja.github.io/go-acme-server/.
Example ¶
Shows how a host mounts the handler and runs the worker. Both are required.
package main
import (
"context"
"errors"
"log"
"net/http"
"net/netip"
"os"
"os/signal"
acmeserver "github.com/misiektoja/go-acme-server"
"github.com/misiektoja/go-acme-server/challenge"
"github.com/misiektoja/go-acme-server/memstore"
"github.com/misiektoja/go-acme-server/nonce"
)
// A placeholder for the host CA. Real hosts sign with their own CA and deduplicate by OperationID.
type exampleCA struct{}
// Refuses every request in this example.
func (exampleCA) Issue(context.Context, acmeserver.IssueRequest) (acmeserver.IssueResult, error) {
return acmeserver.IssueResult{Rejected: acmeserver.NewProblem(acmeserver.ErrorServerInternal, "not implemented")}, nil
}
// Refuses every request in this example.
func (exampleCA) Revoke(context.Context, acmeserver.RevokeRequest) error {
return errors.New("not implemented")
}
// Shows how a host mounts the handler and runs the worker. Both are required.
func main() {
resolver, err := challenge.NewResolver(challenge.ResolverOptions{
Servers: []netip.AddrPort{netip.MustParseAddrPort("192.0.2.53:53")},
})
if err != nil {
log.Fatal(err)
}
http01, err := challenge.NewHTTP01(challenge.HTTPOptions{
Network: challenge.NetworkOptions{Resolver: resolver},
})
if err != nil {
log.Fatal(err)
}
srv, err := acmeserver.New(acmeserver.Config{
BaseURL: "https://ca.example.com/acme/",
Store: memstore.New(),
Nonces: nonce.New(nonce.Options{}),
Issuer: exampleCA{},
Revoker: exampleCA{},
Validators: map[acmeserver.ChallengeType]acmeserver.Validator{
acmeserver.ChallengeHTTP01: http01,
},
Meta: acmeserver.DirectoryMeta{Website: "https://ca.example.com"},
})
if err != nil {
log.Fatal(err)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
go func() {
if err := srv.Run(ctx); err != nil {
log.Print(err)
}
}()
mux := http.NewServeMux()
mux.Handle("/acme/", srv)
// http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux) would serve the directory at
// https://ca.example.com/acme/directory.
_ = mux
}
Output:
Index ¶
- Constants
- Variables
- func ImmediateRenewal(since, now time.Time) (start, end time.Time)
- func Retryable(p *Problem, err error) error
- func ValidNFType(value string) bool
- type Account
- type AccountStatus
- type AccountStore
- type AllowAll
- type Authorization
- type AuthorizationStatus
- type Certificate
- type CertificateStore
- type Challenge
- type ChallengeStatus
- type ChallengeType
- type Clock
- type ClockFunc
- type Config
- type DirectoryMeta
- type ErrorType
- type ExternalAccountKeys
- type GrantingValidator
- type Identifier
- type IdentifierType
- type IssuancePolicy
- type IssuanceState
- type IssueRequest
- type IssueResult
- type Issuer
- type LifetimeRenewal
- type LimitKind
- type LimitRequest
- type Limiter
- type NonceManager
- type Order
- type OrderStatus
- type OrderStore
- type Policy
- type PreApprover
- type Problem
- func (p *Problem) Error() string
- func (p *Problem) HTTPStatus() int
- func (p *Problem) MarshalJSON() ([]byte, error)
- func (p *Problem) UnmarshalJSON(data []byte) error
- func (p *Problem) WithIdentifier(id Identifier) *Problem
- func (p *Problem) WithRetryAfter(d time.Duration) *Problem
- func (p *Problem) WithStatus(status int) *Problem
- type Profile
- type ProfileChooser
- type Refusal
- type RefusalKind
- type RefusalRecorder
- type RenewalAdvisor
- type RenewalInfo
- type RetryableError
- type RevokeRequest
- type Revoker
- type Server
- type Store
- type Task
- type TaskKind
- type Validation
- type ValidationGrant
- type ValidationRequest
- type Validator
- type WorkStore
- type WorkerConfig
Examples ¶
Constants ¶
const ( DefaultMaxRequestBody = 64 << 10 DefaultOrderLifetime = 7 * 24 * time.Hour DefaultAuthorizationLifetime = 30 * 24 * time.Hour DefaultMaxIdentifiers = 100 DefaultDetachedWriteTimeout = 30 * time.Second )
Defaults used when Config leaves a field zero.
const DefaultRenewalRetryAfter = 6 * time.Hour
The Retry-After of renewalInfo responses when the advisor sets none.
const ProblemNamespace = "urn:ietf:params:acme:error:"
Prefixes every ACME error type in a problem document.
const TKAuthTypeATC = "atc"
The only Authority Token subtype this server offers, see RFC 9447 section 4.
Variables ¶
var ( ErrNotFound = errors.New("acmeserver: resource not found") ErrConflict = errors.New("acmeserver: resource already exists") ErrRevisionMismatch = errors.New("acmeserver: resource revision mismatch") // Returned by CreateOrder when order.Replaces names a certificate that another order, which // is not invalid at order.CreatedAt, already replaced. ErrAlreadyReplaced = errors.New("acmeserver: certificate already replaced") )
Errors a Store returns for contract violations. Backend failures must use other errors.
Functions ¶
func ImmediateRenewal ¶ added in v0.6.0
Returns a window that opened at since and ends at now, for a certificate that should be renewed at once. Clients pick a random time inside the window, so a window reaching into the future would let some of them wait. A since that is not before now gives the minute before now.
func Retryable ¶ added in v0.6.0
Returns a retryable failure that reports p once the attempts are exhausted.
func ValidNFType ¶ added in v0.6.0
Reports whether value is a valid RFC 9310 NFType: 1 to 32 printable ASCII characters without spaces. 3GPP TS 29.510 clause 6.1.6.3.3 lists the values in use, such as AMF or NRF, and keeps adding new ones, so the value is not compared with a list.
Types ¶
type Account ¶
type Account struct {
ID string
Status AccountStatus
// The account key. KeyThumbprint is its RFC 7638 thumbprint, unique among accounts.
Key crypto.PublicKey
KeyThumbprint string
Contact []string
TermsOfServiceAgreed bool
// The identity the host derived from a verified external account binding.
ExternalAccountID string
// The single-use claim on that binding, set when Config.SingleUseExternalAccounts is on.
// Stores keep non-empty claims unique among accounts.
ExternalAccountClaim string
CreatedAt time.Time
// Increases on every successful update and guards concurrent modification.
Revision uint64
}
A stored ACME account. Its ID is opaque and appears only in the account URL.
type AccountStatus ¶
type AccountStatus string
The status of an account resource, see RFC 8555 section 7.1.2.
const ( AccountValid AccountStatus = "valid" AccountDeactivated AccountStatus = "deactivated" AccountRevoked AccountStatus = "revoked" )
Account statuses.
func (AccountStatus) CanTransition ¶
func (s AccountStatus) CanTransition(next AccountStatus) bool
Reports whether RFC 8555 section 7.1.6 allows the status to move to next.
func (AccountStatus) Known ¶
func (s AccountStatus) Known() bool
Reports whether the status is one the protocol defines.
type AccountStore ¶
type AccountStore interface {
// Stores a new account with revision 1. It returns ErrConflict when the ID, the key
// thumbprint or a non-empty ExternalAccountClaim is already in use.
CreateAccount(ctx context.Context, account *Account) error
// Returns the account with the given ID or ErrNotFound.
Account(ctx context.Context, id string) (*Account, error)
// Returns the account whose key has the given thumbprint or ErrNotFound.
AccountByKey(ctx context.Context, thumbprint string) (*Account, error)
// Replaces the stored account when the revisions match and increments
// account.Revision. It returns ErrRevisionMismatch, ErrNotFound or ErrConflict otherwise.
UpdateAccount(ctx context.Context, account *Account) error
}
Persists accounts. Every method is atomic and returns copies the caller owns.
type AllowAll ¶
type AllowAll struct{}
A Policy that accepts every request.
func (AllowAll) NewAccount ¶
Accepts the account.
type Authorization ¶
type Authorization struct {
ID string
AccountID string
OrderID string
Identifier Identifier
Status AuthorizationStatus
Expires time.Time
// Marks a wildcard authorization. Identifier then holds the base name.
Wildcard bool
ChallengeIDs []string
// Records that the successful challenge also authorized a CA certificate for the identifier.
CACertificate bool
// The time after which the proof no longer covers a certificate, zero when unbounded.
GrantExpires time.Time
// The further names and network function type the successful challenge granted, see
// ValidationGrant.
GrantNames []Identifier
GrantNFType string
// Marks an authorization the Policy granted without a challenge, see PreApprover. It never
// lets the account revoke another account's certificate.
PreApproved bool
CreatedAt time.Time
Revision uint64
}
A stored authorization resource. Each authorization belongs to exactly one order.
type AuthorizationStatus ¶
type AuthorizationStatus string
The status of an authorization resource, see RFC 8555 section 7.1.4.
const ( AuthorizationPending AuthorizationStatus = "pending" AuthorizationValid AuthorizationStatus = "valid" AuthorizationInvalid AuthorizationStatus = "invalid" AuthorizationDeactivated AuthorizationStatus = "deactivated" AuthorizationExpired AuthorizationStatus = "expired" AuthorizationRevoked AuthorizationStatus = "revoked" )
Authorization statuses.
func (AuthorizationStatus) CanTransition ¶
func (s AuthorizationStatus) CanTransition(next AuthorizationStatus) bool
Reports whether RFC 8555 section 7.1.6 allows the status to move to next.
func (AuthorizationStatus) Known ¶
func (s AuthorizationStatus) Known() bool
Reports whether the status is one the protocol defines.
func (AuthorizationStatus) Terminal ¶
func (s AuthorizationStatus) Terminal() bool
Reports whether no further transition is possible.
type Certificate ¶
type Certificate struct {
// The base64url SHA-256 digest of the DER leaf, which names the certificate resource.
ID string
AccountID string
OrderID string
// The opaque reference returned by the host CA.
CAReference string
// Holds the DER leaf certificate followed by the DER issuer chain the client receives.
Chain [][]byte
NotBefore time.Time
NotAfter time.Time
Revoked bool
RevokedAt time.Time
// The CRL reason code recorded at revocation.
RevocationReason int
// Identifies the revocation the host CA receives. It is committed with the reason before the
// first CA call and reused by every retry, so the CA can deduplicate. It is cleared with the
// reason when the CA refuses the revocation.
RevocationOperationID string
// When the revocation was recorded, before the CA call. RevokedAt is set after the CA succeeded.
RevocationRequestedAt time.Time
// The RFC 9773 identifier built from the leaf's authority key identifier and serial number.
// It is empty when the leaf has no authority key identifier. Stores keep non-empty values
// unique among certificates.
RenewalID string
// The order that claimed this certificate as its predecessor through replaces.
ReplacedByOrderID string
// The validation evidence the issuer received, kept for host audit needs.
Validations []Validation
CreatedAt time.Time
Revision uint64
}
A stored issued certificate together with its chain.
type CertificateStore ¶
type CertificateStore interface {
// Returns the certificate with the given ID or ErrNotFound.
Certificate(ctx context.Context, id string) (*Certificate, error)
// Returns the certificate with the given non-empty RenewalID or ErrNotFound.
CertificateByRenewalID(ctx context.Context, renewalID string) (*Certificate, error)
// Replaces the stored certificate when the revisions match and increments cert.Revision.
UpdateCertificate(ctx context.Context, cert *Certificate) error
}
Persists issued certificates.
type Challenge ¶
type Challenge struct {
ID string
AuthorizationID string
AccountID string
Type ChallengeType
Status ChallengeStatus
Token string
// The Authority Token a tkauth-01 response carried, see RFC 9448 section 4.
AuthorityToken string
// The account key thumbprint captured at response time, so a key rollover
// does not change an in-flight validation.
KeyThumbprint string
Validated time.Time
// Records why validation failed.
Error *Problem
Revision uint64
}
A stored challenge resource.
type ChallengeStatus ¶
type ChallengeStatus string
The status of a challenge resource, see RFC 8555 section 7.1.5.
const ( ChallengePending ChallengeStatus = "pending" ChallengeProcessing ChallengeStatus = "processing" ChallengeValid ChallengeStatus = "valid" ChallengeInvalid ChallengeStatus = "invalid" )
Challenge statuses.
func (ChallengeStatus) CanTransition ¶
func (s ChallengeStatus) CanTransition(next ChallengeStatus) bool
Reports whether the status may move to next, including pending to invalid per erratum 5732.
func (ChallengeStatus) Known ¶
func (s ChallengeStatus) Known() bool
Reports whether the status is one the protocol defines.
func (ChallengeStatus) Terminal ¶
func (s ChallengeStatus) Terminal() bool
Reports whether no further transition is possible.
type ChallengeType ¶
type ChallengeType string
Names a challenge mechanism registered with IANA.
const ( ChallengeHTTP01 ChallengeType = "http-01" ChallengeDNS01 ChallengeType = "dns-01" ChallengeTLSALPN01 ChallengeType = "tls-alpn-01" // Answered with an Authority Token instead of a network proof. ChallengeTKAuth01 ChallengeType = "tkauth-01" )
Challenge types from RFC 8555 section 8, RFC 8737 and RFC 9447.
type Config ¶
type Config struct {
// The absolute URL the handler is served under, including any path prefix.
// It must use https unless AllowInsecureBaseURL is set.
BaseURL string
// Accepts an http BaseURL for tests and local examples.
AllowInsecureBaseURL bool
// Persists resources and background work.
Store Store
// Issues and consumes the single-use request nonces.
Nonces NonceManager
// Defaults to the system clock.
Clock Clock
// Receives operational events. It defaults to a logger that discards everything.
Logger *slog.Logger
// Optional directory metadata. Nothing is advertised when it is empty.
Meta DirectoryMeta
// Bounds the size of a request body in bytes. Zero selects DefaultMaxRequestBody.
MaxRequestBody int64
// Signs certificates for finalized orders.
Issuer Issuer
// Revokes issued certificates.
Revoker Revoker
// The RFC 5280 reason codes clients may request. Empty allows unspecified (0),
// keyCompromise (1), affiliationChanged (3), superseded (4) and cessationOfOperation (5).
// Only 0 to 5, 9 and 10 may be listed, because an ACME revocation is final and cannot be a
// certificateHold (6).
RevocationReasons []int
// Learns about authenticated finalize and revokeCert requests the server refuses. Optional.
Refusals RefusalRecorder
// Limits how often clients may get nonces, create accounts and place orders. Optional. Without
// it nothing is limited.
Limits Limiter
// The challenge types offered to clients. Only configured types appear in authorizations.
Validators map[ChallengeType]Validator
// Verifies external account bindings. Required when Meta.ExternalAccountRequired is set.
ExternalAccounts ExternalAccountKeys
// Binds each external account key identifier to at most one account. The claim is committed
// with the account, and a later newAccount with the same identifier and another key is refused.
SingleUseExternalAccounts bool
// Reviews new accounts and orders. Defaults to AllowAll.
Policy Policy
// Reviews current issuance policy before dispatch, with the accepted CSR and validation evidence.
IssuancePolicy IssuancePolicy
// Refuses new accounts that do not agree to Meta.TermsOfService.
RequireTermsOfServiceAgreed bool
// Accepts IP identifiers as specified in RFC 8738.
IPIdentifiers bool
// Accepts TNAuthList identifiers as specified in RFC 9448. It needs a tkauth-01 validator.
TNAuthListIdentifiers bool
// Lets a certificate issued for a TNAuthList identifier outlive the Authority Token, as
// ATIS-1000080 clause 6.3.8 specifies for STIR/SHAKEN. The token must still be valid when
// signing starts. By default the token expiry bounds the certificate, as RFC 9447 section 7
// recommends.
TNAuthListCertificatesOutliveTokens bool
// Accepts NfInstanceId identifiers of 5G network functions as described in 3GPP TS 33.310
// Annex J. It needs a tkauth-01 validator.
NfInstanceIDIdentifiers bool
// The https URL offered as token-authority on tkauth-01 challenges, see RFC 9447 section 3.
// It is optional, and clients fall back to their own configuration when it is empty.
TokenAuthority string
// How long a new order and its pending authorizations stay valid.
OrderLifetime time.Duration
// How long a validated authorization stays valid.
AuthorizationLifetime time.Duration
// Bounds the identifiers of one order.
MaxIdentifiers int
// Bounds the validation tasks one account may have waiting or running at the same time. A
// challenge response beyond the bound is refused with rateLimited and a Retry-After header
// until earlier validations finish, so one account cannot occupy every worker for long. Zero
// selects the MaxIdentifiers in effect, which lets one full order validate at once.
MaxPendingValidations int
// Bounds a store write that has to finish after the client request it belongs to has ended,
// such as recording a revocation the Revoker already carried out. Zero selects
// DefaultDetachedWriteTimeout.
DetachedWriteTimeout time.Duration
// The certificate profiles advertised in the directory meta and accepted on new orders, see
// draft-ietf-acme-profiles. Empty leaves the extension off. A new order that names a profile is
// then refused with invalidProfile.
Profiles []Profile
// The profile an order gets when the client names none. It is required when Profiles is set
// and must be one of them.
DefaultProfile string
// Serves RFC 9773 renewal information and accepts replaces on new orders when set.
// LifetimeRenewal is the built-in advisor. Nil leaves the extension off.
RenewalInfo RenewalAdvisor
// Tunes Run and Ready.
Workers WorkerConfig
}
Configures a Server. Store, Nonces, Issuer, Revoker and at least one validator are required.
type DirectoryMeta ¶
type DirectoryMeta struct {
// The URL of the current terms of service.
TermsOfService string
// The URL of the CA's website.
Website string
// The hostnames the CA recognizes in CAA issue and issuewild records.
CAAIdentities []string
// Tells clients that newAccount needs an external account binding.
ExternalAccountRequired bool
}
The optional metadata object of the directory, see RFC 8555 section 7.1.1.
type ErrorType ¶
type ErrorType string
Names an ACME error without its URN namespace.
const ( ErrorAccountDoesNotExist ErrorType = "accountDoesNotExist" ErrorAlreadyReplaced ErrorType = "alreadyReplaced" ErrorAlreadyRevoked ErrorType = "alreadyRevoked" ErrorBadCSR ErrorType = "badCSR" ErrorBadNonce ErrorType = "badNonce" ErrorBadPublicKey ErrorType = "badPublicKey" ErrorBadRevocationReason ErrorType = "badRevocationReason" ErrorBadSignatureAlgorithm ErrorType = "badSignatureAlgorithm" ErrorCAA ErrorType = "caa" ErrorCompound ErrorType = "compound" ErrorConnection ErrorType = "connection" ErrorDNS ErrorType = "dns" ErrorDNSSEC ErrorType = "dnssec" ErrorExternalAccountRequired ErrorType = "externalAccountRequired" ErrorIncorrectResponse ErrorType = "incorrectResponse" ErrorInvalidContact ErrorType = "invalidContact" ErrorInvalidProfile ErrorType = "invalidProfile" ErrorMalformed ErrorType = "malformed" ErrorOrderNotReady ErrorType = "orderNotReady" ErrorRateLimited ErrorType = "rateLimited" ErrorRejectedIdentifier ErrorType = "rejectedIdentifier" ErrorServerInternal ErrorType = "serverInternal" ErrorTLS ErrorType = "tls" ErrorUnsupportedContact ErrorType = "unsupportedContact" ErrorUnsupportedIdentifier ErrorType = "unsupportedIdentifier" ErrorUserActionRequired ErrorType = "userActionRequired" )
Error types registered by RFC 8555 section 6.7 and RFC 9773 section 5, plus invalidProfile from draft-ietf-acme-profiles, which the draft asks IANA to register.
type ExternalAccountKeys ¶
type ExternalAccountKeys interface {
// Returns the MAC key for the key identifier or ErrNotFound.
MACKey(ctx context.Context, keyID string) ([]byte, error)
}
Supplies the MAC keys that verify external account bindings, see RFC 8555 section 7.3.4.
type GrantingValidator ¶
type GrantingValidator interface {
Validator
ValidateGrant(ctx context.Context, req ValidationRequest) (ValidationGrant, error)
}
Reports what a response authorizes in addition to checking it. A validator that only proves control of an identifier implements Validator alone, and the server then grants nothing.
type Identifier ¶
type Identifier struct {
Type IdentifierType `json:"type"`
Value string `json:"value"`
}
An ACME identifier object.
func NormalizeIdentifiers ¶
func NormalizeIdentifiers(ids []Identifier) ([]Identifier, error)
Normalizes every identifier and rejects duplicates in the normalized set.
func (Identifier) IsWildcard ¶
func (id Identifier) IsWildcard() bool
Reports whether the identifier is a DNS name whose leftmost label is a wildcard.
func (Identifier) Normalize ¶
func (id Identifier) Normalize() (Identifier, error)
Validates the identifier syntax and returns its canonical form. Errors are *Problem values with type unsupportedIdentifier, malformed or rejectedIdentifier. Issuance policy is left to the host.
func (Identifier) String ¶
func (id Identifier) String() string
Returns the type and value joined by a colon.
func (Identifier) URI ¶ added in v0.6.0
func (id Identifier) URI() string
Returns the URI name a certificate carries for an NfInstanceId identifier, or an https URI identifier's value. Other identifier types have no URI form and return an empty string.
type IdentifierType ¶
type IdentifierType string
Names the kind of subject an order or authorization refers to.
const ( IdentifierDNS IdentifierType = "dns" IdentifierIP IdentifierType = "ip" // A base64url encoded DER TN Authorization List, see RFC 8226 section 9. IdentifierTNAuthList IdentifierType = "TNAuthList" // The UUID of a 5G network function instance, see 3GPP TS 33.310 clause J.3.3.2. A certificate // carries it as the URI name urn:uuid:<value>. IdentifierNfInstanceID IdentifierType = "NfInstanceId" // An https URI name that an authority token adds to a certificate, such as the API root of a // network function. No ACME specification registers it, so orders cannot request it. IdentifierURI IdentifierType = "uri" )
Identifier types registered by RFC 8555 section 9.7.7, RFC 8738, RFC 9448 and 3GPP TS 33.310 Annex J, plus the uri type that only authority tokens grant.
type IssuancePolicy ¶
type IssuancePolicy interface {
AuthorizeIssuance(ctx context.Context, req IssueRequest) error
}
Reviews current policy before the first durable issuance dispatch.
type IssuanceState ¶
type IssuanceState struct {
OperationID string
AuthorizedAt time.Time
Deadline time.Time
Validations []Validation
}
Preserves the authorization deadline and evidence across uncertain issuance attempts.
type IssueRequest ¶
type IssueRequest struct {
OperationID string
AccountID string
AccountURL string
OrderID string
// The parsed certificate request and its DER encoding. The signature and the identifier
// set were already checked against the order. CSR.PublicKey is nil for a composite ML-DSA
// key. Read that key with compositex509.ParsePKIXPublicKey from CSR.RawSubjectPublicKeyInfo
// and certify it with compositex509.CreateCertificate. CSR.PublicKey is also nil for an
// SLH-DSA key (RFC 9909), which the issuer certifies with RawSubjectPublicKeyInfo unchanged.
CSR *x509.CertificateRequest
CSRDER []byte
// The normalized identifiers the order covers.
Identifiers []Identifier
// The requested validity. Zero values mean the host decides. An authority token expiry
// narrows NotAfter. The issuer may return a shorter validity, but the leaf must fit inside
// the requested window and must be valid already unless NotBefore is in the future.
NotBefore time.Time
NotAfter time.Time
// How each identifier was validated.
Validations []Validation
// The earliest order, authorization or authority token expiry, after which new signing is
// forbidden.
Deadline time.Time
// Allows only recovery of an existing result, never a new signing operation.
RecoveryOnly bool
// The Order.Profile name the certificate is issued under. It is empty when the server offered
// no profiles when the order was created.
Profile string
}
What the host CA receives when an order is ready for issuance. The same OperationID is presented on every attempt for one order, so the issuer can deduplicate retries.
type IssueResult ¶
type IssueResult struct {
// The DER leaf certificate followed by the DER issuer chain.
Chain [][]byte
// An opaque host CA reference retained with issued or unpublished results.
CAReference string
// Reports that the CA has not decided yet. The worker asks again after RetryAfter.
Pending bool
RetryAfter time.Duration
// A final refusal that becomes the order error.
Rejected *Problem
}
The outcome of an issuance attempt. Exactly one of Chain, Pending and Rejected is set.
type Issuer ¶
type Issuer interface {
Issue(ctx context.Context, req IssueRequest) (IssueResult, error)
}
Issues or recovers one durable result per operation ID, enforcing Deadline and RecoveryOnly for new signing.
type LifetimeRenewal ¶
type LifetimeRenewal struct {
// Fractions of the lifetime at which the window starts and ends. Zero values mean two
// thirds and five sixths.
Start float64
End float64
// Copied into every response.
ExplanationURL string
RetryAfter time.Duration
// Dates the window of a revoked certificate. Defaults to the system clock.
Clock Clock
}
A RenewalAdvisor that places the window at fixed fractions of the certificate lifetime and asks for immediate renewal of revoked certificates.
func (LifetimeRenewal) RenewalInfo ¶
func (l LifetimeRenewal) RenewalInfo(_ context.Context, cert *Certificate) (RenewalInfo, error)
Returns the window for the certificate. A revoked certificate gets a window that opened at its revocation and ends at the time of the request, so clients renew at once.
type LimitKind ¶ added in v0.6.0
type LimitKind string
Names the request a Limiter is asked about.
const ( // A request that gets a nonce without having spent one: a newNonce request or an error // answer to a request that spent no valid nonce, such as one for an unknown resource, with a // wrong method, an unparsable JWS or a bad nonce. An error answer to a request that spent a // nonce carries its replacement without asking. LimitRequest.Account is nil. LimitNewNonce LimitKind = "newNonce" // A newAccount request that creates an account, asked after the contacts, the terms of service // agreement and any external account binding were checked. A request for an existing account // is not asked about. LimitNewAccount LimitKind = "newAccount" // A newOrder request, asked after its identifiers, profile and replaces member were checked and // before Policy.NewOrder. LimitNewOrder LimitKind = "newOrder" // A challenge response that starts a validation, asked after the response and the // authorization were checked and the account's validations were counted against // Config.MaxPendingValidations, before the validation is enqueued. A response to a challenge // that is no longer pending is not asked about. LimitChallengeResponse LimitKind = "challengeResponse" // An unauthenticated renewalInfo request with a well-formed identifier, asked before the // certificate is looked up. LimitRequest.Account is nil. LimitRenewalInfo LimitKind = "renewalInfo" )
The requests a Limiter is asked about. More kinds may be added, so a Limiter should allow kinds it does not know.
type LimitRequest ¶ added in v0.6.0
type LimitRequest struct {
Kind LimitKind
// The address of the client connection, taken from http.Request.RemoteAddr with IPv4-mapped
// IPv6 addresses unmapped. It is the zero Addr when RemoteAddr holds no IP address. Forwarded
// headers are never read, because any client can set them. A host behind a trusted reverse
// proxy can rewrite RemoteAddr before the request reaches the handler.
Client netip.Addr
// For newAccount the account the request would create, not yet stored. For newOrder and
// challengeResponse the account making the request.
Account *Account
// For newOrder the normalized identifiers of the order. For challengeResponse the identifier
// of the authorization, with its wildcard prefix.
Identifiers []Identifier
// The certificate a new order replaces, checked against the account and the identifiers as
// RFC 9773 section 5 requires. Empty when the order replaces nothing.
Replaces string
// For challengeResponse the validations of the account that are waiting or running before
// this one. The server has already checked that it is below Config.MaxPendingValidations.
PendingValidations int
}
Describes a request a Limiter may refuse. The Limiter must not change Account.
type Limiter ¶ added in v0.6.0
type Limiter interface {
Allow(ctx context.Context, req LimitRequest) error
}
Limits how often clients may get nonces, create accounts, place orders, start validations and look up renewal information. Allow is asked once for each such request and should count the request when it returns nil. A returned *Problem is sent to the client, normally rateLimited with WithRetryAfter so the client knows when to try again. Any other error is logged and answered with serverInternal, so a host that prefers to serve clients while its limit store is unavailable returns nil instead.
type NonceManager ¶
type NonceManager interface {
// Returns a fresh nonce in base64url form.
Issue(ctx context.Context) (string, error)
// Invalidates the nonce and reports whether it was valid. A nonce is valid exactly once.
Consume(ctx context.Context, nonce string) (bool, error)
}
Issues and consumes the single-use nonces that protect requests against replay.
type Order ¶
type Order struct {
ID string
AccountID string
Status OrderStatus
Expires time.Time
Identifiers []Identifier
// The requested validity. Zero values mean unset.
NotBefore time.Time
NotAfter time.Time
AuthorizationIDs []string
// Records why the order became invalid.
Error *Problem
// The DER request accepted at finalization. It never changes once set.
CSR []byte
CertificateID string
// The durable authorization decision made before the first CA call.
Issuance *IssuanceState
// A CA result withheld from clients and retained for host reconciliation.
UnpublishedResult *IssueResult
// The RFC 9773 identifier of the certificate this order replaces, set when the client sent
// replaces and Config.RenewalInfo is enabled. Stores mark that certificate replaced by this
// order when the order is created.
Replaces string
// The certificate profile selected when the order was created, see draft-ietf-acme-profiles.
// It is empty when the server offered no profiles at that time.
Profile string
CreatedAt time.Time
Revision uint64
}
A stored order resource.
type OrderStatus ¶
type OrderStatus string
The status of an order resource, see RFC 8555 section 7.1.3.
const ( OrderPending OrderStatus = "pending" OrderReady OrderStatus = "ready" OrderProcessing OrderStatus = "processing" OrderValid OrderStatus = "valid" OrderInvalid OrderStatus = "invalid" )
Order statuses.
func (OrderStatus) CanTransition ¶
func (s OrderStatus) CanTransition(next OrderStatus) bool
Reports whether RFC 8555 section 7.1.6 allows the status to move to next.
func (OrderStatus) Known ¶
func (s OrderStatus) Known() bool
Reports whether the status is one the protocol defines.
func (OrderStatus) Terminal ¶
func (s OrderStatus) Terminal() bool
Reports whether no further transition is possible.
type OrderStore ¶
type OrderStore interface {
// Stores an order together with its authorizations and challenges, all at revision 1.
// It returns ErrConflict when any ID is already in use. When order.Replaces is set, the
// same operation marks the certificate with that RenewalID as replaced by the order,
// returning ErrNotFound when no such certificate exists and ErrAlreadyReplaced when an order
// that is not invalid at order.CreatedAt already replaced it, see RFC 9773 section 5.
CreateOrder(ctx context.Context, order *Order, authzs []*Authorization, challenges []*Challenge) error
// Returns the order with the given ID or ErrNotFound.
Order(ctx context.Context, id string) (*Order, error)
// Returns up to limit order IDs of the account in creation order, starting after the
// given ID. An empty after starts at the beginning.
OrderIDs(ctx context.Context, accountID, after string, limit int) ([]string, error)
// Returns the authorization with the given ID or ErrNotFound.
Authorization(ctx context.Context, id string) (*Authorization, error)
// Replaces the stored authorization when the revisions match and increments authz.Revision.
// When the authorization reaches a terminal status, the same operation marks its order invalid
// with Error set to InvalidAuthorizationProblem() unless issuance was already dispatched.
UpdateAuthorization(ctx context.Context, authz *Authorization) error
// Reports whether the account holds a valid unexpired authorization that is not PreApproved
// for every identifier, read in one consistent snapshot. Revocation by another account relies
// on it.
AuthorizedFor(ctx context.Context, accountID string, identifiers []Identifier, now time.Time) (bool, error)
// Returns the challenge with the given ID or ErrNotFound.
Challenge(ctx context.Context, id string) (*Challenge, error)
}
Persists orders with their authorizations and challenges.
type Policy ¶
type Policy interface {
// Reviews a new account. The key, contacts and external account identity are set.
NewAccount(ctx context.Context, account *Account) error
// Reviews a new order after identifier normalization. The policy may change NotBefore and
// NotAfter. Any other change, or a NotAfter no later than NotBefore, fails the request with
// serverInternal.
NewOrder(ctx context.Context, account *Account, order *Order) error
}
Reviews requests before the server stores their result. A returned *Problem is sent to the client, any other error is reported as serverInternal. Nil methods are not allowed, embed AllowAll to accept everything.
type PreApprover ¶ added in v0.6.0
type PreApprover interface {
// Returns the identifiers of a new order that need no challenge. It runs after NewOrder has
// accepted the order, which it must not change. Each returned identifier must be one of
// order.Identifiers, wildcard names included. A returned *Problem refuses the order and any
// other error, or an identifier the order does not contain, is reported as serverInternal.
PreApprove(ctx context.Context, account *Account, order *Order) ([]Identifier, error)
}
An optional extension of Policy that authorizes identifiers without a challenge, for example names an administrator approved in advance for an external account binding. Their authorizations start valid with no challenges and expire with the order. An order whose identifiers are all pre-approved starts ready.
type Problem ¶
type Problem struct {
Type ErrorType
Detail string
// Overrides the default HTTP status of the type when it is not zero.
Status int
// Names the identifier a subproblem refers to.
Identifier *Identifier
Subproblems []*Problem
// Lists the supported signature algorithms in a badSignatureAlgorithm problem.
Algorithms []string
// Sets the Retry-After header. It is not serialized.
RetryAfter time.Duration
}
An ACME problem document, see RFC 7807 and RFC 8555 section 6.7. It implements error.
func InvalidAuthorizationProblem ¶ added in v0.5.0
func InvalidAuthorizationProblem() *Problem
Returns the order error for an authorization that stopped being valid before issuance was dispatched. UpdateAuthorization stores it on the order it invalidates.
func NewProblem ¶
Returns a problem of the given type with the default HTTP status of that type.
func (*Problem) HTTPStatus ¶
Returns the status the problem is written with.
func (*Problem) MarshalJSON ¶
Writes the problem document with fully qualified types.
func (*Problem) UnmarshalJSON ¶
Reads a problem document and strips the ACME namespace from its types.
func (*Problem) WithIdentifier ¶
func (p *Problem) WithIdentifier(id Identifier) *Problem
Returns a copy that names the affected identifier.
func (*Problem) WithRetryAfter ¶
Returns a copy that asks the client to wait before retrying.
func (*Problem) WithStatus ¶
Returns a copy that overrides the HTTP status.
type Profile ¶ added in v0.6.0
type Profile struct {
// The name clients send in the newOrder profile member. It has 1 to 64 ASCII letters, digits,
// dots, underscores or hyphens and starts with a letter or digit.
Name string
// A human-readable description the server publishes as plain text at <BaseURL>profiles/<Name>.
Description string
// An absolute https URL of a description published elsewhere. Exactly one of Description and
// URL is set.
URL string
}
A certificate profile offered to clients, see draft-ietf-acme-profiles. The host decides what a profile means and receives the selected name in IssueRequest.Profile.
type ProfileChooser ¶ added in v0.6.0
type ProfileChooser interface {
// Returns the profile for an order whose client named none, or "" for the default.
DefaultProfile(ctx context.Context, account *Account) (string, error)
}
An optional extension of Policy that picks the profile of a new order whose client named none, for example the profile an account's external account binding allows. Policy.NewOrder sees the chosen profile. An empty answer keeps Config.DefaultProfile. A returned *Problem refuses the order. Any other error, or a profile the server does not offer, is reported as serverInternal.
type Refusal ¶ added in v0.6.0
type Refusal struct {
Kind RefusalKind
// The requesting account. It is empty for a revocation signed with the certificate key.
AccountID string
// The order a finalize request was for and the identifiers it covers.
OrderID string
Identifiers []Identifier
// The DER certificate request of a finalize request, when it was valid base64url.
CSR []byte
// The DER certificate of a revocation request, when it was valid base64url, and the reason
// code it asked for.
Certificate []byte
Reason int
// The answer the client receives. The recorder must not change it.
Problem *Problem
}
Describes an authenticated finalize or revokeCert request that was answered with a problem. The server refuses these requests itself, so without a recorder the host never learns about them.
type RefusalKind ¶ added in v0.6.0
type RefusalKind string
Names the request a Refusal answered.
const ( // A finalize request whose CSR or order state the server refused. RefusalFinalize RefusalKind = "finalize" // A revokeCert request the server or the Revoker refused. RefusalRevocation RefusalKind = "revocation" )
The requests whose refusals reach a RefusalRecorder.
type RefusalRecorder ¶ added in v0.6.0
Learns about refused finalize and revokeCert requests, for example to keep an audit trail of why a client got no certificate. Requests that fail authentication and server faults are not reported. RecordRefusal runs before the answer is sent, detached from the client request so a disconnect does not cancel it. A returned error is logged and does not change the answer.
type RenewalAdvisor ¶
type RenewalAdvisor interface {
RenewalInfo(ctx context.Context, cert *Certificate) (RenewalInfo, error)
}
Suggests when a certificate should be renewed, see RFC 9773. Setting Config.RenewalInfo advertises the renewalInfo resource and accepts replaces on new orders.
type RenewalInfo ¶
type RenewalInfo struct {
// The window in which the client should renew. End must be later than Start.
Start time.Time
End time.Time
// An optional page explaining the window.
ExplanationURL string
// How long clients wait before asking again. Zero means DefaultRenewalRetryAfter.
RetryAfter time.Duration
}
The renewalInfo object of RFC 9773 section 4.2 with the Retry-After the response carries.
type RetryableError ¶ added in v0.6.0
A validation failure worth retrying, such as a refused connection or an unanswered DNS query. The worker retries it like any other error. When the attempts run out, Problem becomes the challenge error instead of serverInternal. Err is the cause and must not be a *Problem, because a *Problem in the chain ends validation at once.
func (*RetryableError) Error ¶ added in v0.6.0
func (e *RetryableError) Error() string
Returns the problem followed by the cause.
func (*RetryableError) Unwrap ¶ added in v0.6.0
func (e *RetryableError) Unwrap() error
Returns the cause, never the reported problem.
type RevokeRequest ¶
type RevokeRequest struct {
// Stable across retries until the CA accepts or refuses the revocation. It is stored before the
// first call and discarded when Revoke returns a *Problem.
OperationID string
Certificate *x509.Certificate
DER []byte
// The CRL reason code recorded with the operation. A retry keeps the recorded reason.
Reason int
// The account that requested the revocation. Empty when the certificate key signed the request.
AccountID string
}
What the host CA receives for a revocation.
type Revoker ¶
type Revoker interface {
Revoke(ctx context.Context, req RevokeRequest) error
}
Revokes certificates. Revoke returns nil only after the CA recorded the revocation durably. A returned *Problem is a final refusal sent to the client. Any other error is reported as serverInternal and the next request repeats the same operation. The CA must deduplicate by OperationID because a client retries after an uncertain answer.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Serves the ACME resources below Config.BaseURL. Mount it at the base path, for example http.Handle("/acme/", srv), and run Run in the same or another process. Without Run, challenges are never validated and orders are never issued.
func (*Server) Ready ¶
Reports whether accepted work is being picked up. It returns an error when tasks have waited longer than WorkerConfig.StaleAfter without a claim while no task is leased, which means no Run is active on the store or every worker is stuck. A queue that is deep because clients keep the workers busy is not an error, so client load cannot fail a readiness probe. A worker that died holding leases is reported once they expire.
func (*Server) Run ¶
Processes persisted validation and issuance work until ctx is canceled. Without a worker in this process or another process on the same store, challenges and orders never progress. Canceling ctx stops new claims but does not interrupt a task already in flight, so Run can take up to WorkerConfig.Lease to return when a validator, issuer or store stops responding. Waiting for it is optional: a claimed task is leased and its commit is fenced, so another worker picks it up once the lease lapses.
type Store ¶
type Store interface {
AccountStore
OrderStore
CertificateStore
WorkStore
}
The persistence contract a host supplies. Operations must be free of side effects a retry would repeat.
type Task ¶
type Task struct {
ID string
Kind TaskKind
TargetID string
AccountID string
// The earliest time a worker may claim the task.
RunAt time.Time
// The number of claims so far.
Attempts int
// The end of the current lease. Zero when no worker holds the task.
LeaseUntil time.Time
// Increases on every claim. Completion calls must present the current value.
Fence uint64
CreatedAt time.Time
}
A unit of persisted background work. Run claims tasks with a lease and a fence so a worker that lost its lease cannot commit a stale result.
type Validation ¶
type Validation struct {
Identifier Identifier
Type ChallengeType
Validated time.Time
// Reports that the validation authorized a CA certificate rather than an end-entity one.
CACertificate bool
// The latest acceptable certificate expiry under this proof, zero when unbounded.
GrantExpires time.Time
// Further names the certificate may carry and the network function type, both granted by an
// authority token, see ValidationGrant.
Names []Identifier
NFType string
// Reports that the Policy authorized the identifier without a challenge. Type is then empty
// and Validated is when the order was created.
PreApproved bool
}
Records how one identifier of an order was validated.
type ValidationGrant ¶
type ValidationGrant struct {
// Allows the order to be finalized with a certificate request that asks for a CA
// certificate, see RFC 9448 section 6.
CACertificate bool
// Bounds the validity of certificates issued on this proof, see RFC 9447 section 7. The zero
// value leaves the validity to the order and the issuer.
Expires time.Time
// Further names the certificate may carry on this proof, such as the sans of a 3GPP NF
// Certificate Authority Token. Each is a normalized dns, ip or uri identifier.
Names []Identifier
// The RFC 9310 network function type the proof attests, empty when it attests none.
NFType string
}
What a successful challenge response authorizes beyond control of the identifier.
type ValidationRequest ¶
type ValidationRequest struct {
Challenge Challenge
Identifier Identifier
// Marks a wildcard authorization. Identifier then holds the base name.
Wildcard bool
// The token joined with the account key thumbprint, see RFC 8555 section 8.1.
KeyAuthorization string
// The RFC 7638 thumbprint of the account key captured when the client responded.
AccountKeyThumbprint string
// The Authority Token of a tkauth-01 response, empty for every other challenge type.
AuthorityToken string
}
What a validator receives for one challenge.
type Validator ¶
type Validator interface {
Validate(ctx context.Context, req ValidationRequest) error
}
Checks a challenge response. A nil result marks the challenge valid, a returned *Problem marks it invalid and any other error is retried until the attempt limit.
type WorkStore ¶
type WorkStore interface {
// Stores the challenge when its revision matches and enqueues the task in the same
// operation. It returns ErrConflict when the task ID is in use.
AcceptChallenge(ctx context.Context, challenge *Challenge, task *Task) error
// Stores the order when its revision matches and enqueues the task in the same operation.
FinalizeOrder(ctx context.Context, order *Order, task *Task) error
// Stores order.Issuance as the durable dispatch decision after checking the task fence and the
// revision and status of the order, the account and every authorization.
BeginIssuance(ctx context.Context, task *Task, order *Order, account *Account, authzs []*Authorization) error
// Leases the runnable task with the earliest RunAt, increments its fence and attempts
// and returns a copy. It returns ErrNotFound when no task is runnable at now.
ClaimTask(ctx context.Context, now, leaseUntil time.Time) (*Task, error)
// Releases the lease and stores task.RunAt for a later claim.
RescheduleTask(ctx context.Context, task *Task) error
// Stores the challenge when its revision matches and releases the lease with task.RunAt for
// a later claim, all in one operation. It records the error of a validation attempt that
// will be retried.
RetryValidation(ctx context.Context, task *Task, challenge *Challenge) error
// Removes the task without touching any resource.
FinishTask(ctx context.Context, task *Task) error
// Stores the challenge, authorization and order when every revision matches and removes the
// task, all in one operation. The order revision advances even when its fields are unchanged.
CompleteValidation(ctx context.Context, task *Task, challenge *Challenge, authz *Authorization, order *Order) error
// Stores the order when its revision matches, creates the certificate when it is not nil
// and removes the task, all in one operation. It returns ErrConflict when the certificate
// ID or a non-empty RenewalID is in use.
CompleteIssuance(ctx context.Context, task *Task, order *Order, cert *Certificate) error
// Counts tasks that were runnable at or before the given time and hold no lease past it.
PendingTasks(ctx context.Context, before time.Time) (int, error)
// Counts tasks that hold a lease past the given time.
LeasedTasks(ctx context.Context, now time.Time) (int, error)
// Counts the validation tasks of an account that have not finished, waiting or leased.
AccountValidations(ctx context.Context, accountID string) (int, error)
}
Persists background work together with the resource changes that create or finish it. Every task method that takes a claimed task checks task.Fence against the stored fence and returns ErrRevisionMismatch when another claim superseded it.
type WorkerConfig ¶
type WorkerConfig struct {
// The number of tasks processed at the same time. Defaults to 4.
Concurrency int
// How often an idle worker looks for work. Defaults to one second.
PollInterval time.Duration
// How long a claimed task stays leased. A task run never continues past its lease, so no other
// worker can take the task while it runs. It must be at least twice TaskTimeout and defaults to
// two minutes or twice TaskTimeout, whichever is longer.
Lease time.Duration
// Bounds each phase of a task separately: one validator or issuer call and the store
// operations that record its outcome. A task run uses several of these budgets in
// sequence within its lease. Defaults to 30 seconds or half the lease, whichever is shorter.
TaskTimeout time.Duration
// Limits validation and pre-dispatch policy retries to five by default without limiting issuance recovery.
MaxAttempts int
// The first retry delay, doubled on every further attempt. Defaults to five seconds.
RetryDelay time.Duration
// How long accepted work may wait unclaimed before Ready reports a problem. Defaults to
// one minute.
StaleAfter time.Duration
// Marks that another process runs Run against the same store, which silences the warning
// logged when work is accepted while Run is inactive here.
External bool
}
Configures Run. Zero values select the defaults.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package challenge provides ACME network validators with explicit resolver and egress policy.
|
Package challenge provides ACME network validators with explicit resolver and egress policy. |
|
examples
|
|
|
quickstart
command
A local ACME server for trying go-acme-server: in-memory storage, a throwaway CA and HTTP-01.
|
A local ACME server for trying go-acme-server: in-memory storage, a throwaway CA and HTTP-01. |
|
internal
|
|
|
jws
Package jws parses and verifies the restricted JWS profile of RFC 8555 section 6.2.
|
Package jws parses and verifies the restricted JWS profile of RFC 8555 section 6.2. |
|
slhdsaspki
Package slhdsaspki encodes SLH-DSA public keys as the SubjectPublicKeyInfo of RFC 9909.
|
Package slhdsaspki encodes SLH-DSA public keys as the SubjectPublicKeyInfo of RFC 9909. |
|
slhdsatest
Package slhdsatest creates SLH-DSA certificate requests and certificates for tests, which crypto/x509 cannot create.
|
Package slhdsatest creates SLH-DSA certificate requests and certificates for tests, which crypto/x509 cannot create. |
|
Package memstore provides an in-memory acmeserver.Store for tests and examples.
|
Package memstore provides an in-memory acmeserver.Store for tests and examples. |
|
Package nonce provides a bounded single-process implementation of acmeserver.NonceManager.
|
Package nonce provides a bounded single-process implementation of acmeserver.NonceManager. |
|
Package noncetest checks an acmeserver.NonceManager against the replay protection contract.
|
Package noncetest checks an acmeserver.NonceManager against the replay protection contract. |
|
Package storetest checks an acmeserver.Store against the persistence contract.
|
Package storetest checks an acmeserver.Store against the persistence contract. |