mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Add certificate management for Microsoft MDM (WSTEP) (#12543)
Issue #12261 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [ ] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - [ ] Documented any permissions changes - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] Added/updated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).
This commit is contained in:
parent
a7def8bfa6
commit
410cbc3972
@ -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")
|
||||
|
2
go.mod
2
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
|
||||
|
2
go.sum
2
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=
|
||||
|
@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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.
|
||||
//
|
||||
// ...
|
||||
}
|
@ -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>[[:ascii:]])(?P<nonascii>[^[:ascii:]]+)`)
|
||||
var nonacsiiReplace = regexp.MustCompile(`[^[:ascii:]]`)
|
||||
var (
|
||||
nonascii = regexp.MustCompile(`(?P<ascii>[[:ascii:]])(?P<nonascii>[^[:ascii:]]+)`)
|
||||
nonacsiiReplace = regexp.MustCompile(`[^[:ascii:]]`)
|
||||
)
|
||||
|
||||
func hostSearchLike(sql string, params []interface{}, match string, columns ...string) (string, []interface{}, bool) {
|
||||
var matchesEmail bool
|
||||
|
File diff suppressed because one or more lines are too long
69
server/datastore/mysql/wstep.go
Normal file
69
server/datastore/mysql/wstep.go
Normal file
@ -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
|
||||
}
|
158
server/datastore/mysql/wstep_test.go
Normal file
158
server/datastore/mysql/wstep_test.go
Normal file
@ -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") }
|
@ -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 (
|
||||
|
@ -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)
|
||||
}
|
||||
|
249
server/mdm/microsoft/wstep.go
Normal file
249
server/mdm/microsoft/wstep.go
Normal file
@ -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[:]))
|
||||
}
|
180
server/mdm/microsoft/wstep_test.go
Normal file
180
server/mdm/microsoft/wstep_test.go
Normal file
@ -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") }
|
@ -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)
|
||||
}
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user