mirror of
https://github.com/fleetdm/fleet
synced 2026-05-18 06:28:40 +00:00
For #31048 This change includes some refactoring of orbit code. No functional changes. Moved non-Linux-specific code from `securehw_linux.go` to `securehw_tpm.go` so that tests on any platform can use it. There are no server changes impacting the upcoming 4.72 release. Just tests. # Checklist for submitter ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified that fleetd runs on macOS, Linux and Windows <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new TPM 2.0-based secure hardware interface, enabling creation, loading, and management of ECC keys within a TPM device. * Added support for both standard and RFC 9421-compatible HTTP signatures using TPM-backed keys. * **Bug Fixes** * Improved error handling and resource management for TPM operations. * **Tests** * Added comprehensive unit tests for TPM key file loading scenarios. * Introduced integration tests using a simulated TPM device to validate end-to-end secure hardware and SCEP workflows. * **Chores** * Updated dependencies for enhanced compatibility and security. * Modernized build constraints for improved maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1169 lines
38 KiB
Go
1169 lines
38 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/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"fmt"
|
|
mathrand "math/rand/v2"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
orbitscep "github.com/fleetdm/fleet/v4/ee/orbit/pkg/scep"
|
|
"github.com/fleetdm/fleet/v4/ee/orbit/pkg/securehw"
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttpsig"
|
|
"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 := SetUpSuite(t, "integrationtest.HostIdentity", false)
|
|
|
|
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())
|
|
testOrbitEnrollment(t, s, cert, eccPrivateKey)
|
|
})
|
|
|
|
t.Run("ECC P384, orbit", func(t *testing.T) {
|
|
t.Parallel()
|
|
cert, eccPrivateKey := testGetCertWithCurve(t, s, elliptic.P384())
|
|
testOrbitEnrollment(t, s, cert, eccPrivateKey)
|
|
})
|
|
|
|
t.Run("ECC P384, osquery", func(t *testing.T) {
|
|
t.Parallel()
|
|
cert, eccPrivateKey := testGetCertWithCurve(t, s, elliptic.P384())
|
|
testOsqueryEnrollment(t, s, cert, eccPrivateKey)
|
|
})
|
|
}
|
|
|
|
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) {
|
|
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)
|
|
|
|
clonedRequest := req.Clone(ctx)
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Important: since this subtest deletes the host, it should run last.
|
|
// Test deleting host and trying to enroll with same certificate
|
|
t.Run("delete host and enroll with same certificate", func(t *testing.T) {
|
|
// Get the host using the orbit node key (standard pattern used in Fleet tests)
|
|
hostToDelete, err := s.DS.LoadHostByOrbitNodeKey(ctx, signedEnrollResp.OrbitNodeKey)
|
|
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
|
|
// because deleting the host should have invalidated its certificate
|
|
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")
|
|
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
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.
|
|
// The host needs to request a new cert to re-enroll.
|
|
require.Equal(t, http.StatusUnauthorized, httpResp.StatusCode, "Enrollment with deleted host certificate should fail")
|
|
})
|
|
}
|
|
|
|
func testOsqueryEnrollment(t *testing.T, s *Suite, cert *x509.Certificate, eccPrivateKey *ecdsa.PrivateKey) {
|
|
ctx := t.Context()
|
|
// 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)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Important: since this subtest deletes the host, it should run last.
|
|
// Test deleting host and trying to enroll with same certificate
|
|
t.Run("delete host and enroll with same certificate", func(t *testing.T) {
|
|
// Get the host using the osquery node key (standard pattern used in Fleet tests)
|
|
hostToDelete, err := s.DS.LoadHostByNodeKey(ctx, enrollResp.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
|
|
// because deleting the host should have invalidated its certificate
|
|
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")
|
|
|
|
err = signer.Sign(req)
|
|
require.NoError(t, err)
|
|
|
|
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.
|
|
// The host needs to request a new cert to re-enroll.
|
|
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 in reverse order - keys first, then hardware, then simulator
|
|
t.Cleanup(func() {
|
|
require.NoError(t, tpmHW.Close())
|
|
})
|
|
|
|
// 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)
|
|
// Close the loaded key at the end of this section
|
|
t.Cleanup(func() {
|
|
require.NoError(t, loadedKey.Close())
|
|
})
|
|
|
|
// 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")
|
|
}
|