Skip to content

Commit

Permalink
Certificate issuance (#16)
Browse files Browse the repository at this point in the history
This PR adds the initial CA skeleton for doing order issuance.

Pebble generates a root & intermediate keypair/certificate at startup. The intermediate is used for certificate signing purposes and the root issues the intermediate and is otherwise just there to mimick production. In the future we should introduce additional intermediates & chain options to use the full specification. 

Couple other changes:
* Fixed the embedded challenges in authorizations to use a pointer so that the embedded contents are updated when the challenge is completed.
* Replaced a few WFE `Printf`'s with log statements.
* Updated the VA to log whether an HTTP validation was a success or a failure.
* Added a pointer from Authorizations to the Order they belong to
* Added enforcement of Order expiry for authz updates.
* Moved the MemoryStore out of the `wfe` package. This was primarily to let the CA store the root & intermediate certificates in the DB. This allows using the `/certZ/` endpoint to retrieve the root & intermediate.
  • Loading branch information
cpu authored and jsha committed Mar 22, 2017
1 parent 7142cb8 commit 5112962
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 70 deletions.
15 changes: 8 additions & 7 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ const (
)

const (
StatusPending = "pending"
StatusInvalid = "invalid"
StatusValid = "valid"
StatusPending = "pending"
StatusInvalid = "invalid"
StatusValid = "valid"
StatusProcessing = "processing"

IdentifierDNS = "dns"

Expand Down Expand Up @@ -49,10 +50,10 @@ type Order struct {

// An Authorization is created for each identifier in an order
type Authorization struct {
Status string `json:"status"`
Identifier Identifier `json:"identifier"`
Challenges []Challenge `json:"challenges"`
Expires string `json:"expires"`
Status string `json:"status"`
Identifier Identifier `json:"identifier"`
Challenges []*Challenge `json:"challenges"`
Expires string `json:"expires"`
}

// A Challenge is used to validate an Authorization
Expand Down
220 changes: 220 additions & 0 deletions ca/ca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package ca

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"fmt"
"log"
"math"
"math/big"
"time"

"github.com/letsencrypt/pebble/core"
"github.com/letsencrypt/pebble/db"
)

const (
rootCAPrefix = "Pebble Root CA "
intermediateCAPrefix = "Pebble Intermediate CA "
)

type CAImpl struct {
log *log.Logger
db *db.MemoryStore

root *issuer
intermediate *issuer
}

type issuer struct {
key crypto.Signer
cert *core.Certificate
}

func makeSerial() *big.Int {
serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
panic(fmt.Sprintf("unable to create random serial number: %s", err.Error()))
}
return serial
}

// makeKey and makeRootCert are adapted from MiniCA:
// https://github.com/jsha/minica/blob/3a621c05b61fa1c24bcb42fbde4b261db504a74f/main.go

// makeKey creates a new 2048 bit RSA private key
func makeKey() (*rsa.PrivateKey, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
return key, nil
}

func (ca *CAImpl) makeRootCert(
subjectKey crypto.Signer,
subjCNPrefix string,
signer *issuer) (*core.Certificate, error) {

serial := makeSerial()
template := &x509.Certificate{
Subject: pkix.Name{
CommonName: subjCNPrefix + hex.EncodeToString(serial.Bytes()[:3]),
},
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(30, 0, 0),

KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}

var signerKey crypto.Signer
if signer != nil && signer.key != nil {
signerKey = signer.key
} else {
signerKey = subjectKey
}

der, err := x509.CreateCertificate(rand.Reader, template, template, subjectKey.Public(), signerKey)
if err != nil {
return nil, err
}

cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}

hexSerial := hex.EncodeToString(cert.SerialNumber.Bytes())
newCert := &core.Certificate{
ID: hexSerial,
Cert: cert,
DER: der,
}
if signer != nil && signer.cert != nil {
newCert.Issuer = signer.cert
}
_, err = ca.db.AddCertificate(newCert)
if err != nil {
return nil, err
}
return newCert, nil
}

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

ca.root = &issuer{
key: rk,
cert: rc,
}
ca.log.Printf("Generated new root issuer with serial %s\n", rc.ID)
return 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
}

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

func (ca *CAImpl) NewCertificate(domains []string, key crypto.PublicKey) (*core.Certificate, error) {
var cn string
if len(domains) > 0 {
cn = domains[0]
} else {
return nil, fmt.Errorf("must specify at least one domain name")
}

issuer := ca.intermediate
if issuer == nil || issuer.cert == nil {
return nil, fmt.Errorf("cannot sign certificate - nil issuer")
}

serial := makeSerial()
template := &x509.Certificate{
DNSNames: domains,
Subject: pkix.Name{
CommonName: cn,
},
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(5, 0, 0),

KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: false,
}
der, err := x509.CreateCertificate(rand.Reader, template, issuer.cert.Cert, key, issuer.key)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}

hexSerial := hex.EncodeToString(cert.SerialNumber.Bytes())
newCert := &core.Certificate{
ID: hexSerial,
Cert: cert,
DER: der,
Issuer: issuer.cert,
}
_, err = ca.db.AddCertificate(newCert)
if err != nil {
return nil, err
}
return newCert, nil
}

func New(log *log.Logger, db *db.MemoryStore) *CAImpl {
ca := &CAImpl{
log: log,
db: db,
}
err := ca.newRootIssuer()
if err != nil {
panic(fmt.Sprintf("Error creating new root issuer: %s", err.Error()))
}
err = ca.newIntermediateIssuer()
if err != nil {
panic(fmt.Sprintf("Error creating new intermediate issuer: %s", err.Error()))
}
return ca
}
7 changes: 5 additions & 2 deletions cmd/pebble/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"os"

"github.com/jmhodges/clock"
"github.com/letsencrypt/pebble/ca"
"github.com/letsencrypt/pebble/cmd"
"github.com/letsencrypt/pebble/db"
"github.com/letsencrypt/pebble/va"
"github.com/letsencrypt/pebble/wfe"
)
Expand Down Expand Up @@ -38,9 +40,10 @@ func main() {
cmd.FailOnError(err, "Reading JSON config file into config structure")

clk := clock.Default()

db := db.NewMemoryStore()
va := va.New(logger, clk, c.Pebble.HTTPPort)
wfe, err := wfe.New(logger, clk, va)
ca := ca.New(logger, db)
wfe := wfe.New(logger, clk, db, va, ca)
muxHandler := wfe.Handler()

srv := &http.Server{
Expand Down
54 changes: 52 additions & 2 deletions core/types.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package core

import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"time"

"github.com/letsencrypt/pebble/acme"
Expand All @@ -12,8 +15,10 @@ import (

type Order struct {
acme.Order
ID string
ParsedCSR *x509.CertificateRequest
ID string
ParsedCSR *x509.CertificateRequest
ExpiresDate time.Time
AuthorizationObjects []*Authorization
}

type Registration struct {
Expand All @@ -27,6 +32,7 @@ type Authorization struct {
ID string
URL string
ExpiresDate time.Time
Order *Order
}

type Challenge struct {
Expand All @@ -48,3 +54,47 @@ func (ch Challenge) ExpectedKeyAuthorization(key *jose.JSONWebKey) string {

return ch.Token + "." + base64.RawURLEncoding.EncodeToString(thumbprint)
}

type Certificate struct {
ID string
Cert *x509.Certificate
DER []byte
Issuer *Certificate
}

func (c Certificate) PEM() []byte {
var buf bytes.Buffer

err := pem.Encode(&buf, &pem.Block{
Type: "CERTIFICATE",
Bytes: c.DER,
})
if err != nil {
panic(fmt.Sprintf("Unable to encode certificate %q to PEM: %s",
c.ID, err.Error()))
}

return buf.Bytes()
}

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

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

// Add zero or more issuers
issuer := c.Issuer
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 {
break
}
chain = append(chain, issuer.PEM())
issuer = issuer.Issuer
}

// Return the chain, leaf cert first
return bytes.Join(chain, nil)
}
Loading

0 comments on commit 5112962

Please sign in to comment.