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