Skip to content

Commit 0ab6544

Browse files
Add FIPS TLS encrypted private key tests (#281)
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]>
1 parent 55121c5 commit 0ab6544

File tree

11 files changed

+443
-555
lines changed

11 files changed

+443
-555
lines changed

.buildkite/pipeline.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ steps:
2323
artifact_paths:
2424
- "junit-*.xml"
2525

26+
- label: ":linux: Test Linux FIPS"
27+
key: test-lin-fips
28+
command:
29+
- ".buildkite/scripts/test.sh"
30+
env:
31+
FIPS: "true"
32+
agents:
33+
image: golang:${GO_VERSION}
34+
cpu: "8"
35+
memory: "4G"
36+
artifact_paths:
37+
- "junit-*.xml"
38+
2639
- label: ":windows: Test Windows"
2740
key: test-win
2841
command:
@@ -53,6 +66,8 @@ steps:
5366
depends_on:
5467
- step: "test-lin"
5568
allow_failure: true
69+
- step: "test-lin-fips"
70+
allow_failure: true
5671
- step: "test-win"
5772
allow_failure: true
5873
- step: "test-mac"

.buildkite/scripts/test.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ go version
99
add_bin_path
1010
with_go_junit_report
1111

12+
tags=integration
13+
if [[ "${FIPS:-false}" == "true" ]]; then
14+
tags="${tags},requirefips"
15+
fi
16+
1217
echo "--- Go Test"
1318
set +e
14-
go test -tags integration -race -v ./... > tests-report.txt
19+
go test -tags=${tags} -race -v ./... > tests-report.txt
1520
exit_code=$?
1621
set -e
1722

transport/tlscommon/diag_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import (
2424
"encoding/pem"
2525
"testing"
2626

27-
"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
2827
"github.com/stretchr/testify/require"
28+
29+
"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
2930
)
3031

3132
const verificationDefault = "verification_mode=full"

transport/tlscommon/server_config_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ package tlscommon
2020
import (
2121
"testing"
2222

23-
"github.com/elastic/go-ucfg"
2423
"github.com/stretchr/testify/require"
2524
"gopkg.in/yaml.v2"
25+
26+
"github.com/elastic/go-ucfg"
2627
)
2728

2829
// variables so we can use pointers in tests

transport/tlscommon/tls.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
101101
return nil, err
102102
}
103103

104+
var errs error
104105
for len(content) > 0 {
105106
var block *pem.Block
106107

@@ -117,13 +118,15 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
117118
block, err := decryptPKCS1Key(*block, pass)
118119
if err != nil {
119120
log.Errorf("Dropping encrypted pem block with private key, block type '%s': %s", block.Type, err)
121+
errs = errors.Join(errs, err)
120122
continue
121123
}
122124
blocks = append(blocks, &block)
123125
case block.Type == "ENCRYPTED PRIVATE KEY":
124126
block, err := decryptPKCS8Key(*block, pass)
125127
if err != nil {
126-
log.Errorf("Dropping encrypted pem block with private key, block type '%s', could not decypt as PKCS8: %s", block.Type, err)
128+
log.Errorf("Dropping encrypted pem block with private key, block type '%s', could not decrypt as PKCS8: %s", block.Type, err)
129+
errs = errors.Join(errs, err)
127130
continue
128131
}
129132
blocks = append(blocks, &block)
@@ -133,7 +136,7 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) {
133136
}
134137

135138
if len(blocks) == 0 {
136-
return nil, errors.New("no PEM blocks")
139+
return nil, errors.Join(errors.New("no PEM blocks"), errs)
137140
}
138141

139142
// re-encode available, decrypted blocks

transport/tlscommon/tls_config_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import (
2626
"net/url"
2727
"testing"
2828

29-
"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
3029
"github.com/stretchr/testify/assert"
3130
"github.com/stretchr/testify/require"
31+
32+
"github.com/elastic/elastic-agent-libs/transport/tlscommontest"
3233
)
3334

3435
func TestMakeVerifyServerConnection(t *testing.T) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//go:build requirefips
19+
20+
package tlscommon
21+
22+
import (
23+
"errors"
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
// TestFIPSCertifacteAndKeys tests that encrypted private keys fail in FIPS mode
31+
func TestFIPSCertificateAndKeys(t *testing.T) {
32+
t.Run("embed encrypted PKCS#1 key", func(t *testing.T) {
33+
// Create a dummy configuration and append the CA after.
34+
password := "abcd1234"
35+
key, cert := makeKeyCertPair(t, blockTypePKCS1Encrypted, password)
36+
cfg, err := load(`enabled: true`)
37+
require.NoError(t, err)
38+
cfg.Certificate.Certificate = cert
39+
cfg.Certificate.Key = key
40+
cfg.Certificate.Passphrase = password
41+
42+
_, err = LoadTLSConfig(cfg)
43+
require.Error(t, err)
44+
assert.ErrorIs(t, err, errors.ErrUnsupported, err)
45+
})
46+
47+
t.Run("embed encrypted PKCS#8 key", func(t *testing.T) {
48+
// Create a dummy configuration and append the CA after.
49+
password := "abcd1234"
50+
key, cert := makeKeyCertPair(t, blockTypePKCS8Encrypted, password)
51+
cfg, err := load(`enabled: true`)
52+
require.NoError(t, err)
53+
cfg.Certificate.Certificate = cert
54+
cfg.Certificate.Key = key
55+
cfg.Certificate.Passphrase = password
56+
57+
_, err = LoadTLSConfig(cfg)
58+
assert.ErrorIs(t, err, errors.ErrUnsupported)
59+
})
60+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//go:build !requirefips
19+
20+
package tlscommon
21+
22+
import (
23+
"fmt"
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
// TestNoFIPSCertificateAndKeys tests that encrypted private keys are supported in none FIPS mode
31+
func TestNoFIPSCertificateAndKeys(t *testing.T) {
32+
t.Run("embed encrypted PKCS#1 key", func(t *testing.T) {
33+
// Create a dummy configuration and append the CA after.
34+
password := "abcd1234"
35+
key, cert := makeKeyCertPair(t, blockTypePKCS1Encrypted, password)
36+
cfg, err := load(`enabled: true`)
37+
require.NoError(t, err)
38+
cfg.Certificate.Certificate = cert
39+
cfg.Certificate.Key = key
40+
cfg.Certificate.Passphrase = password
41+
42+
tlsC, err := LoadTLSConfig(cfg)
43+
require.NoError(t, err)
44+
assert.NotNil(t, tlsC)
45+
})
46+
47+
t.Run("embed PKCS#8 key", func(t *testing.T) {
48+
// Create a dummy configuration and append the CA after.
49+
password := "abcd1234"
50+
key, cert := makeKeyCertPair(t, blockTypePKCS8Encrypted, password)
51+
cfg, err := load(`enabled: true`)
52+
require.NoError(t, err)
53+
cfg.Certificate.Certificate = cert
54+
cfg.Certificate.Key = key
55+
cfg.Certificate.Passphrase = password
56+
57+
tlsC, err := LoadTLSConfig(cfg)
58+
require.NoError(t, err)
59+
assert.NotNil(t, tlsC)
60+
})
61+
}
62+
63+
func TestEncryptedKeyPassphrase(t *testing.T) {
64+
const passphrase = "Abcd1234!" // passphrase for testdata/ca.encrypted.key
65+
t.Run("no passphrase", func(t *testing.T) {
66+
_, err := LoadTLSConfig(mustLoad(t, `
67+
enabled: true
68+
certificate: testdata/ca.crt
69+
key: testdata/ca.encrypted.key
70+
`))
71+
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
72+
})
73+
74+
t.Run("wrong passphrase", func(t *testing.T) {
75+
_, err := LoadTLSConfig(mustLoad(t, `
76+
enabled: true
77+
certificate: testdata/ca.crt
78+
key: testdata/ca.encrypted.key
79+
key_passphrase: "abcd1234!"
80+
`))
81+
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
82+
})
83+
84+
t.Run("passphrase value", func(t *testing.T) {
85+
cfg, err := LoadTLSConfig(mustLoad(t, `
86+
enabled: true
87+
certificate: testdata/ca.crt
88+
key: testdata/ca.encrypted.key
89+
key_passphrase: Abcd1234!
90+
`))
91+
require.NoError(t, err)
92+
assert.Equal(t, 1, len(cfg.Certificates), "expected 1 certificate to be loaded")
93+
})
94+
95+
t.Run("passphrase file", func(t *testing.T) {
96+
fileName := writeTestFile(t, passphrase)
97+
cfg, err := LoadTLSConfig(mustLoad(t, fmt.Sprintf(`
98+
enabled: true
99+
certificate: testdata/ca.crt
100+
key: testdata/ca.encrypted.key
101+
key_passphrase_path: %s
102+
`, fileName)))
103+
require.NoError(t, err)
104+
assert.Equal(t, 1, len(cfg.Certificates), "expected 1 certificate to be loaded")
105+
})
106+
107+
t.Run("passphrase file empty", func(t *testing.T) {
108+
fileName := writeTestFile(t, "")
109+
_, err := LoadTLSConfig(mustLoad(t, fmt.Sprintf(`
110+
enabled: true
111+
certificate: testdata/ca.crt
112+
key: testdata/ca.encrypted.key
113+
key_passphrase_path: %s
114+
`, fileName)))
115+
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
116+
})
117+
}

0 commit comments

Comments
 (0)