Skip to content

Commit 6d5b550

Browse files
authored
Restores the TLS tests in CI (#267)
Renews test certificates Recovers and refactors test_conn_tls.cpp Adds a test for TLS reconnection
1 parent 4a2085c commit 6d5b550

File tree

9 files changed

+239
-182
lines changed

9 files changed

+239
-182
lines changed

test/CMakeLists.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ make_test(test_any_adapter)
4040

4141
# Tests that require a real Redis server
4242
make_test(test_conn_quit)
43-
# TODO: Configure a Redis server with TLS in the CI and reenable this test.
44-
#make_test(test_conn_tls)
43+
make_test(test_conn_tls)
4544
make_test(test_conn_exec_retry)
4645
make_test(test_conn_exec_error)
4746
make_test(test_run)

test/test_conn_tls.cpp

Lines changed: 115 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,49 @@
55
*/
66

77
#include <boost/redis/connection.hpp>
8+
#include <boost/redis/ignore.hpp>
89

910
#include <boost/asio/ssl/host_name_verification.hpp>
11+
#include <boost/asio/steady_timer.hpp>
1012
#include <boost/system/error_code.hpp>
13+
14+
#include <cstddef>
15+
#include <string_view>
1116
#define BOOST_TEST_MODULE conn_tls
1217
#include <boost/test/included/unit_test.hpp>
1318

1419
#include "common.hpp"
1520

1621
namespace net = boost::asio;
17-
18-
using connection = boost::redis::connection;
19-
using boost::redis::request;
20-
using boost::redis::response;
21-
using boost::redis::config;
22+
using namespace boost::redis;
23+
using namespace std::chrono_literals;
2224
using boost::system::error_code;
2325

26+
namespace {
27+
2428
// CA certificate that signed the test server's certificate.
2529
// This is a self-signed CA created for testing purposes.
2630
// This must match tools/tls/ca.crt contents
2731
static constexpr const char* ca_certificate = R"%(-----BEGIN CERTIFICATE-----
28-
MIIFSzCCAzOgAwIBAgIUNd7VUuGK4+ylzCOrmeckg2+TqX8wDQYJKoZIhvcNAQEL
29-
BQAwNTETMBEGA1UECgwKUmVkaXMgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUg
30-
QXV0aG9yaXR5MB4XDTI0MDMzMTE0MjUyM1oXDTM0MDMyOTE0MjUyM1owNTETMBEG
31-
A1UECgwKUmVkaXMgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5
32-
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5AMV5V66wt+MM4+oCzH0
33-
xPi++j23p8AOa0o3dxNd4tm5y++gAdKfoxj7oh32ZuYHA5V+sGNEalN/b3GlKXMm
34-
ThdVPSwqOQduny19wrb126ZeQXCfqwgSZQ+rgzaIYpw8/GRRuLDunmsdaR2eiptp
35-
dbv6g6P/aIF6P9mfuekwCC9KBCV6ftqOEnzulNLVw4JjY0rKB9NZqONKVMfWpNyC
36-
zJLCkGmza7BOpybhloZIxGJz033yCjDvIQr9GUWsA5rU9LdUiL+F1W0pWkIel1qo
37-
Evo0EIl3+EOcSSzETI7NPHgnSzNau39ZShV4UBj2lw0DWeNcobeMBQ8ItmqEU6V0
38-
gCEqfUnt10bGIDdmV3D5FKPgvhFvEjQULnblLeLDQ6XDFf+xbGEVjvTzVkLjvyKm
39-
H2D+SKw2O+eDU/0+xhpAf+QsWlm6pmvKWjXI5wK1rh2yssBK2pmY3LuuZCdGrvXb
40-
KX4j/4S9qMr43Hmyoyz0gE5I5rplqot8TvT9O/JsgQYd9fYSvdB+HbqAlJzpBZFl
41-
xbVBXxl0AlDFwQtNMX5ylEQPvYVDKA1M+DTqRTgQKctTfccwvovY3YMV7m5YoODZ
42-
ya2YSBRfQim6VsC+QPYs7p2dk1larIoMMaTaU02oMY+qT2d/eyhWKBv5W9LuowTQ
43-
bWa3ZhWN8lXriPgJOQnZ6iUCAwEAAaNTMFEwHQYDVR0OBBYEFCpEPlClLrgu1zFN
44-
Fmas5G4ybNRJMB8GA1UdIwQYMBaAFCpEPlClLrgu1zFNFmas5G4ybNRJMA8GA1Ud
45-
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFLl1NZHp0NT5Av4GKmsJFeI
46-
cJOgcIygjR4SBGDAxyPqVpZk0x1q64gJsfOe1ARyI4olQPqO08FZMeB+VBYuqR3S
47-
fEVQZz2FT5U7IVAEZwWHOcWkrrVpEZC6PZktYJ7Yqju6+ic93inoPrHhGNZ5XA/Y
48-
GSfwriWkyWm2SOk35ChFH67MbPWmve8CRAXRmrOCByXwXF87wdqVYZUvH9xDe6WU
49-
snFWXVHr2NA7Re8ZIGp7yJOwwW+CZagepNCPUDwnI0fWOahtOTzonIjq8bfgTZPx
50-
2e7lBuAr9tVMpoeyUytVOlNJDojZAtKOpfMwhAG8ydhk+78aK07VVbnSYVhv7ctU
51-
kkkldqP/S3lBlWo44oOxenwLc9vDQNh64py7eQTD7Qv+TjqAG0ljHIDbVqlkQsgR
52-
pQsu7keG9O1xASSTLZVZN2/alNewpqE/eFRfPM3mtUiTiIZvSxiQnWQMbKofAZH5
53-
HwhVli4RKWRWPqpof4GFNkB8XwfBE+gdlFuWtyg0oRyV3sJ6Zn7E+lUpbQX4CFx3
54-
97vekaFNBchNYMcP3TZ9LwxTx1xOWZ5HHrHyzASG3uz2rqwAsEmdRbmK03KfEQyQ
55-
YpNY718btZ1D6lLino9VMgzaPhUs79bHC64O4ncl7hRclK9qa3KLQdCG1cbIR7G0
56-
2XVYrfsnPHX0CsPDIy7L
32+
MIIDhzCCAm+gAwIBAgIUZGttu4o/Exs08EHCneeD3gHw7KkwDQYJKoZIhvcNAQEL
33+
BQAwUjELMAkGA1UEBhMCRVMxGjAYBgNVBAoMEUJvb3N0LlJlZGlzIENJIENBMQsw
34+
CQYDVQQLDAJJVDEaMBgGA1UEAwwRYm9vc3QtcmVkaXMtY2ktY2EwIBcNMjUwNjA3
35+
MTI0NzUwWhgPMjA4MDAzMTAxMjQ3NTBaMFIxCzAJBgNVBAYTAkVTMRowGAYDVQQK
36+
DBFCb29zdC5SZWRpcyBDSSBDQTELMAkGA1UECwwCSVQxGjAYBgNVBAMMEWJvb3N0
37+
LXJlZGlzLWNpLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7XV
38+
sOoHB2J/5VtyJmMOzxhBbHKyQgW1YnMvYIb1JqIm7VuICA831SUw76n3j8mIK3zz
39+
FfK2eYyUWf4Uo2j3uxmXDyjujqzIaUJNLcB53CQXkmIbqDigNhzUTPZ5A2MQ7xT+
40+
t1eDbjsZ7XIM+aTShgtrpyxiccsgPJ3/XXme2RrqKeNvYsTYY6pquWZdyLOg/LOH
41+
IeSJyL1/eQDRu/GsZjnR8UOE6uHfbjrLWls7Tifj/1IueVYCEhQZpJSWS8aUMLBZ
42+
fi+t9YMCCK4DGy+6QlznGgVqdFFbTUt2C7tzqz+iF5dxJ8ogKMUPEeFrWiZpozoS
43+
t60jV8fKwdXz854jLQIDAQABo1MwUTAdBgNVHQ4EFgQU2SoWvvZUW8JiDXtyuXZK
44+
deaYYBswHwYDVR0jBBgwFoAU2SoWvvZUW8JiDXtyuXZKdeaYYBswDwYDVR0TAQH/
45+
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAqY4hGcdCFFPL4zveSDhR9H/akjae
46+
uXbpo/9sHZd8e3Y4BtD8K05xa3417H9u5+S2XtyLQg5MON6J2LZueQEtE3wiR3ja
47+
QIWbizqp8W54O5hTLQs6U/mWggfuL2R/HUw7ab4M8JobwHNEMK/WKZW71z0So/kk
48+
W3wC0+1RH2PjMOZrCIflsD7EXYKIIr9afypAbhCQmCfu/GELuNx+LmaPi5JP4TTE
49+
tDdhzWL04JLcZnA0uXb2Mren1AR9yKYH2I5tg5kQ3Bn/6v9+JiUhiejP3Vcbw84D
50+
yFwRzN54bLanrJNILJhHPwnNIABXOtGUV05SZbYazJpiMst1a6eqDZhv/Q==
5751
-----END CERTIFICATE-----)%";
5852

5953
static config make_tls_config()
@@ -65,13 +59,14 @@ static config make_tls_config()
6559
return cfg;
6660
}
6761

68-
BOOST_AUTO_TEST_CASE(ping_internal_ssl_context)
62+
// Using the default TLS context allows establishing TLS connections and execute requests
63+
BOOST_AUTO_TEST_CASE(exec_default_ssl_context)
6964
{
7065
auto const cfg = make_tls_config();
71-
std::string const in = "Kabuf";
66+
constexpr std::string_view ping_value = "Kabuf";
7267

7368
request req;
74-
req.push("PING", in);
69+
req.push("PING", ping_value);
7570

7671
response<std::string> resp;
7772

@@ -82,30 +77,39 @@ BOOST_AUTO_TEST_CASE(ping_internal_ssl_context)
8277
// that is not trusted by default - skip verification.
8378
conn.next_layer().set_verify_mode(net::ssl::verify_none);
8479

85-
conn.async_exec(req, resp, [&](error_code ec, auto) {
86-
BOOST_TEST(ec == std::error_code());
80+
bool exec_finished = false, run_finished = false;
81+
82+
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
83+
exec_finished = true;
84+
BOOST_TEST(ec == error_code());
8785
conn.cancel();
8886
});
8987

90-
conn.async_run(cfg, {}, [](auto) { });
88+
conn.async_run(cfg, {}, [&](error_code ec) {
89+
run_finished = true;
90+
BOOST_TEST(ec == net::error::operation_aborted);
91+
});
9192

92-
ioc.run();
93+
ioc.run_for(test_timeout);
9394

94-
BOOST_CHECK_EQUAL(in, std::get<0>(resp).value());
95+
BOOST_TEST(exec_finished);
96+
BOOST_TEST(run_finished);
97+
BOOST_TEST(std::get<0>(resp).value() == ping_value);
9598
}
9699

97-
BOOST_AUTO_TEST_CASE(ping_custom_ssl_context)
100+
// Users can pass a custom context with TLS config
101+
BOOST_AUTO_TEST_CASE(exec_custom_ssl_context)
98102
{
99103
auto const cfg = make_tls_config();
100-
std::string const in = "Kabuf";
104+
constexpr std::string_view ping_value = "Kabuf";
101105

102106
request req;
103-
req.push("PING", in);
107+
req.push("PING", ping_value);
104108

105109
response<std::string> resp;
106110

107111
net::io_context ioc;
108-
net::ssl::context ctx{boost::asio::ssl::context::tls_client};
112+
net::ssl::context ctx{net::ssl::context::tls_client};
109113

110114
// Configure the SSL context to trust the CA that signed the server's certificate.
111115
// The test certificate uses "redis" as its common name, regardless of the actual server's hostname
@@ -115,14 +119,74 @@ BOOST_AUTO_TEST_CASE(ping_custom_ssl_context)
115119

116120
connection conn{ioc, std::move(ctx)};
117121

118-
conn.async_exec(req, resp, [&](auto ec, auto) {
119-
BOOST_TEST(ec == std::error_code());
122+
bool exec_finished = false, run_finished = false;
123+
124+
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
125+
exec_finished = true;
126+
BOOST_TEST(ec == error_code());
120127
conn.cancel();
121128
});
122129

123-
conn.async_run(cfg, {}, [](auto) { });
130+
conn.async_run(cfg, {}, [&](error_code ec) {
131+
run_finished = true;
132+
BOOST_TEST(ec == net::error::operation_aborted);
133+
});
134+
135+
ioc.run_for(test_timeout);
136+
137+
BOOST_TEST(exec_finished);
138+
BOOST_TEST(run_finished);
139+
BOOST_TEST(std::get<0>(resp).value() == ping_value);
140+
}
141+
142+
// After an error, a TLS connection can recover.
143+
// Force an error using QUIT, then issue a regular request to verify that we could reconnect
144+
BOOST_AUTO_TEST_CASE(reconnection)
145+
{
146+
// Setup
147+
net::io_context ioc;
148+
net::steady_timer timer{ioc};
149+
connection conn{ioc};
150+
auto const cfg = make_tls_config();
151+
152+
request ping_request;
153+
ping_request.push("PING", "some_value");
154+
155+
request quit_request;
156+
quit_request.push("QUIT");
157+
158+
bool exec_finished = false, run_finished = false;
159+
160+
// Run the connection
161+
conn.async_run(cfg, {}, [&](error_code ec) {
162+
run_finished = true;
163+
BOOST_TEST(ec == net::error::operation_aborted);
164+
});
165+
166+
// The PING is the end of the callback chain
167+
auto ping_callback = [&](error_code ec, std::size_t) {
168+
exec_finished = true;
169+
BOOST_TEST(ec == error_code());
170+
conn.cancel();
171+
};
172+
173+
auto quit_callback = [&](error_code ec, std::size_t) {
174+
BOOST_TEST(ec == error_code());
124175

125-
ioc.run();
176+
// If a request is issued immediately after QUIT, the request sometimes
177+
// fails, probably due to a race condition. This dispatches any pending
178+
// handlers, triggering the reconnection process.
179+
// TODO: this should not be required.
180+
ioc.poll();
181+
conn.async_exec(ping_request, ignore, ping_callback);
182+
};
126183

127-
BOOST_CHECK_EQUAL(in, std::get<0>(resp).value());
184+
conn.async_exec(quit_request, ignore, quit_callback);
185+
186+
ioc.run_for(test_timeout);
187+
188+
BOOST_TEST(exec_finished);
189+
BOOST_TEST(run_finished);
128190
}
191+
192+
} // namespace

tools/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ services:
55
"redis-server",
66
"--tls-port", "6380",
77
"--tls-cert-file", "/tls/server.crt",
8-
"--tls-key-file", "/tls/server-key.key",
8+
"--tls-key-file", "/tls/server.key",
99
"--tls-ca-cert-file", "/tls/ca.crt",
1010
"--tls-auth-clients", "no",
1111
]

tools/gen-certificates.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025 Marcelo Zimbres Silva ([email protected]),
3+
# Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
4+
#
5+
# Distributed under the Boost Software License, Version 1.0. (See
6+
# accompanying file LICENSE.txt)
7+
#
8+
9+
# Generates the ca and certificates used for CI testing.
10+
# Run this in the directory where you want the certificates to be generated.
11+
12+
set -e
13+
14+
# CA private key
15+
openssl genpkey -algorithm RSA -out ca.key -pkeyopt rsa_keygen_bits:2048
16+
17+
# CA certificate
18+
openssl req -x509 -new -nodes -key ca.key -sha256 -days 20000 -out ca.crt \
19+
-subj '/C=ES/O=Boost.Redis CI CA/OU=IT/CN=boost-redis-ci-ca'
20+
21+
# Server private key
22+
openssl genpkey -algorithm RSA -out server.key -pkeyopt rsa_keygen_bits:2048
23+
24+
# Server certificate
25+
openssl req -new -key server.key -out server.csr \
26+
-subj '/C=ES/O=Boost.Redis CI CA/OU=IT/CN=redis'
27+
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
28+
-out server.crt -days 20000 -sha256
29+
rm server.csr
30+
rm ca.srl

tools/tls/ca.crt

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,21 @@
11
-----BEGIN CERTIFICATE-----
2-
MIIFSzCCAzOgAwIBAgIUNd7VUuGK4+ylzCOrmeckg2+TqX8wDQYJKoZIhvcNAQEL
3-
BQAwNTETMBEGA1UECgwKUmVkaXMgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUg
4-
QXV0aG9yaXR5MB4XDTI0MDMzMTE0MjUyM1oXDTM0MDMyOTE0MjUyM1owNTETMBEG
5-
A1UECgwKUmVkaXMgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5
6-
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5AMV5V66wt+MM4+oCzH0
7-
xPi++j23p8AOa0o3dxNd4tm5y++gAdKfoxj7oh32ZuYHA5V+sGNEalN/b3GlKXMm
8-
ThdVPSwqOQduny19wrb126ZeQXCfqwgSZQ+rgzaIYpw8/GRRuLDunmsdaR2eiptp
9-
dbv6g6P/aIF6P9mfuekwCC9KBCV6ftqOEnzulNLVw4JjY0rKB9NZqONKVMfWpNyC
10-
zJLCkGmza7BOpybhloZIxGJz033yCjDvIQr9GUWsA5rU9LdUiL+F1W0pWkIel1qo
11-
Evo0EIl3+EOcSSzETI7NPHgnSzNau39ZShV4UBj2lw0DWeNcobeMBQ8ItmqEU6V0
12-
gCEqfUnt10bGIDdmV3D5FKPgvhFvEjQULnblLeLDQ6XDFf+xbGEVjvTzVkLjvyKm
13-
H2D+SKw2O+eDU/0+xhpAf+QsWlm6pmvKWjXI5wK1rh2yssBK2pmY3LuuZCdGrvXb
14-
KX4j/4S9qMr43Hmyoyz0gE5I5rplqot8TvT9O/JsgQYd9fYSvdB+HbqAlJzpBZFl
15-
xbVBXxl0AlDFwQtNMX5ylEQPvYVDKA1M+DTqRTgQKctTfccwvovY3YMV7m5YoODZ
16-
ya2YSBRfQim6VsC+QPYs7p2dk1larIoMMaTaU02oMY+qT2d/eyhWKBv5W9LuowTQ
17-
bWa3ZhWN8lXriPgJOQnZ6iUCAwEAAaNTMFEwHQYDVR0OBBYEFCpEPlClLrgu1zFN
18-
Fmas5G4ybNRJMB8GA1UdIwQYMBaAFCpEPlClLrgu1zFNFmas5G4ybNRJMA8GA1Ud
19-
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFLl1NZHp0NT5Av4GKmsJFeI
20-
cJOgcIygjR4SBGDAxyPqVpZk0x1q64gJsfOe1ARyI4olQPqO08FZMeB+VBYuqR3S
21-
fEVQZz2FT5U7IVAEZwWHOcWkrrVpEZC6PZktYJ7Yqju6+ic93inoPrHhGNZ5XA/Y
22-
GSfwriWkyWm2SOk35ChFH67MbPWmve8CRAXRmrOCByXwXF87wdqVYZUvH9xDe6WU
23-
snFWXVHr2NA7Re8ZIGp7yJOwwW+CZagepNCPUDwnI0fWOahtOTzonIjq8bfgTZPx
24-
2e7lBuAr9tVMpoeyUytVOlNJDojZAtKOpfMwhAG8ydhk+78aK07VVbnSYVhv7ctU
25-
kkkldqP/S3lBlWo44oOxenwLc9vDQNh64py7eQTD7Qv+TjqAG0ljHIDbVqlkQsgR
26-
pQsu7keG9O1xASSTLZVZN2/alNewpqE/eFRfPM3mtUiTiIZvSxiQnWQMbKofAZH5
27-
HwhVli4RKWRWPqpof4GFNkB8XwfBE+gdlFuWtyg0oRyV3sJ6Zn7E+lUpbQX4CFx3
28-
97vekaFNBchNYMcP3TZ9LwxTx1xOWZ5HHrHyzASG3uz2rqwAsEmdRbmK03KfEQyQ
29-
YpNY718btZ1D6lLino9VMgzaPhUs79bHC64O4ncl7hRclK9qa3KLQdCG1cbIR7G0
30-
2XVYrfsnPHX0CsPDIy7L
2+
MIIDhzCCAm+gAwIBAgIUZGttu4o/Exs08EHCneeD3gHw7KkwDQYJKoZIhvcNAQEL
3+
BQAwUjELMAkGA1UEBhMCRVMxGjAYBgNVBAoMEUJvb3N0LlJlZGlzIENJIENBMQsw
4+
CQYDVQQLDAJJVDEaMBgGA1UEAwwRYm9vc3QtcmVkaXMtY2ktY2EwIBcNMjUwNjA3
5+
MTI0NzUwWhgPMjA4MDAzMTAxMjQ3NTBaMFIxCzAJBgNVBAYTAkVTMRowGAYDVQQK
6+
DBFCb29zdC5SZWRpcyBDSSBDQTELMAkGA1UECwwCSVQxGjAYBgNVBAMMEWJvb3N0
7+
LXJlZGlzLWNpLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7XV
8+
sOoHB2J/5VtyJmMOzxhBbHKyQgW1YnMvYIb1JqIm7VuICA831SUw76n3j8mIK3zz
9+
FfK2eYyUWf4Uo2j3uxmXDyjujqzIaUJNLcB53CQXkmIbqDigNhzUTPZ5A2MQ7xT+
10+
t1eDbjsZ7XIM+aTShgtrpyxiccsgPJ3/XXme2RrqKeNvYsTYY6pquWZdyLOg/LOH
11+
IeSJyL1/eQDRu/GsZjnR8UOE6uHfbjrLWls7Tifj/1IueVYCEhQZpJSWS8aUMLBZ
12+
fi+t9YMCCK4DGy+6QlznGgVqdFFbTUt2C7tzqz+iF5dxJ8ogKMUPEeFrWiZpozoS
13+
t60jV8fKwdXz854jLQIDAQABo1MwUTAdBgNVHQ4EFgQU2SoWvvZUW8JiDXtyuXZK
14+
deaYYBswHwYDVR0jBBgwFoAU2SoWvvZUW8JiDXtyuXZKdeaYYBswDwYDVR0TAQH/
15+
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAqY4hGcdCFFPL4zveSDhR9H/akjae
16+
uXbpo/9sHZd8e3Y4BtD8K05xa3417H9u5+S2XtyLQg5MON6J2LZueQEtE3wiR3ja
17+
QIWbizqp8W54O5hTLQs6U/mWggfuL2R/HUw7ab4M8JobwHNEMK/WKZW71z0So/kk
18+
W3wC0+1RH2PjMOZrCIflsD7EXYKIIr9afypAbhCQmCfu/GELuNx+LmaPi5JP4TTE
19+
tDdhzWL04JLcZnA0uXb2Mren1AR9yKYH2I5tg5kQ3Bn/6v9+JiUhiejP3Vcbw84D
20+
yFwRzN54bLanrJNILJhHPwnNIABXOtGUV05SZbYazJpiMst1a6eqDZhv/Q==
3121
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)