Documentation
¶
Overview ¶
Package servercert says where a daemon's TLS server certificate comes from: a certificate and key on disk, re-read when they change, or an ACME CA through golang.org/x/crypto/acme/autocert, External Account Binding included.
src, err := servercert.New(servercert.Config{
ACME: &servercert.ACME{
DirectoryURL: "https://acme-v02.harica.gr/acme/<uuid>/directory", // as cm.harica.gr shows it
Domains: []string{"files.example.org"},
CacheDir: "/var/lib/fileshare/acme",
EABKeyID: "<key ID from cm.harica.gr>",
EABHMACKeyFile: "/etc/fileshare/harica-hmac",
},
OnError: func(err error) { log.Print(err) },
})
tc := src.TLSConfig()
tc.NextProtos = append(tc.NextProtos, "sunrpc") // APPEND: keep acme-tls/1
Files ¶
CertFile and KeyFile are read by New, which fails at once on a pair that is unreadable, does not parse, or whose key is not the certificate's. At most once every 10 seconds, during a handshake, the files are read again; a pair whose contents changed is parsed (without holding any lock) and swapped in. A changed pair that does not load -- the certificate written before its key, a truncated file -- is reported once to Config.OnError and the last good pair stays in service.
ACME ¶
autocert answers two challenge types and no other:
- tls-alpn-01 (RFC 8737): the CA connects to port 443 of the domain and negotiates the "acme-tls/1" ALPN protocol. Source.TLSConfig carries that protocol; the caller must APPEND its own to NextProtos, never replace them.
- http-01 (RFC 8555 §8.3): the CA fetches a token on port 80. With ACME.HTTPChallenge, Source.HTTPHandler serves it and must be on :80.
There is no dns-01, hence no wildcard, and no IP address certificates; Config.Check refuses both.
A CA may need no challenge at all. autocert's order flow returns as soon as a new order is already "ready" (RFC 8555 §7.1.6): every authorization is valid before any challenge is attempted. That is the case of GÉANT TCS, served by HARICA since 2025-01-10 (it replaced Sectigo): an Enterprise ADMIN account (OV certificates) with pre-validated domains gets its certificates with no ACME challenge; an Enterprise User account (DV) is challenged like any other, and either way the domain's CAA must allow harica.gr (DFN: https://doku.tid.dfn.de/de:dfnpki:tcs:2025:acme). The directory URL, the EAB key ID and the HMAC key are all shown by https://cm.harica.gr for the ACME account -- of the form https://acme-v02.harica.gr/acme/<uuid>/directory in SURF's instructions (https://servicedesk.surf.nl/wiki/spaces/WIKI/pages/147098524/ACME, June 2025); older guides show another form, so copy it rather than build it. So an INTERNAL server reachable on neither port 80 nor 443 from the Internet can still get a publicly trusted certificate.
Certificates are obtained at the first TLS handshake whose SNI names one of ACME.Domains, or before any client with Source.Prefetch, then renewed by autocert before they expire. A client that connects by IP address sends no SNI and is served the FIRST domain's certificate, which it still checks against the address it dialled.
External Account Binding (RFC 8555 §7.3.4) ties the ACME account to an account at the CA. The HMAC key is a secret, so the configuration names a FILE holding it (base64url, as CAs hand it out), never the key itself.
One defect of x/crypto/acme is worked around here (golang/go#77704): after finalizing an order the CA is still processing, the client polls the Location header of the finalize response, which RFC 8555 does not put there; pebble and Buypass do not send it. The ACME client's transport supplies it from the order URL it saw earlier.
Tests ¶
The ACME tests obtain real certificates from pebble, Let's Encrypt's test CA, run in-process, and verify them with crypto/tls against pebble's root: tls-alpn-01, http-01, EAB required (absent, wrong and right keys), and a pre-validated domain issued with no challenge port open.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ACME ¶
type ACME struct {
// DirectoryURL is the CA's ACME directory, https only (RFC 8555 §6.1).
// Empty means Let's Encrypt production, [acme.LetsEncryptURL].
// For GÉANT TCS it is the Server URL cm.harica.gr shows for the ACME
// account, such as https://acme-v02.harica.gr/acme/<uuid>/directory.
DirectoryURL string
// Email is the account's contact address; optional.
Email string
// Domains are the names a certificate may be requested for: the host
// whitelist. Required. No IP address and no wildcard: autocert can do
// neither (it validates with tls-alpn-01 or http-01, and a wildcard needs
// dns-01).
Domains []string
// CacheDir holds the account key and the certificates, which are
// private keys. Required and absolute. Created 0700 if missing; one that
// exists and is accessible by group or others is refused (not checked on
// Windows, where mode bits do not describe access).
CacheDir string
// EABKeyID and EABHMACKeyFile are the External Account Binding (RFC
// 8555 §7.3.4) a CA such as HARICA, ZeroSSL or Sectigo requires to tie
// the ACME account to a customer account. Both or neither. The file
// holds the HMAC key as the CA hands it out, base64url; the key is a
// secret, which is why it is a file and never a value in the config.
EABKeyID string
EABHMACKeyFile string
// HTTPChallenge also answers http-01 (RFC 8555 §8.3) when tls-alpn-01
// fails. The caller must then serve [Source.HTTPHandler] on port 80.
HTTPChallenge bool
// HTTPClient talks to the CA. Nil means http.DefaultClient. Set it to
// trust a private CA's directory certificate, or to go through a proxy.
// It is copied, not modified: the copy's transport is wrapped to work
// around golang/go#77704 (see the package documentation).
HTTPClient *http.Client
}
ACME obtains the certificate from an ACME CA (RFC 8555) through golang.org/x/crypto/acme/autocert.
type Config ¶
type Config struct {
// CertFile and KeyFile are PEM files: the certificate chain, leaf first,
// and its private key. Both absolute. They are re-read when they change
// (see [Source.TLSConfig]).
CertFile, KeyFile string
// ACME obtains and renews the certificate from a CA instead.
ACME *ACME
// OnError, if set, is told about what the source could not do but
// survived: a changed certificate pair that does not parse or whose key
// does not match (the previous pair keeps being served), or an ACME
// issuance that failed for one of the configured domains. It is called
// from inside a TLS handshake, so it must not block.
OnError func(error)
}
Config says where the certificate comes from: a pair of files, or ACME. Exactly one of the two is given.
type Source ¶
type Source struct {
// contains filtered or unexported fields
}
Source serves the server certificate. It is safe for concurrent use.
func New ¶
New checks c and prepares the source. With files, the pair is read now and a pair that cannot be read, does not parse, or whose key does not match the certificate is an error now rather than at the first handshake. With ACME, the EAB key file is read and decoded and the cache directory created (0700) or checked; no request goes to the CA until a client connects.
func (*Source) Close ¶
Close stops serving: every handshake after it fails. Certificates already in the cache directory stay there. It always returns nil.
func (*Source) HTTPHandler ¶
HTTPHandler answers the http-01 challenge under /.well-known/acme-challenge/ and hands every other request to fallback; a nil fallback redirects to https. It is meaningful only with ACME and HTTPChallenge, and must then be served on port 80. Otherwise it is fallback itself, or a 404 handler when fallback is nil.
func (*Source) Prefetch ¶ added in v0.5.1
Prefetch obtains the certificate of every domain now, instead of at the first handshake that names it -- so that the first client does not wait for an issuance, and a server that no client has yet reached by name still has its certificate in hand. Certificates already cached and valid cost nothing.
With tls-alpn-01 or http-01 the CA connects back to validate the name, so call it once the TLS listener on 443, or HTTPHandler on 80, is serving. A CA account whose domains are pre-validated (HARICA Enterprise Admin, for GÉANT TCS) asks for no challenge, and Prefetch may run at once.
A failure is returned, joined, and also told to Config.OnError. It is a no-op with files.
func (*Source) TLSConfig ¶
TLSConfig returns a new tls.Config serving this source's certificate, with MinVersion TLS 1.2.
With files, the pair is checked for change at most once every 10 seconds, during a handshake; a changed pair that fails to load leaves the previous one in service and goes to Config.OnError.
With ACME, the certificate is obtained at the first handshake whose SNI names one of the domains -- or earlier, by Prefetch -- and renewed by autocert before it expires. A client that connects by IP address sends no SNI and is served the first domain's certificate. NextProtos then holds "acme-tls/1", the protocol of the tls-alpn-01 challenge (RFC 8737): APPEND your own protocols to it, never replace it, or the CA's validation fails. Append them, too: a Go TLS server that advertises ALPN protocols refuses a client whose offer shares none of them, so an NFS server must add "sunrpc" (RFC 9289). net/http's ServeTLS adds "http/1.1" (and "h2") by itself.