413 lines
13 KiB
Go
413 lines
13 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package etcdraft
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/hyperledger/fabric-protos-go/common"
|
|
etcdraftproto "github.com/hyperledger/fabric-protos-go/orderer/etcdraft"
|
|
"github.com/hyperledger/fabric/bccsp/sw"
|
|
"github.com/hyperledger/fabric/common/crypto/tlsgen"
|
|
"github.com/hyperledger/fabric/common/flogging"
|
|
"github.com/hyperledger/fabric/orderer/common/cluster"
|
|
"github.com/hyperledger/fabric/protoutil"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIsConsenterOfChannel(t *testing.T) {
|
|
certInsideConfigBlock, err := base64.StdEncoding.DecodeString("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNmekNDQWlhZ0F3SUJBZ0l" +
|
|
"SQUo4bjFLYTVzS1ZaTXRMTHJ1dldERDB3Q2dZSUtvWkl6ajBFQXdJd2JERUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR" +
|
|
"2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMlY0WVcxd2JHVXVZMjl0TVJvd0dBWUR" +
|
|
"WUVFERXhGMGJITmpZUzVsCmVHRnRjR3hsTG1OdmJUQWVGdzB4T0RFeE1EWXdPVFE1TURCYUZ3MHlPREV4TURNd09UUTVNREJhTUZreEN6QU" +
|
|
"oKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SWXdGQVlEVlFRSEV3MVRZVzRnUm5KaApibU5wYzJOdk1SMHdH" +
|
|
"d1lEVlFRREV4UnZjbVJsY21WeU1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkRUVlFZc0" +
|
|
"ZKZWxUcFZDMDFsek5DSkx6OENRMFFGVDBvN1BmSnBwSkl2SXgKUCtRVjQvRGRCSnRqQ0cvcGsvMGFxZXRpSjhZRUFMYmMrOUhmWnExN2tJ" +
|
|
"Q2pnYnN3Z2Jnd0RnWURWUjBQQVFILwpCQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOV" +
|
|
"khSTUJBZjhFCkFqQUFNQ3NHQTFVZEl3UWtNQ0tBSUVBOHFrSVJRTVBuWkxBR2g0TXZla2gzZFpHTmNxcEhZZWlXdzE3Rmw0ZlMKTUV3R0" +
|
|
"ExVWRFUVJGTUVPQ0ZHOXlaR1Z5WlhJeExtVjRZVzF3YkdVdVkyOXRnZ2h2Y21SbGNtVnlNWUlKYkc5agpZV3hvYjNOMGh3Ui9BQUFCaHh" +
|
|
"BQUFBQUFBQUFBQUFBQUFBQUFBQUFCTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDCklFckJZRFVzV0JwOHB0ZVFSaTZyNjNVelhJQi81Sn" +
|
|
"YxK0RlTkRIUHc3aDljQWlCakYrM3V5TzBvMEdRclB4MEUKUWptYlI5T3BVREN2LzlEUkNXWU9GZitkVlE9PQotLS0tLUVORCBDRVJUSU" +
|
|
"ZJQ0FURS0tLS0tCg==")
|
|
require.NoError(t, err)
|
|
|
|
ca, err := tlsgen.NewCA()
|
|
require.NoError(t, err)
|
|
|
|
kp, err := ca.NewClientCertKeyPair()
|
|
require.NoError(t, err)
|
|
|
|
validBlock := func() *common.Block {
|
|
b, err := ioutil.ReadFile(filepath.Join("testdata", "etcdraftgenesis.block"))
|
|
require.NoError(t, err)
|
|
block := &common.Block{}
|
|
err = proto.Unmarshal(b, block)
|
|
require.NoError(t, err)
|
|
return block
|
|
}
|
|
for _, testCase := range []struct {
|
|
name string
|
|
expectedError string
|
|
configBlock *common.Block
|
|
certificate []byte
|
|
}{
|
|
{
|
|
name: "nil block",
|
|
expectedError: "nil block or nil header",
|
|
},
|
|
{
|
|
name: "nil header",
|
|
expectedError: "nil block or nil header",
|
|
configBlock: &common.Block{},
|
|
},
|
|
{
|
|
name: "no block data",
|
|
expectedError: "block data is nil",
|
|
configBlock: &common.Block{Header: &common.BlockHeader{}},
|
|
},
|
|
{
|
|
name: "invalid envelope inside block",
|
|
expectedError: "failed to unmarshal payload from envelope:",
|
|
configBlock: &common.Block{
|
|
Header: &common.BlockHeader{},
|
|
Data: &common.BlockData{
|
|
Data: [][]byte{protoutil.MarshalOrPanic(&common.Envelope{
|
|
Payload: []byte{1, 2, 3},
|
|
})},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "valid config block with cert mismatch",
|
|
configBlock: validBlock(),
|
|
certificate: kp.Cert,
|
|
expectedError: cluster.ErrNotInChannel.Error(),
|
|
},
|
|
{
|
|
name: "valid config block with matching cert",
|
|
configBlock: validBlock(),
|
|
certificate: certInsideConfigBlock,
|
|
},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(sw.NewDummyKeyStore())
|
|
require.NoError(t, err)
|
|
|
|
consenterCertificate := &ConsenterCertificate{
|
|
Logger: flogging.MustGetLogger("test"),
|
|
ConsenterCertificate: testCase.certificate,
|
|
CryptoProvider: cryptoProvider,
|
|
}
|
|
err = consenterCertificate.IsConsenterOfChannel(testCase.configBlock)
|
|
if testCase.expectedError != "" {
|
|
require.ErrorContains(t, err, testCase.expectedError)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifyConfigMetadata(t *testing.T) {
|
|
tlsCA, err := tlsgen.NewCA()
|
|
require.NoError(t, err, "failed to create CA")
|
|
|
|
caRootCert, err := parseCertificateFromBytes(tlsCA.CertBytes())
|
|
require.NoError(t, err, "failed to parse CA certificate")
|
|
|
|
serverPair, err := tlsCA.NewServerCertKeyPair("localhost")
|
|
require.NoError(t, err, "failed to create server key pair")
|
|
|
|
clientPair, err := tlsCA.NewClientCertKeyPair()
|
|
require.NoError(t, err, "failed to create client key pair")
|
|
|
|
unknownTlsCA, err := tlsgen.NewCA()
|
|
require.NoError(t, err, "failed to create unknown CA")
|
|
|
|
unknownServerPair, err := unknownTlsCA.NewServerCertKeyPair("unknownhost")
|
|
require.NoError(t, err, "failed to create unknown server key pair")
|
|
|
|
unknownServerCert, err := parseCertificateFromBytes(unknownServerPair.Cert)
|
|
require.NoError(t, err, "failed to parse unknown server certificate")
|
|
|
|
unknownClientPair, err := unknownTlsCA.NewClientCertKeyPair()
|
|
require.NoError(t, err, "failed to create unknown client key pair")
|
|
|
|
unknownClientCert, err := parseCertificateFromBytes(unknownClientPair.Cert)
|
|
require.NoError(t, err, "failed to parse unknown client certificate")
|
|
|
|
validOptions := &etcdraftproto.Options{
|
|
TickInterval: "500ms",
|
|
ElectionTick: 10,
|
|
HeartbeatTick: 1,
|
|
MaxInflightBlocks: 5,
|
|
SnapshotIntervalSize: 20 * 1024 * 1024, // 20 MB
|
|
}
|
|
singleConsenter := &etcdraftproto.Consenter{
|
|
Host: "host1",
|
|
Port: 10001,
|
|
ClientTlsCert: clientPair.Cert,
|
|
ServerTlsCert: serverPair.Cert,
|
|
}
|
|
|
|
rootCertPool := x509.NewCertPool()
|
|
rootCertPool.AddCert(caRootCert)
|
|
goodVerifyingOpts := x509.VerifyOptions{
|
|
Roots: rootCertPool,
|
|
KeyUsages: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageClientAuth,
|
|
x509.ExtKeyUsageServerAuth,
|
|
},
|
|
}
|
|
|
|
// valid metadata should give nil error
|
|
goodMetadata := &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
singleConsenter,
|
|
},
|
|
}
|
|
assert.Nil(t, VerifyConfigMetadata(goodMetadata, goodVerifyingOpts))
|
|
|
|
// test variety of bad metadata
|
|
for _, testCase := range []struct {
|
|
description string
|
|
metadata *etcdraftproto.ConfigMetadata
|
|
verifyOpts x509.VerifyOptions
|
|
errRegex string
|
|
}{
|
|
{
|
|
description: "nil metadata",
|
|
metadata: nil,
|
|
errRegex: "nil Raft config metadata",
|
|
verifyOpts: goodVerifyingOpts,
|
|
},
|
|
{
|
|
description: "nil options",
|
|
metadata: &etcdraftproto.ConfigMetadata{},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "nil Raft config metadata options",
|
|
},
|
|
{
|
|
description: "HeartbeatTick is 0",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
HeartbeatTick: 0,
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "none of HeartbeatTick .* can be zero",
|
|
},
|
|
{
|
|
description: "ElectionTick is 0",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
HeartbeatTick: validOptions.HeartbeatTick,
|
|
ElectionTick: 0,
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "none of .* ElectionTick .* can be zero",
|
|
},
|
|
{
|
|
description: "MaxInflightBlocks is 0",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
HeartbeatTick: validOptions.HeartbeatTick,
|
|
ElectionTick: validOptions.ElectionTick,
|
|
MaxInflightBlocks: 0,
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "none of .* MaxInflightBlocks .* can be zero",
|
|
},
|
|
{
|
|
description: "ElectionTick is less than HeartbeatTick",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
HeartbeatTick: 10,
|
|
ElectionTick: 1,
|
|
MaxInflightBlocks: validOptions.MaxInflightBlocks,
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "ElectionTick .* must be greater than HeartbeatTick",
|
|
},
|
|
{
|
|
description: "TickInterval is not parsable",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
HeartbeatTick: validOptions.HeartbeatTick,
|
|
ElectionTick: validOptions.ElectionTick,
|
|
MaxInflightBlocks: validOptions.MaxInflightBlocks,
|
|
TickInterval: "abcd",
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "failed to parse TickInterval .* to time duration",
|
|
},
|
|
{
|
|
description: "TickInterval is 0",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
HeartbeatTick: validOptions.HeartbeatTick,
|
|
ElectionTick: validOptions.ElectionTick,
|
|
MaxInflightBlocks: validOptions.MaxInflightBlocks,
|
|
TickInterval: "0s",
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "TickInterval cannot be zero",
|
|
},
|
|
{
|
|
description: "consenter set is empty",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "empty consenter set",
|
|
},
|
|
{
|
|
description: "metadata has nil consenter",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
nil,
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "metadata has nil consenter",
|
|
},
|
|
{
|
|
description: "consenter has invalid server cert",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
{
|
|
ServerTlsCert: []byte("invalid"),
|
|
ClientTlsCert: clientPair.Cert,
|
|
},
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "no PEM data found in cert",
|
|
},
|
|
{
|
|
description: "consenter has invalid client cert",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
{
|
|
ServerTlsCert: serverPair.Cert,
|
|
ClientTlsCert: []byte("invalid"),
|
|
},
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "no PEM data found in cert",
|
|
},
|
|
{
|
|
description: "metadata has duplicate consenters",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
singleConsenter,
|
|
singleConsenter,
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: "duplicate consenter",
|
|
},
|
|
{
|
|
description: "consenter has client cert signed by unknown authority",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
{
|
|
ClientTlsCert: unknownClientPair.Cert,
|
|
ServerTlsCert: serverPair.Cert,
|
|
},
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: fmt.Sprintf("verifying tls client cert with serial number %d: x509: certificate signed by unknown authority", unknownClientCert.SerialNumber),
|
|
},
|
|
{
|
|
description: "consenter has server cert signed by unknown authority",
|
|
metadata: &etcdraftproto.ConfigMetadata{
|
|
Options: validOptions,
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
{
|
|
ServerTlsCert: unknownServerPair.Cert,
|
|
ClientTlsCert: clientPair.Cert,
|
|
},
|
|
},
|
|
},
|
|
verifyOpts: goodVerifyingOpts,
|
|
errRegex: fmt.Sprintf("verifying tls server cert with serial number %d: x509: certificate signed by unknown authority", unknownServerCert.SerialNumber),
|
|
},
|
|
} {
|
|
t.Run(testCase.description, func(t *testing.T) {
|
|
err := VerifyConfigMetadata(testCase.metadata, testCase.verifyOpts)
|
|
require.NotNil(t, err, testCase.description)
|
|
require.Regexp(t, testCase.errRegex, err)
|
|
})
|
|
}
|
|
|
|
t.Run("ExpiredCertificate", func(t *testing.T) {
|
|
clientPair, err := tlsCA.NewClientCertKeyPair()
|
|
require.NoError(t, err, "failed to create client key pair")
|
|
|
|
clientCert := clientPair.TLSCert
|
|
clientCert.NotAfter = time.Now().Add(-24 * time.Hour)
|
|
clientCertBytes, err := x509.CreateCertificate(rand.Reader, clientCert, caRootCert, clientPair.Signer.Public(), tlsCA.Signer())
|
|
require.NoError(t, err, "failed to create expired certificate")
|
|
|
|
clientCert, err = x509.ParseCertificate(clientCertBytes)
|
|
require.NoError(t, err, "failed to parse expired certificate")
|
|
|
|
_, err = clientCert.Verify(goodVerifyingOpts)
|
|
require.Error(t, err, "expected certificate verification to fail")
|
|
cie, ok := err.(x509.CertificateInvalidError)
|
|
require.True(t, ok, "expected an x509.CertificateInvalidError but got %T", err)
|
|
require.Equal(t, x509.Expired, cie.Reason)
|
|
|
|
consenterWithExpiredCerts := &etcdraftproto.Consenter{
|
|
Host: "host1",
|
|
Port: 10001,
|
|
ClientTlsCert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: clientCertBytes}),
|
|
ServerTlsCert: serverPair.Cert,
|
|
}
|
|
|
|
metadataWithExpiredConsenter := &etcdraftproto.ConfigMetadata{
|
|
Options: &etcdraftproto.Options{
|
|
TickInterval: "500ms",
|
|
ElectionTick: 10,
|
|
HeartbeatTick: 1,
|
|
MaxInflightBlocks: 5,
|
|
SnapshotIntervalSize: 20 * 1024 * 1024, // 20 MB
|
|
},
|
|
Consenters: []*etcdraftproto.Consenter{
|
|
consenterWithExpiredCerts,
|
|
},
|
|
}
|
|
|
|
require.Nil(t, VerifyConfigMetadata(metadataWithExpiredConsenter, goodVerifyingOpts))
|
|
})
|
|
}
|