Skip to content

Commit 0a3ca6e

Browse files
authored
Support local certificates and HTTP proxies (#8)
- Allow specifying the local certificate to check and the CA bundle rather than looking up the certificates via URL. - Support providing a proxy host and port
1 parent 37e4a31 commit 0a3ca6e

26 files changed

+1553
-37
lines changed

README.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,61 @@ gem 'ssl-test'
88

99
## Usage
1010

11-
Simply call the `SSLTest.test` method and it'll return 3 values:
11+
Simply call the `SSLTest.test_url` method and it'll return 3 values:
1212

1313
1. the validity of the certificate
1414
2. the error message (if any)
1515
3. the certificate itself
1616

1717
Example with good cert:
18+
1819
```ruby
19-
valid, error, cert = SSLTest.test "https://google.com"
20+
valid, error, cert = SSLTest.test_url "https://google.com"
2021
valid # => true
2122
error # => nil
2223
cert # => #<OpenSSL::X509::Certificate...>
2324
```
2425

2526
Example with bad certificate:
27+
2628
```ruby
27-
valid, error, cert = SSLTest.test "https://testssl-expire.disig.sk"
29+
valid, error, cert = SSLTest.test_url "https://testssl-expire.disig.sk"
2830
valid # => false
2931
error # => "error code 10: certificate has expired"
3032
cert # => #<OpenSSL::X509::Certificate...>
3133
```
3234

3335
If the request fails and we're unable to detemine the validity, here are the returned values:
36+
3437
```ruby
35-
valid, error, cert = SSLTest.test "https://thisisdefinitelynotawebsite.com"
38+
valid, error, cert = SSLTest.test_url "https://thisisdefinitelynotawebsite.com"
3639
valid # => nil
3740
error # => "SSL certificate test failed: getaddrinfo: Name or service not known"
3841
cert # => nil
3942
```
4043

41-
You can also pass custom timeout values:
44+
You can also pass custom timeout values (defaults to 5 seconds for open and read):
45+
4246
```ruby
43-
valid, error, cert = SSLTest.test "https://slowebsite.com", open_timeout: 2, read_timeout: 2
47+
valid, error, cert = SSLTest.test_url "https://slowebsite.com", open_timeout: 2, read_timeout: 2
4448
valid # => nil
4549
error # => "SSL certificate test failed: execution expired"
4650
cert # => nil
4751
```
48-
Default timeout values are 5 seconds each (open and read)
52+
53+
Or a proxy host and port to use for the http requests:
54+
55+
```ruby
56+
valid, error, cert = SSLTest.test_url "https://slowebsite.com", proxy_host: 'localhost', proxy_port: 8080
57+
valid # => true
58+
error # => nil
59+
cert # => #<OpenSSL::X509::Certificate...>
60+
```
4961

5062
Revoked certificates are detected using [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) endpoint by default:
63+
5164
```ruby
52-
valid, error, cert = SSLTest.test "https://revoked.badssl.com"
65+
valid, error, cert = SSLTest.test_url "https://revoked.badssl.com"
5366
valid # => false
5467
error # => "SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2019-10-07 20:30:39 UTC)"
5568
cert # => #<OpenSSL::X509::Certificate...>
@@ -58,13 +71,27 @@ cert # => #<OpenSSL::X509::Certificate...>
5871
If the OCSP endpoint is missing, invalid or unreachable the certificate revocation will be tested using [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list).
5972

6073
If both OCSP and CRL tests are impossible, the certificate will still be considered valid but with an error message:
74+
6175
```ruby
62-
valid, error, cert = SSLTest.test "https://sitewithnoOCSPorCRL.com"
76+
valid, error, cert = SSLTest.test_url "https://sitewithnoOCSPorCRL.com"
6377
valid # => true
6478
error # => "Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension, CRL: Missing crlDistributionPoints extension"
6579
cert # => #<OpenSSL::X509::Certificate...>
6680
```
6781

82+
### Testing when you have the client certificate and Certificate Authority Bundle
83+
84+
If you already have access to the client certificate and the CA certificate bundle to check against, you can call `test_cert` which takes a certificate and ca bundle certificate instead of a URL. it has all the same options as `test_url`
85+
86+
```ruby
87+
cert = OpenSSL::X509::Certificate.new(File.read('path/to/certificate')))
88+
ca_bundle = OpenSSL::X509::Certificate.load(File.read('path/to/ca-bundle-certificate'))
89+
90+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
91+
```
92+
93+
This check will pass for self-signed certificates if the certificate is signed by the ca certificate provided.
94+
6895
## How it works
6996

7097
SSLTester connects as an HTTPS client (without issuing any requests) and then closes the connection. It does so using ruby `net/https` library and verifies the SSL status. It also hooks into the validation process to intercept the raw certificate for you.

lib/ssl-test.rb

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ module SSLTest
1313
VERSION = -"1.4.1"
1414

1515
class << self
16-
def test url, open_timeout: 5, read_timeout: 5, redirection_limit: 5
16+
def test_url url, open_timeout: 5, read_timeout: 5, proxy_host: nil, proxy_port: nil, redirection_limit: 5
17+
cert = failed_cert_reason = chain = nil
18+
1719
uri = URI.parse(url)
1820
return if uri.scheme != 'https' and uri.scheme != 'tcps'
19-
cert = failed_cert_reason = chain = nil
2021

2122
@logger&.info { "SSLTest #{url} started" }
22-
http = Net::HTTP.new(uri.host, uri.port)
23+
http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
2324
http.open_timeout = open_timeout
2425
http.read_timeout = read_timeout
2526
http.use_ssl = true
@@ -33,25 +34,48 @@ def test url, open_timeout: 5, read_timeout: 5, redirection_limit: 5
3334

3435
begin
3536
http.start { }
36-
revoked, message, revocation_date = test_chain_revocation(chain, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit)
37+
38+
revoked, message, revocation_date = test_chain_revocation(chain, open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit)
3739
@logger&.info { "SSLTest #{url} finished: revoked=#{revoked} #{message}" }
38-
return [false, "SSL certificate revoked: #{message} (revocation date: #{revocation_date})", cert] if revoked
39-
return [true, "Revocation test couldn't be performed: #{message}", cert] if message
40-
return [true, nil, cert]
41-
rescue OpenSSL::SSL::SSLError => e
42-
error = e.message
43-
error = "error code %d: %s" % failed_cert_reason if failed_cert_reason
44-
if error =~ /certificate verify failed/
45-
domains = cert_domains(cert)
46-
if matching_domains(domains, uri.host).none?
47-
error = "hostname \"#{uri.host}\" does not match the server certificate (#{domains.join(', ')})"
48-
end
40+
return [!revoked, revocation_message(revoked, revocation_date, message), cert]
41+
rescue OpenSSL::SSL::SSLError => error
42+
error_message = parse_ssl_error(error, cert, failed_cert_reason, uri:)
43+
@logger&.info { "SSLTest #{url} finished: #{error_message}" }
44+
return [false, error_message, cert]
45+
rescue => error
46+
@logger&.error { "SSLTest #{url} failed: #{error.message}" }
47+
return [nil, "SSL certificate test failed: #{error.message}", cert]
48+
end
49+
end
50+
alias :test :test_url
51+
52+
53+
def test_cert client_cert, ca_certs, open_timeout: 5, read_timeout: 5, proxy_host:nil, proxy_port: nil, redirection_limit: 5
54+
cert = failed_cert_reason = chain = store = nil
55+
56+
store = OpenSSL::X509::Store.new
57+
ca_certs.each { store.add_cert(_1) }
58+
store.verify_callback = -> (verify_ok, store_context) {
59+
cert = store_context.current_cert
60+
chain = store_context.chain
61+
failed_cert_reason = [store_context.error, store_context.error_string] if store_context.error != 0
62+
verify_ok
63+
}
64+
65+
begin
66+
store.verify(client_cert)
67+
68+
if failed_cert_reason
69+
error_message = "error code #{failed_cert_reason[0]}: #{failed_cert_reason[1]}"
70+
@logger&.info { "SSLTest #{cert.subject.to_s} finished: #{error_message}" }
71+
return [false, error_message, cert]
72+
else
73+
revoked, message, revocation_date = test_chain_revocation(chain, open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit)
74+
return [!revoked, revocation_message(revoked, revocation_date, message), cert]
4975
end
50-
@logger&.info { "SSLTest #{url} finished: #{error}" }
51-
return [false, error, cert]
52-
rescue => e
53-
@logger&.error { "SSLTest #{url} failed: #{e.message}" }
54-
return [nil, "SSL certificate test failed: #{e.message}", cert]
76+
rescue => error
77+
@logger&.error { "SSLTest #{cert.subject.to_s} failed: #{error.message}" }
78+
return [nil, "SSL certificate test failed: #{error.message}", cert]
5579
end
5680
end
5781

@@ -81,6 +105,28 @@ def logger= logger
81105

82106
private
83107

108+
def revocation_message(revoked, revocation_date, message)
109+
if revoked
110+
"SSL certificate revoked: #{message} (revocation date: #{revocation_date})"
111+
elsif message
112+
"Revocation test couldn't be performed: #{message}"
113+
end
114+
end
115+
116+
def parse_ssl_error(error, cert, failed_cert_reason, uri:)
117+
message = error.message
118+
message = "error code %d: %s" % failed_cert_reason if failed_cert_reason
119+
if message =~ /certificate verify failed/
120+
domains = cert_domains(cert)
121+
if !uri.nil? && matching_domains(domains, uri.host).none?
122+
message = "hostname \"#{uri.host}\" does not match the server certificate (#{domains.join(', ')})"
123+
end
124+
end
125+
126+
message
127+
end
128+
129+
84130
# https://docs.ruby-lang.org/en/2.2.0/OpenSSL/OCSP.html
85131
# https://stackoverflow.com/questions/16244084/how-to-programmatically-check-if-a-certificate-has-been-revoked#answer-16257470
86132
# Returns an array with [certificate_revoked?, error_reason, revocation_date]

lib/ssl-test/crl.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_crl_revocation cert, issuer:, chain:, **options
5151
end
5252

5353
# Returns an array with [response, error_message]
54-
def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limit: 5)
54+
def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limit: 5, proxy_host: nil, proxy_port: nil)
5555
return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0
5656

5757
# Return file from cache if not expired
@@ -61,7 +61,7 @@ def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limi
6161

6262
@logger&.debug { "SSLTest + CRL: fetch URI #{uri}" }
6363
path = uri.path == "" ? "/" : uri.path
64-
http = Net::HTTP.new(uri.hostname, uri.port)
64+
http = Net::HTTP.new(uri.hostname, uri.port, proxy_host, proxy_port)
6565
http.open_timeout = open_timeout
6666
http.read_timeout = read_timeout
6767

@@ -92,7 +92,7 @@ def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limi
9292
}
9393
[http_response.body, nil]
9494
when Net::HTTPRedirection
95-
follow_crl_redirects(URI(http_response["location"]), open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1)
95+
follow_crl_redirects(URI(http_response["location"]), open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit - 1)
9696
else
9797
@logger&.debug { "SSLTest + CRL: Error: #{http_response.class}" }
9898
[nil, "Wrong response type (#{http_response.class})"]

lib/ssl-test/ocsp.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ def test_ocsp_revocation cert, issuer:, chain:, **options
6767
end
6868

6969
# Returns an array with [response, error_message]
70-
def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirection_limit: 5)
70+
def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirection_limit: 5, proxy_host: nil, proxy_port: nil)
7171
return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0
7272

7373
@logger&.debug { "SSLTest + OCSP: fetch URI #{uri}" }
7474
path = uri.path == "" ? "/" : uri.path
75-
http = Net::HTTP.new(uri.hostname, uri.port)
75+
http = Net::HTTP.new(uri.hostname, uri.port, proxy_host, proxy_port)
7676
http.open_timeout = open_timeout
7777
http.read_timeout = read_timeout
7878

@@ -82,7 +82,7 @@ def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirecti
8282
@logger&.debug { "SSLTest + OCSP: 200 OK (#{http_response.body.bytesize} bytes)" }
8383
[http_response.body, nil]
8484
when Net::HTTPRedirection
85-
follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1)
85+
follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit - 1)
8686
else
8787
@logger&.debug { "SSLTest + OCSP: Error: #{http_response.class}" }
8888
[nil, "Wrong response type (#{http_response.class})"]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIG7jCCBdagAwIBAgIQBz2KfzHX7LJ6+D64tWXIFTANBgkqhkiG9w0BAQsFADBE
3+
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMR4wHAYDVQQDExVE
4+
aWdpQ2VydCBFViBSU0EgQ0EgRzIwHhcNMjUxMTAzMDAwMDAwWhcNMjUxMjE5MjM1
5+
OTU5WjCBwTETMBEGCysGAQQBgjc8AgEDEwJVUzEVMBMGCysGAQQBgjc8AgECEwRV
6+
dGFoMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjEVMBMGA1UEBRMMNTI5
7+
OTUzNy0wMTQyMQswCQYDVQQGEwJVUzENMAsGA1UECBMEVXRhaDENMAsGA1UEBxME
8+
TGVoaTEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xGTAXBgNVBAMTEHd3dy5kaWdp
9+
Y2VydC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoFf4dPzKL
10+
K3KtscZ8hWhinM8VizoyYg6qF7zpGIphVHZtiuMowkKcHN8zk+Te9o3BFDnQYU6L
11+
LSvI3JqcCxe8G7xOLxHG94ZDwjhhXUo/CxcHgNgyuSx1+OC5bXISTibb7jF2YU8u
12+
9rh4Vuyn13JNqU4HqrdwqEVZiW7rlC5yPO5sKadgjIu7CjjLsSYfen7uSfM0Mc4i
13+
qs8TNqD2jQViefdIvmQGVqyJf+Fk12LlW2TdUeh89aEOagK+ZhSJx2bipeyj0eBB
14+
ybJ8Q6kd4XLIPpx1QOV7yy755vLdfedllgnCv9C0BdHQF90SS+oADvyefXNNOTqT
15+
fe+XwDdfkATTAgMBAAGjggNcMIIDWDAfBgNVHSMEGDAWgBRqTlC/mGidW3sgddRZ
16+
AXlIZpIyBjAdBgNVHQ4EFgQUGd8hbnJtfOowjka/Sg7kOYi2oeEwKQYDVR0RBCIw
17+
IIIQd3d3LmRpZ2ljZXJ0LmNvbYIMZGlnaWNlcnQuY29tMEoGA1UdIARDMEEwCwYJ
18+
YIZIAYb9bAIBMDIGBWeBDAEBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGln
19+
aWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH
20+
AwEwdQYDVR0fBG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
21+
Z2lDZXJ0RVZSU0FDQUcyLmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQu
22+
Y29tL0RpZ2lDZXJ0RVZSU0FDQUcyLmNybDBzBggrBgEFBQcBAQRnMGUwJAYIKwYB
23+
BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTA9BggrBgEFBQcwAoYxaHR0
24+
cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0RVZSU0FDQUcyLmNydDAM
25+
BgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgDd3Mo0ldfh
26+
FgXnlTL6x5/4PRxQ39sAOhQSdgosrLvIKgAAAZpIkZnpAAAEAwBHMEUCIC8jE0Ey
27+
0Q7xGE3PenZUJKcj+18nm0myb27cIyHx2jsVAiEA+YQXhHCbJc17AXmWgc63Z7RT
28+
PvhE/Fq8k53T2H6SirYAdwDtPEvW6AbCpKIAV9vLJOI4Ad9RL+3EhsVwDyDdtz4/
29+
4AAAAZpIkZnnAAAEAwBIMEYCIQD+lFI/hO60oJOUndldghaqClo9/dy0O6FWaP06
30+
Mn619QIhAMUSTSyO09wXaoiCUGrEZLlPvU1a3woB7Ja63sh1aUkbAHUApELFBklg
31+
YVSPD9TqnPt6LSZFTYepfy/fRVn2J086hFQAAAGaSJGZ+AAABAMARjBEAiBWGFi2
32+
0F9ZZMzWcCcdmVpEz5y5T7cQ91z1DojVjc8Y4AIgGVU0KD/MTHi8b0nZb6B4uiD8
33+
k97tErH3VPd1N5CiMPcwDQYJKoZIhvcNAQELBQADggEBADwGDULnh6YXMyl5Zylo
34+
su7Bzw6lLG6RVYUtkJuiWeDfCCxaXkzUYIA/bsAQFBrWTQmyxBm6vIh/eNgGYUtO
35+
05uFrRbijre0+DiF1QTtfg9lBPtXVp4GwpB3om7C283TQvlDczpyPKkVtYrvsp0L
36+
VUyt7LDPgaR69+ieVMQIn4pH/vWNGA8xlrL1jqv3W9RnGc1w1X/ceCshQD+VcNDe
37+
7xm0tesmOzuFwJbEetuDLWfXHUs2UWkbGyS8XMt76mo6rsesex0GjO2VsTkrV5Fg
38+
xdboAzxOYE7ASn/LTbdtGQBuKyZHS1wH6g2q8vr9INk2rj+XvTCPe6DSffRrKdMf
39+
LbM=
40+
-----END CERTIFICATE-----
41+
42+
-----BEGIN CERTIFICATE-----
43+
MIIFPDCCBCSgAwIBAgIQAWePH++IIlXYsKcOa3uyIDANBgkqhkiG9w0BAQsFADBh
44+
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
45+
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
46+
MjAeFw0yMDA3MDIxMjQyNTBaFw0zMDA3MDIxMjQyNTBaMEQxCzAJBgNVBAYTAlVT
47+
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxHjAcBgNVBAMTFURpZ2lDZXJ0IEVWIFJT
48+
QSBDQSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0eZsx/neTr
49+
f4MXJz0R2fJTIDfN8AwUAu7hy4gI0vp7O8LAAHx2h3bbf8wl+pGMSxaJK9ffDDCD
50+
63FqqFBqE9eTmo3RkgQhlu55a04LsXRLcK6crkBOO0djdonybmhrfGrtBqYvbRat
51+
xenkv0Sg4frhRl4wYh4dnW0LOVRGhbt1G5Q19zm9CqMlq7LlUdAE+6d3a5++ppfG
52+
cnWLmbEVEcLHPAnbl+/iKauQpQlU1Mi+wEBnjE5tK8Q778naXnF+DsedQJ7NEi+b
53+
QoonTHEz9ryeEcUHuQTv7nApa/zCqes5lXn1pMs4LZJ3SVgbkTLj+RbBov/uiwTX
54+
tkBEWawvZH8CAwEAAaOCAgswggIHMB0GA1UdDgQWBBRqTlC/mGidW3sgddRZAXlI
55+
ZpIyBjAfBgNVHSMEGDAWgBROIlQgGJXm427mD/r6uRLtBhePOTAOBgNVHQ8BAf8E
56+
BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQI
57+
MAYBAf8CAQAwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
58+
cC5kaWdpY2VydC5jb20wewYDVR0fBHQwcjA3oDWgM4YxaHR0cDovL2NybDMuZGln
59+
aWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA3oDWgM4YxaHR0cDov
60+
L2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDCBzgYD
61+
VR0gBIHGMIHDMIHABgRVHSAAMIG3MCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5k
62+
aWdpY2VydC5jb20vQ1BTMIGKBggrBgEFBQcCAjB+DHxBbnkgdXNlIG9mIHRoaXMg
63+
Q2VydGlmaWNhdGUgY29uc3RpdHV0ZXMgYWNjZXB0YW5jZSBvZiB0aGUgUmVseWlu
64+
ZyBQYXJ0eSBBZ3JlZW1lbnQgbG9jYXRlZCBhdCBodHRwczovL3d3dy5kaWdpY2Vy
65+
dC5jb20vcnBhLXVhMA0GCSqGSIb3DQEBCwUAA4IBAQBSMgrCdY2+O9spnYNvwHiG
66+
+9lCJbyELR0UsoLwpzGpSdkHD7pVDDFJm3//B8Es+17T1o5Hat+HRDsvRr7d3MEy
67+
o9iXkkxLhKEgApA2Ft2eZfPrTolc95PwSWnn3FZ8BhdGO4brTA4+zkPSKoMXi/X+
68+
WLBNN29Z/nbCS7H/qLGt7gViEvTIdU8x+H4l/XigZMUDaVmJ+B5d7cwSK7yOoQdf
69+
oIBGmA5Mp4LhMzo52rf//kXPfE3wYIZVHqVuxxlnTkFYmffCX9/Lon7SWaGdg6Rc
70+
k4RHhHLWtmz2lTZ5CEo2ljDsGzCFGJP7oT4q6Q8oFC38irvdKIJ95cUxYzj4tnOI
71+
-----END CERTIFICATE-----
72+
73+
-----BEGIN CERTIFICATE-----
74+
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
75+
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
76+
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
77+
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
78+
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
79+
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
80+
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
81+
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
82+
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
83+
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
84+
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
85+
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
86+
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
87+
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
88+
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
89+
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
90+
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
91+
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
92+
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
93+
MrY=
94+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)