From dfc8cca3d85e80d79732d24cfa42136a0a2b449d Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Sun, 15 Oct 2017 21:08:48 -0400 Subject: [PATCH] mdmctl can upload/replace push certificate (#238) Closes #236 --- CHANGELOG.md | 1 + cmd/mdmctl/mdmcert.go | 123 +++++++++++++++++ cmd/mdmctl/mdmcert_test.go | 21 +++ cmd/mdmctl/testdata/ProviderPrivateKey.key | 30 +++++ cmd/mdmctl/testdata/pushcert.p12 | Bin 0 -> 2365 bytes cmd/mdmctl/testdata/pushcert.pem | 18 +++ config/client.go | 48 +++++++ config/config.go | 32 +++++ config/db.go | 150 +++++++++++++++++++++ config/endpoints.go | 44 ++++++ config/internal/configproto/config.go | 3 + config/internal/configproto/config.pb.go | 70 ++++++++++ config/internal/configproto/config.proto | 9 ++ config/service.go | 24 ++++ config/transport_http.go | 90 +++++++++++++ enroll/service.go | 78 +++++++++-- push/service.go | 87 +++++++++++- serve.go | 126 ++++++++++------- 18 files changed, 889 insertions(+), 65 deletions(-) create mode 100644 cmd/mdmctl/mdmcert_test.go create mode 100644 cmd/mdmctl/testdata/ProviderPrivateKey.key create mode 100644 cmd/mdmctl/testdata/pushcert.p12 create mode 100644 cmd/mdmctl/testdata/pushcert.pem create mode 100644 config/client.go create mode 100644 config/config.go create mode 100644 config/db.go create mode 100644 config/endpoints.go create mode 100644 config/internal/configproto/config.go create mode 100644 config/internal/configproto/config.pb.go create mode 100644 config/internal/configproto/config.proto create mode 100644 config/service.go create mode 100644 config/transport_http.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a590b775..16649984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/cmd/mdmctl/mdmcert.go b/cmd/mdmctl/mdmcert.go index 2ed3a34c..1bd33e19 100644 --- a/cmd/mdmctl/mdmcert.go +++ b/cmd/mdmctl/mdmcert.go @@ -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") diff --git a/cmd/mdmctl/mdmcert_test.go b/cmd/mdmctl/mdmcert_test.go new file mode 100644 index 00000000..2423692c --- /dev/null +++ b/cmd/mdmctl/mdmcert_test.go @@ -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) + } +} diff --git a/cmd/mdmctl/testdata/ProviderPrivateKey.key b/cmd/mdmctl/testdata/ProviderPrivateKey.key new file mode 100644 index 00000000..647c64f8 --- /dev/null +++ b/cmd/mdmctl/testdata/ProviderPrivateKey.key @@ -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----- diff --git a/cmd/mdmctl/testdata/pushcert.p12 b/cmd/mdmctl/testdata/pushcert.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c3cd1538b210533527bee750a2242dece5a3593b GIT binary patch literal 2365 zcmV-D3BvX;f(bbS0Ru3C2>%8NDuzgg_YDCD0ic2i@C1Sg>@b1|=rDo9kR>#?tL(O$5?xC|sC+Uil_1dRox2DNRVd0<-*goRTMNLB98oA4 z@ZJe3Mrdd=O)yw7Iw-n0In~)G5`!0kVS7xC*rV zAdQ!rm7uNS&*a24l=bgsHjSjcvfuE0h_^}ba^VZAGQ5fb#~hMG6@hwidv!%T0V7f> z+2T3hQO^`=N;k{RQK_EgoEy8bvJLMQatPZcWG%&B`cq&6>AlIRUZ>V7dofw-?3 zcEOV1Ez(g}noHn_Un6e2KcD1gvDl2uXG81Gd zFxXqzs)CdkcgQec)~Ozpnb;%a1#?+k{kXk+)Ks9Wl^%1Gl@_66tP4rwZqU?nt9!Yr z%qCr6vj54;EC6_<>1v)428qn>%-+$)=vAB*0-JHn!35mjhSVNFTyhOR28`p8xduhn z__^NFc-PNp`FuD&xV5BcoZf#tm~-ScIvDg?8T6Sy%N)2_?_T~^CEa^kgSutl14MG0 zd9cq$7c!?dz&=~_p@Is1rpO3}B5L~%Nl3LDnqjzQD+WF{K36-MZipZ)-reQe#!SR@dfsQ8s&#@A{-kBlurknfOD*bsD2g` z7@7q$P)boHL_64hAQe4ARpQQ+1^?JI6MRso+fP;AKYAcfDq)x%yjDWInb@*v5H;e_ zV)+UwuT1Dk$Z!|dW6j-cd!mwUaD1-W`}zRN1k$OLMz4o-QO}oI-+7h!6$V~2cJS}X zprsC7UNMr)$#HZ(pzNMWS!FnysJ_xPe7^0;4aOeMKW#;WD$eni1#3>8DdzYHRFoFd^1_>&LNQU;|O{5E@RD- z#@=Gn9j_eHD39tYWl?0pvJ-9?0>WUhMZrsXAHNKZMl_fTl!un(W|~0ous;DjFD+&9 z7EavMQW@dc&=uroXVS_{0YigtQ&LX3)GOV1NU^|trMj3PYd@WO z(BNc>KwPn^RBTA|`9*PNj;r^)`m2m3r^hNRi2(+!y+?Cxr_wLEBgD|=?2puPCJUB_ zFZ5t9I8z9#fR$W_hO~G-ii_#{<-2V}?NJJ%SxFECIBP6Y39g0r= z)c=kU5K82~(UqEYzeBK|Hl;IIhc$p@x*@JNDGrrf139wwnAh?-oHH-dWaz{`)gWQh zJq%NhdauDn35JeU!7~IUDDQQ#>k}n6T4$JCEoLDP=o#d_@M;%-9!7s+c)q?X$b>F$ zl(F%ommk_{hWITbp|iVr?*AZe;0&6e=o+or5{M_|j}ClMnI`9J=iTdx2Vx$^kK6X- z=iB;k3gLMVIM1q%e8n5_&x=FPzcpk%SDkY)H zXz~I=k!CQ^X~KD?>-WpGX93fP*O%_PhU+ZUDlMA5w<1ePeQ!n;f3FZ!nbiG}#kVh* z6vm=x?`r$iT3E&nb_%sigE{iB^CDq6X$EeRCg?pJwfc+s2eQ@XWGaKT$jf9a*z@kW zB$b-t65RZxmu!=DA=}wa3>0Le`51@Pg&%Rz`;r@Ef~$J98EcRM#OT7uo>9(yP70x^ z?cgqjBMPE}6YWy*sa!hRsbodVQu5d2z$70N@8d1*4wf&!vJqV~)ma*ytUI=HD9@MT z=a3bt%J7~Yu)JdQ%JtoH=fjdix*Y`5Re_Qx?|LyXAQLh&Ub7J8$&`2`=bV6KyTF6r zgts*P2Em(g$AxjTm2%ivQ?~<^=}F&3z=*f$)3F8b8LVu^T^b-4BKLs~9}O^kH0EczJ) zMNAUPn{93%?(}3Ie~!K2)@7VOZ*YO5HHc)TP$e97e^e zscV*0w%6n2qg&?d%M4k`sNeNi8%sJMc8dO4bMSoGu})7V&QtGd7RzDTFUv69cSz^8 z)DMOQ4-aD}UHO%q#-YUB9C8?GMXE)x*x~xlr}OTjx9J+{M=M%*KRH;=B=^3{{!-z%Vf|AutIB1uG5%0vZJX1Qe_; j9aC~EBY;$Ur`~~t(#ir)axw%69_>Xs1|c4o0s;sCDr#N4 literal 0 HcmV?d00001 diff --git a/cmd/mdmctl/testdata/pushcert.pem b/cmd/mdmctl/testdata/pushcert.pem new file mode 100644 index 00000000..1328e203 --- /dev/null +++ b/cmd/mdmctl/testdata/pushcert.pem @@ -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----- diff --git a/config/client.go b/config/client.go new file mode 100644 index 00000000..bcf1cb9d --- /dev/null +++ b/config/client.go @@ -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 +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..5402ca45 --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config/db.go b/config/db.go new file mode 100644 index 00000000..5f66473e --- /dev/null +++ b/config/db.go @@ -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) +} diff --git a/config/endpoints.go b/config/endpoints.go new file mode 100644 index 00000000..25f03909 --- /dev/null +++ b/config/endpoints.go @@ -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 + } +} diff --git a/config/internal/configproto/config.go b/config/internal/configproto/config.go new file mode 100644 index 00000000..a814ca24 --- /dev/null +++ b/config/internal/configproto/config.go @@ -0,0 +1,3 @@ +package configproto + +//go:generate protoc --go_out=. config.proto diff --git a/config/internal/configproto/config.pb.go b/config/internal/configproto/config.pb.go new file mode 100644 index 00000000..806ce106 --- /dev/null +++ b/config/internal/configproto/config.pb.go @@ -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, +} diff --git a/config/internal/configproto/config.proto b/config/internal/configproto/config.proto new file mode 100644 index 00000000..d5a2ed1d --- /dev/null +++ b/config/internal/configproto/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package configproto; + +message ServerConfig { + bytes push_certificate = 1; + bytes push_certificate_key = 2; +} + diff --git a/config/service.go b/config/service.go new file mode 100644 index 00000000..dca90c47 --- /dev/null +++ b/config/service.go @@ -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") +} diff --git a/config/transport_http.go b/config/transport_http.go new file mode 100644 index 00000000..fa408a16 --- /dev/null +++ b/config/transport_http.go @@ -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 +} diff --git a/enroll/service.go b/enroll/service.go index 0d0820cc..29ea5d19 100644 --- a/enroll/service.go +++ b/enroll/service.go @@ -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 } diff --git a/push/service.go b/push/service.go index cd99a755..609fa29a 100644 --- a/push/service.go +++ b/push/service.go @@ -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 { diff --git a/serve.go b/serve.go index e4733534..8de093c6 100644 --- a/serve.go +++ b/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