Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ variable `PEBBLE_VA_NOSLEEP` to `1`. E.g.
The maximal number of seconds to sleep can be configured by defining
`PEBBLE_VA_SLEEPTIME`. It must be set to a positive integer.

### Testing at a fixed time

By default Pebble uses the system clock when issuing certificates. To issue CA
and leaf certificates at a fixed time, set `PEBBLE_FAKECLOCK` to an RFC3339
timestamp. For example:

`PEBBLE_FAKECLOCK=2030-01-02T03:04:05Z pebble -config ./test/config/pebble-config.json`

### Skipping Validation

If you want to avoid the hassle of having to stand up a challenge response
Expand Down
45 changes: 41 additions & 4 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,38 @@ const (
defaultValidityPeriod = 7776000
)

// Clock supplies the current time to the CA.
type Clock interface {
Now() time.Time
}

// Option configures a CA.
type Option struct {
apply func(*options)
}

// WithClock configures the CA to use clk when issuing certificates.
func WithClock(clk Clock) Option {
return Option{apply: func(options *options) {
if clk != nil {
options.clk = clk
}
}}
}

type options struct {
clk Clock
}

type wallClock struct{}

func (wallClock) Now() time.Time { return time.Now() }

type CAImpl struct {
log *log.Logger
db *db.MemoryStore
ocspResponderURL string
clk Clock

chains []*chain
profiles map[string]*Profile
Expand Down Expand Up @@ -130,11 +158,12 @@ func (ca *CAImpl) makeCACert(
signer *issuer,
) (*core.Certificate, error) {
serial := makeSerial()
now := ca.clk.Now()
template := &x509.Certificate{
Subject: subject,
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(30, 0, 0),
NotBefore: now,
NotAfter: now.AddDate(30, 0, 0),

KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
Expand Down Expand Up @@ -284,7 +313,7 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
return nil, fmt.Errorf("unrecgonized profile name %q", profileName)
}

certNotBefore := time.Now()
certNotBefore := ca.clk.Now()
var err error
if notBefore != "" {
certNotBefore, err = time.Parse(time.RFC3339, notBefore)
Expand Down Expand Up @@ -373,10 +402,18 @@ 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, keyAlg string, alternateRoots int, chainLength int, profiles map[string]Profile) *CAImpl {
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, keyAlg string, alternateRoots int, chainLength int, profiles map[string]Profile, opts ...Option) *CAImpl {
options := options{clk: wallClock{}}
for _, option := range opts {
if option.apply != nil {
option.apply(&options)
}
}

ca := &CAImpl{
log: log,
db: db,
clk: options.clk,
profiles: make(map[string]*Profile, len(profiles)),
}

Expand Down
48 changes: 48 additions & 0 deletions ca/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,54 @@ func makeCa() *CAImpl {
return New(logger, db, "", "ecdsa", 0, 1, map[string]Profile{"default": {}})
}

type fixedClock struct {
now time.Time
}

func (c fixedClock) Now() time.Time { return c.now }

func TestWithClock(t *testing.T) {
now := time.Date(2030, time.January, 2, 3, 4, 5, 0, time.UTC)
logger := log.New(os.Stdout, "Pebble ", log.LstdFlags)
ca := New(
logger,
db.NewMemoryStore(),
"",
"ecdsa",
0,
1,
map[string]Profile{"default": {}},
WithClock(fixedClock{now: now}),
)

root := ca.GetRootCert(0).Cert
if !root.NotBefore.Equal(now) {
t.Fatalf("unexpected NotBefore: got %s, want %s", root.NotBefore, now)
}
wantNotAfter := now.AddDate(30, 0, 0)
if !root.NotAfter.Equal(wantNotAfter) {
t.Fatalf("unexpected NotAfter: got %s, want %s", root.NotAfter, wantNotAfter)
}

key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
leaf, err := ca.newCertificate(
[]string{"example.com"}, nil, key.Public(), "account", "", "", "default", nil,
)
if err != nil {
t.Fatal(err)
}
if !leaf.Cert.NotBefore.Equal(now) {
t.Fatalf("unexpected leaf NotBefore: got %s, want %s", leaf.Cert.NotBefore, now)
}
wantLeafNotAfter := now.Add(time.Duration(defaultValidityPeriod-1) * time.Second)
if !leaf.Cert.NotAfter.Equal(wantLeafNotAfter) {
t.Fatalf("unexpected leaf NotAfter: got %s, want %s", leaf.Cert.NotAfter, wantLeafNotAfter)
}
}

func makeCertOrderWithExtensions(extensions []pkix.Extension) core.Order {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
Expand Down
30 changes: 30 additions & 0 deletions cmd/pebble/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"errors"
"os"
"time"

"github.com/letsencrypt/pebble/v2/ca"
)

const fakeClockEnvVar = "PEBBLE_FAKECLOCK"

type fixedClock struct {
now time.Time
}

func (clk fixedClock) Now() time.Time { return clk.now }

func clockFromEnv() (ca.Clock, error) {
value := os.Getenv(fakeClockEnvVar)
if value == "" {
return nil, nil
}

now, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, errors.New("must use RFC3339 format")
}
return fixedClock{now: now}, nil
}
40 changes: 40 additions & 0 deletions cmd/pebble/clock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"testing"
"time"
)

func TestClockFromEnv(t *testing.T) {
t.Run("unset", func(t *testing.T) {
t.Setenv(fakeClockEnvVar, "")

clk, err := clockFromEnv()
if err != nil {
t.Fatal(err)
}
if clk != nil {
t.Fatal("expected the default wall clock")
}
})

t.Run("valid", func(t *testing.T) {
now := time.Date(2030, time.January, 2, 3, 4, 5, 0, time.UTC)
t.Setenv(fakeClockEnvVar, now.Format(time.RFC3339))

clk, err := clockFromEnv()
if err != nil {
t.Fatal(err)
}
if got := clk.Now(); !got.Equal(now) {
t.Fatalf("unexpected time: got %s, want %s", got, now)
}
})

t.Run("invalid", func(t *testing.T) {
t.Setenv(fakeClockEnvVar, "not-a-time")
if _, err := clockFromEnv(); err == nil {
t.Fatal("expected an error")
}
})
}
4 changes: 3 additions & 1 deletion cmd/pebble/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ func main() {
}

db := db.NewMemoryStore()
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, keyAlg, alternateRoots, chainLength, profiles)
clk, err := clockFromEnv()
cmd.FailOnError(err, "Reading "+fakeClockEnvVar)
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, keyAlg, alternateRoots, chainLength, profiles, ca.WithClock(clk))
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress, db)

for keyID, key := range c.Pebble.ExternalAccountMACKeys {
Expand Down