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:
gillespi314 2023-06-29 17:31:53 -05:00 committed by GitHub
parent a7def8bfa6
commit 410cbc3972
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1085 additions and 4 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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"),
},
}

View File

@ -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

View File

@ -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
}

View File

@ -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.
//
// ...
}

View File

@ -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

View 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
}

View 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") }

View File

@ -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 (

View File

@ -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)
}

View 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[:]))
}

View 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") }

View File

@ -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)
}

View File

@ -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])
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}