mdmctl can upload/replace push certificate (#238)

Closes #236
This commit is contained in:
Victor Vrantchan
2017-10-15 21:08:48 -04:00
committed by GitHub
parent aacac39945
commit dfc8cca3d8
18 changed files with 889 additions and 65 deletions

View File

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

View File

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

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

View 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

Binary file not shown.

18
cmd/mdmctl/testdata/pushcert.pem vendored Normal file
View 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
View 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
View 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
View 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 &notFound{"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
View 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
}
}

View File

@@ -0,0 +1,3 @@
package configproto
//go:generate protoc --go_out=. config.proto

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

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

View File

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

View File

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

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