Skip to content

Commit 6ff829d

Browse files
Enable GODEBUG=fips140=only tests in buildkite. (#302)
Enable unit tests with GODEBUG=fips140=only and -tags=requirefips in buildkite. This is a basic check to make sure we don't accidently add non-compliant crypto when in FIPS mode. When these test are ran, the encrypted private key tests in transport/tlscommon are skipped, the -tags=requirefips tests check that an error is returned, however if fips140=only is set the test will panic. Add a new V2 file keystore that is used with requirefips. This implementation uses NewGCMWithRandomNonce. File keystore support is currently disabled in FIPS artifacts we produce.
1 parent 63b0cac commit 6ff829d

16 files changed

+600
-124
lines changed

.buildkite/pipeline.yml

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

26+
# Run unit tests with requirefips tag to validate functionality
2627
- label: ":linux: Test Linux FIPS"
2728
key: test-lin-fips
2829
command:
@@ -36,6 +37,19 @@ steps:
3637
artifact_paths:
3738
- "junit-*.xml"
3839

40+
# Run unit tests with requirefips tag and GODEBUG=fips140=only
41+
# This is a check against accidentally adding crypto that breaks FIPS compliance.
42+
- label: ":linux: Test Linux fips140=only"
43+
key: test-lin-fipsonly
44+
command:
45+
- ".buildkite/scripts/test-fipsonly.sh"
46+
agents:
47+
image: golang:${GO_VERSION}
48+
cpu: "8"
49+
memory: "4G"
50+
artifact_paths:
51+
- "junit-*.xml"
52+
3953
- label: ":windows: Test Windows"
4054
key: test-win
4155
command:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
junitfile=$1 # filename for jnit annotation plugin
3+
4+
set -euo pipefail
5+
6+
echo "--- Pre install"
7+
source .buildkite/scripts/pre-install-command.sh
8+
go version
9+
add_bin_path
10+
with_go_junit_report
11+
12+
echo "--- Go Test fips140=only"
13+
set +e
14+
GODEBUG=fips140=only go test -tags=integration,requirefips -json -race -v ./... > test-fips-report.json
15+
exit_code=$?
16+
set -e
17+
18+
# Create Junit report for junit annotation plugin
19+
go-junit-report -parser gojson > "${junitfile:-junit-report-fips-linux.xml}" < test-fips-report.json
20+
exit $exit_code

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/elastic/elastic-agent-libs
22

3-
go 1.24.1
3+
go 1.23
44

55
require (
66
github.com/Microsoft/go-winio v0.5.2

keystore/file_keystore.go

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ package keystore
1919

2020
import (
2121
"bytes"
22-
"crypto/aes"
23-
"crypto/cipher"
2422
"crypto/rand"
2523
"encoding/base64"
2624
"encoding/json"
@@ -45,9 +43,6 @@ const (
4543
keyLength = 32
4644
)
4745

48-
// Version of the keystore format, will be added at the beginning of the file.
49-
var version = []byte("v1")
50-
5146
// Packager defines a keystore that we can read the raw bytes and be packaged in an artifact.
5247
type Packager interface {
5348
Package() ([]byte, error)
@@ -322,95 +317,6 @@ func (k *FileKeystore) load() error {
322317
return jsonDecoder.Decode(&k.secrets)
323318
}
324319

325-
// Encrypt the data payload using a derived keys and the AES-256-GCM algorithm.
326-
func (k *FileKeystore) encrypt(reader io.Reader) (io.Reader, error) {
327-
// randomly generate the salt and the initialization vector, this information will be saved
328-
// on disk in the file as part of the header
329-
iv, err := randomBytes(iVLength)
330-
331-
if err != nil {
332-
return nil, err
333-
}
334-
335-
salt, err := randomBytes(saltLength)
336-
if err != nil {
337-
return nil, err
338-
}
339-
340-
// Stretch the user provided key
341-
password, _ := k.password.Get()
342-
passwordBytes, err := k.hashPassword(string(password), salt)
343-
if err != nil {
344-
return nil, fmt.Errorf("could not hash password, error: %w", err)
345-
}
346-
347-
// Select AES-256: because len(passwordBytes) == 32 bytes
348-
block, err := aes.NewCipher(passwordBytes)
349-
if err != nil {
350-
return nil, fmt.Errorf("could not create the keystore cipher to encrypt, error: %w", err)
351-
}
352-
353-
aesgcm, err := cipher.NewGCM(block)
354-
if err != nil {
355-
return nil, fmt.Errorf("could not create the keystore cipher to encrypt, error: %w", err)
356-
}
357-
358-
data, err := io.ReadAll(reader)
359-
if err != nil {
360-
return nil, fmt.Errorf("could not read unencrypted data, error: %w", err)
361-
}
362-
363-
encodedBytes := aesgcm.Seal(nil, iv, data, nil)
364-
365-
// Generate the payload with all the additional information required to decrypt the
366-
// output format of the document: VERSION|SALT|IV|PAYLOAD
367-
buf := bytes.NewBuffer(salt)
368-
buf.Write(iv)
369-
buf.Write(encodedBytes)
370-
371-
return buf, nil
372-
}
373-
374-
// should receive an io.reader...
375-
func (k *FileKeystore) decrypt(reader io.Reader) (io.Reader, error) {
376-
data, err := io.ReadAll(reader)
377-
if err != nil {
378-
return nil, fmt.Errorf("could not read all the data from the encrypted file, error: %w", err)
379-
}
380-
381-
if len(data) < saltLength+iVLength+1 {
382-
return nil, fmt.Errorf("missing information in the file for decrypting the keystore")
383-
}
384-
385-
// extract the necessary information to decrypt the data from the data payload
386-
salt := data[0:saltLength]
387-
iv := data[saltLength : saltLength+iVLength]
388-
encodedBytes := data[saltLength+iVLength:]
389-
390-
password, _ := k.password.Get()
391-
passwordBytes, err := k.hashPassword(string(password), salt)
392-
if err != nil {
393-
return nil, fmt.Errorf("could not hash password, error: %w", err)
394-
}
395-
396-
block, err := aes.NewCipher(passwordBytes)
397-
if err != nil {
398-
return nil, fmt.Errorf("could not create the keystore cipher to decrypt the data: %w", err)
399-
}
400-
401-
aesgcm, err := cipher.NewGCM(block)
402-
if err != nil {
403-
return nil, fmt.Errorf("could not create the keystore cipher to decrypt the data: %w", err)
404-
}
405-
406-
decodedBytes, err := aesgcm.Open(nil, iv, encodedBytes, nil)
407-
if err != nil {
408-
return nil, fmt.Errorf("could not decrypt keystore data: %w", err)
409-
}
410-
411-
return bytes.NewReader(decodedBytes), nil
412-
}
413-
414320
// checkPermission enforces permission on the keystore file itself, the file should have strict
415321
// permission (0600) and the keystore should refuses to start if its not the case.
416322
func (k *FileKeystore) checkPermissions(f string) error {

keystore/file_keystore_fips.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 go1.24 && requirefips
19+
20+
package keystore
21+
22+
import (
23+
"bytes"
24+
"crypto/aes"
25+
"crypto/cipher"
26+
"fmt"
27+
"io"
28+
)
29+
30+
// Version of the keystore format, will be added at the beginning of the file.
31+
var version = []byte("v2")
32+
33+
// Encrypt the data payload using a derived keys and the AES-256-GCM algorithm.
34+
func (k *FileKeystore) encrypt(reader io.Reader) (io.Reader, error) {
35+
salt, err := randomBytes(saltLength)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
// Stretch the user provided key
41+
password, _ := k.password.Get()
42+
passwordBytes, err := k.hashPassword(string(password), salt)
43+
if err != nil {
44+
return nil, fmt.Errorf("could not hash password, error: %w", err)
45+
}
46+
47+
// Select AES-256: because len(passwordBytes) == 32 bytes
48+
block, err := aes.NewCipher(passwordBytes)
49+
if err != nil {
50+
return nil, fmt.Errorf("could not create the keystore cipher to encrypt, error: %w", err)
51+
}
52+
53+
aesgcm, err := cipher.NewGCMWithRandomNonce(block)
54+
if err != nil {
55+
return nil, fmt.Errorf("could not create the keystore cipher to encrypt, error: %w", err)
56+
}
57+
58+
data, err := io.ReadAll(reader)
59+
if err != nil {
60+
return nil, fmt.Errorf("could not read unencrypted data, error: %w", err)
61+
}
62+
63+
encodedBytes := aesgcm.Seal(nil, nil, data, nil)
64+
65+
// Generate the payload with all the additional information required to decrypt the
66+
// output format of the document: VERSION|SALT|PAYLOAD
67+
buf := bytes.NewBuffer(salt)
68+
buf.Write(encodedBytes)
69+
70+
return buf, nil
71+
}
72+
73+
// should receive an io.reader...
74+
func (k *FileKeystore) decrypt(reader io.Reader) (io.Reader, error) {
75+
data, err := io.ReadAll(reader)
76+
if err != nil {
77+
return nil, fmt.Errorf("could not read all the data from the encrypted file, error: %w", err)
78+
}
79+
80+
if len(data) < saltLength+1 {
81+
return nil, fmt.Errorf("missing information in the file for decrypting the keystore")
82+
}
83+
84+
// extract the necessary information to decrypt the data from the data payload
85+
salt := data[0:saltLength]
86+
encodedBytes := data[saltLength:]
87+
88+
password, _ := k.password.Get()
89+
passwordBytes, err := k.hashPassword(string(password), salt)
90+
if err != nil {
91+
return nil, fmt.Errorf("could not hash password, error: %w", err)
92+
}
93+
94+
block, err := aes.NewCipher(passwordBytes)
95+
if err != nil {
96+
return nil, fmt.Errorf("could not create the keystore cipher to decrypt the data: %w", err)
97+
}
98+
99+
aesgcm, err := cipher.NewGCMWithRandomNonce(block)
100+
if err != nil {
101+
return nil, fmt.Errorf("could not create the keystore cipher to decrypt the data: %w", err)
102+
}
103+
104+
decodedBytes, err := aesgcm.Open(nil, nil, encodedBytes, nil)
105+
if err != nil {
106+
return nil, fmt.Errorf("could not decrypt keystore data: %w", err)
107+
}
108+
109+
return bytes.NewReader(decodedBytes), nil
110+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 go1.24 && requirefips
19+
20+
package keystore
21+
22+
import (
23+
"fmt"
24+
"os"
25+
"path/filepath"
26+
"testing"
27+
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
30+
)
31+
32+
func TestShouldRaiseAndErrorWhenVersionDontMatch(t *testing.T) {
33+
temporaryPath := GetTemporaryKeystoreFile(t)
34+
defer os.Remove(temporaryPath)
35+
36+
badVersion := `v1pqH8nRJNCuKLrAHwATQuHpdLcP84sATrxtKMWTvapZTRcoEODVJKf2dsHXiOhSMh1EFrJTikON2oF5wZv4IM37lkJ6wt79MCFaXDqlNxBQtIA9w6vaxWnbS+92rQqtka7WrzTxal1Pd3mcK0o+ow7EAJg553UvxBqA==`
37+
38+
f, err := os.OpenFile(temporaryPath, os.O_CREATE|os.O_WRONLY, 0600)
39+
require.NoError(t, err)
40+
_, _ = f.WriteString(badVersion)
41+
err = f.Close()
42+
require.NoError(t, err)
43+
44+
_, err = NewFileKeystoreWithPassword(temporaryPath, NewSecureString([]byte("")))
45+
if assert.Error(t, err, "Expect version check error") {
46+
assert.Equal(t, err, fmt.Errorf("keystore format doesn't match expected version: 'v2' got 'v1'"))
47+
}
48+
}
49+
50+
func TestOpensV2(t *testing.T) {
51+
ks, err := NewFileKeystoreWithPassword(filepath.Join("testdata", "keystore.v2"), NewSecureString([]byte("")))
52+
require.NoError(t, err)
53+
ls, err := AsListingKeystore(ks)
54+
require.NoError(t, err)
55+
keys, err := ls.List()
56+
require.NoError(t, err)
57+
require.Len(t, keys, 1)
58+
require.Equal(t, keys[0], "key")
59+
}
60+
61+
func TestFailsToOpenV1(t *testing.T) {
62+
_, err := NewFileKeystoreWithPassword(filepath.Join("testdata", "keystore.v1"), NewSecureString([]byte("")))
63+
require.Error(t, err)
64+
}

0 commit comments

Comments
 (0)