Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate Subject DN entries during Certificate based authentication with Vault #5453

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions builtin/credential/cert/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,7 @@ type allowed struct {
organizational_units string // allowed OUs in the certificate
ext string // required extensions in the certificate
metadata_ext string // allowed metadata extensions to add to identity alias
sbjDnOids string // required subject dn entries
}

func testAccStepCert(t *testing.T, name string, cert []byte, policies string, testData allowed, expectError bool) logicaltest.TestStep {
Expand All @@ -1950,6 +1951,7 @@ func testAccStepCertWithExtraParams(t *testing.T, name string, cert []byte, poli
"required_extensions": testData.ext,
"allowed_metadata_extensions": testData.metadata_ext,
"lease": 1000,
"required_subject_oids": testData.sbjDnOids,
}
for k, v := range extraParams {
data[k] = v
Expand Down Expand Up @@ -2340,3 +2342,105 @@ func TestBackend_CertUpgrade(t *testing.T) {
t.Fatal(diff)
}
}

// TestBackend_subjectoids_singleCert tests a self-signed client cert containing subject DN entries that is trusted by root CA.
func TestBackend_subjectoids_singleCert(t *testing.T) {
connState, err := testConnState(
"test-fixtures/root/rootcawsubjoidscert.pem",
"test-fixtures/root/rootcawsubjoidskey.pem",
"test-fixtures/root/rootcacert.pem",
)
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
// First set of cases check all available fields in the Subject DN
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "0.9.2342.19200300.100.1.1:TheUID"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "0.9.2342.19200300.100.1.1:TheUID,2.5.4.3:example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1,2.5.4.11:ExampleDivision2"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1,2.5.4.11:Example*2,2.5.4.11:ExampleDivision3"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:Example*1,2.5.4.11:ExampleDivision2,2.5.4.11:ExampleDivision3,0.9.2342.19200300.100.1.25:ExampleDC"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1,2.5.4.11:Example*2,2.5.4.11:ExampleDivision3,0.9.2342.19200300.100.1.25:ExampleDC,2.5.4.4:ExampleSN"}, false),
testAccStepLogin(t, connState),

// This Second set of test cases check for all available fields in the Subject DN for globbed pattern(s)
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "0.9.2342.19200300.100.1.1:*UID"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "0.9.2342.19200300.100.1.1:*UID,2.5.4.3:example*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:*Org"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:*Org,2.5.4.11:*Division1"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:*Org,2.5.4.11:*Division1,2.5.4.11:*Division2"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:*Org,2.5.4.11:*Division1,2.5.4.11:*Division2,2.5.4.11:*Division3"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:*Org,2.5.4.11:*Division1,2.5.4.11:*Division2,2.5.4.11:*Division3,0.9.2342.19200300.100.1.25:Example*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example*,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:*Org,2.5.4.11:*Division1,2.5.4.11:*Division2,2.5.4.11:*Division3,0.9.2342.19200300.100.1.25:*DC,2.5.4.4:Example*"}, false),
testAccStepLogin(t, connState),

// This Third set of test cases check for all non-matching entries of OIDs in Subject DN
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "0.9.2342.19200300.100.1.1:NotTheUID"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "0.9.2342.19200300.100.1.1:TheUID,2.5.4.3:NotExample.com"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:NotExample.com"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:JP"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:WA"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:San Francisco,2.5.4.10:ExampleOrg"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:NotExampleOrg,2.5.4.11:ExampleDivision1"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:NotExampleDivision1,2.5.4.11:ExampleDivision2"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1,2.5.4.11:NotExampleDivision2,2.5.4.11:ExampleDivision3"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1,2.5.4.11:ExampleDivision2,2.5.4.11:NotExampleDivision3,0.9.2342.19200300.100.1.25:ExampleDC"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com,2.5.4.6:US,2.5.4.8:CA,2.5.4.7:Sunnyvale,2.5.4.10:ExampleOrg,2.5.4.11:ExampleDivision1,2.5.4.11:ExampleDivision2,2.5.4.11:ExampleDivision3,0.9.2342.19200300.100.1.25:NotExampleDC,2.5.4.4:ExampleSN"}, false),
testAccStepLoginInvalid(t, connState),

//+ve Tests for condition when both "allowed_common_names" and "required_subject_oids" are provided.
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com", common_names: "example.com"}, false),
testAccStepLogin(t, connState),

//-ve Tests for condition when both "allowed_common_names" and "required_subject_oids" are provided.
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:example.com", common_names: "Notexample.com"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{sbjDnOids: "2.5.4.3:Notexample.com", common_names: "example.com"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
22 changes: 16 additions & 6 deletions builtin/credential/cert/path_certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ certificate.`,
Description: tokenutil.DeprecationText("token_bound_cidrs"),
Deprecated: true,
},
"required_subject_oids": {
Type: framework.TypeCommaStringSlice,
Description: `A comma-separated string or array of subject name entries
formatted as "oid:value". Expects the oid value to be some type of ASN1 encoded string.
All values much match. Supports globbing on "value".`,
},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
Expand Down Expand Up @@ -308,6 +314,7 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra
"ocsp_servers_override": cert.OcspServersOverride,
"ocsp_fail_open": cert.OcspFailOpen,
"ocsp_query_all_servers": cert.OcspQueryAllServers,
"required_subject_oids": cert.RequiredSubjectOids,
}
cert.PopulateTokenData(data)

Expand Down Expand Up @@ -392,6 +399,9 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
if allowedMetadataExtensionsRaw, ok := d.GetOk("allowed_metadata_extensions"); ok {
cert.AllowedMetadataExtensions = allowedMetadataExtensionsRaw.([]string)
}
if requiredSubjectOidsRaw, ok := d.GetOk("required_subject_oids"); ok {
cert.RequiredSubjectOids = requiredSubjectOidsRaw.([]string)
}

// Get tokenutil fields
if err := cert.ParseTokenFields(req, d); err != nil {
Expand Down Expand Up @@ -509,12 +519,12 @@ type CertEntry struct {
RequiredExtensions []string
AllowedMetadataExtensions []string
BoundCIDRs []*sockaddr.SockAddrMarshaler

OcspCaCertificates string
OcspEnabled bool
OcspServersOverride []string
OcspFailOpen bool
OcspQueryAllServers bool
RequiredSubjectOids []string
OcspCaCertificates string
OcspEnabled bool
OcspServersOverride []string
OcspFailOpen bool
OcspQueryAllServers bool
}

const pathCertHelpSyn = `
Expand Down
53 changes: 52 additions & 1 deletion builtin/credential/cert/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ func (b *backend) matchesConstraints(ctx context.Context, clientCert *x509.Certi
b.matchesEmailSANs(clientCert, config) &&
b.matchesURISANs(clientCert, config) &&
b.matchesOrganizationalUnits(clientCert, config) &&
b.matchesCertificateExtensions(clientCert, config)
b.matchesCertificateExtensions(clientCert, config) &&
b.matchesSubjectOids(clientCert, config)
if config.Entry.OcspEnabled {
ocspGood, err := b.checkForCertInOCSP(ctx, clientCert, trustedChain, conf)
if err != nil {
Expand Down Expand Up @@ -523,6 +524,56 @@ func (b *backend) matchesCertificateExtensions(clientCert *x509.Certificate, con
return true
}

// Matches the Subject dn entries with te required_subject_oids from the the configuration.
// All entries in the required config should match.
func (b *backend) matchesSubjectOids(clientCert *x509.Certificate, config *ParsedCert) bool {
// If no required extensions, nothing to check here
if len(config.Entry.RequiredSubjectOids) == 0 {
return true
}
// Fail fast if we have required Subject OID's but do not have Subject entries in the Cert
if len(clientCert.Subject.Names) == 0 {
return false
}

// Build the 'Subject Names' OID map from the certificate, this will be
// matched in the next step with the required OID's from config.
// Note that this is a Map of slice, So eg. if the subject has multiple
// entries for OU say MyOU1, MyOU2 and MyOU3, this generated Map will have an entry
// 2.5.4.11:[MyOU1 MyOU1 MyOU2]
subjectOidMap := make(map[string][]string, len(clientCert.Subject.Names))
for _, n := range clientCert.Subject.Names {
subjectOidMap[n.Type.String()] = append(subjectOidMap[n.Type.String()], n.Value.(string))
}
// Check if all the required OID's from the config are present in the
// certificate Subject Names i.e. the subjectOidMap we created in previous step
for _, requiredOid := range config.Entry.RequiredSubjectOids {
// expected format for a required OID is OID:Value so we split it accordingly
reqOid := strings.SplitN(requiredOid, ":", 2)

clientSubjOidValue, clientSubjOidValueOk := subjectOidMap[reqOid[0]]
// The match fails if the OID itself is not present or is missing a value
if !clientSubjOidValueOk || len(clientSubjOidValue) == 0 {
return false
}
// If the OID matches, we compare the required OID value with each entry of the
// slice value from the matched map entry
isRequiredOidInCertEntry := false
for _, clientSubjOid := range clientSubjOidValue {
if glob.Glob(reqOid[1], clientSubjOid) {
isRequiredOidInCertEntry = true
break
}
}

// If none of the slice entries match, the overall match fails for a required OID.
if !isRequiredOidInCertEntry {
return false
}
}
return true
}

// certificateExtensionsMetadata returns the metadata from configured
// metadata extensions
func (b *backend) certificateExtensionsMetadata(clientCert *x509.Certificate, config *ParsedCert) map[string]string {
Expand Down
27 changes: 27 additions & 0 deletions builtin/credential/cert/test-fixtures/root/rootcawsubjoids.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[ req ]
default_bits = 2048
encrypt_key = no
prompt = no
default_md = sha256
req_extensions = req_v3
distinguished_name = dn

[ dn ]
UID = TheUID
CN = example.com
C = US
ST = CA
L = Sunnyvale
O = ExampleOrg
0.OU = ExampleDivision1
1.OU = ExampleDivision2
2.OU = ExampleDivision3
DC = ExampleDC
SN = ExampleSN

[ req_v3 ]
subjectAltName = @alt_names

[ alt_names ]
IP.1 = 127.0.0.1
DNS.1 = example.com
31 changes: 31 additions & 0 deletions builtin/credential/cert/test-fixtures/root/rootcawsubjoids.csr
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIFZjCCA04CAQAwgfExFjAUBgoJkiaJk/IsZAEBDAZUaGVVSUQxFDASBgNVBAMM
C2V4YW1wbGUuY29tMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcM
CVN1bm55dmFsZTETMBEGA1UECgwKRXhhbXBsZU9yZzEZMBcGA1UECwwQRXhhbXBs
ZURpdmlzaW9uMTEZMBcGA1UECwwQRXhhbXBsZURpdmlzaW9uMjEZMBcGA1UECwwQ
RXhhbXBsZURpdmlzaW9uMzEZMBcGCgmSJomT8ixkARkWCUV4YW1wbGVEQzESMBAG
A1UEBAwJRXhhbXBsZVNOMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
peUb51cXh3StDo6U/EsfNIorj9XyhiDojI9VojoMObdhJqbwnPWCNWxU/DXgdoE5
LzJvfqUOq1s6Hi28KgwQCJj9dakvll64k4rdMT1iV2aUqGTk3GYHhFSaBFurV9po
KDNCtNwhsQ5bF30nFhUPpzt18KVov7shsbMA3iEyKqsvr4e2STB3Lbmx0u9K66zn
Am5hgwFK68Jg2YmkUvGIyBCUFqe+X+xffv2kr6kofJdghR2K4Ptf74RRxis4RFiZ
JGzSNQW3TD2yzMacoBA3zqcob6i0V0cQLNh1vx6FAB9L0qHgH0jsHXbKsKfeI9Wl
kqqrv2HO7huf7qROjbTejGoYFnQbST3yBHEabgCIrAdjxTISuzn/rEpClBhh6yo9
dIXkNbWzfWGbo3LowxVQMRWVB/n/yZ+u3k5O8UQuLerpOhaNAqu113nkISXYE+XA
r7K/3sUBV7OZCaS8suSInifjwCVaL2hHZ0Bm65aTohtqCa0o+ZI2Wurs4Qivfuse
WtOvcqzdX0ZPBidKgHEfksIwGvz71GMAnYMw7l5kHOnVowxv3c3Um6oynhAuzxpQ
co69kTiT+fgtI1KlQSL0uiQAxtE0nqDFnsjinyMORE66pBCFxBCwmdI3f0veml73
GboxNrBBgTxZpesOI99+FKQ1UOF8GHqLjLGm4Ao000ECAwEAAaAvMC0GCSqGSIb3
DQEJDjEgMB4wHAYDVR0RBBUwE4cEfwAAAYILZXhhbXBsZS5jb20wDQYJKoZIhvcN
AQELBQADggIBAAaMV264TQnYWnjfNzHaFVWHzOXRVh9tXwcpsYgA6sFTqXFlLd5C
bxxCDjOQRr8wkYRksiqGJHrxthjLH9M4ot+SV9Yjq02PmTrIQQj10yGyywGnh8BZ
xDy6bdP3tqEN3i1mPn/spvEhaNej/XQ9pFC6MrufrjcRjvO7pJilFMSOWAaB09e3
dXreXeOtgX6Ee+OxHLt9jxBa/Lrj7+F/2/vtk4oJxc5IRBEXTmPEdBruidbI0KAC
WF5UxbiuwV4iq1X8SkT/PB1p7N2gA14asgBm4nPex59T2Gu+DYHsr7Tso6TSx+GO
NiHZH/h76IOU4LVFS0r4YTN6Goy8jvKvD34UQ1mqxMxeq9ueGl95Ywz5foSO3Ufh
uk1OarZINF6GbqyP3J0jtIXzGukSmiU1J28nExvm/UpIbDWM4DGnNcKfQOhQUKZq
b79IUxaAD1a9AFAxIP4And0R/G2izrEz9GfnUYEzzrZcMR6v84nVU7eLnw0/+jcT
9Bl4L2cNXXHqXhJFhaQMNslGK21zsTptnuIeGoKwMsv+Z6gmCQw5oRhvLGUGpNyD
G/niwZcQLAynvijFbgV1oOIsOp7jJAl/CnIKX9SWXyNla8DixRyb6sT2cGCl4mlz
LgAV95DPIw1sMCSVflBmBKk0DItzamCJDJR9sNgmOYTIc2KZtYQZAwcH
-----END CERTIFICATE REQUEST-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEqzCCA5OgAwIBAgIJANl6FnIQbEiEMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
BAMTC2V4YW1wbGUuY29tMB4XDTE4MDcyOTAwMzkxMFoXDTI4MDcyNjAwMzkxMFow
gfExFjAUBgoJkiaJk/IsZAEBDAZUaGVVSUQxFDASBgNVBAMMC2V4YW1wbGUuY29t
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVN1bm55dmFsZTET
MBEGA1UECgwKRXhhbXBsZU9yZzEZMBcGA1UECwwQRXhhbXBsZURpdmlzaW9uMTEZ
MBcGA1UECwwQRXhhbXBsZURpdmlzaW9uMjEZMBcGA1UECwwQRXhhbXBsZURpdmlz
aW9uMzEZMBcGCgmSJomT8ixkARkWCUV4YW1wbGVEQzESMBAGA1UEBAwJRXhhbXBs
ZVNOMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApeUb51cXh3StDo6U
/EsfNIorj9XyhiDojI9VojoMObdhJqbwnPWCNWxU/DXgdoE5LzJvfqUOq1s6Hi28
KgwQCJj9dakvll64k4rdMT1iV2aUqGTk3GYHhFSaBFurV9poKDNCtNwhsQ5bF30n
FhUPpzt18KVov7shsbMA3iEyKqsvr4e2STB3Lbmx0u9K66znAm5hgwFK68Jg2Ymk
UvGIyBCUFqe+X+xffv2kr6kofJdghR2K4Ptf74RRxis4RFiZJGzSNQW3TD2yzMac
oBA3zqcob6i0V0cQLNh1vx6FAB9L0qHgH0jsHXbKsKfeI9Wlkqqrv2HO7huf7qRO
jbTejGoYFnQbST3yBHEabgCIrAdjxTISuzn/rEpClBhh6yo9dIXkNbWzfWGbo3Lo
wxVQMRWVB/n/yZ+u3k5O8UQuLerpOhaNAqu113nkISXYE+XAr7K/3sUBV7OZCaS8
suSInifjwCVaL2hHZ0Bm65aTohtqCa0o+ZI2Wurs4QivfuseWtOvcqzdX0ZPBidK
gHEfksIwGvz71GMAnYMw7l5kHOnVowxv3c3Um6oynhAuzxpQco69kTiT+fgtI1Kl
QSL0uiQAxtE0nqDFnsjinyMORE66pBCFxBCwmdI3f0veml73GboxNrBBgTxZpesO
I99+FKQ1UOF8GHqLjLGm4Ao000ECAwEAAaMgMB4wHAYDVR0RBBUwE4cEfwAAAYIL
ZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAGOh0YwKhHvlb2sOU3ukjX7J
xrK6oAAGNXpBp6jvEGr7Q7OJLrS+/4WLh+SDXv6hpkyTpNcInJHZTNLZ4wiDjVM1
odD/JFbA46WyKvuxJZ7+5P+RZRLYyni/PIjCaX8GPNypI6v8Vpkdi19gaSsU7uOi
8Ivfk8nQgFVrWzKhvd04mzQplsnteB+lyn6fr59uwX9KNrbTJ7cF7Q0Jtv+pY9ld
nOMgVNOUHMXmR0a6kG0hQcpx/nZkoBelaBD0swgukZPjR2NW+FA5XdV/HXwE+cfm
zNmirBvVjFSb45Pvaf17/andknVI8Z81kN+OuTkdHwGYAcn+J98CTGIXJV1fKLY=
-----END CERTIFICATE-----
Loading