HTTP binding: add TLS config options for client cert verification (#2399)
* adding client cert for client verification Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * logic for getting certs is provided as pem encoded strings Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * adding test for https endpoint Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * tls min version Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * adding tests Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * added more tests and some test refactoring Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * modify err check for file not exist Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> * review comments Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com> Signed-off-by: Pravin Pushkar <ppushkar@microsoft.com>
This commit is contained in:
parent
46652b69d2
commit
1ef514df3f
|
|
@ -16,10 +16,15 @@ package http
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -32,6 +37,13 @@ import (
|
|||
"github.com/dapr/kit/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
MTLSEnable = "MTLSEnable"
|
||||
MTLSRootCA = "MTLSRootCA"
|
||||
MTLSClientCert = "MTLSClientCert"
|
||||
MTLSClientKey = "MTLSClientKey"
|
||||
)
|
||||
|
||||
// HTTPSource is a binding for an http url endpoint invocation
|
||||
//
|
||||
//revive:disable-next-line
|
||||
|
|
@ -43,7 +55,10 @@ type HTTPSource struct {
|
|||
}
|
||||
|
||||
type httpMetadata struct {
|
||||
URL string `mapstructure:"url"`
|
||||
URL string `mapstructure:"url"`
|
||||
MTLSClientCert string `mapstructure:"mtlsClientCert"`
|
||||
MTLSClientKey string `mapstructure:"mtlsClientKey"`
|
||||
MTLSRootCA string `mapstructure:"mtlsRootCA"`
|
||||
}
|
||||
|
||||
// NewHTTP returns a new HTTPSource.
|
||||
|
|
@ -53,9 +68,17 @@ func NewHTTP(logger logger.Logger) bindings.OutputBinding {
|
|||
|
||||
// Init performs metadata parsing.
|
||||
func (h *HTTPSource) Init(metadata bindings.Metadata) error {
|
||||
if err := mapstructure.Decode(metadata.Properties, &h.metadata); err != nil {
|
||||
var err error
|
||||
if err = mapstructure.Decode(metadata.Properties, &h.metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
if h.metadata.MTLSClientCert != "" && h.metadata.MTLSClientKey != "" {
|
||||
tlsConfig, err = h.readMTLSCertificates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// See guidance on proper HTTP client settings here:
|
||||
// https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
|
||||
|
|
@ -66,6 +89,9 @@ func (h *HTTPSource) Init(metadata bindings.Metadata) error {
|
|||
Dial: dialer.Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
if tlsConfig != nil && len(tlsConfig.Certificates) > 0 && tlsConfig.RootCAs != nil {
|
||||
netTransport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
h.client = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: netTransport,
|
||||
|
|
@ -81,6 +107,66 @@ func (h *HTTPSource) Init(metadata bindings.Metadata) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// readMTLSCertificates reads the certificates and key from the metadata and returns a tls.Config.
|
||||
func (h *HTTPSource) readMTLSCertificates() (*tls.Config, error) {
|
||||
clientCertBytes, err := h.getPemBytes(MTLSClientCert, h.metadata.MTLSClientCert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientKeyBytes, err := h.getPemBytes(MTLSClientKey, h.metadata.MTLSClientKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(clientCertBytes, clientKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load client certificate: %w", err)
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
if h.metadata.MTLSRootCA != "" {
|
||||
caCertBytes, err := h.getPemBytes(MTLSRootCA, h.metadata.MTLSRootCA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
ok := caCertPool.AppendCertsFromPEM(caCertBytes)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to add root certificate to certpool")
|
||||
}
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// getPemBytes returns the PEM encoded bytes from the provided certName and certData.
|
||||
// If the certData is a file path, it reads the file and returns the bytes.
|
||||
// Else if the certData is a PEM encoded string, it returns the bytes.
|
||||
// Else it returns an error.
|
||||
func (h *HTTPSource) getPemBytes(certName, certData string) ([]byte, error) {
|
||||
// Read the file
|
||||
pemBytes, err := os.ReadFile(certData)
|
||||
// If there is an error assume it is already PEM encoded string not a file path.
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("failed to read %q file: %w", certName, err)
|
||||
}
|
||||
if !isValidPEM(certData) {
|
||||
return nil, fmt.Errorf("provided %q value is neither a valid file path or nor a valid pem encoded string", certName)
|
||||
}
|
||||
return []byte(certData), nil
|
||||
}
|
||||
return pemBytes, nil
|
||||
}
|
||||
|
||||
// isValidPEM validates the provided input has PEM formatted block.
|
||||
func isValidPEM(val string) bool {
|
||||
block, _ := pem.Decode([]byte(val))
|
||||
return block != nil
|
||||
}
|
||||
|
||||
// Operations returns the supported operations for this binding.
|
||||
func (h *HTTPSource) Operations() []bindings.OperationKind {
|
||||
return []bindings.OperationKind{
|
||||
|
|
|
|||
|
|
@ -15,9 +15,14 @@ package http_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -142,7 +147,160 @@ func TestDefaultBehavior(t *testing.T) {
|
|||
|
||||
hs, err := InitBinding(s, nil)
|
||||
require.NoError(t, err)
|
||||
verifyDefaultBehaviors(t, hs, handler)
|
||||
}
|
||||
|
||||
func TestNon2XXErrorsSuppressed(t *testing.T) {
|
||||
handler := NewHTTPHandler()
|
||||
s := httptest.NewServer(handler)
|
||||
defer s.Close()
|
||||
|
||||
hs, err := InitBinding(s, map[string]string{"errorIfNot2XX": "false"})
|
||||
require.NoError(t, err)
|
||||
verifyNon2XXErrorsSuppressed(t, hs, handler)
|
||||
}
|
||||
|
||||
func InitBindingForHTTPS(s *httptest.Server, extraProps map[string]string) (bindings.OutputBinding, error) {
|
||||
m := bindings.Metadata{Base: metadata.Base{
|
||||
Properties: map[string]string{
|
||||
"url": s.URL,
|
||||
},
|
||||
}}
|
||||
for k, v := range extraProps {
|
||||
m.Properties[k] = v
|
||||
}
|
||||
hs := bindingHttp.NewHTTP(logger.NewLogger("test"))
|
||||
err := hs.Init(m)
|
||||
return hs, err
|
||||
}
|
||||
|
||||
func httpsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// r.TLS gets ignored by HTTP handlers.
|
||||
// in case where client auth is not required, r.TLS.PeerCertificates will be empty.
|
||||
res := fmt.Sprintf("%v", len(r.TLS.PeerCertificates))
|
||||
io.WriteString(w, res)
|
||||
}
|
||||
|
||||
func TestDefaultBehaviorHTTPS(t *testing.T) {
|
||||
handler := NewHTTPHandler()
|
||||
server := setupHTTPSServer(t, true, handler)
|
||||
defer server.Close()
|
||||
|
||||
certMap := map[string]string{
|
||||
"MTLSRootCA": filepath.Join(".", "testdata", "ca.pem"),
|
||||
"MTLSClientCert": filepath.Join(".", "testdata", "client.pem"),
|
||||
"MTLSClientKey": filepath.Join(".", "testdata", "client.key"),
|
||||
}
|
||||
hs, err := InitBindingForHTTPS(server, certMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyDefaultBehaviors(t, hs, handler)
|
||||
}
|
||||
|
||||
func TestNon2XXErrorsSuppressedHTTPS(t *testing.T) {
|
||||
handler := NewHTTPHandler()
|
||||
server := setupHTTPSServer(t, true, handler)
|
||||
defer server.Close()
|
||||
|
||||
certMap := map[string]string{
|
||||
"MTLSRootCA": filepath.Join(".", "testdata", "ca.pem"),
|
||||
"MTLSClientCert": filepath.Join(".", "testdata", "client.pem"),
|
||||
"MTLSClientKey": filepath.Join(".", "testdata", "client.key"),
|
||||
"errorIfNot2XX": "false",
|
||||
}
|
||||
hs, err := InitBindingForHTTPS(server, certMap)
|
||||
require.NoError(t, err)
|
||||
verifyNon2XXErrorsSuppressed(t, hs, handler)
|
||||
}
|
||||
|
||||
func TestHTTPSBinding(t *testing.T) {
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/testhttps", httpsHandler)
|
||||
server := setupHTTPSServer(t, true, handler)
|
||||
defer server.Close()
|
||||
t.Run("get with https with valid client cert and clientAuthEnabled true", func(t *testing.T) {
|
||||
certMap := map[string]string{
|
||||
"MTLSRootCA": filepath.Join(".", "testdata", "ca.pem"),
|
||||
"MTLSClientCert": filepath.Join(".", "testdata", "client.pem"),
|
||||
"MTLSClientKey": filepath.Join(".", "testdata", "client.key"),
|
||||
}
|
||||
hs, err := InitBindingForHTTPS(server, certMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := TestCase{
|
||||
input: "GET",
|
||||
operation: "get",
|
||||
metadata: map[string]string{"path": "/testhttps"},
|
||||
path: "/testhttps",
|
||||
err: "",
|
||||
statusCode: 200,
|
||||
}.ToInvokeRequest()
|
||||
response, err := hs.Invoke(context.Background(), &req)
|
||||
assert.NoError(t, err)
|
||||
peerCerts, err := strconv.Atoi(string(response.Data))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, peerCerts > 0)
|
||||
|
||||
req = TestCase{
|
||||
input: "EXPECTED",
|
||||
operation: "post",
|
||||
metadata: map[string]string{"path": "/testhttps"},
|
||||
path: "/testhttps",
|
||||
err: "",
|
||||
statusCode: 201,
|
||||
}.ToInvokeRequest()
|
||||
response, err = hs.Invoke(context.Background(), &req)
|
||||
assert.NoError(t, err)
|
||||
peerCerts, err = strconv.Atoi(string(response.Data))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, peerCerts > 0)
|
||||
})
|
||||
t.Run("get with https with no client cert and clientAuthEnabled true", func(t *testing.T) {
|
||||
certMap := map[string]string{}
|
||||
hs, err := InitBindingForHTTPS(server, certMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := TestCase{
|
||||
input: "GET",
|
||||
operation: "get",
|
||||
metadata: map[string]string{"path": "/testhttps"},
|
||||
path: "/testhttps",
|
||||
err: "",
|
||||
statusCode: 200,
|
||||
}.ToInvokeRequest()
|
||||
_, err = hs.Invoke(context.Background(), &req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func setupHTTPSServer(t *testing.T, clientAuthEnabled bool, handler http.Handler) *httptest.Server {
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
caCertFile, err := os.ReadFile(filepath.Join(".", "testdata", "ca.pem"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCertFile)
|
||||
|
||||
serverCert := filepath.Join(".", "testdata", "server.pem")
|
||||
serverKey := filepath.Join(".", "testdata", "server.key")
|
||||
cert, err := tls.LoadX509KeyPair(serverCert, serverKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create the TLS Config with the CA pool and enable Client certificate validation
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
if clientAuthEnabled {
|
||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
server.TLS = tlsConfig
|
||||
server.StartTLS()
|
||||
return server
|
||||
}
|
||||
|
||||
func verifyDefaultBehaviors(t *testing.T, hs bindings.OutputBinding, handler *HTTPHandler) {
|
||||
tests := map[string]TestCase{
|
||||
"get": {
|
||||
input: "GET",
|
||||
|
|
@ -269,7 +427,7 @@ func TestDefaultBehavior(t *testing.T) {
|
|||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
req := tc.ToInvokeRequest()
|
||||
response, err := hs.Invoke(context.TODO(), &req)
|
||||
response, err := hs.Invoke(context.Background(), &req)
|
||||
if tc.err == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.path, handler.Path)
|
||||
|
|
@ -286,14 +444,7 @@ func TestDefaultBehavior(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNon2XXErrorsSuppressed(t *testing.T) {
|
||||
handler := NewHTTPHandler()
|
||||
s := httptest.NewServer(handler)
|
||||
defer s.Close()
|
||||
|
||||
hs, err := InitBinding(s, map[string]string{"errorIfNot2XX": "false"})
|
||||
require.NoError(t, err)
|
||||
|
||||
func verifyNon2XXErrorsSuppressed(t *testing.T, hs bindings.OutputBinding, handler *HTTPHandler) {
|
||||
tests := map[string]TestCase{
|
||||
"internal server error": {
|
||||
input: "internal server error",
|
||||
|
|
@ -348,7 +499,7 @@ func TestNon2XXErrorsSuppressed(t *testing.T) {
|
|||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
req := tc.ToInvokeRequest()
|
||||
response, err := hs.Invoke(context.TODO(), &req)
|
||||
response, err := hs.Invoke(context.Background(), &req)
|
||||
if tc.err == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.path, handler.Path)
|
||||
|
|
|
|||
|
|
@ -39,3 +39,18 @@ metadata:
|
|||
# If omitted, uses the same values as "<root>.binding"
|
||||
binding:
|
||||
output: true
|
||||
- name: MTLSRootCA
|
||||
required: false
|
||||
description: "The CA certificate used to generate the client certificate"
|
||||
binding:
|
||||
output: true
|
||||
- name: MTLSClientCert
|
||||
required: false
|
||||
description: "The client certificate to present to server to enable client verification"
|
||||
binding:
|
||||
output: true
|
||||
- name: MTLSClientKey
|
||||
required: false
|
||||
description: "The client certificate key to present to server to enable client verification"
|
||||
binding:
|
||||
output: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICHzCCAcWgAwIBAgIUVmulwhIO5Y2A8ACi3OcX94denFEwCgYIKoZIzj0EAwIw
|
||||
UDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlRTMQswCQYDVQQHDAJIWTETMBEGA1UE
|
||||
CgwKRGFwciBXb3JsZDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwNjEwMDI0
|
||||
NVoXDTI0MDEwNjEwMDI0NVowUDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlRTMQsw
|
||||
CQYDVQQHDAJIWTETMBEGA1UECgwKRGFwciBXb3JsZDESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE03ecfl+LFMzocftTCwpYrcJK
|
||||
YwutP6My8ZNfELp0IUqvMyTCgG3o08niA4GrUGaP73wyUFhO6UaswvFccBI92aN9
|
||||
MHswDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYI
|
||||
KwYBBQUHAwEGCCsGAQUFBwMCMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAd
|
||||
BgNVHQ4EFgQUmYArK7rKcVh57kzZXYMze2RiNrMwCgYIKoZIzj0EAwIDSAAwRQIg
|
||||
QtSMNNo+OWONVB4HfyV5UwjxyMxGmJwNt8qh99PWS2UCIQCyzZzBUOwNAHQv/T3D
|
||||
IsCxyWZQ37yZpvS82Z/SnCIZuw==
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIEobWR8PduGHOPY+T/Id/bNmjS8cjRQ+UID4NnCaROgmoAoGCCqGSM49
|
||||
AwEHoUQDQgAEKx7xYU7DGNX+WDo5QzJGkIvrqhqbw1ZaNKe5h2QREJwwIB2X5TyT
|
||||
DYaNfhqXc8IOB9si4GJ2x0JhyeoAvi/U8w==
|
||||
-----END EC PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICRjCCAeugAwIBAgIUKCvur/fviGKJDitDggTtIkwQMxIwCgYIKoZIzj0EAwIw
|
||||
UDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlRTMQswCQYDVQQHDAJIWTETMBEGA1UE
|
||||
CgwKRGFwciBXb3JsZDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwNjEwMDI0
|
||||
NVoXDTI0MDEwNjEwMDI0NVowUDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlRTMQsw
|
||||
CQYDVQQHDAJIWTETMBEGA1UECgwKRGFwciBXb3JsZDESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKx7xYU7DGNX+WDo5QzJGkIvr
|
||||
qhqbw1ZaNKe5h2QREJwwIB2X5TyTDYaNfhqXc8IOB9si4GJ2x0JhyeoAvi/U86OB
|
||||
ojCBnzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUE
|
||||
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/
|
||||
AAABMB0GA1UdDgQWBBRHcmSCVapIorD+/O1T+ySlkwEI+zAfBgNVHSMEGDAWgBSZ
|
||||
gCsruspxWHnuTNldgzN7ZGI2szAKBggqhkjOPQQDAgNJADBGAiEAh+c11VmTeiv3
|
||||
v4sRikMeiLkneDUl1wuLfffZbCLXsXkCIQC9bfk8RlH5aTTJ6Xrpz5oVyxU58v7E
|
||||
2HVuTU281m67bw==
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIBdj4qCqbLVvFqjcv14xthAm+YGghrMe6uHbE6nKg93EoAoGCCqGSM49
|
||||
AwEHoUQDQgAEJup13iQfS72fRxN9JLJDLP0tel4F8bmxEfcHjvKMGJaupvvtHgZm
|
||||
tlYlY6evzE6yIN5mUIcBqCWzpN+aCPBwqw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICRjCCAeugAwIBAgIUXpf5hT8jws5FV65ZFdVUITeldAYwCgYIKoZIzj0EAwIw
|
||||
UDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlRTMQswCQYDVQQHDAJIWTETMBEGA1UE
|
||||
CgwKRGFwciBXb3JsZDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwNjEwMDI0
|
||||
NVoXDTI0MDEwNjEwMDI0NVowUDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlRTMQsw
|
||||
CQYDVQQHDAJIWTETMBEGA1UECgwKRGFwciBXb3JsZDESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJup13iQfS72fRxN9JLJDLP0t
|
||||
el4F8bmxEfcHjvKMGJaupvvtHgZmtlYlY6evzE6yIN5mUIcBqCWzpN+aCPBwq6OB
|
||||
ojCBnzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUE
|
||||
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/
|
||||
AAABMB0GA1UdDgQWBBRkZKt5a3/g5ea0eVufjBT3QL5xkTAfBgNVHSMEGDAWgBSZ
|
||||
gCsruspxWHnuTNldgzN7ZGI2szAKBggqhkjOPQQDAgNJADBGAiEAkO5nX2Eofa0k
|
||||
R+u1zIiwJilwzgI9A2WnnvLWWmKiQkQCIQDtoS4x1bKaPyCTtLJ91fwSPxWdexhQ
|
||||
CDsniMd431J38g==
|
||||
-----END CERTIFICATE-----
|
||||
Loading…
Reference in New Issue