From 9c4152d5bd959ce6eeda4f34f804ca4626ec0905 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 20 Nov 2024 16:34:00 -0600 Subject: [PATCH 01/12] Initial checkin. No testing. --- server/fleet/integrations.go | 22 ++++++ server/fleet/mdm.go | 3 + server/fleet/service.go | 5 ++ server/service/certificates.go | 123 +++++++++++++++++++++++++++++++++ server/service/handler.go | 3 + 5 files changed, 156 insertions(+) create mode 100644 server/service/certificates.go diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go index 887db9a54ee2..6368a7b2affd 100644 --- a/server/fleet/integrations.go +++ b/server/fleet/integrations.go @@ -7,6 +7,7 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/service/externalsvc" @@ -361,6 +362,26 @@ type NDESSCEPProxyIntegration struct { Password string `json:"password"` // not stored here -- encrypted in DB } +type AuthenticationCertificate struct { + ValidTo time.Time `json:"valid_to"` +} + +type CertificateSAN struct { + UserPrincipalNames []string `json:"user_principal_names"` +} + +type CertificateTemplate struct { + ProfileID string `json:"profile_id"` + SeatID string `json:"seat_id"` + CommonName string `json:"common_name"` + SAN CertificateSAN `json:"san"` +} + +type DigiCertIntegration struct { + Certificate optjson.Any[AuthenticationCertificate] `json:"certificate"` + Templates optjson.Slice[CertificateTemplate] `json:"templates"` +} + // Integrations configures the integrations with external systems. type Integrations struct { Jira []*JiraIntegration `json:"jira"` @@ -368,6 +389,7 @@ type Integrations struct { GoogleCalendar []*GoogleCalendarIntegration `json:"google_calendar"` // NDESSCEPProxy settings. In JSON, not specifying this field means keep current setting, null means clear settings. NDESSCEPProxy optjson.Any[NDESSCEPProxyIntegration] `json:"ndes_scep_proxy"` + DigiCert optjson.Slice[DigiCertIntegration] `json:"digicert"` } func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) { diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 55e28bc7b945..993b72174999 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -723,6 +723,9 @@ const ( // MDMAssetNDESPassword is the password used to retrieve SCEP challenge from // NDES SCEP server. It is used by Fleet's SCEP proxy. MDMAssetNDESPassword MDMAssetName = "ndes_password" + // MDMAssetAuthenticationCertificate is the authentication (RA) certificate for DigiCert. + // TODO: This should be in a separate table since we can have multiple CAs + MDMAssetAuthenticationCertificate MDMAssetName = "authentication_certificate" ) type MDMConfigAsset struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index ad12547a1d5f..94f812989161 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1165,6 +1165,11 @@ type Service interface { // CalendarWebhook handles incoming calendar callback requests. CalendarWebhook(ctx context.Context, eventUUID string, channelID string, resourceState string) error + + // ///////////////////////////////////////////////////////////////////////////// + // Certificate management + + UploadCert(ctx context.Context, cert io.ReadSeeker) error } type KeyValueStore interface { diff --git a/server/service/certificates.go b/server/service/certificates.go new file mode 100644 index 000000000000..edbf0b2ad324 --- /dev/null +++ b/server/service/certificates.go @@ -0,0 +1,123 @@ +package service + +import ( + "context" + "encoding/pem" + "io" + "mime/multipart" + "net/http" + + "github.com/docker/go-units" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" +) + +// ////////////////////////////////////////////////////////////////////////////// +// POST /fleet/certificate_mgmt/certificate +// ////////////////////////////////////////////////////////////////////////////// + +type uploadCertRequest struct { + // TODO: Add an identifier + File *multipart.FileHeader +} + +func (uploadCertRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { + decoded := uploadCertRequest{} + err := r.ParseMultipartForm(512 * units.MiB) + if err != nil { + return nil, &fleet.BadRequestError{ + Message: "failed to parse multipart form", + InternalErr: err, + } + } + + if r.MultipartForm.File["certificate"] == nil || len(r.MultipartForm.File["certificate"]) == 0 { + return nil, &fleet.BadRequestError{ + Message: "certificate multipart field is required", + InternalErr: err, + } + } + + decoded.File = r.MultipartForm.File["certificate"][0] + + return &decoded, nil +} + +type uploadCertResponse struct { + Err error `json:"error,omitempty"` +} + +func (r uploadCertResponse) error() error { + return r.Err +} + +func (r uploadCertResponse) Status() int { return http.StatusAccepted } + +func uploadCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*uploadCertRequest) + file, err := req.File.Open() + if err != nil { + return uploadCertResponse{Err: err}, nil + } + defer file.Close() + + if err := svc.UploadCert(ctx, file); err != nil { + return &uploadCertResponse{Err: err}, nil + } + + return &uploadMDMAppleAPNSCertResponse{}, nil +} + +func (svc *Service) UploadCert(ctx context.Context, cert io.ReadSeeker) error { + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionWrite); err != nil { + return err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return ctxerr.New(ctx, + "Couldn't upload certificate. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if cert == nil { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Invalid certificate. Please provide a valid certificate.")) + } + + // Get cert file bytes + certBytes, err := io.ReadAll(cert) + if err != nil { + return ctxerr.Wrap(ctx, err, "reading certificate") + } + + // Validate cert + block, _ := pem.Decode(certBytes) + if block == nil { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Invalid certificate. Please provide a valid certificate.")) + } + + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { + return err + } + + // TODO: Parse the certificate to determine expiration date + + // delete the old certificate and insert the new one + // TODO(roberto): replacing the certificate should be done in a single transaction in the DB + err = svc.ds.DeleteMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAuthenticationCertificate}) + if err != nil { + return ctxerr.Wrap(ctx, err, "deleting old apns cert from db") + } + err = svc.ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{ + {Name: fleet.MDMAssetAuthenticationCertificate, Value: certBytes}, + }, nil) + if err != nil { + return ctxerr.Wrap(ctx, err, "writing cert to db") + } + + return nil + +} diff --git a/server/service/handler.go b/server/service/handler.go index c79f2daaf669..bb6e35cd8c0a 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -787,6 +787,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // input to `fleetctl apply` ue.POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{}) + // Certificate management + ue.POST("/api/_version_/fleet/certificate_mgmt/certificate", uploadCertEndpoint, uploadCertRequest{}) + errorLimiter := ratelimit.NewErrorMiddleware(limitStore) // device-authenticated endpoints From fc0eb4c22f1bad7fd460a947d5e1235f4c5a098f Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 08:53:57 -0600 Subject: [PATCH 02/12] Modifications to configs. Certificates will be stored separately. --- server/fleet/integrations.go | 12 ++++-------- server/service/appconfig.go | 27 ++++++++++++++++++++++++++ server/service/certificates.go | 35 ++++++++++++++++++++++++++++++++-- server/service/handler.go | 4 +++- 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go index 6368a7b2affd..8ca5ac8a0fe1 100644 --- a/server/fleet/integrations.go +++ b/server/fleet/integrations.go @@ -7,7 +7,6 @@ import ( "net/url" "strconv" "strings" - "time" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/service/externalsvc" @@ -362,15 +361,13 @@ type NDESSCEPProxyIntegration struct { Password string `json:"password"` // not stored here -- encrypted in DB } -type AuthenticationCertificate struct { - ValidTo time.Time `json:"valid_to"` -} - type CertificateSAN struct { UserPrincipalNames []string `json:"user_principal_names"` } type CertificateTemplate struct { + Name string `json:"name"` // Limit the max number of characters, to say, 32? + CAName string `json:"ca_name"` // Limit the max number of characters, to say, 32? ProfileID string `json:"profile_id"` SeatID string `json:"seat_id"` CommonName string `json:"common_name"` @@ -378,8 +375,7 @@ type CertificateTemplate struct { } type DigiCertIntegration struct { - Certificate optjson.Any[AuthenticationCertificate] `json:"certificate"` - Templates optjson.Slice[CertificateTemplate] `json:"templates"` + Templates []CertificateTemplate `json:"templates"` } // Integrations configures the integrations with external systems. @@ -389,7 +385,7 @@ type Integrations struct { GoogleCalendar []*GoogleCalendarIntegration `json:"google_calendar"` // NDESSCEPProxy settings. In JSON, not specifying this field means keep current setting, null means clear settings. NDESSCEPProxy optjson.Any[NDESSCEPProxyIntegration] `json:"ndes_scep_proxy"` - DigiCert optjson.Slice[DigiCertIntegration] `json:"digicert"` + DigiCert optjson.Any[DigiCertIntegration] `json:"digicert"` } func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index ad8778b57035..48359069123a 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -404,6 +404,33 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + if newAppConfig.Integrations.DigiCert.Set && newAppConfig.Integrations.DigiCert.Valid && !license.IsPremium() { + invalid.Append("integrations.digicert", ErrMissingLicense.Error()) + appConfig.Integrations.DigiCert.Valid = false + } else { + switch { + case !newAppConfig.Integrations.DigiCert.Set: + // Nothing is set -- keep the old value + appConfig.Integrations.DigiCert = oldAppConfig.Integrations.DigiCert + case !newAppConfig.Integrations.DigiCert.Valid: + // User is explicitly clearing this setting + appConfig.Integrations.DigiCert.Valid = false + default: + templateNames := make(map[string]struct{}, len(newAppConfig.Integrations.DigiCert.Value.Templates)) + for _, template := range appConfig.Integrations.DigiCert.Value.Templates { + // TODO: Validate length? + template.Name = fleet.Preprocess(template.Name) + if _, ok := templateNames[template.Name]; ok { + invalid.Append("integrations.digicert.templates", "template names must be unique") + break + } + templateNames[template.Name] = struct{}{} + // TODO: Validate length? + template.CAName = fleet.Preprocess(template.CAName) + } + } + } + // EnableDiskEncryption is an optjson.Bool field in order to support the // legacy field under "mdm.macos_settings". If the field provided to the // PATCH endpoint is set but invalid (that is, "enable_disk_encryption": diff --git a/server/service/certificates.go b/server/service/certificates.go index edbf0b2ad324..e74e2b895751 100644 --- a/server/service/certificates.go +++ b/server/service/certificates.go @@ -6,6 +6,8 @@ import ( "io" "mime/multipart" "net/http" + "net/url" + "regexp" "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -16,8 +18,10 @@ import ( // POST /fleet/certificate_mgmt/certificate // ////////////////////////////////////////////////////////////////////////////// +var certificatePathRegexp = regexp.MustCompile(`/certificate/(?P.*)$`) + type uploadCertRequest struct { - // TODO: Add an identifier + Name string File *multipart.FileHeader } @@ -40,6 +44,21 @@ func (uploadCertRequest) DecodeRequest(ctx context.Context, r *http.Request) (in decoded.File = r.MultipartForm.File["certificate"][0] + // regex to get and validate the name + matches := certificatePathRegexp.FindStringSubmatch(r.URL.Path) + for i, name := range certificatePathRegexp.SubexpNames() { + if name == "name" { + certName, err := url.QueryUnescape(matches[i]) + if err != nil { + return nil, &fleet.BadRequestError{ + Message: "certificate name has invalid format", + InternalErr: err, + } + } + decoded.Name = fleet.Preprocess(certName) + } + } + return &decoded, nil } @@ -103,7 +122,10 @@ func (svc *Service) UploadCert(ctx context.Context, cert io.ReadSeeker) error { return err } - // TODO: Parse the certificate to determine expiration date + // TODO: Parse the certificate to determine expiration date and fingerprint + // h := sha256.New() + // _, _ = io.Copy(h, bytes.NewReader(certBytes)) // writes to a Hash can never fail + // sha256Hash := hex.EncodeToString(h.Sum(nil)) // delete the old certificate and insert the new one // TODO(roberto): replacing the certificate should be done in a single transaction in the DB @@ -121,3 +143,12 @@ func (svc *Service) UploadCert(ctx context.Context, cert io.ReadSeeker) error { return nil } + +type getCertRequest struct { +} + +func getCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + // TODO: Implementation + _ = request.(*getCertRequest) + return nil, nil +} diff --git a/server/service/handler.go b/server/service/handler.go index bb6e35cd8c0a..95197d56d59d 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -788,7 +788,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{}) // Certificate management - ue.POST("/api/_version_/fleet/certificate_mgmt/certificate", uploadCertEndpoint, uploadCertRequest{}) + ue.POST("/api/_version_/fleet/certificate_mgmt/certificate/{name}", uploadCertEndpoint, uploadCertRequest{}) + ue.POST("/api/_version_/fleet/certificate_mgmt/certificate/{name}/password", uploadCertEndpoint, uploadCertRequest{}) + ue.GET("/api/_version_/fleet/certificate_mgmt/certificate/{name}", getCertEndpoint, getCertRequest{}) errorLimiter := ratelimit.NewErrorMiddleware(limitStore) From 36fbb759ffdd548a0ae6cbb3c005642f74f44702 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 13:34:14 -0600 Subject: [PATCH 03/12] Added list certificates and request CSR endpoints. --- server/datastore/mysql/certificates.go | 85 ++++++++ .../20241121111624_AddPKICertificates.go | 32 +++ server/fleet/certificates.go | 11 + server/fleet/datastore.go | 7 + server/fleet/service.go | 7 + server/mock/datastore_mock.go | 36 ++++ server/service/certificates.go | 189 +++++++++++++++++- server/service/handler.go | 8 +- 8 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 server/datastore/mysql/certificates.go create mode 100644 server/datastore/mysql/migrations/tables/20241121111624_AddPKICertificates.go create mode 100644 server/fleet/certificates.go diff --git a/server/datastore/mysql/certificates.go b/server/datastore/mysql/certificates.go new file mode 100644 index 000000000000..017e76b74621 --- /dev/null +++ b/server/datastore/mysql/certificates.go @@ -0,0 +1,85 @@ +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 + (?, ?, ?, ?, ?) + 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 + if len(cert.Cert) > 0 { + cert.Cert, err = encrypt(cert.Cert, ds.serverPrivateKey) + if err != nil { + return ctxerr.Wrap(ctx, err, "encrypting pki cert") + } + } + if len(cert.Key) > 0 { + cert.Key, 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, cert.Cert, cert.Key, 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 +} diff --git a/server/datastore/mysql/migrations/tables/20241121111624_AddPKICertificates.go b/server/datastore/mysql/migrations/tables/20241121111624_AddPKICertificates.go new file mode 100644 index 000000000000..9fb4bb0c648f --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241121111624_AddPKICertificates.go @@ -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 +} diff --git a/server/fleet/certificates.go b/server/fleet/certificates.go new file mode 100644 index 000000000000..c2846f006259 --- /dev/null +++ b/server/fleet/certificates.go @@ -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"` +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 6dfaae64b248..e99e1133e5d2 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1864,6 +1864,13 @@ 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) } // MDMAppleStore wraps nanomdm's storage and adds methods to deal with diff --git a/server/fleet/service.go b/server/fleet/service.go index 94f812989161..1a1605de4d96 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1169,7 +1169,14 @@ type Service interface { // ///////////////////////////////////////////////////////////////////////////// // Certificate management + // GetPKICertificates returns the list of PKI certificates (only metadata). + GetPKICertificates(ctx context.Context) ([]PKICertificate, error) + // GetCertCSR returns a CSR as base64 encoded bytes. + GetCertCSR(ctx context.Context, name string) ([]byte, error) + // UploadCert uploads a certificate to Fleet that should have been generated by the downloaded CSR. UploadCert(ctx context.Context, cert io.ReadSeeker) error + // DeleteCert deletes the certificate uploaded to Fleet. + DeleteCert(ctx context.Context) error } type KeyValueStore interface { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 6d8fbd885676..84e32c972cdf 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1165,6 +1165,12 @@ type GetHostMDMCertificateProfileFunc func(ctx context.Context, hostUUID string, type CleanUpMDMManagedCertificatesFunc func(ctx context.Context) error +type GetPKICertificateFunc func(ctx context.Context, name string) (*fleet.PKICertificate, error) + +type SavePKICertificateFunc func(ctx context.Context, cert *fleet.PKICertificate) error + +type ListPKICertificatesFunc func(ctx context.Context) ([]fleet.PKICertificate, error) + type DataStore struct { HealthCheckFunc HealthCheckFunc HealthCheckFuncInvoked bool @@ -2882,6 +2888,15 @@ type DataStore struct { CleanUpMDMManagedCertificatesFunc CleanUpMDMManagedCertificatesFunc CleanUpMDMManagedCertificatesFuncInvoked bool + GetPKICertificateFunc GetPKICertificateFunc + GetPKICertificateFuncInvoked bool + + SavePKICertificateFunc SavePKICertificateFunc + SavePKICertificateFuncInvoked bool + + ListPKICertificatesFunc ListPKICertificatesFunc + ListPKICertificatesFuncInvoked bool + mu sync.Mutex } @@ -6888,3 +6903,24 @@ func (s *DataStore) CleanUpMDMManagedCertificates(ctx context.Context) error { s.mu.Unlock() return s.CleanUpMDMManagedCertificatesFunc(ctx) } + +func (s *DataStore) GetPKICertificate(ctx context.Context, name string) (*fleet.PKICertificate, error) { + s.mu.Lock() + s.GetPKICertificateFuncInvoked = true + s.mu.Unlock() + return s.GetPKICertificateFunc(ctx, name) +} + +func (s *DataStore) SavePKICertificate(ctx context.Context, cert *fleet.PKICertificate) error { + s.mu.Lock() + s.SavePKICertificateFuncInvoked = true + s.mu.Unlock() + return s.SavePKICertificateFunc(ctx, cert) +} + +func (s *DataStore) ListPKICertificates(ctx context.Context) ([]fleet.PKICertificate, error) { + s.mu.Lock() + s.ListPKICertificatesFuncInvoked = true + s.mu.Unlock() + return s.ListPKICertificatesFunc(ctx) +} diff --git a/server/service/certificates.go b/server/service/certificates.go index e74e2b895751..6e2f1d54220a 100644 --- a/server/service/certificates.go +++ b/server/service/certificates.go @@ -2,6 +2,9 @@ package service import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" "encoding/pem" "io" "mime/multipart" @@ -11,14 +14,140 @@ import ( "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" + apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" ) +const rsaKeySize = 2048 + // ////////////////////////////////////////////////////////////////////////////// -// POST /fleet/certificate_mgmt/certificate +// GET /fleet/certificate_mgmt/certificates // ////////////////////////////////////////////////////////////////////////////// -var certificatePathRegexp = regexp.MustCompile(`/certificate/(?P.*)$`) +type getCertificatesResponse struct { + Certificates []fleet.PKICertificate `json:"certificates"` + Err error `json:"error,omitempty"` +} + +func (r getCertificatesResponse) error() error { return r.Err } + +func getCertificatesEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (errorer, error) { + certs, err := svc.GetPKICertificates(ctx) + return &getCertificatesResponse{Certificates: certs, Err: err}, nil +} + +func (svc *Service) GetPKICertificates(ctx context.Context) ([]fleet.PKICertificate, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { + return nil, err + } + certs, err := svc.ds.ListPKICertificates(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "list pki certificates") + } + return certs, nil +} + +// ////////////////////////////////////////////////////////////////////////////// +// GET /fleet/certificate_mgmt/certificate/{pki_name}/request_csr +// ////////////////////////////////////////////////////////////////////////////// + +type getCertCSRRequest struct { + Name string `url:"pki_name"` +} + +type getCertCSRResponse struct { + CSR []byte `json:"csr"` // base64 encoded + Err error `json:"error,omitempty"` +} + +func (r getCertCSRResponse) error() error { return r.Err } + +func getCertCSREndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*getCertCSRRequest) + csr64, err := svc.GetCertCSR(ctx, req.Name) + if err != nil { + return &getCertCSRResponse{Err: err}, nil + } + + return &getCertCSRResponse{CSR: csr64}, nil +} + +func (svc *Service) GetCertCSR(ctx context.Context, nameEscaped string) ([]byte, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionWrite); err != nil { + return nil, err + } + + name, err := url.PathUnescape(nameEscaped) + if err != nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("pki_name", "Invalid pki_name. Please provide a valid pki_name.")) + } + if len(name) > 255 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("pki_name", "pki_name too long. Please provide a valid pki_name.")) + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.Wrap(ctx, + &fleet.BadRequestError{Message: "Couldn't download signed CSR. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key"}) + } + + vc, ok := viewer.FromContext(ctx) + if !ok { + return nil, fleet.ErrNoContext + } + + // Check if we have existing cert and keys + pkiCert, err := svc.ds.GetPKICertificate(ctx, name) + if err != nil && !fleet.IsNotFound(err) { + return nil, ctxerr.Wrap(ctx, err, "loading existing pki cert") + } + var key *rsa.PrivateKey + if pkiCert == nil { + key, err = rsa.GenerateKey(rand.Reader, rsaKeySize) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "generate new private key") + } + // Create new PKI certificate + pkiCert = &fleet.PKICertificate{ + Name: name, + Key: apple_mdm.EncodePrivateKeyPEM(key), + } + err = svc.ds.SavePKICertificate(ctx, pkiCert) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "saving new pki cert") + } + } else { + block, _ := pem.Decode(pkiCert.Key) + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "unmarshaling saved key") + } + } + + // Generate new CSR every time this is called + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get app config") + } + + csr, err := apple_mdm.GenerateAPNSCSR(appConfig.OrgInfo.OrgName, vc.Email(), key) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "generate new CSR") + } + + return csr.Raw, nil +} + +// ////////////////////////////////////////////////////////////////////////////// +// POST /fleet/certificate_mgmt/certificate/{pki_name} +// ////////////////////////////////////////////////////////////////////////////// + +var certificatePathRegexp = regexp.MustCompile(`/certificate/(?P.*)$`) type uploadCertRequest struct { Name string @@ -47,11 +176,11 @@ func (uploadCertRequest) DecodeRequest(ctx context.Context, r *http.Request) (in // regex to get and validate the name matches := certificatePathRegexp.FindStringSubmatch(r.URL.Path) for i, name := range certificatePathRegexp.SubexpNames() { - if name == "name" { + if name == "pki_name" { certName, err := url.QueryUnescape(matches[i]) if err != nil { return nil, &fleet.BadRequestError{ - Message: "certificate name has invalid format", + Message: "certificate pki_name has invalid format", InternalErr: err, } } @@ -144,6 +273,10 @@ func (svc *Service) UploadCert(ctx context.Context, cert io.ReadSeeker) error { } +// ////////////////////////////////////////////////////////////////////////////// +// GET /fleet/certificate_mgmt/certificate/{pki_name} +// ////////////////////////////////////////////////////////////////////////////// + type getCertRequest struct { } @@ -152,3 +285,51 @@ func getCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service _ = request.(*getCertRequest) return nil, nil } + +// ////////////////////////////////////////////////////////////////////////////// +// DELETE /fleet/certificate_mgmt/certificate/{pki_name} +// ////////////////////////////////////////////////////////////////////////////// + +type deleteCertRequest struct{} + +type deleteCertResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteCertResponse) error() error { + return r.Err +} + +func deleteCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + if err := svc.DeleteMDMAppleAPNSCert(ctx); err != nil { + return &deleteMDMAppleAPNSCertResponse{Err: err}, nil + } + + return &deleteMDMAppleAPNSCertResponse{}, nil +} + +func (svc *Service) DeleteCert(ctx context.Context) error { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return err + } + + err := svc.ds.DeleteMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ + fleet.MDMAssetAPNSCert, + fleet.MDMAssetAPNSKey, + fleet.MDMAssetCACert, + fleet.MDMAssetCAKey, + }) + if err != nil { + return ctxerr.Wrap(ctx, err, "deleting apple mdm assets") + } + + // flip the app config flag + appCfg, err := svc.ds.AppConfig(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "retrieving app config") + } + + appCfg.MDM.EnabledAndConfigured = false + + return svc.ds.SaveAppConfig(ctx, appCfg) +} diff --git a/server/service/handler.go b/server/service/handler.go index 95197d56d59d..1a5e16bddfbd 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -788,9 +788,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{}) // Certificate management - ue.POST("/api/_version_/fleet/certificate_mgmt/certificate/{name}", uploadCertEndpoint, uploadCertRequest{}) - ue.POST("/api/_version_/fleet/certificate_mgmt/certificate/{name}/password", uploadCertEndpoint, uploadCertRequest{}) - ue.GET("/api/_version_/fleet/certificate_mgmt/certificate/{name}", getCertEndpoint, getCertRequest{}) + ue.GET("/api/_version_/fleet/certificate_mgmt/certificates", getCertificatesEndpoint, nil) + ue.GET("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}/request_csr", getCertCSREndpoint, getCertCSRRequest{}) + ue.POST("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}", uploadCertEndpoint, uploadCertRequest{}) + ue.GET("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}", getCertEndpoint, getCertRequest{}) + ue.DELETE("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}", deleteCertEndpoint, deleteCertRequest{}) errorLimiter := ratelimit.NewErrorMiddleware(limitStore) From 3c81df08d26ba116e841661cab31a7b6bbbd1e35 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 15:12:50 -0600 Subject: [PATCH 04/12] Upload RA cert endpoint coded. --- server/datastore/mysql/certificates.go | 9 +- server/datastore/mysql/certificates_test.go | 113 ++++++++++++++++++++ server/datastore/mysql/schema.sql | 20 +++- server/fleet/service.go | 2 +- server/service/certificates.go | 65 +++++++---- 5 files changed, 180 insertions(+), 29 deletions(-) create mode 100644 server/datastore/mysql/certificates_test.go diff --git a/server/datastore/mysql/certificates.go b/server/datastore/mysql/certificates.go index 017e76b74621..d2874b51dd90 100644 --- a/server/datastore/mysql/certificates.go +++ b/server/datastore/mysql/certificates.go @@ -44,7 +44,7 @@ func (ds *Datastore) SavePKICertificate(ctx context.Context, cert *fleet.PKICert 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), @@ -52,19 +52,20 @@ func (ds *Datastore) SavePKICertificate(ctx context.Context, cert *fleet.PKICert sha256 = VALUES(sha256) ` var err error + var encryptedCert, encryptedKey []byte if len(cert.Cert) > 0 { - cert.Cert, err = encrypt(cert.Cert, ds.serverPrivateKey) + encryptedCert, err = encrypt(cert.Cert, ds.serverPrivateKey) if err != nil { return ctxerr.Wrap(ctx, err, "encrypting pki cert") } } if len(cert.Key) > 0 { - cert.Key, err = encrypt(cert.Key, ds.serverPrivateKey) + 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, cert.Cert, cert.Key, cert.NotValidAfter, cert.Sha256) + _, 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") } diff --git a/server/datastore/mysql/certificates_test.go b/server/datastore/mysql/certificates_test.go new file mode 100644 index 000000000000..205a5a9fb269 --- /dev/null +++ b/server/datastore/mysql/certificates_test.go @@ -0,0 +1,113 @@ +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) + } + +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 45718cbd1d7c..f950bc007ad9 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -65,7 +65,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `calendar_events` ( @@ -1104,9 +1104,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=331 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=332 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241121111624,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1440,6 +1440,20 @@ CREATE TABLE `password_reset_requests` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `pki_certificates` ( + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `cert_pem` blob, + `key_pem` blob NOT NULL, + `not_valid_after` datetime DEFAULT NULL, + `sha256` binary(32) DEFAULT NULL, + `sha256_hex` char(64) COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (lower(hex(`sha256`))) VIRTUAL, + `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), + PRIMARY KEY (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `policies` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/server/fleet/service.go b/server/fleet/service.go index 1a1605de4d96..3bae66436b09 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1174,7 +1174,7 @@ type Service interface { // GetCertCSR returns a CSR as base64 encoded bytes. GetCertCSR(ctx context.Context, name string) ([]byte, error) // UploadCert uploads a certificate to Fleet that should have been generated by the downloaded CSR. - UploadCert(ctx context.Context, cert io.ReadSeeker) error + UploadCert(ctx context.Context, name string, cert io.ReadSeeker) error // DeleteCert deletes the certificate uploaded to Fleet. DeleteCert(ctx context.Context) error } diff --git a/server/service/certificates.go b/server/service/certificates.go index 6e2f1d54220a..5bf4cde767cf 100644 --- a/server/service/certificates.go +++ b/server/service/certificates.go @@ -1,11 +1,15 @@ package service import ( + "bytes" "context" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" + "encoding/hex" "encoding/pem" + "fmt" "io" "mime/multipart" "net/http" @@ -17,6 +21,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + "github.com/smallstep/pkcs7" ) const rsaKeySize = 2048 @@ -154,7 +159,7 @@ type uploadCertRequest struct { File *multipart.FileHeader } -func (uploadCertRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { +func (uploadCertRequest) DecodeRequest(_ context.Context, r *http.Request) (interface{}, error) { decoded := uploadCertRequest{} err := r.ParseMultipartForm(512 * units.MiB) if err != nil { @@ -209,18 +214,23 @@ func uploadCertEndpoint(ctx context.Context, request interface{}, svc fleet.Serv } defer file.Close() - if err := svc.UploadCert(ctx, file); err != nil { + if err := svc.UploadCert(ctx, req.Name, file); err != nil { return &uploadCertResponse{Err: err}, nil } return &uploadMDMAppleAPNSCertResponse{}, nil } -func (svc *Service) UploadCert(ctx context.Context, cert io.ReadSeeker) error { +func (svc *Service) UploadCert(ctx context.Context, nameEscaped string, cert io.ReadSeeker) error { if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionWrite); err != nil { return err } + name, err := url.PathUnescape(nameEscaped) + if err != nil { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("pki_name", "Invalid pki_name. Please provide a valid pki_name.")) + } + privateKey := svc.config.Server.PrivateKey if testSetEmptyPrivateKey { privateKey = "" @@ -240,33 +250,46 @@ func (svc *Service) UploadCert(ctx context.Context, cert io.ReadSeeker) error { if err != nil { return ctxerr.Wrap(ctx, err, "reading certificate") } + if len(certBytes) == 0 { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Empty certificate. Please provide a valid certificate.")) + } - // Validate cert + // Convert from PEM to DER format if needed block, _ := pem.Decode(certBytes) - if block == nil { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Invalid certificate. Please provide a valid certificate.")) + if block != nil { + // Assume that certificate need to be converted from PEM to DER + certBytes = block.Bytes } - if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { - return err + // Validate cert + p7, err := pkcs7.Parse(certBytes) + if err != nil { + return ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError("certificate", + fmt.Sprintf("Invalid PKCS7 certificate. Please provide a valid certificate. %s", err.Error()))) + } + if len(p7.Certificates) == 0 { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "No certificate found. Please provide a valid certificate.")) } - // TODO: Parse the certificate to determine expiration date and fingerprint - // h := sha256.New() - // _, _ = io.Copy(h, bytes.NewReader(certBytes)) // writes to a Hash can never fail - // sha256Hash := hex.EncodeToString(h.Sum(nil)) - - // delete the old certificate and insert the new one - // TODO(roberto): replacing the certificate should be done in a single transaction in the DB - err = svc.ds.DeleteMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAuthenticationCertificate}) + // Get the saved certificate + pkiCert, err := svc.ds.GetPKICertificate(ctx, name) if err != nil { - return ctxerr.Wrap(ctx, err, "deleting old apns cert from db") + return ctxerr.Wrap(ctx, err, "loading existing pki private key") } - err = svc.ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{ - {Name: fleet.MDMAssetAuthenticationCertificate, Value: certBytes}, - }, nil) + + x509Cert := p7.Certificates[0] + pkiCert.Cert = x509Cert.Raw + pkiCert.NotValidAfter = &x509Cert.NotAfter + + h := sha256.New() + _, _ = io.Copy(h, bytes.NewReader(x509Cert.Raw)) // writes to a Hash can never fail + sha256Hash := hex.EncodeToString(h.Sum(nil)) + pkiCert.Sha256 = &sha256Hash + + err = svc.ds.SavePKICertificate(ctx, pkiCert) if err != nil { - return ctxerr.Wrap(ctx, err, "writing cert to db") + return ctxerr.Wrap(ctx, err, "saving new pki cert") } return nil From 66f170548e6f6d019e49cca96a5aa0933d8fb288 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 15:29:28 -0600 Subject: [PATCH 05/12] Added PKI delete endpoint. --- server/datastore/mysql/certificates.go | 12 ++++++ server/datastore/mysql/certificates_test.go | 10 +++++ server/fleet/datastore.go | 2 + server/fleet/service.go | 2 +- server/mock/datastore_mock.go | 12 ++++++ server/service/certificates.go | 48 ++++++--------------- server/service/handler.go | 11 +++-- 7 files changed, 55 insertions(+), 42 deletions(-) diff --git a/server/datastore/mysql/certificates.go b/server/datastore/mysql/certificates.go index d2874b51dd90..bf90e4f0a4a5 100644 --- a/server/datastore/mysql/certificates.go +++ b/server/datastore/mysql/certificates.go @@ -84,3 +84,15 @@ func (ds *Datastore) ListPKICertificates(ctx context.Context) ([]fleet.PKICertif } 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 +} diff --git a/server/datastore/mysql/certificates_test.go b/server/datastore/mysql/certificates_test.go index 205a5a9fb269..81f48258bf9e 100644 --- a/server/datastore/mysql/certificates_test.go +++ b/server/datastore/mysql/certificates_test.go @@ -110,4 +110,14 @@ func testSavePKICertificate(t *testing.T, ds *Datastore) { 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) + } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index e99e1133e5d2..7149f05cd9e5 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1871,6 +1871,8 @@ type Datastore interface { 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 diff --git a/server/fleet/service.go b/server/fleet/service.go index 3bae66436b09..5d98eefbd151 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1176,7 +1176,7 @@ type Service interface { // UploadCert uploads a certificate to Fleet that should have been generated by the downloaded CSR. UploadCert(ctx context.Context, name string, cert io.ReadSeeker) error // DeleteCert deletes the certificate uploaded to Fleet. - DeleteCert(ctx context.Context) error + DeleteCert(ctx context.Context, name string) error } type KeyValueStore interface { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 84e32c972cdf..10b31ab16777 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1171,6 +1171,8 @@ type SavePKICertificateFunc func(ctx context.Context, cert *fleet.PKICertificate type ListPKICertificatesFunc func(ctx context.Context) ([]fleet.PKICertificate, error) +type DeletePKICertificateFunc func(ctx context.Context, name string) error + type DataStore struct { HealthCheckFunc HealthCheckFunc HealthCheckFuncInvoked bool @@ -2897,6 +2899,9 @@ type DataStore struct { ListPKICertificatesFunc ListPKICertificatesFunc ListPKICertificatesFuncInvoked bool + DeletePKICertificateFunc DeletePKICertificateFunc + DeletePKICertificateFuncInvoked bool + mu sync.Mutex } @@ -6924,3 +6929,10 @@ func (s *DataStore) ListPKICertificates(ctx context.Context) ([]fleet.PKICertifi s.mu.Unlock() return s.ListPKICertificatesFunc(ctx) } + +func (s *DataStore) DeletePKICertificate(ctx context.Context, name string) error { + s.mu.Lock() + s.DeletePKICertificateFuncInvoked = true + s.mu.Unlock() + return s.DeletePKICertificateFunc(ctx, name) +} diff --git a/server/service/certificates.go b/server/service/certificates.go index 5bf4cde767cf..7e365e9c936b 100644 --- a/server/service/certificates.go +++ b/server/service/certificates.go @@ -296,24 +296,13 @@ func (svc *Service) UploadCert(ctx context.Context, nameEscaped string, cert io. } -// ////////////////////////////////////////////////////////////////////////////// -// GET /fleet/certificate_mgmt/certificate/{pki_name} -// ////////////////////////////////////////////////////////////////////////////// - -type getCertRequest struct { -} - -func getCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - // TODO: Implementation - _ = request.(*getCertRequest) - return nil, nil -} - // ////////////////////////////////////////////////////////////////////////////// // DELETE /fleet/certificate_mgmt/certificate/{pki_name} // ////////////////////////////////////////////////////////////////////////////// -type deleteCertRequest struct{} +type deleteCertRequest struct { + Name string `url:"pki_name"` +} type deleteCertResponse struct { Err error `json:"error,omitempty"` @@ -324,35 +313,24 @@ func (r deleteCertResponse) error() error { } func deleteCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - if err := svc.DeleteMDMAppleAPNSCert(ctx); err != nil { - return &deleteMDMAppleAPNSCertResponse{Err: err}, nil - } - - return &deleteMDMAppleAPNSCertResponse{}, nil + req := request.(*deleteCertRequest) + err := svc.DeleteCert(ctx, req.Name) + return &deleteCertResponse{Err: err}, nil } -func (svc *Service) DeleteCert(ctx context.Context) error { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { +func (svc *Service) DeleteCert(ctx context.Context, nameEscaped string) error { + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionWrite); err != nil { return err } - err := svc.ds.DeleteMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{ - fleet.MDMAssetAPNSCert, - fleet.MDMAssetAPNSKey, - fleet.MDMAssetCACert, - fleet.MDMAssetCAKey, - }) + name, err := url.PathUnescape(nameEscaped) if err != nil { - return ctxerr.Wrap(ctx, err, "deleting apple mdm assets") + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("pki_name", "Invalid pki_name. Please provide a valid pki_name.")) } - // flip the app config flag - appCfg, err := svc.ds.AppConfig(ctx) + err = svc.ds.DeletePKICertificate(ctx, name) if err != nil { - return ctxerr.Wrap(ctx, err, "retrieving app config") + return ctxerr.Wrap(ctx, err, "deleting pki cert") } - - appCfg.MDM.EnabledAndConfigured = false - - return svc.ds.SaveAppConfig(ctx, appCfg) + return nil } diff --git a/server/service/handler.go b/server/service/handler.go index 1a5e16bddfbd..eda13af8440a 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -787,12 +787,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // input to `fleetctl apply` ue.POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{}) - // Certificate management - ue.GET("/api/_version_/fleet/certificate_mgmt/certificates", getCertificatesEndpoint, nil) - ue.GET("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}/request_csr", getCertCSREndpoint, getCertCSRRequest{}) - ue.POST("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}", uploadCertEndpoint, uploadCertRequest{}) - ue.GET("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}", getCertEndpoint, getCertRequest{}) - ue.DELETE("/api/_version_/fleet/certificate_mgmt/certificate/{pki_name}", deleteCertEndpoint, deleteCertRequest{}) + // PKI certificate management + ue.GET("/api/_version_/fleet/pki", getCertificatesEndpoint, nil) + ue.GET("/api/_version_/fleet/pki/{pki_name}/request_csr", getCertCSREndpoint, getCertCSRRequest{}) + ue.POST("/api/_version_/fleet/pki/{pki_name}", uploadCertEndpoint, uploadCertRequest{}) + ue.DELETE("/api/_version_/fleet/pki/{pki_name}", deleteCertEndpoint, deleteCertRequest{}) errorLimiter := ratelimit.NewErrorMiddleware(limitStore) From 2b0a4e15a3bd0bc8cd60ca2a683c53dcc522a50a Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 16:02:59 -0600 Subject: [PATCH 06/12] Updating appConfig and tests. --- .../expectedGetConfigAppConfigJson.json | 3 +- .../expectedGetConfigAppConfigYaml.yml | 1 + ...ectedGetConfigIncludeServerConfigJson.json | 3 +- ...pectedGetConfigIncludeServerConfigYaml.yml | 1 + .../macosSetupExpectedAppConfigEmpty.yml | 1 + .../macosSetupExpectedAppConfigSet.yml | 1 + server/fleet/integrations.go | 6 ++-- server/service/appconfig.go | 31 ++++++++++++------- .../generated_files/appconfig.txt | 12 +++++++ 9 files changed, 42 insertions(+), 17 deletions(-) diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index c3779641775a..359f4b365313 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -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, diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index ff6fbaa22eae..62dcffbe6177 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -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 diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index dea76a995b16..0a6eff29bc34 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -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", diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index f6b8136407cd..cf11e9caafd2 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 49c129df695b..eb4f37078531 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -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 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 27e6a2a5459e..309279ed23b7 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -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 diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go index 8ca5ac8a0fe1..61e98020f57a 100644 --- a/server/fleet/integrations.go +++ b/server/fleet/integrations.go @@ -366,8 +366,7 @@ type CertificateSAN struct { } type CertificateTemplate struct { - Name string `json:"name"` // Limit the max number of characters, to say, 32? - CAName string `json:"ca_name"` // Limit the max number of characters, to say, 32? + Name string `json:"name"` // letters, numbers, and underscores only ProfileID string `json:"profile_id"` SeatID string `json:"seat_id"` CommonName string `json:"common_name"` @@ -375,6 +374,7 @@ type CertificateTemplate struct { } type DigiCertIntegration struct { + PKIName string `json:"pki_name"` Templates []CertificateTemplate `json:"templates"` } @@ -385,7 +385,7 @@ type Integrations struct { GoogleCalendar []*GoogleCalendarIntegration `json:"google_calendar"` // NDESSCEPProxy settings. In JSON, not specifying this field means keep current setting, null means clear settings. NDESSCEPProxy optjson.Any[NDESSCEPProxyIntegration] `json:"ndes_scep_proxy"` - DigiCert optjson.Any[DigiCertIntegration] `json:"digicert"` + DigiCert optjson.Slice[DigiCertIntegration] `json:"digicert_pki"` } func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 48359069123a..18bee8e90575 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -13,6 +13,7 @@ import ( "net" "net/http" "net/url" + "regexp" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/pkg/optjson" @@ -29,10 +30,12 @@ import ( "golang.org/x/text/unicode/norm" ) -// Functions that can be overwritten in tests var ( + // Functions that can be overwritten in tests validateNDESSCEPAdminURL = eeservice.ValidateNDESSCEPAdminURL validateNDESSCEPURL = eeservice.ValidateNDESSCEPURL + + wordRegexp = regexp.MustCompile(`^\w+$`) ) //////////////////////////////////////////////////////////////////////////////// @@ -405,7 +408,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } if newAppConfig.Integrations.DigiCert.Set && newAppConfig.Integrations.DigiCert.Valid && !license.IsPremium() { - invalid.Append("integrations.digicert", ErrMissingLicense.Error()) + invalid.Append("integrations.digicert_pki", ErrMissingLicense.Error()) appConfig.Integrations.DigiCert.Valid = false } else { switch { @@ -416,17 +419,21 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle // User is explicitly clearing this setting appConfig.Integrations.DigiCert.Valid = false default: - templateNames := make(map[string]struct{}, len(newAppConfig.Integrations.DigiCert.Value.Templates)) - for _, template := range appConfig.Integrations.DigiCert.Value.Templates { - // TODO: Validate length? - template.Name = fleet.Preprocess(template.Name) - if _, ok := templateNames[template.Name]; ok { - invalid.Append("integrations.digicert.templates", "template names must be unique") - break + templateNames := make(map[string]struct{}, len(newAppConfig.Integrations.DigiCert.Value)) + for _, item := range appConfig.Integrations.DigiCert.Value { + item.PKIName = fleet.Preprocess(item.PKIName) + for _, template := range item.Templates { + template.Name = fleet.Preprocess(template.Name) + if !wordRegexp.MatchString(template.Name) { + invalid.Append("integrations.digicert_pki.templates.name", "template names must contain ASCII word characters only") + break + } + if _, ok := templateNames[template.Name]; ok { + invalid.Append("integrations.digicert_pki.templates.name", "template names must be unique") + break + } + templateNames[template.Name] = struct{}{} } - templateNames[template.Name] = struct{}{} - // TODO: Validate length? - template.CAName = fleet.Preprocess(template.CAName) } } } diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 2454027fa9d2..afe6101077d4 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -103,6 +103,18 @@ github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration URL string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration AdminURL string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Username string github.com/fleetdm/fleet/v4/server/fleet/NDESSCEPProxyIntegration Password string +github.com/fleetdm/fleet/v4/server/fleet/Integrations DigiCert optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.DigiCertIntegration] +github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.DigiCertIntegration] Set bool +github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.DigiCertIntegration] Valid bool +github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.DigiCertIntegration] Value []fleet.DigiCertIntegration +github.com/fleetdm/fleet/v4/server/fleet/DigiCertIntegration PKIName string +github.com/fleetdm/fleet/v4/server/fleet/DigiCertIntegration Templates []fleet.CertificateTemplate +github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplate Name string +github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplate ProfileID string +github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplate SeatID string +github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplate CommonName string +github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplate SAN fleet.CertificateSAN +github.com/fleetdm/fleet/v4/server/fleet/CertificateSAN UserPrincipalNames []string github.com/fleetdm/fleet/v4/server/fleet/AppConfig MDM fleet.MDM github.com/fleetdm/fleet/v4/server/fleet/MDM AppleServerURL string github.com/fleetdm/fleet/v4/server/fleet/MDM DeprecatedAppleBMDefaultTeam string From f401b68f8c2dc963c8aa7bce6a1da9b8f0e0c0f2 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 16:12:55 -0600 Subject: [PATCH 07/12] Update DB schema. --- server/datastore/mysql/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index f950bc007ad9..8a2bb3ee89e9 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -65,7 +65,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert_pki\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `calendar_events` ( From 12ca755788751c81227d23c027d6a6ad16e19f14 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 21 Nov 2024 17:16:03 -0600 Subject: [PATCH 08/12] Added validation for $FLEET_VAR_PKI_CERT_ --- server/service/appconfig.go | 8 ++ server/service/apple_mdm.go | 153 ++++++++++++++++++++++++++---------- 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 18bee8e90575..45f125a98b15 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -422,8 +422,16 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle templateNames := make(map[string]struct{}, len(newAppConfig.Integrations.DigiCert.Value)) for _, item := range appConfig.Integrations.DigiCert.Value { item.PKIName = fleet.Preprocess(item.PKIName) + if item.PKIName == "" { + invalid.Append("integrations.digicert_pki.pki_name", "PKI name must be present") + break + } for _, template := range item.Templates { template.Name = fleet.Preprocess(template.Name) + if template.Name == "" { + invalid.Append("integrations.digicert_pki.templates.name", "template names must be present") + break + } if !wordRegexp.MatchString(template.Name) { invalid.Append("integrations.digicert_pki.templates.name", "template names must contain ASCII word characters only") break diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 2c0f435e55a9..08f2e77982df 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -61,6 +61,8 @@ const ( FleetVarNDESSCEPChallenge = "NDES_SCEP_CHALLENGE" FleetVarNDESSCEPProxyURL = "NDES_SCEP_PROXY_URL" FleetVarHostEndUserEmailIDP = "HOST_END_USER_EMAIL_IDP" + FleetVarHostHardwareSerial = "HOST_HARDWARE_SERIAL" + FleetVarPKICertPrefix = "PKI_CERT_" ) var ( @@ -71,7 +73,8 @@ var ( FleetVarNDESSCEPProxyURL)) fleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarHostEndUserEmailIDP, FleetVarHostEndUserEmailIDP)) - fleetVarsSupportedInConfigProfiles = []string{FleetVarNDESSCEPChallenge, FleetVarNDESSCEPProxyURL, FleetVarHostEndUserEmailIDP} + fleetVarsSupportedInConfigProfiles = []string{FleetVarNDESSCEPChallenge, FleetVarNDESSCEPProxyURL, FleetVarHostEndUserEmailIDP} + fleetVarPrefixesSupportedInConfigProfiles = []string{FleetVarPKICertPrefix} ) type hostProfileUUID struct { @@ -454,8 +457,18 @@ func validateConfigProfileFleetVariables(contents string) error { fleetVars := findFleetVariables(contents) for k := range fleetVars { if !slices.Contains(fleetVarsSupportedInConfigProfiles, k) { - return &fleet.BadRequestError{Message: fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in configuration profiles", - k)} + // Check if this variable has a supported prefix. + prefixFound := false + for _, prefix := range fleetVarPrefixesSupportedInConfigProfiles { + if strings.HasPrefix(k, prefix) { + prefixFound = true + break + } + } + if !prefixFound { + return &fleet.BadRequestError{Message: fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in configuration profiles", + k)} + } } } return nil @@ -3571,39 +3584,66 @@ func preprocessProfileContents( isNDESSCEPConfigured := func(profUUID string, target *cmdTarget) (bool, error) { if !license.IsPremium(ctx) { - profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) - for _, hostUUID := range target.hostUUIDs { - profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] - if !ok { // Should never happen - continue - } - profile.Status = &fleet.MDMDeliveryFailed - profile.Detail = "NDES SCEP Proxy requires a Fleet Premium license." - profilesToUpdate = append(profilesToUpdate, profile) - } - if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { - return false, err - } - return false, nil + detail := "NDES SCEP Proxy requires a Fleet Premium license." + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) } if !appConfig.Integrations.NDESSCEPProxy.Valid { - profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) - for _, hostUUID := range target.hostUUIDs { - profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] - if !ok { // Should never happen - continue + detail := "NDES SCEP Proxy is not configured. " + + "Please configure in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol." + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) + } + return appConfig.Integrations.NDESSCEPProxy.Valid, nil + } + + var pkiCerts map[string]*fleet.PKICertificate + isPKIConfigured := func(profUUID string, target *cmdTarget, templateName string) (bool, error) { + if !license.IsPremium(ctx) { + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, + "PKI integration requires a Fleet Premium license.") + } + pkiConfigured := false + var pkiName string + if appConfig.Integrations.DigiCert.Valid { + for _, items := range appConfig.Integrations.DigiCert.Value { + for _, template := range items.Templates { + if template.Name == templateName { + pkiName = items.PKIName + pkiConfigured = true + break + } } - profile.Status = &fleet.MDMDeliveryFailed - profile.Detail = "NDES SCEP Proxy is not configured. " + - "Please configure in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol." - profilesToUpdate = append(profilesToUpdate, profile) - } - if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { - return false, err } - return false, nil } - return appConfig.Integrations.NDESSCEPProxy.Valid, nil + if !pkiConfigured { + detail := "PKI is not configured. " + fmt.Sprintf("Template '%s' not found. ", templateName) + + "Please configure in Settings > Integrations > Mobile Device Management > Public key infrastructure (PKI)." + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) + } + pkiCert, err := ds.GetPKICertificate(ctx, pkiName) + if fleet.IsNotFound(err) { + detail := "PKI is not configured. " + fmt.Sprintf("PKI '%s' not found. ", pkiName) + + "Please configure in Settings > Integrations > Mobile Device Management > Public key infrastructure (PKI)." + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) + } else if err != nil { + return false, ctxerr.Wrap(ctx, err, "getting PKI certificate") + } + if pkiCert.NotValidAfter == nil { + detail := "PKI is not configured. " + fmt.Sprintf("PKI '%s' is missing the registration authority (RA) certificate. ", + pkiName) + + "Please configure in Settings > Integrations > Mobile Device Management > Public key infrastructure (PKI)." + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) + } + if pkiCert.NotValidAfter.Before(time.Now()) { + detail := "PKI is not configured. " + fmt.Sprintf("PKI '%s' has an expired registration authority (RA) certificate. ", + pkiName) + + "Please configure in Settings > Integrations > Mobile Device Management > Public key infrastructure (PKI)." + return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) + } + if pkiCerts == nil { + pkiCerts = make(map[string]*fleet.PKICertificate) + } + pkiCerts[templateName] = pkiCert + return true, nil } // Copy of NDES SCEP config which will contain unencrypted password, if needed @@ -3640,20 +3680,23 @@ func preprocessProfileContents( case FleetVarHostEndUserEmailIDP: // No extra validation needed for this variable default: - // Error out if we find an unknown variable - profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) - for _, hostUUID := range target.hostUUIDs { - profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] - if !ok { // Should never happen - continue + if strings.HasPrefix(fleetVar, FleetVarPKICertPrefix) { + // Get the template name + templateName := strings.TrimPrefix(fleetVar, FleetVarPKICertPrefix) + var err error + valid, err = isPKIConfigured(profUUID, target, templateName) + if err != nil { + return err } - profile.Status = &fleet.MDMDeliveryFailed - profile.Detail = fmt.Sprintf("Unknown Fleet variable $FLEET_VAR_%s found in profile. Please update or remove.", - fleetVar) - profilesToUpdate = append(profilesToUpdate, profile) + break } - if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { - return ctxerr.Wrap(ctx, err, "updating host MDM Apple profiles for unknown variable") + + // Otherwise, error out since this variable is unknown + detail := fmt.Sprintf("Unknown Fleet variable $FLEET_VAR_%s found in profile. Please update or remove.", + fleetVar) + _, err := markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, detail) + if err != nil { + return err } valid = false } @@ -3812,6 +3855,30 @@ func preprocessProfileContents( return nil } +func markProfilesFailed( + ctx context.Context, + ds fleet.Datastore, + target *cmdTarget, + hostProfilesToInstallMap map[hostProfileUUID]*fleet.MDMAppleBulkUpsertHostProfilePayload, + profUUID string, + detail string, +) (bool, error) { + profilesToUpdate := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(target.hostUUIDs)) + for _, hostUUID := range target.hostUUIDs { + profile, ok := hostProfilesToInstallMap[hostProfileUUID{HostUUID: hostUUID, ProfileUUID: profUUID}] + if !ok { // Should never happen + continue + } + profile.Status = &fleet.MDMDeliveryFailed + profile.Detail = detail + profilesToUpdate = append(profilesToUpdate, profile) + } + if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, profilesToUpdate); err != nil { + return false, ctxerr.Wrap(ctx, err, "marking host profiles failed") + } + return false, nil +} + func replaceFleetVariable(regExp *regexp.Regexp, contents string, replacement string) string { // Escape XML characters b := make([]byte, 0, len(replacement)) From e99810cdbaf2eb760d7f4bf1ba4b75cfc7f517a3 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 22 Nov 2024 11:25:58 -0600 Subject: [PATCH 09/12] Able to send a dummy PKI cert to host. --- server/mdm/apple/mobileconfig/mobileconfig.go | 4 + server/mdm/mdm.go | 3 + server/service/apple_mdm.go | 152 +++++++++++++++++- server/service/apple_mdm_test.go | 22 +++ server/service/certificates.go | 2 +- 5 files changed, 178 insertions(+), 5 deletions(-) diff --git a/server/mdm/apple/mobileconfig/mobileconfig.go b/server/mdm/apple/mobileconfig/mobileconfig.go index 690b15c79690..346d484651fd 100644 --- a/server/mdm/apple/mobileconfig/mobileconfig.go +++ b/server/mdm/apple/mobileconfig/mobileconfig.go @@ -109,6 +109,8 @@ func getSignedProfileData(mc Mobileconfig) (Mobileconfig, error) { // Adapted from https://github.com/micromdm/micromdm/blob/main/platform/profile/profile.go func (mc Mobileconfig) ParseConfigProfile() (*Parsed, error) { mcBytes := mc + // Strip out $FLEET_VAR placeholders since they may not follow strict plist format + mcBytes = mdm.ProfileVariableRegex.ReplaceAll(mcBytes, []byte("")) if mc.isSignedProfile() { profileData, err := getSignedProfileData(mc) if err != nil { @@ -145,6 +147,8 @@ type payloadSummary struct { // See also https://developer.apple.com/documentation/devicemanagement/toplevel func (mc Mobileconfig) payloadSummary() ([]payloadSummary, error) { mcBytes := mc + // Strip out $FLEET_VAR placeholders since they may not follow strict plist format + mcBytes = mdm.ProfileVariableRegex.ReplaceAll(mcBytes, []byte("")) if mc.isSignedProfile() { profileData, err := getSignedProfileData(mc) if err != nil { diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index cf800dcfa690..c23840bb1f3d 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -10,10 +10,13 @@ import ( "encoding/base64" "fmt" "io" + "regexp" "github.com/smallstep/pkcs7" ) +var ProfileVariableRegex = regexp.MustCompile(`(\$FLEET_VAR_(?P\w+))|(\${FLEET_VAR_(?P\w+)})`) + // MaxProfileRetries is the maximum times an install profile command may be // retried, after which marked as failed and no further attempts will be made // to install the profile. diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 08f2e77982df..d99d1238997b 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3,7 +3,12 @@ package service import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/base64" "encoding/json" "encoding/pem" @@ -11,6 +16,7 @@ import ( "errors" "fmt" "io" + "math/big" "mime/multipart" "net/http" "net/url" @@ -52,6 +58,7 @@ import ( "github.com/google/uuid" "github.com/groob/plist" "go.mozilla.org/pkcs7" + "software.sslmate.com/src/go-pkcs12" ) const ( @@ -66,7 +73,6 @@ const ( ) var ( - profileVariableRegex = regexp.MustCompile(`(\$FLEET_VAR_(?P\w+))|(\${FLEET_VAR_(?P\w+)})`) fleetVarNDESSCEPChallengeRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarNDESSCEPChallenge, FleetVarNDESSCEPChallenge)) fleetVarNDESSCEPProxyURLRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarNDESSCEPProxyURL, @@ -3822,7 +3828,29 @@ func preprocessProfileContents( } hostContents = replaceFleetVariable(fleetVarHostEndUserEmailIDPRegexp, hostContents, emails[0]) default: - // This was handled in the above switch statement, so we should never reach this case + if strings.HasPrefix(fleetVar, FleetVarPKICertPrefix) { + // Get the template name + templateName := strings.TrimPrefix(fleetVar, FleetVarPKICertPrefix) + if pkiCerts == nil { + // This should never happen since we validated the PKI configuration earlier + continue + } + pkiCert, ok := pkiCerts[templateName] + if !ok { + // This should never happen since we validated the PKI configuration earlier + continue + } + // Insert the new certificate into the profile contents + certBase64, err := getNewPKICertificate(ctx, ds, pkiCert) + if err != nil { + // This is a server error, so we exit. + return ctxerr.Wrap(ctx, err, "getting new PKI certificate") + } + fleetPKICertRegexp := regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, fleetVar, + fleetVar)) + hostContents = replaceFleetVariable(fleetPKICertRegexp, hostContents, certBase64) + } + // Invalid variables were handled in the earlier global switch statement, so we should never reach this case } } if !failed { @@ -3855,6 +3883,122 @@ func preprocessProfileContents( return nil } +// UPN type for asn1 encoding. This will hold +// our utf-8 encoded string. +type UPN struct { + A string `asn1:"utf8"` +} + +// OtherName type for asn1 encoding +type OtherName struct { + OID asn1.ObjectIdentifier + Value interface{} `asn1:"tag:0"` +} + +// GeneralNames type for asn1 encoding +type GeneralNames struct { + OtherName OtherName `asn1:"tag:0"` +} + +func getNewPKICertificate(ctx context.Context, ds fleet.Datastore, _ *fleet.PKICertificate) (string, error) { + // This method retrieves a new PKI certificate from the CA and returns the base64-encoded certificate. + + notBefore := time.Now().Add(-time.Hour) + notAfter := time.Now().Add(time.Hour * 24 * 365) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "generating serial number") + } + + orgName := "Acme Co" + commonName := "Cert name" + upnValue := "johnDoe@example.com" + + // This is where we create the UPN data structure, and marshal + // it into an asn1 object. + upnExt, err := asn1.Marshal(GeneralNames{ + OtherName: OtherName{ + // init our ASN.1 object identifier + OID: asn1.ObjectIdentifier{ + 1, 3, 6, 1, 4, 1, 311, 20, 2, 3}, + // This is the email address of the person we + // are generating the certificate for. + Value: UPN{ + A: upnValue, + }, + }, + }) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "marshaling UPN extension") + } + // Finally, we create a new extension with + // the OID 2.5.29.17 (SubjectAltName), and set the + // marshaled GeneralNames structure as the Value + // + // http://oid-info.com/get/2.5.29.17 + extSubjectAltName := pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 5, 29, 17}, + Critical: false, + Value: upnExt, + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{orgName}, + CommonName: commonName, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + // Add subjectAltName + ExtraExtensions: []pkix.Extension{extSubjectAltName}, + BasicConstraintsValid: true, + } + + // Get Fleet's CA cert + fleetAssets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) + if err != nil { + return "", ctxerr.Wrap(ctx, + fmt.Errorf("loading %s, %s keypair from the database: %w", fleet.MDMAssetCACert, fleet.MDMAssetCAKey, err)) + } + caCert, err := tls.X509KeyPair(fleetAssets[fleet.MDMAssetCACert].Value, fleetAssets[fleet.MDMAssetCAKey].Value) + if err != nil { + return "", ctxerr.Wrap(ctx, fmt.Errorf("parsing %s, %s keypair: %w", fleet.MDMAssetCACert, fleet.MDMAssetCAKey, err)) + } + caCertx509, err := x509.ParseCertificate(caCert.Certificate[0]) + if err != nil { + return "", ctxerr.Wrap(ctx, fmt.Errorf("parsing %s certificate leaf: %w", fleet.MDMAssetCACert, err)) + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "generating private key") + } + + privateKeyOfSigner := caCert.PrivateKey + derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCertx509, &privateKey.PublicKey, privateKeyOfSigner) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "creating certificate") + } + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "parsing certificate") + } + + // Create PKCS12 + // pfxData, err := pkcs12.Modern.Encode(privateKey, cert, []*x509.Certificate{caCertx509}, "123") // TODO: remove password + // pfxData, err := pkcs12.Modern.Encode(privateKey, cert, nil, "123") // TODO: remove password + pfxData, err := pkcs12.Legacy.Encode(privateKey, cert, nil, "test_password") + if err != nil { + return "", ctxerr.Wrap(ctx, err, "encoding PKCS12") + } + + return base64.StdEncoding.EncodeToString(pfxData), nil +} + func markProfilesFailed( ctx context.Context, ds fleet.Datastore, @@ -3890,12 +4034,12 @@ func replaceFleetVariable(regExp *regexp.Regexp, contents string, replacement st func findFleetVariables(contents string) map[string]interface{} { var result map[string]interface{} - matches := profileVariableRegex.FindAllStringSubmatch(contents, -1) + matches := mdm_types.ProfileVariableRegex.FindAllStringSubmatch(contents, -1) if len(matches) == 0 { return nil } nameToIndex := make(map[string]int, 2) - for i, name := range profileVariableRegex.SubexpNames() { + for i, name := range mdm_types.ProfileVariableRegex.SubexpNames() { if name == "" { continue } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 247eababf1ba..4678e1d5b3a7 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -4236,3 +4236,25 @@ func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) { } }) } + +func TestGetNewPKICertificate(t *testing.T) { + ctx := context.Background() + + ds := new(mock.Store) + crt, key, err := apple_mdm.NewSCEPCACertKey() + require.NoError(t, err) + certPEM := tokenpki.PEMCertificate(crt.Raw) + keyPEM := tokenpki.PEMRSAPrivateKey(key) + ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ + fleet.MDMAssetCACert: {Value: certPEM}, + fleet.MDMAssetCAKey: {Value: keyPEM}, + }, nil + } + + cert64, err := getNewPKICertificate(ctx, ds, nil) + require.NoError(t, err) + assert.NotEmpty(t, cert64) +} diff --git a/server/service/certificates.go b/server/service/certificates.go index 7e365e9c936b..e09a5944b39e 100644 --- a/server/service/certificates.go +++ b/server/service/certificates.go @@ -152,7 +152,7 @@ func (svc *Service) GetCertCSR(ctx context.Context, nameEscaped string) ([]byte, // POST /fleet/certificate_mgmt/certificate/{pki_name} // ////////////////////////////////////////////////////////////////////////////// -var certificatePathRegexp = regexp.MustCompile(`/certificate/(?P.*)$`) +var certificatePathRegexp = regexp.MustCompile(`/pki/(?P.*)$`) type uploadCertRequest struct { Name string From 6953dc0aa52eba7c8abb910b48cdd66d4832656d Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 22 Nov 2024 12:36:43 -0600 Subject: [PATCH 10/12] Happy path working for backend! --- server/datastore/mysql/hosts.go | 14 +++- server/fleet/datastore.go | 2 + server/mock/datastore_mock.go | 12 +++ server/service/apple_mdm.go | 137 ++++++++++++++++++++++---------- 4 files changed, 122 insertions(+), 43 deletions(-) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 542ffac282e6..138cbba8b5e5 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -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, @@ -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) diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 7149f05cd9e5..337b91ebfee9 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -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 diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 10b31ab16777..70453e761eb0 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -181,6 +181,8 @@ type ListHostsFunc func(ctx context.Context, filter fleet.TeamFilter, opt fleet. type ListHostsLiteByUUIDsFunc func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) +type ListHostsLiteByUUIDsNoFilterFunc func(ctx context.Context, uuids []string) ([]*fleet.Host, error) + type ListHostsLiteByIDsFunc func(ctx context.Context, ids []uint) ([]*fleet.Host, error) type MarkHostsSeenFunc func(ctx context.Context, hostIDs []uint, t time.Time) error @@ -1414,6 +1416,9 @@ type DataStore struct { ListHostsLiteByUUIDsFunc ListHostsLiteByUUIDsFunc ListHostsLiteByUUIDsFuncInvoked bool + ListHostsLiteByUUIDsNoFilterFunc ListHostsLiteByUUIDsNoFilterFunc + ListHostsLiteByUUIDsNoFilterFuncInvoked bool + ListHostsLiteByIDsFunc ListHostsLiteByIDsFunc ListHostsLiteByIDsFuncInvoked bool @@ -3465,6 +3470,13 @@ func (s *DataStore) ListHostsLiteByUUIDs(ctx context.Context, filter fleet.TeamF return s.ListHostsLiteByUUIDsFunc(ctx, filter, uuids) } +func (s *DataStore) ListHostsLiteByUUIDsNoFilter(ctx context.Context, uuids []string) ([]*fleet.Host, error) { + s.mu.Lock() + s.ListHostsLiteByUUIDsNoFilterFuncInvoked = true + s.mu.Unlock() + return s.ListHostsLiteByUUIDsNoFilterFunc(ctx, uuids) +} + func (s *DataStore) ListHostsLiteByIDs(ctx context.Context, ids []uint) ([]*fleet.Host, error) { s.mu.Lock() s.ListHostsLiteByIDsFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index d99d1238997b..98fcee19164b 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -69,6 +69,7 @@ const ( FleetVarNDESSCEPProxyURL = "NDES_SCEP_PROXY_URL" FleetVarHostEndUserEmailIDP = "HOST_END_USER_EMAIL_IDP" FleetVarHostHardwareSerial = "HOST_HARDWARE_SERIAL" + FleetVarPKICertPassword = "PKI_PASSWORD" FleetVarPKICertPrefix = "PKI_CERT_" ) @@ -79,7 +80,10 @@ var ( FleetVarNDESSCEPProxyURL)) fleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarHostEndUserEmailIDP, FleetVarHostEndUserEmailIDP)) - fleetVarsSupportedInConfigProfiles = []string{FleetVarNDESSCEPChallenge, FleetVarNDESSCEPProxyURL, FleetVarHostEndUserEmailIDP} + fleetVarHostHardwareSerial = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%s})`, FleetVarHostHardwareSerial, + FleetVarHostHardwareSerial)) + fleetVarsSupportedInConfigProfiles = []string{FleetVarNDESSCEPChallenge, FleetVarNDESSCEPProxyURL, FleetVarHostEndUserEmailIDP, + FleetVarPKICertPassword} fleetVarPrefixesSupportedInConfigProfiles = []string{FleetVarPKICertPrefix} ) @@ -3602,6 +3606,7 @@ func preprocessProfileContents( } var pkiCerts map[string]*fleet.PKICertificate + var pkiTemplates map[string]fleet.CertificateTemplate isPKIConfigured := func(profUUID string, target *cmdTarget, templateName string) (bool, error) { if !license.IsPremium(ctx) { return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, profUUID, @@ -3615,6 +3620,10 @@ func preprocessProfileContents( if template.Name == templateName { pkiName = items.PKIName pkiConfigured = true + if pkiTemplates == nil { + pkiTemplates = make(map[string]fleet.CertificateTemplate) + } + pkiTemplates[templateName] = template break } } @@ -3683,8 +3692,8 @@ func preprocessProfileContents( valid = false break } - case FleetVarHostEndUserEmailIDP: - // No extra validation needed for this variable + case FleetVarHostEndUserEmailIDP, FleetVarPKICertPassword: + // No extra validation needed for these variables default: if strings.HasPrefix(fleetVar, FleetVarPKICertPrefix) { // Get the template name @@ -3827,6 +3836,8 @@ func preprocessProfileContents( break } hostContents = replaceFleetVariable(fleetVarHostEndUserEmailIDPRegexp, hostContents, emails[0]) + case FleetVarPKICertPassword: + // We just keep it as is for now. default: if strings.HasPrefix(fleetVar, FleetVarPKICertPrefix) { // Get the template name @@ -3840,8 +3851,13 @@ func preprocessProfileContents( // This should never happen since we validated the PKI configuration earlier continue } + template, ok := pkiTemplates[templateName] + if !ok { + // This should never happen since we validated the PKI configuration earlier + continue + } // Insert the new certificate into the profile contents - certBase64, err := getNewPKICertificate(ctx, ds, pkiCert) + certBase64, err := getNewPKICertificate(ctx, ds, hostUUID, pkiCert, template, appConfig.OrgInfo.OrgName) if err != nil { // This is a server error, so we exit. return ctxerr.Wrap(ctx, err, "getting new PKI certificate") @@ -3900,7 +3916,8 @@ type GeneralNames struct { OtherName OtherName `asn1:"tag:0"` } -func getNewPKICertificate(ctx context.Context, ds fleet.Datastore, _ *fleet.PKICertificate) (string, error) { +func getNewPKICertificate(ctx context.Context, ds fleet.Datastore, hostUUID string, _ *fleet.PKICertificate, + pkiTemplate fleet.CertificateTemplate, orgName string) (string, error) { // This method retrieves a new PKI certificate from the CA and returns the base64-encoded certificate. notBefore := time.Now().Add(-time.Hour) @@ -3912,52 +3929,90 @@ func getNewPKICertificate(ctx context.Context, ds fleet.Datastore, _ *fleet.PKIC return "", ctxerr.Wrap(ctx, err, "generating serial number") } - orgName := "Acme Co" - commonName := "Cert name" - upnValue := "johnDoe@example.com" - - // This is where we create the UPN data structure, and marshal - // it into an asn1 object. - upnExt, err := asn1.Marshal(GeneralNames{ - OtherName: OtherName{ - // init our ASN.1 object identifier - OID: asn1.ObjectIdentifier{ - 1, 3, 6, 1, 4, 1, 311, 20, 2, 3}, - // This is the email address of the person we - // are generating the certificate for. - Value: UPN{ - A: upnValue, - }, - }, - }) - if err != nil { - return "", ctxerr.Wrap(ctx, err, "marshaling UPN extension") + // TODO: Also add support for end user email IdP + var hardwareSerial = "" + getHardwareSerial := func() (string, error) { + if hardwareSerial != "" { + return hardwareSerial, nil + } + // Get the hardware serial number + hosts, err := ds.ListHostsLiteByUUIDsNoFilter(ctx, []string{hostUUID}) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "getting hosts") + } + if len(hosts) == 0 { + // TODO: Improve error handling here -- a missing host should not cause all profile installations to fail + return "", ctxerr.Wrap(ctx, fmt.Errorf("host %s not found", hostUUID), "getting host") + } + hardwareSerial = hosts[0].HardwareSerial + return hardwareSerial, nil } - // Finally, we create a new extension with - // the OID 2.5.29.17 (SubjectAltName), and set the - // marshaled GeneralNames structure as the Value - // - // http://oid-info.com/get/2.5.29.17 - extSubjectAltName := pkix.Extension{ - Id: asn1.ObjectIdentifier{2, 5, 29, 17}, - Critical: false, - Value: upnExt, + if fleetVarHostHardwareSerial.MatchString(pkiTemplate.CommonName) { + // We only use the first hardware serial number + hardwareSerial, err = getHardwareSerial() + if err != nil { + return "", err + } + pkiTemplate.CommonName = fleetVarHostHardwareSerial.ReplaceAllString(pkiTemplate.CommonName, hardwareSerial) + } + userPrincipalName := "" + if len(pkiTemplate.SAN.UserPrincipalNames) > 0 { + userPrincipalName = pkiTemplate.SAN.UserPrincipalNames[0] + if fleetVarHostHardwareSerial.MatchString(userPrincipalName) { + // We only use the first hardware serial number + hardwareSerial, err = getHardwareSerial() + if err != nil { + return "", err + } + userPrincipalName = fleetVarHostHardwareSerial.ReplaceAllString(userPrincipalName, hardwareSerial) + } } + commonName := pkiTemplate.CommonName + template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{orgName}, CommonName: commonName, }, - NotBefore: notBefore, - NotAfter: notAfter, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - // Add subjectAltName - ExtraExtensions: []pkix.Extension{extSubjectAltName}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, } + if userPrincipalName != "" { + // This is where we create the UPN data structure, and marshal + // it into an asn1 object. + upnExt, err := asn1.Marshal(GeneralNames{ + OtherName: OtherName{ + // init our ASN.1 object identifier + OID: asn1.ObjectIdentifier{ + 1, 3, 6, 1, 4, 1, 311, 20, 2, 3}, + // This is the email address of the person we + // are generating the certificate for. + Value: UPN{ + A: userPrincipalName, + }, + }, + }) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "marshaling UPN extension") + } + // Finally, we create a new extension with + // the OID 2.5.29.17 (SubjectAltName), and set the + // marshaled GeneralNames structure as the Value + // + // http://oid-info.com/get/2.5.29.17 + extSubjectAltName := pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 5, 29, 17}, + Critical: false, + Value: upnExt, + } + template.ExtraExtensions = []pkix.Extension{extSubjectAltName} + } + // Get Fleet's CA cert fleetAssets, err := ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}, nil) if err != nil { @@ -3989,9 +4044,7 @@ func getNewPKICertificate(ctx context.Context, ds fleet.Datastore, _ *fleet.PKIC } // Create PKCS12 - // pfxData, err := pkcs12.Modern.Encode(privateKey, cert, []*x509.Certificate{caCertx509}, "123") // TODO: remove password - // pfxData, err := pkcs12.Modern.Encode(privateKey, cert, nil, "123") // TODO: remove password - pfxData, err := pkcs12.Legacy.Encode(privateKey, cert, nil, "test_password") + pfxData, err := pkcs12.Legacy.Encode(privateKey, cert, nil, "$FLEET_VAR_"+FleetVarPKICertPassword) if err != nil { return "", ctxerr.Wrap(ctx, err, "encoding PKCS12") } From a976f9582afbd0953b65b1fc3109be70f48a8640 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 22 Nov 2024 15:13:15 -0600 Subject: [PATCH 11/12] Fixing test. --- server/service/apple_mdm_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 4678e1d5b3a7..7f16dc55f9ae 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -4254,7 +4254,10 @@ func TestGetNewPKICertificate(t *testing.T) { }, nil } - cert64, err := getNewPKICertificate(ctx, ds, nil) + certTemplate := fleet.CertificateTemplate{ + CommonName: "My CN", + } + cert64, err := getNewPKICertificate(ctx, ds, "", nil, certTemplate, "My Org") require.NoError(t, err) assert.NotEmpty(t, cert64) } From 57d640da609235ccb5d1e76ac3cf22c1d05d2698 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 22 Nov 2024 15:40:19 -0600 Subject: [PATCH 12/12] Fixed config profile test. --- server/service/apple_mdm_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 7f16dc55f9ae..5fa221f10ebd 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -723,8 +723,9 @@ func TestNewMDMAppleConfigProfile(t *testing.T) { svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) - identifier := "Bar.$FLEET_VAR_HOST_END_USER_EMAIL_IDP" - mcBytes := mcBytesForTest("Foo", identifier, "UUID") + identifier := "Bar." + identifierWithVar := identifier + "$FLEET_VAR_HOST_END_USER_EMAIL_IDP" + mcBytes := mcBytesForTest("Foo", identifierWithVar, "UUID") r := bytes.NewReader(mcBytes) ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {