mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-05 00:45:50 +08:00
@@ -1,5 +1,6 @@
|
||||
# TBD
|
||||
|
||||
* Added `mdmctl mdmcert upload` command which uploads/replaces the servers push certificate.
|
||||
* Incorporated certhelper into mdmctl.
|
||||
* Added ENV variables for sensitive flags: `MICROMDM_APNS_KEY_PASSWORD`,`MICROMDM_API_KEY`
|
||||
* Removed the `-redir-addr` flag. Redirect to HTTPS is only enabled when the 443 port is used.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -8,8 +12,13 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
|
||||
"github.com/micromdm/micromdm/config"
|
||||
"github.com/micromdm/micromdm/crypto"
|
||||
"github.com/micromdm/micromdm/crypto/mdmcertutil"
|
||||
)
|
||||
|
||||
@@ -40,6 +49,7 @@ Use the push private key and the push cert you got from identity.apple.com in yo
|
||||
Commands:
|
||||
vendor
|
||||
push
|
||||
upload
|
||||
`
|
||||
fmt.Println(usageText)
|
||||
return nil
|
||||
@@ -58,6 +68,8 @@ func (cmd *mdmcertCommand) Run(args []string) error {
|
||||
run = cmd.runVendor
|
||||
case "push":
|
||||
run = cmd.runPush
|
||||
case "upload":
|
||||
run = cmd.runUpload
|
||||
default:
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
@@ -173,6 +185,117 @@ func (cmd *mdmcertCommand) runPush(args []string) error {
|
||||
return errors.Wrap(err, "creating MDM Push certificate request.")
|
||||
}
|
||||
|
||||
func (cmd *mdmcertCommand) runUpload(args []string) error {
|
||||
flagset := flag.NewFlagSet("upload", flag.ExitOnError)
|
||||
flagset.Usage = usageFor(flagset, "mdmctl mdmcert upload [flags]")
|
||||
var (
|
||||
flKeyPass = flagset.String("password", "", "Password to encrypt/read the RSA key.")
|
||||
flKeyPath = flagset.String("private-key", filepath.Join(mdmcertdir, pushCertificatePrivateKeyFilename), "Path to the push certificate private key.")
|
||||
flCertPath = flagset.String("cert", "", "Path to the MDM Push Certificate.")
|
||||
)
|
||||
if err := flagset.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := LoadClientConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "load mdmctl client config")
|
||||
}
|
||||
logger := log.NewLogfmtLogger(os.Stderr)
|
||||
configsvc, err := config.NewClient(
|
||||
cfg.ServerURL,
|
||||
logger,
|
||||
cfg.APIToken,
|
||||
httptransport.SetClient(skipVerifyHTTPClient(cfg.SkipVerify)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create config service from mdmctl config")
|
||||
}
|
||||
|
||||
cert, key, err := loadPushCerts(*flCertPath, *flKeyPath, *flKeyPass)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "load push certificate")
|
||||
}
|
||||
|
||||
if err := configsvc.SavePushCertificate(context.Background(), cert, key); err != nil {
|
||||
return errors.Wrap(err, "upload push certificate and key to server")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPushCerts(certPath, keyPath, keyPass string) (cert, key []byte, err error) {
|
||||
isP12 := (keyPath == "" && keyPass != "")
|
||||
if isP12 {
|
||||
pkcs12Data, err := ioutil.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "read p12 path %s", certPath)
|
||||
}
|
||||
pkeyi, certificate, err := pkcs12.Decode(pkcs12Data, keyPass)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "decode pkcs12 file")
|
||||
}
|
||||
pkey, ok := pkeyi.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("private key not a valid rsa key")
|
||||
}
|
||||
|
||||
pemKey := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(pkey),
|
||||
})
|
||||
|
||||
pemCert := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certificate.Raw,
|
||||
})
|
||||
return pemCert, pemKey, nil
|
||||
}
|
||||
|
||||
keyData, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "read push certificate private key at path %s", keyPath)
|
||||
}
|
||||
|
||||
keyDataBlock, _ := pem.Decode(keyData)
|
||||
if keyDataBlock == nil {
|
||||
return nil, nil, errors.Errorf("invalid PEM data for private key %s", keyPath)
|
||||
}
|
||||
|
||||
var pemKeyData []byte
|
||||
if x509.IsEncryptedPEMBlock(keyDataBlock) {
|
||||
b, err := x509.DecryptPEMBlock(keyDataBlock, []byte(keyPass))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decrypting DES private key %s", err)
|
||||
}
|
||||
pemKeyData = b
|
||||
} else {
|
||||
pemKeyData = keyDataBlock.Bytes
|
||||
}
|
||||
|
||||
priv, err := x509.ParsePKCS1PrivateKey(pemKeyData)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "parse push certiificate private key %s", keyPath)
|
||||
}
|
||||
|
||||
pemKey := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
})
|
||||
|
||||
certificate, err := crypto.ReadPEMCertificateFile(certPath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "read push certificate from pem file %s", certPath)
|
||||
}
|
||||
|
||||
pemCert := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certificate.Raw,
|
||||
})
|
||||
|
||||
return pemCert, pemKey, nil
|
||||
}
|
||||
|
||||
func checkCSRFlags(cname, country, email string, password []byte) error {
|
||||
if cname == "" {
|
||||
return errors.New("cn flag not specified")
|
||||
|
||||
21
cmd/mdmctl/mdmcert_test.go
Normal file
21
cmd/mdmctl/mdmcert_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLoadPushCerts(t *testing.T) {
|
||||
keypath := "testdata/ProviderPrivateKey.key"
|
||||
certpath := "testdata/pushcert.pem"
|
||||
p12path := "testdata/pushcert.p12"
|
||||
keysecret := "secret"
|
||||
|
||||
_, _, err := loadPushCerts(certpath, keypath, keysecret)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load PEM push certs with err %s", err)
|
||||
}
|
||||
|
||||
// try to load from p12
|
||||
_, _, err = loadPushCerts(p12path, "", keysecret)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load p12 push certs with err %s", err)
|
||||
}
|
||||
}
|
||||
30
cmd/mdmctl/testdata/ProviderPrivateKey.key
vendored
Normal file
30
cmd/mdmctl/testdata/ProviderPrivateKey.key
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,22c0f4c89c7b1fcd
|
||||
|
||||
KVnJeaMzHPrL5pHmkB6yAR9N8cvh2FKBfmJuRP61GiSR+PAI1UxsZ+627cnNZc/8
|
||||
Y2haoxAtW4k5Yl6gIQH8exf4el5yD+Ds4mEUacw7m3RV+WNL32I1+Z+jr55jetzP
|
||||
nb3t9LQmsP07uigSe/oh0vumVhjxxSE31/iB2BISnEfJDsDYHFFc1XWAxNABG+JP
|
||||
ttCnIHPXaFLb7jDdZnnsk2rBNsRkOPwJ27hna9b0RyHVmqMb3XZtQs2RtARTVqap
|
||||
hCFSphMrS20wLqcHyJZ7v0p9MiCp71g58xs4Bl9Vh1FI86AA+AOaosA0Kl8lPa08
|
||||
ChuJf7ng3mhJ+nKRkPUK4eDtnCsNm3Xd0lQQHqRWcoUJ80Gdhla0CodJnt/BT35y
|
||||
SP+j2SUJRuAvNpSeGyAcLv3wDwhMgkVTt1errZOL0tFa/ep5YwfFPMBDVNXx+lOd
|
||||
AHDey8gnoGJFpwoQFdXp6TVZEJjP6kgN8njmAZmbwrFcqMdjg+eMZfAYXUAdy4aT
|
||||
aMbMGwjyjUgP80iXX5OAgGchpQ+HO5otpbecAMKs6SAqAgTMbKMG2rLgZe+mlkbd
|
||||
6xnwyrhkQpQHVN97gqWic2cIggyR3UL58mned/5eS3EcNTTxHJO8D7EwmIjkGVxY
|
||||
fT+tdpJ6iKK6CfjIEZYFgtLafmKMxxDHaplLGnIK8VHiopwUIKxVcji9Pjp+0a1O
|
||||
q9NSVZXAbt45mL2jV94hhEpt83YyBFdRYBFk5LT7AQg2ipVl51g7fO2SoTiHakzL
|
||||
MrKfo+bSxDHOWefu9Z380Vmh1Mm0xDU44C8g9Jc4N7euCQtGfX5bnWuueuGwaaXL
|
||||
lYrqKVuqKdc2y5d8N7KiHEHY0zsfyjFu4+zh/v2zYIBlbuM9ZGiuSlprHvy//wev
|
||||
kPZDEtHNTeFXBn7qDeTGNlYK+aGQyTFOzasXcUbf3RywjpHOM/uL7rUpA11W+Ip/
|
||||
/b1h+mcADy90DMBv1xVX3vZVldwAdcqbQR0QTXxgCS1dzVdedG2N7noJVKUfbA4x
|
||||
tI3NEAaBt7J7eLQm7TawRv6j5G+VB/ZS5B8WVCVbBjwwqbeccJcuzJKLBX0A7c+X
|
||||
TouVuE7xpq9OK0zsaIwg7WMgKOY9ZMuMNzjKnXcSYcq5o9LHHsB1CHyOGdHkmj/J
|
||||
AmME2xnyB+GCLWyN8B17hyC4MwkoNDWLnlUzL1Z3lf5jsorx+zORKho+rgg02Blw
|
||||
hY02jVEcHoO8fup6GXkWtWpq2pljHD6DBg4thqwd2X3tpKBEDCcPYjMNRBmb+EZo
|
||||
ZZrHMZX999BsgQip+9s6gpg4OxLbkn/wB94D+XoQYCF/rKb3ztlMHWygyjasKbWe
|
||||
L5DnI4tDaO0CAYP79ixJWd8DnHy7lkDFCWA3ZdDV5brM81biJVr24b0SoV22/nT/
|
||||
wvTP0wgdRBmBmqFYZojlIWM+Gu09tVqXy8ui5oOGJThXgCyPWPrs9PSIfLyOjfDm
|
||||
vUy/3yhOfCZmLEFG955P3A92ygojs/EvIrE2oWKVxf0X+T0mIVnBxeWpQ0OOVC1q
|
||||
51gz/pyLNjbqxP2FUOkF5hLvRupAHhFpzbP4yZcDHUisNyUgjb4kHQ==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
BIN
cmd/mdmctl/testdata/pushcert.p12
vendored
Normal file
BIN
cmd/mdmctl/testdata/pushcert.p12
vendored
Normal file
Binary file not shown.
18
cmd/mdmctl/testdata/pushcert.pem
vendored
Normal file
18
cmd/mdmctl/testdata/pushcert.pem
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC9zCCAd+gAwIBAgIJAPQybPTtooGmMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
|
||||
BAMMB21kbXB1c2gwHhcNMTcwMzIxMjMyNzEwWhcNNDQwODA2MjMyNzEwWjASMRAw
|
||||
DgYDVQQDDAdtZG1wdXNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
43VCT3Xw62hJ1t3HhLNsyJ/wTTaLJfyNu9zwNeNVY+yOxSaj6V5UVEpWiNIfyB+j
|
||||
J7E+cUdWmEQewwXPaoNsCR4bKBElB1f45UApcrmkoqzruFI4O+yfmLJkN95fO5Sd
|
||||
009VZPudFf96HdYjNpgg8ofIxAxo9dP+ReU+4QgxevIM2Hg+X+4zabDiLspfGX6Y
|
||||
A5Iz/R6rFCx8rc+GEWFhkB4JE0cIV/TifP5xlOr0UFTCYQ1KUEM+30s928PFRXM4
|
||||
tsm/TcM5g0Mi2bNDiVHE0hvzxmJbiL2XjFE3VBK6Dphw9gF1GLe8D2TnZMeIAj3K
|
||||
JW0sjnkmvBxRvTqTHA4LvwIDAQABo1AwTjAdBgNVHQ4EFgQUJmri9d+KEYhPyDu3
|
||||
dHsVm7eFWngwHwYDVR0jBBgwFoAUJmri9d+KEYhPyDu3dHsVm7eFWngwDAYDVR0T
|
||||
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAmRu4hFq4+WN7Vr9FucU+yHYcj0YV
|
||||
kKfJMUSxDK/bab1DR02gxcYVR+bY/xtlcc/dA0zzzXUj97/BKLpkQCxu1+MtqMPx
|
||||
bbVJnrSkGDXBXMmwn9C29WDdMjLQwS4WLrkDTEVc8W9kUqZsEBQalneKlV8u5kXi
|
||||
A9tolc2Kaq2uJxKcsC2Yx1MLqpEm6bqKRlQSPM0G3uWupJBnvIQfXt0n3ErphQhf
|
||||
m6BuVIs2iVvEo2W+VZ+LwfeN992Yd0DeMqvMAh6Rg/N/SGaxmMeD/Co+LHKCAzW4
|
||||
JVjDjF01dBDmWTBs+NC9euIoCBej+PjMwXUo229uA19DucZXMIed9Xyoqg==
|
||||
-----END CERTIFICATE-----
|
||||
48
config/client.go
Normal file
48
config/client.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
)
|
||||
|
||||
func NewClient(instance string, logger log.Logger, token string, opts ...httptransport.ClientOption) (Service, error) {
|
||||
u, err := url.Parse(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var saveEndpoint endpoint.Endpoint
|
||||
{
|
||||
|
||||
saveEndpoint = httptransport.NewClient(
|
||||
"PUT",
|
||||
copyURL(u, "/v1/config/certificate"),
|
||||
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
|
||||
DecodeSavePushCertificateResponse,
|
||||
opts...,
|
||||
).Endpoint()
|
||||
|
||||
}
|
||||
|
||||
return Endpoints{
|
||||
SavePushCertificateEndpoint: saveEndpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRequestWithToken(token string, next httptransport.EncodeRequestFunc) httptransport.EncodeRequestFunc {
|
||||
return func(ctx context.Context, r *http.Request, request interface{}) error {
|
||||
r.SetBasicAuth("micromdm", token)
|
||||
return next(ctx, r, request)
|
||||
}
|
||||
}
|
||||
|
||||
func copyURL(base *url.URL, path string) *url.URL {
|
||||
next := *base
|
||||
next.Path = path
|
||||
return &next
|
||||
}
|
||||
32
config/config.go
Normal file
32
config/config.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/micromdm/micromdm/config/internal/configproto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ServerConfig holds the configuration of the MDM Server.
|
||||
type ServerConfig struct {
|
||||
PushCertificate []byte
|
||||
PrivateKey []byte
|
||||
}
|
||||
|
||||
func MarshalServerConfig(conf *ServerConfig) ([]byte, error) {
|
||||
pb := configproto.ServerConfig{
|
||||
PushCertificate: conf.PushCertificate,
|
||||
PushCertificateKey: conf.PrivateKey,
|
||||
}
|
||||
data, err := proto.Marshal(&pb)
|
||||
return data, errors.Wrap(err, "marshal server config to proto")
|
||||
}
|
||||
|
||||
func UnmarshalServerConfig(data []byte, conf *ServerConfig) error {
|
||||
var pb configproto.ServerConfig
|
||||
if err := proto.Unmarshal(data, &pb); err != nil {
|
||||
return errors.Wrap(err, "unmarshal server config from proto")
|
||||
}
|
||||
conf.PushCertificate = pb.GetPushCertificate()
|
||||
conf.PrivateKey = pb.GetPushCertificateKey()
|
||||
return nil
|
||||
}
|
||||
150
config/db.go
Normal file
150
config/db.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Package config provides an internal store for the configuration of the MDM server.
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/micromdm/micromdm/pubsub"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigBucket = "mdm.ServerConfig"
|
||||
ConfigTopic = "mdm.ServerConfigUpdated"
|
||||
)
|
||||
|
||||
// DB stores server configuration in BoltDB
|
||||
type DB struct {
|
||||
*bolt.DB
|
||||
Publisher pubsub.Publisher
|
||||
}
|
||||
|
||||
func NewDB(db *bolt.DB, pub pubsub.Publisher) (*DB, error) {
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(ConfigBucket))
|
||||
return err
|
||||
})
|
||||
store := &DB{DB: db, Publisher: pub}
|
||||
return store, err
|
||||
}
|
||||
|
||||
func (db *DB) SavePushCertificate(cert, key []byte) error {
|
||||
tx, err := db.DB.Begin(true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "begin transaction to store push certificate in bolt")
|
||||
}
|
||||
bkt := tx.Bucket([]byte(ConfigBucket))
|
||||
if bkt == nil {
|
||||
return fmt.Errorf("config: bucket %q not found", ConfigBucket)
|
||||
}
|
||||
pb, err := MarshalServerConfig(&ServerConfig{
|
||||
PushCertificate: cert,
|
||||
PrivateKey: key,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "save push cert in bolt bucket")
|
||||
}
|
||||
|
||||
if err := bkt.Put([]byte("config"), pb); err != nil {
|
||||
return errors.Wrap(err, "save ServerConfig in bucket")
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Publisher.Publish(context.TODO(), ConfigTopic, []byte("updated")); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) serverConfig() (*ServerConfig, error) {
|
||||
var conf ServerConfig
|
||||
err := db.View(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket([]byte(ConfigBucket))
|
||||
data := bkt.Get([]byte("config"))
|
||||
if data == nil {
|
||||
return ¬Found{"ServerConfig", "no config found in boltdb"}
|
||||
}
|
||||
return UnmarshalServerConfig(data, &conf)
|
||||
})
|
||||
return &conf, errors.Wrap(err, "get server config from bolt")
|
||||
}
|
||||
|
||||
func (db *DB) PushCertificate() (*tls.Certificate, error) {
|
||||
conf, err := db.serverConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get server config for push cert")
|
||||
}
|
||||
|
||||
// load private key
|
||||
pkeyBlock, _ := pem.Decode(conf.PrivateKey)
|
||||
if pkeyBlock == nil {
|
||||
return nil, errors.New("decode private key for push cert")
|
||||
}
|
||||
|
||||
priv, err := x509.ParsePKCS1PrivateKey(pkeyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse push certificate key from server config")
|
||||
}
|
||||
|
||||
// load certificate
|
||||
certBlock, _ := pem.Decode(conf.PushCertificate)
|
||||
if certBlock == nil {
|
||||
return nil, errors.New("decode push certificate PEM")
|
||||
}
|
||||
|
||||
pushCert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse push certificate from server config")
|
||||
}
|
||||
|
||||
cert := tls.Certificate{
|
||||
Certificate: [][]byte{pushCert.Raw},
|
||||
PrivateKey: priv,
|
||||
Leaf: pushCert,
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
func (db *DB) PushTopic() (string, error) {
|
||||
cert, err := db.PushCertificate()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "get push certificate for topic")
|
||||
}
|
||||
topic, err := topicFromCert(cert.Leaf)
|
||||
return topic, errors.Wrap(err, "get topic from push certificate")
|
||||
}
|
||||
|
||||
func topicFromCert(cert *x509.Certificate) (string, error) {
|
||||
var oidASN1UserID = asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}
|
||||
for _, v := range cert.Subject.Names {
|
||||
if v.Type.Equal(oidASN1UserID) {
|
||||
return v.Value.(string), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("could not find Push Topic (UserID OID) in certificate")
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
if _, ok := err.(*notFound); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type notFound struct {
|
||||
ResourceType string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *notFound) Error() string {
|
||||
return fmt.Sprintf("not found: %s %s", e.ResourceType, e.Message)
|
||||
}
|
||||
44
config/endpoints.go
Normal file
44
config/endpoints.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
SavePushCertificateEndpoint endpoint.Endpoint
|
||||
}
|
||||
|
||||
type saveRequest struct {
|
||||
Cert []byte `json:"cert"`
|
||||
Key []byte `json:"key"`
|
||||
}
|
||||
|
||||
type saveResponse struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (r saveResponse) error() error { return r.Err }
|
||||
|
||||
func (e Endpoints) SavePushCertificate(ctx context.Context, cert, key []byte) error {
|
||||
request := saveRequest{
|
||||
Cert: cert,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
response, err := e.SavePushCertificateEndpoint(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.(saveResponse).Err
|
||||
}
|
||||
|
||||
func MakeSavePushCertificateEndpoint(svc Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
|
||||
req := request.(saveRequest)
|
||||
err = svc.SavePushCertificate(ctx, req.Cert, req.Key)
|
||||
return saveResponse{Err: err}, nil
|
||||
}
|
||||
}
|
||||
3
config/internal/configproto/config.go
Normal file
3
config/internal/configproto/config.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package configproto
|
||||
|
||||
//go:generate protoc --go_out=. config.proto
|
||||
70
config/internal/configproto/config.pb.go
Normal file
70
config/internal/configproto/config.pb.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: config.proto
|
||||
|
||||
/*
|
||||
Package configproto is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
config.proto
|
||||
|
||||
It has these top-level messages:
|
||||
ServerConfig
|
||||
*/
|
||||
package configproto
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type ServerConfig struct {
|
||||
PushCertificate []byte `protobuf:"bytes,1,opt,name=push_certificate,json=pushCertificate,proto3" json:"push_certificate,omitempty"`
|
||||
PushCertificateKey []byte `protobuf:"bytes,2,opt,name=push_certificate_key,json=pushCertificateKey,proto3" json:"push_certificate_key,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ServerConfig) Reset() { *m = ServerConfig{} }
|
||||
func (m *ServerConfig) String() string { return proto.CompactTextString(m) }
|
||||
func (*ServerConfig) ProtoMessage() {}
|
||||
func (*ServerConfig) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||
|
||||
func (m *ServerConfig) GetPushCertificate() []byte {
|
||||
if m != nil {
|
||||
return m.PushCertificate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerConfig) GetPushCertificateKey() []byte {
|
||||
if m != nil {
|
||||
return m.PushCertificateKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*ServerConfig)(nil), "configproto.ServerConfig")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("config.proto", fileDescriptor0) }
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 115 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x49, 0xce, 0xcf, 0x4b,
|
||||
0xcb, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x86, 0xf0, 0xc0, 0x1c, 0xa5, 0x6c,
|
||||
0x2e, 0x9e, 0xe0, 0xd4, 0xa2, 0xb2, 0xd4, 0x22, 0x67, 0xb0, 0xa0, 0x90, 0x26, 0x97, 0x40, 0x41,
|
||||
0x69, 0x71, 0x46, 0x7c, 0x72, 0x6a, 0x51, 0x49, 0x66, 0x5a, 0x66, 0x72, 0x62, 0x49, 0xaa, 0x04,
|
||||
0xa3, 0x02, 0xa3, 0x06, 0x4f, 0x10, 0x3f, 0x48, 0xdc, 0x19, 0x21, 0x2c, 0x64, 0xc0, 0x25, 0x82,
|
||||
0xae, 0x34, 0x3e, 0x3b, 0xb5, 0x52, 0x82, 0x09, 0xac, 0x5c, 0x08, 0x4d, 0xb9, 0x77, 0x6a, 0x65,
|
||||
0x12, 0x1b, 0xd8, 0x4e, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xea, 0xa7, 0xb0, 0xbc, 0x90,
|
||||
0x00, 0x00, 0x00,
|
||||
}
|
||||
9
config/internal/configproto/config.proto
Normal file
9
config/internal/configproto/config.proto
Normal file
@@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package configproto;
|
||||
|
||||
message ServerConfig {
|
||||
bytes push_certificate = 1;
|
||||
bytes push_certificate_key = 2;
|
||||
}
|
||||
|
||||
24
config/service.go
Normal file
24
config/service.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
SavePushCertificate(ctx context.Context, cert, key []byte) error
|
||||
}
|
||||
|
||||
type ConfigService struct {
|
||||
store *DB
|
||||
}
|
||||
|
||||
func NewService(db *DB) *ConfigService {
|
||||
return &ConfigService{store: db}
|
||||
}
|
||||
|
||||
func (svc *ConfigService) SavePushCertificate(ctx context.Context, cert, key []byte) error {
|
||||
err := svc.store.SavePushCertificate(cert, key)
|
||||
return errors.Wrap(err, "save push certificate")
|
||||
}
|
||||
90
config/transport_http.go
Normal file
90
config/transport_http.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type HTTPHandlers struct {
|
||||
SavePushCertificateHandler http.Handler
|
||||
}
|
||||
|
||||
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
|
||||
h := HTTPHandlers{
|
||||
SavePushCertificateHandler: httptransport.NewServer(
|
||||
endpoints.SavePushCertificateEndpoint,
|
||||
decodeSavePushCertificateRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func decodeSavePushCertificateRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
var req saveRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type errorWrapper struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type errorer interface {
|
||||
error() error
|
||||
}
|
||||
|
||||
func errorDecoder(r *http.Response) error {
|
||||
var w errorWrapper
|
||||
if err := json.NewDecoder(r.Body).Decode(&w); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New(w.Error)
|
||||
}
|
||||
|
||||
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
if e, ok := response.(errorer); ok && e.error() != nil {
|
||||
EncodeError(ctx, e.error(), w)
|
||||
return nil
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(response)
|
||||
}
|
||||
|
||||
func EncodeError(ctx context.Context, err error, w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
enc.Encode(errorWrapper{Error: err.Error()})
|
||||
}
|
||||
|
||||
// EncodeHTTPGenericRequest is a transport/http.EncodeRequestFunc that
|
||||
// JSON-encodes any request to the request body. Primarily useful in a client.
|
||||
func EncodeHTTPGenericRequest(_ context.Context, r *http.Request, request interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(request); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Body = ioutil.NopCloser(&buf)
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeSavePushCertificateResponse(_ context.Context, r *http.Response) (interface{}, error) {
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return nil, errorDecoder(r)
|
||||
}
|
||||
var resp saveResponse
|
||||
err := json.NewDecoder(r.Body).Decode(&resp)
|
||||
return resp, err
|
||||
}
|
||||
@@ -4,12 +4,17 @@ import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/groob/plist"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/micromdm/micromdm/config"
|
||||
"github.com/micromdm/micromdm/profile"
|
||||
"github.com/micromdm/micromdm/pubsub"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,7 +29,7 @@ type Service interface {
|
||||
OTAPhase3(ctx context.Context) (profile.Mobileconfig, error)
|
||||
}
|
||||
|
||||
func NewService(pushTopic, caCertPath, scepURL, scepChallenge, url, tlsCertPath, scepSubject string, profileDB *profile.DB) (Service, error) {
|
||||
func NewService(topic TopicProvider, sub pubsub.Subscriber, caCertPath, scepURL, scepChallenge, url, tlsCertPath, scepSubject string, profileDB *profile.DB) (Service, error) {
|
||||
var caCert, tlsCert []byte
|
||||
var err error
|
||||
|
||||
@@ -59,16 +64,49 @@ func NewService(pushTopic, caCertPath, scepURL, scepChallenge, url, tlsCertPath,
|
||||
subject = append(subject, [][]string{[]string{subjectKeyValue[0], subjectKeyValue[1]}})
|
||||
}
|
||||
|
||||
return &service{
|
||||
svc := &service{
|
||||
URL: url,
|
||||
SCEPURL: scepURL,
|
||||
SCEPSubject: subject,
|
||||
SCEPChallenge: scepChallenge,
|
||||
Topic: pushTopic,
|
||||
CACert: caCert,
|
||||
TLSCert: tlsCert,
|
||||
ProfileDB: profileDB,
|
||||
}, nil
|
||||
topicProvier: topic,
|
||||
}
|
||||
|
||||
if err := updateTopic(svc, sub); err != nil {
|
||||
return nil, errors.Wrap(err, "enroll: start topic update goroutine")
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func updateTopic(svc *service, sub pubsub.Subscriber) error {
|
||||
configEvents, err := sub.Subscribe(context.TODO(), "enroll-server-configs", config.ConfigTopic)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "update enrollment service")
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-configEvents:
|
||||
topic, err := svc.topicProvier.PushTopic()
|
||||
if err != nil {
|
||||
log.Println("enroll: get push topic %s", topic)
|
||||
}
|
||||
svc.mu.Lock()
|
||||
svc.Topic = topic
|
||||
svc.mu.Unlock()
|
||||
|
||||
// terminate the loop here because the topic should never change
|
||||
goto exit
|
||||
}
|
||||
}
|
||||
exit:
|
||||
return
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
type service struct {
|
||||
@@ -76,10 +114,18 @@ type service struct {
|
||||
SCEPURL string
|
||||
SCEPChallenge string
|
||||
SCEPSubject [][][]string
|
||||
Topic string // APNS Topic for MDM notifications
|
||||
CACert []byte
|
||||
TLSCert []byte
|
||||
ProfileDB *profile.DB
|
||||
|
||||
topicProvier TopicProvider
|
||||
|
||||
mu sync.RWMutex
|
||||
Topic string // APNS Topic for MDM notifications
|
||||
}
|
||||
|
||||
type TopicProvider interface {
|
||||
PushTopic() (string, error)
|
||||
}
|
||||
|
||||
func profileOrPayloadFromFunc(f interface{}) (interface{}, error) {
|
||||
@@ -102,7 +148,7 @@ func profileOrPayloadToMobileconfig(in interface{}) (profile.Mobileconfig, error
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func (svc service) findOrMakeMobileconfig(id string, f interface{}) (profile.Mobileconfig, error) {
|
||||
func (svc *service) findOrMakeMobileconfig(id string, f interface{}) (profile.Mobileconfig, error) {
|
||||
p, err := svc.ProfileDB.ProfileById(id)
|
||||
if err != nil {
|
||||
if profile.IsNotFound(err) {
|
||||
@@ -117,11 +163,11 @@ func (svc service) findOrMakeMobileconfig(id string, f interface{}) (profile.Mob
|
||||
return p.Mobileconfig, nil
|
||||
}
|
||||
|
||||
func (svc service) Enroll(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
func (svc *service) Enroll(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
return svc.findOrMakeMobileconfig(EnrollmentProfileId, svc.MakeEnrollmentProfile)
|
||||
}
|
||||
|
||||
func (svc service) MakeEnrollmentProfile() (Profile, error) {
|
||||
func (svc *service) MakeEnrollmentProfile() (Profile, error) {
|
||||
profile := NewProfile()
|
||||
profile.PayloadIdentifier = EnrollmentProfileId
|
||||
profile.PayloadOrganization = "MicroMDM"
|
||||
@@ -135,13 +181,17 @@ func (svc service) MakeEnrollmentProfile() (Profile, error) {
|
||||
mdmPayload.PayloadIdentifier = EnrollmentProfileId + ".mdm"
|
||||
mdmPayload.PayloadScope = "System"
|
||||
|
||||
svc.mu.Lock()
|
||||
topic := svc.Topic
|
||||
svc.mu.Unlock()
|
||||
|
||||
mdmPayloadContent := MDMPayloadContent{
|
||||
Payload: *mdmPayload,
|
||||
AccessRights: 8191,
|
||||
CheckInURL: svc.URL + "/mdm/checkin",
|
||||
CheckOutWhenRemoved: true,
|
||||
ServerURL: svc.URL + "/mdm/connect",
|
||||
Topic: svc.Topic,
|
||||
Topic: topic,
|
||||
SignMessage: true,
|
||||
ServerCapabilities: []string{"com.apple.mdm.per-user-connections"},
|
||||
}
|
||||
@@ -203,11 +253,11 @@ func (svc service) MakeEnrollmentProfile() (Profile, error) {
|
||||
}
|
||||
|
||||
// OTAEnroll returns an Over-the-Air "Profile Service" Payload for enrollment.
|
||||
func (svc service) OTAEnroll(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
func (svc *service) OTAEnroll(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
return svc.findOrMakeMobileconfig(OTAProfileId, svc.MakeOTAEnrollPayload)
|
||||
}
|
||||
|
||||
func (svc service) MakeOTAEnrollPayload() (Payload, error) {
|
||||
func (svc *service) MakeOTAEnrollPayload() (Payload, error) {
|
||||
payload := NewPayload("Profile Service")
|
||||
payload.PayloadIdentifier = OTAProfileId
|
||||
payload.PayloadDisplayName = "MicroMDM Profile Service"
|
||||
@@ -224,11 +274,11 @@ func (svc service) MakeOTAEnrollPayload() (Payload, error) {
|
||||
}
|
||||
|
||||
// OTAPhase2 returns a SCEP Profile for use in phase 2 of Over-the-Air enrollment.
|
||||
func (svc service) OTAPhase2(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
func (svc *service) OTAPhase2(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
return svc.findOrMakeMobileconfig(OTAProfileId+".phase2", svc.MakeOTAPhase2Profile)
|
||||
}
|
||||
|
||||
func (svc service) MakeOTAPhase2Profile() (Profile, error) {
|
||||
func (svc *service) MakeOTAPhase2Profile() (Profile, error) {
|
||||
profile := NewProfile()
|
||||
profile.PayloadIdentifier = OTAProfileId + ".phase2"
|
||||
profile.PayloadOrganization = "MicroMDM"
|
||||
@@ -267,6 +317,6 @@ func (svc service) MakeOTAPhase2Profile() (Profile, error) {
|
||||
// enrollment process. In our case this would probably be a device-specifc
|
||||
// MDM enrollment payload.
|
||||
// TODO: Not implemented.
|
||||
func (svc service) OTAPhase3(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
func (svc *service) OTAPhase3(ctx context.Context) (profile.Mobileconfig, error) {
|
||||
return profile.Mobileconfig{}, nil
|
||||
}
|
||||
|
||||
@@ -1,39 +1,77 @@
|
||||
package push
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/RobotsAndPencils/buford/payload"
|
||||
"github.com/RobotsAndPencils/buford/push"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/micromdm/micromdm/config"
|
||||
"github.com/micromdm/micromdm/pubsub"
|
||||
"github.com/micromdm/micromdm/queue"
|
||||
)
|
||||
|
||||
type Push struct {
|
||||
db *DB
|
||||
db *DB
|
||||
start chan struct{}
|
||||
provider PushCertificateProvider
|
||||
|
||||
mu sync.RWMutex
|
||||
pushsvc *push.Service
|
||||
}
|
||||
|
||||
func New(db *DB, push *push.Service, sub pubsub.Subscriber) (*Push, error) {
|
||||
pushSvc := Push{db, push}
|
||||
if err := pushSvc.startQueuedSubscriber(push, sub); err != nil {
|
||||
type PushCertificateProvider interface {
|
||||
PushCertificate() (*tls.Certificate, error)
|
||||
}
|
||||
|
||||
type Option func(*Push)
|
||||
|
||||
func WithPushService(svc *push.Service) Option {
|
||||
return func(p *Push) {
|
||||
p.pushsvc = svc
|
||||
}
|
||||
}
|
||||
|
||||
func New(db *DB, provider PushCertificateProvider, sub pubsub.Subscriber, opts ...Option) (*Push, error) {
|
||||
pushSvc := Push{
|
||||
db: db,
|
||||
provider: provider,
|
||||
start: make(chan struct{}),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&pushSvc)
|
||||
}
|
||||
// if there is no push service, the push certificate hasn't been provided.
|
||||
// start a goroutine that delays the run of this service.
|
||||
if err := updateClient(&pushSvc, sub); err != nil {
|
||||
return nil, errors.Wrap(err, "wait for push service config")
|
||||
}
|
||||
|
||||
if err := pushSvc.startQueuedSubscriber(sub); err != nil {
|
||||
return &pushSvc, err
|
||||
}
|
||||
return &pushSvc, nil
|
||||
}
|
||||
|
||||
func (svc *Push) startQueuedSubscriber(push *push.Service, sub pubsub.Subscriber) error {
|
||||
func (svc *Push) startQueuedSubscriber(sub pubsub.Subscriber) error {
|
||||
commandQueuedEvents, err := sub.Subscribe(context.TODO(), "push-info", queue.CommandQueuedTopic)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err,
|
||||
"subscribing push to %s topic", queue.CommandQueuedTopic)
|
||||
}
|
||||
go func() {
|
||||
if svc.pushsvc == nil {
|
||||
log.Println("push: waiting for push certificate before enabling APNS service provider")
|
||||
<-svc.start
|
||||
log.Println("push: service started")
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case event := <-commandQueuedEvents:
|
||||
@@ -54,6 +92,45 @@ func (svc *Push) startQueuedSubscriber(push *push.Service, sub pubsub.Subscriber
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateClient(svc *Push, sub pubsub.Subscriber) error {
|
||||
configEvents, err := sub.Subscribe(context.TODO(), "push-server-configs", config.ConfigTopic)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "update push service client")
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-configEvents:
|
||||
pushsvc, err := NewPushService(svc.provider)
|
||||
if err != nil {
|
||||
log.Println("push: could not get push certificate %s", err)
|
||||
continue
|
||||
}
|
||||
svc.mu.Lock()
|
||||
svc.pushsvc = pushsvc
|
||||
svc.mu.Unlock()
|
||||
go func() { svc.start <- struct{}{} }() // unblock queue
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPushService(provider PushCertificateProvider) (*push.Service, error) {
|
||||
cert, err := provider.PushCertificate()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get push certificate from store")
|
||||
}
|
||||
|
||||
client, err := push.NewClient(*cert)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create push service client")
|
||||
}
|
||||
|
||||
svc := push.NewService(client, push.Production)
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (svc *Push) Push(ctx context.Context, deviceUDID string) (string, error) {
|
||||
info, err := svc.db.PushInfo(deviceUDID)
|
||||
if err != nil {
|
||||
|
||||
126
serve.go
126
serve.go
@@ -3,9 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
@@ -42,6 +40,7 @@ import (
|
||||
"github.com/micromdm/micromdm/blueprint"
|
||||
"github.com/micromdm/micromdm/checkin"
|
||||
"github.com/micromdm/micromdm/command"
|
||||
configsvc "github.com/micromdm/micromdm/config"
|
||||
"github.com/micromdm/micromdm/connect"
|
||||
"github.com/micromdm/micromdm/core/apply"
|
||||
"github.com/micromdm/micromdm/core/list"
|
||||
@@ -137,6 +136,7 @@ func serve(args []string) error {
|
||||
}
|
||||
sm.setupPubSub()
|
||||
sm.setupBolt()
|
||||
sm.setupConfigStore()
|
||||
sm.loadPushCerts()
|
||||
sm.setupSCEP(logger)
|
||||
sm.setupCheckinService()
|
||||
@@ -174,34 +174,61 @@ func serve(args []string) error {
|
||||
|
||||
ctx := context.Background()
|
||||
httpLogger := log.With(logger, "transport", "http")
|
||||
var checkinEndpoint endpoint.Endpoint
|
||||
|
||||
var configHandlers configsvc.HTTPHandlers
|
||||
{
|
||||
checkinEndpoint = checkin.MakeCheckinEndpoint(sm.checkinService)
|
||||
pushCertEndpoint := configsvc.MakeSavePushCertificateEndpoint(sm.configService)
|
||||
configEndpoints := configsvc.Endpoints{
|
||||
SavePushCertificateEndpoint: pushCertEndpoint,
|
||||
}
|
||||
configOpts := []httptransport.ServerOption{
|
||||
httptransport.ServerErrorLogger(httpLogger),
|
||||
httptransport.ServerErrorEncoder(checkin.EncodeError),
|
||||
}
|
||||
configHandlers = configsvc.MakeHTTPHandlers(ctx, configEndpoints, configOpts...)
|
||||
}
|
||||
|
||||
checkinEndpoints := checkin.Endpoints{
|
||||
CheckinEndpoint: checkinEndpoint,
|
||||
var checkinHandlers checkin.HTTPHandlers
|
||||
{
|
||||
e := checkin.Endpoints{
|
||||
CheckinEndpoint: checkin.MakeCheckinEndpoint(sm.checkinService),
|
||||
}
|
||||
opts := []httptransport.ServerOption{
|
||||
httptransport.ServerErrorLogger(httpLogger),
|
||||
httptransport.ServerErrorEncoder(checkin.EncodeError),
|
||||
}
|
||||
checkinHandlers = checkin.MakeHTTPHandlers(ctx, e, opts...)
|
||||
}
|
||||
|
||||
checkinOpts := []httptransport.ServerOption{
|
||||
httptransport.ServerErrorLogger(httpLogger),
|
||||
httptransport.ServerErrorEncoder(checkin.EncodeError),
|
||||
}
|
||||
checkinHandlers := checkin.MakeHTTPHandlers(ctx, checkinEndpoints, checkinOpts...)
|
||||
|
||||
pushEndpoints := nanopush.Endpoints{
|
||||
PushEndpoint: nanopush.MakePushEndpoint(sm.pushService),
|
||||
var pushHandlers nanopush.HTTPHandlers
|
||||
{
|
||||
e := nanopush.Endpoints{
|
||||
PushEndpoint: nanopush.MakePushEndpoint(sm.pushService),
|
||||
}
|
||||
opts := []httptransport.ServerOption{
|
||||
httptransport.ServerErrorLogger(httpLogger),
|
||||
httptransport.ServerErrorEncoder(checkin.EncodeError),
|
||||
}
|
||||
pushHandlers = nanopush.MakeHTTPHandlers(ctx, e, opts...)
|
||||
}
|
||||
|
||||
commandEndpoints := command.Endpoints{
|
||||
NewCommandEndpoint: command.MakeNewCommandEndpoint(sm.commandService),
|
||||
var commandHandlers command.HTTPHandlers
|
||||
{
|
||||
e := command.Endpoints{
|
||||
NewCommandEndpoint: command.MakeNewCommandEndpoint(sm.commandService),
|
||||
}
|
||||
|
||||
opts := []httptransport.ServerOption{
|
||||
httptransport.ServerErrorLogger(httpLogger),
|
||||
httptransport.ServerErrorEncoder(connect.EncodeError),
|
||||
}
|
||||
commandHandlers = command.MakeHTTPHandlers(ctx, e, opts...)
|
||||
}
|
||||
|
||||
connectOpts := []httptransport.ServerOption{
|
||||
httptransport.ServerErrorLogger(httpLogger),
|
||||
httptransport.ServerErrorEncoder(connect.EncodeError),
|
||||
}
|
||||
commandHandlers := command.MakeHTTPHandlers(ctx, commandEndpoints, connectOpts...)
|
||||
|
||||
var connectEndpoint endpoint.Endpoint
|
||||
{
|
||||
@@ -301,7 +328,6 @@ func serve(args []string) error {
|
||||
|
||||
connectHandlers := connect.MakeHTTPHandlers(ctx, connectEndpoints, connectOpts...)
|
||||
|
||||
pushHandlers := nanopush.MakeHTTPHandlers(ctx, pushEndpoints, checkinOpts...)
|
||||
scepHandler := scep.ServiceHandler(ctx, sm.scepService, httpLogger)
|
||||
enrollHandlers := enroll.MakeHTTPHandlers(ctx, enroll.MakeServerEndpoints(sm.enrollService, sm.scepDepot), httptransport.ServerErrorLogger(httpLogger))
|
||||
r := mux.NewRouter()
|
||||
@@ -334,6 +360,7 @@ func serve(args []string) error {
|
||||
r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DefineDEPProfileHandler)).Methods("POST")
|
||||
r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.AppUploadHandler)).Methods("POST")
|
||||
r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListAppsHandler)).Methods("GET")
|
||||
r.Handle("/v1/config/certificate", apiAuthMiddleware(*flAPIKey, configHandlers.SavePushCertificateHandler)).Methods("PUT")
|
||||
}
|
||||
|
||||
if *flRepoPath != "" {
|
||||
@@ -429,6 +456,7 @@ type config struct {
|
||||
tlsCertPath string
|
||||
scepDepot *boltdepot.Depot
|
||||
profileDB *profile.DB
|
||||
configDB *configsvc.DB
|
||||
|
||||
// TODO: refactor enroll service and remove the need to reference
|
||||
// this on-disk cert. but it might be useful to keep the PEM
|
||||
@@ -442,6 +470,7 @@ type config struct {
|
||||
enrollService enroll.Service
|
||||
scepService scep.Service
|
||||
commandService command.Service
|
||||
configService configsvc.Service
|
||||
|
||||
err error
|
||||
}
|
||||
@@ -497,6 +526,10 @@ func (c *config) setupBolt() {
|
||||
}
|
||||
|
||||
func (c *config) loadPushCerts() {
|
||||
if c.APNSCertificatePath == "" && c.APNSPrivateKeyPass == "" && c.APNSPrivateKeyPath == "" {
|
||||
// this is optional, config could also be provided with mdmctl
|
||||
return
|
||||
}
|
||||
if c.err != nil {
|
||||
return
|
||||
}
|
||||
@@ -554,31 +587,47 @@ type pushServiceCert struct {
|
||||
PrivateKey interface{}
|
||||
}
|
||||
|
||||
func (c *config) setupPushService() {
|
||||
func (c *config) setupConfigStore() {
|
||||
if c.err != nil {
|
||||
return
|
||||
}
|
||||
tlsCert := tls.Certificate{
|
||||
Certificate: [][]byte{c.pushCert.Certificate.Raw},
|
||||
PrivateKey: c.pushCert.PrivateKey,
|
||||
Leaf: c.pushCert.Certificate,
|
||||
}
|
||||
client, err := push.NewClient(tlsCert)
|
||||
db, err := configsvc.NewDB(c.db, c.pubclient)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
}
|
||||
c.PushService = &push.Service{
|
||||
Client: client,
|
||||
Host: push.Production,
|
||||
c.configDB = db
|
||||
c.configService = configsvc.NewService(db)
|
||||
|
||||
}
|
||||
|
||||
func (c *config) setupPushService() {
|
||||
if c.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var opts []nanopush.Option
|
||||
{
|
||||
cert, _ := c.configDB.PushCertificate()
|
||||
if cert == nil {
|
||||
goto after
|
||||
}
|
||||
client, err := push.NewClient(*cert)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
}
|
||||
svc := push.NewService(client, push.Production)
|
||||
opts = append(opts, nanopush.WithPushService(svc))
|
||||
}
|
||||
after:
|
||||
|
||||
db, err := nanopush.NewDB(c.db, c.pubclient)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
}
|
||||
c.pushService, err = nanopush.New(db, c.PushService, c.pubclient)
|
||||
c.pushService, err = nanopush.New(db, c.configDB, c.pubclient, opts...)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
@@ -589,17 +638,13 @@ func (c *config) setupEnrollmentService() {
|
||||
if c.err != nil {
|
||||
return
|
||||
}
|
||||
pushTopic, err := topicFromCert(c.pushCert.Certificate)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
}
|
||||
|
||||
var SCEPCertificateSubject string
|
||||
// TODO: clean up order of inputs. Maybe pass *SCEPConfig as an arg?
|
||||
// but if you do, the packages are coupled, better not.
|
||||
c.enrollService, c.err = enroll.NewService(
|
||||
pushTopic,
|
||||
c.configDB,
|
||||
c.pubclient,
|
||||
c.scepCACertPath,
|
||||
c.ServerPublicURL+"/scep",
|
||||
c.SCEPChallenge,
|
||||
@@ -610,17 +655,6 @@ func (c *config) setupEnrollmentService() {
|
||||
)
|
||||
}
|
||||
|
||||
func topicFromCert(cert *x509.Certificate) (string, error) {
|
||||
var oidASN1UserID = asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}
|
||||
for _, v := range cert.Subject.Names {
|
||||
if v.Type.Equal(oidASN1UserID) {
|
||||
return v.Value.(string), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("could not find Push Topic (UserID OID) in certificate")
|
||||
}
|
||||
|
||||
func (c *config) depClient() (dep.Client, error) {
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
|
||||
Reference in New Issue
Block a user