Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • ai3/tools/acmeserver
  • godog/acmeserver
  • svp-bot/acmeserver
3 results
Show changes
Commits on Source (106)
include:
- "https://git.autistici.org/pipelines/debian/raw/master/common.yml"
- "https://git.autistici.org/pipelines/images/test/golang/raw/master/ci.yml"
......@@ -13,7 +13,6 @@ import (
"io/ioutil"
"log"
"net/http"
"path/filepath"
"golang.org/x/crypto/acme"
)
......@@ -46,6 +45,9 @@ func NewACME(config *Config) (*ACME, error) {
if config.Email == "" {
return nil, errors.New("configuration parameter 'email' is unset")
}
if config.AccountKeyPath == "" {
return nil, errors.New("configuration parameter 'account_key_path' is unset")
}
var h http.Handler
v := make(map[string]validator)
......@@ -77,7 +79,7 @@ func NewACME(config *Config) (*ACME, error) {
return &ACME{
email: config.Email,
accountKeyPath: filepath.Join(config.Dir, "account.key"),
accountKeyPath: config.AccountKeyPath,
directoryURL: directoryURL,
handler: h,
validators: v,
......@@ -131,10 +133,16 @@ func (a *ACME) acmeClient(ctx context.Context) (*acme.Client, error) {
// account is already registered we get a StatusConflict,
// which we can ignore.
_, err = client.Register(ctx, ac, func(_ string) bool { return true })
if ae, ok := err.(*acme.Error); err == nil || ok && ae.StatusCode == http.StatusConflict {
if ae, ok := err.(*acme.Error); err == nil || err == acme.ErrAccountAlreadyExists || (ok && ae.StatusCode == http.StatusConflict) {
a.client = client
err = nil
}
// Fetch account info and display it.
if acct, err := client.GetReg(ctx, ""); err == nil {
log.Printf("ACME account %s", acct.URI)
}
return a.client, err
}
......@@ -147,7 +155,8 @@ func (a *ACME) GetCertificate(ctx context.Context, key crypto.Signer, c *certCon
return nil, nil, err
}
if err = a.verifyAll(ctx, client, c); err != nil {
o, err := a.verifyAll(ctx, client, c)
if err != nil {
return nil, nil, err
}
......@@ -155,7 +164,7 @@ func (a *ACME) GetCertificate(ctx context.Context, key crypto.Signer, c *certCon
if err != nil {
return nil, nil, err
}
der, _, err = client.CreateCert(ctx, csr, 0, true)
der, _, err = client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
if err != nil {
return nil, nil, err
}
......@@ -166,56 +175,67 @@ func (a *ACME) GetCertificate(ctx context.Context, key crypto.Signer, c *certCon
return der, leaf, nil
}
func (a *ACME) verifyAll(ctx context.Context, client *acme.Client, c *certConfig) error {
for _, domain := range c.Names {
if err := a.verify(ctx, client, c, domain); err != nil {
return err
}
}
return nil
}
func (a *ACME) verify(ctx context.Context, client *acme.Client, c *certConfig, domain string) error {
func (a *ACME) verifyAll(ctx context.Context, client *acme.Client, c *certConfig) (*acme.Order, error) {
// Make an authorization request to the ACME server, and
// verify that it returns a valid response with challenges.
authz, err := client.Authorize(ctx, domain)
o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(c.Names...))
if err != nil {
return err
}
switch authz.Status {
case acme.StatusValid:
return nil // already authorized
case acme.StatusInvalid:
return fmt.Errorf("invalid authorization %q", authz.URI)
return nil, fmt.Errorf("AuthorizeOrder failed: %v", err)
}
// Pick a challenge that matches our preferences and the
// available validators. The validator fulfills the challenge,
// and returns a cleanup function that we're going to call
// before we return. All steps are sequential and idempotent.
chal := a.pickChallenge(authz.Challenges, c)
if chal == nil {
return fmt.Errorf("unable to authorize %q", domain)
}
v, ok := a.validators[chal.Type]
if !ok {
return fmt.Errorf("challenge type '%s' is not available", chal.Type)
switch o.Status {
case acme.StatusReady:
return o, nil // already authorized
case acme.StatusPending:
default:
return nil, fmt.Errorf("invalid new order status %q", o.Status)
}
cleanup, err := v.Fulfill(ctx, client, domain, chal)
if err != nil {
return err
for _, zurl := range o.AuthzURLs {
z, err := client.GetAuthorization(ctx, zurl)
if err != nil {
return nil, fmt.Errorf("GetAuthorization(%s) failed: %v", zurl, err)
}
if z.Status != acme.StatusPending {
continue
}
// Pick a challenge that matches our preferences and the
// available validators. The validator fulfills the challenge,
// and returns a cleanup function that we're going to call
// before we return. All steps are sequential and idempotent.
chal := a.pickChallenge(z.Challenges, c)
if chal == nil {
return nil, fmt.Errorf("unable to authorize %q", c.Names)
}
v, ok := a.validators[chal.Type]
if !ok {
return nil, fmt.Errorf("challenge type '%s' is not available", chal.Type)
}
log.Printf("attempting fulfillment for %q (identifier: %+v)", c.Names, z.Identifier)
for _, domain := range c.Names {
cleanup, err := v.Fulfill(ctx, client, domain, chal)
if err != nil {
return nil, fmt.Errorf("fulfillment failed: %v", err)
}
defer cleanup()
}
if _, err := client.Accept(ctx, chal); err != nil {
return nil, fmt.Errorf("challenge accept failed: %v", err)
}
if _, err := client.WaitAuthorization(ctx, z.URI); err != nil {
return nil, fmt.Errorf("WaitAuthorization(%s) failed: %v", z.URI, err)
}
}
defer cleanup()
// Tell the ACME server that we've accepted the challenge, and
// then wait, possibly for some time, until there is an
// authorization response (either successful or not) from the
// server.
if _, err = client.Accept(ctx, chal); err != nil {
return err
// Authorizations are satisfied, wait for the CA
// to update the order status.
if _, err = client.WaitOrder(ctx, o.URI); err != nil {
return nil, err
}
_, err = client.WaitAuthorization(ctx, authz.URI)
return err
return o, nil
}
// Pick a challenge with the right type from the Challenge response
......
......@@ -3,38 +3,38 @@ package main
import (
"context"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"git.autistici.org/ai3/acmeserver"
"git.autistici.org/ai3/go-common/serverutil"
"gopkg.in/yaml.v2"
"git.autistici.org/ai3/tools/acmeserver"
"gopkg.in/yaml.v3"
)
var (
addr = flag.String("addr", ":2780", "tcp `address` to listen on")
configFile = flag.String("config", "/etc/acmeserver/config.yml", "configuration `file`")
configFile = flag.String("config", "/etc/acme/config.yml", "configuration `file`")
)
// Config ties together the acmeserver Config and the standard
// serverutil HTTP server configuration.
type Config struct {
ACME *acmeserver.Config `yaml:",inline"`
ACME acmeserver.Config `yaml:",inline"`
Server *serverutil.ServerConfig `yaml:"http_server"`
}
func loadConfig(path string) (*Config, error) {
// Read YAML config.
data, err := ioutil.ReadFile(path) // nolint: gosec
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
if err := yaml.NewDecoder(f).Decode(&config); err != nil {
return nil, err
}
return &config, nil
......@@ -52,7 +52,7 @@ func main() {
var h http.Handler
var cg acmeserver.CertGenerator
if !config.ACME.Testing {
acme, err := acmeserver.NewACME(config.ACME) // nolint: vetshadow
acme, err := acmeserver.NewACME(&config.ACME) // nolint: vetshadow
if err != nil {
log.Fatal(err)
}
......@@ -69,7 +69,7 @@ func main() {
})
}
m, err := acmeserver.NewManager(config.ACME, cg)
m, err := acmeserver.NewManager(&config.ACME, cg)
if err != nil {
log.Fatal(err)
}
......
......@@ -3,30 +3,39 @@ package acmeserver
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"git.autistici.org/ai3/go-common/clientutil"
)
// Config holds the configuration for an acmeserver instance.
//
// The reason for supporting multiple config_dirs is to allow
// integration with third-party automation systems: in some cases,
// automation tools need control of an entire directory in order to
// safely delete entries that no longer exist.
//
// nolint: maligned
type Config struct {
Addr string `yaml:"addr"`
Testing bool `yaml:"testing"`
DirectoryURL string `yaml:"directory_url"`
DefaultChallengeType string `yaml:"default_challenge"`
UseRSA bool `yaml:"use_rsa"`
RenewalDays int `yaml:"renewal_days"`
Email string `yaml:"email"`
Dir string `yaml:"cert_dir"`
ReplDS *clientutil.BackendConfig `yaml:"replds"`
AccountKeyPath string `yaml:"account_key_path"`
Email string `yaml:"email"`
Dirs []string `yaml:"config_dirs"`
Output struct {
Path string `yaml:"path"`
ReplDS *clientutil.BackendConfig `yaml:"replds"`
} `yaml:"output"`
HTTP struct {
Enabled bool `yaml:"enabled"`
......@@ -68,23 +77,23 @@ func (c *certConfig) check() error {
}
func readCertConfigs(path string) ([]*certConfig, error) {
data, err := ioutil.ReadFile(path) // nolint: gosec
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var cc []*certConfig
if err := yaml.Unmarshal(data, &cc); err != nil {
if err := yaml.NewDecoder(f).Decode(&cc); err != nil {
return nil, err
}
return cc, nil
}
func readCertConfigsFromDir(dir string) ([]*certConfig, error) {
func readCertConfigsFromDir(dir string, certs []*certConfig) ([]*certConfig, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.yml"))
if err != nil {
return nil, err
}
var out []*certConfig
for _, f := range files {
cc, err := readCertConfigs(f)
if err != nil {
......@@ -97,8 +106,20 @@ func readCertConfigsFromDir(dir string) ([]*certConfig, error) {
log.Printf("configuration error in %s: %v", f, err)
continue
}
out = append(out, c)
certs = append(certs, c)
}
}
return certs, nil
}
func readCertConfigsFromDirs(dirs []string) ([]*certConfig, error) {
var out []*certConfig
for _, dir := range dirs {
certs, err := readCertConfigsFromDir(dir, out)
if err != nil {
return nil, err
}
out = certs
}
return out, nil
}
......@@ -7,6 +7,7 @@ User=acmeserver
Group=acmeserver
EnvironmentFile=-/etc/default/acmeserver
ExecStart=/usr/bin/acmeserver --addr $ADDR
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
# Hardening
......@@ -16,6 +17,7 @@ PrivateDevices=yes
ProtectHome=yes
ProtectSystem=full
ReadOnlyDirectories=/
ReadWriteDirectories=/var/lib/acme
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
[Install]
......
......@@ -2,4 +2,4 @@ acmeserver (2.0) unstable; urgency=medium
* Initial Release.
-- Autistici/Inventati <debian@autistici.org> Sat, 15 Jun 2018 09:23:40 +0000
-- Autistici/Inventati <debian@autistici.org> Fri, 15 Jun 2018 09:23:40 +0000
10
......@@ -2,12 +2,12 @@ Source: acmeserver
Section: admin
Priority: optional
Maintainer: Autistici/Inventati <debian@autistici.org>
Build-Depends: debhelper (>=9), golang-go, dh-systemd, dh-golang
Build-Depends: debhelper-compat (= 13), golang-any (>= 1.11), dh-golang
Standards-Version: 3.9.6
Package: acmeserver
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Built-Using: ${misc:Built-Using}
Description: ACME server
Automatically manages and renews public SSL certificates.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: acmeserver
Source: <https://git.autistici.org/ai3/acmeserver>
Source: <https://git.autistici.org/ai3/tools/acmeserver>
Files: *
Copyright: 2018 Autistici/Inventati <info@autistici.org>
......
#!/usr/bin/make -f
export DH_GOPKG = git.autistici.org/ai3/acmeserver
export DH_GOPKG = git.autistici.org/ai3/tools/acmeserver
export DH_GOLANG_EXCLUDES = vendor
export DH_GOLANG_INSTALL_ALL := 1
%:
dh $@ --with systemd --with golang --buildsystem golang
dh $@ --with golang --buildsystem golang
override_dh_install:
rm -fr $(CURDIR)/debian/acmeserver/usr/share/gocode
dh_install
override_dh_auto_install:
dh_auto_install -- --no-source
override_dh_systemd_enable:
dh_systemd_enable --no-enable
override_dh_systemd_start:
dh_systemd_start --no-start
override_dh_installsystemd:
dh_installsystemd --no-enable
override_dh_installsystemd:
dh_installsystemd --no-start
......@@ -10,6 +10,7 @@ import (
"github.com/miekg/dns"
"golang.org/x/crypto/acme"
"golang.org/x/net/publicsuffix"
)
const (
......@@ -87,8 +88,16 @@ func (d *dnsValidator) client() *dns.Client {
}
func (d *dnsValidator) Fulfill(ctx context.Context, client *acme.Client, domain string, chal *acme.Challenge) (func(), error) {
zone := domain[strings.Index(domain, ".")+1:]
fqdn := dns.Fqdn(domain)
domain = strings.TrimPrefix(domain, "*.")
zone, err := publicsuffix.EffectiveTLDPlusOne(domain)
if err != nil {
return nil, fmt.Errorf("could not determine effective tld: %w", err)
}
zone = dns.Fqdn(zone)
fqdn := dns.Fqdn("_acme-challenge." + domain)
value, err := client.DNS01ChallengeRecord(chal.Token)
if err != nil {
return nil, err
......@@ -109,6 +118,7 @@ func (d *dnsValidator) Fulfill(ctx context.Context, client *acme.Client, domain
}
func (d *dnsValidator) updateNS(ctx context.Context, ns, zone, fqdn, value string, remove bool) error {
log.Printf("updateNS(%s, %s, %s, %s, %v)", ns, zone, fqdn, value, remove)
rrs := d.makeRR(fqdn, value, rfc2136Timeout)
m := d.makeMsg(zone, rrs, remove)
c := d.client()
......
module git.autistici.org/ai3/tools/acmeserver
go 1.19
require (
git.autistici.org/ai3/go-common v0.0.0-20230816213645-b3aa3fb514d6
git.autistici.org/ai3/tools/replds v0.0.0-20230923170339-b6e6e3cc032b
github.com/miekg/dns v1.1.50
github.com/prometheus/client_golang v1.12.2
golang.org/x/crypto v0.24.0
golang.org/x/net v0.26.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/openzipkin/zipkin-go v0.4.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.9.0 // indirect
go.opentelemetry.io/otel v1.10.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.9.0 // indirect
go.opentelemetry.io/otel/metric v0.31.0 // indirect
go.opentelemetry.io/otel/sdk v1.10.0 // indirect
go.opentelemetry.io/otel/trace v1.10.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
This diff is collapsed.
......@@ -13,14 +13,18 @@ import (
"errors"
"io"
"log"
"path/filepath"
"sync"
"time"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/ai3/tools/replds"
"github.com/prometheus/client_golang/prometheus"
)
var (
defaultRenewalDays = 21
updateInterval = 1 * time.Minute
)
// certInfo represents what we know about the state of the certificate
// at runtime.
type certInfo struct {
......@@ -72,50 +76,54 @@ type CertGenerator interface {
GetCertificate(context.Context, crypto.Signer, *certConfig) ([][]byte, *x509.Certificate, error)
}
// Manager periodically renews certificates before they expire, and
// responds to http-01 validation requests.
// Manager periodically renews certificates before they expire.
type Manager struct {
configDir string
configDirs []string
useRSA bool
storage certStorage
certs []*certInfo
certGen CertGenerator
renewalDays int
configCh chan []*certConfig
doneCh chan bool
mx sync.Mutex
certs []*certInfo
}
// NewManager creates a new Manager with the given configuration.
func NewManager(config *Config, certGen CertGenerator) (*Manager, error) {
// Validate the configuration.
if config.Dir == "" {
return nil, errors.New("configuration parameter 'cert_dir' is unset")
if len(config.Dirs) == 0 {
return nil, errors.New("configuration parameter 'config_dirs' is unset")
}
if config.Output.Path == "" {
return nil, errors.New("'output.path' is unset")
}
if config.RenewalDays <= 0 {
config.RenewalDays = defaultRenewalDays
}
m := &Manager{
useRSA: config.UseRSA,
configDir: filepath.Join(config.Dir, "config"),
configDirs: config.Dirs,
doneCh: make(chan bool),
configCh: make(chan []*certConfig, 1),
certGen: certGen,
renewalDays: config.RenewalDays,
}
if m.renewalDays <= 0 {
m.renewalDays = 15
}
ds := &dirStorage{root: filepath.Join(config.Dir, "certs")}
if config.ReplDS == nil {
ds := &dirStorage{root: config.Output.Path}
if config.Output.ReplDS == nil {
m.storage = ds
} else {
be, err := clientutil.NewBackend(config.ReplDS)
r, err := replds.NewPublicClient(config.Output.ReplDS)
if err != nil {
return nil, err
}
m.storage = &replStorage{
dirStorage: ds,
replClient: be,
replClient: r,
}
}
......@@ -126,7 +134,7 @@ func NewManager(config *Config, certGen CertGenerator) (*Manager, error) {
// cause background processing to stop, interrupting all running
// updates.
func (m *Manager) Start(ctx context.Context) error {
domains, err := readCertConfigsFromDir(m.configDir)
domains, err := readCertConfigsFromDirs(m.configDirs)
if err != nil {
return err
}
......@@ -145,7 +153,7 @@ func (m *Manager) Wait() {
// Reload configuration.
func (m *Manager) Reload() {
domains, err := readCertConfigsFromDir(m.configDir)
domains, err := readCertConfigsFromDirs(m.configDirs)
if err != nil {
log.Printf("error reading config: %v", err)
return
......@@ -156,7 +164,7 @@ func (m *Manager) Reload() {
var (
renewalTimeout = 10 * time.Minute
errorRetryTimeout = 10 * time.Minute
errorRetryTimeout = 6 * time.Hour
)
func (m *Manager) updateAllCerts(ctx context.Context, certs []*certInfo) {
......@@ -239,55 +247,41 @@ func (m *Manager) loadConfig(certs []*certConfig) []*certInfo {
return out
}
func (m *Manager) getCerts() []*certInfo {
m.mx.Lock()
defer m.mx.Unlock()
return m.certs
}
func (m *Manager) setCerts(certs []*certInfo) {
m.mx.Lock()
m.certs = certs
m.mx.Unlock()
}
// This channel is used by the testing code to trigger an update,
// without having to wait for the timer to tick.
var testUpdateCh = make(chan bool)
func (m *Manager) loop(ctx context.Context) {
// Updates are long-term jobs, so they should be
// interruptible. We run updates in a separate goroutine, and
// cancel them when the configuration is reloaded or on exit.
var upCancel context.CancelFunc
var wg sync.WaitGroup
startUpdate := func(certs []*certInfo) context.CancelFunc {
// Ensure the previous update has finished.
wg.Wait()
upCtx, cancel := context.WithCancel(ctx)
wg.Add(1)
go func() {
m.updateAllCerts(upCtx, certs)
wg.Done()
}()
return cancel
}
// Cancel the running update, if any. Called on config
// updates, when exiting.
cancelUpdate := func() {
if upCancel != nil {
upCancel()
reloadCh := make(chan interface{}, 1)
go func() {
for config := range m.configCh {
certs := m.loadConfig(config)
m.setCerts(certs)
reloadCh <- certs
}
wg.Wait()
}
defer cancelUpdate()
}()
tick := time.NewTicker(5 * time.Minute)
defer tick.Stop()
for {
select {
case <-tick.C:
upCancel = startUpdate(m.certs)
case <-testUpdateCh:
upCancel = startUpdate(m.certs)
case certDomains := <-m.configCh:
cancelUpdate()
m.certs = m.loadConfig(certDomains)
case <-ctx.Done():
return
}
}
runWithUpdates(
ctx,
func(ctx context.Context, value interface{}) {
certs := value.([]*certInfo)
m.updateAllCerts(ctx, certs)
},
reloadCh,
updateInterval,
)
}
func concatDER(der [][]byte) []byte {
......@@ -306,10 +300,8 @@ func concatDER(der [][]byte) []byte {
func certRequest(key crypto.Signer, domains []string) ([]byte, error) {
req := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: domains[0]},
}
if len(domains) > 1 {
req.DNSNames = domains[1:]
Subject: pkix.Name{CommonName: domains[0]},
DNSNames: domains,
}
return x509.CreateCertificateRequest(rand.Reader, req, key)
}
......
......@@ -26,6 +26,16 @@ func (s *slowACME) GetCertificate(ctx context.Context, _ crypto.Signer, _ *certC
}
}
func newTestConfig(dir string) *Config {
c := Config{
Dirs: []string{filepath.Join(dir, "config")},
AccountKeyPath: filepath.Join(dir, "account.key"),
Email: "test@example.com",
}
c.Output.Path = filepath.Join(dir, "certs")
return &c
}
// Create a new test function.
// The first function returned is a cleanup callback.
// The second function returned is the cancel callback.
......@@ -42,10 +52,7 @@ func newTestManager(t testing.TB, g CertGenerator) (func(), context.CancelFunc,
0644,
)
m, err := NewManager(&Config{
Dir: dir,
Email: "test@example.com",
}, g)
m, err := NewManager(newTestConfig(dir), g)
if err != nil {
t.Fatal(err)
}
......@@ -70,36 +77,44 @@ func TestManager_Reload(t *testing.T) {
defer cleanup()
// Data race: we read data owned by another goroutine!
if len(m.certs) < 1 {
certs := m.getCerts()
if len(certs) < 1 {
t.Fatal("configuration not loaded?")
}
if m.certs[0].cn() != "example.com" {
t.Fatalf("certs[0].cn() is %s, expected example.com", m.certs[0].cn())
if certs[0].cn() != "example.com" {
t.Fatalf("certs[0].cn() is %s, expected example.com", certs[0].cn())
}
// Try a reload, catch obvious errors.
m.Reload()
time.Sleep(50 * time.Millisecond)
certs = m.getCerts()
if len(m.certs) != 1 {
if len(certs) != 1 {
t.Fatalf("certs count is %d, expected 1", len(m.certs))
}
if m.certs[0].cn() != "example.com" {
t.Fatalf("certs[0].cn() is %s, expected example.com", m.certs[0].cn())
if certs[0].cn() != "example.com" {
t.Fatalf("certs[0].cn() is %s, expected example.com", certs[0].cn())
}
}
func TestManager_NewCert(t *testing.T) {
var oldUpdateInterval time.Duration
oldUpdateInterval, updateInterval = updateInterval, 50*time.Millisecond
defer func() {
updateInterval = oldUpdateInterval
}()
cleanup, _, m := newTestManager(t, NewSelfSignedCertGenerator())
defer cleanup()
now := time.Now()
ci := m.certs[0]
certs := m.getCerts()
ci := certs[0]
if ci.retryDeadline.After(now) {
t.Fatalf("retry deadline is in the future: %v", ci.retryDeadline)
}
testUpdateCh <- true
time.Sleep(100 * time.Millisecond)
// Verify that the retry/renewal timestamp is in the future.
......@@ -114,11 +129,11 @@ func TestManager_NewCert(t *testing.T) {
// Verify that the credentials have successfully been written
// to storage.
p := filepath.Join(m.configDir, "../certs/example.com/cert.pem")
p := filepath.Join(m.configDirs[0], "../certs/example.com/cert.pem")
if _, err := os.Stat(p); err != nil {
t.Fatalf("file not created: %v", err)
}
p = filepath.Join(m.configDir, "../certs/example.com/private_key.pem")
p = filepath.Join(m.configDirs[0], "../certs/example.com/privkey.pem")
if _, err := os.Stat(p); err != nil {
t.Fatalf("file not created: %v", err)
}
......@@ -128,7 +143,7 @@ func TestManager_NewCert(t *testing.T) {
m.Reload()
time.Sleep(50 * time.Millisecond)
ci = m.certs[0]
ci = m.getCerts()[0]
if !ci.valid {
t.Fatal("certificate is invalid after a reload")
}
......
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}
......@@ -16,8 +16,7 @@ import (
"strings"
"time"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/ai3/replds"
"git.autistici.org/ai3/tools/replds"
)
type dirStorage struct {
......@@ -26,7 +25,7 @@ type dirStorage struct {
func (d *dirStorage) GetCert(cn string) ([][]byte, crypto.Signer, error) {
certPath := filepath.Join(d.root, cn, "fullchain.pem")
keyPath := filepath.Join(d.root, cn, "private_key.pem")
keyPath := filepath.Join(d.root, cn, "privkey.pem")
der, err := parseCertsFromFile(certPath)
if err != nil {
......@@ -54,7 +53,7 @@ func (d *dirStorage) PutCert(cn string, der [][]byte, key crypto.Signer) error {
for path, data := range filemap {
var mode os.FileMode = 0644
if strings.HasSuffix(path, "private_key.pem") {
if strings.HasSuffix(path, "privkey.pem") {
mode = 0400
}
log.Printf("writing %s (%03o)", path, mode)
......@@ -80,11 +79,19 @@ func dumpCertsAndKey(cn string, der [][]byte, key crypto.Signer) (map[string][]b
}
m[filepath.Join(cn, "cert.pem")] = data
if len(der) > 1 {
data, err = encodeCerts(der[1:])
if err != nil {
return nil, err
}
m[filepath.Join(cn, "chain.pem")] = data
}
data, err = encodePrivateKey(key)
if err != nil {
return nil, err
}
m[filepath.Join(cn, "private_key.pem")] = data
m[filepath.Join(cn, "privkey.pem")] = data
return m, nil
}
......@@ -93,7 +100,7 @@ func dumpCertsAndKey(cn string, der [][]byte, key crypto.Signer) (map[string][]b
// certificates to replds instead.
type replStorage struct {
*dirStorage
replClient clientutil.Backend
replClient replds.PublicClient
}
func (d *replStorage) PutCert(cn string, der [][]byte, key crypto.Signer) error {
......@@ -105,7 +112,7 @@ func (d *replStorage) PutCert(cn string, der [][]byte, key crypto.Signer) error
now := time.Now()
var req replds.SetNodesRequest
for path, data := range filemap {
req.Nodes = append(req.Nodes, replds.Node{
req.Nodes = append(req.Nodes, &replds.Node{
Path: path,
Value: data,
Timestamp: now,
......@@ -115,8 +122,8 @@ func (d *replStorage) PutCert(cn string, der [][]byte, key crypto.Signer) error
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
var resp replds.SetNodesResponse
if err := clientutil.DoJSONHTTPRequest(ctx, d.replClient.Client(""), d.replClient.URL("")+"/api/set_nodes", req, &resp); err != nil {
resp, err := d.replClient.SetNodes(ctx, &req)
if err != nil {
return err
}
if resp.HostsOk < 1 {
......@@ -172,7 +179,7 @@ func encodePrivateKey(key crypto.Signer) ([]byte, error) {
switch priv := key.(type) {
case *rsa.PrivateKey:
pb = &pem.Block{
Type: "PRIVATE KEY",
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
}
case *ecdsa.PrivateKey:
......
package acmeserver
import (
"context"
"sync"
"time"
)
// Updates are long-term jobs, so they should be interruptible. We run
// updates in a separate goroutine, and cancel them when the
// configuration is reloaded or on exit. A semaphore ensures that only
// one update goroutine will be running at any given time (without
// other ones piling up).
func runWithUpdates(ctx context.Context, fn func(context.Context, interface{}), reloadCh <-chan interface{}, updateInterval time.Duration) {
// Function to cancel the current update, and the associated
// WaitGroup to wait for its termination.
var upCancel context.CancelFunc
var wg sync.WaitGroup
sem := make(chan struct{}, 1)
startUpdate := func(value interface{}) context.CancelFunc {
// Acquire the semaphore, return if we fail to.
// Equivalent to a 'try-lock' construct.
select {
case sem <- struct{}{}:
default:
return nil
}
defer func() {
<-sem
}()
ctx, cancel := context.WithCancel(ctx)
wg.Add(1)
go func() {
fn(ctx, value)
wg.Done()
}()
return cancel
}
// Cancel the running update, if any. Called on config
// updates, when exiting.
cancelUpdate := func() {
if upCancel != nil {
upCancel()
upCancel = nil
}
wg.Wait()
}
defer cancelUpdate()
var cur interface{}
tick := time.NewTicker(updateInterval)
defer tick.Stop()
for {
select {
case <-tick.C:
// Do not cancel running update when running the ticker.
if cancel := startUpdate(cur); cancel != nil {
upCancel = cancel
}
case value := <-reloadCh:
// Cancel the running update when configuration is reloaded.
cancelUpdate()
cur = value
case <-ctx.Done():
return
}
}
}
stages:
- test
run_tests:
stage: test
image: registry.git.autistici.org/ai3/docker/test/golang:master
script:
- run-go-test ./...
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: cover.xml
junit: report.xml
ai3/go-common
===
Common code for ai3 services and tools.
A quick overview of the contents:
* [client](clientutil/) and [server](serverutil/) HTTP-based
"RPC" implementation, just JSON POST requests but with retries,
backoff, timeouts, tracing, etc.
* [server implementation of a generic line-based protocol over a UNIX
socket](unix/).
* a [LDAP connection pool](ldap/).
* utilities to [serialize composite data types](ldap/compositetypes/)
used in our LDAP database.
* a [password hashing library](pwhash/) that uses fancy advanced
crypto by default but is also backwards compatible with old
libc crypto.
* utilities to [manage encryption keys](userenckey/), themselves
encrypted with a password and a KDF.