Skip to content

Commit

Permalink
Implement and offer alternate chains (RFC 8555 7.4.2) (#234)
Browse files Browse the repository at this point in the history
Allows to offer alternate chains for a certificate according to RFC8555, Section
7.4.2. For this, multiple root and intermediate certs are created (all with the
same intermediate private key), and `alternative` relation links are offered
during certificate download. A `/roots/` endpoint has also been added to
allow downloading the alternate roots (`/roots/0`, `/roots/1` etc.); Pebble also
provides `link` headers for its alternate forms in responses to certificate
requests.

This feature can be enabled by setting the environment variable
`PEBBLE_ALTERNATE_ROOTS` to something larger than 0. For example, with
`PEBBLE_ALTERNATE_ROOTS=2`:

```.sh
$ curl -i -k https://0.0.0.0:14000/root
HTTP/2 200 
cache-control: public, max-age=0, no-cache
content-type: application/pem-certificate-chain; charset=utf-8
link: <https://0.0.0.0:14000/dir>;rel="index"
link: <https://0.0.0.0:14000/roots/0>;rel="alternate"
link: <https://0.0.0.0:14000/roots/1>;rel="alternate"
content-length: 1107
date: Sun, 12 May 2019 15:05:37 GMT

-----BEGIN CERTIFICATE-----
...

$ curl -i -k https://0.0.0.0:14000/roots/0
HTTP/2 200 
cache-control: public, max-age=0, no-cache
content-type: application/pem-certificate-chain; charset=utf-8
link: <https://0.0.0.0:14000/dir>;rel="index"
link: <https://0.0.0.0:14000/root>;rel="alternate"
link: <https://0.0.0.0:14000/roots/0>;rel="alternate"
content-length: 1107
date: Sun, 12 May 2019 15:06:07 GMT

-----BEGIN CERTIFICATE-----
...
```
  • Loading branch information
felixfontein authored and Daniel McCarney committed Jun 21, 2019
1 parent afbe2db commit 0abe052
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 64 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ same standards as the Let's Encrypt production CA and their keys. Moreover
these keys are exposed by Pebble and will be lost as soon as the process
terminates: so they are not safe to use for anything other than testing.**

In case alternative root chains are enabled by setting `PEBBLE_ALTERNATE_ROOTS` to a
positive integer, the root certificates for these can be retrieved by doing a `GET`
request to `https://localhost:14000/roots/0`, `https://localhost:14000/root-keys/1`
`https://localhost:14000/intermediates/2`, `https://localhost:14000/intermediate-keys/3`
etc. These endpoints also send `Link` HTTP headers for all alternative root and
intermediate certificates and keys.

### OCSP Responder URL

Pebble does not support the OCSP protocol as a responder and so does not set
Expand Down
116 changes: 74 additions & 42 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type CAImpl struct {
db *db.MemoryStore
ocspResponderURL string

chains []*chain
}

type chain struct {
root *issuer
intermediate *issuer
}
Expand Down Expand Up @@ -105,7 +109,8 @@ func (ca *CAImpl) makeRootCert(
DER: der,
}
if signer != nil && signer.cert != nil {
newCert.Issuer = signer.cert
newCert.Issuers = make([]*core.Certificate, 1)
newCert.Issuers[0] = signer.cert
}
_, err = ca.db.AddCertificate(newCert)
if err != nil {
Expand All @@ -114,48 +119,55 @@ func (ca *CAImpl) makeRootCert(
return newCert, nil
}

func (ca *CAImpl) newRootIssuer() error {
func (ca *CAImpl) newRootIssuer() (*issuer, error) {
// Make a root private key
rk, err := makeKey()
if err != nil {
return err
return nil, err
}
// Make a self-signed root certificate
rc, err := ca.makeRootCert(rk, rootCAPrefix, nil)
if err != nil {
return err
return nil, err
}

ca.root = &issuer{
ca.log.Printf("Generated new root issuer with serial %s\n", rc.ID)
return &issuer{
key: rk,
cert: rc,
}
ca.log.Printf("Generated new root issuer with serial %s\n", rc.ID)
return nil
}, nil
}

func (ca *CAImpl) newIntermediateIssuer() error {
if ca.root == nil {
return fmt.Errorf("newIntermediateIssuer() called before newRootIssuer()")
}

// Make an intermediate private key
ik, err := makeKey()
if err != nil {
return err
func (ca *CAImpl) newIntermediateIssuer(root *issuer, ik crypto.Signer) (*issuer, error) {
if root == nil {
return nil, fmt.Errorf("Internal error: root must not be nil")
}

// Make an intermediate certificate with the root issuer
ic, err := ca.makeRootCert(ik, intermediateCAPrefix, ca.root)
ic, err := ca.makeRootCert(ik, intermediateCAPrefix, root)
if err != nil {
return err
return nil, err
}
ca.intermediate = &issuer{
ca.log.Printf("Generated new intermediate issuer with serial %s\n", ic.ID)
return &issuer{
key: ik,
cert: ic,
}, nil
}

func (ca *CAImpl) newChain(ik crypto.Signer) *chain {
root, err := ca.newRootIssuer()
if err != nil {
panic(fmt.Sprintf("Error creating new root issuer: %s", err.Error()))
}
intermediate, err := ca.newIntermediateIssuer(root, ik)
if err != nil {
panic(fmt.Sprintf("Error creating new intermediate issuer: %s", err.Error()))
}
return &chain{
root: root,
intermediate: intermediate,
}
ca.log.Printf("Generated new intermediate issuer with serial %s\n", ic.ID)
return nil
}

func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.PublicKey, accountID string) (*core.Certificate, error) {
Expand All @@ -168,7 +180,7 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
return nil, fmt.Errorf("must specify at least one domain name or IP address")
}

issuer := ca.intermediate
issuer := ca.chains[0].intermediate
if issuer == nil || issuer.cert == nil {
return nil, fmt.Errorf("cannot sign certificate - nil issuer")
}
Expand Down Expand Up @@ -203,13 +215,18 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
return nil, err
}

issuers := make([]*core.Certificate, len(ca.chains))
for i := 0; i < len(ca.chains); i++ {
issuers[i] = ca.chains[i].intermediate.cert
}

hexSerial := hex.EncodeToString(cert.SerialNumber.Bytes())
newCert := &core.Certificate{
ID: hexSerial,
AccountID: accountID,
Cert: cert,
DER: der,
Issuer: issuer.cert,
Issuers: issuers,
}
_, err = ca.db.AddCertificate(newCert)
if err != nil {
Expand All @@ -218,7 +235,7 @@ 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) *CAImpl {
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternateRoots int) *CAImpl {
ca := &CAImpl{
log: log,
db: db,
Expand All @@ -229,13 +246,13 @@ func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string) *CAImpl {
ca.log.Printf("Setting OCSP responder URL for issued certificates to %q", ca.ocspResponderURL)
}

err := ca.newRootIssuer()
ik, err := makeKey()
if err != nil {
panic(fmt.Sprintf("Error creating new root issuer: %s", err.Error()))
panic(fmt.Sprintf("Error creating new intermediate private key: %s", err.Error()))
}
err = ca.newIntermediateIssuer()
if err != nil {
panic(fmt.Sprintf("Error creating new intermediate issuer: %s", err.Error()))
ca.chains = make([]*chain, 1+alternateRoots)
for i := 0; i < len(ca.chains); i++ {
ca.chains[i] = ca.newChain(ik)
}
return ca
}
Expand Down Expand Up @@ -279,38 +296,53 @@ func (ca *CAImpl) CompleteOrder(order *core.Order) {
order.Unlock()
}

func (ca *CAImpl) GetRootCert() *core.Certificate {
if ca.root == nil {
func (ca *CAImpl) GetNumberOfRootCerts() int {
return len(ca.chains)
}

func (ca *CAImpl) getChain(no int) *chain {
if 0 <= no && no < len(ca.chains) {
return ca.chains[no]
}
return nil
}

func (ca *CAImpl) GetRootCert(no int) *core.Certificate {
chain := ca.getChain(no)
if chain == nil {
return nil
}
return ca.root.cert
return chain.root.cert
}

func (ca *CAImpl) GetRootKey() *rsa.PrivateKey {
if ca.root == nil {
func (ca *CAImpl) GetRootKey(no int) *rsa.PrivateKey {
chain := ca.getChain(no)
if chain == nil {
return nil
}

switch key := ca.root.key.(type) {
switch key := chain.root.key.(type) {
case *rsa.PrivateKey:
return key
}
return nil
}

func (ca *CAImpl) GetIntermediateCert() *core.Certificate {
if ca.intermediate == nil {
func (ca *CAImpl) GetIntermediateCert(no int) *core.Certificate {
chain := ca.getChain(no)
if chain == nil {
return nil
}
return ca.intermediate.cert
return chain.intermediate.cert
}

func (ca *CAImpl) GetIntermediateKey() *rsa.PrivateKey {
if ca.intermediate == nil {
func (ca *CAImpl) GetIntermediateKey(no int) *rsa.PrivateKey {
chain := ca.getChain(no)
if chain == nil {
return nil
}

switch key := ca.intermediate.key.(type) {
switch key := chain.intermediate.key.(type) {
case *rsa.PrivateKey:
return key
}
Expand Down
11 changes: 9 additions & 2 deletions cmd/pebble/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/http"
"os"
"strconv"

"github.com/letsencrypt/pebble/ca"
"github.com/letsencrypt/pebble/cmd"
Expand Down Expand Up @@ -57,8 +58,14 @@ func main() {
setupCustomDNSResolver(*resolverAddress)
}

alternateRoots := 0
alternateRootsVal := os.Getenv("PEBBLE_ALTERNATE_ROOTS")
if val, err := strconv.ParseInt(alternateRootsVal, 10, 0); err == nil && val >= 0 {
alternateRoots = int(val)
}

db := db.NewMemoryStore()
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL)
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots)
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode)

wfeImpl := wfe.New(logger, db, va, ca, *strictMode)
Expand All @@ -67,7 +74,7 @@ func main() {
logger.Printf("Listening on: %s\n", c.Pebble.ListenAddress)
logger.Printf("ACME directory available at: https://%s%s",
c.Pebble.ListenAddress, wfe.DirectoryPath)
logger.Printf("Root CA certificate available at: https://%s%s",
logger.Printf("Root CA certificate(s) available at: https://%s%s",
c.Pebble.ListenAddress, wfe.RootCertPath)
err = http.ListenAndServeTLS(
c.Pebble.ListenAddress,
Expand Down
13 changes: 8 additions & 5 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ type Certificate struct {
ID string
Cert *x509.Certificate
DER []byte
Issuer *Certificate
Issuers []*Certificate
AccountID string
}

Expand All @@ -167,22 +167,25 @@ func (c Certificate) PEM() []byte {
return buf.Bytes()
}

func (c Certificate) Chain() []byte {
func (c Certificate) Chain(no int) []byte {
chain := make([][]byte, 0)

// Add the leaf certificate
chain = append(chain, c.PEM())

// Add zero or more issuers
issuer := c.Issuer
var issuer *Certificate
if 0 <= no && no < len(c.Issuers) {
issuer = c.Issuers[no]
}
for {
// if the issuer is nil, or the issuer's issuer is nil then we've reached
// the root of the chain and can break
if issuer == nil || issuer.Issuer == nil {
if issuer == nil || len(issuer.Issuers) == 0 {
break
}
chain = append(chain, issuer.PEM())
issuer = issuer.Issuer
issuer = issuer.Issuers[0]
}

// Return the chain, leaf cert first
Expand Down
Loading

0 comments on commit 0abe052

Please sign in to comment.