1package duoapi
2
3import (
4	"crypto/hmac"
5	"crypto/sha1"
6	"crypto/tls"
7	"crypto/x509"
8	"encoding/base64"
9	"encoding/hex"
10	"io"
11	"io/ioutil"
12	"math/rand"
13	"net/http"
14	"net/url"
15	"sort"
16	"strings"
17	"time"
18)
19
20const (
21	initialBackoffMS  = 1000
22	maxBackoffMS      = 32000
23	backoffFactor     = 2
24	rateLimitHttpCode = 429
25)
26
27var spaceReplacer *strings.Replacer = strings.NewReplacer("+", "%20")
28
29func canonParams(params url.Values) string {
30	// Values must be in sorted order
31	for key, val := range params {
32		sort.Strings(val)
33		params[key] = val
34	}
35	// Encode will place Keys in sorted order
36	ordered_params := params.Encode()
37	// Encoder turns spaces into +, but we need %XX escaping
38	return spaceReplacer.Replace(ordered_params)
39}
40
41func canonicalize(method string,
42	host string,
43	uri string,
44	params url.Values,
45	date string) string {
46	var canon [5]string
47	canon[0] = date
48	canon[1] = strings.ToUpper(method)
49	canon[2] = strings.ToLower(host)
50	canon[3] = uri
51	canon[4] = canonParams(params)
52	return strings.Join(canon[:], "\n")
53}
54
55func sign(ikey string,
56	skey string,
57	method string,
58	host string,
59	uri string,
60	date string,
61	params url.Values) string {
62	canon := canonicalize(method, host, uri, params, date)
63	mac := hmac.New(sha1.New, []byte(skey))
64	mac.Write([]byte(canon))
65	sig := hex.EncodeToString(mac.Sum(nil))
66	auth := ikey + ":" + sig
67	return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
68}
69
70type DuoApi struct {
71	ikey       string
72	skey       string
73	host       string
74	userAgent  string
75	apiClient  httpClient
76	authClient httpClient
77	sleepSvc   sleepService
78}
79
80type httpClient interface {
81	Do(req *http.Request) (*http.Response, error)
82}
83type sleepService interface {
84	Sleep(duration time.Duration)
85}
86type timeSleepService struct{}
87
88func (svc timeSleepService) Sleep(duration time.Duration) {
89	time.Sleep(duration + (time.Duration(rand.Intn(1000)) * time.Millisecond))
90}
91
92type apiOptions struct {
93	timeout  time.Duration
94	insecure bool
95	proxy    func(*http.Request) (*url.URL, error)
96}
97
98// Optional parameter for NewDuoApi, used to configure timeouts on API calls.
99func SetTimeout(timeout time.Duration) func(*apiOptions) {
100	return func(opts *apiOptions) {
101		opts.timeout = timeout
102		return
103	}
104}
105
106// Optional parameter for testing only.  Bypasses all TLS certificate validation.
107func SetInsecure() func(*apiOptions) {
108	return func(opts *apiOptions) {
109		opts.insecure = true
110	}
111}
112
113// Optional parameter for NewDuoApi, used to configure an HTTP Connect proxy
114// server for all outbound communications.
115func SetProxy(proxy func(*http.Request) (*url.URL, error)) func(*apiOptions) {
116	return func(opts *apiOptions) {
117		opts.proxy = proxy
118	}
119}
120
121// Build an return a DuoApi struct.
122// ikey is your Duo integration key
123// skey is your Duo integration secret key
124// host is your Duo host
125// userAgent allows you to specify the user agent string used when making
126//           the web request to Duo.
127// options are optional parameters.  Use SetTimeout() to specify a timeout value
128//         for Rest API calls.  Use SetProxy() to specify proxy settings for Duo API calls.
129//
130// Example: duoapi.NewDuoApi(ikey,skey,host,userAgent,duoapi.SetTimeout(10*time.Second))
131func NewDuoApi(ikey string,
132	skey string,
133	host string,
134	userAgent string,
135	options ...func(*apiOptions)) *DuoApi {
136	opts := apiOptions{proxy: http.ProxyFromEnvironment}
137	for _, o := range options {
138		o(&opts)
139	}
140
141	// Certificate pinning
142	certPool := x509.NewCertPool()
143	certPool.AppendCertsFromPEM([]byte(duoPinnedCert))
144
145	tr := &http.Transport{
146		Proxy: opts.proxy,
147		TLSClientConfig: &tls.Config{
148			RootCAs:            certPool,
149			InsecureSkipVerify: opts.insecure,
150		},
151	}
152	return &DuoApi{
153		ikey:      ikey,
154		skey:      skey,
155		host:      host,
156		userAgent: userAgent,
157		apiClient: &http.Client{
158			Timeout:   opts.timeout,
159			Transport: tr,
160		},
161		authClient: &http.Client{
162			Transport: tr,
163		},
164		sleepSvc: timeSleepService{},
165	}
166}
167
168type requestOptions struct {
169	timeout bool
170}
171
172type DuoApiOption func(*requestOptions)
173
174// Pass to Request or SignedRequest to configure a timeout on the request
175func UseTimeout(opts *requestOptions) {
176	opts.timeout = true
177}
178
179func (duoapi *DuoApi) buildOptions(options ...DuoApiOption) *requestOptions {
180	opts := &requestOptions{}
181	for _, o := range options {
182		o(opts)
183	}
184	return opts
185}
186
187// API calls will return a StatResult object.  On success, Stat is 'OK'.
188// On error, Stat is 'FAIL', and Code, Message, and Message_Detail
189// contain error information.
190type StatResult struct {
191	Stat           string
192	Code           *int32
193	Message        *string
194	Message_Detail *string
195}
196
197// Make an unsigned Duo Rest API call.  See Duo's online documentation
198// for the available REST API's.
199// method is POST or GET
200// uri is the URI of the Duo Rest call
201// params HTTP query parameters to include in the call.
202// options Optional parameters.  Use UseTimeout to toggle whether the
203//         Duo Rest API call should timeout or not.
204//
205// Example: duo.Call("GET", "/auth/v2/ping", nil, duoapi.UseTimeout)
206func (duoapi *DuoApi) Call(method string,
207	uri string,
208	params url.Values,
209	options ...DuoApiOption) (*http.Response, []byte, error) {
210
211	url := url.URL{
212		Scheme:   "https",
213		Host:     duoapi.host,
214		Path:     uri,
215		RawQuery: params.Encode(),
216	}
217
218	return duoapi.makeRetryableHttpCall(method, url, nil, nil, options...)
219}
220
221// Make a signed Duo Rest API call.  See Duo's online documentation
222// for the available REST API's.
223// method is POST or GET
224// uri is the URI of the Duo Rest call
225// params HTTP query parameters to include in the call.
226// options Optional parameters.  Use UseTimeout to toggle whether the
227//         Duo Rest API call should timeout or not.
228//
229// Example: duo.SignedCall("GET", "/auth/v2/check", nil, duoapi.UseTimeout)
230func (duoapi *DuoApi) SignedCall(method string,
231	uri string,
232	params url.Values,
233	options ...DuoApiOption) (*http.Response, []byte, error) {
234
235	now := time.Now().UTC().Format(time.RFC1123Z)
236	auth_sig := sign(duoapi.ikey, duoapi.skey, method, duoapi.host, uri, now, params)
237
238	url := url.URL{
239		Scheme: "https",
240		Host:   duoapi.host,
241		Path:   uri,
242	}
243	method = strings.ToUpper(method)
244
245	if method == "GET" {
246		url.RawQuery = params.Encode()
247	}
248
249	headers := make(map[string]string)
250	headers["Authorization"] = auth_sig
251	headers["Date"] = now
252	var requestBody io.ReadCloser = nil
253	if method == "POST" || method == "PUT" {
254		headers["Content-Type"] = "application/x-www-form-urlencoded"
255		requestBody = ioutil.NopCloser(strings.NewReader(params.Encode()))
256	}
257
258	return duoapi.makeRetryableHttpCall(method, url, headers, requestBody, options...)
259}
260
261func (duoapi *DuoApi) makeRetryableHttpCall(
262	method string,
263	url url.URL,
264	headers map[string]string,
265	body io.ReadCloser,
266	options ...DuoApiOption) (*http.Response, []byte, error) {
267
268	opts := duoapi.buildOptions(options...)
269
270	client := duoapi.authClient
271	if opts.timeout {
272		client = duoapi.apiClient
273	}
274
275	backoffMs := initialBackoffMS
276	for {
277		request, err := http.NewRequest(method, url.String(), nil)
278		if err != nil {
279			return nil, nil, err
280		}
281
282		if headers != nil {
283			for k, v := range headers {
284				request.Header.Set(k, v)
285			}
286		}
287		if body != nil {
288			request.Body = body
289		}
290
291		resp, err := client.Do(request)
292		var body []byte
293		if err != nil {
294			return resp, body, err
295		}
296
297		if backoffMs > maxBackoffMS || resp.StatusCode != rateLimitHttpCode {
298			body, err = ioutil.ReadAll(resp.Body)
299			resp.Body.Close()
300			return resp, body, err
301		}
302
303		duoapi.sleepSvc.Sleep(time.Millisecond * time.Duration(backoffMs))
304		backoffMs *= backoffFactor
305	}
306}
307
308const duoPinnedCert string = `
309subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA
310-----BEGIN CERTIFICATE-----
311MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl
312MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
313d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
314b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG
315EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
316cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi
317MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c
318JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP
319mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+
320wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4
321VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/
322AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB
323AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
324BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun
325pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC
326dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf
327fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm
328NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx
329H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
330+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
331-----END CERTIFICATE-----
332
333subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA
334-----BEGIN CERTIFICATE-----
335MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
336MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
337d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
338QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
339MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
340b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
3419w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
342CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
343nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
34443C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
345T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
346gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
347BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
348TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
349DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
350hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
35106O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
352PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
353YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
354CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
355-----END CERTIFICATE-----
356
357subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA
358-----BEGIN CERTIFICATE-----
359MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
360MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
361d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
362ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
363MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
364LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
365RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
366+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
367PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
368xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
369Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
370hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
371EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
372MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
373FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
374nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
375eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
376hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
377Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
378vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
379+OkuE6N36B9K
380-----END CERTIFICATE-----
381
382subject= /C=US/O=SecureTrust Corporation/CN=SecureTrust CA
383-----BEGIN CERTIFICATE-----
384MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI
385MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
386FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz
387MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv
388cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN
389AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz
390Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO
3910gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao
392wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj
3937DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS
3948kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT
395BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
396/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg
397JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC
398NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3
3996Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/
4003XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm
401D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS
402CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
4033ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
404-----END CERTIFICATE-----
405
406subject= /C=US/O=SecureTrust Corporation/CN=Secure Global CA
407-----BEGIN CERTIFICATE-----
408MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK
409MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
410GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx
411MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg
412Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG
413SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ
414iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa
415/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ
416jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI
417HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7
418sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w
419gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
420MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw
421KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG
422AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L
423URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO
424H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm
425I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY
426iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
427f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
428-----END CERTIFICATE-----`
429