Wait-list gating (#534)

This commit is contained in:
Bryan White 2018-10-26 15:52:37 +02:00 committed by Dennis Coyle
parent fdfb5f1da9
commit df1f7a6214
6 changed files with 166 additions and 34 deletions

View File

@ -36,8 +36,15 @@ var (
ErrTLSTemplate = errs.Class("tls template error") ErrTLSTemplate = errs.Class("tls template error")
// ErrVerifyPeerCert is used when an error occurs during `VerifyPeerCertificate` // ErrVerifyPeerCert is used when an error occurs during `VerifyPeerCertificate`
ErrVerifyPeerCert = errs.Class("tls peer certificate verification error") ErrVerifyPeerCert = errs.Class("tls peer certificate verification error")
// ErrParseCerts is used when an error occurs while parsing a certificate or cert chain
ErrParseCerts = errs.Class("unable to parse certificate")
// ErrVerifySignature is used when a cert-chain signature verificaion error occurs // ErrVerifySignature is used when a cert-chain signature verificaion error occurs
ErrVerifySignature = errs.Class("tls certificate signature verification error") ErrVerifySignature = errs.Class("tls certificate signature verification error")
// ErrVerifyCertificateChain is used when a certificate chain can't be verified from leaf to root
// (i.e.: each cert in the chain should be signed by the preceding cert and the root should be self-signed)
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")
) )
// PeerCertVerificationFunc is the signature for a `*tls.Config{}`'s // PeerCertVerificationFunc is the signature for a `*tls.Config{}`'s
@ -90,13 +97,13 @@ func VerifyPeerFunc(next ...PeerCertVerificationFunc) PeerCertVerificationFunc {
return func(chain [][]byte, _ [][]*x509.Certificate) error { return func(chain [][]byte, _ [][]*x509.Certificate) error {
c, err := parseCertificateChains(chain) c, err := parseCertificateChains(chain)
if err != nil { if err != nil {
return err return ErrVerifyPeerCert.Wrap(err)
} }
for _, n := range next { for _, n := range next {
if n != nil { if n != nil {
if err := n(chain, [][]*x509.Certificate{c}); err != nil { if err := n(chain, [][]*x509.Certificate{c}); err != nil {
return err return ErrVerifyPeerCert.Wrap(err)
} }
} }
} }
@ -110,6 +117,25 @@ func VerifyPeerCertChains(_ [][]byte, parsedChains [][]*x509.Certificate) error
return verifyChainSignatures(parsedChains[0]) 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 {
if cas == nil {
return nil
}
return func(_ [][]byte, parsedChains [][]*x509.Certificate) error {
var err error
for _, ca := range cas {
err = verifyCertSignature(ca, parsedChains[0][0])
if err == nil {
return nil
}
}
return ErrVerifyCAWhitelist.Wrap(err)
}
}
// NewKeyBlock converts an ASN1/DER-encoded byte-slice of a private key into // NewKeyBlock converts an ASN1/DER-encoded byte-slice of a private key into
// a `pem.Block` pointer // a `pem.Block` pointer
func NewKeyBlock(b []byte) *pem.Block { func NewKeyBlock(b []byte) *pem.Block {

View File

@ -124,4 +124,65 @@ func TestVerifyPeerCertChains(t *testing.T) {
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{l.Raw, c.Raw}, nil) err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err) assert.NoError(t, err)
c, err = NewCert(ct, nil, &cp.PublicKey, k)
assert.NoError(t, err)
k2, err := NewKey()
assert.NoError(t, err)
l, err = NewCert(lt, nil, &lp.PublicKey, k2)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyPeerCertChains)([][]byte{l.Raw, c.Raw}, nil)
assert.True(t, ErrVerifyPeerCert.Has(err))
assert.True(t, ErrVerifyCertificateChain.Has(err))
}
func TestVerifyCAWhitelist(t *testing.T) {
k, err := NewKey()
assert.NoError(t, err)
ct, err := CATemplate()
assert.NoError(t, err)
cp, ok := k.(*ecdsa.PrivateKey)
assert.True(t, ok)
c, err := NewCert(ct, nil, &cp.PublicKey, k)
assert.NoError(t, err)
lt, err := LeafTemplate()
assert.NoError(t, err)
lp, ok := k.(*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)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c}))([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err)
zk, err := NewKey()
assert.NoError(t, err)
zt, err := CATemplate()
assert.NoError(t, err)
zp, ok := zk.(*ecdsa.PrivateKey)
assert.True(t, ok)
z, err := NewCert(zt, nil, &zp.PublicKey, zk)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{z}))([][]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)
assert.NoError(t, err)
err = VerifyPeerFunc(VerifyCAWhitelist([]*x509.Certificate{c, z}))([][]byte{l.Raw, c.Raw}, nil)
assert.NoError(t, err)
} }

View File

@ -44,7 +44,7 @@ func parseCerts(rawCerts [][]byte) ([]*x509.Certificate, error) {
var err error var err error
certs[i], err = x509.ParseCertificate(c) certs[i], err = x509.ParseCertificate(c)
if err != nil { if err != nil {
return nil, ErrVerifyPeerCert.New("unable to parse certificate: %v", err) return nil, ErrParseCerts.New("unable to parse certificate at index %d", i)
} }
} }
return certs, nil return certs, nil
@ -54,53 +54,48 @@ func verifyChainSignatures(certs []*x509.Certificate) error {
for i, cert := range certs { for i, cert := range certs {
j := len(certs) j := len(certs)
if i+1 < j { if i+1 < j {
isValid, err := verifyCertSignature(certs[i+1], cert) err := verifyCertSignature(certs[i+1], cert)
if err != nil { if err != nil {
return ErrVerifyPeerCert.Wrap(err) return ErrVerifyCertificateChain.Wrap(err)
}
if !isValid {
return ErrVerifyPeerCert.New("certificate chain signature verification failed")
} }
continue continue
} }
rootIsValid, err := verifyCertSignature(cert, cert) err := verifyCertSignature(cert, cert)
if err != nil { if err != nil {
return ErrVerifyPeerCert.Wrap(err) return ErrVerifyCertificateChain.Wrap(err)
} }
if !rootIsValid {
return ErrVerifyPeerCert.New("certificate chain signature verification failed")
}
} }
return nil return nil
} }
func verifyCertSignature(parentCert, childCert *x509.Certificate) (bool, error) { func verifyCertSignature(parentCert, childCert *x509.Certificate) error {
pubKey, ok := parentCert.PublicKey.(*ecdsa.PublicKey) pubKey, ok := parentCert.PublicKey.(*ecdsa.PublicKey)
if !ok { if !ok {
return false, ErrUnsupportedKey.New("%T", parentCert.PublicKey) return ErrUnsupportedKey.New("%T", parentCert.PublicKey)
} }
signature := new(ecdsaSignature) signature := new(ecdsaSignature)
if _, err := asn1.Unmarshal(childCert.Signature, signature); err != nil { if _, err := asn1.Unmarshal(childCert.Signature, signature); err != nil {
return false, ErrVerifySignature.New("unable to unmarshal ecdsa signature: %v", err) return ErrVerifySignature.New("unable to unmarshal ecdsa signature: %v", err)
} }
h := crypto.SHA256.New() h := crypto.SHA256.New()
_, err := h.Write(childCert.RawTBSCertificate) _, err := h.Write(childCert.RawTBSCertificate)
if err != nil { if err != nil {
return false, err return ErrVerifySignature.Wrap(err)
} }
digest := h.Sum(nil) digest := h.Sum(nil)
isValid := ecdsa.Verify(pubKey, digest, signature.R, signature.S) if !ecdsa.Verify(pubKey, digest, signature.R, signature.S) {
return ErrVerifySignature.New("signature is not valid")
}
return isValid, nil return nil
} }
func newSerialNumber() (*big.Int, error) { func newSerialNumber() (*big.Int, error) {

View File

@ -53,6 +53,9 @@ type FullIdentity struct {
ID nodeID ID nodeID
// Key is the key this identity uses with the leaf for communication. // Key is the key this identity uses with the leaf for communication.
Key crypto.PrivateKey Key crypto.PrivateKey
// 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
} }
// IdentitySetupConfig allows you to run a set of Responsibilities with the given // IdentitySetupConfig allows you to run a set of Responsibilities with the given
@ -67,14 +70,15 @@ type IdentitySetupConfig struct {
// IdentityConfig allows you to run a set of Responsibilities with the given // IdentityConfig allows you to run a set of Responsibilities with the given
// identity. You can also just load an Identity from disk. // identity. You can also just load an Identity from disk.
type IdentityConfig struct { type IdentityConfig struct {
CertPath string `help:"path to the certificate chain for this identity" default:"$CONFDIR/identity.cert"` 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"` KeyPath string `help:"path to the private key for this identity" default:"$CONFDIR/identity.key"`
Address string `help:"address to listen on" default:":7777"` PeerCAWhitelistPath string `help:"path to the CA cert whitelist (peer identities must be signed by one these to be verified)"`
Address string `help:"address to listen on" default:":7777"`
} }
// FullIdentityFromPEM loads a FullIdentity from a certificate chain and // FullIdentityFromPEM loads a FullIdentity from a certificate chain and
// private key file // private key file
func FullIdentityFromPEM(chainPEM, keyPEM []byte) (*FullIdentity, error) { func FullIdentityFromPEM(chainPEM, keyPEM, CAWhitelistPEM []byte) (*FullIdentity, error) {
cb, err := decodePEM(chainPEM) cb, err := decodePEM(chainPEM)
if err != nil { if err != nil {
return nil, errs.Wrap(err) return nil, errs.Wrap(err)
@ -101,11 +105,27 @@ func FullIdentityFromPEM(chainPEM, keyPEM []byte) (*FullIdentity, error) {
return nil, err return nil, err
} }
var (
wb [][]byte
whitelist []*x509.Certificate
)
if CAWhitelistPEM != nil {
wb, err = decodePEM(CAWhitelistPEM)
if err != nil {
return nil, errs.Wrap(err)
}
whitelist, err = ParseCertChain(wb)
if err != nil {
return nil, errs.Wrap(err)
}
}
return &FullIdentity{ return &FullIdentity{
CA: ch[1], CA: ch[1],
Leaf: ch[0], Leaf: ch[0],
Key: k, Key: k,
ID: i, ID: i,
PeerCAWhitelist: whitelist,
}, nil }, nil
} }
@ -190,8 +210,12 @@ func (ic IdentityConfig) Load() (*FullIdentity, error) {
if err != nil { if err != nil {
return nil, peertls.ErrNotExist.Wrap(err) return nil, peertls.ErrNotExist.Wrap(err)
} }
w, err := ioutil.ReadFile(ic.PeerCAWhitelistPath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
fi, err := FullIdentityFromPEM(c, k) fi, err := FullIdentityFromPEM(c, k, w)
if err != nil { if err != nil {
return nil, errs.New("failed to load identity %#v, %#v: %v", return nil, errs.New("failed to load identity %#v, %#v: %v",
ic.CertPath, ic.KeyPath, err) ic.CertPath, ic.KeyPath, err)
@ -251,19 +275,23 @@ func (ic IdentityConfig) Run(ctx context.Context, interceptor grpc.UnaryServerIn
// ServerOption returns a grpc `ServerOption` for incoming connections // ServerOption returns a grpc `ServerOption` for incoming connections
// to the node with this full identity // to the node with this full identity
func (fi *FullIdentity) ServerOption() (grpc.ServerOption, error) { func (fi *FullIdentity) ServerOption(pcvFuncs ...peertls.PeerCertVerificationFunc) (grpc.ServerOption, error) {
ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw} ch := [][]byte{fi.Leaf.Raw, fi.CA.Raw}
c, err := peertls.TLSCert(ch, fi.Leaf, fi.Key) c, err := peertls.TLSCert(ch, fi.Leaf, fi.Key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pcvFuncs = append(
[]peertls.PeerCertVerificationFunc{peertls.VerifyPeerCertChains},
pcvFuncs...,
)
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*c}, Certificates: []tls.Certificate{*c},
InsecureSkipVerify: true, InsecureSkipVerify: true,
ClientAuth: tls.RequireAnyClientCert, ClientAuth: tls.RequireAnyClientCert,
VerifyPeerCertificate: peertls.VerifyPeerFunc( VerifyPeerCertificate: peertls.VerifyPeerFunc(
peertls.VerifyPeerCertChains, pcvFuncs...,
), ),
} }

View File

@ -9,8 +9,10 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
@ -87,7 +89,7 @@ func TestFullIdentityFromPEM(t *testing.T) {
keyPEM := bytes.NewBuffer([]byte{}) keyPEM := bytes.NewBuffer([]byte{})
assert.NoError(t, pem.Encode(keyPEM, peertls.NewKeyBlock(lkB))) assert.NoError(t, pem.Encode(keyPEM, peertls.NewKeyBlock(lkB)))
fi, err := FullIdentityFromPEM(chainPEM.Bytes(), keyPEM.Bytes()) fi, err := FullIdentityFromPEM(chainPEM.Bytes(), keyPEM.Bytes(), nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, l.Raw, fi.Leaf.Raw) assert.Equal(t, l.Raw, fi.Leaf.Raw)
assert.Equal(t, c.Raw, fi.CA.Raw) assert.Equal(t, c.Raw, fi.CA.Raw)
@ -183,7 +185,7 @@ AwEHoUQDQgAEoLy/0hs5deTXZunRumsMkiHpF0g8wAc58aXANmr7Mxx9tzoIYFnx
ic, cleanup, err := tempIdentityConfig() ic, cleanup, err := tempIdentityConfig()
assert.NoError(t, err) assert.NoError(t, err)
fi, err := FullIdentityFromPEM([]byte(chain), []byte(key)) fi, err := FullIdentityFromPEM([]byte(chain), []byte(key), nil)
assert.NoError(t, err) assert.NoError(t, err)
return cleanup, ic, fi, difficulty return cleanup, ic, fi, difficulty
@ -198,6 +200,7 @@ func TestIdentityConfig_LoadIdentity(t *testing.T) {
fi, err := ic.Load() fi, err := ic.Load()
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, fi.PeerCAWhitelist)
assert.NotEmpty(t, fi) assert.NotEmpty(t, fi)
assert.NotEmpty(t, fi.Key) assert.NotEmpty(t, fi.Key)
assert.NotEmpty(t, fi.Leaf) assert.NotEmpty(t, fi.Leaf)
@ -208,6 +211,24 @@ func TestIdentityConfig_LoadIdentity(t *testing.T) {
assert.Equal(t, expectedFI.Leaf, fi.Leaf) assert.Equal(t, expectedFI.Leaf, fi.Leaf)
assert.Equal(t, expectedFI.CA, fi.CA) assert.Equal(t, expectedFI.CA, fi.CA)
assert.Equal(t, expectedFI.ID.Bytes(), fi.ID.Bytes()) assert.Equal(t, expectedFI.ID.Bytes(), fi.ID.Bytes())
tmp := path.Join(os.TempDir(), "temp-ca-whitelist.pem")
w, err := os.Create(tmp)
assert.NoError(t, err)
defer func() {
err := os.RemoveAll(tmp)
if err != nil {
fmt.Printf("unable to cleanup temp ca whitelist at \"%s\": %s", tmp, err.Error())
}
}()
err = peertls.WriteChain(w, fi.CA)
assert.NoError(t, err)
ic.PeerCAWhitelistPath = tmp
fi, err = ic.Load()
assert.NoError(t, err)
assert.NotEmpty(t, fi.PeerCAWhitelist)
} }
func TestNodeID_Difficulty(t *testing.T) { func TestNodeID_Difficulty(t *testing.T) {

View File

@ -15,6 +15,7 @@ import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"storj.io/storj/pkg/peertls"
"storj.io/storj/storage" "storj.io/storj/storage"
) )
@ -44,7 +45,7 @@ type Provider struct {
func NewProvider(identity *FullIdentity, lis net.Listener, interceptor grpc.UnaryServerInterceptor, func NewProvider(identity *FullIdentity, lis net.Listener, interceptor grpc.UnaryServerInterceptor,
responsibilities ...Responsibility) (*Provider, error) { responsibilities ...Responsibility) (*Provider, error) {
// NB: talk to anyone with an identity // NB: talk to anyone with an identity
ident, err := identity.ServerOption() ident, err := identity.ServerOption(peertls.VerifyCAWhitelist(identity.PeerCAWhitelist))
if err != nil { if err != nil {
return nil, err return nil, err
} }