Use a remote CFSSL instance for signing

This commit is contained in:
Richard Barnes 2015-03-04 11:35:51 -05:00
parent d2c6035abb
commit ba3a892de2
4 changed files with 186 additions and 115 deletions

View File

@ -47,21 +47,32 @@ func main() {
app.Usage = "Command-line utility to start Boulder's servers in stand-alone mode"
app.Version = "0.0.0"
// Specify AMQP Server
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "amqp",
Value: "amqp://guest:guest@localhost:5672",
EnvVar: "AMQP_SERVER",
Usage: "AMQP Broker URI",
Name: "amqp",
Value: "amqp://guest:guest@localhost:5672",
EnvVar: "AMQP_SERVER",
Usage: "AMQP Broker URI",
},
/*
cli.StringFlag{
Name: "cfssl",
Value: "localhost:8888",
EnvVar: "CFSSL_SERVER",
Usage: "CFSSL Server URI",
},
cli.StringFlag{
Name: "cfssl",
Value: "tcp://localhost:8888",
EnvVar: "CFSSL_PORT",
Usage: "CFSSL Server URI",
cli.StringFlag{
Name: "cfsslAuthKey",
EnvVar: "CFSSL_AUTH_KEY",
Usage: "CFSSL authentication key",
},
cli.StringFlag{
Name: "cfsslProfile",
EnvVar: "CFSSL_PROFILE",
Usage: "CFSSL signing profile",
},
*/
}
// One command per element of the system
@ -79,18 +90,30 @@ func main() {
Name: "monolithic",
Usage: "Start the CA in monolithic mode, without using AMQP",
Action: func(c *cli.Context) {
// XXX Print the config
fmt.Println(c.GlobalString("amqp"))
fmt.Println(c.GlobalString("cfssl"))
fmt.Println(c.GlobalString("cfsslAuthKey"))
fmt.Println(c.GlobalString("cfsslProfile"))
// Grab parameters
cfsslServer := c.GlobalString("cfssl")
authKey := c.GlobalString("cfsslAuthKey")
profile := c.GlobalString("cfsslProfile")
// Create the components
wfe := boulder.NewWebFrontEndImpl()
sa := boulder.NewSimpleStorageAuthorityImpl()
ra := boulder.NewRegistrationAuthorityImpl()
va := boulder.NewValidationAuthorityImpl()
ca, err := boulder.NewCertificateAuthorityImpl()
ca, err := boulder.NewCertificateAuthorityImpl(cfsslServer, authKey, profile)
failOnError(err, "Unable to create CA")
// Wire them up
wfe.RA = &ra
wfe.SA = &sa
ra.CA = &ca
ra.CA = ca
ra.SA = &sa
ra.VA = &va
va.RA = &ra
@ -115,6 +138,11 @@ func main() {
Name: "monolithic-amqp",
Usage: "Start the CA in monolithic mode, using AMQP",
Action: func(c *cli.Context) {
// Grab parameters
cfsslServer := c.GlobalString("cfssl")
authKey := c.GlobalString("cfsslAuthKey")
profile := c.GlobalString("cfsslProfile")
// Create an AMQP channel
ch := amqpChannel(c.GlobalString("amqp"))
@ -131,7 +159,9 @@ func main() {
// ... and corresponding servers
// (We need this order so that we can give the servers
// references to the clients)
cas, err := boulder.NewCertificateAuthorityServer("CA.server", ch)
cai, err := boulder.NewCertificateAuthorityImpl(cfsslServer, authKey, profile)
failOnError(err, "Failed to create CA impl")
cas, err := boulder.NewCertificateAuthorityServer("CA.server", ch, cai)
failOnError(err, "Failed to create CA server")
vas, err := boulder.NewValidationAuthorityServer("VA.server", ch, &rac)
failOnError(err, "Failed to create VA server")
@ -203,9 +233,16 @@ func main() {
Name: "ca",
Usage: "Start the CertificateAuthority",
Action: func(c *cli.Context) {
// Grab parameters
cfsslServer := c.GlobalString("cfssl")
authKey := c.GlobalString("cfsslAuthKey")
profile := c.GlobalString("cfsslProfile")
ch := amqpChannel(c.GlobalString("amqp"))
cas, err := boulder.NewCertificateAuthorityServer("CA.server", ch)
cai, err := boulder.NewCertificateAuthorityImpl(cfsslServer, authKey, profile)
failOnError(err, "Failed to create CA impl")
cas, err := boulder.NewCertificateAuthorityServer("CA.server", ch, cai)
failOnError(err, "Unable to create CA server")
runForever(cas)
},

View File

@ -6,117 +6,96 @@
package boulder
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"time"
"github.com/cloudflare/cfssl/auth"
"github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/signer"
"github.com/cloudflare/cfssl/signer/remote"
)
type CertificateAuthorityImpl struct {
privateKey interface{}
certificate x509.Certificate
derCertificate []byte
signer signer.Signer
profile string
}
var (
serialNumberBits = uint(64)
oneYear = 365 * 24 * time.Hour
rootCertificateTemplate = x509.Certificate{
SignatureAlgorithm: x509.SHA256WithRSA,
Subject: pkix.Name{Organization: []string{"ACME CA"}},
IsCA: true,
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
// NewCertificateAuthorityImpl creates a CA that talks to a remote CFSSL
// instance. (To use a local signer, simply instantiate CertificateAuthorityImpl
// directly.) Communications with the CA are authenticated with MACs,
// using CFSSL's authenticated signature scheme. A CA created in this way
// issues for a single profile on the remote signer, which is indicated
// by name in this constructor.
func NewCertificateAuthorityImpl(hostport string, authKey string, profile string) (ca *CertificateAuthorityImpl, err error) {
// Create the remote signer
localProfile := config.SigningProfile{
Expiry: 60 * time.Minute, // BOGUS: Required by CFSSL, but not used
RemoteName: hostport,
}
eeCertificateTemplate = x509.Certificate{
SignatureAlgorithm: x509.SHA256WithRSA,
IsCA: false,
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
)
func newSerialNumber() (*big.Int, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), serialNumberBits)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
localProfile.Provider, err = auth.New(authKey, nil)
if err != nil {
return nil, err
return
}
return serialNumber, nil
signer, err := remote.NewSigner(&config.Signing{Default: &localProfile})
if err != nil {
return
}
ca = &CertificateAuthorityImpl{signer: signer, profile: profile}
return
}
func NewCertificateAuthorityImpl() (CertificateAuthorityImpl, error) {
zero := CertificateAuthorityImpl{}
// Generate a key pair
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return zero, err
func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest) (cert []byte, err error) {
// XXX Take in authorizations and verify that union covers CSR?
// Pull hostnames from CSR
hostNames := csr.DNSNames // DNSNames + CN from CSR
if len(hostNames) < 1 {
err = errors.New("Cannot issue a certificate without a hostname.")
return
}
// Sign the certificate
template := rootCertificateTemplate
template.SerialNumber, err = newSerialNumber()
if err != nil {
return zero, err
}
template.NotBefore = time.Now()
template.NotAfter = template.NotBefore.Add(oneYear)
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return zero, err
}
// Parse the certificate
certs, err := x509.ParseCertificates(der)
if err != nil || len(certs) == 0 {
return zero, err
}
return CertificateAuthorityImpl{
privateKey: priv,
certificate: *certs[0],
derCertificate: der,
}, nil
}
func (ca *CertificateAuthorityImpl) CACertificate() []byte {
return ca.derCertificate
}
func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest) ([]byte, error) {
template := eeCertificateTemplate
// Set serial
serialNumber, err := newSerialNumber()
if err != nil {
return nil, err
}
template.SerialNumber = serialNumber
// Set validity
template.NotBefore = time.Now()
template.NotAfter = template.NotBefore.Add(oneYear)
// Set hostnames
domains := csr.DNSNames
var commonName string
if len(csr.Subject.CommonName) > 0 {
domains = append(domains, csr.Subject.CommonName)
commonName = csr.Subject.CommonName
} else {
commonName = hostNames[0]
}
if len(domains) == 0 {
return []byte{}, errors.New("No names provided for certificate")
}
template.Subject = pkix.Name{CommonName: domains[0]}
template.DNSNames = domains
// Sign
return x509.CreateCertificate(rand.Reader, &template, &ca.certificate, csr.PublicKey, ca.privateKey)
// Convert the CSR to PEM
csrPEM := string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr.Raw,
}))
// Send the cert off for signing
req := signer.SignRequest{
Request: csrPEM,
Profile: ca.profile,
Hostname: commonName,
Subject: &signer.Subject{
CN: commonName,
Hosts: hostNames,
},
}
certPEM, err := ca.signer.Sign(req)
if err != nil {
return
}
if len(certPEM) == 0 {
err = errors.New("No certificate returned by server")
return
}
block, _ := pem.Decode(certPEM)
if block == nil || block.Type != "CERTIFICATE" {
err = errors.New("Invalid certificate value returned")
return
}
cert = block.Bytes
return
}

View File

@ -0,0 +1,60 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package boulder
import (
"crypto/x509"
"encoding/hex"
"testing"
)
// CSR generated by Go:
// * Random public key
// * CN = example.com
// * DNSNames = example.com, www.example.com
var CSR_HEX = "308202953082017d0201003016311430120603" +
"550403130b6578616d706c652e636f6d30820122300d06092a864886f70d0101010500038201" +
"0f003082010a0282010100baaf16e891828470cad87b849a73356f65e20ad3699fd5583a7200" +
"e924512d9eeb1dbe16441ad7bd804fa2e5726a06f0af5279012fe6354a5677259f5591984aa9" +
"99b8ea3ea10fbd5ecfa30e5f563b41c419374decfc98ea62c611046ad011c326470a2426f46d" +
"be6cc44fae3b247e19710810585f9f3ad7f64b2f305aebb72e2829866f89b20b03a300b7ff5f" +
"4e6204f41420d9fa731252692cee8e616636723abe8a7053fd86e2673190fa8b618ada5bc735" +
"ba57a145af86904a8f55a288d4d6ba9e501530f23f197f5b623443193fc92b7f87d6abbf740d" +
"9fc92800c7e0e1484d5eec6ffae1007c139c1ec19d67e749743fe8d8396fe190cfbcf2f90e05" +
"230203010001a03a303806092a864886f70d01090e312b302930270603551d110420301e820b" +
"6578616d706c652e636f6d820f7777772e6578616d706c652e636f6d300d06092a864886f70d" +
"01010b05000382010100514c622dc866b31c86777a26e9b2911618ce5b188591f08007b42772" +
"3497b733676a7d493c434fc819b8089869442fd299aa99ff7f7b9df881843727f0c8b89ca62a" +
"f8a12b38c963e9210148c4e1c0fc964aef2605f88ed777e6497d2e43e9a9713835d1ae96260c" +
"ca826c34c7ae52c77f5d8468643ee1936eadf04e1ebf8bbbb68b0ec7d0ef694432451292e4a3" +
"1989fd8339c07e01e04b6dd3834872b828d3f5b2b4dadda0596396e15fbdf446998066a74873" +
"2baf53f3f7ebb849e83cf734753c35ab454d1b62e1741a6514c5c575c0c805b4d668fcf71746" +
"ef32017613a52d6b807e2977f4fbc0a88b2e263347c4d9e35435333cf4f8288be53df41228ec"
func TestIssueCertificate(t *testing.T) {
// Decode pre-generated CSR
csrDER, _ := hex.DecodeString(CSR_HEX)
csr, _ := x509.ParseCertificateRequest(csrDER)
// Create a CA
// This assumes that there is a CFSSL instance running that supports these parameters
ca, err := NewCertificateAuthorityImpl("localhost:9000", "79999d86250c367a2b517a1ae7d409c1", "ee")
AssertNotError(t, err, "Failed to create CA")
// Sign CSR
certDER, err := ca.IssueCertificate(*csr)
AssertNotError(t, err, "Failed to sign certificate")
// Verify cert contents
cert, err := x509.ParseCertificate(certDER)
AssertNotError(t, err, "Certificate failed to parse")
AssertEquals(t, cert.Subject.CommonName, "example.com")
if len(cert.DNSNames) != 2 || cert.DNSNames[0] != "example.com" || cert.DNSNames[1] != "www.example.com" {
t.Errorf("Improper list of domain names %v", cert.DNSNames)
}
}

View File

@ -282,14 +282,9 @@ func (vac ValidationAuthorityClient) UpdateValidations(authz Authorization) erro
// CertificateAuthorityClient / Server
// -> IssueCertificate
func NewCertificateAuthorityServer(serverQueue string, channel *amqp.Channel) (rpc *AmqpRpcServer, err error) {
func NewCertificateAuthorityServer(serverQueue string, channel *amqp.Channel, impl CertificateAuthority) (rpc *AmqpRpcServer, err error) {
rpc = NewAmqpRpcServer(serverQueue, channel)
impl, err := NewCertificateAuthorityImpl()
if err != nil {
return
}
rpc.Handle(MethodIssueCertificate, func(req []byte) []byte {
zero := []byte{}