Skip to content

Instantly share code, notes, and snippets.

@ldez

ldez/acme.go Secret

Forked from mmalone/acme.go
Last active February 5, 2025 16:22
Show Gist options
  • Select an option

  • Save ldez/e975a1026b704e55f1d1f85143b377b7 to your computer and use it in GitHub Desktop.

Select an option

Save ldez/e975a1026b704e55f1d1f85143b377b7 to your computer and use it in GitHub Desktop.
package main
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"net/http"
"sync"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
)
const (
// The domain name for which we'll be getting a certificate
domain = "example.com"
// The email address to use during ACME registration
acmeEmail = "you@example.org"
// The ACME directory URL for your ACME server
acmeDirectoryURL = "https://example.org/acme/acme/directory"
// The root certificate for the CA that issued the ACME server's
// certificate.
rootCertificate = "/home/user/.step/certs/root_ca.crt"
// How frequently we should check whether our cert needs renewal
tickFrequency = 15 * time.Second
// The listen address for our HTTPS server
listenAddr = ":5443"
)
// getHTTPClient gets an HTTP client configured to trust our CA's root certificate.
func getHTTPClient(rootCert string) (*http.Client, error) {
certPool, err := lego.CreateCertPool([]string{rootCert}, true)
if err != nil {
return nil, err
}
return &http.Client{
Timeout: 2 * time.Minute,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
},
}, nil
}
// LegoUser implements registration.User, required by lego.
type LegoUser struct {
email string
registration *registration.Resource
key crypto.PrivateKey
}
func (l *LegoUser) GetEmail() string {
return l.email
}
func (l *LegoUser) GetRegistration() *registration.Resource {
return l.registration
}
func (l *LegoUser) GetPrivateKey() crypto.PrivateKey {
return l.key
}
// Uses techniques from https://diogomonica.com/2017/01/11/hitless-tls-certificate-rotation-in-go/
// to automatically rotate certificates when they're renewed.
// ACMECertManager manages ACME certificate renewals and makes it easy to use
// certificates with the tls package.`
type ACMECertManager struct {
sync.RWMutex
acmeClient *lego.Client
certificate *tls.Certificate
domains []string
leaf *x509.Certificate
resource *certificate.Resource
}
// NewACMECertManager configures an ACME client, creates & registers a new ACME
// user. After creating a client you must call ObtainCertificate and
// RenewCertificate yourself.
func NewACMECertManager(domains []string, email, rootCert, caDirURL string) (*ACMECertManager, error) {
// Create a new ACME user with a new key.
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
user := &LegoUser{
email: email,
key: key,
}
// Get an HTTPS client configured to trust our root certificate.
httpClient, err := getHTTPClient(rootCert)
if err != nil {
return nil, err
}
// Create a configuration using our HTTPS client, ACME server, user details.
config := &lego.Config{
CADirURL: caDirURL,
User: user,
HTTPClient: httpClient,
Certificate: lego.CertificateConfig{
KeyType: certcrypto.RSA2048,
Timeout: 30 * time.Second,
},
}
// Create an ACME client and configure use of `http-01` challenge
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80"))
if err != nil {
return nil, err
}
// Register our ACME user
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, err
}
user.registration = reg
return &ACMECertManager{
acmeClient: client,
domains: domains,
}, nil
}
// ObtainCertificate gets a new certificate using ACME. Not thread safe.
func (a *ACMECertManager) ObtainCertificate() error {
request := certificate.ObtainRequest{
Domains: a.domains,
Bundle: true,
}
resource, err := a.acmeClient.Certificate.Obtain(request)
if err != nil {
return err
}
return a.switchCertificate(resource)
}
// RenewCertificate renews an existing certificate using ACME. Not thread safe.
func (a *ACMECertManager) RenewCertificate() error {
resource, err := a.acmeClient.Certificate.RenewWithOptions(*a.resource, &certificate.RenewOptions{Bundle: true})
if err != nil {
return err
}
return a.switchCertificate(resource)
}
// GetCertificate locks around returning a tls.Certificate; use as tls.Config.GetCertificate.
func (a *ACMECertManager) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
a.RLock()
defer a.RUnlock()
return a.certificate, nil
}
// GetLeaf returns the currently valid leaf x509.Certificate
func (a *ACMECertManager) GetLeaf() x509.Certificate {
a.RLock()
defer a.RUnlock()
return *a.leaf
}
// NextRenewal returns when the certificate will be 2/3 of the way to expiration.
func (a *ACMECertManager) NextRenewal() time.Time {
leaf := a.GetLeaf()
lifetime := leaf.NotAfter.Sub(leaf.NotBefore).Seconds()
return leaf.NotBefore.Add(time.Duration(lifetime*2/3) * time.Second)
}
// NeedsRenewal returns true if the certificate's age is more than 2/3 it's
// lifetime.
func (a *ACMECertManager) NeedsRenewal() bool {
return time.Now().After(a.NextRenewal())
}
func (a *ACMECertManager) switchCertificate(newResource *certificate.Resource) error {
// The certificate.Resource represents our certificate as a PEM-encoded
// bundle of bytes. Let's process it. First create a tls.Certificate
// for use with the tls package.
crt, err := tls.X509KeyPair(newResource.Certificate, newResource.PrivateKey)
if err != nil {
return err
}
// Now create a x509.Certificate so we can figure out when the cert
// expires. Note that the first certificate in the bundle is the leaf.
// Go ahead and set crt.Leaf as an optimization.
leaf, err := x509.ParseCertificate(crt.Certificate[0])
if err != nil {
return err
}
crt.Leaf = leaf
a.Lock()
defer a.Unlock()
a.resource = newResource
a.certificate = &crt
a.leaf = leaf
return nil
}
func main() {
// Let's create a little web server that responds to TLS or mutually
// authenticated TLS connections. If we get a mutual TLS connection
// we'll respond with a friendly greeting using the client's name.
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
fmt.Fprintf(w, "Hello, TLS!\n")
} else {
name := r.TLS.PeerCertificates[0].Subject.CommonName
fmt.Fprintf(w, "Hello, %s!\n", name)
}
})
// Create a trust pool with our CA's root certificate; used to validate
// client certificates.
roots, err := lego.CreateCertPool([]string{rootCertificate}, true)
if err != nil {
log.Fatal(err)
}
// Configure our ACME cert manager and get a certificate using ACME!
acm, err := NewACMECertManager([]string{domain}, acmeEmail, rootCertificate, acmeDirectoryURL)
if err != nil {
log.Fatal("Error creating ACMECertManager", err)
}
err = acm.ObtainCertificate()
if err != nil {
log.Fatal("Error loading certificate and key", err)
}
// Create a TLS configuration for our HTTPS server. The fancy bits here
// are commented.
cfg := &tls.Config{
// This makes client authentication optional. Switch to
// tls.RequireAndVerifyClientCert to require authentication.
ClientAuth: tls.VerifyClientCertIfGiven,
// We'll be trusting client certificates issued by our CA.
ClientCAs: roots,
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
// Dynamically load certificate from ACMECertManager with every
// connection, so renewals work.
GetCertificate: acm.GetCertificate,
}
// Make a server!
srv := &http.Server{
Addr: listenAddr,
Handler: mux,
TLSConfig: cfg,
}
// Schedule periodic certificate renewal (do ACME again periodically).
// We'll tick every timeFrequency but only renew if the certificate
// is approaching expiration. That'll give us some resilience to CA
// downtime.
done := make(chan struct{})
go func() {
ticker := time.NewTicker(tickFrequency)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if acm.NeedsRenewal() {
fmt.Println("Renewing certificate")
err := acm.RenewCertificate()
if err != nil {
log.Println("Error loading certificate and key", err)
} else {
leaf := acm.GetLeaf()
fmt.Printf("Renewed certificate: %s [%s - %s]\n", leaf.Subject, leaf.NotBefore, leaf.NotAfter)
fmt.Printf("Next renewal at %s (%s)\n", acm.NextRenewal(), acm.NextRenewal().Sub(time.Now()))
}
} else {
fmt.Printf("Waiting to renew at %s (%s)\n", acm.NextRenewal(), acm.NextRenewal().Sub(time.Now()))
}
case <-done:
return
}
}
}()
defer close(done)
log.Printf("Listening on %s\n", listenAddr)
// Start serving HTTPS!
err = srv.ListenAndServeTLS("", "")
if err != nil {
log.Fatal("ListenAndServerTLS: ", err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment