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 (3)
...@@ -7,6 +7,6 @@ require ( ...@@ -7,6 +7,6 @@ require (
git.autistici.org/ai3/tools/replds v0.0.0-20220814170053-28106a9463f5 git.autistici.org/ai3/tools/replds v0.0.0-20220814170053-28106a9463f5
github.com/miekg/dns v1.1.50 github.com/miekg/dns v1.1.50
github.com/prometheus/client_golang v1.12.2 github.com/prometheus/client_golang v1.12.2
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
...@@ -855,6 +855,8 @@ golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5y ...@@ -855,6 +855,8 @@ golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
......
...@@ -20,6 +20,11 @@ import ( ...@@ -20,6 +20,11 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
var (
defaultRenewalDays = 21
updateInterval = 1 * time.Minute
)
// certInfo represents what we know about the state of the certificate // certInfo represents what we know about the state of the certificate
// at runtime. // at runtime.
type certInfo struct { type certInfo struct {
...@@ -71,18 +76,19 @@ type CertGenerator interface { ...@@ -71,18 +76,19 @@ type CertGenerator interface {
GetCertificate(context.Context, crypto.Signer, *certConfig) ([][]byte, *x509.Certificate, error) GetCertificate(context.Context, crypto.Signer, *certConfig) ([][]byte, *x509.Certificate, error)
} }
// Manager periodically renews certificates before they expire, and // Manager periodically renews certificates before they expire.
// responds to http-01 validation requests.
type Manager struct { type Manager struct {
configDirs []string configDirs []string
useRSA bool useRSA bool
storage certStorage storage certStorage
certs []*certInfo
certGen CertGenerator certGen CertGenerator
renewalDays int renewalDays int
configCh chan []*certConfig configCh chan []*certConfig
doneCh chan bool doneCh chan bool
mx sync.Mutex
certs []*certInfo
} }
// NewManager creates a new Manager with the given configuration. // NewManager creates a new Manager with the given configuration.
...@@ -94,6 +100,9 @@ func NewManager(config *Config, certGen CertGenerator) (*Manager, error) { ...@@ -94,6 +100,9 @@ func NewManager(config *Config, certGen CertGenerator) (*Manager, error) {
if config.Output.Path == "" { if config.Output.Path == "" {
return nil, errors.New("'output.path' is unset") return nil, errors.New("'output.path' is unset")
} }
if config.RenewalDays <= 0 {
config.RenewalDays = defaultRenewalDays
}
m := &Manager{ m := &Manager{
useRSA: config.UseRSA, useRSA: config.UseRSA,
...@@ -103,9 +112,6 @@ func NewManager(config *Config, certGen CertGenerator) (*Manager, error) { ...@@ -103,9 +112,6 @@ func NewManager(config *Config, certGen CertGenerator) (*Manager, error) {
certGen: certGen, certGen: certGen,
renewalDays: config.RenewalDays, renewalDays: config.RenewalDays,
} }
if m.renewalDays <= 0 {
m.renewalDays = 15
}
ds := &dirStorage{root: config.Output.Path} ds := &dirStorage{root: config.Output.Path}
if config.Output.ReplDS == nil { if config.Output.ReplDS == nil {
...@@ -241,71 +247,41 @@ func (m *Manager) loadConfig(certs []*certConfig) []*certInfo { ...@@ -241,71 +247,41 @@ func (m *Manager) loadConfig(certs []*certConfig) []*certInfo {
return out 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, // This channel is used by the testing code to trigger an update,
// without having to wait for the timer to tick. // without having to wait for the timer to tick.
var testUpdateCh = make(chan bool) var testUpdateCh = make(chan bool)
func (m *Manager) loop(ctx context.Context) { func (m *Manager) loop(ctx context.Context) {
// Updates are long-term jobs, so they should be reloadCh := make(chan interface{}, 1)
// interruptible. We run updates in a separate goroutine, and go func() {
// cancel them when the configuration is reloaded or on exit. for config := range m.configCh {
// A simple channel is used as a semaphore, so that only one certs := m.loadConfig(config)
// update goroutine can be running at any given time (without m.setCerts(certs)
// other ones piling up). reloadCh <- certs
var upCancel context.CancelFunc
var wg sync.WaitGroup
sem := make(chan struct{}, 1)
startUpdate := func(certs []*certInfo) context.CancelFunc {
// Acquire the semaphore, return if we fail to.
select {
case sem <- struct{}{}:
default:
return nil
}
defer func() {
<-sem
}()
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()
upCancel = nil
} }
wg.Wait() }()
}
defer cancelUpdate()
tick := time.NewTicker(5 * time.Minute) runWithUpdates(
defer tick.Stop() ctx,
for { func(ctx context.Context, value interface{}) {
var c func() certs := value.([]*certInfo)
select { m.updateAllCerts(ctx, certs)
case <-tick.C: },
c = startUpdate(m.certs) reloadCh,
case <-testUpdateCh: updateInterval,
c = startUpdate(m.certs) )
case certDomains := <-m.configCh:
cancelUpdate()
m.certs = m.loadConfig(certDomains)
case <-ctx.Done():
return
}
if c != nil {
upCancel = nil
}
}
} }
func concatDER(der [][]byte) []byte { func concatDER(der [][]byte) []byte {
...@@ -324,7 +300,7 @@ func concatDER(der [][]byte) []byte { ...@@ -324,7 +300,7 @@ func concatDER(der [][]byte) []byte {
func certRequest(key crypto.Signer, domains []string) ([]byte, error) { func certRequest(key crypto.Signer, domains []string) ([]byte, error) {
req := &x509.CertificateRequest{ req := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: domains[0]}, Subject: pkix.Name{CommonName: domains[0]},
DNSNames: domains, DNSNames: domains,
} }
return x509.CreateCertificateRequest(rand.Reader, req, key) return x509.CreateCertificateRequest(rand.Reader, req, key)
......
...@@ -77,36 +77,44 @@ func TestManager_Reload(t *testing.T) { ...@@ -77,36 +77,44 @@ func TestManager_Reload(t *testing.T) {
defer cleanup() defer cleanup()
// Data race: we read data owned by another goroutine! // 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?") t.Fatal("configuration not loaded?")
} }
if m.certs[0].cn() != "example.com" { if certs[0].cn() != "example.com" {
t.Fatalf("certs[0].cn() is %s, expected example.com", m.certs[0].cn()) t.Fatalf("certs[0].cn() is %s, expected example.com", certs[0].cn())
} }
// Try a reload, catch obvious errors. // Try a reload, catch obvious errors.
m.Reload() m.Reload()
time.Sleep(50 * time.Millisecond) 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)) t.Fatalf("certs count is %d, expected 1", len(m.certs))
} }
if m.certs[0].cn() != "example.com" { if certs[0].cn() != "example.com" {
t.Fatalf("certs[0].cn() is %s, expected example.com", m.certs[0].cn()) t.Fatalf("certs[0].cn() is %s, expected example.com", certs[0].cn())
} }
} }
func TestManager_NewCert(t *testing.T) { 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()) cleanup, _, m := newTestManager(t, NewSelfSignedCertGenerator())
defer cleanup() defer cleanup()
now := time.Now() now := time.Now()
ci := m.certs[0] certs := m.getCerts()
ci := certs[0]
if ci.retryDeadline.After(now) { if ci.retryDeadline.After(now) {
t.Fatalf("retry deadline is in the future: %v", ci.retryDeadline) t.Fatalf("retry deadline is in the future: %v", ci.retryDeadline)
} }
testUpdateCh <- true
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
// Verify that the retry/renewal timestamp is in the future. // Verify that the retry/renewal timestamp is in the future.
...@@ -135,7 +143,7 @@ func TestManager_NewCert(t *testing.T) { ...@@ -135,7 +143,7 @@ func TestManager_NewCert(t *testing.T) {
m.Reload() m.Reload()
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
ci = m.certs[0] ci = m.getCerts()[0]
if !ci.valid { if !ci.valid {
t.Fatal("certificate is invalid after a reload") t.Fatal("certificate is invalid after a reload")
} }
......
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
}
}
}
...@@ -310,9 +310,9 @@ func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) ...@@ -310,9 +310,9 @@ func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error)
// On success client's Key is updated which is not concurrency safe. // On success client's Key is updated which is not concurrency safe.
// On failure an error will be returned. // On failure an error will be returned.
// The new key is already registered with the ACME provider if the following is true: // The new key is already registered with the ACME provider if the following is true:
// - error is of type acme.Error // - error is of type acme.Error
// - StatusCode should be 409 (Conflict) // - StatusCode should be 409 (Conflict)
// - Location header will have the KID of the associated account // - Location header will have the KID of the associated account
// //
// More about account key rollover can be found at // More about account key rollover can be found at
// https://tools.ietf.org/html/rfc8555#section-7.3.5. // https://tools.ietf.org/html/rfc8555#section-7.3.5.
......
...@@ -86,7 +86,7 @@ go.opentelemetry.io/otel/sdk/resource ...@@ -86,7 +86,7 @@ go.opentelemetry.io/otel/sdk/resource
go.opentelemetry.io/otel/sdk/trace go.opentelemetry.io/otel/sdk/trace
# go.opentelemetry.io/otel/trace v1.9.0 # go.opentelemetry.io/otel/trace v1.9.0
go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace
# golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa # golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
## explicit ## explicit
golang.org/x/crypto/acme golang.org/x/crypto/acme
# golang.org/x/mod v0.4.2 # golang.org/x/mod v0.4.2
......