Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DigiCert certificate management #23994

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
"jira": null,
"zendesk": null,
"google_calendar": null,
"ndes_scep_proxy": null
"ndes_scep_proxy": null,
"digicert_pki": null
},
"mdm": {
"apple_bm_terms_expired": false,
Expand Down
1 change: 1 addition & 0 deletions cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ spec:
enable_host_users: true
enable_software_inventory: false
integrations:
digicert_pki: null
google_calendar: null
jira: null
ndes_scep_proxy: null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@
"jira": null,
"zendesk": null,
"google_calendar": null,
"ndes_scep_proxy": null
"ndes_scep_proxy": null,
"digicert_pki": null
},
"update_interval": {
"osquery_detail": "1h0m0s",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ spec:
enable_host_users: true
enable_software_inventory: false
integrations:
digicert_pki: null
google_calendar: null
jira: null
ndes_scep_proxy: null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ spec:
activity_expiry_enabled: false
activity_expiry_window: 0
integrations:
digicert_pki: null
google_calendar: null
jira: null
ndes_scep_proxy: null
Expand Down
1 change: 1 addition & 0 deletions cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ spec:
activity_expiry_enabled: false
activity_expiry_window: 0
integrations:
digicert_pki: null
google_calendar: null
jira: null
ndes_scep_proxy: null
Expand Down
98 changes: 98 additions & 0 deletions server/datastore/mysql/certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package mysql

import (
"context"
"database/sql"
"errors"

"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
)

func (ds *Datastore) GetPKICertificate(ctx context.Context, name string) (*fleet.PKICertificate, error) {
stmt := `
SELECT name, cert_pem, key_pem, sha256_hex, not_valid_after
FROM pki_certificates
WHERE name = ?
`
var cert fleet.PKICertificate
err := sqlx.GetContext(ctx, ds.reader(ctx), &cert, stmt, name)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, notFound("pki certificate").WithName(name)
case err != nil:
return nil, ctxerr.Wrap(ctx, err, "get pki certificate")
}
if len(cert.Cert) > 0 {
cert.Cert, err = decrypt(cert.Cert, ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decrypting pki cert")
}
}
if len(cert.Key) > 0 {
cert.Key, err = decrypt(cert.Key, ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decrypting pki key")
}
}
return &cert, nil
}

func (ds *Datastore) SavePKICertificate(ctx context.Context, cert *fleet.PKICertificate) error {
const stmt = `
INSERT INTO
pki_certificates (name, cert_pem, key_pem, not_valid_after, sha256)
VALUES
(?, ?, ?, ?, UNHEX(?))
ON DUPLICATE KEY UPDATE
cert_pem = VALUES(cert_pem),
key_pem = VALUES(key_pem),
not_valid_after = VALUES(not_valid_after),
sha256 = VALUES(sha256)
`
var err error
var encryptedCert, encryptedKey []byte
if len(cert.Cert) > 0 {
encryptedCert, err = encrypt(cert.Cert, ds.serverPrivateKey)
if err != nil {
return ctxerr.Wrap(ctx, err, "encrypting pki cert")
}
}
if len(cert.Key) > 0 {
encryptedKey, err = encrypt(cert.Key, ds.serverPrivateKey)
if err != nil {
return ctxerr.Wrap(ctx, err, "encrypting pki key")
}
}
_, err = ds.writer(ctx).ExecContext(ctx, stmt, cert.Name, encryptedCert, encryptedKey, cert.NotValidAfter, cert.Sha256)
if err != nil {
return ctxerr.Wrap(ctx, err, "save pki certificate")
}
return nil
}

func (ds *Datastore) ListPKICertificates(ctx context.Context) ([]fleet.PKICertificate, error) {
// Since name is the primary key, results are always sorted by name
stmt := `
SELECT name, not_valid_after, sha256_hex
FROM pki_certificates
`
var certs []fleet.PKICertificate
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &certs, stmt); err != nil {
return nil, ctxerr.Wrap(ctx, err, "list pki certificates")
}
return certs, nil
}

func (ds *Datastore) DeletePKICertificate(ctx context.Context, name string) error {
stmt := `
DELETE FROM pki_certificates
WHERE name = ?
`
_, err := ds.writer(ctx).ExecContext(ctx, stmt, name)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete pki certificate")
}
return nil
}
123 changes: 123 additions & 0 deletions server/datastore/mysql/certificates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package mysql

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"io"
"strings"
"testing"
"time"

"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCertificates(t *testing.T) {
ds := CreateMySQLDS(t)

cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"SavePKICertificate", testSavePKICertificate},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}

func testSavePKICertificate(t *testing.T, ds *Datastore) {
ctx := context.Background()

// Failure case
err := ds.SavePKICertificate(ctx, &fleet.PKICertificate{})
require.Error(t, err)

certs, err := ds.ListPKICertificates(ctx)
require.NoError(t, err)
assert.Empty(t, certs)

// Save name and key only
pkiCert := &fleet.PKICertificate{
Name: "test",
Key: []byte("key"),
}

// Save name and key only
err = ds.SavePKICertificate(ctx, pkiCert)
require.NoError(t, err)
retrieved, err := ds.GetPKICertificate(ctx, pkiCert.Name)
require.NoError(t, err)
assert.Equal(t, pkiCert.Name, retrieved.Name)
assert.Equal(t, pkiCert.Key, retrieved.Key)

// Update cert with other values
pkiCert.Cert = []byte("cert")
now := time.Now().Truncate(time.Second).UTC()
pkiCert.NotValidAfter = &now
h := sha256.New()
_, _ = io.Copy(h, bytes.NewReader(pkiCert.Cert)) // writes to a Hash can never fail
sha256Hash := hex.EncodeToString(h.Sum(nil))
pkiCert.Sha256 = &sha256Hash
err = ds.SavePKICertificate(ctx, pkiCert)
require.NoError(t, err)

retrieved, err = ds.GetPKICertificate(ctx, pkiCert.Name)
require.NoError(t, err)
assert.Equal(t, pkiCert.Name, retrieved.Name)
assert.Equal(t, pkiCert.Key, retrieved.Key)
assert.Equal(t, pkiCert.Cert, retrieved.Cert)
assert.Equal(t, pkiCert.NotValidAfter, retrieved.NotValidAfter)
assert.Equal(t, pkiCert.Sha256, retrieved.Sha256)
pkiCerts := []*fleet.PKICertificate{pkiCert}

// Now save all values at once
later := time.Now().Truncate(time.Second).UTC().Add(time.Hour)
fakeSha256 := strings.Repeat("b", 64)
pkiCert = &fleet.PKICertificate{
Name: "test2",
Key: []byte("key2"),
Cert: []byte("cert2"),
NotValidAfter: &later,
Sha256: &fakeSha256,
}
err = ds.SavePKICertificate(ctx, pkiCert)
require.NoError(t, err)

retrieved, err = ds.GetPKICertificate(ctx, pkiCert.Name)
require.NoError(t, err)
assert.Equal(t, pkiCert.Name, retrieved.Name)
assert.Equal(t, pkiCert.Key, retrieved.Key)
assert.Equal(t, pkiCert.Cert, retrieved.Cert)
assert.Equal(t, pkiCert.NotValidAfter, retrieved.NotValidAfter)
assert.Equal(t, pkiCert.Sha256, retrieved.Sha256)
pkiCerts = append(pkiCerts, pkiCert)

certs, err = ds.ListPKICertificates(ctx)
require.NoError(t, err)
require.Len(t, certs, 2)
for i, cert := range certs {
assert.Equal(t, pkiCerts[i].Name, cert.Name)
assert.Empty(t, cert.Cert)
assert.Empty(t, cert.Key)
assert.Equal(t, pkiCerts[i].NotValidAfter, cert.NotValidAfter)
assert.Equal(t, pkiCerts[i].Sha256, cert.Sha256)
}

// Delete certs
require.NoError(t, ds.DeletePKICertificate(ctx, pkiCerts[0].Name))
certs, err = ds.ListPKICertificates(ctx)
require.NoError(t, err)
assert.Len(t, certs, 1)
require.NoError(t, ds.DeletePKICertificate(ctx, pkiCerts[1].Name))
certs, err = ds.ListPKICertificates(ctx)
require.NoError(t, err)
assert.Empty(t, certs)

}
14 changes: 13 additions & 1 deletion server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2669,10 +2669,22 @@ func (ds *Datastore) HostIDsByIdentifier(ctx context.Context, filter fleet.TeamF
}

func (ds *Datastore) ListHostsLiteByUUIDs(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
return ds.listHostsLiteByUUIDs(ctx, &filter, uuids)
}

func (ds *Datastore) ListHostsLiteByUUIDsNoFilter(ctx context.Context, uuids []string) ([]*fleet.Host, error) {
return ds.listHostsLiteByUUIDs(ctx, nil, uuids)
}

func (ds *Datastore) listHostsLiteByUUIDs(ctx context.Context, filter *fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
if len(uuids) == 0 {
return nil, nil
}

whereFilterHostsByTeams := "TRUE"
if filter != nil {
whereFilterHostsByTeams = ds.whereFilterHostsByTeams(*filter, "h")
}
stmt := fmt.Sprintf(`
SELECT
h.id,
Expand Down Expand Up @@ -2704,7 +2716,7 @@ LEFT OUTER JOIN
ON
hdep.host_id = h.id AND hdep.deleted_at IS NULL
WHERE h.uuid IN (?) AND %s
`, ds.whereFilterHostsByTeams(filter, "h"),
`, whereFilterHostsByTeams,
)

stmt, args, err := sqlx.In(stmt, uuids)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20241121111624, Down_20241121111624)
}

func Up_20241121111624(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE pki_certificates (
name VARCHAR(255) PRIMARY KEY,
cert_pem BLOB NULL, -- 65,535 bytes max
key_pem BLOB NOT NULL, -- 65,535 bytes max
not_valid_after DATETIME NULL,
sha256 BINARY(32) NULL,
sha256_hex CHAR(64) GENERATED ALWAYS AS (LOWER(HEX(sha256))) VIRTUAL NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`)
if err != nil {
return fmt.Errorf("failed to create pki_certificates table: %w", err)
}
return nil
}

func Down_20241121111624(_ *sql.Tx) error {
return nil
}
20 changes: 17 additions & 3 deletions server/datastore/mysql/schema.sql

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions server/fleet/certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package fleet

import "time"

type PKICertificate struct {
Name string `json:"name" db:"name"`
Cert []byte `json:"-" db:"cert_pem"`
Key []byte `json:"-" db:"key_pem"`
Sha256 *string `json:"sha256" db:"sha256_hex"`
NotValidAfter *time.Time `json:"not_valid_after" db:"not_valid_after"`
}
11 changes: 11 additions & 0 deletions server/fleet/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ type Datastore interface {
// identifier. The "lite" version is a subset of the fields related to the
// host. See the implementation for the exact list.
ListHostsLiteByUUIDs(ctx context.Context, filter TeamFilter, uuids []string) ([]*Host, error)
// ListHostsLiteByUUIDsNoFilter is the same as ListHostsLiteByUUIDs but without any filtering.
ListHostsLiteByUUIDsNoFilter(ctx context.Context, uuids []string) ([]*Host, error)

// ListHostsLiteByIDs returns the "lite" version of hosts corresponding to
// the provided ids. The "lite" version is a subset of the fields related to
Expand Down Expand Up @@ -1864,6 +1866,15 @@ type Datastore interface {

// CleanUpMDMManagedCertificates removes all managed certificates that are not associated with any host+profile.
CleanUpMDMManagedCertificates(ctx context.Context) error

// GetPKICertificate returns the PKI certificate with the given name.
GetPKICertificate(ctx context.Context, name string) (*PKICertificate, error)
// SavePKICertificate creates or updates the PKI certificate using the provided struct.
SavePKICertificate(ctx context.Context, cert *PKICertificate) error
// ListPKICertificates returns metadata for PKI certificates.
ListPKICertificates(ctx context.Context) ([]PKICertificate, error)
// DeletePKICertificate deletes the PKI certificate with the given name.
DeletePKICertificate(ctx context.Context, name string) error
}

// MDMAppleStore wraps nanomdm's storage and adds methods to deal with
Expand Down
Loading
Loading