diff --git a/ca/certificates.go b/ca/certificates.go index 7a638fe136..54c3b296f9 100644 --- a/ca/certificates.go +++ b/ca/certificates.go @@ -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" @@ -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") } @@ -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") @@ -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 @@ -956,7 +966,14 @@ 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 @@ -964,7 +981,6 @@ func GenerateNewCSR() ([]byte, []byte, error) { // 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 { @@ -972,11 +988,7 @@ func EncryptECPrivateKey(key []byte, passphraseStr string) ([]byte, error) { 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 } diff --git a/ca/certificates_test.go b/ca/certificates_test.go index 10ff249173..f43a192331 100644 --- a/ca/certificates_test.go +++ b/ca/certificates_test.go @@ -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" @@ -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) @@ -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) @@ -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) { @@ -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 @@ -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 @@ -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 { diff --git a/ca/config_test.go b/ca/config_test.go index 1187172400..7b9bdab53c 100644 --- a/ca/config_test.go +++ b/ca/config_test.go @@ -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 @@ -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) @@ -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 diff --git a/ca/keyreadwriter.go b/ca/keyreadwriter.go index 4a7c0a0eee..c929523f38 100644 --- a/ca/keyreadwriter.go +++ b/ca/keyreadwriter.go @@ -1,7 +1,6 @@ package ca import ( - cryptorand "crypto/rand" "crypto/x509" "encoding/pem" "io/ioutil" @@ -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" ) @@ -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 } @@ -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) @@ -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 @@ -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) { diff --git a/ca/keyreadwriter_test.go b/ca/keyreadwriter_test.go index 624adfef31..4b2610a74b 100644 --- a/ca/keyreadwriter_test.go +++ b/ca/keyreadwriter_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/docker/swarmkit/ca" + "github.com/docker/swarmkit/ca/keyutils" + "github.com/docker/swarmkit/ca/pkcs8" "github.com/docker/swarmkit/ca/testutils" "github.com/stretchr/testify/require" ) @@ -412,3 +414,106 @@ func TestKeyReadWriterMigrate(t *testing.T) { _, _, err = krw.Read() require.NoError(t, err) } + +type downgradeTestCase struct { + encrypted bool + pkcs8 bool + errorStr string +} + +func testKeyReadWriterDowngradeKeyCase(t *testing.T, tc downgradeTestCase) error { + cert, key, err := testutils.CreateRootCertAndKey("cn") + require.NoError(t, err) + + if !tc.pkcs8 { + key, err = pkcs8.ConvertToECPrivateKeyPEM(key) + require.NoError(t, err) + } + + var kek []byte + if tc.encrypted { + block, _ := pem.Decode(key) + require.NotNil(t, block) + + kek = []byte("kek") + block, err = keyutils.EncryptPEMBlock(block.Bytes, kek) + require.NoError(t, err) + + key = pem.EncodeToMemory(block) + } + + tempdir, err := ioutil.TempDir("", "KeyReadWriterDowngrade") + require.NoError(t, err) + defer os.RemoveAll(tempdir) + + path := ca.NewConfigPaths(filepath.Join(tempdir)) + + block, _ := pem.Decode(key) + require.NotNil(t, block) + + // add kek-version to later check if it is still there + block.Headers["kek-version"] = "5" + + key = pem.EncodeToMemory(block) + require.NoError(t, ioutil.WriteFile(path.Node.Cert, cert, 0644)) + require.NoError(t, ioutil.WriteFile(path.Node.Key, key, 0600)) + + // if the update headers callback function fails, updating headers fails + k := ca.NewKeyReadWriter(path.Node, kek, nil) + if err := k.DowngradeKey(); err != nil { + return err + } + + // read the key directly from fs so we can check if key + key, err = ioutil.ReadFile(path.Node.Key) + require.NoError(t, err) + + keyBlock, _ := pem.Decode(key) + require.NotNil(t, block) + require.False(t, keyutils.IsPKCS8(keyBlock.Bytes)) + + if tc.encrypted { + require.True(t, keyutils.IsEncryptedPEMBlock(keyBlock)) + } + require.Equal(t, "5", keyBlock.Headers["kek-version"]) + + // check if KeyReaderWriter can read the key + _, _, err = k.Read() + require.NoError(t, err) + return nil +} + +func TestKeyReadWriterDowngradeKey(t *testing.T) { + invalid := []downgradeTestCase{ + { + encrypted: false, + pkcs8: false, + errorStr: "key is already downgraded to PKCS#1", + }, { + encrypted: true, + pkcs8: false, + errorStr: "key is already downgraded to PKCS#1", + }, + } + + for _, c := range invalid { + err := testKeyReadWriterDowngradeKeyCase(t, c) + require.Error(t, err) + require.EqualError(t, err, c.errorStr) + } + + valid := []downgradeTestCase{ + { + encrypted: false, + pkcs8: true, + }, { + encrypted: true, + pkcs8: true, + }, + } + + for _, c := range valid { + err := testKeyReadWriterDowngradeKeyCase(t, c) + require.NoError(t, err) + } +} diff --git a/ca/keyutils/keyutils.go b/ca/keyutils/keyutils.go new file mode 100644 index 0000000000..ee6ad8fc16 --- /dev/null +++ b/ca/keyutils/keyutils.go @@ -0,0 +1,92 @@ +// Package keyutils serves as a utility to parse, encrypt and decrypt +// PKCS#1 and PKCS#8 private keys based on current FIPS mode status, +// supporting only EC type keys. It always allows PKCS#8 private keys +// and disallow PKCS#1 private keys in FIPS-mode. +package keyutils + +import ( + "crypto" + cryptorand "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" + "os" + + "github.com/cloudflare/cfssl/helpers" + "github.com/docker/swarmkit/ca/pkcs8" +) + +var errFIPSUnsupportedKeyFormat = errors.New("unsupported key format due to FIPS compliance") + +// FIPSEnvVar is the environment variable which stores FIPS mode state +const FIPSEnvVar = "GOFIPS" + +// FIPSEnabled returns true when FIPS mode is enabled +func FIPSEnabled() bool { + return os.Getenv(FIPSEnvVar) != "" +} + +// IsPKCS8 returns true if the provided der bytes is encrypted/unencrypted PKCS#8 key +func IsPKCS8(derBytes []byte) bool { + if _, err := x509.ParsePKCS8PrivateKey(derBytes); err == nil { + return true + } + + return pkcs8.IsEncryptedPEMBlock(&pem.Block{ + Type: "PRIVATE KEY", + Headers: nil, + Bytes: derBytes, + }) +} + +// ParsePrivateKeyPEMWithPassword parses an encrypted or a decrypted PKCS#1 or PKCS#8 PEM to crypto.Signer. +// It returns an error in FIPS mode if PKCS#1 PEM bytes are passed. +func ParsePrivateKeyPEMWithPassword(pemBytes, password []byte) (crypto.Signer, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("Could not parse PEM") + } + + if IsPKCS8(block.Bytes) { + return pkcs8.ParsePrivateKeyPEMWithPassword(pemBytes, password) + } else if FIPSEnabled() { + return nil, errFIPSUnsupportedKeyFormat + } + + return helpers.ParsePrivateKeyPEMWithPassword(pemBytes, password) +} + +// IsEncryptedPEMBlock checks if a PKCS#1 or PKCS#8 PEM-block is encrypted or not +// It returns false in FIPS mode even if PKCS#1 is encrypted +func IsEncryptedPEMBlock(block *pem.Block) bool { + return pkcs8.IsEncryptedPEMBlock(block) || (!FIPSEnabled() && x509.IsEncryptedPEMBlock(block)) +} + +// DecryptPEMBlock requires PKCS#1 or PKCS#8 PEM Block and password to decrypt and return unencrypted der []byte +// It returns an error in FIPS mode when PKCS#1 PEM Block is passed. +func DecryptPEMBlock(block *pem.Block, password []byte) ([]byte, error) { + if IsPKCS8(block.Bytes) { + return pkcs8.DecryptPEMBlock(block, password) + } else if FIPSEnabled() { + return nil, errFIPSUnsupportedKeyFormat + } + + return x509.DecryptPEMBlock(block, password) +} + +// EncryptPEMBlock takes DER-format bytes and password to return an encrypted PKCS#1 or PKCS#8 PEM-block +// It returns an error in FIPS mode when PKCS#1 PEM bytes are passed. +func EncryptPEMBlock(data, password []byte) (*pem.Block, error) { + if IsPKCS8(data) { + return pkcs8.EncryptPEMBlock(data, password) + } else if FIPSEnabled() { + return nil, errFIPSUnsupportedKeyFormat + } + + cipherType := x509.PEMCipherAES256 + return x509.EncryptPEMBlock(cryptorand.Reader, + "EC PRIVATE KEY", + data, + password, + cipherType) +} diff --git a/ca/keyutils/keyutils_test.go b/ca/keyutils/keyutils_test.go new file mode 100644 index 0000000000..15f52d69ef --- /dev/null +++ b/ca/keyutils/keyutils_test.go @@ -0,0 +1,193 @@ +package keyutils + +import ( + "encoding/pem" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + decryptedPKCS1 = `-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEHECF7HdJ4QZ7Dx0FBzzV/6vgI+bZNZGWtmbVwPIMu/bZE1p2qz5HGS +EFsmor5X6t7KYLa4nQNqbloWaneRNNukk6AHBgUrgQQAI6GBiQOBhgAEAW4hBUpI ++ckv40lP6HIUTr/71yhrZWjCWGh84xNk8LxNA54oy4DV4hS7E9+NLHKJrwnLDlnG +FR9il6zgU/9IsJdWAVcqVY7vsOKs8dquQ1HLXcOos22TOXbQne3Ua66HC0mjJ9Xp +LrnqZrqoHphZCknCX9HFSrlvdq6PEBSaCgfe3dd/ +-----END EC PRIVATE KEY----- +` + encryptedPKCS1 = `-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,8EE2B3B5A92822309E6157EBFFB238ED + +clpdzQaCjXy2ZNLEsiGSpt0//DRdO1haJ4wrDTrhb78npiWrWjVsyAEwBoSPRwPW +ZnGKjAV+tv7w4XujycwijsSBVCzGvCbMYnzO+n0zApD6eo1SF/bRCZqEPcWDnsCK +UtLuqa3o8F0q3Bh8woOJ6NOq8dNWA2XHNkNhs77aqTh+bDR+jruDjFDB5/HZxDU2 +aCpI96TeakB+8upn+/1wkpxfAJLpbkOdWDIgTEMhhwZUBQocoZezEORn4JIpYknY +0fOJaoM+gMMVLDPvXWUZFulP+2TpIOsHWspY2D4mYUE= +-----END EC PRIVATE KEY----- +` + decryptedPKCS8 = `-----BEGIN PRIVATE KEY----- +MHgCAQAwEAYHKoZIzj0CAQYFK4EEACEEYTBfAgEBBBwCTYvOWrsYitgVHwD6F4GH +1re5Oe05CtZ4PUgkoTwDOgAETRlz5X662R8MX3tcoTTZiE2psZScMQNo6X/6gH+L +5xPO1GTcpbAt8U+ULn/4S5Bgq+WIgA8bI4g= +-----END PRIVATE KEY----- +` + encryptedPKCS8 = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHOMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiGRncJ5A+72AICCAAw +HQYJYIZIAWUDBAEqBBA0iGGDrKda4SbsQlW8hgiOBIGA1rDEtNqghfQ+8AtdB7kY +US05ElIO2ooXviNo0M36Shltv+1ntd/Qxn+El1B+0BT8MngB8yBV6oFach1dfKvR +PkeX/+bOnd1WTKMx3IPNMWxbA9YPTeoaObaKI7awvI03o51HLd+a5BuHJ55N2CX4 +aMbljbOLAjpZS3/VnQteab4= +-----END ENCRYPTED PRIVATE KEY----- +` + decryptedPKCS8Block, _ = pem.Decode([]byte(decryptedPKCS8)) + encryptedPKCS8Block, _ = pem.Decode([]byte(encryptedPKCS8)) + decryptedPKCS1Block, _ = pem.Decode([]byte(decryptedPKCS1)) + encryptedPKCS1Block, _ = pem.Decode([]byte(encryptedPKCS1)) +) + +func TestFIPSEnabled(t *testing.T) { + os.Unsetenv(FIPSEnvVar) + assert.False(t, FIPSEnabled()) + + os.Setenv(FIPSEnvVar, "1") + defer os.Unsetenv(FIPSEnvVar) + assert.True(t, FIPSEnabled()) +} + +func TestIsPKCS8(t *testing.T) { + // Check PKCS8 keys + assert.True(t, IsPKCS8([]byte(decryptedPKCS8Block.Bytes))) + assert.True(t, IsPKCS8([]byte(encryptedPKCS8Block.Bytes))) + + // Check PKCS1 keys + assert.False(t, IsPKCS8([]byte(decryptedPKCS1Block.Bytes))) + assert.False(t, IsPKCS8([]byte(encryptedPKCS1Block.Bytes))) +} + +func TestIsEncryptedPEMBlock(t *testing.T) { + // Disable FIPS mode + os.Unsetenv(FIPSEnvVar) + + // Check PKCS8 keys + assert.False(t, IsEncryptedPEMBlock(decryptedPKCS8Block)) + assert.True(t, IsEncryptedPEMBlock(encryptedPKCS8Block)) + + // Check PKCS1 keys + assert.False(t, IsEncryptedPEMBlock(decryptedPKCS1Block)) + assert.True(t, IsEncryptedPEMBlock(encryptedPKCS1Block)) + + // Enable FIPS mode + os.Setenv(FIPSEnvVar, "1") + defer os.Unsetenv(FIPSEnvVar) + + // Check PKCS8 keys again + assert.False(t, IsEncryptedPEMBlock(decryptedPKCS8Block)) + assert.True(t, IsEncryptedPEMBlock(encryptedPKCS8Block)) + + // Check PKCS1 keys again + assert.False(t, IsEncryptedPEMBlock(decryptedPKCS1Block)) + assert.False(t, IsEncryptedPEMBlock(encryptedPKCS1Block)) +} + +func TestDecryptPEMBlock(t *testing.T) { + // Disable FIPS mode + os.Unsetenv(FIPSEnvVar) + + // Check PKCS8 keys + _, err := DecryptPEMBlock(encryptedPKCS8Block, []byte("pony")) + require.Error(t, err) + + decryptedDer, err := DecryptPEMBlock(encryptedPKCS8Block, []byte("ponies")) + require.NoError(t, err) + require.Equal(t, decryptedPKCS8Block.Bytes, decryptedDer) + + // Check PKCS1 keys + _, err = DecryptPEMBlock(encryptedPKCS1Block, []byte("pony")) + require.Error(t, err) + + decryptedDer, err = DecryptPEMBlock(encryptedPKCS1Block, []byte("ponies")) + require.NoError(t, err) + require.Equal(t, decryptedPKCS1Block.Bytes, decryptedDer) + + // Enable FIPS mode + os.Setenv(FIPSEnvVar, "1") + defer os.Unsetenv(FIPSEnvVar) + + // Try to decrypt PKCS1 + _, err = DecryptPEMBlock(encryptedPKCS1Block, []byte("ponies")) + require.Error(t, err) +} + +func TestEncryptPEMBlock(t *testing.T) { + // Disable FIPS mode + os.Unsetenv(FIPSEnvVar) + + // Check PKCS8 keys + encryptedBlock, err := EncryptPEMBlock(decryptedPKCS8Block.Bytes, []byte("knock knock")) + require.NoError(t, err) + + // Try to decrypt the same encrypted block + _, err = DecryptPEMBlock(encryptedBlock, []byte("hey there")) + require.Error(t, err) + + decryptedDer, err := DecryptPEMBlock(encryptedBlock, []byte("knock knock")) + require.NoError(t, err) + require.Equal(t, decryptedPKCS8Block.Bytes, decryptedDer) + + // Check PKCS1 keys + encryptedBlock, err = EncryptPEMBlock(decryptedPKCS1Block.Bytes, []byte("knock knock")) + require.NoError(t, err) + + // Try to decrypt the same encrypted block + _, err = DecryptPEMBlock(encryptedBlock, []byte("hey there")) + require.Error(t, err) + + decryptedDer, err = DecryptPEMBlock(encryptedBlock, []byte("knock knock")) + require.NoError(t, err) + require.Equal(t, decryptedPKCS1Block.Bytes, decryptedDer) + + // Enable FIPS mode + os.Setenv(FIPSEnvVar, "1") + defer os.Unsetenv(FIPSEnvVar) + + // Try to encrypt PKCS1 + _, err = EncryptPEMBlock(decryptedPKCS1Block.Bytes, []byte("knock knock")) + require.Error(t, err) +} + +func TestParsePrivateKeyPEMWithPassword(t *testing.T) { + // Disable FIPS mode + os.Unsetenv(FIPSEnvVar) + + // Check PKCS8 keys + _, err := ParsePrivateKeyPEMWithPassword([]byte(encryptedPKCS8), []byte("pony")) + require.Error(t, err) + + _, err = ParsePrivateKeyPEMWithPassword([]byte(encryptedPKCS8), []byte("ponies")) + require.NoError(t, err) + + _, err = ParsePrivateKeyPEMWithPassword([]byte(decryptedPKCS8), nil) + require.NoError(t, err) + + // Check PKCS1 keys + _, err = ParsePrivateKeyPEMWithPassword([]byte(encryptedPKCS1), []byte("pony")) + require.Error(t, err) + + _, err = ParsePrivateKeyPEMWithPassword([]byte(encryptedPKCS1), []byte("ponies")) + require.NoError(t, err) + + _, err = ParsePrivateKeyPEMWithPassword([]byte(decryptedPKCS1), nil) + require.NoError(t, err) + + // Enable FIPS mode + os.Setenv(FIPSEnvVar, "1") + defer os.Unsetenv(FIPSEnvVar) + + // Try to parse PKCS1 + _, err = ParsePrivateKeyPEMWithPassword([]byte(encryptedPKCS1), []byte("ponies")) + require.Error(t, err) +} diff --git a/ca/pkcs8/pkcs8.go b/ca/pkcs8/pkcs8.go new file mode 100644 index 0000000000..223fc99d5d --- /dev/null +++ b/ca/pkcs8/pkcs8.go @@ -0,0 +1,311 @@ +// Package pkcs8 implements functions to encrypt, decrypt, parse and to convert +// EC private keys to PKCS#8 format. However this package is hard forked from +// https://github.com/youmark/pkcs8 and modified function signatures to match +// signatures of crypto/x509 and cloudflare/cfssl/helpers to simplify package +// swapping. License for original package is as follow: +// +// The MIT License (MIT) +// +// Copyright (c) 2014 youmark +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +package pkcs8 + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha1" + "encoding/asn1" + "encoding/pem" + "errors" + + "github.com/cloudflare/cfssl/helpers/derhelpers" + "golang.org/x/crypto/pbkdf2" +) + +// Copy from crypto/x509 +var ( + oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} +) + +// Unencrypted PKCS#8 +var ( + oidPKCS5PBKDF2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 12} + oidPBES2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + oidAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} +) + +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +type privateKeyInfo struct { + Version int + PrivateKeyAlgorithm []asn1.ObjectIdentifier + PrivateKey []byte +} + +// Encrypted PKCS8 +type pbkdf2Params struct { + Salt []byte + IterationCount int +} + +type pbkdf2Algorithms struct { + IDPBKDF2 asn1.ObjectIdentifier + PBKDF2Params pbkdf2Params +} + +type pbkdf2Encs struct { + EncryAlgo asn1.ObjectIdentifier + IV []byte +} + +type pbes2Params struct { + KeyDerivationFunc pbkdf2Algorithms + EncryptionScheme pbkdf2Encs +} + +type pbes2Algorithms struct { + IDPBES2 asn1.ObjectIdentifier + PBES2Params pbes2Params +} + +type encryptedPrivateKeyInfo struct { + EncryptionAlgorithm pbes2Algorithms + EncryptedData []byte +} + +// ParsePrivateKeyPEMWithPassword parses an encrypted or a decrypted PKCS#8 PEM to crypto.signer +func ParsePrivateKeyPEMWithPassword(pemBytes, password []byte) (crypto.Signer, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("invalid pem file") + } + + var ( + der []byte + err error + ) + der = block.Bytes + + if ok := IsEncryptedPEMBlock(block); ok { + der, err = DecryptPEMBlock(block, password) + if err != nil { + return nil, err + } + } + + return derhelpers.ParsePrivateKeyDER(der) +} + +// IsEncryptedPEMBlock checks if a PKCS#8 PEM-block is encrypted or not +func IsEncryptedPEMBlock(block *pem.Block) bool { + der := block.Bytes + + var privKey encryptedPrivateKeyInfo + if _, err := asn1.Unmarshal(der, &privKey); err != nil { + return false + } + + return true +} + +// DecryptPEMBlock requires PKCS#8 PEM Block and password to decrypt and return unencrypted der []byte +func DecryptPEMBlock(block *pem.Block, password []byte) ([]byte, error) { + der := block.Bytes + + var privKey encryptedPrivateKeyInfo + if _, err := asn1.Unmarshal(der, &privKey); err != nil { + return nil, errors.New("pkcs8: only PKCS #5 v2.0 supported") + } + + if !privKey.EncryptionAlgorithm.IDPBES2.Equal(oidPBES2) { + return nil, errors.New("pkcs8: only PBES2 supported") + } + + if !privKey.EncryptionAlgorithm.PBES2Params.KeyDerivationFunc.IDPBKDF2.Equal(oidPKCS5PBKDF2) { + return nil, errors.New("pkcs8: only PBKDF2 supported") + } + + encParam := privKey.EncryptionAlgorithm.PBES2Params.EncryptionScheme + kdfParam := privKey.EncryptionAlgorithm.PBES2Params.KeyDerivationFunc.PBKDF2Params + + switch { + case encParam.EncryAlgo.Equal(oidAES256CBC): + iv := encParam.IV + salt := kdfParam.Salt + iter := kdfParam.IterationCount + + encryptedKey := privKey.EncryptedData + symkey := pbkdf2.Key(password, salt, iter, 32, sha1.New) + block, err := aes.NewCipher(symkey) + if err != nil { + return nil, err + } + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(encryptedKey, encryptedKey) + + if _, err := derhelpers.ParsePrivateKeyDER(encryptedKey); err != nil { + return nil, errors.New("pkcs8: incorrect password") + } + + // Remove padding from key as it might be used to encode to memory as pem + keyLen := len(encryptedKey) + padLen := int(encryptedKey[keyLen-1]) + if padLen > keyLen || padLen > aes.BlockSize { + return nil, errors.New("pkcs8: invalid padding size") + } + encryptedKey = encryptedKey[:keyLen-padLen] + + return encryptedKey, nil + default: + return nil, errors.New("pkcs8: only AES-256-CBC supported") + } +} + +func encryptPrivateKey(pkey, password []byte) ([]byte, error) { + // Calculate key from password based on PKCS5 algorithm + // Use 8 byte salt, 16 byte IV, and 2048 iteration + iter := 2048 + salt := make([]byte, 8) + iv := make([]byte, 16) + + if _, err := rand.Reader.Read(salt); err != nil { + return nil, err + } + + if _, err := rand.Reader.Read(iv); err != nil { + return nil, err + } + + key := pbkdf2.Key(password, salt, iter, 32, sha1.New) + + // Use AES256-CBC mode, pad plaintext with PKCS5 padding scheme + n := len(pkey) + padLen := aes.BlockSize - n%aes.BlockSize + if padLen > 0 { + padValue := []byte{byte(padLen)} + padding := bytes.Repeat(padValue, padLen) + pkey = append(pkey, padding...) + } + + encryptedKey := make([]byte, len(pkey)) + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(encryptedKey, pkey) + + pbkdf2algo := pbkdf2Algorithms{oidPKCS5PBKDF2, pbkdf2Params{salt, iter}} + pbkdf2encs := pbkdf2Encs{oidAES256CBC, iv} + pbes2algo := pbes2Algorithms{oidPBES2, pbes2Params{pbkdf2algo, pbkdf2encs}} + + encryptedPkey := encryptedPrivateKeyInfo{pbes2algo, encryptedKey} + return asn1.Marshal(encryptedPkey) +} + +// EncryptPEMBlock takes DER-format bytes and password to return an encrypted PKCS#8 PEM-block +func EncryptPEMBlock(data, password []byte) (*pem.Block, error) { + encryptedBytes, err := encryptPrivateKey(data, password) + if err != nil { + return nil, err + } + + return &pem.Block{ + Type: "ENCRYPTED PRIVATE KEY", + Headers: map[string]string{}, + Bytes: encryptedBytes, + }, nil +} + +// ConvertECPrivateKeyPEM takes an EC Private Key as input and returns PKCS#8 version of it +func ConvertECPrivateKeyPEM(inPEM []byte) ([]byte, error) { + block, _ := pem.Decode(inPEM) + if block == nil { + return nil, errors.New("invalid pem bytes") + } + + var ecPrivKey ecPrivateKey + if _, err := asn1.Unmarshal(block.Bytes, &ecPrivKey); err != nil { + return nil, errors.New("invalid ec private key") + } + + var pkey privateKeyInfo + pkey.Version = 0 + pkey.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 2) + pkey.PrivateKeyAlgorithm[0] = oidPublicKeyECDSA + pkey.PrivateKeyAlgorithm[1] = ecPrivKey.NamedCurveOID + + // remove curve oid from private bytes as it is already mentioned in algorithm + ecPrivKey.NamedCurveOID = nil + + privatekey, err := asn1.Marshal(ecPrivKey) + if err != nil { + return nil, err + } + pkey.PrivateKey = privatekey + + der, err := asn1.Marshal(pkey) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: der, + }), nil +} + +// ConvertToECPrivateKeyPEM takes an unencrypted PKCS#8 PEM and converts it to +// EC Private Key +func ConvertToECPrivateKeyPEM(inPEM []byte) ([]byte, error) { + block, _ := pem.Decode(inPEM) + if block == nil { + return nil, errors.New("invalid pem bytes") + } + + var pkey privateKeyInfo + if _, err := asn1.Unmarshal(block.Bytes, &pkey); err != nil { + return nil, errors.New("invalid pkcs8 key") + } + + var ecPrivKey ecPrivateKey + if _, err := asn1.Unmarshal(pkey.PrivateKey, &ecPrivKey); err != nil { + return nil, errors.New("invalid private key") + } + + ecPrivKey.NamedCurveOID = pkey.PrivateKeyAlgorithm[1] + key, err := asn1.Marshal(ecPrivKey) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: key, + }), nil +} diff --git a/ca/pkcs8/pkcs8_test.go b/ca/pkcs8/pkcs8_test.go new file mode 100644 index 0000000000..e49392f7a8 --- /dev/null +++ b/ca/pkcs8/pkcs8_test.go @@ -0,0 +1,133 @@ +package pkcs8 + +import ( + "encoding/pem" + "testing" + + "github.com/cloudflare/cfssl/helpers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + ecKeyPEM = `-----BEGIN EC PRIVATE KEY----- +MGgCAQEEHAJNi85auxiK2BUfAPoXgYfWt7k57TkK1ng9SCSgBwYFK4EEACGhPAM6 +AARNGXPlfrrZHwxfe1yhNNmITamxlJwxA2jpf/qAf4vnE87UZNylsC3xT5Quf/hL +kGCr5YiADxsjiA== +-----END EC PRIVATE KEY----- +` + decryptedPEM = `-----BEGIN PRIVATE KEY----- +MHgCAQAwEAYHKoZIzj0CAQYFK4EEACEEYTBfAgEBBBwCTYvOWrsYitgVHwD6F4GH +1re5Oe05CtZ4PUgkoTwDOgAETRlz5X662R8MX3tcoTTZiE2psZScMQNo6X/6gH+L +5xPO1GTcpbAt8U+ULn/4S5Bgq+WIgA8bI4g= +-----END PRIVATE KEY----- +` + encryptedPEM = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHOMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiGRncJ5A+72AICCAAw +HQYJYIZIAWUDBAEqBBA0iGGDrKda4SbsQlW8hgiOBIGA1rDEtNqghfQ+8AtdB7kY +US05ElIO2ooXviNo0M36Shltv+1ntd/Qxn+El1B+0BT8MngB8yBV6oFach1dfKvR +PkeX/+bOnd1WTKMx3IPNMWxbA9YPTeoaObaKI7awvI03o51HLd+a5BuHJ55N2CX4 +aMbljbOLAjpZS3/VnQteab4= +-----END ENCRYPTED PRIVATE KEY----- +` + encryptedPEMInvalidPadding = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHOMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAjxk6v6kjceLAICCAAw +HQYJYIZIAWUDBAEqBBBVCqGMzL53rwf6Bv4OEPeJBIGAEuEUhjZd/d1BEbntAoZU +3cCB6ewYMqj97p6MncR1EFq+a26R/ehoCZg7O2L5AJrZK8K6UuZG8HxpZkraS5Mh +L5dg6PPGclig3Xn1sCPUmHi13x+DPISBuUdkQEep5lEpqxLSRQerllbXmhaTznAk +aqc20eq8ndE9DjZ7gDPnslY= +-----END ENCRYPTED PRIVATE KEY-----` +) + +func TestIsEncryptedPEMBlock(t *testing.T) { + decryptedPEMBlock, _ := pem.Decode([]byte(decryptedPEM)) + encryptedPEMBlock, _ := pem.Decode([]byte(encryptedPEM)) + + assert.False(t, IsEncryptedPEMBlock(decryptedPEMBlock)) + assert.True(t, IsEncryptedPEMBlock(encryptedPEMBlock)) +} + +func TestDecryptPEMBlock(t *testing.T) { + expectedBlock, _ := pem.Decode([]byte(decryptedPEM)) + block, _ := pem.Decode([]byte(encryptedPEM)) + + _, err := DecryptPEMBlock(block, []byte("pony")) + require.EqualError(t, err, "pkcs8: incorrect password") + + decryptedDer, err := DecryptPEMBlock(block, []byte("ponies")) + require.NoError(t, err) + require.Equal(t, expectedBlock.Bytes, decryptedDer) + + // Try to decrypt an already decrypted key + decryptedKeyBlock, _ := pem.Decode([]byte(decryptedPEM)) + _, err = DecryptPEMBlock(decryptedKeyBlock, []byte("ponies")) + require.Error(t, err) + + // Decrypt a key with 32bit padding length + invalidPadLenKeyBlock, _ := pem.Decode([]byte(encryptedPEMInvalidPadding)) + _, err = DecryptPEMBlock(invalidPadLenKeyBlock, []byte("poonies")) + require.EqualError(t, err, "pkcs8: invalid padding size") +} + +func TestEncryptPEMBlock(t *testing.T) { + block, _ := pem.Decode([]byte(decryptedPEM)) + encryptedBlock, err := EncryptPEMBlock(block.Bytes, []byte("knock knock")) + require.NoError(t, err) + + // Try to decrypt the same encrypted block + _, err = DecryptPEMBlock(encryptedBlock, []byte("hey there")) + require.Error(t, err) + + decryptedDer, err := DecryptPEMBlock(encryptedBlock, []byte("knock knock")) + require.NoError(t, err) + require.Equal(t, block.Bytes, decryptedDer) +} + +func TestParsePrivateKeyPEMWithPassword(t *testing.T) { + _, err := ParsePrivateKeyPEMWithPassword([]byte(encryptedPEM), []byte("pony")) + require.Error(t, err) + + _, err = ParsePrivateKeyPEMWithPassword([]byte(encryptedPEM), []byte("ponies")) + require.NoError(t, err) + + _, err = ParsePrivateKeyPEMWithPassword([]byte(decryptedPEM), nil) + require.NoError(t, err) +} + +func TestConvertECPrivateKeyPEM(t *testing.T) { + _, err := ConvertECPrivateKeyPEM([]byte(`garbage pem`)) + require.Error(t, err) + + _, err = ConvertECPrivateKeyPEM([]byte(`-----BEGIN EC PRIVATE KEY----- +garbage key +-----END EC PRIVATE KEY-----`)) + require.Error(t, err) + + out, err := ConvertECPrivateKeyPEM([]byte(ecKeyPEM)) + require.NoError(t, err) + + _, err = helpers.ParsePrivateKeyPEM([]byte(ecKeyPEM)) + require.NoError(t, err) + _, err = helpers.ParsePrivateKeyPEM(out) + require.NoError(t, err) + require.Equal(t, []byte(decryptedPEM), out) +} + +func TestConvertToECPrivateKeyPEM(t *testing.T) { + _, err := ConvertToECPrivateKeyPEM([]byte(`garbage pem`)) + require.Error(t, err) + + _, err = ConvertToECPrivateKeyPEM([]byte(`-----BEGIN PRIVATE KEY----- +garbage key +-----END PRIVATE KEY-----`)) + require.Error(t, err) + + out, err := ConvertToECPrivateKeyPEM([]byte(decryptedPEM)) + require.NoError(t, err) + + _, err = helpers.ParsePrivateKeyPEM([]byte(decryptedPEM)) + require.NoError(t, err) + _, err = helpers.ParsePrivateKeyPEM(out) + require.NoError(t, err) + require.Equal(t, []byte(ecKeyPEM), out) +} diff --git a/ca/testutils/cautils.go b/ca/testutils/cautils.go index cb61b036f5..2171d7f234 100644 --- a/ca/testutils/cautils.go +++ b/ca/testutils/cautils.go @@ -17,6 +17,7 @@ import ( "github.com/cloudflare/cfssl/initca" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" + "github.com/docker/swarmkit/ca/pkcs8" "github.com/docker/swarmkit/connectionbroker" "github.com/docker/swarmkit/identity" "github.com/docker/swarmkit/ioutils" @@ -295,6 +296,11 @@ func genSecurityConfig(s *store.MemoryStore, rootCA ca.RootCA, krw *ca.KeyReadWr return nil, nil, err } + key, err = pkcs8.ConvertECPrivateKeyPEM(key) + if err != nil { + return nil, nil, err + } + // Obtain a signed Certificate nodeID := identity.NewID() @@ -386,6 +392,15 @@ func CreateRootCertAndKey(rootCN string) ([]byte, []byte, error) { // Generate the CA and get the certificate and private key cert, _, key, err := initca.New(&req) + if err != nil { + return nil, nil, err + } + + key, err = pkcs8.ConvertECPrivateKeyPEM(key) + if err != nil { + return nil, nil, err + } + return cert, key, err } diff --git a/ca/testutils/staticcerts.go b/ca/testutils/staticcerts.go index 52268c8e00..9d073a2a06 100644 --- a/ca/testutils/staticcerts.go +++ b/ca/testutils/staticcerts.go @@ -361,4 +361,27 @@ ru3FP92pSYiJZkJemN593BYYkFdnbpvlFA== -----END EC PRIVATE KEY----- `), } + + // ECDSACertChainPKCS8Keys contains 3 SHA256 curve P-256 keys in PKCS#8 format: + // corresponding, respectively, to the certificates in ECDSACertChain + ECDSACertChainPKCS8Keys = [][]byte{ + []byte(`-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg34FobLEYtKAQMuOZ +AEd0vsm64AhiVIYLBhLYIJTEwQ6hRANCAATCVPwZBGYQ0SpeXahXzU8BB+ZBjdw9 +WsKBa03qSic4O0qtUrLTQSvg2bWoKlo2fVe5g6Sl29gMm0912fTG5nHr +-----END PRIVATE KEY----- + `), + []byte(`-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/vI19pQiYPvqrWl+ +DH7w9hKrnTMrWKaHjVIoU/NHl+KhRANCAAQviDj/BaFkz9h8KT/MRduZMiBqI1Sx +eup7FcauwV6jF+iMAS4Dy3KAjya7jlV82TXqfM7gB9cwUGIKEH188TV0 +-----END PRIVATE KEY----- + `), + []byte(`-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgMiASkKmfvAQRi3+E +tP417A7SWAE8FGTcDmdTgBI5KEOhRANCAAT6NjQeSstS/gi2wN+AoWnMZaLfiBjp +NSqryqEiPH03viwbtWMG9aCu7cU/3alJiIlmQl6Y3n3cFhiQV2dum+UU +-----END PRIVATE KEY----- + `), + } ) diff --git a/cmd/swarm-rafttool/common.go b/cmd/swarm-rafttool/common.go index 1542fbd195..4d14eb2832 100644 --- a/cmd/swarm-rafttool/common.go +++ b/cmd/swarm-rafttool/common.go @@ -14,6 +14,7 @@ import ( "github.com/docker/swarmkit/manager" "github.com/docker/swarmkit/manager/encryption" "github.com/docker/swarmkit/manager/state/raft/storage" + "github.com/docker/swarmkit/node" ) func certPaths(swarmdir string) *ca.SecurityConfigPaths { @@ -108,3 +109,26 @@ func decryptRaftData(swarmdir, outdir, unlockKey string) error { filepath.Join(swarmdir, "raft", "wal-v3-encrypted"), walDir, storage.NewWALFactory(encryption.NoopCrypter, d), storage.OriginalWAL, walsnap) } + +func downgradeKey(swarmdir, unlockKey string) error { + var ( + kek []byte + err error + ) + if unlockKey != "" { + kek, err = encryption.ParseHumanReadableKey(unlockKey) + if err != nil { + return err + } + } + + n, err := node.New(&node.Config{ + StateDir: swarmdir, + UnlockKey: kek, + }) + if err != nil { + return err + } + + return n.DowngradeKey() +} diff --git a/cmd/swarm-rafttool/main.go b/cmd/swarm-rafttool/main.go index 282cd48bca..a5d2963c6e 100644 --- a/cmd/swarm-rafttool/main.go +++ b/cmd/swarm-rafttool/main.go @@ -139,6 +139,24 @@ var ( return dumpObject(stateDir, unlockKey, args[0], selector) }, } + + downgradeKeyCmd = &cobra.Command{ + Use: "downgrade-key", + Short: "Downgrade swarm node key from PKCS8 to PKCS1", + RunE: func(cmd *cobra.Command, args []string) error { + stateDir, err := cmd.Flags().GetString("state-dir") + if err != nil { + return err + } + + unlockKey, err := cmd.Flags().GetString("unlock-key") + if err != nil { + return err + } + + return downgradeKey(stateDir, unlockKey) + }, + } ) func init() { @@ -150,6 +168,7 @@ func init() { dumpWALCmd, dumpSnapshotCmd, dumpObjectCmd, + downgradeKeyCmd, ) dumpSnapshotCmd.Flags().Bool("redact", false, "Redact the values of secrets, configs, and environment variables") diff --git a/integration/cluster.go b/integration/cluster.go index 72b98b374e..c08e7bbdaa 100644 --- a/integration/cluster.go +++ b/integration/cluster.go @@ -1,17 +1,21 @@ package integration import ( + "crypto/tls" "fmt" "math/rand" + "net" "sync" "time" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" "github.com/docker/swarmkit/identity" "github.com/docker/swarmkit/log" + "github.com/docker/swarmkit/manager/encryption" "github.com/docker/swarmkit/node" "github.com/docker/swarmkit/testutils" "github.com/sirupsen/logrus" @@ -409,3 +413,61 @@ func (c *testCluster) RotateRootCA(cert, key []byte) error { return err }, opsTimeout) } + +func (c *testCluster) RotateUnlockKey() error { + // poll in case something else changes the cluster before we can update it + return testutils.PollFuncWithTimeout(nil, func() error { + clusterInfo, err := c.GetClusterInfo() + if err != nil { + return err + } + _, err = c.api.UpdateCluster(context.Background(), &api.UpdateClusterRequest{ + ClusterID: clusterInfo.ID, + Spec: &clusterInfo.Spec, + ClusterVersion: &clusterInfo.Meta.Version, + Rotation: api.KeyRotation{ + ManagerUnlockKey: true, + }, + }) + return err + }, opsTimeout) +} + +func (c *testCluster) AutolockManagers(autolock bool) error { + // poll in case something else changes the cluster before we can update it + return testutils.PollFuncWithTimeout(nil, func() error { + clusterInfo, err := c.GetClusterInfo() + if err != nil { + return err + } + newSpec := clusterInfo.Spec.Copy() + newSpec.EncryptionConfig.AutoLockManagers = autolock + _, err = c.api.UpdateCluster(context.Background(), &api.UpdateClusterRequest{ + ClusterID: clusterInfo.ID, + Spec: newSpec, + ClusterVersion: &clusterInfo.Meta.Version, + }) + return err + }, opsTimeout) +} + +func (c *testCluster) GetUnlockKey() (string, error) { + opts := []grpc.DialOption{} + insecureCreds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}) + opts = append(opts, grpc.WithTransportCredentials(insecureCreds)) + opts = append(opts, grpc.WithDialer( + func(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("unix", addr, timeout) + })) + conn, err := grpc.Dial(c.RandomManager().config.ListenControlAPI, opts...) + if err != nil { + return "", err + } + + resp, err := api.NewCAClient(conn).GetUnlockKey(context.Background(), &api.GetUnlockKeyRequest{}) + if err != nil { + return "", err + } + + return encryption.HumanReadableKey(resp.UnlockKey), nil +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 040f528540..b559df986e 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -20,6 +20,7 @@ import ( events "github.com/docker/go-events" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" + "github.com/docker/swarmkit/ca/keyutils" cautils "github.com/docker/swarmkit/ca/testutils" "github.com/docker/swarmkit/identity" "github.com/docker/swarmkit/manager" @@ -165,6 +166,19 @@ func newCluster(t *testing.T, numWorker, numManager int) *testCluster { return cl } +func newClusterWithRootCA(t *testing.T, numWorker, numManager int, rootCA *ca.RootCA) *testCluster { + cl := newTestCluster(t.Name()) + for i := 0; i < numManager; i++ { + require.NoError(t, cl.AddManager(false, rootCA), "manager number %d", i+1) + } + for i := 0; i < numWorker; i++ { + require.NoError(t, cl.AddAgent(), "agent number %d", i+1) + } + + pollClusterReady(t, cl, numWorker, numManager) + return cl +} + func TestClusterCreate(t *testing.T) { t.Parallel() @@ -251,6 +265,52 @@ func TestNodeOps(t *testing.T) { pollClusterReady(t, cl, numWorker, numManager) } +func TestAutolockManagers(t *testing.T) { + t.Parallel() + + // run this twice, once with root ca with pkcs1 key and then pkcs8 key + defer os.Unsetenv(keyutils.FIPSEnvVar) + for _, pkcs1 := range []bool{true, false} { + if pkcs1 { + os.Unsetenv(keyutils.FIPSEnvVar) + } else { + os.Setenv(keyutils.FIPSEnvVar, "1") + } + + rootCA, err := ca.CreateRootCA("rootCN") + require.NoError(t, err) + numWorker, numManager := 1, 1 + cl := newClusterWithRootCA(t, numWorker, numManager, &rootCA) + defer func() { + require.NoError(t, cl.Stop()) + }() + + // check that the cluster is not locked initially + unlockKey, err := cl.GetUnlockKey() + require.NoError(t, err) + require.Equal(t, "SWMKEY-1-", unlockKey) + + // lock the cluster and make sure the unlock key is not empty + require.NoError(t, cl.AutolockManagers(true)) + unlockKey, err = cl.GetUnlockKey() + require.NoError(t, err) + require.NotEqual(t, "SWMKEY-1-", unlockKey) + + // rotate unlock key + require.NoError(t, cl.RotateUnlockKey()) + newUnlockKey, err := cl.GetUnlockKey() + require.NoError(t, err) + require.NotEqual(t, "SWMKEY-1-", newUnlockKey) + require.NotEqual(t, unlockKey, newUnlockKey) + + // unlock the cluster + require.NoError(t, cl.AutolockManagers(false)) + unlockKey, err = cl.GetUnlockKey() + require.NoError(t, err) + require.Equal(t, "SWMKEY-1-", unlockKey) + } +} + func TestDemotePromote(t *testing.T) { t.Parallel() @@ -560,102 +620,116 @@ func pollRootRotationDone(t *testing.T, cl *testCluster) { func TestSuccessfulRootRotation(t *testing.T) { t.Parallel() - numWorker, numManager := 2, 3 - cl := newCluster(t, numWorker, numManager) - defer func() { - require.NoError(t, cl.Stop()) - }() - pollClusterReady(t, cl, numWorker, numManager) - // Take down one of managers and both workers, so we can't actually ever finish root rotation. - resp, err := cl.api.ListNodes(context.Background(), &api.ListNodesRequest{}) - require.NoError(t, err) - var ( - downManagerID string - downWorkerIDs []string - oldTLSInfo *api.NodeTLSInfo - ) - for _, n := range resp.Nodes { - if oldTLSInfo != nil { - require.Equal(t, oldTLSInfo, n.Description.TLSInfo) + // run this twice, once with root ca with pkcs1 key and then pkcs8 key + defer os.Unsetenv(keyutils.FIPSEnvVar) + for _, pkcs1 := range []bool{true, false} { + if pkcs1 { + os.Unsetenv(keyutils.FIPSEnvVar) } else { - oldTLSInfo = n.Description.TLSInfo - } - if n.Role == api.NodeRoleManager { - if !n.ManagerStatus.Leader && downManagerID == "" { - downManagerID = n.ID - require.NoError(t, cl.nodes[n.ID].Pause(false)) - } - continue + os.Setenv(keyutils.FIPSEnvVar, "1") } - downWorkerIDs = append(downWorkerIDs, n.ID) - require.NoError(t, cl.nodes[n.ID].Pause(false)) - } - // perform a root rotation, and wait until all the nodes that are up have newly issued certs - newRootCert, newRootKey, err := cautils.CreateRootCertAndKey("newRootCN") - require.NoError(t, err) - require.NoError(t, cl.RotateRootCA(newRootCert, newRootKey)) + rootCA, err := ca.CreateRootCA("rootCN") + require.NoError(t, err) - require.NoError(t, testutils.PollFuncWithTimeout(nil, func() error { + numWorker, numManager := 2, 3 + cl := newClusterWithRootCA(t, numWorker, numManager, &rootCA) + defer func() { + require.NoError(t, cl.Stop()) + }() + pollClusterReady(t, cl, numWorker, numManager) + + // Take down one of managers and both workers, so we can't actually ever finish root rotation. resp, err := cl.api.ListNodes(context.Background(), &api.ListNodesRequest{}) - if err != nil { - return err - } + require.NoError(t, err) + var ( + downManagerID string + downWorkerIDs []string + oldTLSInfo *api.NodeTLSInfo + ) for _, n := range resp.Nodes { - isDown := n.ID == downManagerID || n.ID == downWorkerIDs[0] || n.ID == downWorkerIDs[1] - if reflect.DeepEqual(n.Description.TLSInfo, oldTLSInfo) != isDown { - return fmt.Errorf("expected TLS info to have changed: %v", !isDown) + if oldTLSInfo != nil { + require.Equal(t, oldTLSInfo, n.Description.TLSInfo) + } else { + oldTLSInfo = n.Description.TLSInfo } + if n.Role == api.NodeRoleManager { + if !n.ManagerStatus.Leader && downManagerID == "" { + downManagerID = n.ID + require.NoError(t, cl.nodes[n.ID].Pause(false)) + } + continue + } + downWorkerIDs = append(downWorkerIDs, n.ID) + require.NoError(t, cl.nodes[n.ID].Pause(false)) } - // root rotation isn't done - clusterInfo, err := cl.GetClusterInfo() - if err != nil { - return err - } - require.NotNil(t, clusterInfo.RootCA.RootRotation) // if root rotation is already done, fail and finish the test here - return nil - }, opsTimeout)) - - // Bring the other manager back. Also bring one worker back, kill the other worker, - // and add a new worker - show that we can converge on a root rotation. - require.NoError(t, cl.StartNode(downManagerID)) - require.NoError(t, cl.StartNode(downWorkerIDs[0])) - require.NoError(t, cl.RemoveNode(downWorkerIDs[1], false)) - require.NoError(t, cl.AddAgent()) - - // we can finish root rotation even though the previous leader was down because it had - // already rotated its cert - pollRootRotationDone(t, cl) + // perform a root rotation, and wait until all the nodes that are up have newly issued certs + newRootCert, newRootKey, err := cautils.CreateRootCertAndKey("newRootCN") + require.NoError(t, err) + require.NoError(t, cl.RotateRootCA(newRootCert, newRootKey)) - // wait until all the nodes have gotten their new certs and trust roots - require.NoError(t, testutils.PollFuncWithTimeout(nil, func() error { - resp, err = cl.api.ListNodes(context.Background(), &api.ListNodesRequest{}) - if err != nil { - return err - } - var newTLSInfo *api.NodeTLSInfo - for _, n := range resp.Nodes { - if newTLSInfo == nil { - newTLSInfo = n.Description.TLSInfo - if bytes.Equal(newTLSInfo.CertIssuerPublicKey, oldTLSInfo.CertIssuerPublicKey) || - bytes.Equal(newTLSInfo.CertIssuerSubject, oldTLSInfo.CertIssuerSubject) { - return errors.New("expecting the issuer to have changed") - } - if !bytes.Equal(newTLSInfo.TrustRoot, newRootCert) { - return errors.New("expecting the the root certificate to have changed") + require.NoError(t, testutils.PollFuncWithTimeout(nil, func() error { + resp, err := cl.api.ListNodes(context.Background(), &api.ListNodesRequest{}) + if err != nil { + return err + } + for _, n := range resp.Nodes { + isDown := n.ID == downManagerID || n.ID == downWorkerIDs[0] || n.ID == downWorkerIDs[1] + if reflect.DeepEqual(n.Description.TLSInfo, oldTLSInfo) != isDown { + return fmt.Errorf("expected TLS info to have changed: %v", !isDown) } - } else if !reflect.DeepEqual(newTLSInfo, n.Description.TLSInfo) { - return fmt.Errorf("the nodes have not converged yet, particularly %s", n.ID) } - if n.Certificate.Status.State != api.IssuanceStateIssued { - return errors.New("nodes have yet to finish renewing their TLS certificates") + // root rotation isn't done + clusterInfo, err := cl.GetClusterInfo() + if err != nil { + return err } - } - return nil - }, opsTimeout)) + require.NotNil(t, clusterInfo.RootCA.RootRotation) // if root rotation is already done, fail and finish the test here + return nil + }, opsTimeout)) + + // Bring the other manager back. Also bring one worker back, kill the other worker, + // and add a new worker - show that we can converge on a root rotation. + require.NoError(t, cl.StartNode(downManagerID)) + require.NoError(t, cl.StartNode(downWorkerIDs[0])) + require.NoError(t, cl.RemoveNode(downWorkerIDs[1], false)) + require.NoError(t, cl.AddAgent()) + + // we can finish root rotation even though the previous leader was down because it had + // already rotated its cert + pollRootRotationDone(t, cl) + + // wait until all the nodes have gotten their new certs and trust roots + require.NoError(t, testutils.PollFuncWithTimeout(nil, func() error { + resp, err = cl.api.ListNodes(context.Background(), &api.ListNodesRequest{}) + if err != nil { + return err + } + var newTLSInfo *api.NodeTLSInfo + for _, n := range resp.Nodes { + if newTLSInfo == nil { + newTLSInfo = n.Description.TLSInfo + if bytes.Equal(newTLSInfo.CertIssuerPublicKey, oldTLSInfo.CertIssuerPublicKey) || + bytes.Equal(newTLSInfo.CertIssuerSubject, oldTLSInfo.CertIssuerSubject) { + return errors.New("expecting the issuer to have changed") + } + if !bytes.Equal(newTLSInfo.TrustRoot, newRootCert) { + return errors.New("expecting the the root certificate to have changed") + } + } else if !reflect.DeepEqual(newTLSInfo, n.Description.TLSInfo) { + return fmt.Errorf("the nodes have not converged yet, particularly %s", n.ID) + } + + if n.Certificate.Status.State != api.IssuanceStateIssued { + return errors.New("nodes have yet to finish renewing their TLS certificates") + } + } + return nil + }, opsTimeout)) + } } func TestRepeatedRootRotation(t *testing.T) { diff --git a/manager/deks_test.go b/manager/deks_test.go index fdff015f74..4873b05c62 100644 --- a/manager/deks_test.go +++ b/manager/deks_test.go @@ -406,16 +406,15 @@ func TestRaftDEKManagerMaybeUpdateKEK(t *testing.T) { // determine if the KEK is valid. func TestDecryptTLSKeyFalsePositive(t *testing.T) { badKey := []byte(` ------BEGIN EC PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-256-CBC,e7927e79e748233776c03c2eb7275f09 +-----BEGIN ENCRYPTED PRIVATE KEY----- kek-version: 392 raft-dek: CAESMBrzZ0gNVPe3FRs42743q8RtkUBrK1ICQpHWX2vdQ8iqSKt1WoKdFDFD2r28LYAVLxoYQguwHbijMx9k+BALUNBAI3s199S5tvnr -JfGenNvzm++AvsOh+UmcBY+JgI6lnfzaCB68agmlmEZYLYi5tqtAU7gif6VIJpCW -+Pj23Fzkw8sKKOOBeapSC5lp+Cjx9OsCci/R9xrdx+uxnnzKJNxOB/qzqcQfZDMh -id2LxdliFcPEk/Yj5gNGpT0UMFJ4G52enbOwOru46f0= ------END EC PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQge1soOUock01aIHDn +QGz2uSNlS0fFdTIYmqKkzjefLNWgCgYIKoZIzj0DAQehRANCAARjorw9uRP83LqU +RUHSjimzx0vTMeyZVIZVp5dIkdCuVYVSFF41B7ffBrl+oA47OMlMxCkhsWD7EmJZ +xvc0Km0E +-----END ENCRYPTED PRIVATE KEY----- `) // not actually a real swarm cert - generated a cert corresponding to the key that expires in 20 years diff --git a/manager/manager.go b/manager/manager.go index 1ce727dcac..84716b3217 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -2,7 +2,6 @@ package manager import ( "crypto/tls" - "crypto/x509" "encoding/pem" "fmt" "net" @@ -13,12 +12,12 @@ import ( "syscall" "time" - "github.com/cloudflare/cfssl/helpers" "github.com/docker/docker/pkg/plugingetter" "github.com/docker/go-events" gmetrics "github.com/docker/go-metrics" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" + "github.com/docker/swarmkit/ca/keyutils" "github.com/docker/swarmkit/connectionbroker" "github.com/docker/swarmkit/identity" "github.com/docker/swarmkit/log" @@ -817,10 +816,10 @@ func (m *Manager) rotateRootCAKEK(ctx context.Context, clusterID string) error { return fmt.Errorf("invalid PEM-encoded private key inside of cluster %s", clusterID) } - if x509.IsEncryptedPEMBlock(keyBlock) { + if keyutils.IsEncryptedPEMBlock(keyBlock) { // PEM encryption does not have a digest, so sometimes decryption doesn't // error even with the wrong passphrase. So actually try to parse it into a valid key. - _, err := helpers.ParsePrivateKeyPEMWithPassword(privKeyPEM, []byte(passphrase)) + _, err := keyutils.ParsePrivateKeyPEMWithPassword(privKeyPEM, []byte(passphrase)) if err == nil { // This key is already correctly encrypted with the correct KEK, nothing to do here return nil @@ -829,7 +828,7 @@ func (m *Manager) rotateRootCAKEK(ctx context.Context, clusterID string) error { // This key is already encrypted, but failed with current main passphrase. // Let's try to decrypt with the previous passphrase, and parse into a valid key, for the // same reason as above. - _, err = helpers.ParsePrivateKeyPEMWithPassword(privKeyPEM, []byte(passphrasePrev)) + _, err = keyutils.ParsePrivateKeyPEMWithPassword(privKeyPEM, []byte(passphrasePrev)) if err != nil { // We were not able to decrypt either with the main or backup passphrase, error return err @@ -837,7 +836,7 @@ func (m *Manager) rotateRootCAKEK(ctx context.Context, clusterID string) error { // ok the above passphrase is correct, so decrypt the PEM block so we can re-encrypt - // since the key was successfully decrypted above, there will be no error doing PEM // decryption - unencryptedDER, _ := x509.DecryptPEMBlock(keyBlock, []byte(passphrasePrev)) + unencryptedDER, _ := keyutils.DecryptPEMBlock(keyBlock, []byte(passphrasePrev)) unencryptedKeyBlock := &pem.Block{ Type: keyBlock.Type, Bytes: unencryptedDER, diff --git a/manager/manager_test.go b/manager/manager_test.go index bf72ef1b30..8fc4c30d37 100644 --- a/manager/manager_test.go +++ b/manager/manager_test.go @@ -3,7 +3,6 @@ package manager import ( "bytes" "crypto/tls" - "crypto/x509" "encoding/pem" "errors" "fmt" @@ -20,6 +19,7 @@ import ( "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" + "github.com/docker/swarmkit/ca/keyutils" cautils "github.com/docker/swarmkit/ca/testutils" "github.com/docker/swarmkit/manager/dispatcher" "github.com/docker/swarmkit/manager/encryption" @@ -291,7 +291,7 @@ func TestManagerLockUnlock(t *testing.T) { require.NoError(t, err) keyBlock, _ := pem.Decode(key) require.NotNil(t, keyBlock) - require.False(t, x509.IsEncryptedPEMBlock(keyBlock)) + require.False(t, keyutils.IsEncryptedPEMBlock(keyBlock)) require.Len(t, keyBlock.Headers, 2) currentDEK, err := decodePEMHeaderValue(keyBlock.Headers[pemHeaderRaftDEK], nil) require.NoError(t, err) @@ -338,7 +338,7 @@ func TestManagerLockUnlock(t *testing.T) { keyBlock, _ = pem.Decode(updatedKey) require.NotNil(t, keyBlock) // this should never error due to atomic writes - if !x509.IsEncryptedPEMBlock(keyBlock) { + if !keyutils.IsEncryptedPEMBlock(keyBlock) { return fmt.Errorf("Key not encrypted") } @@ -409,7 +409,7 @@ func TestManagerLockUnlock(t *testing.T) { // but not rotated keyBlock, _ = pem.Decode(unlockedKey) require.NotNil(t, keyBlock) - require.False(t, x509.IsEncryptedPEMBlock(keyBlock)) + require.False(t, keyutils.IsEncryptedPEMBlock(keyBlock)) unencryptedDEK, err := decodePEMHeaderValue(keyBlock.Headers[pemHeaderRaftDEK], nil) require.NoError(t, err) @@ -505,10 +505,10 @@ func TestManagerEncryptsDecryptsRootKeyMaterial(t *testing.T) { if keyBlock == nil { return fmt.Errorf("could not pem decode root key") } - if !x509.IsEncryptedPEMBlock(keyBlock) { + if !keyutils.IsEncryptedPEMBlock(keyBlock) { return fmt.Errorf("root key material not encrypted yet") } - _, err = x509.DecryptPEMBlock(keyBlock, []byte("kek")) + _, err = keyutils.DecryptPEMBlock(keyBlock, []byte("kek")) return err }) }) @@ -540,7 +540,7 @@ func TestManagerEncryptsDecryptsRootKeyMaterial(t *testing.T) { if keyBlock == nil { return fmt.Errorf("could not pem decode root key") } - if x509.IsEncryptedPEMBlock(keyBlock) { + if keyutils.IsEncryptedPEMBlock(keyBlock) { return fmt.Errorf("root key material not decrypted yet") } return nil @@ -558,14 +558,13 @@ func TestManagerEncryptsDecryptsRootKeyMaterial(t *testing.T) { return fmt.Errorf("cluster gone") } cluster.RootCA.CAKey = []byte(` ------BEGIN EC PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-256-CBC,fcc97c79c251d2fedeab96a19f3b826e - -8IHMsMKfCMWXDpBNLp7tyuwUQ1FmisiPyDZg9UvoX4RvIDUxj7sIiw4lsP+EgnKG -09oKeXHSYRpawB58dvLqxPtjnrEj1jLqoMydTrhRDJ+zBMxPxpTJh/BASADhMOmf -G80TfNRRr/qdB9hLwfyOyk2tBipkAgs6cl+CZAaqx3k= ------END EC PRIVATE KEY----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiLGJtiTmJ3rQICCAAw +HQYJYIZIAWUDBAEqBBBeDoliB0Qe73DdcMeFCuRzBIGQP/iFMPj9BJ/81GV//fMp +KPozbY0EWodXt7KArbeROd5+uWw1muLANUa3KkkXyQhmzlR2Zv3Y/kBuPay9RweU +md94ZD/HY9K+ISv4tIA7u8gp2Hqr0elfG0QqBuwrh688ZF5jii6umZzXtLVVMvWd +NF7w1CA6b8w1aTIklVjv0AJ9tgtGQb9phVigPAdyyw6v +-----END ENCRYPTED PRIVATE KEY----- `) return store.UpdateCluster(tx, cluster) })) diff --git a/node/node.go b/node/node.go index adad42b722..d7b6011207 100644 --- a/node/node.go +++ b/node/node.go @@ -971,6 +971,15 @@ func (n *Node) superviseManager(ctx context.Context, securityConfig *ca.Security } } +// DowngradeKey reverts the node key to older format so that it can +// run on older version of swarmkit +func (n *Node) DowngradeKey() error { + paths := ca.NewConfigPaths(filepath.Join(n.config.StateDir, certDirectory)) + krw := ca.NewKeyReadWriter(paths.Node, n.config.UnlockKey, nil) + + return krw.DowngradeKey() +} + type persistentRemotes struct { sync.RWMutex c *sync.Cond diff --git a/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go b/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go new file mode 100644 index 0000000000..593f653008 --- /dev/null +++ b/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go @@ -0,0 +1,77 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC +2898 / PKCS #5 v2.0. + +A key derivation function is useful when encrypting data based on a password +or any other not-fully-random data. It uses a pseudorandom function to derive +a secure encryption key based on the password. + +While v2.0 of the standard defines only one pseudorandom function to use, +HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved +Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To +choose, you can pass the `New` functions from the different SHA packages to +pbkdf2.Key. +*/ +package pbkdf2 // import "golang.org/x/crypto/pbkdf2" + +import ( + "crypto/hmac" + "hash" +) + +// Key derives a key from the password, salt and iteration count, returning a +// []byte of length keylen that can be used as cryptographic key. The key is +// derived based on the method described as PBKDF2 with the HMAC variant using +// the supplied hash function. +// +// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you +// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by +// doing: +// +// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New) +// +// Remember to get a good random salt. At least 8 bytes is recommended by the +// RFC. +// +// Using a higher iteration count will increase the cost of an exhaustive +// search but will also make derivation proportionally slower. +func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { + prf := hmac.New(h, password) + hashLen := prf.Size() + numBlocks := (keyLen + hashLen - 1) / hashLen + + var buf [4]byte + dk := make([]byte, 0, numBlocks*hashLen) + U := make([]byte, hashLen) + for block := 1; block <= numBlocks; block++ { + // N.B.: || means concatenation, ^ means XOR + // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter + // U_1 = PRF(password, salt || uint(i)) + prf.Reset() + prf.Write(salt) + buf[0] = byte(block >> 24) + buf[1] = byte(block >> 16) + buf[2] = byte(block >> 8) + buf[3] = byte(block) + prf.Write(buf[:4]) + dk = prf.Sum(dk) + T := dk[len(dk)-hashLen:] + copy(U, T) + + // U_n = PRF(password, U_(n-1)) + for n := 2; n <= iter; n++ { + prf.Reset() + prf.Write(U) + U = U[:0] + U = prf.Sum(U) + for x := range U { + T[x] ^= U[x] + } + } + } + return dk[:keyLen] +}