diff --git a/ca/ca.go b/ca/ca.go index c16d7f4a..60118477 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -19,6 +19,8 @@ import ( "strings" "time" + "github.com/jmhodges/clock" + "github.com/letsencrypt/pebble/v2/acme" "github.com/letsencrypt/pebble/v2/core" "github.com/letsencrypt/pebble/v2/db" @@ -34,6 +36,7 @@ type CAImpl struct { log *log.Logger db *db.MemoryStore ocspResponderURL string + clockSource clock.Clock chains []*chain @@ -118,8 +121,8 @@ func (ca *CAImpl) makeRootCert( template := &x509.Certificate{ Subject: subject, SerialNumber: serial, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(30, 0, 0), + NotBefore: ca.clockSource.Now(), + NotAfter: ca.clockSource.Now().AddDate(30, 0, 0), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, @@ -269,7 +272,7 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ return nil, fmt.Errorf("cannot create subject key ID: %s", err.Error()) } - certNotBefore := time.Now() + certNotBefore := ca.clockSource.Now() if notBefore != "" { certNotBefore, err = time.Parse(time.RFC3339, notBefore) if err != nil { @@ -342,10 +345,11 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ return newCert, nil } -func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternateRoots int, chainLength int, certificateValidityPeriod uint64) *CAImpl { +func New(log *log.Logger, clockSource clock.Clock, db *db.MemoryStore, ocspResponderURL string, alternateRoots int, chainLength int, certificateValidityPeriod uint64) *CAImpl { ca := &CAImpl{ log: log, db: db, + clockSource: clockSource, certValidityPeriod: defaultValidityPeriod, } @@ -353,6 +357,9 @@ func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternate ca.ocspResponderURL = ocspResponderURL ca.log.Printf("Setting OCSP responder URL for issued certificates to %q", ca.ocspResponderURL) } + if clockSource == nil { + ca.clockSource = clock.New() + } intermediateSubject := pkix.Name{ CommonName: intermediateCAPrefix + hex.EncodeToString(makeSerial().Bytes()[:3]), diff --git a/cmd/pebble/main.go b/cmd/pebble/main.go index 9fb0886c..ef86b8ab 100644 --- a/cmd/pebble/main.go +++ b/cmd/pebble/main.go @@ -8,6 +8,7 @@ import ( "os" "strconv" + "github.com/jmhodges/clock" "github.com/letsencrypt/pebble/v2/ca" "github.com/letsencrypt/pebble/v2/cmd" "github.com/letsencrypt/pebble/v2/db" @@ -93,9 +94,10 @@ func main() { chainLength = int(val) } - db := db.NewMemoryStore() - ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength, c.Pebble.CertificateValidityPeriod) - va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress) + clockSource := clock.New() + db := db.NewMemoryStore(clockSource) + ca := ca.New(logger, clockSource, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength, c.Pebble.CertificateValidityPeriod) + va := va.New(logger, clockSource, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress) for keyID, key := range c.Pebble.ExternalAccountMACKeys { err := db.AddExternalAccountKeyByID(keyID, key) @@ -107,7 +109,7 @@ func main() { cmd.FailOnError(err, "Failed to add domain to block list") } - wfeImpl := wfe.New(logger, db, va, ca, *strictMode, c.Pebble.ExternalAccountBindingRequired, c.Pebble.RetryAfter.Authz, c.Pebble.RetryAfter.Order) + wfeImpl := wfe.New(logger, clockSource, db, va, ca, *strictMode, c.Pebble.ExternalAccountBindingRequired, c.Pebble.RetryAfter.Authz, c.Pebble.RetryAfter.Order) muxHandler := wfeImpl.Handler() if c.Pebble.ManagementListenAddress != "" { diff --git a/core/types.go b/core/types.go index b94d5a77..27fdc843 100644 --- a/core/types.go +++ b/core/types.go @@ -29,7 +29,7 @@ type Order struct { CertificateObject *Certificate } -func (o *Order) GetStatus() (string, error) { +func (o *Order) GetStatus(currentTime time.Time) (string, error) { // Lock the order for reading o.RLock() defer o.RUnlock() @@ -50,7 +50,7 @@ func (o *Order) GetStatus() (string, error) { authzStatuses[authzStatus]++ - if authzExpires.Before(time.Now()) { + if authzExpires.Before(currentTime) { authzStatuses[acme.StatusExpired]++ } } diff --git a/db/memorystore.go b/db/memorystore.go index bf45796d..43d61d34 100644 --- a/db/memorystore.go +++ b/db/memorystore.go @@ -14,9 +14,9 @@ import ( "strconv" "strings" "sync" - "time" "github.com/go-jose/go-jose/v4" + "github.com/jmhodges/clock" "github.com/letsencrypt/pebble/v2/acme" "github.com/letsencrypt/pebble/v2/core" @@ -38,6 +38,8 @@ func (e ExistingAccountError) Error() string { type MemoryStore struct { sync.RWMutex + clockSource clock.Clock + accountRand *rand.Rand accountsByID map[string]*core.Account @@ -61,9 +63,13 @@ type MemoryStore struct { blockListByDomain [][]string } -func NewMemoryStore() *MemoryStore { +func NewMemoryStore(clockSource clock.Clock) *MemoryStore { + if clockSource == nil { + clockSource = clock.New() + } return &MemoryStore{ - accountRand: rand.New(rand.NewSource(time.Now().UnixNano())), + clockSource: clockSource, + accountRand: rand.New(rand.NewSource(clockSource.Now().UnixNano())), accountsByID: make(map[string]*core.Account), accountsByKeyID: make(map[string]*core.Account), ordersByID: make(map[string]*core.Order), @@ -200,7 +206,7 @@ func (m *MemoryStore) GetOrderByID(id string) *core.Order { defer m.RUnlock() if order, ok := m.ordersByID[id]; ok { - orderStatus, err := order.GetStatus() + orderStatus, err := order.GetStatus(m.clockSource.Now()) if err != nil { panic(err) } @@ -218,7 +224,7 @@ func (m *MemoryStore) GetOrdersByAccountID(accountID string) []*core.Order { if orders, ok := m.ordersByAccountID[accountID]; ok { for _, order := range orders { - orderStatus, err := order.GetStatus() + orderStatus, err := order.GetStatus(m.clockSource.Now()) if err != nil { panic(err) } @@ -268,7 +274,7 @@ func (m *MemoryStore) FindValidAuthorization(accountID string, identifier acme.I authz.RLock() if authz.Status == acme.StatusValid && identifier.Equals(authz.Identifier) && authz.Order != nil && authz.Order.AccountID == accountID && - authz.ExpiresDate.After(time.Now()) { + authz.ExpiresDate.After(m.clockSource.Now()) { authz.RUnlock() return authz } diff --git a/go.mod b/go.mod index 388cee75..3eed21d5 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/go-jose/go-jose/v4 v4.0.1 + github.com/jmhodges/clock v1.2.0 github.com/letsencrypt/challtestsrv v1.3.2 github.com/miekg/dns v1.1.58 ) diff --git a/go.sum b/go.sum index c1d4f94f..d892c500 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWq github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/letsencrypt/challtestsrv v1.3.2 h1:pIDLBCLXR3B1DLmOmkkqg29qVa7DDozBnsOpL9PxmAY= github.com/letsencrypt/challtestsrv v1.3.2/go.mod h1:Ur4e4FvELUXLGhkMztHOsPIsvGxD/kzSJninOrkM+zc= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= diff --git a/va/va.go b/va/va.go index f4cffaa3..f01ac56b 100644 --- a/va/va.go +++ b/va/va.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/jmhodges/clock" "github.com/miekg/dns" "github.com/letsencrypt/challtestsrv" @@ -98,6 +99,7 @@ type vaTask struct { } type VAImpl struct { + clockSource clock.Clock log *log.Logger httpPort int tlsPort int @@ -112,11 +114,13 @@ type VAImpl struct { func New( log *log.Logger, + clockSource clock.Clock, httpPort, tlsPort int, strict bool, customResolverAddr string, ) *VAImpl { va := &VAImpl{ log: log, + clockSource: clockSource, httpPort: httpPort, tlsPort: tlsPort, tasks: make(chan *vaTask, taskQueueSize), @@ -126,6 +130,10 @@ func New( customResolverAddr: customResolverAddr, } + if clockSource == nil { + va.clockSource = clock.New() + } + if customResolverAddr != "" { va.log.Printf("Using custom DNS resolver for ACME challenges: %s", customResolverAddr) va.dnsClient = new(dns.Client) @@ -195,7 +203,7 @@ func (va VAImpl) setAuthzValid(authz *core.Authorization, chal *core.Challenge) authz.Lock() defer authz.Unlock() // Update the authz expiry for the new validity period - now := time.Now().UTC() + now := va.clockSource.Now().UTC() authz.ExpiresDate = now.Add(validAuthzExpire) authz.Expires = authz.ExpiresDate.Format(time.RFC3339) // Update the authz status @@ -244,7 +252,7 @@ func (va VAImpl) process(task *vaTask) { chal := task.Challenge chal.Lock() // Update the validated date for the challenge - now := time.Now().UTC() + now := va.clockSource.Now().UTC() chal.ValidatedDate = now chal.Validated = chal.ValidatedDate.Format(time.RFC3339) authz := chal.Authz @@ -292,7 +300,7 @@ func (va VAImpl) performValidation(task *vaTask, results chan<- *core.Validation // the URL to the `_acme-challenge` subdomain. results <- &core.ValidationRecord{ URL: task.Identifier.Value, - ValidatedAt: time.Now(), + ValidatedAt: va.clockSource.Now(), } return } @@ -317,7 +325,7 @@ func (va VAImpl) validateDNS01(task *vaTask) *core.ValidationRecord { result := &core.ValidationRecord{ URL: challengeSubdomain, - ValidatedAt: time.Now(), + ValidatedAt: va.clockSource.Now(), } txts, err := va.getTXTEntry(challengeSubdomain) @@ -360,7 +368,7 @@ func (va VAImpl) validateDNSAccount01(task *vaTask) *core.ValidationRecord { result := &core.ValidationRecord{ URL: challengeSubdomain, - ValidatedAt: time.Now(), + ValidatedAt: va.clockSource.Now(), } txts, err := va.getTXTEntry(challengeSubdomain) @@ -404,7 +412,7 @@ func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord { } result := &core.ValidationRecord{ URL: net.JoinHostPort(task.Identifier.Value, portString), - ValidatedAt: time.Now(), + ValidatedAt: va.clockSource.Now(), } addrs, err := va.resolveIP(task.Identifier.Value) @@ -522,7 +530,7 @@ func (va VAImpl) validateHTTP01(task *vaTask) *core.ValidationRecord { result := &core.ValidationRecord{ URL: url, - ValidatedAt: time.Now(), + ValidatedAt: va.clockSource.Now(), Error: err, } if result.Error != nil { diff --git a/va/va_test.go b/va/va_test.go index d8d40fe9..bc9d2c03 100644 --- a/va/va_test.go +++ b/va/va_test.go @@ -6,6 +6,8 @@ import ( "sync" "testing" + "github.com/jmhodges/clock" + "github.com/letsencrypt/pebble/v2/acme" "github.com/letsencrypt/pebble/v2/core" "github.com/letsencrypt/pebble/v2/db" @@ -30,8 +32,8 @@ func TestAuthzRace(_ *testing.T) { // MemoryStore.FindValidAuthorization searches and tests authz.Status // This whole test can be removed if/when the MemoryStore becomes 100% by value - ms := db.NewMemoryStore() - va := New(log.New(os.Stdout, "Pebble/TestRace", log.LstdFlags), 14000, 15000, false, "") + ms := db.NewMemoryStore(clock.New()) + va := New(log.New(os.Stdout, "Pebble/TestRace", log.LstdFlags), clock.New(), 14000, 15000, false, "") authz := &core.Authorization{ ID: "auth-id", diff --git a/vendor/github.com/jmhodges/clock/.travis.yml b/vendor/github.com/jmhodges/clock/.travis.yml new file mode 100644 index 00000000..f287c5e2 --- /dev/null +++ b/vendor/github.com/jmhodges/clock/.travis.yml @@ -0,0 +1,8 @@ +language: go + +go: + - 1.3 + - 1.4 + - 1.5 + +sudo: false \ No newline at end of file diff --git a/vendor/github.com/jmhodges/clock/LICENSE b/vendor/github.com/jmhodges/clock/LICENSE new file mode 100644 index 00000000..4317dd16 --- /dev/null +++ b/vendor/github.com/jmhodges/clock/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright © 2014 Jeff Hodges + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/jmhodges/clock/README.md b/vendor/github.com/jmhodges/clock/README.md new file mode 100644 index 00000000..40825a31 --- /dev/null +++ b/vendor/github.com/jmhodges/clock/README.md @@ -0,0 +1,28 @@ +clock +==== + +[![GoDoc](https://img.shields.io/badge/pkg.go.dev-doc-blue)](http://pkg.go.dev/github.com/jmhodges/clock) + +Package clock provides an abstraction for system time that enables testing of +time-sensitive code. + +Where you'd use `time.Now`, instead use `clk.Now` where `clk` is an instance of +`clock.Clock`. + +When running your code in production, pass it a `Clock` given by +`clock.Default()` and when you're running it in your tests, pass it an instance +of `Clock` from `NewFake()`. + +When you do that, you can use `FakeClock`'s `Add` and `Set` methods to control +how time behaves in your tests. That makes the tests you'll write more reliable +while also expanding the space of problems you can test. + +This code intentionally does not attempt to provide an abstraction over +`time.Ticker` and `time.Timer` because Go does not have the runtime or API hooks +available to do so reliably. See https://github.com/golang/go/issues/8869 + +As with any use of `time`, be sure to test `Time` equality with +`time.Time#Equal`, not `==`. + +For API documentation, see the +[godoc](http://godoc.org/github.com/jmhodges/clock). diff --git a/vendor/github.com/jmhodges/clock/clock.go b/vendor/github.com/jmhodges/clock/clock.go new file mode 100644 index 00000000..91657e5e --- /dev/null +++ b/vendor/github.com/jmhodges/clock/clock.go @@ -0,0 +1,230 @@ +// Package clock provides an abstraction for system time that enables testing of +// time-sensitive code. +// +// Where you'd use time.Now, instead use clk.Now where clk is an instance of +// Clock. +// +// When running your code in production, pass it a Clock given by +// clock.Default() and when you're running it in your tests, pass it an instance +// of Clock from NewFake(). +// +// When you do that, you can use FakeClock's Add and Set methods to control how +// time behaves in your tests. That makes the tests you'll write more reliable +// while also expanding the space of problems you can test. +// +// This code intentionally does not attempt to provide an abstraction over +// time.Ticker and time.Timer because Go does not have the runtime or API hooks +// available to do so reliably. See https://github.com/golang/go/issues/8869 +package clock + +import ( + "sort" + "sync" + "time" +) + +// Some in-use reflection-heavy systems, like facebookgo/inject, fail when given +// a value type like sysClock{}. Since it's hidden by an interface, this has +// surprised users. We fixed that by making systemClock a &sysClock. +var systemClock Clock = &sysClock{} + +// New returns a Clock that matches the actual system time. +func New() Clock { + // This is a method instead of a public var to prevent folks from + // "making things work" by writing to the var instead of passing + // in a Clock. + return systemClock +} + +// Deprecated: Default is just an alias for New but less memorable. +func Default() Clock { + return systemClock +} + +// Clock is an abstraction over system time. New instances of it can +// be made with Default and NewFake. +type Clock interface { + // Now returns the Clock's current view of the time. Mutating the + // returned Time will not mutate the clock's time. + Now() time.Time + + // Sleep causes the current goroutine to sleep for the given duration. + Sleep(time.Duration) + + // After returns a channel that fires after the given duration. + After(time.Duration) <-chan time.Time + + // Since is a short hand for Now().Sub(t). + Since(time.Time) time.Duration + + // NewTimer makes a Timer based on this clock's time. Using Timers and + // negative durations in the Clock or Timer API is undefined behavior and + // may be changed. + NewTimer(time.Duration) *Timer +} + +type sysClock struct{} + +func (s *sysClock) Now() time.Time { + return time.Now() +} + +func (s *sysClock) Sleep(d time.Duration) { + time.Sleep(d) +} + +func (s *sysClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +func (s *sysClock) Since(t time.Time) time.Duration { + return time.Since(t) +} + +func (s *sysClock) NewTimer(d time.Duration) *Timer { + tt := time.NewTimer(d) + return &Timer{C: tt.C, timer: tt} +} + +// NewFake returns a FakeClock to be used in tests that need to +// manipulate time. Its initial value is always the unix epoch in the +// UTC timezone. The FakeClock returned is thread-safe. +func NewFake() FakeClock { + // We're explicit about this time construction to avoid early user + // questions about why the time object doesn't have a Location by + // default. + return &fake{t: time.Unix(0, 0).UTC()} +} + +// FakeClock is a Clock with additional controls. The return value of +// Now return can be modified with Add. Use NewFake to get a +// thread-safe FakeClock implementation. +type FakeClock interface { + Clock + // Adjust the time that will be returned by Now. + Add(d time.Duration) + + // Set the Clock's time to exactly the time given. + Set(t time.Time) +} + +// To prevent mistakes with the API, we hide this behind NewFake. It's +// easy forget to create a pointer to a fake since time.Time (and +// sync.Mutex) are also simple values. The code will appear to work +// but the clock's time will never be adjusted. +type fake struct { + sync.RWMutex + t time.Time + sends sortedSends +} + +func (f *fake) Now() time.Time { + f.RLock() + defer f.RUnlock() + return f.t +} + +func (f *fake) Sleep(d time.Duration) { + if d < 0 { + // time.Sleep just returns immediately. Do the same. + return + } + f.Add(d) +} + +func (f *fake) After(d time.Duration) <-chan time.Time { + return f.NewTimer(d).C +} + +func (f *fake) Since(t time.Time) time.Duration { + return f.Now().Sub(t) +} + +func (f *fake) NewTimer(d time.Duration) *Timer { + f.Lock() + defer f.Unlock() + ch := make(chan time.Time, 1) + tt := f.t.Add(d) + ft := &fakeTimer{c: ch, clk: f, active: true} + t := &Timer{ + C: ch, + fakeTimer: ft, + } + s := f.addSend(tt, ft) + ft.sends = []*send{s} + return t +} + +func (f *fake) Add(d time.Duration) { + f.Lock() + defer f.Unlock() + f.t = f.t.Add(d) + f.sendTimes() +} + +func (f *fake) Set(t time.Time) { + f.Lock() + defer f.Unlock() + f.t = t + f.sendTimes() +} + +// Only to be called while the fake's lock is held +func (f *fake) sendTimes() { + newSends := make(sortedSends, 0) + for _, s := range f.sends { + if !s.active || !s.ft.active { + continue + } + if s.target.Equal(f.t) || s.target.Before(f.t) { + s.ft.active = false + s.active = false + // The select is to drop second sends from resets without a user + // receiving from ft.c. + select { + case s.ft.c <- s.target: + default: + } + } + if s.active { + newSends = append(newSends, s) + } + } + f.sends = newSends +} + +// Only to be called while the fake's lock is held +func (f *fake) addSend(target time.Time, ft *fakeTimer) *send { + s := &send{target: target, ft: ft, active: true} + f.sends = append(f.sends, s) + // This will be a small enough slice to be fast. Can be replaced with a more + // complicated container if someone is making many timers. + sort.Sort(f.sends) + return s +} + +// send is a struct that represents a scheduled send of a time.Time to its +// fakeTimer's channel. They are actually sent when the relevant fake's time +// goes equal or past their target time, as long as the relevant fakeTimer has +// not been Reset or Stop'ed. When a Timer is Reset, the old sends are +// deactivated and will be removed from the clocks list on the next attempt to +// send. +type send struct { + target time.Time + active bool + ft *fakeTimer +} + +type sortedSends []*send + +func (s sortedSends) Len() int { + return len(s) +} + +func (s sortedSends) Less(i, j int) bool { + return s[i].target.Before(s[j].target) +} + +func (s sortedSends) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/vendor/github.com/jmhodges/clock/timer.go b/vendor/github.com/jmhodges/clock/timer.go new file mode 100644 index 00000000..d6e0ba4c --- /dev/null +++ b/vendor/github.com/jmhodges/clock/timer.go @@ -0,0 +1,66 @@ +package clock + +import "time" + +type Timer struct { + C <-chan time.Time + + timer *time.Timer + fakeTimer *fakeTimer +} + +func (t *Timer) Reset(d time.Duration) bool { + if t.timer != nil { + return t.timer.Reset(d) + } + return t.fakeTimer.Reset(d) +} + +func (t *Timer) Stop() bool { + if t.timer != nil { + return t.timer.Stop() + } + return t.fakeTimer.Stop() +} + +type fakeTimer struct { + // c is the same chan as C in the Timer that contains this fakeTimer + c chan<- time.Time + // clk is kept so we can maintain just one lock and to add and attempt to + // send the times made by this timer during Resets and Stops + clk *fake + // active is true until the fakeTimer's send is attempted or it has been + // stopped + active bool + // sends is where we store all the sends made by this timer so we can + // deactivate the old ones when Reset or Stop is called. + sends []*send +} + +func (ft *fakeTimer) Reset(d time.Duration) bool { + ft.clk.Lock() + defer ft.clk.Unlock() + target := ft.clk.t.Add(d) + active := ft.active + ft.active = true + for _, s := range ft.sends { + s.active = false + } + s := ft.clk.addSend(target, ft) + ft.sends = []*send{s} + ft.clk.sendTimes() + return active +} + +func (ft *fakeTimer) Stop() bool { + ft.clk.Lock() + defer ft.clk.Unlock() + active := ft.active + ft.active = false + for _, s := range ft.sends { + s.active = false + } + ft.sends = nil + ft.clk.sendTimes() + return active +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f34108bc..0e5966aa 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -5,6 +5,9 @@ github.com/go-jose/go-jose/v4/cipher github.com/go-jose/go-jose/v4/json # github.com/google/go-cmp v0.6.0 ## explicit; go 1.13 +# github.com/jmhodges/clock v1.2.0 +## explicit; go 1.17 +github.com/jmhodges/clock # github.com/letsencrypt/challtestsrv v1.3.2 ## explicit; go 1.13 github.com/letsencrypt/challtestsrv diff --git a/wfe/wfe.go b/wfe/wfe.go index 0f47fc26..edc1b898 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -27,6 +27,7 @@ import ( "unicode" "github.com/go-jose/go-jose/v4" + "github.com/jmhodges/clock" "github.com/letsencrypt/pebble/v2/acme" "github.com/letsencrypt/pebble/v2/ca" @@ -155,6 +156,7 @@ func (th *topHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { type WebFrontEndImpl struct { log *log.Logger + clockSource clock.Clock db *db.MemoryStore nonce *nonceMap nonceErrPercent int @@ -172,6 +174,7 @@ const ToSURL = "data:text/plain,Do%20what%20thou%20wilt" func New( log *log.Logger, + clockSource clock.Clock, db *db.MemoryStore, va *va.VAImpl, ca *ca.CAImpl, @@ -224,6 +227,7 @@ func New( return WebFrontEndImpl{ log: log, + clockSource: clockSource, db: db, nonce: newNonceMap(), nonceErrPercent: nonceErrPercent, @@ -1517,7 +1521,7 @@ func (wfe *WebFrontEndImpl) makeAuthorizations(order *core.Order, request *http. order.RLock() // Add one authz for each name in the order's parsed CSR for _, name := range order.Identifiers { - now := time.Now().UTC() + now := wfe.clockSource.Now().UTC() expires := now.Add(pendingAuthzExpire) ident := acme.Identifier{ Type: name.Type, @@ -1677,7 +1681,7 @@ func (wfe *WebFrontEndImpl) NewOrder( for _, ip := range orderIPs { uniquenames = append(uniquenames, acme.Identifier{Value: ip.String(), Type: acme.IdentifierIP}) } - expires := time.Now().AddDate(0, 0, 1) + expires := wfe.clockSource.Now().AddDate(0, 0, 1) order := &core.Order{ ID: newToken(), AccountID: existingReg.ID, @@ -1813,7 +1817,7 @@ func (wfe *WebFrontEndImpl) Order( } if order.Status == acme.StatusProcessing { - addRetryAfterHeader(response, wfe.retryAfterOrder) + addRetryAfterHeader(response, wfe.clockSource.Now(), wfe.retryAfterOrder) } // Prepare the order for display as JSON @@ -1878,7 +1882,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder( } // The existing order must not be expired - if orderExpires.Before(time.Now()) { + if orderExpires.Before(wfe.clockSource.Now()) { wfe.sendError(acme.NotFoundProblem(fmt.Sprintf( "Order %q expired %s", orderID, orderExpires)), response) return @@ -1996,7 +2000,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder( // Set the existingOrder to processing before displaying to the user existingOrder.Status = acme.StatusProcessing - addRetryAfterHeader(response, wfe.retryAfterOrder) + addRetryAfterHeader(response, wfe.clockSource.Now(), wfe.retryAfterOrder) // Prepare the order for display as JSON orderReq := wfe.orderForDisplay(existingOrder, request) @@ -2133,7 +2137,7 @@ func (wfe *WebFrontEndImpl) Authz( challengeStatus := c.Status c.RUnlock() if challengeStatus == acme.StatusProcessing { - addRetryAfterHeader(response, wfe.retryAfterAuthz) + addRetryAfterHeader(response, wfe.clockSource.Now(), wfe.retryAfterAuthz) break } } @@ -2255,7 +2259,7 @@ func (wfe *WebFrontEndImpl) validateAuthzForChallenge(authz *core.Authorization) fmt.Sprintf("Authorization identifier was type %s, only %s and %s are supported", ident.Type, acme.IdentifierDNS, acme.IdentifierIP)) } - now := time.Now() + now := wfe.clockSource.Now() if now.After(authz.ExpiresDate) { return nil, acme.MalformedProblem( fmt.Sprintf("Authorization expired %s", @@ -2359,7 +2363,7 @@ func (wfe *WebFrontEndImpl) updateChallenge( expiry := existingOrder.ExpiresDate existingOrder.RUnlock() - now := time.Now() + now := wfe.clockSource.Now() if now.After(expiry) { wfe.sendError( acme.MalformedProblem(fmt.Sprintf("order expired %s", @@ -2522,14 +2526,14 @@ func addNoCacheHeader(response http.ResponseWriter) { response.Header().Add("Cache-Control", "public, max-age=0, no-cache") } -func addRetryAfterHeader(response http.ResponseWriter, second int) { +func addRetryAfterHeader(response http.ResponseWriter, currentTime time.Time, second int) { if second > 0 { if rand.Intn(2) == 0 { response.Header().Add("Retry-After", strconv.Itoa(second)) } else { // IMF-fixdate // see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1 - currentTime := time.Now().In(time.UTC) + currentTime := currentTime.In(time.UTC) retryAfter := currentTime.Add(time.Second * time.Duration(second)) response.Header().Add("Retry-After", retryAfter.Format(http.TimeFormat)) } @@ -2756,7 +2760,7 @@ func (wfe *WebFrontEndImpl) processRevocation( wfe.db.RevokeCertificate(&core.RevokedCertificate{ Certificate: cert, - RevokedAt: time.Now(), + RevokedAt: wfe.clockSource.Now(), Reason: revokeCertReq.Reason, }) return nil