Skip to content

Commit

Permalink
Add FIPS TLS encrypted private key tests (#281)
Browse files Browse the repository at this point in the history
Add a test to ensure that when if FIPS mode attempting to decrypt an encrypted private key will result in errors.ErrUnsuported. Change the ReadPEM method to return a joined error so that an encrypted block will return an error instead of just having the "no PEM blocks" error.
Change tests to generate a majority of their test cert+key pairs.

---------

Co-authored-by: simitt <[email protected]>
  • Loading branch information
michel-laterman and simitt authored Feb 21, 2025
1 parent 55121c5 commit 0ab6544
Show file tree
Hide file tree
Showing 11 changed files with 443 additions and 555 deletions.
15 changes: 15 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ steps:
artifact_paths:
- "junit-*.xml"

- label: ":linux: Test Linux FIPS"
key: test-lin-fips
command:
- ".buildkite/scripts/test.sh"
env:
FIPS: "true"
agents:
image: golang:${GO_VERSION}
cpu: "8"
memory: "4G"
artifact_paths:
- "junit-*.xml"

- label: ":windows: Test Windows"
key: test-win
command:
Expand Down Expand Up @@ -53,6 +66,8 @@ steps:
depends_on:
- step: "test-lin"
allow_failure: true
- step: "test-lin-fips"
allow_failure: true
- step: "test-win"
allow_failure: true
- step: "test-mac"
Expand Down
7 changes: 6 additions & 1 deletion .buildkite/scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ go version
add_bin_path
with_go_junit_report

tags=integration
if [[ "${FIPS:-false}" == "true" ]]; then
tags="${tags},requirefips"
fi

echo "--- Go Test"
set +e
go test -tags integration -race -v ./... > tests-report.txt
go test -tags=${tags} -race -v ./... > tests-report.txt
exit_code=$?
set -e

Expand Down
3 changes: 2 additions & 1 deletion transport/tlscommon/diag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
"encoding/pem"
"testing"

"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
"github.com/stretchr/testify/require"

"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
)

const verificationDefault = "verification_mode=full"
Expand Down
3 changes: 2 additions & 1 deletion transport/tlscommon/server_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ package tlscommon
import (
"testing"

"github.com/elastic/go-ucfg"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"

"github.com/elastic/go-ucfg"
)

// variables so we can use pointers in tests
Expand Down
7 changes: 5 additions & 2 deletions transport/tlscommon/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
return nil, err
}

var errs error
for len(content) > 0 {
var block *pem.Block

Expand All @@ -117,13 +118,15 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
block, err := decryptPKCS1Key(*block, pass)
if err != nil {
log.Errorf("Dropping encrypted pem block with private key, block type '%s': %s", block.Type, err)
errs = errors.Join(errs, err)
continue
}
blocks = append(blocks, &block)
case block.Type == "ENCRYPTED PRIVATE KEY":
block, err := decryptPKCS8Key(*block, pass)
if err != nil {
log.Errorf("Dropping encrypted pem block with private key, block type '%s', could not decypt as PKCS8: %s", block.Type, err)
log.Errorf("Dropping encrypted pem block with private key, block type '%s', could not decrypt as PKCS8: %s", block.Type, err)
errs = errors.Join(errs, err)
continue
}
blocks = append(blocks, &block)
Expand All @@ -133,7 +136,7 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
}

if len(blocks) == 0 {
return nil, errors.New("no PEM blocks")
return nil, errors.Join(errors.New("no PEM blocks"), errs)
}

// re-encode available, decrypted blocks
Expand Down
3 changes: 2 additions & 1 deletion transport/tlscommon/tls_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import (
"net/url"
"testing"

"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
)

func TestMakeVerifyServerConnection(t *testing.T) {
Expand Down
60 changes: 60 additions & 0 deletions transport/tlscommon/tls_fips_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build requirefips

package tlscommon

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestFIPSCertifacteAndKeys tests that encrypted private keys fail in FIPS mode
func TestFIPSCertificateAndKeys(t *testing.T) {
t.Run("embed encrypted PKCS#1 key", func(t *testing.T) {
// Create a dummy configuration and append the CA after.
password := "abcd1234"
key, cert := makeKeyCertPair(t, blockTypePKCS1Encrypted, password)
cfg, err := load(`enabled: true`)
require.NoError(t, err)
cfg.Certificate.Certificate = cert
cfg.Certificate.Key = key
cfg.Certificate.Passphrase = password

_, err = LoadTLSConfig(cfg)
require.Error(t, err)
assert.ErrorIs(t, err, errors.ErrUnsupported, err)
})

t.Run("embed encrypted PKCS#8 key", func(t *testing.T) {
// Create a dummy configuration and append the CA after.
password := "abcd1234"
key, cert := makeKeyCertPair(t, blockTypePKCS8Encrypted, password)
cfg, err := load(`enabled: true`)
require.NoError(t, err)
cfg.Certificate.Certificate = cert
cfg.Certificate.Key = key
cfg.Certificate.Passphrase = password

_, err = LoadTLSConfig(cfg)
assert.ErrorIs(t, err, errors.ErrUnsupported)
})
}
117 changes: 117 additions & 0 deletions transport/tlscommon/tls_nofips_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build !requirefips

package tlscommon

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestNoFIPSCertificateAndKeys tests that encrypted private keys are supported in none FIPS mode
func TestNoFIPSCertificateAndKeys(t *testing.T) {
t.Run("embed encrypted PKCS#1 key", func(t *testing.T) {
// Create a dummy configuration and append the CA after.
password := "abcd1234"
key, cert := makeKeyCertPair(t, blockTypePKCS1Encrypted, password)
cfg, err := load(`enabled: true`)
require.NoError(t, err)
cfg.Certificate.Certificate = cert
cfg.Certificate.Key = key
cfg.Certificate.Passphrase = password

tlsC, err := LoadTLSConfig(cfg)
require.NoError(t, err)
assert.NotNil(t, tlsC)
})

t.Run("embed PKCS#8 key", func(t *testing.T) {
// Create a dummy configuration and append the CA after.
password := "abcd1234"
key, cert := makeKeyCertPair(t, blockTypePKCS8Encrypted, password)
cfg, err := load(`enabled: true`)
require.NoError(t, err)
cfg.Certificate.Certificate = cert
cfg.Certificate.Key = key
cfg.Certificate.Passphrase = password

tlsC, err := LoadTLSConfig(cfg)
require.NoError(t, err)
assert.NotNil(t, tlsC)
})
}

func TestEncryptedKeyPassphrase(t *testing.T) {
const passphrase = "Abcd1234!" // passphrase for testdata/ca.encrypted.key
t.Run("no passphrase", func(t *testing.T) {
_, err := LoadTLSConfig(mustLoad(t, `
enabled: true
certificate: testdata/ca.crt
key: testdata/ca.encrypted.key
`))
assert.ErrorContains(t, err, "no PEM blocks") // ReadPEMFile will generate an internal "no passphrase available" error that is logged and the no PEM blocks error is returned instead
})

t.Run("wrong passphrase", func(t *testing.T) {
_, err := LoadTLSConfig(mustLoad(t, `
enabled: true
certificate: testdata/ca.crt
key: testdata/ca.encrypted.key
key_passphrase: "abcd1234!"
`))
assert.ErrorContains(t, err, "no PEM blocks") // ReadPEMFile will fail decrypting with x509.IncorrectPasswordError that will be logged and a no PEM blocks error is returned instead
})

t.Run("passphrase value", func(t *testing.T) {
cfg, err := LoadTLSConfig(mustLoad(t, `
enabled: true
certificate: testdata/ca.crt
key: testdata/ca.encrypted.key
key_passphrase: Abcd1234!
`))
require.NoError(t, err)
assert.Equal(t, 1, len(cfg.Certificates), "expected 1 certificate to be loaded")
})

t.Run("passphrase file", func(t *testing.T) {
fileName := writeTestFile(t, passphrase)
cfg, err := LoadTLSConfig(mustLoad(t, fmt.Sprintf(`
enabled: true
certificate: testdata/ca.crt
key: testdata/ca.encrypted.key
key_passphrase_path: %s
`, fileName)))
require.NoError(t, err)
assert.Equal(t, 1, len(cfg.Certificates), "expected 1 certificate to be loaded")
})

t.Run("passphrase file empty", func(t *testing.T) {
fileName := writeTestFile(t, "")
_, err := LoadTLSConfig(mustLoad(t, fmt.Sprintf(`
enabled: true
certificate: testdata/ca.crt
key: testdata/ca.encrypted.key
key_passphrase_path: %s
`, fileName)))
assert.ErrorContains(t, err, "no PEM blocks") // ReadPEMFile will generate an internal "no passphrase available" error that is logged and the no PEM blocks error is returned instead
})
}
Loading

0 comments on commit 0ab6544

Please sign in to comment.