mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-01 06:06:11 +08:00
Move the server type to it's own package (#458)
This commit is contained in:
committed by
Victor Vrantchan
parent
8f5838ceea
commit
d7f07bb022
456
server/server.go
Normal file
456
server/server.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/RobotsAndPencils/buford/push"
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/micromdm/dep"
|
||||
boltdepot "github.com/micromdm/scep/depot/bolt"
|
||||
scep "github.com/micromdm/scep/server"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
|
||||
"github.com/micromdm/micromdm/dep/depsync"
|
||||
"github.com/micromdm/micromdm/mdm"
|
||||
"github.com/micromdm/micromdm/mdm/enroll"
|
||||
"github.com/micromdm/micromdm/pkg/crypto"
|
||||
"github.com/micromdm/micromdm/platform/apns"
|
||||
apnsbuiltin "github.com/micromdm/micromdm/platform/apns/builtin"
|
||||
"github.com/micromdm/micromdm/platform/command"
|
||||
"github.com/micromdm/micromdm/platform/config"
|
||||
configbuiltin "github.com/micromdm/micromdm/platform/config/builtin"
|
||||
"github.com/micromdm/micromdm/platform/profile"
|
||||
profilebuiltin "github.com/micromdm/micromdm/platform/profile/builtin"
|
||||
"github.com/micromdm/micromdm/platform/pubsub"
|
||||
"github.com/micromdm/micromdm/platform/pubsub/inmem"
|
||||
"github.com/micromdm/micromdm/platform/queue"
|
||||
block "github.com/micromdm/micromdm/platform/remove"
|
||||
blockbuiltin "github.com/micromdm/micromdm/platform/remove/builtin"
|
||||
"github.com/micromdm/micromdm/workflow/webhook"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ConfigPath string
|
||||
Depsim string
|
||||
PubClient pubsub.PublishSubscriber
|
||||
DB *bolt.DB
|
||||
PushCert pushServiceCert
|
||||
ServerPublicURL string
|
||||
SCEPChallenge string
|
||||
APNSPrivateKeyPath string
|
||||
APNSCertificatePath string
|
||||
APNSPrivateKeyPass string
|
||||
TLSCertPath string
|
||||
SCEPDepot *boltdepot.Depot
|
||||
ProfileDB profile.Store
|
||||
ConfigDB config.Store
|
||||
RemoveDB block.Store
|
||||
CommandWebhookURL string
|
||||
DEPClient dep.Client
|
||||
|
||||
// TODO: refactor enroll service and remove the need to reference
|
||||
// this on-disk cert. but it might be useful to keep the PEM
|
||||
// around for anyone who will need to export the CA.
|
||||
SCEPCACertPath string
|
||||
|
||||
PushService *push.Service // bufford push
|
||||
APNSPushService apns.Service
|
||||
CommandService command.Service
|
||||
MDMService mdm.Service
|
||||
EnrollService enroll.Service
|
||||
SCEPService scep.Service
|
||||
ConfigService config.Service
|
||||
|
||||
WebhooksHTTPClient *http.Client
|
||||
}
|
||||
|
||||
func (c *Server) Setup(logger log.Logger) error {
|
||||
if err := c.setupPubSub(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupBolt(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupRemoveService(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupConfigStore(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.loadPushCerts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupSCEP(logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupPushService(logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupCommandService(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupWebhooks(logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupCommandQueue(logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupDepClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupProfileDB(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.setupEnrollmentService(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupProfileDB() error {
|
||||
profileDB, err := profilebuiltin.NewDB(c.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.ProfileDB = profileDB
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupPubSub() error {
|
||||
c.PubClient = inmem.NewPubSub()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupWebhooks(logger log.Logger) error {
|
||||
if c.CommandWebhookURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ww := webhook.New(c.CommandWebhookURL, c.PubClient, webhook.WithLogger(logger), webhook.WithHTTPClient(c.WebhooksHTTPClient))
|
||||
go ww.Run(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupRemoveService() error {
|
||||
removeDB, err := blockbuiltin.NewDB(c.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.RemoveDB = removeDB
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupCommandService() error {
|
||||
commandService, err := command.New(c.PubClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.CommandService = commandService
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupCommandQueue(logger log.Logger) error {
|
||||
q, err := queue.NewQueue(c.DB, c.PubClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var mdmService mdm.Service
|
||||
{
|
||||
svc := mdm.NewService(c.PubClient, q)
|
||||
mdmService = svc
|
||||
mdmService = block.RemoveMiddleware(c.RemoveDB)(mdmService)
|
||||
}
|
||||
c.MDMService = mdmService
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupBolt() error {
|
||||
dbPath := filepath.Join(c.ConfigPath, "micromdm.db")
|
||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "opening boltdb")
|
||||
}
|
||||
c.DB = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) loadPushCerts() error {
|
||||
if c.APNSCertificatePath == "" && c.APNSPrivateKeyPass == "" && c.APNSPrivateKeyPath == "" {
|
||||
// this is optional, config could also be provided with mdmctl
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
|
||||
if c.APNSPrivateKeyPath == "" {
|
||||
var pkcs12Data []byte
|
||||
|
||||
pkcs12Data, err = ioutil.ReadFile(c.APNSCertificatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.PushCert.PrivateKey, c.PushCert.Certificate, err =
|
||||
pkcs12.Decode(pkcs12Data, c.APNSPrivateKeyPass)
|
||||
return err
|
||||
}
|
||||
|
||||
c.PushCert.Certificate, err = crypto.ReadPEMCertificateFile(c.APNSCertificatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pemData []byte
|
||||
pemData, err = ioutil.ReadFile(c.APNSPrivateKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pkeyBlock := new(bytes.Buffer)
|
||||
pemBlock, _ := pem.Decode(pemData)
|
||||
if pemBlock == nil {
|
||||
return errors.New("invalid PEM data for privkey")
|
||||
}
|
||||
|
||||
if x509.IsEncryptedPEMBlock(pemBlock) {
|
||||
b, err := x509.DecryptPEMBlock(pemBlock, []byte(c.APNSPrivateKeyPass))
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypting DES private key %s", err)
|
||||
}
|
||||
pkeyBlock.Write(b)
|
||||
} else {
|
||||
pkeyBlock.Write(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
priv, err := x509.ParsePKCS1PrivateKey(pkeyBlock.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing pkcs1 private key: %s", err)
|
||||
}
|
||||
c.PushCert.PrivateKey = priv
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type pushServiceCert struct {
|
||||
*x509.Certificate
|
||||
PrivateKey interface{}
|
||||
}
|
||||
|
||||
func (c *Server) setupConfigStore() error {
|
||||
db, err := configbuiltin.NewDB(c.DB, c.PubClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.ConfigDB = db
|
||||
c.ConfigService = config.New(db)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupPushService(logger log.Logger) error {
|
||||
var opts []apns.Option
|
||||
{
|
||||
cert, _ := c.ConfigDB.PushCertificate()
|
||||
if c.PushCert.Certificate != nil && cert == nil {
|
||||
cert = &tls.Certificate{
|
||||
Certificate: [][]byte{c.PushCert.Certificate.Raw},
|
||||
PrivateKey: c.PushCert.PrivateKey,
|
||||
Leaf: c.PushCert.Certificate,
|
||||
}
|
||||
}
|
||||
if cert == nil {
|
||||
goto after
|
||||
}
|
||||
client, err := push.NewClient(*cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
svc := push.NewService(client, push.Production)
|
||||
opts = append(opts, apns.WithPushService(svc))
|
||||
}
|
||||
after:
|
||||
|
||||
db, err := apnsbuiltin.NewDB(c.DB, c.PubClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service, err := apns.New(db, c.ConfigDB, c.PubClient, opts...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "starting micromdm push service")
|
||||
}
|
||||
c.APNSPushService = apns.LoggingMiddleware(
|
||||
log.With(level.Info(logger), "component", "apns"),
|
||||
)(service)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) setupEnrollmentService() error {
|
||||
var topicProvider enroll.TopicProvider
|
||||
var err error
|
||||
if c.PushCert.Certificate != nil {
|
||||
pushTopic, err := crypto.TopicFromCert(c.PushCert.Certificate)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get apns topic from certificate")
|
||||
}
|
||||
topicProvider = staticTopicProvider{topic: pushTopic}
|
||||
} else {
|
||||
topicProvider = c.ConfigDB
|
||||
}
|
||||
|
||||
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, err = enroll.NewService(
|
||||
topicProvider,
|
||||
c.PubClient,
|
||||
c.SCEPCACertPath,
|
||||
c.ServerPublicURL+"/scep",
|
||||
c.SCEPChallenge,
|
||||
c.ServerPublicURL,
|
||||
c.TLSCertPath,
|
||||
SCEPCertificateSubject,
|
||||
c.ProfileDB,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the apns-cert flags are specified this provider will be used in the enroll service.
|
||||
type staticTopicProvider struct{ topic string }
|
||||
|
||||
func (p staticTopicProvider) PushTopic() (string, error) {
|
||||
return p.topic, nil
|
||||
}
|
||||
|
||||
func (c *Server) setupDepClient() error {
|
||||
// depsim config
|
||||
depsim := c.Depsim
|
||||
var conf *dep.Config
|
||||
|
||||
// try getting the oauth config from bolt
|
||||
tokens, err := c.ConfigDB.DEPTokens()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tokens) >= 1 {
|
||||
conf = new(dep.Config)
|
||||
conf.ConsumerSecret = tokens[0].ConsumerSecret
|
||||
conf.ConsumerKey = tokens[0].ConsumerKey
|
||||
conf.AccessSecret = tokens[0].AccessSecret
|
||||
conf.AccessToken = tokens[0].AccessToken
|
||||
// TODO: handle expiration
|
||||
}
|
||||
|
||||
// override with depsim keys if specified on CLI
|
||||
if depsim != "" {
|
||||
conf = &dep.Config{
|
||||
ConsumerKey: "CK_48dd68d198350f51258e885ce9a5c37ab7f98543c4a697323d75682a6c10a32501cb247e3db08105db868f73f2c972bdb6ae77112aea803b9219eb52689d42e6",
|
||||
ConsumerSecret: "CS_34c7b2b531a600d99a0e4edcf4a78ded79b86ef318118c2f5bcfee1b011108c32d5302df801adbe29d446eb78f02b13144e323eb9aad51c79f01e50cb45c3a68",
|
||||
AccessToken: "AT_927696831c59ba510cfe4ec1a69e5267c19881257d4bca2906a99d0785b785a6f6fdeb09774954fdd5e2d0ad952e3af52c6d8d2f21c924ba0caf4a031c158b89",
|
||||
AccessSecret: "AS_c31afd7a09691d83548489336e8ff1cb11b82b6bca13f793344496a556b1f4972eaff4dde6deb5ac9cf076fdfa97ec97699c34d515947b9cf9ed31c99dded6ba",
|
||||
}
|
||||
}
|
||||
|
||||
if conf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
depServerURL := "https://mdmenrollment.apple.com"
|
||||
if depsim != "" {
|
||||
depServerURL = depsim
|
||||
}
|
||||
client, err := dep.NewClient(conf, dep.ServerURL(depServerURL))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.DEPClient = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) CreateDEPSyncer(logger log.Logger) (depsync.Syncer, error) {
|
||||
client := c.DEPClient
|
||||
opts := []depsync.Option{
|
||||
depsync.WithLogger(log.With(logger, "component", "depsync")),
|
||||
}
|
||||
if client != nil {
|
||||
opts = append(opts, depsync.WithClient(client))
|
||||
}
|
||||
|
||||
var syncer depsync.Syncer
|
||||
syncer, err := depsync.New(c.PubClient, c.DB, logger, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return syncer, nil
|
||||
}
|
||||
|
||||
func (c *Server) setupSCEP(logger log.Logger) error {
|
||||
depot, err := boltdepot.NewBoltDepot(c.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := depot.CreateOrLoadKey(2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caCert, err := depot.CreateOrLoadCA(key, 5, "MicroMDM", "US")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SCEPCACertPath = filepath.Join(c.ConfigPath, "SCEPCACert.pem")
|
||||
|
||||
err = crypto.WritePEMCertificateFile(caCert, c.SCEPCACertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := []scep.ServiceOption{
|
||||
scep.ClientValidity(365),
|
||||
scep.ChallengePassword(c.SCEPChallenge),
|
||||
}
|
||||
c.SCEPDepot = depot
|
||||
c.SCEPService, err = scep.NewService(depot, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.SCEPService = scep.NewLoggingService(logger, c.SCEPService)
|
||||
|
||||
return nil
|
||||
}
|
||||
31
server/server_test.go
Normal file
31
server/server_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLoadPushCerts(t *testing.T) {
|
||||
keypath := "testdata/ProviderPrivateKey.key"
|
||||
certpath := "testdata/pushcert.pem"
|
||||
p12path := "testdata/pushcert.p12"
|
||||
keysecret := "secret"
|
||||
|
||||
s := &Server{
|
||||
APNSPrivateKeyPath: keypath,
|
||||
APNSCertificatePath: certpath,
|
||||
APNSPrivateKeyPass: keysecret,
|
||||
}
|
||||
|
||||
// test separate key and cert
|
||||
err := s.loadPushCerts()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test p12 with secret
|
||||
s.APNSCertificatePath = p12path
|
||||
s.APNSPrivateKeyPath = ""
|
||||
err = s.loadPushCerts()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
30
server/testdata/ProviderPrivateKey.key
vendored
Normal file
30
server/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
server/testdata/pushcert.p12
vendored
Normal file
BIN
server/testdata/pushcert.p12
vendored
Normal file
Binary file not shown.
18
server/testdata/pushcert.pem
vendored
Normal file
18
server/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-----
|
||||
Reference in New Issue
Block a user