better waitlist-gating (#557)

* better waitlist-gating

(cherry picked from commit 490fe02b7c3558da18678dfb651c92ec9c4a75b5)

* fix broken test

* linter fixes

* linter fixes

* make extension verification optional

* add certifcate gating script for captplanet

* fixing tests

* linter fixes

* linter fixes?

* moar linter fixes

* Revert "moar linter fixes"

This reverts commit 8139ccbd73cbbead987b7667567844f50f7df2c8.

* just kill me

* refactor

* refactor tests

* liniter...

* cleanup
This commit is contained in:
Bryan White 2018-11-01 16:48:43 +01:00 committed by GitHub
parent 2a8b681c4d
commit 8b9711cb5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 368 additions and 102 deletions

View File

@ -30,7 +30,7 @@ var argError = errs.Class("argError")
func main() {
cobra.EnableCommandSorting = false
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
if err != nil {
log.Fatal(err)
}

View File

@ -36,7 +36,7 @@ func main() {
logger, _ := zap.NewDevelopment()
defer printError(logger.Sync)
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
if err != nil {
logger.Error("Failed to create certificate authority: ", zap.Error(err))
os.Exit(1)

View File

@ -44,7 +44,7 @@ func main() {
logger, _ := zap.NewDevelopment()
defer printError(logger.Sync)
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
if err != nil {
logger.Error("Failed to create certificate authority: ", zap.Error(err))
os.Exit(1)

View File

@ -24,7 +24,7 @@ type Reporter struct {
// NewReporter instantiates a reporter
func NewReporter(ctx context.Context, statDBPort string, maxRetries int) (reporter *Reporter, err error) {
ca, err := provider.NewCA(ctx, 12, 14)
ca, err := provider.NewTestCA(ctx)
if err != nil {
return nil, err
}

View File

@ -16,7 +16,7 @@ import (
func TestGenerateSignature(t *testing.T) {
ctx := context.Background()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -41,7 +41,7 @@ func TestGenerateSignature(t *testing.T) {
func TestSignedMessageVerifier(t *testing.T) {
ctx := context.Background()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

View File

@ -64,7 +64,7 @@ func TestNewKademlia(t *testing.T) {
for i, v := range cases {
dir := filepath.Join(rootdir, strconv.Itoa(i))
ca, err := provider.NewCA(context.Background(), 12, 4)
ca, err := provider.NewTestCA(context.Background())
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

View File

@ -92,7 +92,7 @@ func TestWorkerLookup(t *testing.T) {
{
name: "test valid chore returned",
worker: func() *worker {
ca, err := provider.NewCA(context.Background(), 12, 4)
ca, err := provider.NewTestCA(context.Background())
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -138,7 +138,7 @@ func TestUpdate(t *testing.T) {
{
name: "test nil nodes",
worker: func() *worker {
ca, err := provider.NewCA(context.Background(), 12, 4)
ca, err := provider.NewTestCA(context.Background())
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -154,7 +154,7 @@ func TestUpdate(t *testing.T) {
{
name: "test combined less than k",
worker: func() *worker {
ca, err := provider.NewCA(context.Background(), 12, 4)
ca, err := provider.NewTestCA(context.Background())
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -182,7 +182,7 @@ func TestUpdate(t *testing.T) {
}
func newTestServer(nn []*pb.Node) (*grpc.Server, *mockNodeServer) {
ca, err := provider.NewCA(context.Background(), 12, 4)
ca, err := provider.NewTestCA(context.Background())
if err != nil {
return nil, nil
}

View File

@ -14,7 +14,10 @@ type ID string
// NewFullIdentity creates a new ID for nodes with difficulty and concurrency params
func NewFullIdentity(ctx context.Context, difficulty uint16, concurrency uint) (*provider.FullIdentity, error) {
ca, err := provider.NewCA(ctx, difficulty, concurrency)
ca, err := provider.NewCA(ctx, provider.NewCAOptions{
Difficulty: difficulty,
Concurrency: concurrency,
})
if err != nil {
return nil, err
}

View File

@ -59,7 +59,7 @@ func TestLookup(t *testing.T) {
mdht.EXPECT().GetRoutingTable(gomock.Any()).Return(mrt, nil)
mrt.EXPECT().ConnectionSuccess(gomock.Any()).Return(nil)
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -154,7 +154,7 @@ func NewNodeID(t *testing.T) string {
}
func newTestIdentity(t *testing.T) *provider.FullIdentity {
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

View File

@ -68,7 +68,7 @@ func bootstrapTestNetwork(t *testing.T, ip, port string) ([]dht.DHT, pb.Node) {
intro, err := kademlia.GetIntroNode(net.JoinHostPort(ip, pm))
assert.NoError(t, err)
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

View File

@ -42,7 +42,7 @@ func TestNewOverlayClient(t *testing.T) {
}
for _, v := range cases {
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -101,7 +101,7 @@ func TestChoose(t *testing.T) {
})
}
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -151,7 +151,7 @@ func TestLookup(t *testing.T) {
go func() { assert.NoError(t, srv.Serve(lis)) }()
defer srv.Stop()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -187,7 +187,7 @@ func TestBulkLookup(t *testing.T) {
go func() { assert.NoError(t, srv.Serve(lis)) }()
defer srv.Stop()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -219,7 +219,7 @@ func TestBulkLookupV2(t *testing.T) {
go func() { assert.NoError(t, srv.Serve(lis)) }()
defer srv.Stop()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)
@ -291,7 +291,7 @@ func TestBulkLookupV2(t *testing.T) {
}
func newServer(ctx context.Context, redisAddr string) (*grpc.Server, *Server, error) {
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
if err != nil {
return nil, nil, err
}
@ -317,7 +317,7 @@ func newServer(ctx context.Context, redisAddr string) (*grpc.Server, *Server, er
}
func newTestServer(ctx context.Context) (*grpc.Server, *mockOverlayServer, error) {
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
if err != nil {
return nil, nil, err
}

View File

@ -9,6 +9,7 @@ import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"io"
@ -26,6 +27,9 @@ const (
)
var (
// AuthoritySignatureExtID is the asn1 object ID for a pkix extension holding a signature of the leaf cert, signed by some CA (e.g. the root cert)
// This extension allows for an additional signature per certificate
AuthoritySignatureExtID = asn1.ObjectIdentifier{2, 999, 1}
// ErrNotExist is used when a file or directory doesn't exist
ErrNotExist = errs.Class("file or directory not found error")
// ErrGenerate is used when an error occurred during cert/key generation
@ -45,6 +49,8 @@ var (
ErrVerifyCertificateChain = errs.Class("certificate chain signature verification failed")
// ErrVerifyCAWhitelist is used when the leaf of a peer certificate isn't signed by any CA in the whitelist
ErrVerifyCAWhitelist = errs.Class("certificate isn't signed by any CA in the whitelist")
// ErrSign is used when something goes wrong while generating a signature
ErrSign = errs.Class("unable to generate signature")
)
// PeerCertVerificationFunc is the signature for a `*tls.Config{}`'s
@ -117,21 +123,41 @@ func VerifyPeerCertChains(_ [][]byte, parsedChains [][]*x509.Certificate) error
return verifyChainSignatures(parsedChains[0])
}
// VerifyCAWhitelist verifies that the peer identity's leaf was signed by any one of the
// (certificate authority) certificates in the provided whitelist
func VerifyCAWhitelist(cas []*x509.Certificate) PeerCertVerificationFunc {
// VerifyCAWhitelist verifies that the peer identity's CA and leaf-extension was signed
// by any one of the (certificate authority) certificates in the provided whitelist
func VerifyCAWhitelist(cas []*x509.Certificate, verifyExtension bool) PeerCertVerificationFunc {
if cas == nil {
return nil
}
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
var err error
var (
leaf = parsedChains[0][0]
err error
)
// Leaf extension must contain leaf signature, signed by a CA in the whitelist.
// That *same* CA must also have signed the leaf's parent cert (regular cert chain signature, not extension).
for _, ca := range cas {
err = verifyCertSignature(ca, parsedChains[0][0])
err = verifyCertSignature(ca, parsedChains[0][1])
if err == nil {
return nil
if !verifyExtension {
break
}
for _, ext := range leaf.Extensions {
if ext.Id.Equal(AuthoritySignatureExtID) {
err = verifySignature(ext.Value, leaf.RawTBSCertificate, leaf.PublicKey)
if err != nil {
return ErrVerifyCAWhitelist.New("authority signature extension verification error: %s", err.Error())
}
return nil
}
}
break
}
}
return ErrVerifyCAWhitelist.Wrap(err)
}
}

View File

@ -154,15 +154,19 @@ func TestVerifyCAWhitelist(t *testing.T) {
lt, err := LeafTemplate()
assert.NoError(t, err)
lp, ok := k.(*ecdsa.PrivateKey)
lk, err := NewKey()
assert.NoError(t, err)
lp, ok := lk.(*ecdsa.PrivateKey)
assert.True(t, ok)
l, err := NewCert(lt, ct, &lp.PublicKey, k)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist(nil))([][]byte{l.Raw, c.Raw}, nil)
err = VerifyPeerFunc(VerifyCAWhitelist(nil, false))([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c}))([][]byte{l.Raw, c.Raw}, nil)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c}, false))([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err)
zk, err := NewKey()
@ -176,13 +180,48 @@ func TestVerifyCAWhitelist(t *testing.T) {
z, err := NewCert(zt, nil, &zp.PublicKey, zk)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z}))([][]byte{l.Raw, c.Raw}, nil)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z}, false))([][]byte{l.Raw, c.Raw}, nil)
assert.True(t, ErrVerifyCAWhitelist.Has(err))
assert.True(t, ErrVerifySignature.Has(err))
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z, c}))([][]byte{l.Raw, c.Raw}, nil)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z, c}, false))([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c, z}))([][]byte{l.Raw, c.Raw}, nil)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c, z}, false))([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err)
xt, err := LeafTemplate()
assert.NoError(t, err)
xk, err := NewKey()
assert.NoError(t, err)
xp, ok := xk.(*ecdsa.PrivateKey)
assert.True(t, ok)
x, err := NewCert(xt, zt, &xp.PublicKey, zk)
assert.NoError(t, err)
yt, err := LeafTemplate()
assert.NoError(t, err)
yk, err := NewKey()
assert.NoError(t, err)
yp, ok := yk.(*ecdsa.PrivateKey)
assert.True(t, ok)
y, err := NewCert(yt, xt, &yp.PublicKey, xk)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z, c}, false))([][]byte{z.Raw, x.Raw, y.Raw}, nil)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c, z}, false))([][]byte{z.Raw, x.Raw, y.Raw}, nil)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z, c}, true))([][]byte{z.Raw, x.Raw, y.Raw}, nil)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c, z}, true))([][]byte{z.Raw, x.Raw, y.Raw}, nil)
assert.NoError(t, err)
}

View File

@ -23,7 +23,9 @@ import (
"github.com/zeebo/errs"
)
type ecdsaSignature struct {
// ECDSASignature holds the `r` and `s` values in an ecdsa signature
// (see https://golang.org/pkg/crypto/ecdsa)
type ECDSASignature struct {
R, S *big.Int
}
@ -73,28 +75,31 @@ func verifyChainSignatures(certs []*x509.Certificate) error {
}
func verifyCertSignature(parentCert, childCert *x509.Certificate) error {
pubKey, ok := parentCert.PublicKey.(*ecdsa.PublicKey)
return verifySignature(childCert.Signature, childCert.RawTBSCertificate, parentCert.PublicKey)
}
func verifySignature(signedData []byte, data []byte, pubKey crypto.PublicKey) error {
key, ok := pubKey.(*ecdsa.PublicKey)
if !ok {
return ErrUnsupportedKey.New("%T", parentCert.PublicKey)
return ErrUnsupportedKey.New("%T", key)
}
signature := new(ecdsaSignature)
signature := new(ECDSASignature)
if _, err := asn1.Unmarshal(childCert.Signature, signature); err != nil {
if _, err := asn1.Unmarshal(signedData, signature); err != nil {
return ErrVerifySignature.New("unable to unmarshal ecdsa signature: %v", err)
}
h := crypto.SHA256.New()
_, err := h.Write(childCert.RawTBSCertificate)
_, err := h.Write(data)
if err != nil {
return ErrVerifySignature.Wrap(err)
}
digest := h.Sum(nil)
if !ecdsa.Verify(pubKey, digest, signature.R, signature.S) {
if !ecdsa.Verify(key, digest, signature.R, signature.S) {
return ErrVerifySignature.New("signature is not valid")
}
return nil
}

View File

@ -535,14 +535,14 @@ func NewTestServer(t *testing.T) *TestServer {
}
}
caS, err := provider.NewCA(context.Background(), 12, 4)
caS, err := provider.NewTestCA(context.Background())
check(err)
fiS, err := caS.NewIdentity()
check(err)
so, err := fiS.ServerOption()
check(err)
caC, err := provider.NewCA(context.Background(), 12, 4)
caC, err := provider.NewTestCA(context.Background())
check(err)
fiC, err := caC.NewIdentity()
check(err)

View File

@ -300,7 +300,7 @@ func TestDelete(t *testing.T) {
func TestSignedMessage(t *testing.T) {
ctx := context.Background()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

View File

@ -67,7 +67,7 @@ func TestServicePut(t *testing.T) {
func TestServiceGet(t *testing.T) {
ctx := context.Background()
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

View File

@ -13,7 +13,10 @@ import (
func TestNewCA(t *testing.T) {
expectedDifficulty := uint16(4)
ca, err := NewCA(context.Background(), expectedDifficulty, 5)
ca, err := NewCA(context.Background(), NewCAOptions{
Difficulty: expectedDifficulty,
Concurrency: 5,
})
assert.NoError(t, err)
assert.NotEmpty(t, ca)
@ -28,7 +31,7 @@ func TestFullCertificateAuthority_NewIdentity(t *testing.T) {
}
}
ca, err := NewCA(context.Background(), 12, 5)
ca, err := NewTestCA(context.Background())
check(err, ca)
fi, err := ca.NewIdentity()
check(err, fi)
@ -44,7 +47,10 @@ func TestFullCertificateAuthority_NewIdentity(t *testing.T) {
func NewCABenchmark(b *testing.B, difficulty uint16, concurrency uint) {
for i := 0; i < b.N; i++ {
_, _ = NewCA(context.Background(), difficulty, concurrency)
_, _ = NewCA(context.Background(), NewCAOptions{
Difficulty: difficulty,
Concurrency: concurrency,
})
}
}

View File

@ -11,7 +11,6 @@ import (
"encoding/pem"
"io/ioutil"
"os"
"reflect"
"github.com/zeebo/errs"
@ -21,6 +20,7 @@ import (
// PeerCertificateAuthority represents the CA which is used to validate peer identities
type PeerCertificateAuthority struct {
RestChain []*x509.Certificate
// Cert is the x509 certificate of the CA
Cert *x509.Certificate
// The ID is calculated from the CA public key.
@ -29,6 +29,7 @@ type PeerCertificateAuthority struct {
// FullCertificateAuthority represents the CA which is used to author and validate full identities
type FullCertificateAuthority struct {
RestChain []*x509.Certificate
// Cert is the x509 certificate of the CA
Cert *x509.Certificate
// The ID is calculated from the CA public key.
@ -39,12 +40,26 @@ type FullCertificateAuthority struct {
// CASetupConfig is for creating a CA
type CASetupConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/ca.cert"`
KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/ca.key"`
Difficulty uint64 `help:"minimum difficulty for identity generation" default:"12"`
Timeout string `help:"timeout for CA generation; golang duration string (0 no timeout)" default:"5m"`
Overwrite bool `help:"if true, existing CA certs AND keys will overwritten" default:"false"`
Concurrency uint `help:"number of concurrent workers for certificate authority generation" default:"4"`
ParentCertPath string `help:"path to the parent authority's certificate chain"`
ParentKeyPath string `help:"path to the parent authority's private key"`
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/ca.cert"`
KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/ca.key"`
Difficulty uint64 `help:"minimum difficulty for identity generation" default:"12"`
Timeout string `help:"timeout for CA generation; golang duration string (0 no timeout)" default:"5m"`
Overwrite bool `help:"if true, existing CA certs AND keys will overwritten" default:"false"`
Concurrency uint `help:"number of concurrent workers for certificate authority generation" default:"4"`
}
// NewCAOptions is used to pass parameters to `NewCA`
type NewCAOptions struct {
// Difficulty is the number of trailing zero-bits the nodeID must have
Difficulty uint16
// Concurrency is the number of go routines used to generate a CA of sufficient difficulty
Concurrency uint
// ParentCert, if provided will be prepended to the certificate chain
ParentCert *x509.Certificate
// ParentKey ()
ParentKey crypto.PrivateKey
}
// PeerCAConfig is for locating a CA certificate without a private key
@ -65,7 +80,30 @@ func (caS CASetupConfig) Status() TLSFilesStatus {
// Create generates and saves a CA using the config
func (caS CASetupConfig) Create(ctx context.Context) (*FullCertificateAuthority, error) {
ca, err := NewCA(ctx, uint16(caS.Difficulty), caS.Concurrency)
var (
err error
parent *FullCertificateAuthority
)
if caS.ParentCertPath != "" && caS.ParentKeyPath != "" {
parent, err = FullCAConfig{
CertPath: caS.ParentCertPath,
KeyPath: caS.ParentKeyPath,
}.Load()
}
if err != nil {
return nil, err
}
if parent == nil {
parent = &FullCertificateAuthority{}
}
ca, err := NewCA(ctx, NewCAOptions{
Difficulty: uint16(caS.Difficulty),
Concurrency: caS.Concurrency,
ParentCert: parent.Cert,
ParentKey: parent.Key,
})
if err != nil {
return nil, err
}
@ -93,19 +131,11 @@ func (fc FullCAConfig) Load() (*FullCertificateAuthority, error) {
return nil, errs.New("unable to parse EC private key: %v", err)
}
ec, ok := p.Cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, peertls.ErrUnsupportedKey.New("certificate public key type not supported: %T", k)
}
if !reflect.DeepEqual(k.PublicKey, *ec) {
return nil, errs.New("certificate public key and loaded")
}
return &FullCertificateAuthority{
Cert: p.Cert,
Key: k,
ID: p.ID,
RestChain: p.RestChain,
Cert: p.Cert,
Key: k,
ID: p.ID,
}, nil
}
@ -138,28 +168,29 @@ func (pc PeerCAConfig) Load() (*PeerCertificateAuthority, error) {
pc.CertPath, err)
}
i, err := idFromKey(c[len(c)-1].PublicKey)
i, err := idFromKey(c[0].PublicKey)
if err != nil {
return nil, err
}
return &PeerCertificateAuthority{
Cert: c[0],
ID: i,
RestChain: c[1:],
Cert: c[0],
ID: i,
}, nil
}
// NewCA creates a new full identity with the given difficulty
func NewCA(ctx context.Context, difficulty uint16, concurrency uint) (*FullCertificateAuthority, error) {
if concurrency < 1 {
concurrency = 1
func NewCA(ctx context.Context, opts NewCAOptions) (*FullCertificateAuthority, error) {
if opts.Concurrency < 1 {
opts.Concurrency = 1
}
ctx, cancel := context.WithCancel(ctx)
eC := make(chan error)
caC := make(chan FullCertificateAuthority, 1)
for i := 0; i < int(concurrency); i++ {
go newCAWorker(ctx, difficulty, caC, eC)
for i := 0; i < int(opts.Concurrency); i++ {
go newCAWorker(ctx, opts.Difficulty, opts.ParentCert, opts.ParentKey, caC, eC)
}
select {
@ -186,7 +217,9 @@ func (fc FullCAConfig) Save(ca *FullCertificateAuthority) error {
}
defer utils.LogClose(k)
if err = peertls.WriteChain(c, ca.Cert); err != nil {
chain := []*x509.Certificate{ca.Cert}
chain = append(chain, ca.RestChain...)
if err = peertls.WriteChain(c, chain...); err != nil {
return err
}
if err = peertls.WriteKey(k, ca.Key); err != nil {
@ -217,9 +250,18 @@ func (ca FullCertificateAuthority) NewIdentity() (*FullIdentity, error) {
}
return &FullIdentity{
CA: ca.Cert,
Leaf: l,
Key: k,
ID: ca.ID,
RestChain: ca.RestChain,
CA: ca.Cert,
Leaf: l,
Key: k,
ID: ca.ID,
}, nil
}
// NewTestCA returns a ca with a default difficulty and concurrency for use in tests
func NewTestCA(ctx context.Context) (*FullCertificateAuthority, error) {
return NewCA(ctx, NewCAOptions{
Difficulty: 12,
Concurrency: 4,
})
}

View File

@ -32,6 +32,7 @@ const (
// PeerIdentity represents another peer on the network.
type PeerIdentity struct {
RestChain []*x509.Certificate
// CA represents the peer's self-signed CA
CA *x509.Certificate
// Leaf represents the leaf they're currently using. The leaf should be
@ -44,6 +45,7 @@ type PeerIdentity struct {
// FullIdentity represents you on the network. In addition to a PeerIdentity,
// a FullIdentity also has a Key, which a PeerIdentity doesn't have.
type FullIdentity struct {
RestChain []*x509.Certificate
// CA represents the peer's self-signed CA. The ID is taken from this cert.
CA *x509.Certificate
// Leaf represents the leaf they're currently using. The leaf should be
@ -56,6 +58,9 @@ type FullIdentity struct {
// PeerCAWhitelist is a whitelist of CA certs which, if present, restricts which peers this identity will verify as valid;
// peer certs must be signed by a CA in this list to pass peer certificate verification.
PeerCAWhitelist []*x509.Certificate
// VerfyAuthExtSig if true, client leafs which handshake with this identity must contain a valid "authority signature extension"
// (NB: authority signature extensions are verified against certs in the `PeerCAWhitelist`; i.e. if true, a whitelist must be provided)
VerifyAuthExtSig bool
}
// IdentitySetupConfig allows you to run a set of Responsibilities with the given
@ -73,6 +78,7 @@ type IdentityConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/identity.cert"`
KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/identity.key"`
PeerCAWhitelistPath string `help:"path to the CA cert whitelist (peer identities must be signed by one these to be verified)"`
VerifyAuthExtSig bool `help:"if true, client leafs must contain a valid \"authority signature extension\" (NB: authority signature extensions are verified against certs in the peer ca whitelist; i.e. if true, a whitelist must be provided)" default:"false"`
Address string `help:"address to listen on" default:":7777"`
}
@ -121,6 +127,7 @@ func FullIdentityFromPEM(chainPEM, keyPEM, CAWhitelistPEM []byte) (*FullIdentity
}
return &FullIdentity{
RestChain: ch[2:],
CA: ch[1],
Leaf: ch[0],
Key: k,
@ -143,16 +150,17 @@ func ParseCertChain(chain [][]byte) ([]*x509.Certificate, error) {
}
// PeerIdentityFromCerts loads a PeerIdentity from a pair of leaf and ca x509 certificates
func PeerIdentityFromCerts(leaf, ca *x509.Certificate) (*PeerIdentity, error) {
func PeerIdentityFromCerts(leaf, ca *x509.Certificate, rest []*x509.Certificate) (*PeerIdentity, error) {
i, err := idFromKey(ca.PublicKey.(crypto.PublicKey))
if err != nil {
return nil, err
}
return &PeerIdentity{
CA: ca,
ID: i,
Leaf: leaf,
RestChain: rest,
CA: ca,
ID: i,
Leaf: leaf,
}, nil
}
@ -163,7 +171,7 @@ func PeerIdentityFromPeer(peer *peer.Peer) (*PeerIdentity, error) {
if len(c) < 2 {
return nil, Error.New("invalid certificate chain")
}
pi, err := PeerIdentityFromCerts(c[0], c[1])
pi, err := PeerIdentityFromCerts(c[0], c[1], c[2:])
if err != nil {
return nil, err
}
@ -237,7 +245,9 @@ func (ic IdentityConfig) Save(fi *FullIdentity) error {
}
defer utils.LogClose(k)
if err = peertls.WriteChain(c, fi.Leaf, fi.CA); err != nil {
chain := []*x509.Certificate{fi.Leaf, fi.CA}
chain = append(chain, fi.RestChain...)
if err = peertls.WriteChain(c, chain...); err != nil {
return err
}
if err = peertls.WriteKey(k, fi.Key); err != nil {
@ -247,9 +257,7 @@ func (ic IdentityConfig) Save(fi *FullIdentity) error {
}
// Run will run the given responsibilities with the configured identity.
func (ic IdentityConfig) Run(ctx context.Context, interceptor grpc.UnaryServerInterceptor,
responsibilities ...Responsibility) (
err error) {
func (ic IdentityConfig) Run(ctx context.Context, interceptor grpc.UnaryServerInterceptor, responsibilities ...Responsibility) (err error) {
defer mon.Task()(&ctx)(&err)
pi, err := ic.Load()
@ -273,10 +281,20 @@ func (ic IdentityConfig) Run(ctx context.Context, interceptor grpc.UnaryServerIn
return s.Run(ctx)
}
// RestChainRaw returns the rest (excluding leaf and CA) of the certficate chain as a 2d byte slice
func (fi *FullIdentity) RestChainRaw() [][]byte {
var chain [][]byte
for _, cert := range fi.RestChain {
chain = append(chain, cert.Raw)
}
return chain
}
// ServerOption returns a grpc `ServerOption` for incoming connections
// to the node with this full identity
func (fi *FullIdentity) ServerOption(pcvFuncs ...peertls.PeerCertVerificationFunc) (grpc.ServerOption, error) {
ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw}
ch = append(ch, fi.RestChainRaw()...)
c, err := peertls.TLSCert(ch, fi.Leaf, fi.Key)
if err != nil {
return nil, err
@ -303,6 +321,7 @@ func (fi *FullIdentity) ServerOption(pcvFuncs ...peertls.PeerCertVerificationFun
func (fi *FullIdentity) DialOption() (grpc.DialOption, error) {
// TODO(coyle): add ID
ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw}
ch = append(ch, fi.RestChainRaw()...)
c, err := peertls.TLSCert(ch, fi.Leaf, fi.Key)
if err != nil {
return nil, err

View File

@ -43,7 +43,7 @@ func TestPeerIdentityFromCertChain(t *testing.T) {
l, err := peertls.NewCert(lT, caT, &lp.PublicKey, k)
assert.NoError(t, err)
pi, err := PeerIdentityFromCerts(l, c)
pi, err := PeerIdentityFromCerts(l, c, nil)
assert.NoError(t, err)
assert.Equal(t, c, pi.CA)
assert.Equal(t, l, pi.Leaf)
@ -246,7 +246,7 @@ func TestVerifyPeer(t *testing.T) {
}
}
ca, err := NewCA(context.Background(), 12, 4)
ca, err := NewTestCA(context.Background())
check(err)
fi, err := ca.NewIdentity()
check(err)

View File

@ -45,7 +45,10 @@ type Provider struct {
func NewProvider(identity *FullIdentity, lis net.Listener, interceptor grpc.UnaryServerInterceptor,
responsibilities ...Responsibility) (*Provider, error) {
// NB: talk to anyone with an identity
ident, err := identity.ServerOption(peertls.VerifyCAWhitelist(identity.PeerCAWhitelist))
ident, err := identity.ServerOption(peertls.VerifyCAWhitelist(
identity.PeerCAWhitelist,
identity.VerifyAuthExtSig,
))
if err != nil {
return nil, err
}

View File

@ -7,7 +7,10 @@ import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"os"
@ -56,7 +59,7 @@ func decodePEM(PEMBytes []byte) ([][]byte, error) {
return DERBytes, nil
}
func newCAWorker(ctx context.Context, difficulty uint16, caC chan FullCertificateAuthority, eC chan error) {
func newCAWorker(ctx context.Context, difficulty uint16, parentCert *x509.Certificate, parentKey crypto.PrivateKey, caC chan FullCertificateAuthority, eC chan error) {
var (
k crypto.PrivateKey
i nodeID
@ -96,12 +99,7 @@ func newCAWorker(ctx context.Context, difficulty uint16, caC chan FullCertificat
return
}
p, ok := k.(*ecdsa.PrivateKey)
if !ok {
eC <- peertls.ErrUnsupportedKey.New("%T", k)
return
}
c, err := peertls.NewCert(ct, nil, &p.PublicKey, k)
c, err := newCACert(k, parentKey, ct, parentCert)
if err != nil {
eC <- err
return
@ -112,9 +110,59 @@ func newCAWorker(ctx context.Context, difficulty uint16, caC chan FullCertificat
Key: k,
ID: i,
}
if parentCert != nil {
ca.RestChain = []*x509.Certificate{parentCert}
}
caC <- ca
}
func newCACert(key, parentKey crypto.PrivateKey, template, parentCert *x509.Certificate) (*x509.Certificate, error) {
p, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, peertls.ErrUnsupportedKey.New("%T", key)
}
var signingKey crypto.PrivateKey
if parentKey != nil {
signingKey = parentKey
} else {
signingKey = key
}
cert, err := peertls.NewCert(template, parentCert, &p.PublicKey, signingKey)
if err != nil {
return nil, err
}
if parentKey != nil {
p, ok := parentKey.(*ecdsa.PrivateKey)
if !ok {
return nil, peertls.ErrUnsupportedKey.New("%T", key)
}
hash := crypto.SHA256.New()
_, err := hash.Write(cert.RawTBSCertificate)
if err != nil {
return nil, peertls.ErrSign.Wrap(err)
}
r, s, err := ecdsa.Sign(rand.Reader, p, hash.Sum(nil))
if err != nil {
return nil, peertls.ErrSign.Wrap(err)
}
signature, err := asn1.Marshal(peertls.ECDSASignature{R: r, S: s})
if err != nil {
return nil, peertls.ErrSign.Wrap(err)
}
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
Id: peertls.AuthoritySignatureExtID,
Value: signature,
})
}
return cert, nil
}
func idFromKey(k crypto.PublicKey) (nodeID, error) {
kb, err := x509.MarshalPKIXPublicKey(k)
if err != nil {

View File

@ -15,7 +15,7 @@ import (
var ctx = context.Background()
func TestDialNode(t *testing.T) {
ca, err := provider.NewCA(ctx, 12, 4)
ca, err := provider.NewTestCA(ctx)
assert.NoError(t, err)
identity, err := ca.NewIdentity()
assert.NoError(t, err)

75
scripts/cert-gating.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/bash
basepath=$HOME/.storj/capt
alpha_config=$basepath/config-alpha.yaml
unauthorized_config=$basepath/config-unauthorized.yaml
ca_whitelist=$basepath/ca-alpha-whitelist.cert
ca_count=5
ca_basepath=$basepath/ca-alpha-
ca_i_basepath() {
echo "$ca_basepath$1"
}
rand_ca_basepath() {
let i="($RANDOM % $ca_count) + 1"
echo $(ca_i_basepath $i)
}
case $1 in
--help)
echo "usage: $(basename $0) [setup|alpha|unauthorized]"
;;
setup)
echo "setting up captplanet"
captplanet setup --overwrite
echo "clearing whitelist"
echo > $ca_whitelist
echo -n "generating alpha certificate authorities.."
for i in $(seq 1 $ca_count); do
echo -n "$i.."
_basepath=$(ca_i_basepath $i)
identity ca new --ca.overwrite \
--ca.cert-path $_basepath.cert \
--ca.key-path $_basepath.key
cat $_basepath.cert >> $ca_whitelist
done
echo "done"
echo -n "generating alpha identities"
for dir in $basepath/{f*,sat*,up*}; do
echo -n "."
_ca_basepath=$(rand_ca_basepath)
_ca_cert=$dir/ca-alpha.cert
_ca_key=$dir/ca-alpha.key
identity ca new --ca.overwrite \
--ca.cert-path $_ca_cert \
--ca.key-path $_ca_key \
--ca.parent-cert-path $_ca_basepath.cert \
--ca.parent-key-path $_ca_basepath.key
identity id new --identity.overwrite \
--identity.cert-path $dir/identity-alpha.cert \
--identity.key-path $dir/identity-alpha.key \
--ca.cert-path $_ca_cert \
--ca.key-path $_ca_key
done
echo "done"
echo "writing alpha config"
cat $basepath/config.yaml | \
sed "s,peer-ca-whitelist-path: \"\",peer-ca-whitelist-path: $ca_whitelist,g" | \
sed -E 's,cert-path: (.+)\.cert,cert-path: \1-alpha.cert,g' | \
sed -E 's,key-path: (.+)\.key,key-path: \1-alpha.key,g' \
> $alpha_config
echo "writing unauthorized config"
cat $basepath/config.yaml | sed -E "s,peer-ca-whitelist-path: \"\",peer-ca-whitelist-path: $ca_whitelist,g" > "$unauthorized_config"
;;
alpha)
captplanet run --config $alpha_config
;;
unauthorized)
captplanet run --config $unauthorized_config
;;
run)
captplanet run
;;
*)
$0 --help
;;
esac