diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 03d413456a..0bb7b3ee93 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -41,6 +41,7 @@ import ( "github.com/fleetdm/fleet/v4/server/logging" "github.com/fleetdm/fleet/v4/server/mail" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/service/async" @@ -548,6 +549,23 @@ the way that the Fleet server works. appCfg.MDM.EnabledAndConfigured = true } + // register the Microsoft MDM services + var ( + wstepCertManager microsoft_mdm.CertManager + ) + // TODO: check if MicrosoftMDM is enabled? + if config.MDM.IsMicrosoftWSTEPSet() { + _, crtPEM, keyPEM, err := config.MDM.MicrosoftWSTEP() + if err != nil { + initFatal(err, "validate Microsoft WSTEP certificate and key") + } + wstepCertManager, err = microsoft_mdm.NewCertManager(ds, crtPEM, keyPEM) + if err != nil { + initFatal(err, "initialize mdm microsoft wstep depot") + } + + } + // save the app config with the updated MDM.Enabled value if err := ds.SaveAppConfig(context.Background(), appCfg); err != nil { initFatal(err, "saving app config") @@ -599,6 +617,7 @@ the way that the Fleet server works. mdmPushService, mdmPushCertTopic, cronSchedules, + wstepCertManager, ) if err != nil { initFatal(err, "initializing service") diff --git a/go.mod b/go.mod index a699ddc3fb..a12eea1008 100644 --- a/go.mod +++ b/go.mod @@ -247,6 +247,7 @@ require ( github.com/jonboulle/clockwork v0.2.2 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/klauspost/compress v1.15.11 // indirect + github.com/lib/pq v1.10.6 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -296,6 +297,7 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + github.com/ziutek/mymysql v1.5.4 // indirect go.elastic.co/apm v1.15.0 // indirect go.elastic.co/apm/module/apmhttp/v2 v2.3.0 // indirect go.elastic.co/fastjson v1.1.0 // indirect diff --git a/go.sum b/go.sum index 8cbba0d982..3dc1ef8ee3 100644 --- a/go.sum +++ b/go.sum @@ -900,6 +900,7 @@ github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/macadmins/osquery-extension v0.0.14 h1:GhTdqp5fEfD9AsIng1r9jIsYpmahelXwxVVd8by5NEk= @@ -1269,6 +1270,7 @@ github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPR github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c h1:TWQ2UvXPkhPxI2KmApKBOCaV6yD2N4mlvqFQ/DlPtpQ= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= diff --git a/server/config/config.go b/server/config/config.go index 0059e43963..e5d720c786 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -464,6 +464,20 @@ type MDMConfig struct { // AppleSCEPSignerAllowRenewalDays are the allowable renewal days for // certificates. AppleSCEPSignerAllowRenewalDays int `yaml:"apple_scep_signer_allow_renewal_days"` + + // MicrosoftWSTEPIdentityCert is the path to the certificate used to sign + // WSTEP responses. + MicrosoftWSTEPIdentityCert string `yaml:"microsoft_wstep_identity_cert"` + // MicrosoftWSTEPIdentityKey is the path to the private key used to sign + // WSTEP responses. + MicrosoftWSTEPIdentityKey string `yaml:"microsoft_wstep_identity_key"` + + // the following fields hold the parsed, validated TLS certificate set the + // first time Microsoft WSTEP is called, as well as the PEM-encoded + // bytes for the certificate and private key. + microsoftWSTEP *tls.Certificate + microsoftWSTEPCertPEM []byte + microsoftWSTEPKeyPEM []byte } type x509KeyPairConfig struct { @@ -654,6 +668,38 @@ func (m *MDMConfig) loadAppleBMEncryptedToken() ([]byte, error) { return tokBytes, nil } +// MicrosoftWSTEP returns the parsed and validated TLS certificate for Microsoft WSTEP. +// It parses and validates it if it hasn't been done yet. +func (m *MDMConfig) MicrosoftWSTEP() (cert *tls.Certificate, pemCert, pemKey []byte, err error) { + // TODO: should we also implement support for setting raw bytes in the config (like we do for Apple MDM)? + if m.microsoftWSTEP == nil { + pair := x509KeyPairConfig{ + m.MicrosoftWSTEPIdentityCert, + nil, + m.MicrosoftWSTEPIdentityKey, + nil, + } + cert, err := pair.Parse(true) + if err != nil { + return nil, nil, nil, fmt.Errorf("Microsoft MDM WSTEP configuration: %w", err) + } + m.microsoftWSTEP = cert + m.microsoftWSTEPCertPEM = pair.certBytes + m.microsoftWSTEPKeyPEM = pair.keyBytes + } + return m.microsoftWSTEP, m.microsoftWSTEPCertPEM, m.microsoftWSTEPKeyPEM, nil +} + +func (m *MDMConfig) IsMicrosoftWSTEPSet() bool { + pair := x509KeyPairConfig{ + m.MicrosoftWSTEPIdentityCert, + nil, + m.MicrosoftWSTEPIdentityKey, + nil, + } + return pair.IsSet() +} + type TLS struct { TLSCert string TLSKey string @@ -1044,6 +1090,8 @@ func (man Manager) addConfigs() { man.addConfigInt("mdm.apple_scep_signer_allow_renewal_days", 14, "Allowable renewal days for client certificates") man.addConfigString("mdm.apple_scep_challenge", "", "SCEP static challenge for enrollment") man.addConfigDuration("mdm.apple_dep_sync_periodicity", 1*time.Minute, "How much time to wait for DEP profile assignment") + man.addConfigString("mdm.microsoft_wstep_identity_cert", "", "Microsoft WSTEP PEM-encoded certificate path") + man.addConfigString("mdm.microsoft_wstep_identity_key", "", "Microsoft WSTEP PEM-encoded private key path") } // LoadConfig will load the config variables into a fully initialized @@ -1304,6 +1352,8 @@ func (man Manager) LoadConfig() FleetConfig { AppleSCEPSignerAllowRenewalDays: man.getConfigInt("mdm.apple_scep_signer_allow_renewal_days"), AppleSCEPChallenge: man.getConfigString("mdm.apple_scep_challenge"), AppleDEPSyncPeriodicity: man.getConfigDuration("mdm.apple_dep_sync_periodicity"), + MicrosoftWSTEPIdentityCert: man.getConfigString("mdm.microsoft_wstep_identity_cert"), + MicrosoftWSTEPIdentityKey: man.getConfigString("mdm.microsoft_wstep_identity_key"), }, } diff --git a/server/config/config_test.go b/server/config/config_test.go index be27be984a..fb10d7eafb 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -513,6 +513,52 @@ func TestAppleBMConfig(t *testing.T) { } } +func TestMicrosoftWSTEPConfig(t *testing.T) { + dir := t.TempDir() + certFile, keyFile, garbageFile, invalidKeyFile := filepath.Join(dir, "cert"), + filepath.Join(dir, "key"), + filepath.Join(dir, "garbage"), + filepath.Join(dir, "invalid_key") + require.NoError(t, os.WriteFile(certFile, testCert, 0o600)) + require.NoError(t, os.WriteFile(keyFile, testKey, 0o600)) + require.NoError(t, os.WriteFile(garbageFile, []byte("zzzz"), 0o600)) + require.NoError(t, os.WriteFile(invalidKeyFile, unrelatedTestKey, 0o600)) + + cases := []struct { + name string + in MDMConfig + errMatches string + }{ + {"missing cert", MDMConfig{MicrosoftWSTEPIdentityKey: keyFile}, `Microsoft MDM WSTEP configuration: no certificate provided`}, + {"missing key", MDMConfig{MicrosoftWSTEPIdentityCert: certFile}, "Microsoft MDM WSTEP configuration: no key provided"}, + {"cert file does not exist", MDMConfig{MicrosoftWSTEPIdentityCert: "no-such-file", MicrosoftWSTEPIdentityKey: keyFile}, `open no-such-file: no such file or directory`}, + {"key file does not exist", MDMConfig{MicrosoftWSTEPIdentityKey: "no-such-file", MicrosoftWSTEPIdentityCert: certFile}, `open no-such-file: no such file or directory`}, + {"valid file pairs", MDMConfig{MicrosoftWSTEPIdentityCert: certFile, MicrosoftWSTEPIdentityKey: keyFile}, ""}, + {"invalid file pairs", MDMConfig{MicrosoftWSTEPIdentityCert: certFile, MicrosoftWSTEPIdentityKey: invalidKeyFile}, "tls: private key does not match public key"}, + {"invalid file key", MDMConfig{MicrosoftWSTEPIdentityCert: certFile, MicrosoftWSTEPIdentityKey: garbageFile}, "tls: failed to find any PEM data"}, + {"invalid file cert", MDMConfig{MicrosoftWSTEPIdentityCert: garbageFile, MicrosoftWSTEPIdentityKey: keyFile}, "tls: failed to find any PEM data"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if c.in.MicrosoftWSTEPIdentityCert != "" || c.in.MicrosoftWSTEPIdentityKey != "" { + got, pemCert, pemKey, err := c.in.MicrosoftWSTEP() + if c.errMatches != "" { + require.Error(t, err) + require.Nil(t, got) + require.Regexp(t, c.errMatches, err.Error()) + } else { + require.NoError(t, err) + require.NotNil(t, got) + require.NotNil(t, got.Leaf) // TODO: confirm cert is not kept, not needed? + require.NotEmpty(t, pemCert) + require.NotEmpty(t, pemKey) + } + } + }) + } +} + var ( testCA = []byte(`-----BEGIN CERTIFICATE----- MIIFSzCCAzOgAwIBAgIUf4lOcb9bkN2+u6FjWL0fSFCjGGgwDQYJKoZIhvcNAQEL diff --git a/server/datastore/mysql/migrations/tables/20230629140529_AddWstepTables.go b/server/datastore/mysql/migrations/tables/20230629140529_AddWstepTables.go new file mode 100644 index 0000000000..7b71a41aa3 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230629140529_AddWstepTables.go @@ -0,0 +1,66 @@ +package tables + +import ( + "database/sql" +) + +func init() { + MigrationClient.AddMigration(Up_20230629140529, Down_20230629140529) +} + +func Up_20230629140529(tx *sql.Tx) error { + _, err := tx.Exec(` +CREATE TABLE wstep_serials ( + serial BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (serial) +);`) + if err != nil { + return err + } + + // assume that the first serial number is assigned to the CA cert + _, err = tx.Exec(` +ALTER TABLE wstep_serials AUTO_INCREMENT = 2;`) + if err != nil { + return err + } + + _, err = tx.Exec(` +CREATE TABLE wstep_certificates ( + serial BIGINT(20) UNSIGNED NOT NULL, + name VARCHAR(1024) NOT NULL, + not_valid_before DATETIME NOT NULL, + not_valid_after DATETIME NOT NULL, + certificate_pem TEXT NOT NULL, + revoked TINYINT(1) NOT NULL DEFAULT 0, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (serial), + FOREIGN KEY (serial) REFERENCES wstep_serials (serial) +)`) + if err != nil { + return err + } + + _, err = tx.Exec(` +CREATE TABLE wstep_cert_auth_associations ( + id VARCHAR(255) NOT NULL, + sha256 CHAR(64) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (id, sha256) +)`) + if err != nil { + return err + } + + return nil +} + +func Down_20230629140529(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230629140529_AddWstepTables_test.go b/server/datastore/mysql/migrations/tables/20230629140529_AddWstepTables_test.go new file mode 100644 index 0000000000..1d2147d39f --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230629140529_AddWstepTables_test.go @@ -0,0 +1,20 @@ +package tables + +import "testing" + +func TestUp_20230629140529(t *testing.T) { + db := applyUpToPrev(t) + + // + // Insert data to test the migration + // + // ... + + // Apply current migration. + applyNext(t, db) + + // + // Check data, insert new entries, e.g. to verify migration is safe. + // + // ... +} diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index fa329ed4f3..7096be9448 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -1111,8 +1111,10 @@ while the second part `[^[:ascii:]]` matches any character that is not within th So, when these two parts are combined with no space in between, the resulting regex matches any sequence of characters where the first character is within the ASCII range and the following characters are not within the ASCII range. */ -var nonascii = regexp.MustCompile(`(?P[[:ascii:]])(?P[^[:ascii:]]+)`) -var nonacsiiReplace = regexp.MustCompile(`[^[:ascii:]]`) +var ( + nonascii = regexp.MustCompile(`(?P[[:ascii:]])(?P[^[:ascii:]]+)`) + nonacsiiReplace = regexp.MustCompile(`[^[:ascii:]]`) +) func hostSearchLike(sql string, params []interface{}, match string, columns ...string) (string, []interface{}, bool) { var matchesEmail bool diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 2c354ed202..e1ec706164 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -641,9 +641,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=195 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=196 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'); +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'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1208,6 +1208,39 @@ CREATE TABLE `windows_updates` ( KEY `idx_update_date` (`host_id`,`date_epoch`) ) 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 */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `wstep_cert_auth_associations` ( + `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `sha256` char(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`,`sha256`) +) 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 */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `wstep_certificates` ( + `serial` bigint(20) unsigned NOT NULL, + `name` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL, + `not_valid_before` datetime NOT NULL, + `not_valid_after` datetime NOT NULL, + `certificate_pem` text COLLATE utf8mb4_unicode_ci NOT NULL, + `revoked` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`serial`), + CONSTRAINT `wstep_certificates_ibfk_1` FOREIGN KEY (`serial`) REFERENCES `wstep_serials` (`serial`) +) 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 */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `wstep_serials` ( + `serial` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`serial`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; /*!50001 DROP VIEW IF EXISTS `nano_view_queue`*/; /*!50001 SET @saved_cs_client = @@character_set_client */; /*!50001 SET @saved_cs_results = @@character_set_results */; diff --git a/server/datastore/mysql/wstep.go b/server/datastore/mysql/wstep.go new file mode 100644 index 0000000000..a031ee18e3 --- /dev/null +++ b/server/datastore/mysql/wstep.go @@ -0,0 +1,69 @@ +package mysql + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "errors" + "fmt" + "math/big" + "strings" + + apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" +) + +// CertStore implements storage tasks associated with MS-WSTEP messages in the MS-MDE2 +// protocol. It is implemented by fleet.Datastore. +var _ microsoft_mdm.CertStore = (*Datastore)(nil) + +// WSTEPStoreCertificate stores a certificate under the given name. +// +// If the provided certificate has empty crt.Subject.CommonName, +// then the hex sha256 of the crt.Raw is used as name. +func (ds *Datastore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error { + if crt.Subject.CommonName == "" { + name = fmt.Sprintf("%x", sha256.Sum256(crt.Raw)) + } + if !crt.SerialNumber.IsInt64() { + return errors.New("cannot represent serial number as int64") + } + certPEM := apple_mdm.EncodeCertPEM(crt) + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO wstep_certificates + (serial, name, not_valid_before, not_valid_after, certificate_pem) +VALUES + (?, ?, ?, ?, ?)`, + crt.SerialNumber.Int64(), + name, + crt.NotBefore, + crt.NotAfter, + certPEM, + ) + return err +} + +// WSTEPNewSerial allocates and returns a new (increasing) serial number. +func (ds *Datastore) WSTEPNewSerial(ctx context.Context) (*big.Int, error) { + result, err := ds.writer(ctx).ExecContext(ctx, `INSERT INTO wstep_serials () VALUES ();`) + if err != nil { + return nil, err + } + lid, err := result.LastInsertId() // TODO: ok if sequential and not random? + if err != nil { + return nil, err + } + // TODO: check maxSerialNumber? + return big.NewInt(lid), nil +} + +func (ds *Datastore) WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error { + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO wstep_cert_auth_associations (id, sha256) VALUES (?, ?) AS new +ON DUPLICATE KEY +UPDATE sha256 = new.sha256;`, + deviceUUID, + strings.ToUpper(hash), // TODO: confirm if this is necessary + ) + return err +} diff --git a/server/datastore/mysql/wstep_test.go b/server/datastore/mysql/wstep_test.go new file mode 100644 index 0000000000..8d42f47574 --- /dev/null +++ b/server/datastore/mysql/wstep_test.go @@ -0,0 +1,158 @@ +package mysql + +import ( + "context" + "crypto/sha256" + "encoding/pem" + "fmt" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/micromdm/nanomdm/cryptoutil" + "github.com/stretchr/testify/require" +) + +func TestWSTEPStore(t *testing.T) { + ds := CreateMySQLDS(t) + + wantCert, err := cryptoutil.DecodePEMCertificate(testCert) + require.NoError(t, err) + require.NoError(t, err) + + // serial number should start at 2 because 1 is reserved for the CA cert + sn, err := ds.WSTEPNewSerial(context.Background()) + require.NoError(t, err) + require.NotNil(t, sn) + require.Equal(t, int64(2), sn.Int64()) + + // serial should increment + sn, err = ds.WSTEPNewSerial(context.Background()) + require.NoError(t, err) + require.NotNil(t, sn) + require.Equal(t, int64(3), sn.Int64()) + + testCert := *wantCert + testCert.SerialNumber = sn + + // store without setting a common name in the cert + err = ds.WSTEPStoreCertificate(context.Background(), "test", &testCert) + require.NoError(t, err) + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + var dest []struct { + Name string `db:"name"` + CertPEM []byte `db:"certificate_pem"` + } + err = sqlx.SelectContext(context.Background(), q, &dest, "SELECT name, certificate_pem FROM wstep_certificates where serial = 3") + if err != nil { + return err + } + require.Len(t, dest, 1) + wantPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: testCert.Raw, + }) + require.Equal(t, wantPEM, dest[0].CertPEM) + + // name wasn't set in the test cert, so it should default to sha256 of the cert + require.Equal(t, fmt.Sprintf("%x", sha256.Sum256(testCert.Raw)), dest[0].Name) + + return nil + }) + + // store with a common name in the cert + testCert.Subject.CommonName = "test" + err = ds.WSTEPStoreCertificate(context.Background(), "test", &testCert) + require.Error(t, err) // duplicate serial number + + // get a new serial number + sn, err = ds.WSTEPNewSerial(context.Background()) + require.NoError(t, err) + require.NotNil(t, sn) + require.Equal(t, int64(4), sn.Int64()) + + testCert.SerialNumber = sn + err = ds.WSTEPStoreCertificate(context.Background(), "test", &testCert) + require.NoError(t, err) + + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + var dest []struct { + Name string `db:"name"` + CertPEM []byte `db:"certificate_pem"` + } + err = sqlx.SelectContext(context.Background(), q, &dest, "SELECT name, certificate_pem FROM wstep_certificates where serial = 4") + if err != nil { + return err + } + require.Len(t, dest, 1) + wantPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: testCert.Raw, + }) + require.Equal(t, wantPEM, dest[0].CertPEM) + + // name wasn't set in the test cert, so it should default to sha256 of the cert + require.Equal(t, "test", dest[0].Name) + + return nil + }) + + // TODO: test WSTEPAssociateCertHash when the intended usage is clear +} + +var testCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAvMQkwBwYD +VQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1NDRVAg +Q0EwHhcNMjIxMjIyMTM0NDMzWhcNMzIxMjIyMTM0NDMzWjAvMQkw +BwYDVQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1ND +RVAgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV +u9YVfl7gu0UgUkOJoES/XrN0WZdIjgvS2upKfvP4LSJOq1Mnp3bH +wWOA2NkHem/kjOVeotOk1aEYIzxbic6VlvNOz9huOhbJyoV4TO5v +tp/GFFcJ4IXh+f1Q4vm/NeH/XxEWn9S20B9OkSMOUievYsAu6iSi +oWaa74q1mnfpzM29p3dNM82mCKutYdkW0EusixU/CQxcVhdcxC+R +RyM4jzBFIipa7H20UtqdkZ03/9BoowJb/h/r4X7TN4tKg2vcwpZK +uJo7VcTBNPxhBowzg3JUmzjCnxPbuU/Ow5kPGOLJtbf4766ToNTM +/J63i3UPshKUBqAE8mIZO3qb7s25AgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTxPEY4 +WvsLCt+HDQfnEPOKrHu0gTANBgkqhkiG9w0BAQsFAAOCAQEAGNf5 +R60vRxIfvSOUyV3X7lUk+fVvi1CKC43DsP5OsQ6g5YVGcVXN40U4 +2o7JUeb9K1jvqnzWB/3k+lSCkEb0a5KabjZE5Vpdt9xctmgrfNnQ +PBCfDdyb0Upjm61CJeB2SW9+ibT2L+OtL/nZjjlugL7ir9ramQBh +0IY6oB9Yc3TyZyPjnXwbi0jv5cildzIYaYPvPkPPTjezOUqUDgUH +JtdWRBQeJ/6WxAAm9il0KVXOsRPgAsdiDJTF6FdW4lsY8V/R6y0H +hTN1ZSyqklKAuvEZZznfmJsrNYRII2Fv2zOk0Uv/+E+EKTOHbgcC +PQAARDBzDlWvlMGWcbdrdypdeA== +-----END CERTIFICATE----- +`) + +// var testKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- +// MIIEowIBAAKCAQEA1bvWFX5e4LtFIFJDiaBEv16zdFmXSI4L0trqSn7z+C0iTqtT +// J6d2x8FjgNjZB3pv5IzlXqLTpNWhGCM8W4nOlZbzTs/YbjoWycqFeEzub7afxhRX +// CeCF4fn9UOL5vzXh/18RFp/UttAfTpEjDlInr2LALuokoqFmmu+KtZp36czNvad3 +// TTPNpgirrWHZFtBLrIsVPwkMXFYXXMQvkUcjOI8wRSIqWux9tFLanZGdN//QaKMC +// W/4f6+F+0zeLSoNr3MKWSriaO1XEwTT8YQaMM4NyVJs4wp8T27lPzsOZDxjiybW3 +// +O+uk6DUzPyet4t1D7ISlAagBPJiGTt6m+7NuQIDAQABAoIBAE6LXL1BV3SW3Wxn +// TtKAx0Lcdm5HjkTnjojKUldWGCoXzAfFBiYIcKov83UiO394Cy6eaJxCkix9JVpN +// eJzbI8PtWTSZRRwc1MsLVclD3EvJfSW5y9KhZBILYIAdKVKPZqIGOa1qxyz3hsnE +// pHFa16KoU5/qA9SQI7jEVuEuBusv4D/dRlEWvva7QOhnLrBPrSnTSZ5LxCFKRviS +// XrEQ9AuRJeXCKx4WzXd4IZPpgldYHMJSSGMr0TeVcURbsfveI2IWvOLag0ofTHhx +// tolBT2sKzInItLTwt/irZEp5lV08mMGxHuxoCdzhxjFQP8eGOZzPW65c6/D9hEXd +// DzWnjdECgYEA9QtTQosOTtAyU1i4Fm76ltT6nywHy23KAMhBaoKgTMccNtjaOCg/ +// 5FCCRD+qoo7TF4jdliP2NrMIbAIhr4jEfHSMKaD/rae1xqInseDCrGi9gzvm8UxG +// 84VG30Id8s70ZQWZjR/PFFDeNZjNhlk8COO0XoLaqJSZr+A30aSyeUsCgYEA30ok +// 3EvO1+/gjZv28J9vApdbiEwtO9xoteghElFzdtuEuzA+wL83w8xvKvdb4Rk5xigE +// 6mV69dBPj8zSyGp0lFTYLFvry5N4S8L6QPzt2nk+Lc3cDKSA5CkAkQ5Dmt5JwhxF +// qIPDNZGXmoldIWJ0p/ZSu98/1yXBMQ9gCje/losCgYBwuk4KLbheT27nYsgFIfbL +// zpyg/vty/UXRiE53tjISQALdxHLXJMUHvnW++d8Au12m1QLDIDYTQdddALoIa42g +// h2k3eWZFuAJqp4xFS1WjROfx6Gu8k8+MFcLd0CfA3K4XjzTtdDWqbe1bkLjz1jdF +// C6OdWutGZF4zR53GJtMn8wKBgCfA95cRGB5x4rTTk797YzQ+5lj51wPVVf8s+NZe +// EgSTSKpbCJEgejkt6IzpxT3qU9LnxRhGQQIKuF+Nw+lSqrbN9D7RjsWL19sFN7Di +// VyaSd3OINyk5EImOkz9AHuEvukoI5o3+B38+EJO+6QnMkaBlxo0UTjVrz12As0Se +// cEnJAoGBAOUXjez9oUSzLzqG/WJFrIfHyjDA1vBS1j39XuhDuJGqMdNLlCE8Yr7h +// d3gpZeuV3ZC33QAuwAXfRBNnKIDtDGpcrozM1NndcBVDs9GYvobaTiUaODGjsH44 +// oHwpyQbv9Qs+3bjPOQ7DkwekT+w1cptEKudBCC3WQKui1P0NNL0R +// -----END RSA PRIVATE KEY----- +// `) + +// // prevent static analysis tools from raising issues due to detection of private key +// // in code. +// func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 23692c2663..146c5951c2 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -2,9 +2,11 @@ package fleet import ( "context" + "crypto/x509" "encoding/json" "errors" "io" + "math/big" "time" "github.com/fleetdm/fleet/v4/server/config" @@ -985,6 +987,16 @@ type Datastore interface { // DeleteHostDEPAssignments marks as deleted entries in // host_dep_assignments for host with matching serials. DeleteHostDEPAssignments(ctx context.Context, serials []string) error + + /////////////////////////////////////////////////////////////////////////////// + // Microsoft MDM + + // WSTEPStoreCertificate stores a certificate in the database. + WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error + // WSTEPNewSerial returns a new serial number for a certificate. + WSTEPNewSerial(ctx context.Context) (*big.Int, error) + // WSTEPAssociateCertHash associates a certificate hash with a device. + WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error } const ( diff --git a/server/fleet/service.go b/server/fleet/service.go index 81434a74af..30e3783298 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -2,6 +2,7 @@ package fleet import ( "context" + "crypto/x509" "encoding/json" "io" "time" @@ -763,4 +764,8 @@ type Service interface { // GetAuthorizedSoapFault authorize the request so SoapFault message can be returned GetAuthorizedSoapFault(ctx context.Context, eType string, origMsg int, errorMsg error) *SoapFault + + // SignMDMMicrosoftClientCSR returns a signed certificate from the client certificate signing request and the + // certificate fingerprint. The certificate common name should be passed in the subject parameter. + SignMDMMicrosoftClientCSR(ctx context.Context, subject string, csr *x509.CertificateRequest) ([]byte, string, error) } diff --git a/server/mdm/microsoft/wstep.go b/server/mdm/microsoft/wstep.go new file mode 100644 index 0000000000..7344bd664a --- /dev/null +++ b/server/mdm/microsoft/wstep.go @@ -0,0 +1,249 @@ +package microsoft_mdm + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" //nolint:gosec + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "time" + + "github.com/fleetdm/fleet/v4/server" + "github.com/micromdm/nanomdm/cryptoutil" + "go.mozilla.org/pkcs7" +) + +// TODO: Replace with imports from Marcos' PR +const ( + DocProvisioningAppProviderID = "FleetDM" + CertRenewalPeriodInSecs = "15552000" +) + +// TODO: Replace with imports from Marcos' PR +type BinSecTokenType int + +// TODO: Replace with imports from Marcos' PR +const ( + MDETokenPKCS7 BinSecTokenType = iota + MDETokenPKCS10 + MDETokenPKCSInvalid +) + +// CertManager is an interface for certificate management tasks associated with Microsoft MDM (e.g., +// signing CSRs). +type CertManager interface { + // IdentityFingerprint returns the hex-encoded, uppercased sha1 fingerprint of the identity certificate. + IdentityFingerprint() string + + // SignClientCSR signs a client CSR and returns the signed, DER-encoded certificate bytes and + // its uppercased, hex-endcoded sha1 fingerprint. The subject passed is set as the common name of + // the signed certificate. + SignClientCSR(ctx context.Context, subject string, clientCSR *x509.CertificateRequest) ([]byte, string, error) + + // TODO: implement other methods as needed: + // - verify certificate-device association + // - certificate lifecycle management (e.g., renewal, revocation) +} + +// CertStore implements storage tasks associated with MS-WSTEP messages in the MS-MDE2 +// protocol. It is implemented by fleet.Datastore. +type CertStore interface { + WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error + WSTEPNewSerial(ctx context.Context) (*big.Int, error) + WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error +} + +type manager struct { + store CertStore + + // identityCert holds the identity certificate of the depot. + identityCert *x509.Certificate + // identityPrivateKey holds the private key of the depot. + identityPrivateKey *rsa.PrivateKey + // identityFingerprint holds the hex-encoded, sha1 fingerprint of the identity certificate. + identityFingerprint string + // maxSerialNumber holds the maximum serial number. The maximum value a serial number can have + // is 2^160. However, this could be limited further if required. + maxSerialNumber *big.Int +} + +// NewCertManager returns a new CertManager instance. +func NewCertManager(store CertStore, certPEM []byte, privKeyPEM []byte) (CertManager, error) { + return newManager(store, certPEM, privKeyPEM) +} + +func newManager(store CertStore, certPEM []byte, privKeyPEM []byte) (*manager, error) { + crt, err := cryptoutil.DecodePEMCertificate(certPEM) + if err != nil { + return nil, fmt.Errorf("decode certificate: %w", err) + } + key, err := server.DecodePrivateKeyPEM(privKeyPEM) + if err != nil { + return nil, fmt.Errorf("decode private key: %w", err) + } + fp := CertFingerprintHexStr(crt) + + return &manager{ + store: store, + identityCert: crt, + identityPrivateKey: key, + identityFingerprint: fp, + maxSerialNumber: new(big.Int).Lsh(big.NewInt(1), 128), // 2^12, + }, nil +} + +func (m *manager) IdentityFingerprint() string { + return m.identityFingerprint +} + +// TODO: Marcos to update the POC implementation of this function +func (m *manager) SignClientCSR(ctx context.Context, subject string, clientCSR *x509.CertificateRequest) ([]byte, string, error) { + if m.identityCert == nil || m.identityPrivateKey == nil { + return nil, "", errors.New("invalid identity certificate or private key") + } + + // serial number is used to uniquely identify the certificate + sn, err := m.store.WSTEPNewSerial(ctx) + if err != nil { + return nil, "", fmt.Errorf("failed to generate serial number: %w", err) + } + + // populate the client certificate template + tmpl, err := populateClientCert(sn, subject, m.identityCert, clientCSR) + if err != nil { + return nil, "", fmt.Errorf("failed to populate client certificate: %w", err) + } + + rawSignedDER, err := x509.CreateCertificate(rand.Reader, tmpl, m.identityCert, clientCSR.PublicKey, m.identityPrivateKey) + if err != nil { + return nil, "", fmt.Errorf("failed to sign client certificate: %w", err) + } + + signedCert, err := x509.ParseCertificate(rawSignedDER) + if err != nil { + return nil, "", fmt.Errorf("failed to parse client certificate: %w", err) + } + + if err := m.store.WSTEPStoreCertificate(ctx, subject, signedCert); err != nil { + return nil, "", fmt.Errorf("failed to store client certificate: %w", err) + } + + return rawSignedDER, CertFingerprintHexStr(signedCert), nil +} + +func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificate, csr *x509.CertificateRequest) (*x509.Certificate, error) { + certRenewalPeriodInSecsInt, err := strconv.Atoi(CertRenewalPeriodInSecs) + if err != nil { + return nil, fmt.Errorf("invalid renewal time: %w", err) + } + + notBeforeDuration := time.Now().Add(time.Duration(certRenewalPeriodInSecsInt) * -time.Second) + yearDuration := 365 * 24 * time.Hour + + certSubject := pkix.Name{ + OrganizationalUnit: []string{DocProvisioningAppProviderID}, + CommonName: subject, + } + + tmpl := &x509.Certificate{ + Subject: certSubject, + Issuer: issuerCert.Issuer, + Version: csr.Version, + PublicKey: csr.PublicKey, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + Signature: csr.Signature, + SignatureAlgorithm: csr.SignatureAlgorithm, + Extensions: csr.Extensions, + ExtraExtensions: csr.ExtraExtensions, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + DNSNames: csr.DNSNames, + URIs: csr.URIs, + NotBefore: notBeforeDuration, + NotAfter: notBeforeDuration.Add(yearDuration), + SerialNumber: sn, + KeyUsage: x509.KeyUsageDigitalSignature, + + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: false, + } + return tmpl, nil +} + +// GetClientCSR returns the client certificate signing request from the BinarySecurityToken +// +// TODO: Marcos to update the POC implementation of this function +func GetClientCSR(binSecTokenData string, tokenType BinSecTokenType) (*x509.CertificateRequest, error) { + // Verify the token padding type + if (tokenType != MDETokenPKCS7) && (tokenType != MDETokenPKCS10) { + return nil, fmt.Errorf("provided binary security token type is invalid: %d", tokenType) + } + + // Decoding the Base64 encoded binary security token to obtain the client CSR bytes + rawCSR, err := base64.StdEncoding.DecodeString(binSecTokenData) + if err != nil { + return nil, fmt.Errorf("problem decoding the binary security token: %v", err) + } + + // Sanity checks on binary signature token + // Sanity checks are done on PKCS10 for the moment + if tokenType == MDETokenPKCS7 { + // Parse the CSR in PKCS7 Syntax Standard + pk7CSR, err := pkcs7.Parse(rawCSR) + if err != nil { + return nil, fmt.Errorf("problem parsing the binary security token: %v", err) + } + + // Verify the signatures of the CSR PKCS7 object + err = pk7CSR.Verify() + if err != nil { + return nil, fmt.Errorf("problem verifying CSR data: %v", err) + } + + // Verify signing time + currentTime := time.Now() + if currentTime.Before(pk7CSR.GetOnlySigner().NotBefore) || currentTime.After(pk7CSR.GetOnlySigner().NotAfter) { + return nil, fmt.Errorf("invalid CSR signing time: %v", err) + } + } + + // Decode and verify CSR + certCSR, err := x509.ParseCertificateRequest(rawCSR) + if err != nil { + return nil, fmt.Errorf("problem parsing CSR data: %v", err) + } + + err = certCSR.CheckSignature() + if err != nil { + return nil, fmt.Errorf("invalid CSR signature: %v", err) + } + + if certCSR.PublicKey == nil { + return nil, fmt.Errorf("invalid CSR public key: %v", err) + } + + if len(certCSR.Subject.String()) == 0 { + return nil, fmt.Errorf("invalid CSR subject: %v", err) + } + + return certCSR, nil +} + +// CertFingerprintHexStr returns the hex-encoded, uppercased sha1 fingerprint of the certificate. +func CertFingerprintHexStr(cert *x509.Certificate) string { + // Windows Certificate Store requires passing the certificate thumbprint, which is the same as + // SHA1 fingerprint. See also: + // https://security.stackexchange.com/questions/14330/what-is-the-actual-value-of-a-certificate-fingerprint + // https://www.thesslstore.com/blog/ssl-certificate-still-sha-1-thumbprint/ + fingerprint := sha1.Sum(cert.Raw) //nolint:gosec + return strings.ToUpper(hex.EncodeToString(fingerprint[:])) +} diff --git a/server/mdm/microsoft/wstep_test.go b/server/mdm/microsoft/wstep_test.go new file mode 100644 index 0000000000..66c202a5b3 --- /dev/null +++ b/server/mdm/microsoft/wstep_test.go @@ -0,0 +1,180 @@ +package microsoft_mdm + +import ( + "context" + "crypto/sha1" //nolint:gosec + "crypto/x509" + "encoding/hex" + "errors" + "math/big" + "strings" + "testing" + + "github.com/fleetdm/fleet/v4/server" + "github.com/micromdm/nanomdm/cryptoutil" + "github.com/stretchr/testify/require" +) + +type mockStore struct{} + +func (m *mockStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error { + return nil +} + +func (m *mockStore) WSTEPNewSerial(ctx context.Context) (*big.Int, error) { + return nil, nil +} + +func (m *mockStore) WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error { + return nil +} + +var _ CertStore = (*mockStore)(nil) + +func TestNewCertManager(t *testing.T) { + var store CertStore + + wantCert, err := cryptoutil.DecodePEMCertificate(testCert) + require.NoError(t, err) + wantKey, err := server.DecodePrivateKeyPEM(testKey) + require.NoError(t, err) + wantIdentityFingerprint := CertFingerprintHexStr(wantCert) + + // Test that NewCertManager returns an error if the cert PEM is invalid. + _, err = NewCertManager(store, []byte("invalid"), testKey) + require.Error(t, err) + require.ErrorContains(t, err, "failed to decode PEM certificate") + + // Test that NewCertManager returns an error if the key PEM is invalid. + _, err = NewCertManager(store, testCert, []byte("invalid")) + require.Error(t, err) + require.ErrorContains(t, err, "decode private key: no PEM-encoded data found") + + // Test that NewCertManager returns an error if the cert PEM is not a certificate. + _, err = NewCertManager(store, testKey, testKey) + require.Error(t, err) + require.ErrorContains(t, err, "failed to decode PEM certificate") + + // Test that NewCertManager returns an error if the key PEM is not a private key. + _, err = NewCertManager(store, testCert, testCert) + require.Error(t, err) + require.ErrorContains(t, err, "decode private key: unexpected block type") + + // Test that NewCertManager returns a *WSTEPDepot if the cert and key PEMs are valid. + cm, err := NewCertManager(store, testCert, testKey) + require.NoError(t, err) + require.NotNil(t, cm) + require.Equal(t, wantIdentityFingerprint, cm.IdentityFingerprint()) + + // Test that newManager sets the correct fields. + m := cm.(*manager) + require.NoError(t, err) + require.Equal(t, *wantCert, *m.identityCert) + require.NoError(t, err) + require.Equal(t, *wantKey, *m.identityPrivateKey) + require.Equal(t, wantIdentityFingerprint, m.identityFingerprint) +} + +func TestSignClientCSR(t *testing.T) { + // TODO +} + +func TestGetClientCSR(t *testing.T) { + // TODO +} + +func TestCertFingerprintHexStr(t *testing.T) { + cases := []struct { + name string + cert []byte + err error + }{ + { + name: "valid cert", + cert: testCert, + err: nil, + }, + { + name: "invalid cert", + cert: []byte("invalid"), + err: errors.New("failed to decode PEM certificate"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cert, err := cryptoutil.DecodePEMCertificate(tc.cert) + if tc.err != nil { + require.Error(t, err) + require.ErrorContains(t, err, tc.err.Error()) + return + } + + require.NoError(t, err) + csum := sha1.Sum(cert.Raw) // nolint:gosec + want := strings.ToUpper(hex.EncodeToString(csum[:])) + fp := CertFingerprintHexStr(cert) + require.Equal(t, want, fp) + }) + } +} + +var ( + testCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAvMQkwBwYD +VQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1NDRVAg +Q0EwHhcNMjIxMjIyMTM0NDMzWhcNMzIxMjIyMTM0NDMzWjAvMQkw +BwYDVQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1ND +RVAgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV +u9YVfl7gu0UgUkOJoES/XrN0WZdIjgvS2upKfvP4LSJOq1Mnp3bH +wWOA2NkHem/kjOVeotOk1aEYIzxbic6VlvNOz9huOhbJyoV4TO5v +tp/GFFcJ4IXh+f1Q4vm/NeH/XxEWn9S20B9OkSMOUievYsAu6iSi +oWaa74q1mnfpzM29p3dNM82mCKutYdkW0EusixU/CQxcVhdcxC+R +RyM4jzBFIipa7H20UtqdkZ03/9BoowJb/h/r4X7TN4tKg2vcwpZK +uJo7VcTBNPxhBowzg3JUmzjCnxPbuU/Ow5kPGOLJtbf4766ToNTM +/J63i3UPshKUBqAE8mIZO3qb7s25AgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTxPEY4 +WvsLCt+HDQfnEPOKrHu0gTANBgkqhkiG9w0BAQsFAAOCAQEAGNf5 +R60vRxIfvSOUyV3X7lUk+fVvi1CKC43DsP5OsQ6g5YVGcVXN40U4 +2o7JUeb9K1jvqnzWB/3k+lSCkEb0a5KabjZE5Vpdt9xctmgrfNnQ +PBCfDdyb0Upjm61CJeB2SW9+ibT2L+OtL/nZjjlugL7ir9ramQBh +0IY6oB9Yc3TyZyPjnXwbi0jv5cildzIYaYPvPkPPTjezOUqUDgUH +JtdWRBQeJ/6WxAAm9il0KVXOsRPgAsdiDJTF6FdW4lsY8V/R6y0H +hTN1ZSyqklKAuvEZZznfmJsrNYRII2Fv2zOk0Uv/+E+EKTOHbgcC +PQAARDBzDlWvlMGWcbdrdypdeA== +-----END CERTIFICATE----- +`) + + testKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- +MIIEowIBAAKCAQEA1bvWFX5e4LtFIFJDiaBEv16zdFmXSI4L0trqSn7z+C0iTqtT +J6d2x8FjgNjZB3pv5IzlXqLTpNWhGCM8W4nOlZbzTs/YbjoWycqFeEzub7afxhRX +CeCF4fn9UOL5vzXh/18RFp/UttAfTpEjDlInr2LALuokoqFmmu+KtZp36czNvad3 +TTPNpgirrWHZFtBLrIsVPwkMXFYXXMQvkUcjOI8wRSIqWux9tFLanZGdN//QaKMC +W/4f6+F+0zeLSoNr3MKWSriaO1XEwTT8YQaMM4NyVJs4wp8T27lPzsOZDxjiybW3 ++O+uk6DUzPyet4t1D7ISlAagBPJiGTt6m+7NuQIDAQABAoIBAE6LXL1BV3SW3Wxn +TtKAx0Lcdm5HjkTnjojKUldWGCoXzAfFBiYIcKov83UiO394Cy6eaJxCkix9JVpN +eJzbI8PtWTSZRRwc1MsLVclD3EvJfSW5y9KhZBILYIAdKVKPZqIGOa1qxyz3hsnE +pHFa16KoU5/qA9SQI7jEVuEuBusv4D/dRlEWvva7QOhnLrBPrSnTSZ5LxCFKRviS +XrEQ9AuRJeXCKx4WzXd4IZPpgldYHMJSSGMr0TeVcURbsfveI2IWvOLag0ofTHhx +tolBT2sKzInItLTwt/irZEp5lV08mMGxHuxoCdzhxjFQP8eGOZzPW65c6/D9hEXd +DzWnjdECgYEA9QtTQosOTtAyU1i4Fm76ltT6nywHy23KAMhBaoKgTMccNtjaOCg/ +5FCCRD+qoo7TF4jdliP2NrMIbAIhr4jEfHSMKaD/rae1xqInseDCrGi9gzvm8UxG +84VG30Id8s70ZQWZjR/PFFDeNZjNhlk8COO0XoLaqJSZr+A30aSyeUsCgYEA30ok +3EvO1+/gjZv28J9vApdbiEwtO9xoteghElFzdtuEuzA+wL83w8xvKvdb4Rk5xigE +6mV69dBPj8zSyGp0lFTYLFvry5N4S8L6QPzt2nk+Lc3cDKSA5CkAkQ5Dmt5JwhxF +qIPDNZGXmoldIWJ0p/ZSu98/1yXBMQ9gCje/losCgYBwuk4KLbheT27nYsgFIfbL +zpyg/vty/UXRiE53tjISQALdxHLXJMUHvnW++d8Au12m1QLDIDYTQdddALoIa42g +h2k3eWZFuAJqp4xFS1WjROfx6Gu8k8+MFcLd0CfA3K4XjzTtdDWqbe1bkLjz1jdF +C6OdWutGZF4zR53GJtMn8wKBgCfA95cRGB5x4rTTk797YzQ+5lj51wPVVf8s+NZe +EgSTSKpbCJEgejkt6IzpxT3qU9LnxRhGQQIKuF+Nw+lSqrbN9D7RjsWL19sFN7Di +VyaSd3OINyk5EImOkz9AHuEvukoI5o3+B38+EJO+6QnMkaBlxo0UTjVrz12As0Se +cEnJAoGBAOUXjez9oUSzLzqG/WJFrIfHyjDA1vBS1j39XuhDuJGqMdNLlCE8Yr7h +d3gpZeuV3ZC33QAuwAXfRBNnKIDtDGpcrozM1NndcBVDs9GYvobaTiUaODGjsH44 +oHwpyQbv9Qs+3bjPOQ7DkwekT+w1cptEKudBCC3WQKui1P0NNL0R +-----END RSA PRIVATE KEY----- +`)) +) + +// prevent static analysis tools from raising issues due to detection of private key +// in code. +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 309b6078de..840dd1f38b 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -4,7 +4,9 @@ package mock import ( "context" + "crypto/x509" "encoding/json" + "math/big" "sync" "time" @@ -646,6 +648,12 @@ type GetMatchingHostSerialsFunc func(ctx context.Context, serials []string) (map type DeleteHostDEPAssignmentsFunc func(ctx context.Context, serials []string) error +type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error + +type WSTEPNewSerialFunc func(ctx context.Context) (*big.Int, error) + +type WSTEPAssociateCertHashFunc func(ctx context.Context, deviceUUID string, hash string) error + type DataStore struct { HealthCheckFunc HealthCheckFunc HealthCheckFuncInvoked bool @@ -1592,6 +1600,15 @@ type DataStore struct { DeleteHostDEPAssignmentsFunc DeleteHostDEPAssignmentsFunc DeleteHostDEPAssignmentsFuncInvoked bool + WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc + WSTEPStoreCertificateFuncInvoked bool + + WSTEPNewSerialFunc WSTEPNewSerialFunc + WSTEPNewSerialFuncInvoked bool + + WSTEPAssociateCertHashFunc WSTEPAssociateCertHashFunc + WSTEPAssociateCertHashFuncInvoked bool + mu sync.Mutex } @@ -3799,3 +3816,24 @@ func (s *DataStore) DeleteHostDEPAssignments(ctx context.Context, serials []stri s.mu.Unlock() return s.DeleteHostDEPAssignmentsFunc(ctx, serials) } + +func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error { + s.mu.Lock() + s.WSTEPStoreCertificateFuncInvoked = true + s.mu.Unlock() + return s.WSTEPStoreCertificateFunc(ctx, name, crt) +} + +func (s *DataStore) WSTEPNewSerial(ctx context.Context) (*big.Int, error) { + s.mu.Lock() + s.WSTEPNewSerialFuncInvoked = true + s.mu.Unlock() + return s.WSTEPNewSerialFunc(ctx) +} + +func (s *DataStore) WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error { + s.mu.Lock() + s.WSTEPAssociateCertHashFuncInvoked = true + s.mu.Unlock() + return s.WSTEPAssociateCertHashFunc(ctx, deviceUUID, hash) +} diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 3176d0578d..4b83512714 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -2,7 +2,13 @@ package service import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" "errors" + "math/big" + "os" "testing" "time" @@ -12,6 +18,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/test" + "github.com/micromdm/scep/v2/cryptoutil/x509util" "github.com/stretchr/testify/require" ) @@ -177,3 +184,72 @@ func TestVerifyMDMWindowsConfigured(t *testing.T) { ds.AppConfigFuncInvoked = false require.False(t, authzCtx.Checked()) } + +func TestMicrosoftWSTEPConfig(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierFree} + + ds.WSTEPNewSerialFunc = func(context.Context) (*big.Int, error) { + return big.NewInt(1337), nil + } + ds.WSTEPStoreCertificateFunc = func(ctx context.Context, name string, crt *x509.Certificate) error { + require.Equal(t, "test-client", name) + require.Equal(t, "test-client", crt.Subject.CommonName) + require.Equal(t, "FleetDM", crt.Subject.OrganizationalUnit[0]) + return nil + } + + certPath := "testdata/server.pem" + keyPath := "testdata/server.key" + + // sanity check that the test data is valid + wantCertPEM, err := os.ReadFile(certPath) + require.NoError(t, err) + wantKeyPEM, err := os.ReadFile(keyPath) + require.NoError(t, err) + + // specify the test data in the server config + cfg := config.TestConfig() + cfg.MDM.MicrosoftWSTEPIdentityCert = certPath + cfg.MDM.MicrosoftWSTEPIdentityKey = keyPath + + // check that config.MDM.MicrosoftWSTEP() returns the expected values + _, cfgCertPEM, cfgKeyPEM, err := cfg.MDM.MicrosoftWSTEP() + require.NoError(t, err) + require.NotEmpty(t, cfgCertPEM) + require.Equal(t, wantCertPEM, cfgCertPEM) + require.NotEmpty(t, cfgKeyPEM) + require.Equal(t, wantKeyPEM, cfgKeyPEM) + + // start the test service + svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + ctx = test.UserContext(ctx, test.UserAdmin) + + // test CSR signing + clienPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + csrTemplate := x509util.CertificateRequest{ + CertificateRequest: x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "test-cient", + }, + SignatureAlgorithm: x509.SHA256WithRSA, + }, + } + csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, clienPrivateKey) + require.NoError(t, err) + csr, err := x509.ParseCertificateRequest(csrDerBytes) + require.NoError(t, err) + + // test the service method + rawDER, _, err := svc.SignMDMMicrosoftClientCSR(ctx, "test-client", csr) + require.NoError(t, err) + require.True(t, ds.WSTEPNewSerialFuncInvoked) + require.True(t, ds.WSTEPStoreCertificateFuncInvoked) + + // TODO: additional assertions on the signed certificate + parsedCert, err := x509.ParseCertificate(rawDER) + require.NoError(t, err) + require.Equal(t, "test-client", parsedCert.Subject.CommonName) + require.Equal(t, "FleetDM", parsedCert.Subject.OrganizationalUnit[0]) +} diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 9a622fa762..a38a84db92 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/x509" "encoding/base64" "encoding/json" "encoding/xml" @@ -579,3 +580,19 @@ func (svc *Service) GetAuthorizedSoapFault(ctx context.Context, eType string, or return &soapFault } + +func (svc *Service) SignMDMMicrosoftClientCSR(ctx context.Context, subject string, csr *x509.CertificateRequest) ([]byte, string, error) { + // TODO: check if this method should require explicit authorization + svc.authz.SkipAuthorization(ctx) + + cert, fpHex, err := svc.wstepCertManager.SignClientCSR(ctx, subject, csr) + if err != nil { + return nil, "signing wstep client csr", ctxerr.Wrap(ctx, err) + } + + // TODO: if desired, the signature of this method can be modified to accept a device UUID so + // that we can associate the certificate with the host here by calling + // svc.wstepCertManager.AssociateCertHash + + return cert, fpHex, nil +} diff --git a/server/service/service.go b/server/service/service.go index ad67599b14..7d73f5392a 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -14,6 +14,7 @@ import ( "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/fleet" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/fleetdm/fleet/v4/server/service/async" "github.com/fleetdm/fleet/v4/server/sso" kitlog "github.com/go-kit/kit/log" @@ -60,6 +61,8 @@ type Service struct { mdmAppleCommander *apple_mdm.MDMAppleCommander cronSchedulesService fleet.CronSchedulesService + + wstepCertManager microsoft_mdm.CertManager } func (svc *Service) LookupGeoIP(ctx context.Context, ip string) *fleet.GeoLocation { @@ -105,6 +108,7 @@ func NewService( mdmPushService nanomdm_push.Pusher, mdmPushCertTopic string, cronSchedulesService fleet.CronSchedulesService, + wstepCertManager microsoft_mdm.CertManager, ) (fleet.Service, error) { authorizer, err := authz.NewAuthorizer() if err != nil { @@ -139,6 +143,7 @@ func NewService( mdmPushCertTopic: mdmPushCertTopic, mdmAppleCommander: apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), cronSchedulesService: cronSchedulesService, + wstepCertManager: wstepCertManager, } return validationMiddleware{svc, ds, sso}, nil } diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 598d1bed0f..d0a4e87d58 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -21,6 +21,7 @@ import ( "github.com/fleetdm/fleet/v4/server/logging" "github.com/fleetdm/fleet/v4/server/mail" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service/async" @@ -133,6 +134,17 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf mdmPushCertTopic = opts[0].APNSTopic } + var wstepManager microsoft_mdm.CertManager + if fleetConfig.MDM.MicrosoftWSTEPIdentityCert != "" && fleetConfig.MDM.MicrosoftWSTEPIdentityKey != "" { + rawCert, err := os.ReadFile(fleetConfig.MDM.MicrosoftWSTEPIdentityCert) + require.NoError(t, err) + rawKey, err := os.ReadFile(fleetConfig.MDM.MicrosoftWSTEPIdentityKey) + require.NoError(t, err) + + wstepManager, err = microsoft_mdm.NewCertManager(ds, rawCert, rawKey) + require.NoError(t, err) + } + svc, err := NewService( ctx, ds, @@ -155,6 +167,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf mdmPusher, mdmPushCertTopic, cronSchedulesService, + wstepManager, ) if err != nil { panic(err) diff --git a/server/utils.go b/server/utils.go index 34c029b463..af207709ca 100644 --- a/server/utils.go +++ b/server/utils.go @@ -4,8 +4,12 @@ import ( "bytes" "context" "crypto/rand" + "crypto/rsa" + "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" + "errors" "fmt" "io/ioutil" "net/http" @@ -56,3 +60,18 @@ func PostJSONWithTimeout(ctx context.Context, url string, v interface{}) error { return nil } + +// TODO: Consider moving other crypto functions from server/mdm/apple/util to here + +// DecodePrivateKeyPEM decodes PEM-encoded private key data. +func DecodePrivateKeyPEM(encoded []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(encoded) + if block == nil { + return nil, errors.New("no PEM-encoded data found") + } + if block.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("unexpected block type %s", block.Type) + } + + return x509.ParsePKCS1PrivateKey(block.Bytes) +}