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

osbuild: new stage 'cacert' (HMS-4839) #907

Merged
merged 4 commits into from
Nov 21, 2024
Merged
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
5 changes: 5 additions & 0 deletions pkg/blueprint/ca_customizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package blueprint

type CACustomization struct {
PEMCerts []string `json:"pem_certs,omitempty" toml:"pem_certs,omitempty"`
}
31 changes: 31 additions & 0 deletions pkg/blueprint/customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"slices"
"strings"

"github.com/osbuild/images/pkg/cert"
"github.com/osbuild/images/pkg/customizations/anaconda"
)

Expand All @@ -31,6 +32,7 @@ type Customizations struct {
Installer *InstallerCustomization `json:"installer,omitempty" toml:"installer,omitempty"`
RPM *RPMCustomization `json:"rpm,omitempty" toml:"rpm,omitempty"`
RHSM *RHSMCustomization `json:"rhsm,omitempty" toml:"rhsm,omitempty"`
CACerts *CACustomization `json:"cacerts,omitempty" toml:"ca,omitempty"`
}

type IgnitionCustomization struct {
Expand Down Expand Up @@ -425,3 +427,32 @@ func (c *Customizations) GetRHSM() *RHSMCustomization {
}
return c.RHSM
}

func (c *Customizations) checkCACerts() error {
if c == nil {
return nil
}

if c.CACerts != nil {
for _, bundle := range c.CACerts.PEMCerts {
_, err := cert.ParseCerts(bundle)
if err != nil {
return err
}
}
}

return nil
}

func (c *Customizations) GetCACerts() (*CACustomization, error) {
if c == nil {
return nil, nil
}

if err := c.checkCACerts(); err != nil {
return nil, err
}

return c.CACerts, nil
}
46 changes: 46 additions & 0 deletions pkg/cert/parsecerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cert

import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
)

var ErrNoValidPEMCertificatesFound = errors.New("no valid PEM certificates found")

// ParseCerts parses a PEM-encoded certificate chain formatted as concatenated strings
// and returns a slice of x509.Certificate. In case of unparsable certificates, the
// function returns an empty slice.
mvo5 marked this conversation as resolved.
Show resolved Hide resolved
// Returns an error when a cert cannot be parsed, or when no certificates are recognized
// in the input.
func ParseCerts(cert string) ([]*x509.Certificate, error) {
result := make([]*x509.Certificate, 0, 1)
block := []byte(cert)
var blocks [][]byte
for {
var certDERBlock *pem.Block
certDERBlock, block = pem.Decode(block)
if certDERBlock == nil {
break
}

if certDERBlock.Type == "CERTIFICATE" {
blocks = append(blocks, certDERBlock.Bytes)
}
}

for _, block := range blocks {
cert, err := x509.ParseCertificate(block)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
result = append(result, cert)
}

if len(result) == 0 {
return nil, fmt.Errorf("%w in: %s", ErrNoValidPEMCertificatesFound, cert)
}

return result, nil
}
52 changes: 52 additions & 0 deletions pkg/cert/parsecerts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cert

import (
"testing"

"github.com/stretchr/testify/assert"
)

// taken from osbuild:test/data/certs/cert1.pem
const exampleCert = `
-----BEGIN CERTIFICATE-----
MIIDhTCCAm2gAwIBAgIUVya7VJ3O8W8SqwuEa0BZ4HSsXvAwDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNDA4MjYx
MDQyNDBaGA8yMTI0MDgwMjEwNDI0MFowUTELMAkGA1UEBhMCREUxDzANBgNVBAgM
BkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMM
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnGjlvN
O3F/Z7Lr/r+6Xp2DosnNwoPHhG2e61KnFzgZfaxbklal5ORpuV/gLIg7lrbpdZe7
WvK+16RanL6fLitis/tYVFyvz1MXqBYYrEoFGvVg9fOiis7hjpdZcpNDH9SngoAN
O0Wvv4T6LQS0cC7ZAFZjvmJ+RiZEbzRkNG5pUddZXbotE6htNfLgA5L1wIBgllrM
4DVkG0yNKmzqPNzfPTbdUgWCfjaQShHy1GP8KNEwFxM31F2wvQxsEb77o1S44Out
mlsi83tti6P7KjDk7w2j2zZO1X0xI8pflv3TBkJT1Am8vnk6rVnNO4pCpop3+kma
pDUEzBQmSQA5R1ECAwEAAaNTMFEwHQYDVR0OBBYEFDxFcFgPEsgsDixfKxB0uYGN
aJmzMB8GA1UdIwQYMBaAFDxFcFgPEsgsDixfKxB0uYGNaJmzMA8GA1UdEwEB/wQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFih4lUbLlhKwIAV9x3/W7Mih8xUEdZr
olquZgaHedFet+ByAHvoES3pec7AVYTOD53mjgyZubD6INnVHzKyS4AG9ydD73o4
cmm3DKxBaesvlHeTn0MOKsoM8QCxeyFJmiUPpgDBok/PFnbGR9+JcsrlGJAnsSKD
vWpiwYcBauZ9nnK5yDe5M9XNFPkNDZzbKvWU7Sw3ziMT/+bRJse5vTrYcyOnNGgy
gZNz2nimKy1U8XZVAVwOV0rdGEFrfMln8DkRW86rGK/EncaVsl0SSP/rmjQgiX8Q
3CZraQGujJP932HSwUfdCX9yh+rTjE3MEnbqMoLzJa4BXB2aDQWtywU=
-----END CERTIFICATE-----
`

func TestParseCerts(t *testing.T) {
certs, err := ParseCerts(exampleCert)
assert.Nil(t, err)
assert.Equal(t, len(certs), 1)
assert.Equal(t, "localhost", certs[0].Subject.CommonName)
}

func TestParseConcatenatedCerts(t *testing.T) {
certs, err := ParseCerts(exampleCert + "\n" + exampleCert)
assert.Nil(t, err)
assert.Equal(t, len(certs), 2)
assert.Equal(t, "localhost", certs[0].Subject.CommonName)
}
mvo5 marked this conversation as resolved.
Show resolved Hide resolved

func TestParseGarbageCerts(t *testing.T) {
_, err := ParseCerts("garbage")
assert.NotNil(t, err)
assert.Equal(t, "no valid PEM certificates found in: garbage", err.Error())
}
8 changes: 8 additions & 0 deletions pkg/distro/fedora/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ func osCustomizations(
osc.Files = append(osc.Files, imageConfig.Files...)
osc.Directories = append(osc.Directories, imageConfig.Directories...)

ca, err := c.GetCACerts()
if err != nil {
panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I am not sure we should panic here, I see we do this a lot in this function whichI find confusing given that we have an error return here and that the certs come from the user so they maybe wrong and panic() seems a bit heavy handed for user inputs. But we can tweak in a followup given the rest of the function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

}
if ca != nil {
osc.CACerts = ca.PEMCerts
}

return osc, nil
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/distro/rhel/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ func osCustomizations(
osc.NoBLS = *imageConfig.NoBLS
}

ca, err := c.GetCACerts()
if err != nil {
panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
mvo5 marked this conversation as resolved.
Show resolved Hide resolved
}
if ca != nil {
osc.CACerts = ca.PEMCerts
}

return osc, nil
}

Expand Down
17 changes: 17 additions & 0 deletions pkg/manifest/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ type OSCustomizations struct {
Directories []*fsnode.Directory
Files []*fsnode.File

CACerts []string

FIPS bool

// NoBLS configures the image bootloader with traditional menu entries
Expand Down Expand Up @@ -791,6 +793,21 @@ func (p *OS) serialize() osbuild.Pipeline {
}))
}

if len(p.CACerts) > 0 {
for _, cc := range p.CACerts {
files, err := osbuild.NewCAFileNodes(cc)
if err != nil {
panic(err.Error())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(super nitpick) panic(err) would also work (and is slightly shorter)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a pattern in the whole file, I would rather not change it in this PR.

}

if len(files) > 0 {
p.Files = append(p.Files, files...)
pipeline.AddStages(osbuild.GenFileNodesStages(files)...)
}
}
pipeline.AddStage(osbuild.NewCAStageStage())
}

if p.SElinux != "" {
pipeline.AddStage(osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
FileContexts: fmt.Sprintf("etc/selinux/%s/contexts/files/file_contexts", p.SElinux),
Expand Down
35 changes: 35 additions & 0 deletions pkg/osbuild/pki_update_ca_trust_stage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package osbuild

import (
"encoding/pem"
"fmt"
"path/filepath"

"github.com/osbuild/images/pkg/cert"
"github.com/osbuild/images/pkg/customizations/fsnode"
)

func NewCAStageStage() *Stage {
return &Stage{
Type: "org.osbuild.pki.update-ca-trust",
}
}

func NewCAFileNodes(bundle string) ([]*fsnode.File, error) {
var files []*fsnode.File
certs, err := cert.ParseCerts(bundle)
if err != nil {
return nil, fmt.Errorf("failed to parse CA certificates: %v", err)
}

for _, c := range certs {
path := filepath.Join("/etc/pki/ca-trust/source/anchors", filepath.Base(c.SerialNumber.Text(16))+".pem")
f, err := fsnode.NewFile(path, nil, "root", "root", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}))
if err != nil {
panic(err)
}
files = append(files, f)
}

return files, nil
}
72 changes: 72 additions & 0 deletions pkg/osbuild/pki_update_ca_trust_stage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package osbuild

import (
"encoding/pem"
"testing"

"github.com/osbuild/images/pkg/cert"
"github.com/stretchr/testify/assert"
)

// taken from osbuild:test/data/certs/cert{1,2}.pem
const exampleCert = `
-----BEGIN CERTIFICATE-----
MIIDhTCCAm2gAwIBAgIUVya7VJ3O8W8SqwuEa0BZ4HSsXvAwDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNDA4MjYx
MDQyNDBaGA8yMTI0MDgwMjEwNDI0MFowUTELMAkGA1UEBhMCREUxDzANBgNVBAgM
BkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMM
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnGjlvN
O3F/Z7Lr/r+6Xp2DosnNwoPHhG2e61KnFzgZfaxbklal5ORpuV/gLIg7lrbpdZe7
WvK+16RanL6fLitis/tYVFyvz1MXqBYYrEoFGvVg9fOiis7hjpdZcpNDH9SngoAN
O0Wvv4T6LQS0cC7ZAFZjvmJ+RiZEbzRkNG5pUddZXbotE6htNfLgA5L1wIBgllrM
4DVkG0yNKmzqPNzfPTbdUgWCfjaQShHy1GP8KNEwFxM31F2wvQxsEb77o1S44Out
mlsi83tti6P7KjDk7w2j2zZO1X0xI8pflv3TBkJT1Am8vnk6rVnNO4pCpop3+kma
pDUEzBQmSQA5R1ECAwEAAaNTMFEwHQYDVR0OBBYEFDxFcFgPEsgsDixfKxB0uYGN
aJmzMB8GA1UdIwQYMBaAFDxFcFgPEsgsDixfKxB0uYGNaJmzMA8GA1UdEwEB/wQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFih4lUbLlhKwIAV9x3/W7Mih8xUEdZr
olquZgaHedFet+ByAHvoES3pec7AVYTOD53mjgyZubD6INnVHzKyS4AG9ydD73o4
cmm3DKxBaesvlHeTn0MOKsoM8QCxeyFJmiUPpgDBok/PFnbGR9+JcsrlGJAnsSKD
vWpiwYcBauZ9nnK5yDe5M9XNFPkNDZzbKvWU7Sw3ziMT/+bRJse5vTrYcyOnNGgy
gZNz2nimKy1U8XZVAVwOV0rdGEFrfMln8DkRW86rGK/EncaVsl0SSP/rmjQgiX8Q
3CZraQGujJP932HSwUfdCX9yh+rTjE3MEnbqMoLzJa4BXB2aDQWtywU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDhTCCAm2gAwIBAgIUBFUPUz3MqjWa0bINLdvSfNDQ3iQwDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNDA4MjYx
MDQyNDVaGA8yMTI0MDgwMjEwNDI0NVowUTELMAkGA1UEBhMCREUxDzANBgNVBAgM
BkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMM
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN/SEvmx
m9UHhP/rXQrx82SmIft940HoJtM9Bsbp1Pj8Nk7WA+M/WgTVz0J/08uU8pZgzj0U
pfaawxfB2lo++GBa9vCeqQzIo6YKQf0Rg50+KJ1oQ3+ZIDxIr2ou38dg5KyG1D+A
XnGIg7fKu3RczCYbgudB2Yq2LvQdLTSb+PE08lczc7e9bGobHLfqCilFS71Q2BEa
B14xfrz3LF1Cf0r6qL6SXg2+vyjsMy1sGwGY1XGqAgvT8rgm+ZfVknFLyzhly6Cf
eVNg+JWapR5iDVIG6F64Ayj63wd52aveYD3JEljpzF3I8lq4qQ8oI3z0S/t4Dx01
ErexkyAFhIlWVvsCAwEAAaNTMFEwHQYDVR0OBBYEFDccfxp07zFNvuWqvSTgFmju
JrfCMB8GA1UdIwQYMBaAFDccfxp07zFNvuWqvSTgFmjuJrfCMA8GA1UdEwEB/wQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAK2sJWg9Eg2Ekv7TgQeln7lGWNl2EYS5
uDc1qhgkOos3QSonvSU9n90DECY+m3wcHBWF6OSB1RFSQmROdi3tKMZUgM9gnjG/
Us8D8HxS0+FLYX0wHlx/anCBzy+tKMDKq+tQzuZZGpKFhDZf527Uqj+R8DVMcqKV
oqdILvb3asYiISCXDu2Fj9HfqPHKKER6hboQ2LvcLq+t4FOtvGiTxdiDaOhSJWcL
0Izcs3Wi0RUNTIULtLFukFvYCTFoxYSLP178xXSf3N8Z9o6ezb7aUI86d6rLmwga
GHY+cBQvEsjW85elwClvspptKd1Xx4z8geH0+Y0qaIGBn1cyze5k5fk=
-----END CERTIFICATE-----
`

func TestNewCAFileNodes(t *testing.T) {
certs, err := cert.ParseCerts(exampleCert)
assert.Nil(t, err)

files, err := NewCAFileNodes(exampleCert)
assert.Nil(t, err)
assert.Equal(t, 2, len(certs), len(files))
expectedPaths := []string{
"/etc/pki/ca-trust/source/anchors/5726bb549dcef16f12ab0b846b4059e074ac5ef0.pem",
"/etc/pki/ca-trust/source/anchors/4550f533dccaa359ad1b20d2ddbd27cd0d0de24.pem",
}
for i, c := range certs {
assert.Equal(t, expectedPaths[i], files[i].Path())
assert.Equal(t, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}), files[i].Data())
mvo5 marked this conversation as resolved.
Show resolved Hide resolved
}
}
7 changes: 6 additions & 1 deletion test/configs/all-customizations.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQGiBGRBSJURBACzCoe9UNfxOUiFLq9b60weSBFdr39mLViscecDWATNvXtgRoK/\nxl/4qpayzALRCQ2Ek/pMrbKPF/3ngECuBv7S+rI4n/rIia4FNcqzYeZAz4DE4NP/\neUGvz49tWhmH17hX/rmF9kz5kLq2bDZI4GDgZW/oMDdt2ivj092Ljm9jRwCgyQy3\nWEK6RJvIcSEh9vbdwVdMPOcD/iHqNejTMFwGyZfCWB0eIOoxUOUn/ZZpELTL2UpW\nGduCf3txb5SkK7M+WDbb0S5IvNXoi0tc13STiD6Oxg2O9PkSvvYb+8zxlhNoSTwy\n54j7Rf5FlnQ3TAFfjtQ5LCx56LKK73j4RjvKW//ktm5n54exsgo9Ry/e12T46dRg\n7tIlA/91rzLm57Qyc73A7zjgIzef9O6V5ZzowC+pp/jfb5pS9hXgROekLkMgX0vg\niA5rM5OpqK4bArVP1lRWnLyvghwO+TW763RVuXlS0scfzMy4g0NgrG6j7TIOKEqz\n4xQxOuwkudqiQr/kOqKuLxQBXa+5MJkyhfPmqYw5wpqyCwFa/7Q4b3NidWlsZCB0\nZXN0IChvc2J1aWxkIHRlc3QgZ3Bna2V5KSA8b3NidWlsZEBleGFtcGxlLmNvbT6I\newQTEQIAOxYhBGB8woiEPRKBO8Cr31lulpQgMejzBQJkQUiVAhsjBQsJCAcCAiIC\nBhUKCQgLAgQWAgMBAh4HAheAAAoJEFlulpQgMejzapMAoLmUg1mNDTRUaCrN/fzm\nHYLHL6jkAJ9pEKkJQiHB6SfD0fkiD2GkELYLubkBDQRkQUiVEAQAlAAXrQ572vuw\nxI3W8GSZmOQiAYOQmOKRloLEy6VZ3NSOb9y2TXj33QTkJBPOM17AzB7E+YjZrpUt\ngl6LlXmfjMcJAcXhFaUBCilAcMwMlLl7DtnSkLnLIXYmHiN0v83BH/H0EPutOc5l\n0QIyugutifp9SJz2+EWpC4bjA7GFkQ8AAwUD/1tLEGqCJ37O8gfzYt2PWkqBEoOY\n0Z3zwVS6PWW/IIkak9dAJ0iX5NMeFWpzFNfviDPHqhEdUR55zsxyUZIZlCX5jwmA\nt7qm3cbH4HNU1Ogq3Q9hykbTPWPZVkpvNm/TO8TA2brhkz3nuS8Hbmh+rjXFOSZj\nDQBUxItuuj2hhpQEiGAEGBECACAWIQRgfMKIhD0SgTvAq99ZbpaUIDHo8wUCZEFI\nlQIbDAAKCRBZbpaUIDHo83fQAKDHgFIaggaNsvDQkj7vMX0fecHRhACfS9Bvxn2W\nWSb6T+gChmYBseZwk/k=\n=DQ3i\n-----END PGP PUBLIC KEY BLOCK-----\n"
]
}
]
],
"cacerts": {
"pem_certs": [
"-----BEGIN CERTIFICATE-----\nMIIDszCCApugAwIBAgIUJ4lK+JfdJCNgcEVxZDinJfKKbQswDQYJKoZIhvcNAQEL\nBQAwaDELMAkGA1UEBhMCVVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYD\nVQQHDAdSYWxlaWdoMRAwDgYDVQQKDAdSZWQgSGF0MRwwGgYDVQQDDBNUZXN0IENB\nIGZvciBvc2J1aWxkMCAXDTI0MDkwMzEzMjkyMFoYDzIyOTgwNjE4MTMyOTIwWjBo\nMQswCQYDVQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcM\nB1JhbGVpZ2gxEDAOBgNVBAoMB1JlZCBIYXQxHDAaBgNVBAMME1Rlc3QgQ0EgZm9y\nIG9zYnVpbGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDeA7OcWTrV\ngstoBsUaeJKm8nelg7Lc0WNXH6yOTLsr4td4yHs0YOvFGwgSf+ffV3RAG1mgqnMG\nMgkD2+z+7QhHbHHs3y0d0zfhA2bg0KVvfCWk7fNRPHY0UOePpXk245Bfw3D0VTpl\nF7nePk1I7ZY09snPWUeb2rjKXzYjKjzM0h27+ykV8I8+FbdyPk/pR8whyDqtHLUa\nXfFy2TFloDSYMkHKVd38BnL0bj91x5F+KsZkN4HzfbYwxLbCQfOSgy7q6TWce9kq\nLo6tya9vuvpWFm1dye7L+BodAQAq/dI/JMeCfyTb0eFb+tyzfr5aVIoqqDN+p9ft\ncw4OefpHbhtNAgMBAAGjUzBRMB0GA1UdDgQWBBRV2A9YmusekPzu5Yf08cV0oPL1\nwjAfBgNVHSMEGDAWgBRV2A9YmusekPzu5Yf08cV0oPL1wjAPBgNVHRMBAf8EBTAD\nAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCgQZ2Xfj+NxaKBZgn2KNxS0MTbhzHRz6Rn\nqJs+h8OUz2Crmaf6N+RHlmDRZXUrDjSHpxVT2LxFy7ofRrLYIezFDUYfb920VkkV\nSVcxh1YDFROJalfMoE6wdyR/LnK4MJZS9fUpeCJJc/A0J+9FK9CwcyUrHgJ8XbJh\nMKYyQ+cf6O7wzutuBpMyRqSKS+hVM7BQTmSFvv1eAJlo6klGAmmKiYmAEvcQadH1\ndjrujsA3Cn5vX2L+0yuiLB5/zoxqx5cEy97TuKUYB8OqMMujAXNzF4L3HJDUNba2\nAhEkFozMXwYX73TGbGZ0mawPS5D3v3tYTEmJFf6SnVCmUW1fs57g\n-----END CERTIFICATE-----\n"
]
}
}
}
}
Loading