Skip to content

Commit

Permalink
transport: local CA implementation
Browse files Browse the repository at this point in the history
This change adds a local CA that is intended to be used in testing.
  • Loading branch information
kisom committed May 1, 2016
1 parent 0967dce commit 56dfed7
Show file tree
Hide file tree
Showing 6 changed files with 575 additions and 9 deletions.
170 changes: 170 additions & 0 deletions transport/ca/localca/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Package localca implements a localca that is useful for testing the
// transport package. To use the localca, see the New and Load
// functions.
package localca

import (
"crypto/x509"
"encoding/pem"
"errors"
"time"

"github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/csr"
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/initca"
"github.com/cloudflare/cfssl/signer"
"github.com/cloudflare/cfssl/signer/local"
"github.com/kisom/goutils/assert"
)

// CA is a local transport CertificateAuthority that is useful for
// tests.
type CA struct {
s *local.Signer
disabled bool

// Label and Profile are used to select the CFSSL signer
// components if they should be anything but the default.
Label string `json:"label"`
Profile string `json:"profile"`

// The KeyFile and CertFile are required when using Load to
// construct a CA.
KeyFile string `json:"private_key,omitempty"`
CertFile string `json:"certificate,omitempty"`
}

// Toggle switches the CA between operable mode and inoperable
// mode. This is useful in testing to verify behaviours when a CA is
// unavailable.
func (lca *CA) Toggle() {
lca.disabled = !lca.disabled
}

var errNotSetup = errors.New("transport: local CA has not been setup")

// CACertificate returns the certificate authority's certificate.
func (lca *CA) CACertificate() ([]byte, error) {
if lca.s == nil {
return nil, errNotSetup
}

cert, err := lca.s.Certificate(lca.Label, lca.Profile)
if err != nil {
return nil, err
}

p := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
return pem.EncodeToMemory(p), nil
}

var errDisabled = errors.New("transport: local CA is deactivated")

// SignCSR submits a PKCS #10 certificate signing request to a CA for
// signing.
func (lca *CA) SignCSR(csrPEM []byte) ([]byte, error) {
if lca == nil || lca.s == nil {
return nil, errNotSetup
}

if lca.disabled {
return nil, errDisabled
}

p, _ := pem.Decode(csrPEM)
if p == nil || p.Type != "CERTIFICATE REQUEST" {
return nil, errors.New("transport: invalid PEM-encoded certificate signing request")
}

csr, err := x509.ParseCertificateRequest(p.Bytes)
if err != nil {
return nil, err
}

hosts := make([]string, 0, len(csr.DNSNames)+len(csr.IPAddresses))
copy(hosts, csr.DNSNames)

for i := range csr.IPAddresses {
hosts = append(hosts, csr.IPAddresses[i].String())
}

sreq := signer.SignRequest{
Hosts: hosts,
Request: string(csrPEM),
Profile: lca.Profile,
Label: lca.Label,
}

return lca.s.Sign(sreq)
}

// ExampleRequest can be used as a sample request, or the returned
// request can be modified.
func ExampleRequest() *csr.CertificateRequest {
return &csr.CertificateRequest{
Hosts: []string{"localhost"},
KeyRequest: &csr.BasicKeyRequest{
A: "ecdsa",
S: 256,
},
CN: "Transport Failover Test Local CA",
CA: &csr.CAConfig{
PathLength: 1,
Expiry: "30m",
},
}
}

// ExampleSigningConfig returns a sample config.Signing with only a
// default profile.
func ExampleSigningConfig() *config.Signing {
return &config.Signing{
Default: &config.SigningProfile{
Expiry: 15 * time.Minute,
Usage: []string{
"server auth", "client auth",
"signing", "key encipherment",
},
},
}
}

// New generates a new CA from a certificate request and signing profile.
func New(req *csr.CertificateRequest, profiles *config.Signing) (*CA, error) {
certPEM, _, keyPEM, err := initca.New(req)
if err != nil {
return nil, err
}

// If initca returns successfully, the following (which are
// all CFSSL internal functions) should not return an
// error. If they do, we should abort --- something about
// CFSSL has become inconsistent, and it can't be trusted.

priv, err := helpers.ParsePrivateKeyPEM(keyPEM)
assert.NoError(err, "CFSSL-generated private key can't be parsed")

cert, err := helpers.ParseCertificatePEM(certPEM)
assert.NoError(err, "CFSSL-generated certificate can't be parsed")

s, err := local.NewSigner(priv, cert, helpers.SignerAlgo(priv), profiles)
assert.NoError(err, "a signer could not be constructed")

return NewFromSigner(s), nil
}

// NewFromSigner constructs a local CA from a CFSSL signer.
func NewFromSigner(s *local.Signer) *CA {
return &CA{s: s}
}

// Load reads the key and certificate from the files specified in the
// CA.
func Load(lca *CA, profiles *config.Signing) (err error) {
lca.s, err = local.NewSignerFromFile(lca.CertFile, lca.KeyFile, profiles)
return err
}
161 changes: 161 additions & 0 deletions transport/ca/localca/signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package localca

import (
"encoding/pem"
"io/ioutil"
"os"
"testing"

"github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/csr"
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/initca"
"github.com/cloudflare/cfssl/selfsign"
"github.com/kisom/goutils/assert"
)

func tempName() (string, error) {
tmpf, err := ioutil.TempFile("", "transport_cachedkp_")
if err != nil {
return "", err
}

name := tmpf.Name()
tmpf.Close()
return name, nil
}

func testGenerateKeypair(req *csr.CertificateRequest) (keyFile, certFile string, err error) {
fail := func(err error) (string, string, error) {
if keyFile != "" {
os.Remove(keyFile)
}
if certFile != "" {
os.Remove(certFile)
}
return "", "", err
}

keyFile, err = tempName()
if err != nil {
return fail(err)
}

certFile, err = tempName()
if err != nil {
return fail(err)
}

csrPEM, keyPEM, err := csr.ParseRequest(req)
if err != nil {
return fail(err)
}

if err = ioutil.WriteFile(keyFile, keyPEM, 0644); err != nil {
return fail(err)
}

priv, err := helpers.ParsePrivateKeyPEM(keyPEM)
if err != nil {
return fail(err)
}

cert, err := selfsign.Sign(priv, csrPEM, config.DefaultConfig())
if err != nil {
return fail(err)
}

if err = ioutil.WriteFile(certFile, cert, 0644); err != nil {
return fail(err)
}

return
}

func TestEncodePEM(t *testing.T) {
p := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: []byte(`¯\_(ツ)_/¯`),
}
t.Logf("PEM:\n%s\n\n", string(pem.EncodeToMemory(p)))
}

func TestLoadSigner(t *testing.T) {
lca := &CA{}
certPEM, csrPEM, keyPEM, err := initca.New(ExampleRequest())
assert.NoErrorT(t, err)

_, err = lca.CACertificate()
assert.ErrorEqT(t, errNotSetup, err)

_, err = lca.SignCSR(csrPEM)
assert.ErrorEqT(t, errNotSetup, err)

lca.KeyFile, err = tempName()
assert.NoErrorT(t, err)
defer os.Remove(lca.KeyFile)

lca.CertFile, err = tempName()
assert.NoErrorT(t, err)
defer os.Remove(lca.CertFile)

err = ioutil.WriteFile(lca.KeyFile, keyPEM, 0644)
assert.NoErrorT(t, err)

err = ioutil.WriteFile(lca.CertFile, certPEM, 0644)
assert.NoErrorT(t, err)

err = Load(lca, ExampleSigningConfig())
assert.NoErrorT(t, err)
}

var testRequest = &csr.CertificateRequest{
CN: "Transport Test Identity",
KeyRequest: &csr.BasicKeyRequest{
A: "ecdsa",
S: 256,
},
Hosts: []string{"127.0.0.1"},
}

func TestNewSigner(t *testing.T) {
req := ExampleRequest()
lca, err := New(req, ExampleSigningConfig())
assert.NoErrorT(t, err)

csrPEM, _, err := csr.ParseRequest(testRequest)
assert.NoErrorT(t, err)

certPEM, err := lca.SignCSR(csrPEM)
assert.NoErrorT(t, err)

_, err = helpers.ParseCertificatePEM(certPEM)
assert.NoErrorT(t, err)

certPEM, err = lca.CACertificate()
assert.NoErrorT(t, err)

cert, err := helpers.ParseCertificatePEM(certPEM)
assert.NoErrorT(t, err)

assert.BoolT(t, cert.Subject.CommonName == req.CN,
"common names don't match")

lca.Toggle()
_, err = lca.SignCSR(csrPEM)
assert.ErrorEqT(t, errDisabled, err)
lca.Toggle()

_, err = lca.SignCSR(certPEM)
assert.ErrorT(t, err, "shouldn't be able to sign non-CSRs")

p := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: []byte(`¯\_(ツ)_/¯`),
}
junkCSR := pem.EncodeToMemory(p)

_, err = lca.SignCSR(junkCSR)
assert.ErrorT(t, err, "signing a junk CSR should fail")
t.Logf("error: %s", err)
}
14 changes: 5 additions & 9 deletions transport/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,20 +225,18 @@ func (tr *Transport) RefreshKeys() (err error) {
log.Debugf("couldn't get a CSR: %v", err)
if tr.Provider.SignalFailure(err) {
return tr.RefreshKeys()
} else {
return err
}
return err
}

log.Debug("requesting certificate from CA")
cert, err := tr.CA.SignCSR(req)
if err != nil {
if tr.Provider.SignalFailure(err) {
return tr.RefreshKeys()
} else {
log.Debugf("failed to get the certificate signed: %v", err)
return err
}
log.Debugf("failed to get the certificate signed: %v", err)
return err
}

log.Debug("giving the certificate to the provider")
Expand All @@ -247,9 +245,8 @@ func (tr *Transport) RefreshKeys() (err error) {
log.Debugf("failed to set the provider's certificate: %v", err)
if tr.Provider.SignalFailure(err) {
return tr.RefreshKeys()
} else {
return err
}
return err
}

if tr.Provider.Persistent() {
Expand All @@ -260,9 +257,8 @@ func (tr *Transport) RefreshKeys() (err error) {
log.Debugf("the provider failed to store the certificate: %v", err)
if tr.Provider.SignalFailure(err) {
return tr.RefreshKeys()
} else {
return err
}
return err
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions vendor/github.com/kisom/goutils/assert/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 56dfed7

Please sign in to comment.