Skip to content

Commit 8f580a3

Browse files
kruskallericywl
andauthored
test: add basic job for fips system-tests (#16174)
* test: add basic job for fips system-tests * test: simplify systemtest apm-server build func * test: update certificates for fips compliance * test: generate subject key id for fips compliance * test: update TLS protocol error message * Update tls_test.go * Update command.go * Update command.go * test: remove unnecessary files and update tls tests * Update tls_nofips_test.go * Update systemtest/apmservertest/server.go Co-authored-by: Eric <22215921+ericywl@users.noreply.github.com> --------- Co-authored-by: Eric <22215921+ericywl@users.noreply.github.com>
1 parent 7c595fd commit 8f580a3

File tree

9 files changed

+189
-106
lines changed

9 files changed

+189
-106
lines changed

.github/workflows/ci.yml

+19
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@ jobs:
9696
GH_TOKEN: ${{ github.token }}
9797
run: make system-test
9898

99+
system-test-fips:
100+
runs-on: ubuntu-latest
101+
steps:
102+
- uses: actions/checkout@v4
103+
- uses: actions/setup-go@v5
104+
with:
105+
go-version-file: systemtest/go.mod
106+
cache: true
107+
cache-dependency-path: |
108+
go.sum
109+
systemtest/go.sum
110+
- run: docker compose up -d
111+
- env:
112+
GOTESTFLAGS: "-v -tags=requirefips"
113+
GOFIPS140: "latest"
114+
GODEBUG: "fips140=only"
115+
GH_TOKEN: ${{ github.token }}
116+
run: make system-test
117+
99118
test-package:
100119
runs-on: ubuntu-latest
101120
steps:

systemtest/apmservertest/cert.pem

-14
This file was deleted.

systemtest/apmservertest/client_cert.pem

-18
This file was deleted.

systemtest/apmservertest/client_key.pem

-28
This file was deleted.

systemtest/apmservertest/command.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package apmservertest
1919

2020
import (
2121
"context"
22+
"crypto/fips140"
2223
"fmt"
2324
"log"
2425
"os"
@@ -168,15 +169,22 @@ func BuildServerBinary(goos, goarch string) (string, error) {
168169
if err != nil {
169170
return "", err
170171
}
171-
relpath := filepath.Join("build", fmt.Sprintf("apm-server-%s-%s", goos, goarch))
172+
name := "apm-server"
173+
abspath := filepath.Join(repoRoot, "build", fmt.Sprintf("apm-server-%s-%s", goos, goarch))
174+
if fips140.Enabled() {
175+
abspath += "-fips"
176+
name += "-fips"
177+
}
172178
if goos == "windows" {
173-
relpath += ".exe"
179+
abspath += ".exe"
174180
}
175-
abspath := filepath.Join(repoRoot, relpath)
176181

177-
log.Println("Building apm-server...")
178-
cmd := exec.Command("make", relpath)
182+
log.Printf("Building %s...", name)
183+
cmd := exec.Command("make", name)
179184
cmd.Dir = repoRoot
185+
cmd.Env = append(cmd.Env, os.Environ()...)
186+
cmd.Env = append(cmd.Env, "NOCP=1") // prevent race condition
187+
cmd.Env = append(cmd.Env, "GOOS="+goos, "GOARCH="+goarch)
180188
cmd.Stdout = os.Stdout
181189
cmd.Stderr = os.Stderr
182190
if err := cmd.Run(); err != nil {

systemtest/apmservertest/key.pem

-15
This file was deleted.

systemtest/apmservertest/server.go

+92-11
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,24 @@ package apmservertest
2020
import (
2121
"bytes"
2222
"context"
23+
"crypto/ecdsa"
24+
"crypto/elliptic"
25+
"crypto/rand"
26+
"crypto/sha256"
2327
"crypto/tls"
2428
"crypto/x509"
29+
"crypto/x509/pkix"
2530
"encoding/json"
31+
"encoding/pem"
2632
"errors"
2733
"fmt"
2834
"io"
35+
"math/big"
2936
"net"
3037
"net/http"
3138
"net/url"
3239
"os"
3340
"os/exec"
34-
"path/filepath"
3541
"strings"
3642
"sync"
3743
"testing"
@@ -60,6 +66,11 @@ type Server struct {
6066
// The temporary directory will be removed when the server is closed.
6167
Dir string
6268

69+
// CertDir is the directory of the TLS certificates.
70+
//
71+
// If Dir is empty the current directory is used
72+
CertDir string
73+
6374
// Log holds an optional io.Writer to which the process's Stderr will
6475
// be written, in addition to being available through the Server.Logs
6576
// field.
@@ -125,6 +136,7 @@ func NewServerTB(tb testing.TB, args ...string) *Server {
125136
// will be written under apm-server/systemtest/logs/<test-name>/.
126137
func NewUnstartedServerTB(tb testing.TB, args ...string) *Server {
127138
s := NewUnstartedServer(args...)
139+
s.CertDir = tb.TempDir()
128140
logfile := createLogfile(tb, "apm-server")
129141
s.Log = logfile
130142
tb.Cleanup(func() {
@@ -253,14 +265,8 @@ func (s *Server) start(tls bool) error {
253265
}
254266

255267
func (s *Server) initTLS() (serverCertPath, serverKeyPath, caCertPath string, _ error) {
256-
repoRoot, err := getRepoRoot()
257-
if err != nil {
258-
panic(err)
259-
}
260-
268+
serverCertPath, serverKeyPath, err := generateCerts(s.CertDir, true, x509.ExtKeyUsageServerAuth, "127.0.0.1", "::1")
261269
// Load a self-signed server certificate for testing TLS encryption.
262-
serverCertPath = filepath.Join(repoRoot, "systemtest", "apmservertest", "cert.pem")
263-
serverKeyPath = filepath.Join(repoRoot, "systemtest", "apmservertest", "key.pem")
264270
serverCertBytes, err := os.ReadFile(serverCertPath)
265271
if err != nil {
266272
return "", "", "", err
@@ -270,9 +276,12 @@ func (s *Server) initTLS() (serverCertPath, serverKeyPath, caCertPath string, _
270276
panic("failed to add CA certificate to cert pool")
271277
}
272278

279+
clientCertPath, clientKeyPath, err := generateCerts(s.CertDir, false, x509.ExtKeyUsageClientAuth, "")
280+
if err != nil {
281+
return "", "", "", err
282+
}
283+
273284
// Load a self-signed client certificate for testing TLS client certificate auth.
274-
clientCertPath := filepath.Join(repoRoot, "systemtest", "apmservertest", "client_cert.pem")
275-
clientKeyPath := filepath.Join(repoRoot, "systemtest", "apmservertest", "client_key.pem")
276285
clientCertBytes, err := os.ReadFile(clientCertPath)
277286
if err != nil {
278287
return "", "", "", err
@@ -289,11 +298,83 @@ func (s *Server) initTLS() (serverCertPath, serverKeyPath, caCertPath string, _
289298
s.TLS = &tls.Config{
290299
Certificates: []tls.Certificate{clientCert},
291300
RootCAs: certpool,
292-
MinVersion: tls.VersionTLS12,
293301
}
294302
return serverCertPath, serverKeyPath, clientCertPath, nil
295303
}
296304

305+
func generateCerts(dir string, ca bool, keyUsage x509.ExtKeyUsage, hosts ...string) (string, string, error) {
306+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
307+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
308+
if err != nil {
309+
return "", "", fmt.Errorf("Failed to generate serial number: %w", err)
310+
}
311+
notBefore := time.Now()
312+
notAfter := notBefore.Add(24 * time.Hour)
313+
template := x509.Certificate{
314+
SerialNumber: serialNumber,
315+
Subject: pkix.Name{
316+
Organization: []string{"Org"},
317+
},
318+
NotBefore: notBefore,
319+
NotAfter: notAfter,
320+
321+
KeyUsage: x509.KeyUsageDigitalSignature,
322+
ExtKeyUsage: []x509.ExtKeyUsage{keyUsage},
323+
BasicConstraintsValid: true,
324+
}
325+
326+
for _, h := range hosts {
327+
if ip := net.ParseIP(h); ip != nil {
328+
template.IPAddresses = append(template.IPAddresses, ip)
329+
}
330+
}
331+
332+
if ca {
333+
template.IsCA = true
334+
template.KeyUsage |= x509.KeyUsageCertSign
335+
}
336+
337+
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
338+
if err != nil {
339+
return "", "", fmt.Errorf("failed to generate client key: %w", err)
340+
}
341+
privBytes, err := x509.MarshalPKCS8PrivateKey(clientKey)
342+
if err != nil {
343+
return "", "", fmt.Errorf("unable to marshal private key: %w", err)
344+
}
345+
346+
h := sha256.Sum256(privBytes)
347+
template.SubjectKeyId = h[:]
348+
349+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, clientKey.Public(), clientKey)
350+
if err != nil {
351+
return "", "", fmt.Errorf("failed to create certificate: %w", err)
352+
}
353+
certOut, err := os.CreateTemp(dir, "client_cert.pem")
354+
if err != nil {
355+
return "", "", fmt.Errorf("failed to open client_cert.pem for writing: %w", err)
356+
}
357+
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
358+
return "", "", fmt.Errorf("failed to write data to client_cert.pem: %w", err)
359+
}
360+
if err := certOut.Close(); err != nil {
361+
return "", "", fmt.Errorf("error closing client_cert.pem: %w", err)
362+
}
363+
364+
keyOut, err := os.CreateTemp(dir, "client_key.pem")
365+
if err != nil {
366+
return "", "", fmt.Errorf("failed to open client_key.pem for writing: %w", err)
367+
}
368+
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
369+
return "", "", fmt.Errorf("failed to write data to client_key.pem: %w", err)
370+
}
371+
if err := keyOut.Close(); err != nil {
372+
return "", "", fmt.Errorf("error closing client_key.pem: %w", err)
373+
}
374+
375+
return certOut.Name(), keyOut.Name(), nil
376+
}
377+
297378
func (s *Server) printCmdline(w io.Writer, args []string) {
298379
var buf bytes.Buffer
299380
fmt.Fprint(&buf, "# Running apm-server\n")

systemtest/tls_nofips_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 systemtest_test
21+
22+
import (
23+
"crypto/tls"
24+
"net/http"
25+
"testing"
26+
27+
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/require"
29+
30+
"github.com/elastic/apm-server/systemtest/apmservertest"
31+
)
32+
33+
func TestDefaultIncompatibleTLSConfig(t *testing.T) {
34+
srv := apmservertest.NewUnstartedServerTB(t)
35+
require.NoError(t, srv.StartTLS())
36+
37+
attemptRequest := func(t *testing.T, minVersion, maxVersion uint16, cipherSuites ...uint16) error {
38+
tlsConfig := &tls.Config{RootCAs: srv.TLS.RootCAs}
39+
tlsConfig.MinVersion = minVersion
40+
tlsConfig.MaxVersion = maxVersion
41+
tlsConfig.CipherSuites = cipherSuites
42+
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
43+
resp, err := httpClient.Get(srv.URL)
44+
if err != nil {
45+
return err
46+
}
47+
defer resp.Body.Close()
48+
return nil
49+
}
50+
51+
t.Run("incompatible_cipher_suite", func(t *testing.T) {
52+
err := attemptRequest(t, tls.VersionTLS12, tls.VersionTLS12, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA)
53+
require.Error(t, err)
54+
assert.Regexp(t, ".*tls: handshake failure", err.Error())
55+
})
56+
57+
t.Run("incompatible_protocol", func(t *testing.T) {
58+
err := attemptRequest(t, tls.VersionTLS10, tls.VersionTLS10)
59+
require.Error(t, err)
60+
assert.Regexp(t, ".*tls: protocol version not supported", err.Error())
61+
})
62+
}

0 commit comments

Comments
 (0)