From 161f2102a3c8aa417ad96a7e7c591b2053a8f1ae Mon Sep 17 00:00:00 2001 From: Lukas Zapletal Date: Mon, 18 Nov 2024 14:46:28 +0100 Subject: [PATCH 1/4] osbuild: new stage 'cacert' --- pkg/osbuild/pki_update_ca_trust_stage.go | 35 +++++++++ pkg/osbuild/pki_update_ca_trust_stage_test.go | 72 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 pkg/osbuild/pki_update_ca_trust_stage.go create mode 100644 pkg/osbuild/pki_update_ca_trust_stage_test.go diff --git a/pkg/osbuild/pki_update_ca_trust_stage.go b/pkg/osbuild/pki_update_ca_trust_stage.go new file mode 100644 index 0000000000..72ef8200a2 --- /dev/null +++ b/pkg/osbuild/pki_update_ca_trust_stage.go @@ -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 +} diff --git a/pkg/osbuild/pki_update_ca_trust_stage_test.go b/pkg/osbuild/pki_update_ca_trust_stage_test.go new file mode 100644 index 0000000000..b07a60fff4 --- /dev/null +++ b/pkg/osbuild/pki_update_ca_trust_stage_test.go @@ -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()) + } +} From e377b2105f105ae1761eeb02db5d53dc9d2291ab Mon Sep 17 00:00:00 2001 From: Lukas Zapletal Date: Mon, 18 Nov 2024 14:46:58 +0100 Subject: [PATCH 2/4] osbuild: manifest changes for 'cacert' --- pkg/manifest/os.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/manifest/os.go b/pkg/manifest/os.go index 38554595f2..6192128bd1 100644 --- a/pkg/manifest/os.go +++ b/pkg/manifest/os.go @@ -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 @@ -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()) + } + + 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), From e8bfa11124ad9f31b583ec72353cce7ee9d3ca39 Mon Sep 17 00:00:00 2001 From: Lukas Zapletal Date: Mon, 18 Nov 2024 14:48:29 +0100 Subject: [PATCH 3/4] blueprint: customizations for 'cacert' --- pkg/blueprint/ca_customizations.go | 5 +++ pkg/blueprint/customizations.go | 31 ++++++++++++++++++ pkg/cert/parsecerts.go | 46 ++++++++++++++++++++++++++ pkg/cert/parsecerts_test.go | 52 ++++++++++++++++++++++++++++++ pkg/distro/fedora/images.go | 8 +++++ pkg/distro/rhel/images.go | 8 +++++ 6 files changed, 150 insertions(+) create mode 100644 pkg/blueprint/ca_customizations.go create mode 100644 pkg/cert/parsecerts.go create mode 100644 pkg/cert/parsecerts_test.go diff --git a/pkg/blueprint/ca_customizations.go b/pkg/blueprint/ca_customizations.go new file mode 100644 index 0000000000..560bac66fa --- /dev/null +++ b/pkg/blueprint/ca_customizations.go @@ -0,0 +1,5 @@ +package blueprint + +type CACustomization struct { + PEMCerts []string `json:"pem_certs,omitempty" toml:"pem_certs,omitempty"` +} diff --git a/pkg/blueprint/customizations.go b/pkg/blueprint/customizations.go index 28ea761051..f0d800077f 100644 --- a/pkg/blueprint/customizations.go +++ b/pkg/blueprint/customizations.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/osbuild/images/pkg/cert" "github.com/osbuild/images/pkg/customizations/anaconda" ) @@ -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 { @@ -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 +} diff --git a/pkg/cert/parsecerts.go b/pkg/cert/parsecerts.go new file mode 100644 index 0000000000..d446ddb184 --- /dev/null +++ b/pkg/cert/parsecerts.go @@ -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. +// 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 +} diff --git a/pkg/cert/parsecerts_test.go b/pkg/cert/parsecerts_test.go new file mode 100644 index 0000000000..92423fa076 --- /dev/null +++ b/pkg/cert/parsecerts_test.go @@ -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) +} + +func TestParseGarbageCerts(t *testing.T) { + _, err := ParseCerts("garbage") + assert.NotNil(t, err) + assert.Equal(t, "no valid PEM certificates found in: garbage", err.Error()) +} diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go index c40c42e943..4c04a06c4f 100644 --- a/pkg/distro/fedora/images.go +++ b/pkg/distro/fedora/images.go @@ -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)) + } + if ca != nil { + osc.CACerts = ca.PEMCerts + } + return osc, nil } diff --git a/pkg/distro/rhel/images.go b/pkg/distro/rhel/images.go index 7a20febcc3..806463b921 100644 --- a/pkg/distro/rhel/images.go +++ b/pkg/distro/rhel/images.go @@ -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)) + } + if ca != nil { + osc.CACerts = ca.PEMCerts + } + return osc, nil } From 4ca7cb457f24a71d72234104d128a6ff90a45266 Mon Sep 17 00:00:00 2001 From: Lukas Zapletal Date: Wed, 20 Nov 2024 12:31:07 +0100 Subject: [PATCH 4/4] test: cacert added to all-customization --- test/configs/all-customizations.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/configs/all-customizations.json b/test/configs/all-customizations.json index e7ab8f346d..beb3a92390 100644 --- a/test/configs/all-customizations.json +++ b/test/configs/all-customizations.json @@ -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" + ] + } } } }