mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 14:58:33 +00:00
1604 lines
54 KiB
Go
1604 lines
54 KiB
Go
//go:build !windows
|
|
|
|
// Windows is disabled because the TPM simulator requires CGO, which causes lint failures on Windows.
|
|
|
|
package hostidentity
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
mathrand "math/rand/v2"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/ee/orbit/pkg/hostidentity"
|
|
orbitscep "github.com/fleetdm/fleet/v4/ee/orbit/pkg/scep"
|
|
"github.com/fleetdm/fleet/v4/ee/orbit/pkg/securehw"
|
|
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/types"
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttpsig"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/scep/x509util"
|
|
"github.com/fleetdm/fleet/v4/server/service/contract"
|
|
"github.com/google/go-tpm/tpm2/transport/simulator"
|
|
"github.com/remitly-oss/httpsig-go"
|
|
"github.com/rs/zerolog"
|
|
"github.com/smallstep/scep"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testEnrollmentSecret = "test_secret"
|
|
|
|
func TestHostIdentity(t *testing.T) {
|
|
s := SetUpSuiteWithConfig(t, "integrationtest.HostIdentity", false, func(cfg *config.FleetConfig) {
|
|
cfg.Osquery.EnrollCooldown = 0 // Disable rate limiting for tests
|
|
})
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, s *Suite)
|
|
}{
|
|
{"GetCertAndSignReq", testGetCertAndSignReq},
|
|
{"GetCertFailures", testGetCertFailures},
|
|
{"WrongCertAuthentication", testWrongCertAuthentication},
|
|
{"RealSecureHWAndSCEP", testRealSecureHWAndSCEP},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer mysql.TruncateTables(t, s.BaseSuite.DS, []string{
|
|
"host_identity_scep_serials", "host_identity_scep_certificates",
|
|
}...)
|
|
c.fn(t, s)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testGetCertAndSignReq(t *testing.T, s *Suite) {
|
|
t.Run("ECC P256, orbit", func(t *testing.T) {
|
|
t.Parallel()
|
|
cert, eccPrivateKey := testGetCertWithCurve(t, s, elliptic.P256())
|
|
nodeKey := testOrbitEnrollment(t, s, cert, eccPrivateKey)
|
|
testCertificateRenewal(t, s, cert, eccPrivateKey, nodeKey, false) // false = orbit
|
|
testDeleteHostAndReenroll(t, s, cert, eccPrivateKey, nodeKey)
|
|
})
|
|
|
|
t.Run("ECC P384, orbit", func(t *testing.T) {
|
|
t.Parallel()
|
|
cert, eccPrivateKey := testGetCertWithCurve(t, s, elliptic.P384())
|
|
nodeKey := testOrbitEnrollment(t, s, cert, eccPrivateKey)
|
|
testCertificateRenewal(t, s, cert, eccPrivateKey, nodeKey, false) // false = orbit
|
|
testDeleteHostAndReenroll(t, s, cert, eccPrivateKey, nodeKey)
|
|
})
|
|
|
|
t.Run("ECC P384, osquery", func(t *testing.T) {
|
|
t.Parallel()
|
|
cert, eccPrivateKey := testGetCertWithCurve(t, s, elliptic.P384())
|
|
nodeKey := testOsqueryEnrollment(t, s, cert, eccPrivateKey)
|
|
testCertificateRenewal(t, s, cert, eccPrivateKey, nodeKey, true) // true = osquery
|
|
testDeleteHostAndReenrollOsquery(t, s, cert, eccPrivateKey, nodeKey)
|
|
})
|
|
}
|
|
|
|
func generateRandomString(length int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
result := make([]byte, length)
|
|
for i := range result {
|
|
result[i] = charset[mathrand.IntN(len(charset))] // nolint:gosec // waive G404 since this is test code
|
|
}
|
|
return string(result)
|
|
}
|
|
|
|
func testGetCertWithCurve(t *testing.T, s *Suite, curve elliptic.Curve) (cert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey) {
|
|
ctx := t.Context()
|
|
// Create an enrollment secret
|
|
err := s.DS.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{
|
|
{
|
|
Secret: testEnrollmentSecret,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create ECC private key with specified curve
|
|
eccPrivateKey, err = ecdsa.GenerateKey(curve, rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Create SCEP client
|
|
scepURL := fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", s.Server.URL)
|
|
scepClient, err := scepclient.New(scepURL, s.Logger)
|
|
require.NoError(t, err)
|
|
|
|
// Get CA certificate
|
|
resp, _, err := scepClient.GetCACert(ctx, "")
|
|
require.NoError(t, err)
|
|
caCerts, err := x509.ParseCertificates(resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, caCerts)
|
|
|
|
// Create CSR using ECC key
|
|
hostIdentifier := generateRandomString(16)
|
|
csrTemplate := x509util.CertificateRequest{
|
|
CertificateRequest: x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: hostIdentifier,
|
|
},
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
},
|
|
ChallengePassword: testEnrollmentSecret,
|
|
}
|
|
|
|
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, eccPrivateKey)
|
|
require.NoError(t, err)
|
|
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
|
require.NoError(t, err)
|
|
|
|
tempRSAKey, deviceCert := createTempRSAKeyAndCert(t, hostIdentifier)
|
|
|
|
// Create SCEP PKI message
|
|
pkiMsgReq := &scep.PKIMessage{
|
|
MessageType: scep.PKCSReq,
|
|
Recipients: caCerts,
|
|
SignerKey: tempRSAKey, // Use RSA key for SCEP protocol
|
|
SignerCert: deviceCert,
|
|
}
|
|
|
|
msg, err := scep.NewCSRRequest(csr, pkiMsgReq, scep.WithLogger(s.Logger))
|
|
require.NoError(t, err)
|
|
|
|
// Send PKI operation request
|
|
respBytes, err := scepClient.PKIOperation(ctx, msg.Raw)
|
|
require.NoError(t, err)
|
|
|
|
// Parse response
|
|
pkiMsgResp, err := scep.ParsePKIMessage(respBytes, scep.WithLogger(s.Logger), scep.WithCACerts(msg.Recipients))
|
|
require.NoError(t, err)
|
|
|
|
// Verify successful response
|
|
require.Equal(t, scep.SUCCESS, pkiMsgResp.PKIStatus, "SCEP request should succeed")
|
|
|
|
// Decrypt PKI envelope using RSA key
|
|
err = pkiMsgResp.DecryptPKIEnvelope(deviceCert, tempRSAKey)
|
|
require.NoError(t, err)
|
|
|
|
// Verify we got a certificate
|
|
require.NotNil(t, pkiMsgResp.CertRepMessage)
|
|
require.NotNil(t, pkiMsgResp.CertRepMessage.Certificate)
|
|
|
|
// Verify the certificate was signed by the CA
|
|
cert = pkiMsgResp.CertRepMessage.Certificate
|
|
require.NotNil(t, cert)
|
|
|
|
// Verify certificate properties
|
|
assert.Equal(t, hostIdentifier, cert.Subject.CommonName)
|
|
assert.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm)
|
|
certPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "Certificate should contain ECC public key")
|
|
assert.True(t, eccPrivateKey.PublicKey.Equal(certPubKey), "Certificate public key should match our ECC private key")
|
|
assert.Equal(t, curve, certPubKey.Curve, "Certificate should use the expected elliptic curve")
|
|
|
|
// Retrieve the certificate from datastore and verify it matches SCEP response
|
|
storedCert, err := s.DS.GetHostIdentityCertBySerialNumber(ctx, cert.SerialNumber.Uint64())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, storedCert)
|
|
|
|
// Verify stored certificate properties match the SCEP response
|
|
assert.Equal(t, cert.SerialNumber.Uint64(), storedCert.SerialNumber)
|
|
assert.Equal(t, cert.Subject.CommonName, storedCert.CommonName)
|
|
assert.Equal(t, cert.NotAfter, storedCert.NotValidAfter)
|
|
|
|
// Verify the stored public key matches the certificate public key
|
|
storedPubKey, err := storedCert.UnmarshalPublicKey()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, storedPubKey)
|
|
assert.True(t, certPubKey.Equal(storedPubKey), "Stored public key should match certificate public key")
|
|
assert.Equal(t, curve, storedPubKey.Curve, "Stored public key should use the expected elliptic curve")
|
|
|
|
return cert, eccPrivateKey
|
|
}
|
|
|
|
// createHTTPSigner creates an HTTP signature signer for the given ECC private key and certificate
|
|
func createHTTPSigner(t *testing.T, eccPrivateKey *ecdsa.PrivateKey, cert *x509.Certificate) *httpsig.Signer {
|
|
// Determine the algorithm based on the curve
|
|
var algo httpsig.Algorithm
|
|
switch eccPrivateKey.Curve {
|
|
case elliptic.P256():
|
|
algo = httpsig.Algo_ECDSA_P256_SHA256
|
|
case elliptic.P384():
|
|
algo = httpsig.Algo_ECDSA_P384_SHA384
|
|
default:
|
|
t.Fatalf("Unsupported curve: %v", eccPrivateKey.Curve)
|
|
}
|
|
|
|
// Create signer
|
|
signer, err := fleethttpsig.Signer(
|
|
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
|
eccPrivateKey,
|
|
algo,
|
|
)
|
|
require.NoError(t, err)
|
|
return signer
|
|
}
|
|
|
|
func testOrbitEnrollment(t *testing.T, s *Suite, cert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey) string {
|
|
ctx := t.Context()
|
|
// Test orbit enrollment with the certificate
|
|
enrollRequest := contract.EnrollOrbitRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HardwareUUID: "test-uuid-" + cert.Subject.CommonName,
|
|
HardwareSerial: "test-serial-" + cert.Subject.CommonName,
|
|
Hostname: "test-hostname-" + cert.Subject.CommonName,
|
|
OsqueryIdentifier: cert.Subject.CommonName,
|
|
}
|
|
|
|
// This request is sent without an HTTP signature, so it should fail.
|
|
var enrollResp enrollOrbitResponse
|
|
s.DoJSON(t, "POST", "/api/fleet/orbit/enroll", enrollRequest, http.StatusUnauthorized, &enrollResp)
|
|
|
|
// Now send the same request with an HTTP signature
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Create signer using the shared helper
|
|
signer := createHTTPSigner(t, eccPrivateKey, cert)
|
|
|
|
// Sign the request
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
// Clone the request before sending it to preserve the body
|
|
clonedRequest := req.Clone(ctx)
|
|
clonedRequest.Body = io.NopCloser(bytes.NewReader(reqBody))
|
|
|
|
// Send the signed request
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// The request with a valid HTTP signature should succeed
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Request with HTTP signature should succeed")
|
|
|
|
// Parse the response
|
|
var signedEnrollResp enrollOrbitResponse
|
|
err = json.NewDecoder(httpResp.Body).Decode(&signedEnrollResp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, signedEnrollResp.OrbitNodeKey, "Should receive orbit node key")
|
|
require.NoError(t, signedEnrollResp.Err)
|
|
|
|
// Send the same request again. We don't have replay protection, so it should succeed.
|
|
httpResp, err = client.Do(clonedRequest)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Same request with HTTP signature should succeed")
|
|
|
|
// Parse the response
|
|
signedEnrollResp = enrollOrbitResponse{}
|
|
err = json.NewDecoder(httpResp.Body).Decode(&signedEnrollResp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, signedEnrollResp.OrbitNodeKey, "Should receive orbit node key")
|
|
require.NoError(t, signedEnrollResp.Err)
|
|
|
|
// Test /api/fleet/orbit/config endpoint with different signature scenarios
|
|
t.Run("config endpoint signature tests", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
setupRequest func() (*http.Request, error)
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "without signature",
|
|
setupRequest: func() (*http.Request, error) {
|
|
configReq := orbitConfigRequest{OrbitNodeKey: signedEnrollResp.OrbitNodeKey}
|
|
reqBody, err := json.Marshal(configReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return req, nil
|
|
},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
},
|
|
{
|
|
name: "with valid signature",
|
|
setupRequest: func() (*http.Request, error) {
|
|
configReq := orbitConfigRequest{OrbitNodeKey: signedEnrollResp.OrbitNodeKey}
|
|
reqBody, err := json.Marshal(configReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
err = signer.Sign(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "with corrupted signature",
|
|
setupRequest: func() (*http.Request, error) {
|
|
configReq := orbitConfigRequest{OrbitNodeKey: signedEnrollResp.OrbitNodeKey}
|
|
reqBody, err := json.Marshal(configReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with the correct signer first
|
|
err = signer.Sign(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Then corrupt the signature by modifying the signature header
|
|
sigHeader := req.Header.Get("Signature")
|
|
if sigHeader != "" {
|
|
// Corrupt the signature by changing the last character
|
|
corrupted := sigHeader[:len(sigHeader)-1] + "X"
|
|
req.Header.Set("Signature", corrupted)
|
|
}
|
|
return req, nil
|
|
},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req, err := tc.setupRequest()
|
|
require.NoError(t, err)
|
|
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, tc.expectedStatus, httpResp.StatusCode)
|
|
})
|
|
}
|
|
})
|
|
|
|
return signedEnrollResp.OrbitNodeKey
|
|
}
|
|
|
|
func testOsqueryEnrollment(t *testing.T, s *Suite, cert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey) string {
|
|
// Test osquery enrollment with the certificate
|
|
enrollRequest := contract.EnrollOsqueryAgentRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HostIdentifier: cert.Subject.CommonName,
|
|
HostDetails: map[string]map[string]string{
|
|
"osquery_info": {
|
|
"version": "5.0.0",
|
|
},
|
|
},
|
|
}
|
|
|
|
// This request is sent without an HTTP signature, so it should fail.
|
|
var enrollResp contract.EnrollOsqueryAgentResponse
|
|
s.DoJSON(t, "POST", "/api/v1/osquery/enroll", enrollRequest, http.StatusUnauthorized, &enrollResp)
|
|
|
|
// Now send the same request with HTTP message signature
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/osquery/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Create signer using the shared helper
|
|
signer := createHTTPSigner(t, eccPrivateKey, cert)
|
|
|
|
// Sign the request
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
// Send the signed request
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// The request with a valid HTTP signature should succeed
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Osquery enrollment with HTTP signature should succeed")
|
|
|
|
// Parse the response
|
|
enrollResp = contract.EnrollOsqueryAgentResponse{}
|
|
err = json.NewDecoder(httpResp.Body).Decode(&enrollResp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, enrollResp.NodeKey, "Should receive node key")
|
|
require.NoError(t, enrollResp.Err)
|
|
|
|
// Test /api/osquery/config endpoint with different signature scenarios
|
|
t.Run("osquery config endpoint signature tests", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
setupRequest func() (*http.Request, error)
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "without signature",
|
|
setupRequest: func() (*http.Request, error) {
|
|
configReq := osqueryConfigRequest{NodeKey: enrollResp.NodeKey}
|
|
reqBody, err := json.Marshal(configReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/osquery/config", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return req, nil
|
|
},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
},
|
|
{
|
|
name: "with valid signature",
|
|
setupRequest: func() (*http.Request, error) {
|
|
configReq := osqueryConfigRequest{NodeKey: enrollResp.NodeKey}
|
|
reqBody, err := json.Marshal(configReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/osquery/config", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
err = signer.Sign(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "with corrupted signature",
|
|
setupRequest: func() (*http.Request, error) {
|
|
configReq := osqueryConfigRequest{NodeKey: enrollResp.NodeKey}
|
|
reqBody, err := json.Marshal(configReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/osquery/config", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with the correct signer first
|
|
err = signer.Sign(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Then corrupt the signature by modifying the signature header
|
|
sigHeader := req.Header.Get("Signature")
|
|
if sigHeader != "" {
|
|
// Corrupt the signature by changing the last character
|
|
corrupted := sigHeader[:len(sigHeader)-1] + "X"
|
|
req.Header.Set("Signature", corrupted)
|
|
}
|
|
return req, nil
|
|
},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req, err := tc.setupRequest()
|
|
require.NoError(t, err)
|
|
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, tc.expectedStatus, httpResp.StatusCode)
|
|
})
|
|
}
|
|
})
|
|
|
|
return enrollResp.NodeKey
|
|
}
|
|
|
|
// testCertificateRenewal tests the SCEP certificate renewal flow with proof-of-possession
|
|
func testCertificateRenewal(t *testing.T, s *Suite, existingCert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey, nodeKey string, isOsquery bool) {
|
|
ctx := t.Context()
|
|
|
|
// Get the original certificate's host_id before renewal (it will get revoked)
|
|
originalStoredCert, err := s.DS.GetHostIdentityCertBySerialNumber(ctx, existingCert.SerialNumber.Uint64())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, originalStoredCert)
|
|
require.NotNil(t, originalStoredCert.HostID, "Original certificate should have host_id")
|
|
originalHostID := *originalStoredCert.HostID
|
|
|
|
// Generate a new ECC key pair for the renewed certificate
|
|
newEccPrivateKey, err := ecdsa.GenerateKey(eccPrivateKey.Curve, rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Create the renewal data
|
|
serialHex := fmt.Sprintf("0x%x", existingCert.SerialNumber.Bytes())
|
|
|
|
// Sign the message with the existing private key
|
|
hash := sha256.Sum256([]byte(serialHex))
|
|
signature, err := ecdsa.SignASN1(rand.Reader, eccPrivateKey, hash[:])
|
|
require.NoError(t, err)
|
|
|
|
renewalData := types.RenewalData{
|
|
SerialNumber: serialHex,
|
|
Signature: base64.StdEncoding.EncodeToString(signature),
|
|
}
|
|
|
|
renewalDataJSON, err := json.Marshal(renewalData)
|
|
require.NoError(t, err)
|
|
|
|
// Create CSR with renewal extension
|
|
csrTemplate := x509util.CertificateRequest{
|
|
CertificateRequest: x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: existingCert.Subject.CommonName,
|
|
},
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: types.RenewalExtensionOID,
|
|
Value: renewalDataJSON,
|
|
},
|
|
},
|
|
},
|
|
// No challenge password for renewal
|
|
}
|
|
|
|
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, newEccPrivateKey)
|
|
require.NoError(t, err)
|
|
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
|
require.NoError(t, err)
|
|
|
|
// Create SCEP client
|
|
scepURL := fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", s.Server.URL)
|
|
scepClient, err := scepclient.New(scepURL, s.Logger)
|
|
require.NoError(t, err)
|
|
|
|
// Get CA certificate
|
|
resp, _, err := scepClient.GetCACert(ctx, "")
|
|
require.NoError(t, err)
|
|
caCerts, err := x509.ParseCertificates(resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, caCerts)
|
|
|
|
// Create temporary RSA key for SCEP envelope
|
|
tempRSAKey, tempRSACert := createTempRSAKeyAndCert(t, existingCert.Subject.CommonName)
|
|
|
|
// Create SCEP PKI message for renewal
|
|
pkiMsgReq := &scep.PKIMessage{
|
|
MessageType: scep.PKCSReq,
|
|
Recipients: caCerts,
|
|
SignerKey: tempRSAKey,
|
|
SignerCert: tempRSACert,
|
|
}
|
|
|
|
msg, err := scep.NewCSRRequest(csr, pkiMsgReq, scep.WithLogger(s.Logger))
|
|
require.NoError(t, err)
|
|
|
|
// Send PKI operation request
|
|
respBytes, err := scepClient.PKIOperation(ctx, msg.Raw)
|
|
require.NoError(t, err)
|
|
|
|
// Parse response
|
|
pkiMsgResp, err := scep.ParsePKIMessage(respBytes, scep.WithLogger(s.Logger), scep.WithCACerts(msg.Recipients))
|
|
require.NoError(t, err)
|
|
|
|
// The renewal should succeed
|
|
require.Equal(t, scep.SUCCESS, pkiMsgResp.PKIStatus, "Renewal should succeed")
|
|
|
|
// Decrypt PKI envelope using RSA key
|
|
err = pkiMsgResp.DecryptPKIEnvelope(tempRSACert, tempRSAKey)
|
|
require.NoError(t, err)
|
|
|
|
// Verify we got a new certificate
|
|
require.NotNil(t, pkiMsgResp.CertRepMessage)
|
|
require.NotNil(t, pkiMsgResp.CertRepMessage.Certificate)
|
|
|
|
renewedCert := pkiMsgResp.CertRepMessage.Certificate
|
|
require.NotNil(t, renewedCert)
|
|
|
|
// Verify renewed certificate properties
|
|
assert.Equal(t, existingCert.Subject.CommonName, renewedCert.Subject.CommonName, "Common name should be preserved")
|
|
assert.Equal(t, x509.ECDSA, renewedCert.PublicKeyAlgorithm)
|
|
|
|
// Verify the renewed certificate has the new public key
|
|
renewedPubKey, ok := renewedCert.PublicKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "Renewed certificate should contain ECC public key")
|
|
assert.True(t, newEccPrivateKey.PublicKey.Equal(renewedPubKey), "Renewed certificate should have the new public key")
|
|
|
|
// Verify the renewed certificate has a different serial number
|
|
assert.NotEqual(t, existingCert.SerialNumber, renewedCert.SerialNumber, "Renewed certificate should have a new serial number")
|
|
|
|
// Verify the renewed certificate maintains the host_id association
|
|
renewedStoredCert, err := s.DS.GetHostIdentityCertBySerialNumber(ctx, renewedCert.SerialNumber.Uint64())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, renewedStoredCert)
|
|
require.NotNil(t, renewedStoredCert.HostID, "Renewed certificate should maintain host_id association")
|
|
require.Equal(t, originalHostID, *renewedStoredCert.HostID, "Renewed certificate should have the same host_id as the original")
|
|
|
|
// Test that we can use the renewed certificate to access the config endpoint
|
|
t.Run("test config endpoint with renewed certificate", func(t *testing.T) {
|
|
var configReq interface{}
|
|
var configURL string
|
|
|
|
if isOsquery {
|
|
configReq = osqueryConfigRequest{NodeKey: nodeKey}
|
|
configURL = s.Server.URL + "/api/osquery/config"
|
|
} else {
|
|
configReq = orbitConfigRequest{OrbitNodeKey: nodeKey}
|
|
configURL = s.Server.URL + "/api/fleet/orbit/config"
|
|
}
|
|
|
|
configReqBody, err := json.Marshal(configReq)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", configURL, bytes.NewReader(configReqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Create signer with the renewed certificate and new private key
|
|
signer := createHTTPSigner(t, newEccPrivateKey, renewedCert)
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// Should succeed with the renewed certificate
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Config request with renewed certificate should succeed")
|
|
})
|
|
|
|
// Test that config endpoint does not work with old certificate after renewal
|
|
t.Run("config endpoint fails with old certificate after renewal", func(t *testing.T) {
|
|
var configReq interface{}
|
|
var configURL string
|
|
|
|
if isOsquery {
|
|
configReq = osqueryConfigRequest{NodeKey: nodeKey}
|
|
configURL = s.Server.URL + "/api/osquery/config"
|
|
} else {
|
|
configReq = orbitConfigRequest{OrbitNodeKey: nodeKey}
|
|
configURL = s.Server.URL + "/api/fleet/orbit/config"
|
|
}
|
|
|
|
configReqBody, err := json.Marshal(configReq)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", configURL, bytes.NewReader(configReqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Create signer with the OLD certificate and OLD private key
|
|
signer := createHTTPSigner(t, eccPrivateKey, existingCert)
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// Should fail because the old certificate has been revoked
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Config request with old certificate should fail after renewal")
|
|
})
|
|
|
|
// Test that renewal cannot be retried with the same serial number
|
|
t.Run("renewal fails when retrying with same serial", func(t *testing.T) {
|
|
// Try to renew again using the same old certificate serial number
|
|
// This should fail because the certificate has already been revoked
|
|
|
|
// Generate another new key pair for this attempt
|
|
anotherNewKey, err := ecdsa.GenerateKey(eccPrivateKey.Curve, rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Use the same renewal data as before (same serial and signature)
|
|
retryCSRTemplate := x509util.CertificateRequest{
|
|
CertificateRequest: x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: existingCert.Subject.CommonName,
|
|
},
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: types.RenewalExtensionOID,
|
|
Value: renewalDataJSON, // Reuse the same renewal data
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
retryCSRDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &retryCSRTemplate, anotherNewKey)
|
|
require.NoError(t, err)
|
|
retryCSR, err := x509.ParseCertificateRequest(retryCSRDerBytes)
|
|
require.NoError(t, err)
|
|
|
|
// Create new temp RSA key for SCEP envelope
|
|
retryTempRSAKey, retryTempRSACert := createTempRSAKeyAndCert(t, existingCert.Subject.CommonName)
|
|
|
|
// Create SCEP PKI message for retry
|
|
retryPkiMsgReq := &scep.PKIMessage{
|
|
MessageType: scep.PKCSReq,
|
|
Recipients: caCerts,
|
|
SignerKey: retryTempRSAKey,
|
|
SignerCert: retryTempRSACert,
|
|
}
|
|
|
|
retryMsg, err := scep.NewCSRRequest(retryCSR, retryPkiMsgReq, scep.WithLogger(s.Logger))
|
|
require.NoError(t, err)
|
|
|
|
// Send PKI operation request
|
|
retryRespBytes, err := scepClient.PKIOperation(ctx, retryMsg.Raw)
|
|
require.NoError(t, err)
|
|
|
|
// Parse response
|
|
retryPkiMsgResp, err := scep.ParsePKIMessage(retryRespBytes, scep.WithLogger(s.Logger), scep.WithCACerts(retryMsg.Recipients))
|
|
require.NoError(t, err)
|
|
|
|
// Should fail - the certificate has already been revoked
|
|
require.Equal(t, scep.FAILURE, retryPkiMsgResp.PKIStatus, "Renewal retry with same serial should fail")
|
|
})
|
|
}
|
|
|
|
func testDeleteHostAndReenroll(t *testing.T, s *Suite, cert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey, nodeKey string) {
|
|
ctx := t.Context()
|
|
|
|
// Get the host using the orbit node key
|
|
hostToDelete, err := s.DS.LoadHostByOrbitNodeKey(ctx, nodeKey)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, hostToDelete, "Should find the enrolled host")
|
|
|
|
// Delete the host using the API endpoint
|
|
s.Do(t, "DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostToDelete.ID), nil, http.StatusOK)
|
|
|
|
// Try to enroll the same host with the same certificate - this should fail
|
|
enrollRequest := contract.EnrollOrbitRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HardwareUUID: "test-uuid-" + cert.Subject.CommonName,
|
|
HardwareSerial: "test-serial-" + cert.Subject.CommonName,
|
|
Hostname: "test-hostname-" + cert.Subject.CommonName,
|
|
OsqueryIdentifier: cert.Subject.CommonName,
|
|
}
|
|
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
signer := createHTTPSigner(t, eccPrivateKey, cert)
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// This should fail because the host certificate should be deleted when the host is deleted
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with deleted host certificate should fail")
|
|
}
|
|
|
|
func testDeleteHostAndReenrollOsquery(t *testing.T, s *Suite, cert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey, nodeKey string) {
|
|
ctx := t.Context()
|
|
|
|
// Get the host using the osquery node key
|
|
hostToDelete, err := s.DS.LoadHostByNodeKey(ctx, nodeKey)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, hostToDelete, "Should find the enrolled host")
|
|
|
|
// Delete the host using the API endpoint
|
|
s.Do(t, "DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostToDelete.ID), nil, http.StatusOK)
|
|
|
|
// Try to enroll the same host with the same certificate - this should fail
|
|
enrollRequest := contract.EnrollOsqueryAgentRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HostIdentifier: cert.Subject.CommonName,
|
|
HostDetails: map[string]map[string]string{
|
|
"osquery_info": {
|
|
"version": "5.0.0",
|
|
},
|
|
},
|
|
}
|
|
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/osquery/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
signer := createHTTPSigner(t, eccPrivateKey, cert)
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// This should fail because the host certificate should be deleted when the host is deleted
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with deleted host certificate should fail")
|
|
}
|
|
|
|
func createTempRSAKeyAndCert(t *testing.T, commonName string) (*rsa.PrivateKey, *x509.Certificate) {
|
|
// Create temporary RSA key for SCEP envelope (required by SCEP protocol)
|
|
tempRSAKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
|
|
// Create self-signed certificate for SCEP protocol using RSA key
|
|
deviceCertTemplate := x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: commonName,
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
deviceCertDerBytes, err := x509.CreateCertificate(
|
|
rand.Reader,
|
|
&deviceCertTemplate,
|
|
&deviceCertTemplate,
|
|
&tempRSAKey.PublicKey,
|
|
tempRSAKey,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
deviceCert, err := x509.ParseCertificate(deviceCertDerBytes)
|
|
require.NoError(t, err)
|
|
return tempRSAKey, deviceCert
|
|
}
|
|
|
|
func testGetCertFailures(t *testing.T, s *Suite) {
|
|
cases := []struct {
|
|
name string
|
|
config SCEPFailureConfig
|
|
}{
|
|
{
|
|
name: "empty challenge password",
|
|
config: SCEPFailureConfig{
|
|
ChallengePassword: "",
|
|
CommonName: "test-host-identity",
|
|
UseECC: true,
|
|
},
|
|
},
|
|
{
|
|
name: "wrong challenge password",
|
|
config: SCEPFailureConfig{
|
|
ChallengePassword: "wrong-secret",
|
|
CommonName: "test-host-identity",
|
|
UseECC: true,
|
|
},
|
|
},
|
|
{
|
|
name: "CN longer than 255 characters",
|
|
config: SCEPFailureConfig{
|
|
ChallengePassword: testEnrollmentSecret,
|
|
CommonName: strings.Repeat("a", 256),
|
|
UseECC: true,
|
|
},
|
|
},
|
|
{
|
|
name: "non-ECC algorithm used",
|
|
config: SCEPFailureConfig{
|
|
ChallengePassword: testEnrollmentSecret,
|
|
CommonName: "test-host-identity",
|
|
UseECC: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
testSCEPFailure(t, s, c.config)
|
|
})
|
|
}
|
|
}
|
|
|
|
type SCEPFailureConfig struct {
|
|
ChallengePassword string
|
|
CommonName string
|
|
UseECC bool
|
|
}
|
|
|
|
func testSCEPFailure(t *testing.T, s *Suite, config SCEPFailureConfig) {
|
|
ctx := t.Context()
|
|
|
|
// Create an enrollment secret
|
|
err := s.DS.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{
|
|
{
|
|
Secret: testEnrollmentSecret,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create SCEP client
|
|
scepURL := fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", s.Server.URL)
|
|
scepClient, err := scepclient.New(scepURL, s.Logger)
|
|
require.NoError(t, err)
|
|
|
|
// Get CA certificate
|
|
resp, _, err := scepClient.GetCACert(ctx, "")
|
|
require.NoError(t, err)
|
|
caCerts, err := x509.ParseCertificates(resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, caCerts)
|
|
|
|
var privateKey interface{}
|
|
var sigAlg x509.SignatureAlgorithm
|
|
|
|
if config.UseECC {
|
|
// Create ECC private key
|
|
eccKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
privateKey = eccKey
|
|
sigAlg = x509.ECDSAWithSHA256
|
|
} else {
|
|
// Create RSA private key to test non-ECC algorithm rejection (should fail)
|
|
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
privateKey = rsaKey
|
|
sigAlg = x509.SHA256WithRSA
|
|
}
|
|
|
|
// Create CSR
|
|
csrTemplate := x509util.CertificateRequest{
|
|
CertificateRequest: x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: config.CommonName,
|
|
},
|
|
SignatureAlgorithm: sigAlg,
|
|
},
|
|
ChallengePassword: config.ChallengePassword,
|
|
}
|
|
|
|
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey)
|
|
require.NoError(t, err)
|
|
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
|
require.NoError(t, err)
|
|
|
|
tempRSAKey, deviceCert := createTempRSAKeyAndCert(t, config.CommonName)
|
|
|
|
// Create SCEP PKI message
|
|
pkiMsgReq := &scep.PKIMessage{
|
|
MessageType: scep.PKCSReq,
|
|
Recipients: caCerts,
|
|
SignerKey: tempRSAKey,
|
|
SignerCert: deviceCert,
|
|
}
|
|
|
|
msg, err := scep.NewCSRRequest(csr, pkiMsgReq, scep.WithLogger(s.Logger))
|
|
require.NoError(t, err)
|
|
|
|
// Send PKI operation request
|
|
respBytes, err := scepClient.PKIOperation(ctx, msg.Raw)
|
|
require.NoError(t, err)
|
|
|
|
// Parse response
|
|
pkiMsgResp, err := scep.ParsePKIMessage(respBytes, scep.WithLogger(s.Logger), scep.WithCACerts(msg.Recipients))
|
|
require.NoError(t, err)
|
|
|
|
// Verify failure response
|
|
assert.Equal(t, scep.FAILURE, pkiMsgResp.PKIStatus, "SCEP request should fail")
|
|
}
|
|
|
|
func testWrongCertAuthentication(t *testing.T, s *Suite) {
|
|
// Test that hosts cannot use another host's certificate for authentication
|
|
|
|
// Create two P384 certificates for different hosts
|
|
certHost1, eccPrivateKeyHost1 := testGetCertWithCurve(t, s, elliptic.P384())
|
|
certHost2, eccPrivateKeyHost2 := testGetCertWithCurve(t, s, elliptic.P384())
|
|
|
|
// Create signers for both hosts
|
|
signerHost1 := createHTTPSigner(t, eccPrivateKeyHost1, certHost1)
|
|
signerHost2 := createHTTPSigner(t, eccPrivateKeyHost2, certHost2)
|
|
|
|
// Generate a local ECC P384 private key (not from Fleet SCEP)
|
|
localPrivateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Create a signer using the local private key with a fake certificate serial
|
|
localSigner, err := fleethttpsig.Signer(
|
|
"999999", // Fake certificate serial number
|
|
localPrivateKey,
|
|
httpsig.Algo_ECDSA_P384_SHA384,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
enrollRequest := contract.EnrollOrbitRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HardwareUUID: "test-uuid-" + certHost1.Subject.CommonName,
|
|
HardwareSerial: "test-serial-" + certHost1.Subject.CommonName,
|
|
Hostname: "test-hostname-" + certHost1.Subject.CommonName,
|
|
OsqueryIdentifier: certHost1.Subject.CommonName,
|
|
}
|
|
|
|
// Test enrollment with wrong certificate
|
|
enrollHostWithOtherHostCertShouldFail := func(t *testing.T) {
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with host2's signer (wrong cert)
|
|
err = signerHost2.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// Should fail because the certificate doesn't match the host identifier
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with wrong certificate should fail")
|
|
}
|
|
t.Run("enroll host1 with host2 cert should fail", enrollHostWithOtherHostCertShouldFail)
|
|
|
|
// Test enrollment with local private key
|
|
enrollHostWithLocalPrivateKeyShouldFail := func(t *testing.T) {
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with local private key (not managed by Fleet)
|
|
err = localSigner.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// Should fail because the certificate is not managed by Fleet
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with local private key should fail")
|
|
}
|
|
t.Run("enroll host1 with local private key should fail", enrollHostWithLocalPrivateKeyShouldFail)
|
|
|
|
// Successfully enroll host1 with correct certificate
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with host1's signer (correct cert)
|
|
err = signerHost1.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Enrollment with correct certificate should succeed")
|
|
|
|
var enrollResp enrollOrbitResponse
|
|
err = json.NewDecoder(httpResp.Body).Decode(&enrollResp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, enrollResp.OrbitNodeKey)
|
|
nodeKeyHost1 := enrollResp.OrbitNodeKey
|
|
|
|
type orbitConfigRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
}
|
|
|
|
t.Run("re-enroll host1 with host2 cert should fail", enrollHostWithOtherHostCertShouldFail)
|
|
t.Run("re-enroll host1 with local private key should fail", enrollHostWithLocalPrivateKeyShouldFail)
|
|
|
|
// Try to use host1's endpoint with host2's certificate
|
|
t.Run("host1 config with host2 cert should fail", func(t *testing.T) {
|
|
configRequest := orbitConfigRequest{
|
|
OrbitNodeKey: nodeKeyHost1,
|
|
}
|
|
|
|
reqBody, err := json.Marshal(configRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with host2's signer (wrong cert)
|
|
err = signerHost2.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Config request with wrong certificate should fail")
|
|
})
|
|
|
|
// Successfully enroll host2 with correct certificate
|
|
enrollRequest2 := contract.EnrollOrbitRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HardwareUUID: "test-uuid-" + certHost2.Subject.CommonName,
|
|
HardwareSerial: "test-serial-" + certHost2.Subject.CommonName,
|
|
Hostname: "test-hostname-" + certHost2.Subject.CommonName,
|
|
OsqueryIdentifier: certHost2.Subject.CommonName,
|
|
}
|
|
|
|
reqBody, err = json.Marshal(enrollRequest2)
|
|
require.NoError(t, err)
|
|
|
|
req, err = http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with host2's signer (correct cert)
|
|
err = signerHost2.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
httpResp, err = client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Enrollment with correct certificate should succeed")
|
|
|
|
enrollResp = enrollOrbitResponse{}
|
|
err = json.NewDecoder(httpResp.Body).Decode(&enrollResp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, enrollResp.OrbitNodeKey)
|
|
nodeKeyHost2 := enrollResp.OrbitNodeKey
|
|
|
|
t.Run("re-enroll host1 with host2-enrolled cert should still fail", enrollHostWithOtherHostCertShouldFail)
|
|
|
|
// Try to use host2's endpoint with host1's certificate
|
|
t.Run("host2 config with host1 cert should fail", func(t *testing.T) {
|
|
configRequest := orbitConfigRequest{
|
|
OrbitNodeKey: nodeKeyHost2,
|
|
}
|
|
|
|
reqBody, err := json.Marshal(configRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with host1's signer (wrong cert)
|
|
err = signerHost1.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Config request with wrong certificate should fail")
|
|
})
|
|
|
|
// Test config request with local private key
|
|
t.Run("config request with local private key should fail", func(t *testing.T) {
|
|
configRequest := orbitConfigRequest{
|
|
OrbitNodeKey: nodeKeyHost1,
|
|
}
|
|
|
|
reqBody, err := json.Marshal(configRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with local private key (not managed by Fleet)
|
|
err = localSigner.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// Should fail because the certificate is not managed by Fleet
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Config request with local private key should fail")
|
|
})
|
|
|
|
// Test enrollment failures after host is enrolled - use different host identifiers to avoid re-enrollment
|
|
t.Run("enroll new host with host1 cert should fail after enrollment", func(t *testing.T) {
|
|
newHostEnrollRequest := contract.EnrollOrbitRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HardwareUUID: "test-uuid-new-host-wrong-cert",
|
|
HardwareSerial: "test-serial-new-host-wrong-cert",
|
|
Hostname: "test-hostname-new-host-wrong-cert",
|
|
OsqueryIdentifier: "new-host-wrong-cert",
|
|
}
|
|
|
|
reqBody, err := json.Marshal(newHostEnrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with host1's signer (wrong cert for this new host)
|
|
err = signerHost1.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// Should fail because the certificate doesn't match the host identifier
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with wrong certificate should fail even after other hosts are enrolled")
|
|
})
|
|
}
|
|
|
|
// testRealSecureHWAndSCEP uses the SecureHW and SCEP packages that are used by Orbit. Only the TPM device is fake/simulated.
|
|
func testRealSecureHWAndSCEP(t *testing.T, s *Suite) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
|
|
// Create TPM simulator
|
|
sim, err := simulator.OpenSimulator()
|
|
require.NoError(t, err)
|
|
|
|
// Create a temporary directory for metadata
|
|
tempDir := t.TempDir()
|
|
|
|
// Create a zerolog logger for the test
|
|
zerologLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
|
|
|
// Create SecureHW instance with TPM simulator
|
|
tpmHW, err := securehw.NewTestSecureHW(sim, tempDir, zerologLogger)
|
|
require.NoError(t, err)
|
|
|
|
// Create a new key in the TPM
|
|
tpmKey, err := tpmHW.CreateKey()
|
|
require.NoError(t, err)
|
|
|
|
// Set up cleanup - the TPM hardware will be closed once at the end
|
|
t.Cleanup(func() {
|
|
if err := tpmHW.Close(); err != nil {
|
|
// Don't fail if already closed
|
|
t.Logf("TPM close error (may be expected): %v", err)
|
|
}
|
|
})
|
|
|
|
// Verify we can get the public key
|
|
pubKey, err := tpmKey.Public()
|
|
require.NoError(t, err)
|
|
eccPubKey, ok := pubKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "Expected ECC public key")
|
|
|
|
// Create enrollment secret
|
|
err = s.DS.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{
|
|
{
|
|
Secret: testEnrollmentSecret,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Generate a unique common name
|
|
commonName := generateRandomString(16)
|
|
|
|
// Create SCEP client with the TPM key
|
|
scepClient, err := orbitscep.NewClient(
|
|
orbitscep.WithSigningKey(tpmKey),
|
|
orbitscep.WithURL(fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", s.Server.URL)),
|
|
orbitscep.WithCommonName(commonName),
|
|
orbitscep.WithChallenge(testEnrollmentSecret),
|
|
orbitscep.WithLogger(zerologLogger),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Fetch certificate using SCEP
|
|
cert, err := scepClient.FetchCert(ctx)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cert)
|
|
|
|
// Verify certificate properties
|
|
assert.Equal(t, commonName, cert.Subject.CommonName)
|
|
assert.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm)
|
|
|
|
// Verify the certificate's public key matches our TPM key
|
|
certPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "Certificate should contain ECC public key")
|
|
assert.True(t, eccPubKey.Equal(certPubKey), "Certificate public key should match TPM key")
|
|
|
|
// Test enrollment with HTTP signature using TPM key
|
|
enrollRequest := contract.EnrollOrbitRequest{
|
|
EnrollSecret: testEnrollmentSecret,
|
|
HardwareUUID: "test-uuid-" + commonName,
|
|
HardwareSerial: "test-serial-" + commonName,
|
|
Hostname: "test-hostname-" + commonName,
|
|
OsqueryIdentifier: commonName,
|
|
}
|
|
|
|
reqBody, err := json.Marshal(enrollRequest)
|
|
require.NoError(t, err)
|
|
|
|
req, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/enroll", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Get HTTP signer from TPM key
|
|
httpSigner, err := tpmKey.HTTPSigner()
|
|
require.NoError(t, err)
|
|
|
|
// Determine algorithm based on the curve
|
|
var algo httpsig.Algorithm
|
|
switch httpSigner.ECCAlgorithm() {
|
|
case securehw.ECCAlgorithmP256:
|
|
algo = httpsig.Algo_ECDSA_P256_SHA256
|
|
case securehw.ECCAlgorithmP384:
|
|
algo = httpsig.Algo_ECDSA_P384_SHA384
|
|
default:
|
|
t.Fatalf("Unsupported ECC algorithm from TPM")
|
|
}
|
|
|
|
// Create HTTP signature signer
|
|
signer, err := fleethttpsig.Signer(
|
|
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
|
httpSigner,
|
|
algo,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Sign the request
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
// Send the signed request
|
|
client := fleethttp.NewClient()
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
// The request with a valid HTTP signature should succeed
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Request with TPM-based HTTP signature should succeed")
|
|
|
|
// Parse the response
|
|
var enrollResp enrollOrbitResponse
|
|
err = json.NewDecoder(httpResp.Body).Decode(&enrollResp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, enrollResp.OrbitNodeKey, "Should receive orbit node key")
|
|
require.NoError(t, enrollResp.Err)
|
|
|
|
// Test that we can load the key from storage
|
|
require.NoError(t, tpmKey.Close()) // Close the original key
|
|
|
|
loadedKey, err := tpmHW.LoadKey()
|
|
require.NoError(t, err)
|
|
|
|
// Verify loaded key has same public key
|
|
loadedPubKey, err := loadedKey.Public()
|
|
require.NoError(t, err)
|
|
loadedECCPubKey, ok := loadedPubKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "Loaded key should be ECC")
|
|
assert.True(t, eccPubKey.Equal(loadedECCPubKey), "Loaded key should match original")
|
|
|
|
// Test config endpoint with loaded key
|
|
configRequest := orbitConfigRequest{
|
|
OrbitNodeKey: enrollResp.OrbitNodeKey,
|
|
}
|
|
|
|
configReqBody, err := json.Marshal(configRequest)
|
|
require.NoError(t, err)
|
|
|
|
configReq, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(configReqBody))
|
|
require.NoError(t, err)
|
|
configReq.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with loaded key
|
|
loadedHTTPSigner, err := loadedKey.HTTPSigner()
|
|
require.NoError(t, err)
|
|
|
|
loadedSigner, err := fleethttpsig.Signer(
|
|
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
|
loadedHTTPSigner,
|
|
algo,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = loadedSigner.Sign(configReq)
|
|
require.NoError(t, err)
|
|
|
|
httpResp, err = client.Do(configReq)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Config request with loaded TPM key should succeed")
|
|
|
|
t.Run("renew certificate with real SecureHW and SCEP client", func(t *testing.T) {
|
|
// Get the original certificate's host_id before renewal (it will get revoked)
|
|
originalStoredCert, err := s.DS.GetHostIdentityCertBySerialNumber(ctx, cert.SerialNumber.Uint64())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, originalStoredCert)
|
|
require.NotNil(t, originalStoredCert.HostID, "Original certificate should have host_id")
|
|
originalHostID := *originalStoredCert.HostID
|
|
// Save the current certificate to the expected location
|
|
certPath := filepath.Join(tempDir, constant.FleetHTTPSignatureCertificateFileName)
|
|
certFile, err := os.OpenFile(certPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
require.NoError(t, err)
|
|
err = pem.Encode(certFile, &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Raw,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, certFile.Close())
|
|
|
|
// Now we can use hostidentity.RenewCertificate directly since SecureHW is exported
|
|
// Create a Credentials struct with our test TPM
|
|
credentials := &hostidentity.Credentials{
|
|
Certificate: cert,
|
|
SecureHWKey: loadedKey,
|
|
CertificatePath: certPath,
|
|
SecureHW: tpmHW,
|
|
}
|
|
|
|
// Use the hostidentity.RenewCertificate method directly
|
|
renewedCert, err := hostidentity.RenewCertificate(
|
|
ctx,
|
|
tempDir,
|
|
credentials,
|
|
fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", s.Server.URL),
|
|
"", // rootCA - empty for insecure
|
|
true, // insecure
|
|
zerologLogger,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, renewedCert)
|
|
|
|
// The RenewCertificate method should have updated credentials.SecureHWKey
|
|
// and saved the renewed certificate
|
|
|
|
// Verify renewed certificate properties
|
|
assert.Equal(t, cert.Subject.CommonName, renewedCert.Subject.CommonName, "Common name should be preserved")
|
|
assert.NotEqual(t, cert.SerialNumber, renewedCert.SerialNumber, "Serial number should be different")
|
|
assert.Equal(t, x509.ECDSA, renewedCert.PublicKeyAlgorithm)
|
|
|
|
// Verify the renewed certificate has a new public key (from the new TPM key)
|
|
renewedPubKey, ok := renewedCert.PublicKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "Renewed certificate should contain ECC public key")
|
|
assert.False(t, certPubKey.Equal(renewedPubKey), "Renewed certificate should have a different public key")
|
|
|
|
// Verify the new key's public key matches the renewed certificate
|
|
// The new key is now in credentials.SecureHWKey
|
|
newPubKey, err := credentials.SecureHWKey.Public()
|
|
require.NoError(t, err)
|
|
newECCPubKey, ok := newPubKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok, "New key should be ECC")
|
|
assert.True(t, renewedPubKey.Equal(newECCPubKey), "Renewed certificate public key should match new TPM key")
|
|
|
|
// Verify the renewed certificate maintains the host_id association
|
|
renewedStoredCert, err := s.DS.GetHostIdentityCertBySerialNumber(ctx, renewedCert.SerialNumber.Uint64())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, renewedStoredCert)
|
|
require.NotNil(t, renewedStoredCert.HostID, "Renewed certificate should maintain host_id association")
|
|
require.Equal(t, originalHostID, *renewedStoredCert.HostID, "Renewed certificate should have the same host_id as the original")
|
|
|
|
// Test that we can use the renewed certificate and new key
|
|
renewedConfigRequest := orbitConfigRequest{
|
|
OrbitNodeKey: enrollResp.OrbitNodeKey,
|
|
}
|
|
|
|
renewedConfigReqBody, err := json.Marshal(renewedConfigRequest)
|
|
require.NoError(t, err)
|
|
|
|
renewedConfigReq, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(renewedConfigReqBody))
|
|
require.NoError(t, err)
|
|
renewedConfigReq.Header.Set("Content-Type", "application/json")
|
|
|
|
// Sign with renewed certificate and new key
|
|
renewedHTTPSigner, err := credentials.SecureHWKey.HTTPSigner()
|
|
require.NoError(t, err)
|
|
|
|
// Determine algorithm for renewed key
|
|
var renewedAlgo httpsig.Algorithm
|
|
switch renewedHTTPSigner.ECCAlgorithm() {
|
|
case securehw.ECCAlgorithmP256:
|
|
renewedAlgo = httpsig.Algo_ECDSA_P256_SHA256
|
|
case securehw.ECCAlgorithmP384:
|
|
renewedAlgo = httpsig.Algo_ECDSA_P384_SHA384
|
|
default:
|
|
t.Fatalf("Unsupported ECC algorithm from renewed TPM key")
|
|
}
|
|
|
|
renewedSigner, err := fleethttpsig.Signer(
|
|
fmt.Sprintf("%d", renewedCert.SerialNumber.Uint64()),
|
|
renewedHTTPSigner,
|
|
renewedAlgo,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = renewedSigner.Sign(renewedConfigReq)
|
|
require.NoError(t, err)
|
|
|
|
httpResp, err = client.Do(renewedConfigReq)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusOK, httpResp.StatusCode, "Config request with renewed certificate should succeed")
|
|
|
|
// Test that old certificate no longer works
|
|
// Since the old key was closed and replaced, we need to recreate the signer with the old serial
|
|
oldConfigReq, err := http.NewRequest("POST", s.Server.URL+"/api/fleet/orbit/config", bytes.NewReader(renewedConfigReqBody))
|
|
require.NoError(t, err)
|
|
oldConfigReq.Header.Set("Content-Type", "application/json")
|
|
|
|
// Create a signer with the old certificate serial but it should fail since the cert was replaced
|
|
oldSerialSigner, err := fleethttpsig.Signer(
|
|
fmt.Sprintf("%d", cert.SerialNumber.Uint64()),
|
|
renewedHTTPSigner, // Using new key with old serial
|
|
renewedAlgo,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = oldSerialSigner.Sign(oldConfigReq)
|
|
require.NoError(t, err)
|
|
|
|
httpResp, err = client.Do(oldConfigReq)
|
|
require.NoError(t, err)
|
|
defer httpResp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Config request with old certificate serial should fail after renewal")
|
|
|
|
// Verify the old key backup was cleaned up by RenewCertificate
|
|
oldKeyPath := filepath.Join(tempDir, constant.FleetHTTPSignatureTPMKeyBackupFileName)
|
|
_, err = os.Stat(oldKeyPath)
|
|
require.True(t, os.IsNotExist(err), "Old key backup should have been removed by RenewCertificate")
|
|
|
|
// Clean up the new key
|
|
t.Cleanup(func() {
|
|
_ = credentials.SecureHWKey.Close()
|
|
})
|
|
})
|
|
}
|