forked from cloudflare/cfssl
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This change adds a local CA that is intended to be used in testing.
- Loading branch information
Showing
6 changed files
with
575 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.