mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #40540 # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - Changes present in previous PR ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Updated internal logging infrastructure to improve consistency and maintainability across the application. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
287 lines
9.1 KiB
Go
287 lines
9.1 KiB
Go
package hostidentity
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
cryptorand "crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"math/big"
|
|
"math/rand"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttpsig"
|
|
scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/scep/x509util"
|
|
"github.com/remitly-oss/httpsig-go"
|
|
"github.com/smallstep/scep"
|
|
)
|
|
|
|
// Config holds the configuration needed for host identity certificate requests
|
|
type Config struct {
|
|
ServerAddress string
|
|
EnrollSecret string
|
|
HostUUID string
|
|
AgentIndex int
|
|
}
|
|
|
|
// Client manages host identity certificates and HTTP message signing
|
|
type Client struct {
|
|
config Config
|
|
hostIdentityCert *x509.Certificate
|
|
hostIdentityKey *ecdsa.PrivateKey
|
|
httpSigner *httpsig.Signer
|
|
useHTTPSignatures bool
|
|
httpMessageSignatureP384Prob float64
|
|
}
|
|
|
|
// NewClient creates a new host identity client
|
|
func NewClient(config Config, useHTTPSignatures bool, httpMessageSignatureP384Prob float64) *Client {
|
|
return &Client{
|
|
config: config,
|
|
useHTTPSignatures: useHTTPSignatures,
|
|
httpMessageSignatureP384Prob: httpMessageSignatureP384Prob,
|
|
}
|
|
}
|
|
|
|
// createTempRSAKeyAndCert creates a temporary RSA key and certificate for SCEP protocol
|
|
func createTempRSAKeyAndCert(hostIdentifier string) (*rsa.PrivateKey, *x509.Certificate, error) {
|
|
// Generate temporary RSA key for SCEP protocol
|
|
tempRSAKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate temp RSA key: %w", err)
|
|
}
|
|
|
|
// Create temporary certificate for SCEP
|
|
certTemplate := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
CommonName: hostIdentifier,
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
certDER, err := x509.CreateCertificate(cryptorand.Reader, &certTemplate, &certTemplate, &tempRSAKey.PublicKey, tempRSAKey)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create temp certificate: %w", err)
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to parse temp certificate: %w", err)
|
|
}
|
|
|
|
return tempRSAKey, cert, nil
|
|
}
|
|
|
|
// RequestCertificate requests a host identity certificate from Fleet via SCEP
|
|
func (c *Client) RequestCertificate() error {
|
|
if !c.useHTTPSignatures {
|
|
return nil // Not needed if not using HTTP signatures
|
|
}
|
|
|
|
// Create ECC private key (randomly choose P384 or P256 with 50-50 probability)
|
|
var curve elliptic.Curve
|
|
if rand.Float64() < c.httpMessageSignatureP384Prob { // nolint:gosec // ignore weak randomizer
|
|
curve = elliptic.P384()
|
|
} else {
|
|
curve = elliptic.P256()
|
|
}
|
|
eccPrivateKey, err := ecdsa.GenerateKey(curve, cryptorand.Reader)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to generate ECC private key: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
|
|
// Create SCEP client with no-op logger and 30-second timeout
|
|
scepURL := fmt.Sprintf("%s/api/fleet/orbit/host_identity/scep", c.config.ServerAddress)
|
|
timeout := 30 * time.Second
|
|
scepClient, err := scepclient.New(scepURL, slog.New(slog.DiscardHandler),
|
|
scepclient.WithTimeout(&timeout),
|
|
scepclient.Insecure(),
|
|
)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to create SCEP client: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
|
|
// Get CA certificate with 30-second timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
|
defer cancel()
|
|
start := time.Now()
|
|
resp, _, err := scepClient.GetCACert(ctx, "")
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to get CA cert: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
log.Printf("Agent %d: GetCACert duration: %s", c.config.AgentIndex, time.Since(start))
|
|
|
|
start = time.Now()
|
|
caCerts, err := x509.ParseCertificates(resp)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to parse CA cert: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
if len(caCerts) == 0 {
|
|
log.Printf("Agent %d: No CA certificates received", c.config.AgentIndex)
|
|
return errors.New("no CA certificates received")
|
|
}
|
|
log.Printf("Agent %d: parse CA certificates duration: %s", c.config.AgentIndex, time.Since(start))
|
|
|
|
// Create host identifier using UUID
|
|
hostIdentifier := c.config.HostUUID
|
|
|
|
// Create CSR using ECC key
|
|
csrTemplate := x509util.CertificateRequest{
|
|
CertificateRequest: x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: hostIdentifier,
|
|
},
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
},
|
|
ChallengePassword: c.config.EnrollSecret,
|
|
}
|
|
|
|
start = time.Now()
|
|
csrDerBytes, err := x509util.CreateCertificateRequest(cryptorand.Reader, &csrTemplate, eccPrivateKey)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to create CSR: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to parse CSR: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
log.Printf("Agent %d: create and parse CSR duration: %s", c.config.AgentIndex, time.Since(start))
|
|
|
|
// Create temporary RSA key and cert for SCEP protocol
|
|
start = time.Now()
|
|
tempRSAKey, deviceCert, err := createTempRSAKeyAndCert(hostIdentifier)
|
|
if err != nil {
|
|
log.Printf("Agent %d: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to create SCEP message: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
log.Printf("Agent %d: create temp RSA key and CSR request: %s", c.config.AgentIndex, time.Since(start))
|
|
|
|
// Send PKI operation request
|
|
start = time.Now()
|
|
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Minute)
|
|
defer cancel()
|
|
respBytes, err := scepClient.PKIOperation(ctx, msg.Raw)
|
|
if err != nil {
|
|
log.Printf("Agent %d: SCEP PKI operation failed: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
log.Printf("Agent %d: PKIOperation duration: %s", c.config.AgentIndex, time.Since(start))
|
|
|
|
// Parse response
|
|
start = time.Now()
|
|
pkiMsgResp, err := scep.ParsePKIMessage(respBytes, scep.WithCACerts(msg.Recipients))
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to parse SCEP response: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
|
|
// Verify successful response
|
|
if pkiMsgResp.PKIStatus != scep.SUCCESS {
|
|
log.Printf("Agent %d: SCEP request failed with status: %v", c.config.AgentIndex, pkiMsgResp.PKIStatus)
|
|
return fmt.Errorf("SCEP request failed with status: %v", pkiMsgResp.PKIStatus)
|
|
}
|
|
|
|
// Decrypt PKI envelope using RSA key
|
|
err = pkiMsgResp.DecryptPKIEnvelope(deviceCert, tempRSAKey)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to decrypt SCEP response: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
|
|
// Extract the certificate
|
|
certRepMsg := pkiMsgResp.CertRepMessage
|
|
if certRepMsg == nil {
|
|
log.Printf("Agent %d: No certificate in SCEP response", c.config.AgentIndex)
|
|
return errors.New("no certificate in SCEP response")
|
|
}
|
|
|
|
cert := certRepMsg.Certificate
|
|
if cert == nil {
|
|
log.Printf("Agent %d: No certificate in CertRepMessage", c.config.AgentIndex)
|
|
return errors.New("no certificate in CertRepMessage")
|
|
}
|
|
|
|
// Store the certificate and private key
|
|
c.hostIdentityCert = cert
|
|
c.hostIdentityKey = eccPrivateKey
|
|
|
|
// Create an HTTP signer
|
|
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:
|
|
log.Printf("Agent %d: Unsupported curve: %v", c.config.AgentIndex, eccPrivateKey.Curve)
|
|
return fmt.Errorf("unsupported curve: %v", eccPrivateKey.Curve)
|
|
}
|
|
|
|
signer, err := fleethttpsig.Signer(fmt.Sprintf("%X", cert.SerialNumber), eccPrivateKey, algo)
|
|
if err != nil {
|
|
log.Printf("Agent %d: Failed to create HTTP signer: %v", c.config.AgentIndex, err)
|
|
return err
|
|
}
|
|
c.httpSigner = signer
|
|
|
|
log.Printf("Agent %d: parse PKIMessage and decrypt duration: %s", c.config.AgentIndex, time.Since(start))
|
|
log.Printf("Agent %d: Successfully obtained host identity certificate with serial %X", c.config.AgentIndex, cert.SerialNumber)
|
|
return nil
|
|
}
|
|
|
|
// SignRequest signs an HTTP request with the host identity certificate if available
|
|
func (c *Client) SignRequest(req *http.Request) error {
|
|
if c.httpSigner == nil {
|
|
return nil // No signer available
|
|
}
|
|
|
|
return c.httpSigner.Sign(req)
|
|
}
|
|
|
|
// IsEnabled returns whether HTTP message signatures are enabled for this client
|
|
func (c *Client) IsEnabled() bool {
|
|
return c.useHTTPSignatures
|
|
}
|
|
|
|
// HasSigner returns whether the client has a valid HTTP signer
|
|
func (c *Client) HasSigner() bool {
|
|
return c.httpSigner != nil
|
|
}
|
|
|
|
// GetSigner returns the HTTP signer for use with httpsig.NewHTTPClient
|
|
func (c *Client) GetSigner() *httpsig.Signer {
|
|
return c.httpSigner
|
|
}
|