Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions ca/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"github.com/cloudflare/cfssl/signer/local"
"github.com/docker/go-events"
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/ca/keyutils"
"github.com/docker/swarmkit/ca/pkcs8"
"github.com/docker/swarmkit/connectionbroker"
"github.com/docker/swarmkit/ioutils"
"github.com/opencontainers/go-digest"
Expand Down Expand Up @@ -655,9 +657,9 @@ func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPo
}

// Attempt to decrypt the current private-key with the passphrases provided
priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase)
priv, err = keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase)
if err != nil {
priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev)
priv, err = keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev)
if err != nil {
return nil, errors.Wrap(err, "malformed private key")
}
Expand All @@ -677,7 +679,7 @@ func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPo
// ensure it is encrypted, so it doesn't hit raft in plain-text
// we don't have to check for nil, because if we couldn't pem-decode the bytes, then parsing above would have failed
keyBlock, _ := pem.Decode(keyBytes)
if passphraseStr != "" && !x509.IsEncryptedPEMBlock(keyBlock) {
if passphraseStr != "" && !keyutils.IsEncryptedPEMBlock(keyBlock) {
keyBytes, err = EncryptECPrivateKey(keyBytes, passphraseStr)
if err != nil {
return nil, errors.Wrap(err, "unable to encrypt signing CA key material")
Expand Down Expand Up @@ -815,6 +817,14 @@ func CreateRootCA(rootCN string) (RootCA, error) {
return RootCA{}, err
}

// Convert key to PKCS#8 in FIPS mode
if keyutils.FIPSEnabled() {
key, err = pkcs8.ConvertECPrivateKeyPEM(key)
if err != nil {
return RootCA{}, err
}
}

rootCA, err := NewRootCA(cert, cert, key, DefaultNodeCertExpiration, nil)
if err != nil {
return RootCA{}, err
Expand Down Expand Up @@ -956,27 +966,29 @@ func GenerateNewCSR() ([]byte, []byte, error) {
req := &cfcsr.CertificateRequest{
KeyRequest: cfcsr.NewBasicKeyRequest(),
}
return cfcsr.ParseRequest(req)

csr, key, err := cfcsr.ParseRequest(req)
if err != nil {
return nil, nil, err
}

key, err = pkcs8.ConvertECPrivateKeyPEM(key)
return csr, key, err
}

// EncryptECPrivateKey receives a PEM encoded private key and returns an encrypted
// AES256 version using a passphrase
// TODO: Make this method generic to handle RSA keys
func EncryptECPrivateKey(key []byte, passphraseStr string) ([]byte, error) {
passphrase := []byte(passphraseStr)
cipherType := x509.PEMCipherAES256

keyBlock, _ := pem.Decode(key)
if keyBlock == nil {
// This RootCA does not have a valid signer.
return nil, errors.New("error while decoding PEM key")
}

encryptedPEMBlock, err := x509.EncryptPEMBlock(cryptorand.Reader,
"EC PRIVATE KEY",
keyBlock.Bytes,
passphrase,
cipherType)
encryptedPEMBlock, err := keyutils.EncryptPEMBlock(keyBlock.Bytes, passphrase)
if err != nil {
return nil, err
}
Expand Down
46 changes: 39 additions & 7 deletions ca/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"github.com/docker/go-events"
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/ca"
"github.com/docker/swarmkit/ca/keyutils"
"github.com/docker/swarmkit/ca/pkcs8"
cautils "github.com/docker/swarmkit/ca/testutils"
"github.com/docker/swarmkit/connectionbroker"
"github.com/docker/swarmkit/identity"
Expand Down Expand Up @@ -80,6 +82,31 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}

func TestCreateRootCAKeyFormat(t *testing.T) {
// Check if the CA key generated is PKCS#1 when FIPS-mode is off
rootCA, err := ca.CreateRootCA("rootCA")
require.NoError(t, err)

s, err := rootCA.Signer()
require.NoError(t, err)
block, _ := pem.Decode(s.Key)
require.NotNil(t, block)
require.Equal(t, "EC PRIVATE KEY", block.Type)

// Check if the CA key generated is PKCS#8 when FIPS-mode is on
os.Setenv(keyutils.FIPSEnvVar, "1")
defer os.Unsetenv(keyutils.FIPSEnvVar)

rootCA, err = ca.CreateRootCA("rootCA")
require.NoError(t, err)

s, err = rootCA.Signer()
require.NoError(t, err)
block, _ = pem.Decode(s.Key)
require.NotNil(t, block)
require.Equal(t, "PRIVATE KEY", block.Type)
}

func TestCreateRootCASaveRootCA(t *testing.T) {
tempBaseDir, err := ioutil.TempDir("", "swarm-ca-test-")
assert.NoError(t, err)
Expand Down Expand Up @@ -196,9 +223,9 @@ func TestGetLocalRootCAInvalidKey(t *testing.T) {
require.NoError(t, ca.SaveRootCA(rootCA, paths.RootCA))

// Write some garbage to the root key - this will cause the loading to fail
require.NoError(t, ioutil.WriteFile(paths.RootCA.Key, []byte(`-----BEGIN EC PRIVATE KEY-----\n
require.NoError(t, ioutil.WriteFile(paths.RootCA.Key, []byte(`-----BEGIN PRIVATE KEY-----\n
some random garbage\n
-----END EC PRIVATE KEY-----`), 0600))
-----END PRIVATE KEY-----`), 0600))

_, err = ca.GetLocalRootCA(paths.RootCA)
require.Error(t, err)
Expand All @@ -216,8 +243,7 @@ func TestEncryptECPrivateKey(t *testing.T) {

keyBlock, _ := pem.Decode(encryptedKey)
assert.NotNil(t, keyBlock)
assert.Equal(t, keyBlock.Headers["Proc-Type"], "4,ENCRYPTED")
assert.Contains(t, keyBlock.Headers["DEK-Info"], "AES-256-CBC")
assert.True(t, pkcs8.IsEncryptedPEMBlock(keyBlock))
}

func TestParseValidateAndSignCSR(t *testing.T) {
Expand Down Expand Up @@ -1324,7 +1350,9 @@ func TestNewRootCAWithPassphrase(t *testing.T) {
assert.NotEqual(t, rcaSigner.Key, nrcaSigner.Key)
assert.Equal(t, rootCA.Certs, newRootCA.Certs)
assert.NotContains(t, string(rcaSigner.Key), string(nrcaSigner.Key))
assert.Contains(t, string(nrcaSigner.Key), "Proc-Type: 4,ENCRYPTED")
keyBlock, _ := pem.Decode(nrcaSigner.Key)
assert.NotNil(t, keyBlock)
assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock))

// Ensure that we're decrypting the Key bytes out of NewRoot if there
// is a passphrase set as an env Var
Expand All @@ -1334,7 +1362,9 @@ func TestNewRootCAWithPassphrase(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, newRootCA, anotherNewRootCA)
assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key))
assert.Contains(t, string(anrcaSigner.Key), "Proc-Type: 4,ENCRYPTED")
keyBlock, _ = pem.Decode(anrcaSigner.Key)
assert.NotNil(t, keyBlock)
assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock))

// Ensure that we cant decrypt the Key bytes out of NewRoot if there
// is a wrong passphrase set as an env Var
Expand All @@ -1355,7 +1385,9 @@ func TestNewRootCAWithPassphrase(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, newRootCA, anotherNewRootCA)
assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key))
assert.Contains(t, string(anrcaSigner.Key), "Proc-Type: 4,ENCRYPTED")
keyBlock, _ = pem.Decode(anrcaSigner.Key)
assert.NotNil(t, keyBlock)
assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock))
}

type certTestCase struct {
Expand Down
75 changes: 58 additions & 17 deletions ca/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,27 @@ func TestCreateSecurityConfigNoCerts(t *testing.T) {
validateNodeConfig(&rootCA)
}

func testGRPCConnection(t *testing.T, secConfig *ca.SecurityConfig) {
// set up a GRPC server using these credentials
secConfig.ServerTLSCreds.Config().ClientAuth = tls.RequireAndVerifyClientCert
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
serverOpts := []grpc.ServerOption{grpc.Creds(secConfig.ServerTLSCreds)}
grpcServer := grpc.NewServer(serverOpts...)
go grpcServer.Serve(l)
defer grpcServer.Stop()

// we should be able to connect to the server using the client credentials
dialOpts := []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTimeout(10 * time.Second),
grpc.WithTransportCredentials(secConfig.ClientTLSCreds),
}
conn, err := grpc.Dial(l.Addr().String(), dialOpts...)
require.NoError(t, err)
conn.Close()
}

func TestLoadSecurityConfigExpiredCert(t *testing.T) {
if cautils.External {
return // this doesn't require any servers at all
Expand Down Expand Up @@ -233,9 +254,9 @@ func TestLoadSecurityConfigInvalidKey(t *testing.T) {
defer tc.Stop()

// Write some garbage to the Key
ioutil.WriteFile(tc.Paths.Node.Key, []byte(`-----BEGIN EC PRIVATE KEY-----\n
ioutil.WriteFile(tc.Paths.Node.Key, []byte(`-----BEGIN PRIVATE KEY-----\n
some random garbage\n
-----END EC PRIVATE KEY-----`), 0644)
-----END PRIVATE KEY-----`), 0644)

krw := ca.NewKeyReadWriter(tc.Paths.Node, nil, nil)

Expand Down Expand Up @@ -296,24 +317,44 @@ func TestLoadSecurityConfigIntermediates(t *testing.T) {
require.Equal(t, intermediate.RawSubjectPublicKeyInfo, issuerInfo.PublicKey)
require.Equal(t, intermediate.RawSubject, issuerInfo.Subject)

// set up a GRPC server using these credentials
secConfig.ServerTLSCreds.Config().ClientAuth = tls.RequireAndVerifyClientCert
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
serverOpts := []grpc.ServerOption{grpc.Creds(secConfig.ServerTLSCreds)}
grpcServer := grpc.NewServer(serverOpts...)
go grpcServer.Serve(l)
defer grpcServer.Stop()
testGRPCConnection(t, secConfig)
}

// we should be able to connect to the server using the client credentials
dialOpts := []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTimeout(10 * time.Second),
grpc.WithTransportCredentials(secConfig.ClientTLSCreds),
func TestLoadSecurityConfigKeyFormat(t *testing.T) {
if cautils.External {
return // this doesn't require any servers at all
}
conn, err := grpc.Dial(l.Addr().String(), dialOpts...)
tempdir, err := ioutil.TempDir("", "test-load-config")
require.NoError(t, err)
conn.Close()
defer os.RemoveAll(tempdir)
paths := ca.NewConfigPaths(tempdir)
krw := ca.NewKeyReadWriter(paths.Node, nil, nil)

rootCA, err := ca.NewRootCA(cautils.ECDSACertChain[1], nil, nil, ca.DefaultNodeCertExpiration, nil)
require.NoError(t, err)

ctx := log.WithLogger(context.Background(), log.L.WithFields(logrus.Fields{
"testname": t.Name(),
"testHasExternalCA": false,
}))

// load leaf cert with its PKCS#1 format key
require.NoError(t, krw.Write(cautils.ECDSACertChain[0], cautils.ECDSACertChainKeys[0], nil))
secConfig, cancel, err := ca.LoadSecurityConfig(ctx, rootCA, krw, false)
require.NoError(t, err)
defer cancel()
require.NotNil(t, secConfig)

testGRPCConnection(t, secConfig)

// load leaf cert with its PKCS#8 format key
require.NoError(t, krw.Write(cautils.ECDSACertChain[0], cautils.ECDSACertChainPKCS8Keys[0], nil))
secConfig, cancel, err = ca.LoadSecurityConfig(ctx, rootCA, krw, false)
require.NoError(t, err)
defer cancel()
require.NotNil(t, secConfig)

testGRPCConnection(t, secConfig)
}

// When the root CA is updated on the security config, the root pools are updated
Expand Down
63 changes: 54 additions & 9 deletions ca/keyreadwriter.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ca

import (
cryptorand "crypto/rand"
"crypto/x509"
"encoding/pem"
"io/ioutil"
Expand All @@ -13,6 +12,8 @@ import (

"crypto/tls"

"github.com/docker/swarmkit/ca/keyutils"
"github.com/docker/swarmkit/ca/pkcs8"
"github.com/docker/swarmkit/ioutils"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -313,7 +314,7 @@ func (k *KeyReadWriter) readKey() (*pem.Block, error) {
return nil, err
}

if !x509.IsEncryptedPEMBlock(keyBlock) {
if !keyutils.IsEncryptedPEMBlock(keyBlock) {
return keyBlock, nil
}

Expand All @@ -323,10 +324,16 @@ func (k *KeyReadWriter) readKey() (*pem.Block, error) {
return nil, ErrInvalidKEK{Wrapped: x509.IncorrectPasswordError}
}

derBytes, err := x509.DecryptPEMBlock(keyBlock, k.kekData.KEK)
derBytes, err := keyutils.DecryptPEMBlock(keyBlock, k.kekData.KEK)
if err != nil {
return nil, ErrInvalidKEK{Wrapped: err}
}

// change header only if its pkcs8
if keyBlock.Type == "ENCRYPTED PRIVATE KEY" {
keyBlock.Type = "PRIVATE KEY"
}

// remove encryption PEM headers
headers := make(map[string]string)
mergePEMHeaders(headers, keyBlock.Headers)
Expand All @@ -342,15 +349,11 @@ func (k *KeyReadWriter) readKey() (*pem.Block, error) {
// writing it to disk. If the kek is nil, writes it to disk unencrypted.
func (k *KeyReadWriter) writeKey(keyBlock *pem.Block, kekData KEKData, pkh PEMKeyHeaders) error {
if kekData.KEK != nil {
encryptedPEMBlock, err := x509.EncryptPEMBlock(cryptorand.Reader,
keyBlock.Type,
keyBlock.Bytes,
kekData.KEK,
x509.PEMCipherAES256)
encryptedPEMBlock, err := keyutils.EncryptPEMBlock(keyBlock.Bytes, kekData.KEK)
if err != nil {
return err
}
if encryptedPEMBlock.Headers == nil {
if !keyutils.IsEncryptedPEMBlock(encryptedPEMBlock) {
return errors.New("unable to encrypt key - invalid PEM file produced")
}
keyBlock = encryptedPEMBlock
Expand All @@ -373,6 +376,48 @@ func (k *KeyReadWriter) writeKey(keyBlock *pem.Block, kekData KEKData, pkh PEMKe
return nil
}

// DowngradeKey converts the PKCS#8 key to PKCS#1 format and save it
func (k *KeyReadWriter) DowngradeKey() error {
_, key, err := k.Read()
if err != nil {
return err
}

oldBlock, _ := pem.Decode(key)
if oldBlock == nil {
return errors.New("invalid PEM-encoded private key")
}

// stop if the key is already downgraded to pkcs1
if !keyutils.IsPKCS8(oldBlock.Bytes) {
return errors.New("key is already downgraded to PKCS#1")
}

eckey, err := pkcs8.ConvertToECPrivateKeyPEM(key)
if err != nil {
return err
}

newBlock, _ := pem.Decode(eckey)
if newBlock == nil {
return errors.New("invalid PEM-encoded private key")
}

if k.kekData.KEK != nil {
newBlock, err = keyutils.EncryptPEMBlock(newBlock.Bytes, k.kekData.KEK)
if err != nil {
return err
}
}

// add kek-version header back to the new key
newBlock.Headers[versionHeader] = strconv.FormatUint(k.kekData.Version, 10)
mergePEMHeaders(newBlock.Headers, oldBlock.Headers)

// do not use krw.Write as it will convert the key to pkcs8
return ioutils.AtomicWriteFile(k.paths.Key, pem.EncodeToMemory(newBlock), keyPerms)
}

// merges one set of PEM headers onto another, excepting for key encryption value
// "proc-type" and "dek-info"
func mergePEMHeaders(original, newSet map[string]string) {
Expand Down
Loading