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
1 change: 1 addition & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ func (a *Agent) nodeDescriptionWithHostname(ctx context.Context, tlsInfo *api.No
desc.Hostname = a.config.Hostname
}
desc.TLSInfo = tlsInfo
desc.FIPS = a.config.FIPS
}
return desc, err
}
Expand Down
7 changes: 6 additions & 1 deletion agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,17 +232,20 @@ func TestHandleSessionMessageNodeChanges(t *testing.T) {
require.Empty(t, closedSessions)
}

// when the node description changes, the session is restarted and propagated up to the dispatcher
// when the node description changes, the session is restarted and propagated up to the dispatcher.
// the node description includes the FIPSness of the agent.
func TestSessionRestartedOnNodeDescriptionChange(t *testing.T) {
tlsCh := make(chan events.Event, 1)
defer close(tlsCh)
tester := agentTestEnv(t, nil, tlsCh)
tester.agent.config.FIPS = true // start out with the agent in FIPS-enabled mode
defer tester.cleanup()
defer tester.StartAgent(t)()

currSession, closedSessions := tester.dispatcher.GetSessions()
require.NotNil(t, currSession)
require.NotNil(t, currSession.Description)
require.True(t, currSession.Description.FIPS)
require.Empty(t, closedSessions)

tester.executor.UpdateNodeDescription(&api.NodeDescription{
Expand All @@ -262,6 +265,7 @@ func TestSessionRestartedOnNodeDescriptionChange(t *testing.T) {
require.NotEqual(t, currSession, gotSession)
require.NotNil(t, gotSession.Description)
require.Equal(t, "testAgent", gotSession.Description.Hostname)
require.True(t, gotSession.Description.FIPS)
currSession = gotSession

// If nothing changes, the session is not re-established
Expand Down Expand Up @@ -291,6 +295,7 @@ func TestSessionRestartedOnNodeDescriptionChange(t *testing.T) {
require.NotNil(t, gotSession.Description)
require.Equal(t, "testAgent", gotSession.Description.Hostname)
require.Equal(t, newTLSInfo, gotSession.Description.TLSInfo)
require.True(t, gotSession.Description.FIPS)
}

// If the dispatcher returns an error, if it times out, or if it's unreachable, no matter
Expand Down
3 changes: 3 additions & 0 deletions agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type Config struct {
// SessionTracker, if provided, will have its SessionClosed and SessionError methods called
// when sessions close and error.
SessionTracker SessionTracker

// FIPS returns whether the node is FIPS-enabled
FIPS bool
}

func (c *Config) validate() error {
Expand Down
27 changes: 27 additions & 0 deletions api/api.pb.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2171,6 +2171,16 @@ file {
}
json_name: "tlsInfo"
}
field {
name: "fips"
number: 6
label: LABEL_OPTIONAL
type: TYPE_BOOL
options {
65004: "FIPS"
}
json_name: "fips"
}
}
message_type {
name: "NodeTLSInfo"
Expand Down Expand Up @@ -3928,6 +3938,13 @@ file {
66001: "NACLSecretboxSalsa20Poly1305"
}
}
value {
name: "FERNET_AES_128_CBC"
number: 2
options {
66001: "FernetAES128CBC"
}
}
}
}
message_type {
Expand Down Expand Up @@ -6150,6 +6167,16 @@ file {
type_name: ".docker.swarmkit.v1.EncryptionKey"
json_name: "unlockKeys"
}
field {
name: "fips"
number: 10
label: LABEL_OPTIONAL
type: TYPE_BOOL
options {
65004: "FIPS"
}
json_name: "fips"
}
nested_type {
name: "BlacklistedCertificatesEntry"
field {
Expand Down
234 changes: 137 additions & 97 deletions api/objects.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions api/objects.proto
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@ message Cluster {
// If the key is empty, the node will be unlocked (will not require a key
// to start up from a shut down state).
repeated EncryptionKey unlock_keys = 9;

// FIPS specifies whether this cluster should be in FIPS mode. This changes
// the format of the join tokens, and nodes that are not FIPS-enabled should
// reject joining the cluster. Nodes that report themselves to be non-FIPS
// should be rejected from the cluster.
bool fips = 10 [(gogoproto.customname) = "FIPS"];
}

// Secret represents a secret that should be passed to a container or a node,
Expand Down
677 changes: 360 additions & 317 deletions api/types.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions api/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ message NodeDescription {

// Information on the node's TLS setup
NodeTLSInfo tls_info = 5 [(gogoproto.customname) = "TLSInfo"];

// FIPS indicates whether the node has FIPS-enabled
bool fips = 6 [(gogoproto.customname) = "FIPS"];
}

message NodeTLSInfo {
Expand Down Expand Up @@ -1035,6 +1038,7 @@ message MaybeEncryptedRecord {
enum Algorithm {
NONE = 0 [(gogoproto.enumvalue_customname) = "NotEncrypted"];
SECRETBOX_SALSA20_POLY1305 = 1 [(gogoproto.enumvalue_customname) = "NACLSecretboxSalsa20Poly1305"];
FERNET_AES_128_CBC = 2 [(gogoproto.enumvalue_customname) = "FernetAES128CBC"];
}

Algorithm algorithm = 1;
Expand Down
75 changes: 3 additions & 72 deletions ca/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ 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"
Expand All @@ -51,13 +50,6 @@ const (
RootKeySize = 256
// RootKeyAlgo defines the default algorithm for the root CA Key
RootKeyAlgo = "ecdsa"
// PassphraseENVVar defines the environment variable to look for the
// root CA private key material encryption key
PassphraseENVVar = "SWARM_ROOT_CA_PASSPHRASE"
// PassphraseENVVarPrev defines the alternate environment variable to look for the
// root CA private key material encryption key. It can be used for seamless
// KEK rotations.
PassphraseENVVarPrev = "SWARM_ROOT_CA_PASSPHRASE_PREV"
// RootCAExpiration represents the default expiration for the root CA in seconds (20 years)
RootCAExpiration = "630720000s"
// DefaultNodeCertExpiration represents the default expiration for node certificates (3 months)
Expand Down Expand Up @@ -641,28 +633,10 @@ func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPo
return nil, errors.Wrap(err, "error while validating signing CA certificate against roots and intermediates")
}

var (
passphraseStr string
passphrase, passphrasePrev []byte
priv crypto.Signer
)

// Attempt two distinct passphrases, so we can do a hitless passphrase rotation
if passphraseStr = os.Getenv(PassphraseENVVar); passphraseStr != "" {
passphrase = []byte(passphraseStr)
}

if p := os.Getenv(PassphraseENVVarPrev); p != "" {
passphrasePrev = []byte(p)
}

// Attempt to decrypt the current private-key with the passphrases provided
priv, err = keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase)
// The key should not be encrypted, but it could be in PKCS8 format rather than PKCS1
priv, err := helpers.ParsePrivateKeyPEM(keyBytes)
if err != nil {
priv, err = keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev)
if err != nil {
return nil, errors.Wrap(err, "malformed private key")
}
return nil, errors.Wrap(err, "malformed private key")
}

// We will always use the first certificate inside of the root bundle as the active one
Expand All @@ -675,17 +649,6 @@ func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPo
return nil, err
}

// If the key was loaded from disk unencrypted, but there is a passphrase set,
// 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 != "" && !keyutils.IsEncryptedPEMBlock(keyBlock) {
keyBytes, err = EncryptECPrivateKey(keyBytes, passphraseStr)
if err != nil {
return nil, errors.Wrap(err, "unable to encrypt signing CA key material")
}
}

return &LocalSigner{Cert: certBytes, Key: keyBytes, Signer: signer, parsedCert: parsedCerts[0], cryptoSigner: priv}, nil
}

Expand Down Expand Up @@ -817,14 +780,6 @@ 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 @@ -976,30 +931,6 @@ func GenerateNewCSR() ([]byte, []byte, error) {
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)

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 := keyutils.EncryptPEMBlock(keyBlock.Bytes, passphrase)
if err != nil {
return nil, err
}

if encryptedPEMBlock.Headers == nil {
return nil, errors.New("unable to encrypt key - invalid PEM file produced")
}

return pem.EncodeToMemory(encryptedPEMBlock), nil
}

// NormalizePEMs takes a bundle of PEM-encoded certificates in a certificate bundle,
// decodes them, removes headers, and re-encodes them to make sure that they have
// consistent whitespace. Note that this is intended to normalize x509 certificates
Expand Down
104 changes: 0 additions & 104 deletions ca/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ 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 All @@ -44,9 +42,6 @@ import (
)

func init() {
os.Setenv(ca.PassphraseENVVar, "")
os.Setenv(ca.PassphraseENVVarPrev, "")

ca.RenewTLSExponentialBackoff = events.ExponentialBackoffConfig{
Base: 250 * time.Millisecond,
Factor: 250 * time.Millisecond,
Expand Down Expand Up @@ -82,31 +77,6 @@ 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 @@ -231,21 +201,6 @@ some random garbage\n
require.Error(t, err)
}

func TestEncryptECPrivateKey(t *testing.T) {
tempBaseDir, err := ioutil.TempDir("", "swarm-ca-test-")
assert.NoError(t, err)
defer os.RemoveAll(tempBaseDir)

_, key, err := ca.GenerateNewCSR()
assert.NoError(t, err)
encryptedKey, err := ca.EncryptECPrivateKey(key, "passphrase")
assert.NoError(t, err)

keyBlock, _ := pem.Decode(encryptedKey)
assert.NotNil(t, keyBlock)
assert.True(t, pkcs8.IsEncryptedPEMBlock(keyBlock))
}

func TestParseValidateAndSignCSR(t *testing.T) {
rootCA, err := ca.CreateRootCA("rootCN")
assert.NoError(t, err)
Expand Down Expand Up @@ -1331,65 +1286,6 @@ func TestRootCAWithCrossSignedIntermediates(t *testing.T) {
checkValidateAgainstAllRoots(tlsCert)
}

func TestNewRootCAWithPassphrase(t *testing.T) {
defer os.Setenv(ca.PassphraseENVVar, "")
defer os.Setenv(ca.PassphraseENVVarPrev, "")

rootCA, err := ca.CreateRootCA("rootCN")
assert.NoError(t, err)
rcaSigner, err := rootCA.Signer()
assert.NoError(t, err)

// Ensure that we're encrypting the Key bytes out of NewRoot if there
// is a passphrase set as an env Var
os.Setenv(ca.PassphraseENVVar, "password1")
newRootCA, err := ca.NewRootCA(rootCA.Certs, rcaSigner.Cert, rcaSigner.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
nrcaSigner, err := newRootCA.Signer()
assert.NoError(t, err)
assert.NotEqual(t, rcaSigner.Key, nrcaSigner.Key)
assert.Equal(t, rootCA.Certs, newRootCA.Certs)
assert.NotContains(t, string(rcaSigner.Key), string(nrcaSigner.Key))
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
anotherNewRootCA, err := ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
anrcaSigner, err := anotherNewRootCA.Signer()
assert.NoError(t, err)
assert.Equal(t, newRootCA, anotherNewRootCA)
assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key))
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
os.Setenv(ca.PassphraseENVVar, "password2")
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil)
assert.Error(t, err)

// Ensure that we cant decrypt the Key bytes out of NewRoot if there
// is a wrong passphrase set as an env Var
os.Setenv(ca.PassphraseENVVarPrev, "password2")
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil)
assert.Error(t, err)

// Ensure that we can decrypt the Key bytes out of NewRoot if there
// is a wrong passphrase set as an env Var, but a valid as Prev
os.Setenv(ca.PassphraseENVVarPrev, "password1")
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
assert.Equal(t, newRootCA, anotherNewRootCA)
assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key))
keyBlock, _ = pem.Decode(anrcaSigner.Key)
assert.NotNil(t, keyBlock)
assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock))
}

type certTestCase struct {
cert []byte
errorStr string
Expand Down
Loading