acme

package module
v0.0.0-...-cb89fa5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 12 Imported by: 0

README

acme — go-ruby-acme

go-ruby-acme/acme

Go Reference CI Coverage

A pure-Go (no cgo) reimplementation of the surface of Ruby's acme-client gem — the ACME / Let's Encrypt (RFC 8555) protocol client. It mirrors the ergonomics of Acme::Client — accounts, orders, authorizations, challenges, finalization and certificate download — while delegating the ACME/JWS wire protocol itself to the standard, pure-Go golang.org/x/crypto/acme client, so no part of RFC 8555 is reimplemented here.

It is the ACME backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-jwt and go-ruby-oauth2.

Surface

The Ruby object graph is reproduced with idiomatic Go types:

  • ClientNewClient(privateKey, directory)Acme::Client.new(private_key:, directory:)
  • AccountNewAccount(ctx, contact, tosAgreed)#new_account, Account(ctx) for lookup
  • OrderNewOrder(ctx, identifiers)#new_order; Order#Authorizations, #Finalize(csr), #Certificate (PEM chain), #Status, #Reload
  • Authorization#Challenges, #HTTP01 / #DNS01 / #TLSALPN01, #Domain, #Status
  • Challenge#KeyAuthorization (token.thumbprint), #RequestValidation, #Status, #Error, #Reload
  • Errors — an error tree rooted at *Error with *Unauthorized, *RateLimited, *BadNonce and *Malformed, mapping ACME problem documents onto the Acme::Client::Error subclasses

Usage

import "github.com/go-ruby-acme/acme"

client, _  := acme.NewClient(accountKey, "https://acme-v02.api.letsencrypt.org/directory")
account, _ := client.NewAccount(ctx, []string{"mailto:me@example.com"}, true)
order, _   := client.NewOrder(ctx, []string{"example.com"})

authzs, _ := order.Authorizations(ctx)
chal      := authzs[0].HTTP01()
ka, _     := chal.KeyAuthorization()   // serve at /.well-known/acme-challenge/<token>
_ = chal.RequestValidation(ctx)

csr, _ := acme.NewCSR(certKey, "example.com")
_ = order.Finalize(ctx, csr)
pemChain, _ := order.Certificate(ctx)

Transport seam

The ACME transport is injectable. WithHTTPClient swaps the *http.Client (the whole flow runs against an in-process mock ACME server with no live network) and WithRetryBackoff tunes the retry policy. The test suite drives the full account → order → authz → http-01 → finalize → certificate flow, plus the badNonce retry, rate-limit, unauthorized and invalid-challenge error paths, against a mock CA — deterministic, and with no real Let's Encrypt.

Tests & coverage

go test ./... runs the full flow against the mock ACME server. The CI gate enforces 100% statement coverage under -race and builds/tests on all six 64-bit Go targets — amd64, arm64, riscv64, loong64, ppc64le, s390x (big-endian).

License

BSD-3-Clause. Copyright (c) the go-ruby-acme/acme authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package acme is a pure-Go (CGO-free), MRI-faithful reimplementation of the surface of Ruby's acme-client gem — the ACME / Let's Encrypt protocol client.

It mirrors the ergonomics of Acme::Client while delegating the ACME/JWS protocol itself to the standard, pure-Go golang.org/x/crypto/acme client, so no part of the RFC 8555 wire protocol is reimplemented here.

The Ruby object graph is reproduced with idiomatic Go types:

client, _  := acme.NewClient(accountKey, directoryURL)
account, _ := client.NewAccount(ctx, []string{"mailto:me@example.com"}, true)
order, _   := client.NewOrder(ctx, []string{"example.com"})
authzs, _  := order.Authorizations(ctx)
chal       := authzs[0].HTTP01()
ka, _      := chal.KeyAuthorization()      // token.thumbprint to serve
_ = chal.RequestValidation(ctx)            // ask the CA to validate
csr, _     := acme.NewCSR(certKey, "example.com")
_ = order.Finalize(ctx, csr)
pem, _     := order.Certificate(ctx)       // PEM certificate chain

The ACME transport is a host seam: an *http.Client can be injected with WithHTTPClient, and the retry policy with WithRetryBackoff, so the whole flow runs against an in-process mock ACME server with no live network and no real Let's Encrypt.

Problem documents returned by the CA are mapped onto an error tree rooted at *Error, with *Unauthorized, *RateLimited, *BadNonce and *Malformed mirroring the Acme::Client::Error subclasses.

Index

Constants

View Source
const (
	ChallengeHTTP01    = "http-01"
	ChallengeDNS01     = "dns-01"
	ChallengeTLSALPN01 = "tls-alpn-01"
)

ACME challenge type identifiers.

Variables

This section is empty.

Functions

func NewCSR

func NewCSR(key crypto.Signer, names ...string) ([]byte, error)

NewCSR builds a DER-encoded PKCS#10 certificate signing request for the given key and DNS names, suitable for Order.Finalize. The first name becomes the subject common name and every name is included as a DNS SAN, mirroring the CSR that Acme::Client::CertificateRequest produces.

Types

type Account

type Account struct {
	// URL is the account resource URL (the KID used to sign later requests).
	URL string
	// Status is the account status, e.g. "valid".
	Status string
	// Contact is the list of contact URIs registered for the account.
	Contact []string
}

Account is a registered ACME account, mirroring the account object returned by Acme::Client#new_account.

type Authorization

type Authorization struct {
	// contains filtered or unexported fields
}

Authorization is an authorization for a single identifier, mirroring the Acme::Client authorization object.

func (*Authorization) Challenges

func (a *Authorization) Challenges() []*Challenge

Challenges returns every challenge offered for the authorization, mirroring Authorization#challenges.

func (*Authorization) DNS01

func (a *Authorization) DNS01() *Challenge

DNS01 returns the dns-01 challenge, or nil if the CA did not offer one. Mirrors Authorization#dns01.

func (*Authorization) Domain

func (a *Authorization) Domain() string

Domain returns the identifier value this authorization is for.

func (*Authorization) HTTP01

func (a *Authorization) HTTP01() *Challenge

HTTP01 returns the http-01 challenge, or nil if the CA did not offer one. Mirrors Authorization#http01.

func (*Authorization) Status

func (a *Authorization) Status() string

Status returns the authorization status, e.g. "pending" or "valid".

func (*Authorization) TLSALPN01

func (a *Authorization) TLSALPN01() *Challenge

TLSALPN01 returns the tls-alpn-01 challenge, or nil if the CA did not offer one. Mirrors Authorization#tls_alpn01.

func (*Authorization) URL

func (a *Authorization) URL() string

URL returns the authorization resource URL.

func (*Authorization) Wildcard

func (a *Authorization) Wildcard() bool

Wildcard reports whether the authorization is for a wildcard identifier.

type BadNonce

type BadNonce struct{ Problem *Error }

BadNonce maps urn:ietf:params:acme:error:badNonce — the client sent an unacceptable anti-replay nonce. Mirrors Acme::Client::Error::BadNonce.

func (*BadNonce) Error

func (e *BadNonce) Error() string

Error implements the error interface.

func (*BadNonce) Unwrap

func (e *BadNonce) Unwrap() error

Unwrap exposes the underlying *Error.

type Challenge

type Challenge struct {
	// contains filtered or unexported fields
}

Challenge is a single ACME challenge, mirroring the Acme::Client challenge object.

func (*Challenge) Error

func (ch *Challenge) Error() error

Error returns the reason the challenge failed, or nil, mirroring Challenge#error. The value is mapped onto the acme error tree.

func (*Challenge) KeyAuthorization

func (ch *Challenge) KeyAuthorization() (string, error)

KeyAuthorization returns the key authorization ("token.thumbprint") to be served for the challenge, mirroring Challenge#key_authorization.

func (*Challenge) Reload

func (ch *Challenge) Reload(ctx context.Context) error

Reload re-fetches the challenge from the CA, refreshing its status and error, mirroring Challenge#reload.

func (*Challenge) RequestValidation

func (ch *Challenge) RequestValidation(ctx context.Context) error

RequestValidation tells the CA the client is ready to answer the challenge, mirroring Challenge#request_validation. The challenge's status is refreshed from the CA's response.

func (*Challenge) Status

func (ch *Challenge) Status() string

Status returns the challenge status, e.g. "pending" or "valid", mirroring Challenge#status.

func (*Challenge) Token

func (ch *Challenge) Token() string

Token returns the challenge token.

func (*Challenge) Type

func (ch *Challenge) Type() string

Type returns the challenge type, e.g. "http-01".

func (*Challenge) URL

func (ch *Challenge) URL() string

URL returns the challenge resource URL.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a pure-Go ACME client, mirroring Ruby's Acme::Client.

It is constructed from an account private key and the CA's directory URL and is the entry point for account registration and order creation.

func NewClient

func NewClient(privateKey crypto.Signer, directory string, opts ...Option) (*Client, error)

NewClient builds a Client from an account private key and a directory URL, mirroring Acme::Client.new(private_key:, directory:).

privateKey must be an RSA or ECDSA key (per RFC 7518); directory is the CA's ACME directory endpoint.

func (*Client) Account

func (c *Client) Account(ctx context.Context) (*Account, error)

Account looks up the account already associated with the client's key, mirroring account lookup on an existing Acme::Client.

func (*Client) NewAccount

func (c *Client) NewAccount(ctx context.Context, contact []string, termsOfServiceAgreed bool) (*Account, error)

NewAccount registers a new account with the CA, mirroring Acme::Client#new_account(contact:, terms_of_service_agreed:).

contact is a list of contact URIs (e.g. "mailto:me@example.com"); when termsOfServiceAgreed is true the client agrees to the CA's terms of service.

func (*Client) NewOrder

func (c *Client) NewOrder(ctx context.Context, identifiers []string) (*Order, error)

NewOrder creates a new certificate order for the given DNS identifiers, mirroring Acme::Client#new_order(identifiers:).

type Error

type Error struct {
	// ProblemType is the RFC 8555 problem type URN.
	ProblemType string
	// Detail is the human-readable explanation from the CA.
	Detail string
	// StatusCode is the HTTP status code the CA responded with.
	StatusCode int
}

Error is the root of the ACME error tree, mirroring Acme::Client::Error.

It wraps an ACME problem document (RFC 8555 §6.7): ProblemType is the problem URN (e.g. "urn:ietf:params:acme:error:unauthorized"), Detail is the human-readable explanation and StatusCode is the HTTP status the CA returned.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type Malformed

type Malformed struct{ Problem *Error }

Malformed maps urn:ietf:params:acme:error:malformed — the request message was malformed. Mirrors Acme::Client::Error::Malformed.

func (*Malformed) Error

func (e *Malformed) Error() string

Error implements the error interface.

func (*Malformed) Unwrap

func (e *Malformed) Unwrap() error

Unwrap exposes the underlying *Error.

type Option

type Option func(*Client)

Option configures a Client at construction time.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient injects the HTTP client used for every ACME request. This is the transport seam: tests point it at an in-process mock ACME server.

func WithRetryBackoff

func WithRetryBackoff(fn func(n int, r *http.Request, resp *http.Response) time.Duration) Option

WithRetryBackoff installs a custom retry-backoff policy. Returning a non-positive duration disables further retries of a failed request. Mirrors the tunable retry behaviour exposed by the underlying transport.

type Order

type Order struct {
	// contains filtered or unexported fields
}

Order is a certificate order, mirroring the Acme::Client order object.

func (*Order) Authorizations

func (o *Order) Authorizations(ctx context.Context) ([]*Authorization, error)

Authorizations fetches the authorization objects for the order, mirroring Order#authorizations.

func (*Order) Certificate

func (o *Order) Certificate(ctx context.Context) (string, error)

Certificate returns the issued certificate chain as a PEM string, mirroring Order#certificate. If the chain was already downloaded by Order.Finalize it is returned directly; otherwise the order is reloaded and the chain fetched from the CA.

func (*Order) Finalize

func (o *Order) Finalize(ctx context.Context, csr []byte) error

Finalize submits the certificate signing request to the CA, waits for the order to become valid and downloads the issued chain, mirroring Order#finalize(csr:). csr is the DER-encoded PKCS#10 request; use NewCSR to build one.

func (*Order) Identifiers

func (o *Order) Identifiers() []string

Identifiers returns the DNS identifiers this order covers.

func (*Order) Reload

func (o *Order) Reload(ctx context.Context) error

Reload re-fetches the order from the CA, refreshing its status and URLs, mirroring Order#reload.

func (*Order) Status

func (o *Order) Status() string

Status returns the current order status, e.g. "pending", "ready", "processing" or "valid".

func (*Order) URL

func (o *Order) URL() string

URL returns the order resource URL.

type RateLimited

type RateLimited struct {
	Problem *Error
	// RetryAfter is the delay advertised by the CA's Retry-After header, if any.
	RetryAfter time.Duration
}

RateLimited maps urn:ietf:params:acme:error:rateLimited — the request exceeds a rate limit. RetryAfter carries the parsed Retry-After hint when present. Mirrors Acme::Client::Error::RateLimited.

func (*RateLimited) Error

func (e *RateLimited) Error() string

Error implements the error interface.

func (*RateLimited) Unwrap

func (e *RateLimited) Unwrap() error

Unwrap exposes the underlying *Error.

type Unauthorized

type Unauthorized struct{ Problem *Error }

Unauthorized maps urn:ietf:params:acme:error:unauthorized — the client lacks authorization to act on a resource. Mirrors Acme::Client::Error::Unauthorized.

func (*Unauthorized) Error

func (e *Unauthorized) Error() string

Error implements the error interface.

func (*Unauthorized) Unwrap

func (e *Unauthorized) Unwrap() error

Unwrap exposes the underlying *Error so errors.As(err, new(*Error)) matches.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL