Files
fleet/server/datastore/mysql/host_certificates_test.go
Victor Lyuboslavsky f72de68f43 Fixed unreleased Windows cert ingestion perf issue (#49787)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #49705 

Verified fix with 100k host Windows load test.

# Checklist for submitter

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Alerted the release DRI if additional load testing is needed

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Bug Fixes**
* Improved host certificate deduplication and automated self-healing
when duplicate certificate records are ingested.
* Updated source reconciliation to only change what’s stale, preventing
unnecessary rewrites of unchanged source entries.
* Ensured certificate sources consistently associate to the newest
canonical certificate record for each certificate hash.
* Improved certificate listing accuracy by returning a deduplicated set
of certificate/source pairs with correct usernames.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 13:16:48 -05:00

2289 lines
100 KiB
Go

package mysql
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha1" // nolint:gosec // test-only unique sha1 generator
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"fmt"
"math/big"
mathrand "math/rand/v2"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHostCertificates(t *testing.T) {
ds := CreateMySQLDS(t)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"UpdateAndList", testUpdateAndListHostCertificates},
{"Update with host_mdm_managed_certificates to update", testUpdatingHostMDMManagedCertificates},
{"Insert host_mdm_managed_certificates from non-proxied ingestion", testInsertingHostMDMManagedCertificatesFromIngestion},
{"Matcher recovers stuck hmmc rows", testMatcherRecoversStuckHMMCRows},
{"Update certificate sources isolation", testUpdateHostCertificatesSourcesIsolation},
{"Windows scope-aware reconciliation", testUpdateHostCertificatesWindowsScopeReconciliation},
{"Re-ingest after soft delete attaches sources to live row", testUpdateHostCertificatesReingestAfterSoftDelete},
{"Self-heals duplicate cert rows and converges", testUpdateHostCertificatesSelfHealsDuplicates},
{"Precise source writes preserve unchanged rows", testUpdateHostCertificatesPreciseSourceWrites},
{"Origin-scoped delete", testUpdateHostCertificatesOriginScopedDelete},
{"Origin downgrade on osquery rediscovery", testUpdateHostCertificatesOriginDowngrade},
{"Create certificates with long country code", testHostCertificateWithInvalidCountryCode},
{"Truncate long certificate fields", testTruncateLongCertificateFields},
{"Count matches main query", testListHostCertificatesCountMatches},
{"Sweep mdm certs for unenrolled hosts", testSoftDeleteMDMHostCertificatesForUnenrolledHosts},
{"Windows proxied SCEP profile verification", testWindowsSCEPProfileVerification},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testWindowsSCEPProfileVerification(t *testing.T, ds *Datastore) {
ctx := t.Context()
mkHost := func(t *testing.T, suffix string) *fleet.Host {
t.Helper()
osqueryID := "wscep-osquery-" + suffix
nodeKey := "wscep-node-" + suffix
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: &osqueryID,
NodeKey: &nodeKey,
UUID: "wscep-host-" + suffix,
Hostname: "wscep-hostname-" + suffix,
})
require.NoError(t, err)
return h
}
upsertWinProfile := func(t *testing.T, h *fleet.Host, profileUUID, cmdUUID string, status fleet.MDMDeliveryStatus, retries int) {
t.Helper()
require.NoError(t, ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{{
ProfileUUID: profileUUID,
ProfileName: "p-" + profileUUID,
HostUUID: h.UUID,
CommandUUID: cmdUUID,
OperationType: fleet.MDMOperationTypeInstall,
Status: &status,
Checksum: []byte{1},
}}))
if retries > 0 {
_, err := ds.writer(ctx).ExecContext(ctx,
`UPDATE host_mdm_windows_profiles SET retries = ? WHERE host_uuid = ? AND profile_uuid = ?`, retries, h.UUID, profileUUID)
require.NoError(t, err)
}
}
upsertHMMC := func(t *testing.T, h *fleet.Host, profileUUID string, caType fleet.CAConfigAssetType, nvb, nva *time.Time) {
t.Helper()
hmmc := &fleet.MDMManagedCertificate{HostUUID: h.UUID, ProfileUUID: profileUUID, Type: caType, CAName: "ca-" + profileUUID}
if nva != nil {
hmmc.NotValidBefore = nvb
hmmc.NotValidAfter = nva
serial := "serial-" + profileUUID
hmmc.Serial = &serial
}
require.NoError(t, ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{hmmc}))
}
getProfile := func(t *testing.T, h *fleet.Host, profileUUID string) (fleet.MDMDeliveryStatus, string, int) {
t.Helper()
var row struct {
Status fleet.MDMDeliveryStatus `db:"status"`
Detail string `db:"detail"`
Retries int `db:"retries"`
}
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &row,
`SELECT status, detail, retries FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_uuid = ?`, h.UUID, profileUUID))
return row.Status, row.Detail, row.Retries
}
// certWithRenewalID builds an ingestable host cert whose OU carries the profile's renewal-ID marker.
certWithRenewalID := func(t *testing.T, h *fleet.Host, renewalProfileUUID string, serial int64) *fleet.HostCertificateRecord {
t.Helper()
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "device " + renewalProfileUUID,
Organization: []string{"Org"},
OrganizationalUnit: []string{"fleet-" + renewalProfileUUID},
},
Issuer: pkix.Name{CommonName: "issuer.test.example.com", Organization: []string{"Issuer"}},
SerialNumber: big.NewInt(serial),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(365 * 24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
return generateTestHostCertificateRecord(t, h.ID, tmpl)
}
ingest := func(t *testing.T, h *fleet.Host, recs ...*fleet.HostCertificateRecord) {
t.Helper()
require.NoError(t, ds.UpdateHostCertificates(ctx, h.ID, h.UUID, recs, fleet.HostCertificateOriginOsquery, nil))
}
ackVerified := func(t *testing.T, h *fleet.Host, cmdUUID string) {
t.Helper()
verified := fleet.MDMDeliveryVerified
require.NoError(t, updateMDMWindowsHostProfileStatusFromResponseDB(ctx, ds.writer(ctx),
[]*fleet.MDMWindowsProfilePayload{{HostUUID: h.UUID, CommandUUID: cmdUUID, Status: &verified}}))
}
// insertConfigProfile creates the team config profile row whose SyncML scope (./Device vs ./User) the backstop
// inspects when only the system store was observed.
insertConfigProfile := func(t *testing.T, profileUUID, name string, userScoped bool) {
t.Helper()
locURI := "./Device/Vendor/MSFT/ClientCertificateInstall/SCEP"
if userScoped {
locURI = "./User/Vendor/MSFT/ClientCertificateInstall/SCEP"
}
syncml := fmt.Sprintf(`<Replace><Item><Target><LocURI>%s/%s/Install/Enroll</LocURI></Target></Item></Replace>`, locURI, profileUUID)
_, err := ds.writer(ctx).ExecContext(ctx,
`INSERT INTO mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml, uploaded_at) VALUES (?, 0, ?, ?, NOW())`,
profileUUID, name, syncml)
require.NoError(t, err)
}
backdateProfile := func(t *testing.T, h *fleet.Host, profileUUID string, ago time.Duration) {
t.Helper()
_, err := ds.writer(ctx).ExecContext(ctx,
`UPDATE host_mdm_windows_profiles SET updated_at = DATE_SUB(NOW(), INTERVAL ? SECOND) WHERE host_uuid = ? AND profile_uuid = ?`,
int(ago.Seconds()), h.UUID, profileUUID)
require.NoError(t, err)
}
// plainCert builds an ingestable host cert that does NOT carry any profile's renewal-ID marker.
plainCert := func(t *testing.T, h *fleet.Host, cn string, source fleet.HostCertificateSource, username string) *fleet.HostCertificateRecord {
t.Helper()
sum := make([]byte, 20)
copy(sum, cn)
return &fleet.HostCertificateRecord{
HostID: h.ID,
CommonName: cn,
SubjectCommonName: cn,
SHA1Sum: sum,
NotValidBefore: time.Now().Add(-time.Hour),
NotValidAfter: time.Now().Add(365 * 24 * time.Hour),
Source: source,
Username: username,
}
}
// ACK status mapping: a proxied SCEP profile moves to "verifying" on the device's 2xx ACK
for i, tc := range []struct {
name string
caType fleet.CAConfigAssetType // "" means no managed-certificate row
want fleet.MDMDeliveryStatus
}{
{"proxied SCEP moves to verifying", fleet.CAConfigCustomSCEPProxy, fleet.MDMDeliveryVerifying},
{"non-certificate profile stays verified", "", fleet.MDMDeliveryVerified},
{"DigiCert profile stays verified", fleet.CAConfigDigiCert, fleet.MDMDeliveryVerified},
} {
t.Run("ACK: "+tc.name, func(t *testing.T) {
h := mkHost(t, fmt.Sprintf("ack-%d", i))
p, cmd := "w-ack", "cmd-ack"
upsertWinProfile(t, h, p, cmd, fleet.MDMDeliveryPending, 0)
if tc.caType != "" {
upsertHMMC(t, h, p, tc.caType, nil, nil)
}
ackVerified(t, h, cmd)
status, _, _ := getProfile(t, h, p)
require.Equal(t, tc.want, status)
})
}
t.Run("cert observation flips verifying to verified", func(t *testing.T) {
h := mkHost(t, "flip")
p := "w-scep-flip"
upsertWinProfile(t, h, p, "cmd-flip", fleet.MDMDeliveryVerifying, 0)
upsertHMMC(t, h, p, fleet.CAConfigCustomSCEPProxy, nil, nil)
ingest(t, h, certWithRenewalID(t, h, p, 7001))
status, _, _ := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryVerified, status)
})
t.Run("cert observation self-heals failed to verified and clears detail", func(t *testing.T) {
h := mkHost(t, "heal")
p := "w-scep-heal"
upsertWinProfile(t, h, p, "cmd-heal", fleet.MDMDeliveryFailed, 0)
upsertHMMC(t, h, p, fleet.CAConfigCustomSCEPProxy, nil, nil)
_, err := ds.writer(ctx).ExecContext(ctx,
`UPDATE host_mdm_windows_profiles SET detail = ? WHERE host_uuid = ? AND profile_uuid = ?`,
"SCEP PKIOperation failed: HTTP 500", h.UUID, p)
require.NoError(t, err)
ingest(t, h, certWithRenewalID(t, h, p, 7002))
status, detail, _ := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryVerified, status)
require.Empty(t, detail)
})
t.Run("no matching cert keeps profile verifying", func(t *testing.T) {
h := mkHost(t, "nomatch")
p := "w-scep-nomatch"
upsertWinProfile(t, h, p, "cmd-nomatch", fleet.MDMDeliveryVerifying, 0)
upsertHMMC(t, h, p, fleet.CAConfigCustomSCEPProxy, nil, nil)
// A cert for an unrelated profile: exercises the ingest path but matches nothing here.
ingest(t, h, certWithRenewalID(t, h, "w-unrelated-profile", 7003))
status, _, _ := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryVerifying, status)
})
t.Run("DigiCert profile not flipped even with observed cert dates", func(t *testing.T) {
h := mkHost(t, "digicert-flip")
p := "w-digicert-flip"
upsertWinProfile(t, h, p, "cmd-digicert-flip", fleet.MDMDeliveryVerifying, 0)
nvb := time.Now().Add(-time.Hour).Truncate(time.Second).UTC()
nva := time.Now().Add(365 * 24 * time.Hour).Truncate(time.Second).UTC()
upsertHMMC(t, h, p, fleet.CAConfigDigiCert, &nvb, &nva)
ingest(t, h, certWithRenewalID(t, h, "w-trigger-only", 7004))
status, _, _ := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryVerifying, status)
})
t.Run("SetMDMWindowsHostProfileFailed marks failed and preserves retries", func(t *testing.T) {
h := mkHost(t, "setfailed")
p := "w-fail"
upsertWinProfile(t, h, p, "cmd-fail", fleet.MDMDeliveryVerifying, 1)
require.NoError(t, ds.SetMDMWindowsHostProfileFailed(ctx, h.UUID, p, "SCEP PKIOperation failed: HTTP 500"))
status, detail, retries := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryFailed, status)
require.Equal(t, "SCEP PKIOperation failed: HTTP 500", detail)
require.Equal(t, 1, retries)
})
t.Run("SetMDMWindowsHostProfileFailed does not clobber verified", func(t *testing.T) {
h := mkHost(t, "setfailed-verified")
p := "w-fail-verified"
upsertWinProfile(t, h, p, "cmd-fail-verified", fleet.MDMDeliveryVerified, 0)
require.NoError(t, ds.SetMDMWindowsHostProfileFailed(ctx, h.UUID, p, "SCEP GetCACert failed: timeout"))
status, _, _ := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryVerified, status)
})
t.Run("SetMDMWindowsHostProfileFailed no-ops for a removed profile", func(t *testing.T) {
h := mkHost(t, "setfailed-missing")
// No profile row exists for this (host, profile): must not error and must not resurrect a row.
require.NoError(t, ds.SetMDMWindowsHostProfileFailed(ctx, h.UUID, "w-missing", "SCEP PKIOperation failed: HTTP 403"))
var count int
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &count,
`SELECT COUNT(*) FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_uuid = ?`, h.UUID, "w-missing"))
require.Equal(t, 0, count)
})
// Verification backstop: once the grace period elapses and a report proves the certificate's store was readable
// but the cert is absent, the profile fails. Device-scoped relies on the always-readable system store; user-scoped
// needs a user cert in the report (single-user assumption).
for i, tc := range []struct {
name string
userScoped bool
backdate time.Duration // 0 = still within the grace window
certSource fleet.HostCertificateSource
username string
retries int
wantStatus fleet.MDMDeliveryStatus
wantDetail string
}{
{"device-scoped fails past grace when cert absent", false, 2 * time.Hour, fleet.SystemHostCertificate, "", 2, fleet.MDMDeliveryFailed, windowsSCEPCertNotFoundDetail},
{"device-scoped stays verifying within grace", false, 0, fleet.SystemHostCertificate, "", 0, fleet.MDMDeliveryVerifying, ""},
{"user-scoped stays verifying when no user cert observed", true, 2 * time.Hour, fleet.SystemHostCertificate, "", 0, fleet.MDMDeliveryVerifying, ""},
{"user-scoped fails past grace once a user cert is observed", true, 2 * time.Hour, fleet.UserHostCertificate, "alice", 0, fleet.MDMDeliveryFailed, windowsSCEPCertNotFoundDetail},
} {
t.Run("backstop: "+tc.name, func(t *testing.T) {
h := mkHost(t, fmt.Sprintf("backstop-%d", i))
p := fmt.Sprintf("w-backstop-%d", i)
upsertWinProfile(t, h, p, "cmd-backstop", fleet.MDMDeliveryVerifying, tc.retries)
insertConfigProfile(t, p, fmt.Sprintf("backstop-%d", i), tc.userScoped)
upsertHMMC(t, h, p, fleet.CAConfigCustomSCEPProxy, nil, nil)
if tc.backdate > 0 {
backdateProfile(t, h, p, tc.backdate)
}
// The ingested cert never carries this profile's renewal-ID marker; only its source (system vs user)
// varies, which decides whether the user store is proven readable.
ingest(t, h, plainCert(t, h, "some-cert", tc.certSource, tc.username))
status, detail, retries := getProfile(t, h, p)
require.Equal(t, tc.wantStatus, status)
require.Equal(t, tc.wantDetail, detail)
require.Equal(t, tc.retries, retries)
})
}
t.Run("observed cert wins over the backstop even past grace", func(t *testing.T) {
h := mkHost(t, "backstop-flipwins")
p := "w-backstop-flipwins"
upsertWinProfile(t, h, p, "cmd-bs-flipwins", fleet.MDMDeliveryVerifying, 0)
insertConfigProfile(t, p, "bs-flipwins", false)
upsertHMMC(t, h, p, fleet.CAConfigCustomSCEPProxy, nil, nil)
backdateProfile(t, h, p, 2*time.Hour)
// The matching cert is present, so the verified-flip must win and the backstop must not fire.
ingest(t, h, certWithRenewalID(t, h, p, 7101))
status, _, _ := getProfile(t, h, p)
require.Equal(t, fleet.MDMDeliveryVerified, status)
})
}
func testUpdateAndListHostCertificates(t *testing.T, ds *Datastore) {
ctx := t.Context()
createX509Cert := func(commonName string, notAfter time.Duration) x509.Certificate {
return x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: commonName,
Organization: []string{"Org"},
OrganizationalUnit: []string{"Engineering"},
},
Issuer: pkix.Name{
Country: []string{"US"},
CommonName: "issuer.test.example.com",
Organization: []string{"Issuer"},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(notAfter).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
}
expected1 := createX509Cert("test.example.com", 24*time.Hour)
expected2 := createX509Cert("another.test.example.com", 48*time.Hour)
payload := []*fleet.HostCertificateRecord{
generateTestHostCertificateRecord(t, 1, &expected1),
generateTestHostCertificateRecord(t, 1, &expected2),
}
require.NoError(t, ds.UpdateHostCertificates(ctx, 1, "95816502-d8c0-462c-882f-39991cc89a0c", payload, fleet.HostCertificateOriginOsquery, nil))
// verify that we saved the records correctly
certs, meta, err := ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name", IncludeMetadata: true})
require.NoError(t, err)
require.Len(t, certs, 2)
require.Equal(t, expected2.Subject.CommonName, certs[0].CommonName)
require.Equal(t, expected2.Subject.CommonName, certs[0].SubjectCommonName)
require.Equal(t, fleet.SystemHostCertificate, certs[0].Source)
require.Equal(t, expected1.Subject.CommonName, certs[1].CommonName)
require.Equal(t, expected1.Subject.CommonName, certs[1].SubjectCommonName)
require.Equal(t, fleet.SystemHostCertificate, certs[1].Source)
require.EqualValues(t, 2, meta.TotalResults)
// order by not_valid_after descending
certs, _, err = ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "not_valid_after", OrderDirection: fleet.OrderAscending})
require.NoError(t, err)
require.Len(t, certs, 2)
require.Equal(t, expected1.Subject.CommonName, certs[0].CommonName)
require.Equal(t, expected1.Subject.CommonName, certs[0].SubjectCommonName)
require.Equal(t, expected2.Subject.CommonName, certs[1].CommonName)
require.Equal(t, expected2.Subject.CommonName, certs[1].SubjectCommonName)
// simulate removal of a certificate
require.NoError(t, ds.UpdateHostCertificates(ctx, 1, "95816502-d8c0-462c-882f-39991cc89a0c", []*fleet.HostCertificateRecord{payload[1]}, fleet.HostCertificateOriginOsquery, nil))
certs, _, err = ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name"})
require.NoError(t, err)
require.Len(t, certs, 1)
require.Equal(t, expected2.Subject.CommonName, certs[0].CommonName)
require.Equal(t, expected2.Subject.CommonName, certs[0].SubjectCommonName)
// re-add first certificate but as a "user" source
payload[0].Source = fleet.UserHostCertificate
payload[0].Username = "A"
require.NoError(t, ds.UpdateHostCertificates(ctx, 1, "95816502-d8c0-462c-882f-39991cc89a0c", []*fleet.HostCertificateRecord{payload[0], payload[1]}, fleet.HostCertificateOriginOsquery, nil))
certs, _, err = ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name"})
require.NoError(t, err)
require.Len(t, certs, 2)
require.Equal(t, expected2.Subject.CommonName, certs[0].CommonName)
require.Equal(t, expected2.Subject.CommonName, certs[0].SubjectCommonName)
require.Equal(t, fleet.SystemHostCertificate, certs[0].Source)
require.Equal(t, "", certs[0].Username)
require.Equal(t, expected1.Subject.CommonName, certs[1].CommonName)
require.Equal(t, expected1.Subject.CommonName, certs[1].SubjectCommonName)
require.Equal(t, fleet.UserHostCertificate, certs[1].Source)
require.Equal(t, "A", certs[1].Username)
hostCert1SrcUserA := payload[0]
hostCert2SrcSys := payload[1]
expected3 := createX509Cert("multi.test.example.com", 24*time.Hour)
hostCert3SrcUserB := generateTestHostCertificateRecord(t, 1, &expected3)
hostCert3SrcUserB.Source = fleet.UserHostCertificate
hostCert3SrcUserB.Username = "B"
cloneC := *hostCert3SrcUserB // copy to create a new record
hostCert3SrcUserC := &cloneC
hostCert3SrcUserC.Source = fleet.UserHostCertificate
hostCert3SrcUserC.Username = "C"
cloneD := *hostCert3SrcUserB // copy to create a new record
hostCert3SrcUserD := &cloneD
hostCert3SrcUserD.Source = fleet.UserHostCertificate
hostCert3SrcUserD.Username = "D"
cases := []struct {
desc string
ingest []*fleet.HostCertificateRecord
}{
{desc: "nil slice", ingest: nil},
{desc: "cert 1 and 2", ingest: []*fleet.HostCertificateRecord{hostCert2SrcSys, hostCert1SrcUserA}},
{desc: "cert 2 and 3 (B, C)", ingest: []*fleet.HostCertificateRecord{hostCert2SrcSys, hostCert3SrcUserB, hostCert3SrcUserC}},
{desc: "no change", ingest: []*fleet.HostCertificateRecord{hostCert2SrcSys, hostCert3SrcUserB, hostCert3SrcUserC}},
{desc: "added cert 3 source (D)", ingest: []*fleet.HostCertificateRecord{hostCert2SrcSys, hostCert3SrcUserB, hostCert3SrcUserC, hostCert3SrcUserD}},
{desc: "removed cert3 source (B)", ingest: []*fleet.HostCertificateRecord{hostCert2SrcSys, hostCert3SrcUserC, hostCert3SrcUserD}},
{desc: "removed cert3 source (C)", ingest: []*fleet.HostCertificateRecord{hostCert2SrcSys, hostCert3SrcUserB, hostCert3SrcUserD}},
{desc: "cleared, added cert 1", ingest: []*fleet.HostCertificateRecord{hostCert1SrcUserA}},
{desc: "all cleared", ingest: []*fleet.HostCertificateRecord{}},
}
for _, c := range cases {
t.Log(c.desc)
err := ds.UpdateHostCertificates(ctx, 1, "95816502-d8c0-462c-882f-39991cc89a0c", c.ingest, fleet.HostCertificateOriginOsquery, nil)
require.NoError(t, err)
certs, _, err := ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name", TestSecondaryOrderKey: "username"})
require.NoError(t, err)
require.Len(t, certs, len(c.ingest))
for i, cert := range certs {
require.Equal(t, c.ingest[i].CommonName, cert.CommonName, "index %d", i)
require.Equal(t, c.ingest[i].Source, cert.Source, "index %d", i)
require.Equal(t, c.ingest[i].Username, cert.Username, "index %d", i)
}
}
}
func testUpdatingHostMDMManagedCertificates(t *testing.T, ds *Datastore) {
// test that we can update the host_mdm_managed_certificates table when
// ingesting the associated certificate from the host
ctx := context.Background()
initialCPs := storeDummyConfigProfilesForTest(t, ds, 2)
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("host0-osquery-id"),
NodeKey: ptr.String("host0-node-key"),
UUID: "host0-test-mdm-profiles",
Hostname: "hostname0",
})
require.NoError(t, err)
err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: initialCPs[0].ProfileUUID,
ProfileIdentifier: initialCPs[0].Identifier,
ProfileName: initialCPs[0].Name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid-1",
Checksum: []byte("checksum1"),
Scope: fleet.PayloadScopeSystem,
},
{
ProfileUUID: initialCPs[1].ProfileUUID,
ProfileIdentifier: initialCPs[1].Identifier,
ProfileName: initialCPs[1].Name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid-2",
Checksum: []byte("checksum2"),
Scope: fleet.PayloadScopeSystem,
},
},
)
require.NoError(t, err)
// Initial certificate state where a host has been requested to install but we have no metadata
challengeRetrievedAt := time.Now().Add(-time.Hour).UTC().Round(time.Microsecond)
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: initialCPs[0].ProfileUUID,
Type: fleet.CAConfigCustomSCEPProxy,
CAName: "custom-ca",
},
{
HostUUID: host.UUID,
ProfileUUID: initialCPs[1].ProfileUUID,
ChallengeRetrievedAt: &challengeRetrievedAt,
Type: fleet.CAConfigSmallstep,
CAName: "step-ca",
},
})
require.NoError(t, err)
expected1 := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "MYHWSERIAL fleet-" + initialCPs[0].ProfileUUID,
Organization: []string{"Org 1"},
OrganizationalUnit: []string{"Engineering"},
},
Issuer: pkix.Name{
Country: []string{"US"},
CommonName: "issuer.test.example.com",
Organization: []string{"Issuer 1"},
},
SerialNumber: big.NewInt(1337),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
expected2 := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "MYOTHERHWSERIAL",
Organization: []string{"Org 2"},
OrganizationalUnit: []string{"Engineering"},
},
SerialNumber: big.NewInt(31337),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-2 * time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(48 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
expected3 := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "MYHWSERIAL 2",
Organization: []string{"Org 1"},
OrganizationalUnit: []string{"fleet-" + initialCPs[1].ProfileUUID},
},
Issuer: pkix.Name{
Country: []string{"US"},
CommonName: "issuer.test.example.com",
Organization: []string{"Issuer 1"},
},
SerialNumber: big.NewInt(1338),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
payload := []*fleet.HostCertificateRecord{
generateTestHostCertificateRecord(t, host.ID, &expected1),
generateTestHostCertificateRecord(t, host.ID, &expected2),
generateTestHostCertificateRecord(t, host.ID, &expected3),
}
require.NoError(t, ds.UpdateHostCertificates(context.Background(), host.ID, host.UUID, payload, fleet.HostCertificateOriginOsquery, nil))
// verify that we saved the records correctly
certs, _, err := ds.ListHostCertificates(context.Background(), 1, fleet.ListOptions{OrderKey: "common_name"})
require.NoError(t, err)
require.Len(t, certs, 3)
require.Equal(t, expected3.Subject.CommonName, certs[0].CommonName)
require.Equal(t, expected3.Subject.CommonName, certs[0].SubjectCommonName)
require.Equal(t, expected1.Subject.CommonName, certs[1].CommonName)
require.Equal(t, expected1.Subject.CommonName, certs[1].SubjectCommonName)
require.Equal(t, expected2.Subject.CommonName, certs[2].CommonName)
require.Equal(t, expected2.Subject.CommonName, certs[2].SubjectCommonName)
// Check that the managed certificate details were updated correctly
profile, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCPs[0].ProfileUUID, "custom-ca")
require.NoError(t, err)
require.NotNil(t, profile)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCPs[0].ProfileUUID, profile.ProfileUUID)
require.Nil(t, profile.ChallengeRetrievedAt)
assert.Equal(t, fleet.CAConfigCustomSCEPProxy, profile.Type)
require.NotNil(t, profile.Serial)
assert.Equal(t, fmt.Sprintf("%040s", expected1.SerialNumber.Text(16)), *profile.Serial)
require.NotNil(t, profile.NotValidBefore)
assert.Equal(t, expected1.NotBefore, *profile.NotValidBefore)
require.NotNil(t, profile.NotValidAfter)
assert.Equal(t, expected1.NotAfter, *profile.NotValidAfter)
assert.Equal(t, "custom-ca", profile.CAName)
// Check that the managed certificate details were updated correctly
profile2, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCPs[1].ProfileUUID, "step-ca")
require.NoError(t, err)
require.NotNil(t, profile2)
assert.Equal(t, host.UUID, profile2.HostUUID)
assert.Equal(t, initialCPs[1].ProfileUUID, profile2.ProfileUUID)
require.NotNil(t, profile2.ChallengeRetrievedAt)
assert.Equal(t, &challengeRetrievedAt, profile2.ChallengeRetrievedAt)
assert.Equal(t, fleet.CAConfigSmallstep, profile2.Type)
require.NotNil(t, profile2.Serial)
assert.Equal(t, fmt.Sprintf("%040s", expected3.SerialNumber.Text(16)), *profile2.Serial)
require.NotNil(t, profile2.NotValidBefore)
assert.Equal(t, expected3.NotBefore, *profile2.NotValidBefore)
require.NotNil(t, profile2.NotValidAfter)
assert.Equal(t, expected3.NotAfter, *profile2.NotValidAfter)
assert.Equal(t, "step-ca", profile2.CAName)
// simulate removal of a certificate
require.NoError(t, ds.UpdateHostCertificates(context.Background(), host.ID, "95816502-d8c0-462c-882f-39991cc89a0c", []*fleet.HostCertificateRecord{payload[1], payload[2]}, fleet.HostCertificateOriginOsquery, nil))
certs3, _, err := ds.ListHostCertificates(context.Background(), host.ID, fleet.ListOptions{OrderKey: "common_name"})
require.NoError(t, err)
require.Len(t, certs3, 2)
require.Equal(t, expected3.Subject.CommonName, certs3[0].CommonName)
require.Equal(t, expected3.Subject.CommonName, certs3[0].SubjectCommonName)
require.Equal(t, expected2.Subject.CommonName, certs3[1].CommonName)
require.Equal(t, expected2.Subject.CommonName, certs3[1].SubjectCommonName)
// Check that the managed certificate details were not updated
profile, err = ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, initialCPs[0].ProfileUUID, "custom-ca")
require.NoError(t, err)
require.NotNil(t, profile)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCPs[0].ProfileUUID, profile.ProfileUUID)
require.Nil(t, profile.ChallengeRetrievedAt)
assert.Equal(t, fleet.CAConfigCustomSCEPProxy, profile.Type)
require.NotNil(t, profile.Serial)
assert.Equal(t, fmt.Sprintf("%040s", expected1.SerialNumber.Text(16)), *profile.Serial)
require.NotNil(t, profile.NotValidBefore)
assert.Equal(t, expected1.NotBefore, *profile.NotValidBefore)
require.NotNil(t, profile.NotValidAfter)
assert.Equal(t, expected1.NotAfter, *profile.NotValidAfter)
assert.Equal(t, "custom-ca", profile.CAName)
}
// checksumForTest returns a 16-byte value suitable for the BINARY(16) checksum
// column on host_mdm_*_profiles tables. Truncates a sha1 hash of the input.
func checksumForTest(s string) []byte {
sum := sha1.Sum([]byte(s)) // nolint:gosec // test-only checksum, not security-sensitive
return sum[:16]
}
func testMatcherRecoversStuckHMMCRows(t *testing.T, ds *Datastore) {
ctx := t.Context()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("matcher-osquery-id"),
NodeKey: ptr.String("matcher-node-key"),
UUID: "matcher-host-uuid",
Hostname: "matcher-host",
})
require.NoError(t, err)
renewalCertTemplate := func(profileUUID, suffix string, notBefore, notAfter time.Time, serial int64) *x509.Certificate {
return &x509.Certificate{
Subject: pkix.Name{
CommonName: "ABC123 WIFI fleet-" + profileUUID + suffix,
Organization: []string{"Org"},
},
Issuer: pkix.Name{
CommonName: "issuer.test.example.com",
Organization: []string{"Issuer"},
},
SerialNumber: big.NewInt(serial),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
}
}
unrelatedCertTemplate := func(name string, notAfter time.Duration, serial int64) *x509.Certificate {
return &x509.Certificate{
Subject: pkix.Name{CommonName: name, Organization: []string{"Org"}},
Issuer: pkix.Name{CommonName: "issuer.test.example.com", Organization: []string{"Issuer"}},
SerialNumber: big.NewInt(serial),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(notAfter).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
}
backdateHMMC := func(t *testing.T, profileUUID string, ago time.Duration) {
t.Helper()
_, err := ds.writer(ctx).ExecContext(ctx, `
UPDATE host_mdm_managed_certificates
SET updated_at = DATE_SUB(NOW(), INTERVAL ? SECOND)
WHERE host_uuid = ? AND profile_uuid = ?
`, int(ago.Seconds()), host.UUID, profileUUID)
require.NoError(t, err)
}
getApple := func(t *testing.T, profileUUID, caName string) *fleet.HostMDMCertificateProfile {
t.Helper()
profile, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, profileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
return profile
}
type seedOpts struct {
profileUUID string
certType fleet.CAConfigAssetType
caName string
// Zero means hmmc is created with NULL validity columns.
populatedNotBefore time.Time
populatedNotAfter time.Time
populatedSerial string
}
seedAppleProfile := func(t *testing.T, opts seedOpts, status fleet.MDMDeliveryStatus) {
t.Helper()
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
ProfileUUID: opts.profileUUID,
HostUUID: host.UUID,
Status: &status,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "cmd-" + opts.profileUUID,
Checksum: checksumForTest(opts.profileUUID),
Scope: fleet.PayloadScopeSystem,
}}))
hmmc := &fleet.MDMManagedCertificate{
HostUUID: host.UUID,
ProfileUUID: opts.profileUUID,
Type: opts.certType,
CAName: opts.caName,
}
if !opts.populatedNotAfter.IsZero() {
hmmc.NotValidBefore = &opts.populatedNotBefore
hmmc.NotValidAfter = &opts.populatedNotAfter
hmmc.Serial = ptr.String(opts.populatedSerial)
}
require.NoError(t, ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{hmmc}))
}
// Returns the generated records so callers re-pass them through triggerMatcher.
// generateTestHostCertificateRecord produces a fresh SHA1 each call, so a
// re-generation from the same template would land in toInsert.
seedHostCertificates := func(t *testing.T, certs []*x509.Certificate) []*fleet.HostCertificateRecord {
t.Helper()
payload := make([]*fleet.HostCertificateRecord, 0, len(certs))
for _, c := range certs {
payload = append(payload, generateTestHostCertificateRecord(t, host.ID, c))
}
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, payload, fleet.HostCertificateOriginOsquery, nil))
return payload
}
// Adds an unrelated cert to populate toInsert (the matcher's gate).
triggerMatcher := func(t *testing.T, existingRecs []*fleet.HostCertificateRecord, unrelatedSerial int64) {
t.Helper()
payload := make([]*fleet.HostCertificateRecord, 0, len(existingRecs)+1)
payload = append(payload, existingRecs...)
unrelated := unrelatedCertTemplate(fmt.Sprintf("unrelated-%d", unrelatedSerial), 24*time.Hour, unrelatedSerial)
payload = append(payload, generateTestHostCertificateRecord(t, host.ID, unrelated))
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, payload, fleet.HostCertificateOriginOsquery, nil))
}
t.Run("MissedIngestRecovered", func(t *testing.T) {
profileUUID := "missed-ingest"
freshCert := renewalCertTemplate(profileUUID, "", time.Now().Add(-time.Hour).Truncate(time.Second).UTC(), time.Now().Add(365*24*time.Hour).Truncate(time.Second).UTC(), 4001)
recs := seedHostCertificates(t, []*x509.Certificate{freshCert})
seedAppleProfile(t, seedOpts{profileUUID: profileUUID, certType: fleet.CAConfigCustomSCEPProxy, caName: "ca-missed"}, fleet.MDMDeliveryVerified)
backdateHMMC(t, profileUUID, 5*time.Hour)
triggerMatcher(t, recs, 4002)
got := getApple(t, profileUUID, "ca-missed")
require.NotNil(t, got.NotValidAfter)
assert.True(t, freshCert.NotAfter.Equal(*got.NotValidAfter), "stuck hmmc should have been backfilled with the renewal cert's not_valid_after")
require.NotNil(t, got.Serial)
assert.Equal(t, fmt.Sprintf("%040s", freshCert.SerialNumber.Text(16)), *got.Serial)
})
t.Run("GraceBoundary", func(t *testing.T) {
profileUUID := "grace-boundary"
freshCert := renewalCertTemplate(profileUUID, "", time.Now().Add(-time.Hour).Truncate(time.Second).UTC(), time.Now().Add(365*24*time.Hour).Truncate(time.Second).UTC(), 4101)
recs := seedHostCertificates(t, []*x509.Certificate{freshCert})
seedAppleProfile(t, seedOpts{profileUUID: profileUUID, certType: fleet.CAConfigCustomSCEPProxy, caName: "ca-grace"}, fleet.MDMDeliveryVerified)
backdateHMMC(t, profileUUID, 3*time.Hour) // within grace
triggerMatcher(t, recs, 4102)
got := getApple(t, profileUUID, "ca-grace")
assert.Nil(t, got.NotValidAfter, "row updated within grace must NOT be touched")
})
t.Run("DigiCertNotClobbered", func(t *testing.T) {
profileUUID := "digicert-row"
seededNotBefore := time.Now().Add(-2 * time.Hour).Truncate(time.Second).UTC()
seededNotAfter := time.Now().Add(48 * time.Hour).Truncate(time.Second).UTC()
seededSerial := fmt.Sprintf("%040s", "deadbeef")
conflicting := renewalCertTemplate(profileUUID, "", time.Now().Add(-time.Hour).Truncate(time.Second).UTC(), time.Now().Add(365*24*time.Hour).Truncate(time.Second).UTC(), 4201)
recs := seedHostCertificates(t, []*x509.Certificate{conflicting})
seedAppleProfile(t, seedOpts{
profileUUID: profileUUID,
certType: fleet.CAConfigDigiCert,
caName: "ca-digicert",
populatedNotBefore: seededNotBefore,
populatedNotAfter: seededNotAfter,
populatedSerial: seededSerial,
}, fleet.MDMDeliveryVerified)
backdateHMMC(t, profileUUID, 5*time.Hour)
triggerMatcher(t, recs, 4202)
got := getApple(t, profileUUID, "ca-digicert")
require.NotNil(t, got.NotValidAfter)
assert.True(t, seededNotAfter.Equal(*got.NotValidAfter), "DigiCert row must NOT be touched by matcher")
require.NotNil(t, got.Serial)
assert.Equal(t, seededSerial, *got.Serial)
})
t.Run("PendingProfileSkipped", func(t *testing.T) {
profileUUID := "pending-profile"
freshCert := renewalCertTemplate(profileUUID, "", time.Now().Add(-time.Hour).Truncate(time.Second).UTC(), time.Now().Add(365*24*time.Hour).Truncate(time.Second).UTC(), 4301)
recs := seedHostCertificates(t, []*x509.Certificate{freshCert})
seedAppleProfile(t, seedOpts{profileUUID: profileUUID, certType: fleet.CAConfigCustomSCEPProxy, caName: "ca-pending"}, fleet.MDMDeliveryPending)
backdateHMMC(t, profileUUID, 5*time.Hour)
triggerMatcher(t, recs, 4302)
got := getApple(t, profileUUID, "ca-pending")
assert.Nil(t, got.NotValidAfter, "in-flight (pending) profile must NOT be backfilled, even when stuck")
})
t.Run("FailedProfileSkipped", func(t *testing.T) {
// SCEP delivery failure is terminal across the platform (admin must
// resend). Recovering here from the OLD cert in inventory would
// re-arm the renewal cron into a push loop. See design §8.
profileUUID := "failed-profile"
oldCert := renewalCertTemplate(profileUUID, "", time.Now().Add(-30*24*time.Hour).Truncate(time.Second).UTC(), time.Now().Add(60*24*time.Hour).Truncate(time.Second).UTC(), 4351)
recs := seedHostCertificates(t, []*x509.Certificate{oldCert})
seedAppleProfile(t, seedOpts{profileUUID: profileUUID, certType: fleet.CAConfigCustomSCEPProxy, caName: "ca-failed"}, fleet.MDMDeliveryFailed)
backdateHMMC(t, profileUUID, 5*time.Hour)
triggerMatcher(t, recs, 4352)
got := getApple(t, profileUUID, "ca-failed")
assert.Nil(t, got.NotValidAfter, "failed (terminal) profile must NOT be backfilled, even when stuck")
})
t.Run("TieBreaker", func(t *testing.T) {
profileUUID := "tie-breaker"
olderCert := renewalCertTemplate(profileUUID, "-old", time.Now().Add(-48*time.Hour).Truncate(time.Second).UTC(), time.Now().Add(180*24*time.Hour).Truncate(time.Second).UTC(), 4401)
newerCert := renewalCertTemplate(profileUUID, "-new", time.Now().Add(-time.Hour).Truncate(time.Second).UTC(), time.Now().Add(365*24*time.Hour).Truncate(time.Second).UTC(), 4402)
// older first so result isn't iteration-order-dependent
recs := seedHostCertificates(t, []*x509.Certificate{olderCert, newerCert})
seedAppleProfile(t, seedOpts{profileUUID: profileUUID, certType: fleet.CAConfigCustomSCEPProxy, caName: "ca-tb"}, fleet.MDMDeliveryVerified)
backdateHMMC(t, profileUUID, 5*time.Hour)
triggerMatcher(t, recs, 4403)
got := getApple(t, profileUUID, "ca-tb")
require.NotNil(t, got.NotValidAfter)
assert.True(t, newerCert.NotAfter.Equal(*got.NotValidAfter), "matcher should pick the cert with latest not_valid_before")
})
t.Run("StableCertListRecovers", func(t *testing.T) {
// Reviewer requirement: recovery must fire even when the host's cert
// inventory hasn't changed this call (toInsert empty).
profileUUID := "stable-list"
freshCert := renewalCertTemplate(profileUUID, "", time.Now().Add(-time.Hour).Truncate(time.Second).UTC(), time.Now().Add(365*24*time.Hour).Truncate(time.Second).UTC(), 4601)
recs := seedHostCertificates(t, []*x509.Certificate{freshCert})
seedAppleProfile(t, seedOpts{profileUUID: profileUUID, certType: fleet.CAConfigCustomSCEPProxy, caName: "ca-stable"}, fleet.MDMDeliveryVerified)
backdateHMMC(t, profileUUID, 5*time.Hour)
// Re-pass the same records — toInsert will be empty, but recovery still runs.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, recs, fleet.HostCertificateOriginOsquery, nil))
got := getApple(t, profileUUID, "ca-stable")
require.NotNil(t, got.NotValidAfter)
assert.True(t, freshCert.NotAfter.Equal(*got.NotValidAfter), "stuck row must recover even when toInsert is empty")
})
t.Run("MonotonicForward", func(t *testing.T) {
profileUUID := "monotonic-forward"
freshNotBefore := time.Now().Add(-time.Hour).Truncate(time.Second).UTC()
freshNotAfter := time.Now().Add(400 * 24 * time.Hour).Truncate(time.Second).UTC()
seedAppleProfile(t, seedOpts{
profileUUID: profileUUID,
certType: fleet.CAConfigCustomSCEPProxy,
caName: "ca-mono",
populatedNotBefore: freshNotBefore,
populatedNotAfter: freshNotAfter,
populatedSerial: fmt.Sprintf("%040s", "fresh"),
}, fleet.MDMDeliveryVerified)
// older cert lands in toInsert; monotonic-forward must reject it.
olderCert := renewalCertTemplate(profileUUID, "-old", time.Now().Add(-48*time.Hour).Truncate(time.Second).UTC(), time.Now().Add(48*time.Hour).Truncate(time.Second).UTC(), 4501)
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, []*fleet.HostCertificateRecord{
generateTestHostCertificateRecord(t, host.ID, olderCert),
}, fleet.HostCertificateOriginOsquery, nil))
got := getApple(t, profileUUID, "ca-mono")
require.NotNil(t, got.NotValidAfter)
assert.True(t, freshNotAfter.Equal(*got.NotValidAfter), "monotonic-forward must prevent regression")
})
}
// testInsertingHostMDMManagedCertificatesFromIngestion exercises the
// non-proxied insert path: when a profile is installed on a host without an
// existing host_mdm_managed_certificates row, an ingested cert whose Subject
// carries the `fleet-<profile_uuid>` marker creates the row. Also validates
// that the matcher's SupportsRenewalID() guard does NOT skip empty/NULL
// Type rows on subsequent ingestion (Decision 2.2 knock-on).
func testInsertingHostMDMManagedCertificatesFromIngestion(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Three profiles installed on the host:
// nonProxied — no existing hmmc row; ingestion will create one (NULL Type).
// proxied — existing hmmc row (custom_scep_proxy); matcher updates it.
// noMatch — no existing hmmc row; no incoming cert carries its marker.
cps := storeDummyConfigProfilesForTest(t, ds, 3)
nonProxiedProfileUUID := cps[0].ProfileUUID
proxiedProfileUUID := cps[1].ProfileUUID
noMatchProfileUUID := cps[2].ProfileUUID
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("ingest-host-osq"),
NodeKey: ptr.String("ingest-host-nk"),
UUID: "ingest-host-uuid",
Hostname: "ingest-host",
})
require.NoError(t, err)
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: nonProxiedProfileUUID,
ProfileIdentifier: cps[0].Identifier,
ProfileName: cps[0].Name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "cmd-non-proxied",
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeSystem,
},
{
ProfileUUID: proxiedProfileUUID,
ProfileIdentifier: cps[1].Identifier,
ProfileName: cps[1].Name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "cmd-proxied",
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeSystem,
},
{
ProfileUUID: noMatchProfileUUID,
ProfileIdentifier: cps[2].Identifier,
ProfileName: cps[2].Name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "cmd-no-match",
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeSystem,
},
}))
// Pre-existing proxied hmmc row for proxiedProfileUUID.
require.NoError(t, ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: proxiedProfileUUID,
Type: fleet.CAConfigCustomSCEPProxy,
CAName: "custom-ca",
},
}))
// Build incoming certs:
// certNonProxied — Subject CN carries marker for nonProxiedProfileUUID,
// issued by a parent so IssuerCommonName is preserved
// certProxied — Subject OU carries marker for proxiedProfileUUID
// certUnrelated — no Fleet marker
notBefore := time.Now().Add(-time.Hour).Truncate(time.Second).UTC()
notAfter := time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC()
customerCAParent := x509.Certificate{
Subject: pkix.Name{
CommonName: "Customer Hydrant ACME",
Country: []string{"US"},
Organization: []string{"Customer"},
},
SerialNumber: big.NewInt(9000),
BasicConstraintsValid: true,
IsCA: true,
NotBefore: notBefore.Add(-time.Hour),
NotAfter: notAfter.Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign,
}
certNonProxied := x509.Certificate{
Subject: pkix.Name{
CommonName: "MAC-SERIAL fleet-" + nonProxiedProfileUUID,
Country: []string{"US"},
Organization: []string{"Org Non-Proxied"},
},
SerialNumber: big.NewInt(7001),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
}
certProxied := x509.Certificate{
Subject: pkix.Name{
CommonName: "MAC-SERIAL Proxied",
Country: []string{"US"},
Organization: []string{"Org Proxied"},
OrganizationalUnit: []string{"fleet-" + proxiedProfileUUID},
},
Issuer: pkix.Name{
CommonName: "Custom SCEP Issuer",
Country: []string{"US"},
Organization: []string{"Custom"},
},
SerialNumber: big.NewInt(7002),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
}
certUnrelated := x509.Certificate{
Subject: pkix.Name{
CommonName: "Some Other Cert",
Country: []string{"US"},
Organization: []string{"Unrelated"},
},
Issuer: pkix.Name{
CommonName: "Other Issuer",
Country: []string{"US"},
Organization: []string{"Other"},
},
SerialNumber: big.NewInt(7003),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
}
payload := []*fleet.HostCertificateRecord{
generateTestHostCertificateRecordWithParent(t, host.ID, &certNonProxied, &customerCAParent),
generateTestHostCertificateRecord(t, host.ID, &certProxied),
generateTestHostCertificateRecord(t, host.ID, &certUnrelated),
}
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, payload, fleet.HostCertificateOriginOsquery, nil))
// nonProxiedProfileUUID — row was inserted with NULL Type, matching cert's metadata.
all, err := ds.ListHostMDMManagedCertificates(ctx, host.UUID)
require.NoError(t, err)
var nonProxiedRow, proxiedRow *fleet.MDMManagedCertificate
for _, r := range all {
switch r.ProfileUUID {
case nonProxiedProfileUUID:
nonProxiedRow = r
case proxiedProfileUUID:
proxiedRow = r
case noMatchProfileUUID:
t.Fatalf("noMatchProfileUUID should not have an hmmc row but does: %+v", r)
}
}
require.NotNil(t, nonProxiedRow, "non-proxied profile should have a created hmmc row")
assert.Equal(t, fleet.CAConfigAssetType(""), nonProxiedRow.Type, "Type should be NULL/empty for non-proxied row")
assert.Equal(t, "non_proxied", nonProxiedRow.CAName, "CAName should be the fixed non-proxied sentinel, not derived from the cert")
require.NotNil(t, nonProxiedRow.Serial)
assert.Equal(t, fmt.Sprintf("%040s", certNonProxied.SerialNumber.Text(16)), *nonProxiedRow.Serial)
require.NotNil(t, nonProxiedRow.NotValidAfter)
assert.Equal(t, notAfter, *nonProxiedRow.NotValidAfter)
// proxiedProfileUUID — existing row updated with cert's serial / dates by the matcher.
require.NotNil(t, proxiedRow)
assert.Equal(t, fleet.CAConfigCustomSCEPProxy, proxiedRow.Type, "Existing proxied Type preserved")
require.NotNil(t, proxiedRow.Serial)
assert.Equal(t, fmt.Sprintf("%040s", certProxied.SerialNumber.Text(16)), *proxiedRow.Serial)
// Subsequent ingestion of a renewed cert for the non-proxied profile must
// advance not_valid_after — validates the matcher guard fix (Decision 2.2):
// without it, the SupportsRenewalID() skip silently excludes NULL-Type rows.
notAfter2 := notAfter.Add(48 * time.Hour)
certRenewed := certNonProxied
certRenewed.SerialNumber = big.NewInt(7011)
// NotBefore must remain in the past (matcher filters out future-valid certs)
// but later than the original so best-match-wins picks the renewed cert.
certRenewed.NotBefore = notBefore.Add(30 * time.Minute)
certRenewed.NotAfter = notAfter2
renewedPayload := []*fleet.HostCertificateRecord{
generateTestHostCertificateRecordWithParent(t, host.ID, &certRenewed, &customerCAParent),
generateTestHostCertificateRecord(t, host.ID, &certProxied),
generateTestHostCertificateRecord(t, host.ID, &certUnrelated),
}
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, renewedPayload, fleet.HostCertificateOriginOsquery, nil))
all2, err := ds.ListHostMDMManagedCertificates(ctx, host.UUID)
require.NoError(t, err)
var nonProxiedRow2 *fleet.MDMManagedCertificate
for _, r := range all2 {
if r.ProfileUUID == nonProxiedProfileUUID {
nonProxiedRow2 = r
}
}
require.NotNil(t, nonProxiedRow2)
require.NotNil(t, nonProxiedRow2.NotValidAfter)
assert.Equal(t, notAfter2, *nonProxiedRow2.NotValidAfter, "matcher must advance not_valid_after on NULL-Type rows")
require.NotNil(t, nonProxiedRow2.Serial)
assert.Equal(t, fmt.Sprintf("%040s", certRenewed.SerialNumber.Text(16)), *nonProxiedRow2.Serial)
assert.Equal(t, fleet.CAConfigAssetType(""), nonProxiedRow2.Type, "Type should still be NULL/empty after update")
}
// testUpdateHostCertificatesReingestAfterSoftDelete covers the soft-delete-then-re-report cycle
func testUpdateHostCertificatesReingestAfterSoftDelete(t *testing.T, ds *Datastore) {
ctx := t.Context()
const (
hostID = uint(77)
hostUUID = "windows-reingest-host-uuid"
)
cert := mkTestCertRecord(t, hostID, "reingest.example.com", fleet.UserHostCertificate, "alice")
sha1Hex := strings.ToUpper(hex.EncodeToString(cert.SHA1Sum))
scopes := []fleet.HostCertificateScope{{Source: fleet.UserHostCertificate, Username: "alice"}}
ingest := func() {
reported := *cert // fresh copy each report; UpdateHostCertificates mutates the record
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{&reported}, fleet.HostCertificateOriginOsquery, scopes))
}
softDeleteAll := func() {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_certificates SET deleted_at = NOW(6) WHERE host_id = ?`, hostID)
return err
})
}
ingest()
// Soft-delete the row, then re-report
softDeleteAll()
ingest()
// Also clone the live row as a soft-deleted duplicate with a HIGHER id.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_certificates
(host_id, sha1_sum, not_valid_after, not_valid_before, certificate_authority, common_name,
key_algorithm, key_strength, key_usage, serial, signing_algorithm, subject_country, subject_org,
subject_org_unit, subject_common_name, issuer_country, issuer_org, issuer_org_unit,
issuer_common_name, origin, deleted_at)
SELECT host_id, sha1_sum, not_valid_after, not_valid_before, certificate_authority, common_name,
key_algorithm, key_strength, key_usage, serial, signing_algorithm, subject_country, subject_org,
subject_org_unit, subject_common_name, issuer_country, issuer_org, issuer_org_unit,
issuer_common_name, origin, NOW(6)
FROM host_certificates WHERE host_id = ? AND deleted_at IS NULL`, hostID)
return err
})
var rowStates []struct {
ID uint `db:"id"`
Deleted bool `db:"deleted"`
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &rowStates,
`SELECT id, deleted_at IS NOT NULL AS deleted FROM host_certificates WHERE host_id = ? ORDER BY id`, hostID)
})
require.Len(t, rowStates, 3)
require.True(t, rowStates[0].Deleted, "expected the original row to be the soft-deleted one")
require.False(t, rowStates[1].Deleted, "expected the re-ingested row to be live")
require.True(t, rowStates[2].Deleted, "expected the cloned row to be soft-deleted")
liveID := rowStates[1].ID
// The sha1 -> id lookup used to attach sources must resolve to the live row only.
got, err := loadHostCertIDsForSHA1DB(ctx, ds.reader(ctx), hostID, []string{sha1Hex})
require.NoError(t, err)
require.Equal(t, map[string]uint{sha1Hex: liveID}, got)
// End to end: the certificate lists with its user source intact.
listed, _, err := ds.ListHostCertificates(ctx, hostID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, listed, 1)
require.Equal(t, fleet.UserHostCertificate, listed[0].Source)
require.Equal(t, "alice", listed[0].Username)
// With every row soft-deleted, the lookup returns nothing.
softDeleteAll()
got, err = loadHostCertIDsForSHA1DB(ctx, ds.reader(ctx), hostID, []string{sha1Hex})
require.NoError(t, err)
require.Empty(t, got)
}
// testUpdateHostCertificatesSelfHealsDuplicates reproduces the bad host state behind issue: host_certificates has no unique index
// on (host_id, sha1_sum), so concurrent re-ingestion of the same report (osquery resends results after a timed-out write, or a
// stale replica read) can leave duplicate active rows per certificate. The fix must soft-delete the duplicates on the next report
// (self-heal) and produce zero source-row writes on subsequent identical reports (convergence).
func testUpdateHostCertificatesSelfHealsDuplicates(t *testing.T, ds *Datastore) {
ctx := t.Context()
const (
hostID = uint(88)
hostUUID = "dup-rows-host-uuid"
)
certSys := mkTestCertRecord(t, hostID, "dup-sys.example.com", fleet.SystemHostCertificate, "")
certPairSys := mkTestCertRecord(t, hostID, "dup-pair.example.com", fleet.SystemHostCertificate, "")
// The same certificate observed in a user scope too (two source rows, one cert row).
pairUserCopy := *certPairSys
pairUserCopy.Source = fleet.UserHostCertificate
pairUserCopy.Username = "alice"
certPairUser := &pairUserCopy
ingest := func() {
sysCopy := *certSys
pairSysCopy := *certPairSys
pairUserCopyLocal := *certPairUser
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{&sysCopy, &pairSysCopy, &pairUserCopyLocal},
fleet.HostCertificateOriginOsquery, nil))
}
ingest()
// Simulate the concurrent double-ingest: duplicate every active cert row and its source rows under new ids.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_certificates
(host_id, sha1_sum, not_valid_after, not_valid_before, certificate_authority, common_name,
key_algorithm, key_strength, key_usage, serial, signing_algorithm, subject_country, subject_org,
subject_org_unit, subject_common_name, issuer_country, issuer_org, issuer_org_unit,
issuer_common_name, origin)
SELECT host_id, sha1_sum, not_valid_after, not_valid_before, certificate_authority, common_name,
key_algorithm, key_strength, key_usage, serial, signing_algorithm, subject_country, subject_org,
subject_org_unit, subject_common_name, issuer_country, issuer_org, issuer_org_unit,
issuer_common_name, origin
FROM host_certificates WHERE host_id = ? AND deleted_at IS NULL`, hostID)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_certificate_sources (host_certificate_id, source, username)
SELECT hcDup.id, hcs.source, hcs.username
FROM host_certificate_sources hcs
JOIN host_certificates hcOrig ON hcOrig.id = hcs.host_certificate_id
JOIN host_certificates hcDup
ON hcDup.host_id = hcOrig.host_id AND hcDup.sha1_sum = hcOrig.sha1_sum AND hcDup.id > hcOrig.id
WHERE hcOrig.host_id = ?`, hostID)
return err
})
countActiveCerts := func() int {
var n int
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &n,
`SELECT COUNT(*) FROM host_certificates WHERE host_id = ? AND deleted_at IS NULL`, hostID)
})
return n
}
sourceRowIDs := func() []uint {
var ids []uint
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &ids, `
SELECT hcs.id FROM host_certificate_sources hcs
JOIN host_certificates hc ON hc.id = hcs.host_certificate_id
WHERE hc.host_id = ? ORDER BY hcs.id`, hostID)
})
return ids
}
require.Equal(t, 4, countActiveCerts(), "setup: expected duplicated active cert rows")
require.Len(t, sourceRowIDs(), 6, "setup: expected duplicated source rows")
// One identical report self-heals: duplicates soft-deleted, their source rows removed.
ingest()
require.Equal(t, 2, countActiveCerts(), "expected duplicates to be soft-deleted")
healedSourceIDs := sourceRowIDs()
require.Len(t, healedSourceIDs, 3, "expected duplicate source rows to be removed")
// Convergence: further identical reports must not rewrite any source rows.
for range 2 {
ingest()
require.Equal(t, healedSourceIDs, sourceRowIDs(), "identical report must not rewrite host_certificate_sources rows")
require.Equal(t, 2, countActiveCerts())
}
// The API view is deduplicated again: exactly one row per (cert, source), with the right scopes surviving.
listed, _, err := ds.ListHostCertificates(ctx, hostID, fleet.ListOptions{OrderKey: "common_name", TestSecondaryOrderKey: "username"})
require.NoError(t, err)
type listedRow struct {
commonName string
source fleet.HostCertificateSource
username string
}
got := make([]listedRow, 0, len(listed))
for _, l := range listed {
got = append(got, listedRow{l.CommonName, l.Source, l.Username})
}
require.Equal(t, []listedRow{
{"dup-pair.example.com", fleet.SystemHostCertificate, ""},
{"dup-pair.example.com", fleet.UserHostCertificate, "alice"},
{"dup-sys.example.com", fleet.SystemHostCertificate, ""},
}, got)
}
// testUpdateHostCertificatesPreciseSourceWrites verifies that a source-set change touches only the rows that actually
// changed: unchanged source rows must keep their primary key (no delete-and-reinsert).
func testUpdateHostCertificatesPreciseSourceWrites(t *testing.T, ds *Datastore) {
ctx := t.Context()
const (
hostID = uint(89)
hostUUID = "precise-writes-host-uuid"
)
base := mkTestCertRecord(t, hostID, "precise.example.com", fleet.SystemHostCertificate, "")
ingest := func(sources ...fleet.HostCertificateScope) {
records := make([]*fleet.HostCertificateRecord, 0, len(sources))
for _, s := range sources {
rec := *base
rec.Source = s.Source
rec.Username = s.Username
records = append(records, &rec)
}
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID, records, fleet.HostCertificateOriginOsquery, nil))
}
type sourceRow struct {
ID uint `db:"id"`
Source string `db:"source"`
Username string `db:"username"`
}
sourceRows := func() []sourceRow {
var rows []sourceRow
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &rows, `
SELECT hcs.id, hcs.source, hcs.username FROM host_certificate_sources hcs
JOIN host_certificates hc ON hc.id = hcs.host_certificate_id
WHERE hc.host_id = ? ORDER BY hcs.source, hcs.username`, hostID)
})
return rows
}
ingest(
fleet.HostCertificateScope{Source: fleet.SystemHostCertificate},
fleet.HostCertificateScope{Source: fleet.UserHostCertificate, Username: "alice"},
)
before := sourceRows()
require.Len(t, before, 2)
systemRowID := before[0].ID
require.Equal(t, "system", before[0].Source)
require.Equal(t, "alice", before[1].Username)
// alice's scope is replaced by bob's; the system row must be untouched.
ingest(
fleet.HostCertificateScope{Source: fleet.SystemHostCertificate},
fleet.HostCertificateScope{Source: fleet.UserHostCertificate, Username: "bob"},
)
after := sourceRows()
require.Len(t, after, 2)
require.Equal(t, systemRowID, after[0].ID, "unchanged system source row must keep its primary key")
require.Equal(t, "system", after[0].Source)
require.Equal(t, "bob", after[1].Username, "alice's source row should be replaced by bob's")
}
// mkTestCertRecord builds a HostCertificateRecord for hostID with a random serial, valid from an hour ago to 24 hours
// from now, scoped to the given source and username.
func mkTestCertRecord(t *testing.T, hostID uint, commonName string, source fleet.HostCertificateSource, username string) *fleet.HostCertificateRecord {
tmpl := x509.Certificate{
Subject: pkix.Name{CommonName: commonName, Organization: []string{"Org"}},
Issuer: pkix.Name{CommonName: "issuer.example.com"},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
rec := generateTestHostCertificateRecord(t, hostID, &tmpl)
rec.Source = source
rec.Username = username
return rec
}
func generateTestHostCertificateRecord(t *testing.T, hostID uint, template *x509.Certificate) *fleet.HostCertificateRecord {
b, _, err := GenerateTestCertBytes(template)
require.NoError(t, err)
block, _ := pem.Decode(b)
parsed, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
require.NotNil(t, parsed)
return fleet.NewHostCertificateRecord(hostID, parsed)
}
// generateTestHostCertificateRecordWithParent creates a certificate signed by a parent certificate
// allowing for different issuer attributes (like country code) than the certificate itself.
// This is useful for testing scenarios where the certificate's subject country differs from
// the issuer's country, which is common in real-world certificate chains.
func generateTestHostCertificateRecordWithParent(t *testing.T, hostID uint, certTemplate, parentTemplate *x509.Certificate) *fleet.HostCertificateRecord {
// Generate parent key pair
parentPriv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
// Create parent certificate (self-signed)
parentCertBytes, err := x509.CreateCertificate(rand.Reader, parentTemplate, parentTemplate, &parentPriv.PublicKey, parentPriv)
require.NoError(t, err)
parentCert, err := x509.ParseCertificate(parentCertBytes)
require.NoError(t, err)
// Generate certificate key pair
certPriv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
// Create certificate signed by parent
certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, parentCert, &certPriv.PublicKey, parentPriv)
require.NoError(t, err)
parsed, err := x509.ParseCertificate(certBytes)
require.NoError(t, err)
require.NotNil(t, parsed)
return fleet.NewHostCertificateRecord(hostID, parsed)
}
// testUpdateHostCertificatesWindowsScopeReconciliation exercises the observed-scopes reconciliation used by the Windows
// ingestion path: osquery can only enumerate a user's certificates while that user is logged in, so a logged-off user's
// certificates must be preserved rather than soft-deleted.
func testUpdateHostCertificatesWindowsScopeReconciliation(t *testing.T, ds *Datastore) {
ctx := t.Context()
const (
hostID = uint(42)
hostUUID = "windows-scope-host-uuid"
)
mkCert := func(commonName string, source fleet.HostCertificateSource, username string) *fleet.HostCertificateRecord {
return mkTestCertRecord(t, hostID, commonName, source, username)
}
listKeys := func() []string {
certs, _, err := ds.ListHostCertificates(ctx, hostID, fleet.ListOptions{OrderKey: "common_name"})
require.NoError(t, err)
keys := make([]string, 0, len(certs))
for _, c := range certs {
keys = append(keys, fmt.Sprintf("%s|%s|%s", c.CommonName, c.Source, c.Username))
}
return keys
}
sysScope := fleet.HostCertificateScope{Source: fleet.SystemHostCertificate}
aliceScope := fleet.HostCertificateScope{Source: fleet.UserHostCertificate, Username: "alice"}
bobScope := fleet.HostCertificateScope{Source: fleet.UserHostCertificate, Username: "bob"}
certSys := mkCert("sys.example.com", fleet.SystemHostCertificate, "")
certAlice := mkCert("alice-old.example.com", fleet.UserHostCertificate, "alice")
certBob := mkCert("bob.example.com", fleet.UserHostCertificate, "bob")
// shared cert: present in the System store and in alice's store (same SHA1, two sources).
sharedSys := mkCert("shared.example.com", fleet.SystemHostCertificate, "")
sharedAliceClone := *sharedSys
sharedAlice := &sharedAliceClone
sharedAlice.Source = fleet.UserHostCertificate
sharedAlice.Username = "alice"
// 1. Initial report: alice and bob both logged in.
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{certSys, certAlice, certBob, sharedSys, sharedAlice},
fleet.HostCertificateOriginOsquery,
[]fleet.HostCertificateScope{sysScope, aliceScope, bobScope}))
require.ElementsMatch(t, []string{
"sys.example.com|system|",
"alice-old.example.com|user|alice",
"bob.example.com|user|bob",
"shared.example.com|system|",
"shared.example.com|user|alice",
}, listKeys())
// 2. Alice logs off: her hive is not loaded so her certs are simply absent.
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{certSys, certBob, sharedSys},
fleet.HostCertificateOriginOsquery,
[]fleet.HostCertificateScope{sysScope, bobScope}))
require.ElementsMatch(t, []string{
"sys.example.com|system|",
"alice-old.example.com|user|alice", // preserved (alice not observed)
"bob.example.com|user|bob",
"shared.example.com|system|",
"shared.example.com|user|alice", // preserved
}, listKeys())
// 3. Alice logs back in but has removed her old cert and her copy of the shared cert, and installed a new one.
certAlice2 := mkCert("alice-new.example.com", fleet.UserHostCertificate, "alice")
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{certSys, certBob, sharedSys, certAlice2},
fleet.HostCertificateOriginOsquery,
[]fleet.HostCertificateScope{sysScope, aliceScope, bobScope}))
require.ElementsMatch(t, []string{
"sys.example.com|system|",
"bob.example.com|user|bob",
"shared.example.com|system|", // alice's shared source dropped, system kept
"alice-new.example.com|user|alice",
}, listKeys())
// 4. A System certificate removed while present in the report → deleted, since System scope is always observed.
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{certBob, sharedSys, certAlice2},
fleet.HostCertificateOriginOsquery,
[]fleet.HostCertificateScope{sysScope, aliceScope, bobScope}))
require.ElementsMatch(t, []string{
"bob.example.com|user|bob",
"shared.example.com|system|",
"alice-new.example.com|user|alice",
}, listKeys())
// 5. Alice logs back in and re-installs the shared cert, so it is again in both the System store and her store.
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{certBob, sharedSys, sharedAlice, certAlice2},
fleet.HostCertificateOriginOsquery,
[]fleet.HostCertificateScope{sysScope, aliceScope, bobScope}))
require.ElementsMatch(t, []string{
"bob.example.com|user|bob",
"shared.example.com|system|",
"shared.example.com|user|alice",
"alice-new.example.com|user|alice",
}, listKeys())
// 6. The shared cert is removed from the machine store while alice is logged off, so it is absent from the report
// entirely. Its (observed) System source is dropped, but her (unobserved) source keeps the cert alive (it survives
// showing only her scope). alice's other cert is likewise preserved.
require.NoError(t, ds.UpdateHostCertificates(ctx, hostID, hostUUID,
[]*fleet.HostCertificateRecord{certBob},
fleet.HostCertificateOriginOsquery,
[]fleet.HostCertificateScope{sysScope, bobScope}))
require.ElementsMatch(t, []string{
"bob.example.com|user|bob",
"shared.example.com|user|alice", // System source dropped, alice's preserved
"alice-new.example.com|user|alice",
}, listKeys())
}
func testUpdateHostCertificatesSourcesIsolation(t *testing.T, ds *Datastore) {
// regression test for #30574
ctx := context.Background()
host1, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("host1-osquery-id"),
NodeKey: ptr.String("host1-node-key"),
UUID: "host1-uuid",
Hostname: "host1",
})
require.NoError(t, err)
host2, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("host2-osquery-id"),
NodeKey: ptr.String("host2-node-key"),
UUID: "host2-uuid",
Hostname: "host2",
})
require.NoError(t, err)
// Create identical certificates for both hosts (same SHA1 sum)
// This simulates the real-world scenario where multiple hosts have the same certificate
// installed (e.g., a company root CA certificate)
sharedCert := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "shared.example.com",
Organization: []string{"Shared Org"},
OrganizationalUnit: []string{"Engineering"},
},
Issuer: pkix.Name{
Country: []string{"US"},
CommonName: "issuer.example.com",
Organization: []string{"Issuer"},
},
SerialNumber: big.NewInt(12345),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
// Generate certificate records for both hosts using the same certificate data
// We need to create the certificate bytes once and reuse them to ensure same SHA1
certBytes, _, err := GenerateTestCertBytes(&sharedCert)
require.NoError(t, err)
block, _ := pem.Decode(certBytes)
parsed, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
host1Cert := fleet.NewHostCertificateRecord(host1.ID, parsed)
host1Cert.Source = fleet.UserHostCertificate
host1Cert.Username = "jdoe"
host2Cert := fleet.NewHostCertificateRecord(host2.ID, parsed)
host2Cert.Source = fleet.UserHostCertificate
host2Cert.Username = "jsmith"
// Add the same certificate to both hosts
require.NoError(t, ds.UpdateHostCertificates(ctx, host1.ID, host1.UUID, []*fleet.HostCertificateRecord{host1Cert}, fleet.HostCertificateOriginOsquery, nil))
require.NoError(t, ds.UpdateHostCertificates(ctx, host2.ID, host2.UUID, []*fleet.HostCertificateRecord{host2Cert}, fleet.HostCertificateOriginOsquery, nil))
// Verify both hosts have the correct certs, with the correct sources
host1Certs, _, err := ds.ListHostCertificates(ctx, host1.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host1Certs, 1)
require.Equal(t, fleet.UserHostCertificate, host1Certs[0].Source)
require.Equal(t, "jdoe", host1Certs[0].Username)
host2Certs, _, err := ds.ListHostCertificates(ctx, host2.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host2Certs, 1)
require.Equal(t, fleet.UserHostCertificate, host2Certs[0].Source)
require.Equal(t, "jsmith", host2Certs[0].Username)
// Trigger a change in host 2's cert sources to force delete/recreate of source records. Prior to the fix this
// wouldn't modify the correct certs because the DB query would return host 1's cert for the given hash.
host2CertUpdated := fleet.NewHostCertificateRecord(host2.ID, parsed)
host2CertUpdated.Source = fleet.UserHostCertificate
host2CertUpdated.Username = "janesmith"
require.NoError(t, ds.UpdateHostCertificates(ctx, host2.ID, host2.UUID, []*fleet.HostCertificateRecord{host2CertUpdated}, fleet.HostCertificateOriginOsquery, nil))
// Verify host1's certificate source was *not* updated
host1CertsAfter, _, err := ds.ListHostCertificates(ctx, host1.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host1CertsAfter, 1)
require.Equal(t, "jdoe", host1CertsAfter[0].Username)
// Verify host2's certificate source *was* updated
host2CertsAfter, _, err := ds.ListHostCertificates(ctx, host2.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host2CertsAfter, 1)
require.Equal(t, "janesmith", host2CertsAfter[0].Username)
// Verify no-op case
err = ds.UpdateHostCertificates(ctx, host2.ID, host2.UUID, []*fleet.HostCertificateRecord{host2CertUpdated}, fleet.HostCertificateOriginOsquery, nil)
require.NoError(t, err)
// Verify host2's certificate source was updated
host2CertsNoop, _, err := ds.ListHostCertificates(ctx, host2.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host2CertsNoop, 1)
require.Equal(t, fleet.UserHostCertificate, host2CertsNoop[0].Source)
require.Equal(t, "janesmith", host2CertsNoop[0].Username)
// Confirm that adding the cert to the system store gets picked up properly
systemCertOnHost2 := fleet.NewHostCertificateRecord(host2.ID, parsed)
systemCertOnHost2.Source = fleet.SystemHostCertificate
require.NoError(t, ds.UpdateHostCertificates(ctx, host2.ID, host2.UUID, []*fleet.HostCertificateRecord{host2CertUpdated, systemCertOnHost2}, fleet.HostCertificateOriginOsquery, nil))
// Verify host2 now has the certificate with both sources
host2CertsMultiSource, _, err := ds.ListHostCertificates(ctx, host2.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host2CertsMultiSource, 2)
var hasUserCert, hasSystemCert bool
for _, cert := range host2CertsMultiSource {
if cert.Source == fleet.UserHostCertificate {
require.Equal(t, "janesmith", cert.Username)
hasUserCert = true
} else {
require.Empty(t, cert.Username)
require.Equal(t, fleet.SystemHostCertificate, cert.Source)
hasSystemCert = true
}
}
require.True(t, hasUserCert)
require.True(t, hasSystemCert)
// Verify host1 still has only its original certificate source
host1CertsMultiSource, _, err := ds.ListHostCertificates(ctx, host1.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, host1CertsMultiSource, 1)
require.Equal(t, fleet.UserHostCertificate, host1CertsMultiSource[0].Source)
require.Equal(t, "jdoe", host1CertsMultiSource[0].Username)
}
// testUpdateHostCertificatesOriginScopedDelete verifies that each ingestion
// source only soft-deletes rows it owns: an osquery sync that omits an
// MDM-only cert must not remove that cert, and vice versa.
func testUpdateHostCertificatesOriginScopedDelete(t *testing.T, ds *Datastore) {
ctx := t.Context()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("origin-host-osquery-id"),
NodeKey: ptr.String("origin-host-node-key"),
UUID: "origin-host-uuid",
Hostname: "origin-host",
})
require.NoError(t, err)
mkCert := func(commonName string) *fleet.HostCertificateRecord {
template := x509.Certificate{
Subject: pkix.Name{CommonName: commonName, Organization: []string{"Org"}},
Issuer: pkix.Name{CommonName: "issuer", Organization: []string{"Issuer"}},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageDigitalSignature,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
certBytes, _, err := GenerateTestCertBytes(&template)
require.NoError(t, err)
block, _ := pem.Decode(certBytes)
parsed, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
rec := fleet.NewHostCertificateRecord(host.ID, parsed)
rec.Source = fleet.SystemHostCertificate
return rec
}
osqueryOnly := mkCert("osquery-only")
mdmOnly := mkCert("mdm-only")
// Initial state: osquery reports osqueryOnly; MDM reports mdmOnly.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{osqueryOnly}, fleet.HostCertificateOriginOsquery, nil))
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{mdmOnly}, fleet.HostCertificateOriginMDM, nil))
certs, _, err := ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 2)
originByCN := func(certs []*fleet.HostCertificateRecord) map[string]fleet.HostCertificateOrigin {
m := make(map[string]fleet.HostCertificateOrigin, len(certs))
for _, c := range certs {
m[c.CommonName] = c.Origin
}
return m
}
require.Equal(t, map[string]fleet.HostCertificateOrigin{
"osquery-only": fleet.HostCertificateOriginOsquery,
"mdm-only": fleet.HostCertificateOriginMDM,
}, originByCN(certs))
// Osquery sync runs again with an EMPTY cert list. The osquery-only cert
// should be soft-deleted, but the mdm-only cert must survive.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{}, fleet.HostCertificateOriginOsquery, nil))
certs, _, err = ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 1, "mdm-only cert should survive an osquery sync that omits it")
require.Equal(t, "mdm-only", certs[0].CommonName)
require.Equal(t, fleet.HostCertificateOriginMDM, certs[0].Origin)
// Now the symmetric case: osquery re-reports its cert, MDM sync runs with an
// empty list. The mdm-only cert should be soft-deleted, osquery-only survives.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{osqueryOnly}, fleet.HostCertificateOriginOsquery, nil))
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{}, fleet.HostCertificateOriginMDM, nil))
certs, _, err = ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 1, "osquery-only cert should survive an MDM sync that omits it")
require.Equal(t, "osquery-only", certs[0].CommonName)
require.Equal(t, fleet.HostCertificateOriginOsquery, certs[0].Origin)
}
// testUpdateHostCertificatesOriginDowngrade verifies that osquery rediscovery
// of an mdm-origin cert flips origin to osquery, and that the downgrade is
// one-way.
func testUpdateHostCertificatesOriginDowngrade(t *testing.T, ds *Datastore) {
ctx := t.Context()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: new("downgrade-host-osquery-id"),
NodeKey: new("downgrade-host-node-key"),
UUID: "downgrade-host-uuid",
Hostname: "downgrade-host",
})
require.NoError(t, err)
mkCert := func(commonName string) *fleet.HostCertificateRecord {
template := x509.Certificate{
Subject: pkix.Name{CommonName: commonName, Organization: []string{"Org"}},
Issuer: pkix.Name{CommonName: "issuer", Organization: []string{"Issuer"}},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageDigitalSignature,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
certBytes, _, err := GenerateTestCertBytes(&template)
require.NoError(t, err)
block, _ := pem.Decode(certBytes)
parsed, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
rec := fleet.NewHostCertificateRecord(host.ID, parsed)
rec.Source = fleet.SystemHostCertificate
return rec
}
rootCA := mkCert("user-installed-root-ca")
mdmDelivered := mkCert("mdm-delivered-only")
// MDM ingestion sees both certs first.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{rootCA, mdmDelivered}, fleet.HostCertificateOriginMDM, nil))
originByCN := func() map[string]fleet.HostCertificateOrigin {
certs, _, err := ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{})
require.NoError(t, err)
m := make(map[string]fleet.HostCertificateOrigin, len(certs))
for _, c := range certs {
m[c.CommonName] = c.Origin
}
return m
}
require.Equal(t, map[string]fleet.HostCertificateOrigin{
"user-installed-root-ca": fleet.HostCertificateOriginMDM,
"mdm-delivered-only": fleet.HostCertificateOriginMDM,
}, originByCN())
// Osquery rediscovers the Root CA; row downgrades. MDM-only cert unchanged.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{rootCA}, fleet.HostCertificateOriginOsquery, nil))
require.Equal(t, map[string]fleet.HostCertificateOrigin{
"user-installed-root-ca": fleet.HostCertificateOriginOsquery,
"mdm-delivered-only": fleet.HostCertificateOriginMDM,
}, originByCN())
// Downgrade is sticky across repeated osquery syncs.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{rootCA}, fleet.HostCertificateOriginOsquery, nil))
require.Equal(t, fleet.HostCertificateOriginOsquery, originByCN()["user-installed-root-ca"])
// Source-scoped delete preserved: osquery omitting the MDM-only cert does not soft-delete it.
certs, _, err := ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 2)
// One-way: MDM rediscovery does not re-upgrade the downgraded row.
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID,
[]*fleet.HostCertificateRecord{rootCA, mdmDelivered}, fleet.HostCertificateOriginMDM, nil))
require.Equal(t, map[string]fleet.HostCertificateOrigin{
"user-installed-root-ca": fleet.HostCertificateOriginOsquery,
"mdm-delivered-only": fleet.HostCertificateOriginMDM,
}, originByCN())
}
// testHostCertificateWithInvalidCountryCode tests that a certificate with a country code longer than the standard 2 letters works
func testHostCertificateWithInvalidCountryCode(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Create certificate templates for the actual certificates
certWithLongSubjectCountryTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{"Internet"},
CommonName: "long.example.com",
Organization: []string{"Org"},
OrganizationalUnit: []string{"Engineering"},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
parentWithNormalCountryTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "issuer.test.example.com",
Organization: []string{"Issuer"},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
IsCA: true,
BasicConstraintsValid: true,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-2 * time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(365 * 24 * time.Hour).Truncate(time.Second).UTC(),
}
certWithNormalCountryTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "another.long.example.com",
Organization: []string{"Org"},
OrganizationalUnit: []string{"Engineering"},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(48 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
parentWithLongIssuerCountryTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{"Internet"},
CommonName: "issuer.test.example.com",
Organization: []string{"Issuer"},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
IsCA: true,
BasicConstraintsValid: true,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-2 * time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(365 * 24 * time.Hour).Truncate(time.Second).UTC(),
}
payload := []*fleet.HostCertificateRecord{
generateTestHostCertificateRecordWithParent(t, 1, &certWithLongSubjectCountryTemplate, &parentWithNormalCountryTemplate),
generateTestHostCertificateRecordWithParent(t, 1, &certWithNormalCountryTemplate, &parentWithLongIssuerCountryTemplate),
}
// Manually override the country codes to preserve the full length for testing
// (they get truncated to 2 characters by the database VARCHAR(2) constraint)
payload[0].SubjectCountry = certWithLongSubjectCountryTemplate.Subject.Country[0]
payload[0].IssuerCountry = parentWithNormalCountryTemplate.Subject.Country[0]
payload[1].SubjectCountry = certWithNormalCountryTemplate.Subject.Country[0]
payload[1].IssuerCountry = parentWithLongIssuerCountryTemplate.Subject.Country[0]
require.NoError(t, ds.UpdateHostCertificates(ctx, 1, "95816502-d8c0-462c-882f-39991cc89a0c", payload, fleet.HostCertificateOriginOsquery, nil))
// verify that we saved the records correctly
certs, _, err := ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name"})
require.NoError(t, err)
require.Len(t, certs, 2)
// First certificate (another.long.example.com) - cert with normal country, issuer with long country
assert.Equal(t, []string{certWithNormalCountryTemplate.Subject.Country[0]}, []string{certs[0].SubjectCountry})
assert.Equal(t, []string{parentWithLongIssuerCountryTemplate.Subject.Country[0]}, []string{certs[0].IssuerCountry})
require.Equal(t, certWithNormalCountryTemplate.Subject.CommonName, certs[0].CommonName)
require.Equal(t, certWithNormalCountryTemplate.Subject.CommonName, certs[0].SubjectCommonName)
require.Equal(t, fleet.SystemHostCertificate, certs[0].Source)
// Second certificate (long.example.com) - cert with long subject country, issuer with normal country
assert.Equal(t, []string{certWithLongSubjectCountryTemplate.Subject.Country[0]}, []string{certs[1].SubjectCountry})
assert.Equal(t, []string{parentWithNormalCountryTemplate.Subject.Country[0]}, []string{certs[1].IssuerCountry})
require.Equal(t, certWithLongSubjectCountryTemplate.Subject.CommonName, certs[1].CommonName)
require.Equal(t, certWithLongSubjectCountryTemplate.Subject.CommonName, certs[1].SubjectCommonName)
require.Equal(t, fleet.SystemHostCertificate, certs[1].Source)
}
// testTruncateLongCertificateFields tests that all string fields in certificates are properly truncated
// when they exceed the database column limits
func testTruncateLongCertificateFields(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Create strings that exceed database limits
longString256 := strings.Repeat("a", 256) // Exceeds varchar(255)
longString300 := strings.Repeat("b", 300) // Exceeds varchar(255)
longCountry33 := strings.Repeat("c", 33) // Exceeds varchar(32)
longCountry50 := strings.Repeat("d", 50) // Exceeds varchar(32)
longUsername260 := strings.Repeat("u", 260) // Exceeds varchar(255)
// Expected truncated values
expectedString255 := strings.Repeat("a", 255)
expectedString255B := strings.Repeat("b", 255)
expectedCountry32 := strings.Repeat("c", 32)
expectedCountry32D := strings.Repeat("d", 32)
expectedUsername255 := strings.Repeat("u", 255)
// Create a certificate template with all fields exceeding limits
certTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{longCountry33},
CommonName: longString256,
Organization: []string{longString300},
OrganizationalUnit: []string{longString256},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
// Create a parent certificate for signing (with long fields)
parentTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{longCountry50},
CommonName: longString300,
Organization: []string{longString256},
OrganizationalUnit: []string{longString300},
},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
IsCA: true,
BasicConstraintsValid: true,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-2 * time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(365 * 24 * time.Hour).Truncate(time.Second).UTC(),
}
// Generate the certificate
cert := generateTestHostCertificateRecordWithParent(t, 1, &certTemplate, &parentTemplate)
// Override all string fields with long values to test truncation
cert.CommonName = longString256
cert.KeyAlgorithm = longString300
cert.KeyUsage = longString256
cert.Serial = longString300
cert.SigningAlgorithm = longString256
cert.SubjectCountry = longCountry33
cert.SubjectOrganization = longString300
cert.SubjectOrganizationalUnit = longString256
cert.SubjectCommonName = longString300
cert.IssuerCountry = longCountry50
cert.IssuerOrganization = longString256
cert.IssuerOrganizationalUnit = longString300
cert.IssuerCommonName = longString256
cert.Username = longUsername260
cert.Source = fleet.UserHostCertificate
// Create a host for testing
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("test-truncate-host-osquery-id"),
NodeKey: ptr.String("test-truncate-host-node-key"),
UUID: "test-truncate-host-uuid",
Hostname: "test-truncate-host",
})
require.NoError(t, err)
// Update certificates - this should trigger truncation
err = ds.UpdateHostCertificates(ctx, host.ID, host.UUID, []*fleet.HostCertificateRecord{cert}, fleet.HostCertificateOriginOsquery, nil)
require.NoError(t, err)
// Retrieve the certificate and verify all fields were truncated
certs, _, err := ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 1)
savedCert := certs[0]
// Verify all varchar(255) fields were truncated to 255 characters
assert.Equal(t, expectedString255, savedCert.CommonName, "CommonName should be truncated to 255 chars")
assert.Equal(t, expectedString255B, savedCert.KeyAlgorithm, "KeyAlgorithm should be truncated to 255 chars")
assert.Equal(t, expectedString255, savedCert.KeyUsage, "KeyUsage should be truncated to 255 chars")
assert.Equal(t, expectedString255B, savedCert.Serial, "Serial should be truncated to 255 chars")
assert.Equal(t, expectedString255, savedCert.SigningAlgorithm, "SigningAlgorithm should be truncated to 255 chars")
assert.Equal(t, expectedString255B, savedCert.SubjectOrganization, "SubjectOrganization should be truncated to 255 chars")
assert.Equal(t, expectedString255, savedCert.SubjectOrganizationalUnit, "SubjectOrganizationalUnit should be truncated to 255 chars")
assert.Equal(t, expectedString255B, savedCert.SubjectCommonName, "SubjectCommonName should be truncated to 255 chars")
assert.Equal(t, expectedString255, savedCert.IssuerOrganization, "IssuerOrganization should be truncated to 255 chars")
assert.Equal(t, expectedString255B, savedCert.IssuerOrganizationalUnit, "IssuerOrganizationalUnit should be truncated to 255 chars")
assert.Equal(t, expectedString255, savedCert.IssuerCommonName, "IssuerCommonName should be truncated to 255 chars")
assert.Equal(t, expectedUsername255, savedCert.Username, "Username should be truncated to 255 chars")
// Verify varchar(32) country fields were truncated to 32 characters
assert.Equal(t, expectedCountry32, savedCert.SubjectCountry, "SubjectCountry should be truncated to 32 chars")
assert.Equal(t, expectedCountry32D, savedCert.IssuerCountry, "IssuerCountry should be truncated to 32 chars")
// Verify non-string fields remain unchanged
assert.Equal(t, fleet.UserHostCertificate, savedCert.Source, "Source should not be changed")
assert.Equal(t, host.ID, savedCert.HostID, "HostID should not be changed")
}
func testListHostCertificatesCountMatches(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create host
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("count-mismatch-host-osquery-id"),
NodeKey: ptr.String("count-mismatch-host-node-key"),
UUID: "count-mismatch-host-uuid",
Hostname: "count-mismatch-host",
})
require.NoError(t, err)
// create a cert template and record
certTemplate := x509.Certificate{
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "count.example.com",
Organization: []string{"Org"},
OrganizationalUnit: []string{"Eng"},
},
Issuer: pkix.Name{
Country: []string{"US"},
CommonName: "issuer.example.com",
Organization: []string{"Issuer"},
},
SerialNumber: big.NewInt(424242),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
certRec := generateTestHostCertificateRecord(t, host.ID, &certTemplate)
// Update using ds.UpdateHostCertificates with two sources: system and user
certSys := *certRec
certSys.Source = fleet.SystemHostCertificate
certSys.Username = ""
certUser := *certRec
certUser.Source = fleet.UserHostCertificate
certUser.Username = "alice"
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, []*fleet.HostCertificateRecord{&certSys, &certUser}, fleet.HostCertificateOriginOsquery, nil))
// Now list with metadata
certs, meta, err := ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{IncludeMetadata: true})
require.NoError(t, err)
require.NotNil(t, meta)
// We expect two returned rows (one per source)
require.Len(t, certs, 2)
require.Equal(t, uint(len(certs)), meta.TotalResults, "expected total results to match returned rows")
}
// testSoftDeleteMDMHostCertificatesForUnenrolledHosts verifies the cron sweep
// clears MDM-origin certs for hosts reporting host_mdm.enrolled=0 while leaving
// osquery-origin certs and enrolled hosts' certs intact.
func testSoftDeleteMDMHostCertificatesForUnenrolledHosts(t *testing.T, ds *Datastore) {
ctx := t.Context()
mkCert := func(hostID uint, commonName string) *fleet.HostCertificateRecord {
template := x509.Certificate{
Subject: pkix.Name{CommonName: commonName},
Issuer: pkix.Name{CommonName: "issuer"},
SerialNumber: big.NewInt(mathrand.Int64()), // nolint:gosec
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
BasicConstraintsValid: true,
}
certBytes, _, err := GenerateTestCertBytes(&template)
require.NoError(t, err)
block, _ := pem.Decode(certBytes)
parsed, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
return fleet.NewHostCertificateRecord(hostID, parsed)
}
newHost := func(suffix string) *fleet.Host {
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("sweep-osq-" + suffix),
NodeKey: ptr.String("sweep-nk-" + suffix),
UUID: "sweep-uuid-" + suffix,
Hostname: "sweep-" + suffix,
Platform: "darwin",
})
require.NoError(t, err)
return h
}
// Unenrolled host with both an osquery-origin and an mdm-origin cert.
unenrolled := newHost("unenrolled")
require.NoError(t, ds.UpdateHostCertificates(ctx, unenrolled.ID, unenrolled.UUID,
[]*fleet.HostCertificateRecord{mkCert(unenrolled.ID, "u-osquery")}, fleet.HostCertificateOriginOsquery, nil))
require.NoError(t, ds.UpdateHostCertificates(ctx, unenrolled.ID, unenrolled.UUID,
[]*fleet.HostCertificateRecord{mkCert(unenrolled.ID, "u-mdm")}, fleet.HostCertificateOriginMDM, nil))
require.NoError(t, ds.SetOrUpdateMDMData(ctx, unenrolled.ID, false, false, "https://mdm.example.com", false, "Fleet", "", false))
// Enrolled host with an mdm-origin cert that must NOT be swept.
enrolled := newHost("enrolled")
require.NoError(t, ds.UpdateHostCertificates(ctx, enrolled.ID, enrolled.UUID,
[]*fleet.HostCertificateRecord{mkCert(enrolled.ID, "e-mdm")}, fleet.HostCertificateOriginMDM, nil))
require.NoError(t, ds.SetOrUpdateMDMData(ctx, enrolled.ID, false, true, "https://mdm.example.com", false, "Fleet", "", false))
count, err := ds.SoftDeleteMDMHostCertificatesForUnenrolledHosts(ctx)
require.NoError(t, err)
require.EqualValues(t, 1, count)
// Unenrolled host: only the osquery cert remains.
certs, _, err := ds.ListHostCertificates(ctx, unenrolled.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 1)
require.Equal(t, "u-osquery", certs[0].CommonName)
// Enrolled host: mdm cert untouched.
certs, _, err = ds.ListHostCertificates(ctx, enrolled.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, certs, 1)
require.Equal(t, "e-mdm", certs[0].CommonName)
}