From 83c95d9db818ec660a40f0ac97faf2278bd08b5f Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 14 Nov 2025 14:39:29 -0800 Subject: [PATCH] cmd: add ca-signed tool. Verify certificates are signed by a CA. --- cmd/ca-signed/README.txt | 40 ++++ cmd/ca-signed/main.go | 233 ++++++++++++++++++++++++ cmd/ca-signed/testdata/goog-wr2.pem | 29 +++ cmd/ca-signed/testdata/gts-r1.pem | 31 ++++ cmd/ca-signed/testdata/isrg-root-x1.pem | 31 ++++ cmd/ca-signed/testdata/le-e7.pem | 26 +++ 6 files changed, 390 insertions(+) create mode 100644 cmd/ca-signed/README.txt create mode 100644 cmd/ca-signed/main.go create mode 100644 cmd/ca-signed/testdata/goog-wr2.pem create mode 100644 cmd/ca-signed/testdata/gts-r1.pem create mode 100644 cmd/ca-signed/testdata/isrg-root-x1.pem create mode 100644 cmd/ca-signed/testdata/le-e7.pem diff --git a/cmd/ca-signed/README.txt b/cmd/ca-signed/README.txt new file mode 100644 index 0000000..19fd639 --- /dev/null +++ b/cmd/ca-signed/README.txt @@ -0,0 +1,40 @@ +ca-signed +--------- + +Description + ca-signed verifies whether one or more certificates are signed by a given + Certificate Authority (CA). It prints a concise status per input certificate + along with the certificate’s expiration date when validation succeeds. + +Usage + ca-signed CA.pem cert1.pem [cert2.pem ...] + + - CA.pem: A file containing one or more CA certificates in PEM, DER, or PKCS#7/PKCS#12 formats. + - certN.pem: A file containing the end-entity (leaf) certificate to verify. If the file contains a chain, + the first certificate is treated as the leaf and the remaining ones are used as intermediates. + +Output format + For each input certificate file, one line is printed: + : OK (expires YYYY-MM-DD) + : INVALID + +Special self-test mode + ca-signed selftest + + Runs a built-in test suite using embedded certificates. This mode requires no + external files or network access. The program exits with code 0 if all tests + pass, or a non-zero exit code if any test fails. Example output lines include + whether validation succeeds and the leaf’s expiration when applicable. + +Examples + # Verify a server certificate against a root CA + ca-signed isrg-root-x1.pem le-e7.pem + + # Run the embedded self-test suite + ca-signed selftest + +Notes + - The tool attempts to parse certificates in PEM first, then falls back to + DER/PKCS#7/PKCS#12 (with an empty password) where applicable. + - Expiration is shown for the leaf certificate only. + - In selftest mode, test certificates are compiled into the binary using go:embed. diff --git a/cmd/ca-signed/main.go b/cmd/ca-signed/main.go new file mode 100644 index 0000000..6e8cfa2 --- /dev/null +++ b/cmd/ca-signed/main.go @@ -0,0 +1,233 @@ +package main + +import ( + "crypto/x509" + "embed" + "fmt" + "os" + "path/filepath" + "time" + + "git.wntrmute.dev/kyle/goutils/certlib" +) + +// loadCertsFromFile attempts to parse certificates from a file that may be in +// PEM or DER/PKCS#7 format. Returns the parsed certificates or an error. +func loadCertsFromFile(path string) ([]*x509.Certificate, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // Try PEM first + if certs, err := certlib.ParseCertificatesPEM(data); err == nil { + return certs, nil + } + + // Try DER/PKCS7/PKCS12 (with no password) + if certs, _, err := certlib.ParseCertificatesDER(data, ""); err == nil { + return certs, nil + } else { + return nil, err + } +} + +func makePoolFromFile(path string) (*x509.CertPool, error) { + // Try PEM via helper (it builds a pool) + if pool, err := certlib.LoadPEMCertPool(path); err == nil && pool != nil { + return pool, nil + } + + // Fallback: read as DER(s), add to a new pool + certs, err := loadCertsFromFile(path) + if err != nil || len(certs) == 0 { + return nil, fmt.Errorf("failed to load CA certificates from %s", path) + } + pool := x509.NewCertPool() + for _, c := range certs { + pool.AddCert(c) + } + return pool, nil +} + +//go:embed testdata/*.pem +var embeddedTestdata embed.FS + +// loadCertsFromBytes attempts to parse certificates from bytes that may be in +// PEM or DER/PKCS#7 format. +func loadCertsFromBytes(data []byte) ([]*x509.Certificate, error) { + // Try PEM first + if certs, err := certlib.ParseCertificatesPEM(data); err == nil { + return certs, nil + } + // Try DER/PKCS7/PKCS12 (with no password) + if certs, _, err := certlib.ParseCertificatesDER(data, ""); err == nil { + return certs, nil + } else { + return nil, err + } +} + +func makePoolFromBytes(data []byte) (*x509.CertPool, error) { + certs, err := loadCertsFromBytes(data) + if err != nil || len(certs) == 0 { + return nil, fmt.Errorf("failed to load CA certificates from embedded bytes") + } + pool := x509.NewCertPool() + for _, c := range certs { + pool.AddCert(c) + } + return pool, nil +} + +func verifyAgainstCA(caPool *x509.CertPool, path string) (ok bool, expiry string) { + certs, err := loadCertsFromFile(path) + if err != nil || len(certs) == 0 { + return false, "" + } + + leaf := certs[0] + ints := x509.NewCertPool() + if len(certs) > 1 { + for _, ic := range certs[1:] { + ints.AddCert(ic) + } + } + + opts := x509.VerifyOptions{ + Roots: caPool, + Intermediates: ints, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + } + if _, err := leaf.Verify(opts); err != nil { + return false, "" + } + + return true, leaf.NotAfter.Format("2006-01-02") +} + +func verifyAgainstCABytes(caPool *x509.CertPool, certData []byte) (ok bool, expiry string) { + certs, err := loadCertsFromBytes(certData) + if err != nil || len(certs) == 0 { + return false, "" + } + + leaf := certs[0] + ints := x509.NewCertPool() + if len(certs) > 1 { + for _, ic := range certs[1:] { + ints.AddCert(ic) + } + } + + opts := x509.VerifyOptions{ + Roots: caPool, + Intermediates: ints, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + } + if _, err := leaf.Verify(opts); err != nil { + return false, "" + } + + return true, leaf.NotAfter.Format("2006-01-02") +} + +// selftest runs built-in validation using embedded certificates. +func selftest() int { + type testCase struct { + name string + caFile string + certFile string + expectOK bool + } + + cases := []testCase{ + {name: "ISRG Root X1 validates LE E7", caFile: "testdata/isrg-root-x1.pem", certFile: "testdata/le-e7.pem", expectOK: true}, + {name: "ISRG Root X1 does NOT validate Google WR2", caFile: "testdata/isrg-root-x1.pem", certFile: "testdata/goog-wr2.pem", expectOK: false}, + {name: "GTS R1 validates Google WR2", caFile: "testdata/gts-r1.pem", certFile: "testdata/goog-wr2.pem", expectOK: true}, + {name: "GTS R1 does NOT validate LE E7", caFile: "testdata/gts-r1.pem", certFile: "testdata/le-e7.pem", expectOK: false}, + } + + failures := 0 + for _, tc := range cases { + caBytes, err := embeddedTestdata.ReadFile(tc.caFile) + if err != nil { + fmt.Fprintf(os.Stderr, "selftest: failed to read embedded %s: %v\n", tc.caFile, err) + failures++ + continue + } + certBytes, err := embeddedTestdata.ReadFile(tc.certFile) + if err != nil { + fmt.Fprintf(os.Stderr, "selftest: failed to read embedded %s: %v\n", tc.certFile, err) + failures++ + continue + } + pool, err := makePoolFromBytes(caBytes) + if err != nil || pool == nil { + fmt.Fprintf(os.Stderr, "selftest: failed to build CA pool for %s: %v\n", tc.caFile, err) + failures++ + continue + } + ok, exp := verifyAgainstCABytes(pool, certBytes) + if ok != tc.expectOK { + fmt.Printf("%s: unexpected result: got %v, want %v\n", tc.name, ok, tc.expectOK) + failures++ + } else { + if ok { + fmt.Printf("%s: OK (expires %s)\n", tc.name, exp) + } else { + fmt.Printf("%s: INVALID (as expected)\n", tc.name) + } + } + } + + if failures == 0 { + fmt.Println("selftest: PASS") + return 0 + } + fmt.Fprintf(os.Stderr, "selftest: FAIL (%d failure(s))\n", failures) + return 1 +} + +func main() { + // Special selftest mode: single argument "selftest" + if len(os.Args) == 2 && os.Args[1] == "selftest" { + os.Exit(selftest()) + } + + if len(os.Args) < 3 { + prog := filepath.Base(os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage:\n %s ca.pem cert1.pem cert2.pem ...\n %s selftest\n", prog, prog) + os.Exit(2) + } + + caPath := os.Args[1] + caPool, err := makePoolFromFile(caPath) + if err != nil || caPool == nil { + fmt.Fprintf(os.Stderr, "failed to load CA certificate(s): %v\n", err) + os.Exit(1) + } + + for _, certPath := range os.Args[2:] { + ok, exp := verifyAgainstCA(caPool, certPath) + name := filepath.Base(certPath) + if ok { + // Display with the requested format + // Example: file: OK (expires 2031-01-01) + // Ensure deterministic date formatting + // Note: no timezone displayed; date only as per example + // If exp ended up empty for some reason, recompute safely + if exp == "" { + if certs, err := loadCertsFromFile(certPath); err == nil && len(certs) > 0 { + exp = certs[0].NotAfter.Format("2006-01-02") + } else { + // fallback to the current date to avoid empty; though shouldn't happen + exp = time.Now().Format("2006-01-02") + } + } + fmt.Printf("%s: OK (expires %s)\n", name, exp) + } else { + fmt.Printf("%s: INVALID\n", name) + } + } +} diff --git a/cmd/ca-signed/testdata/goog-wr2.pem b/cmd/ca-signed/testdata/goog-wr2.pem new file mode 100644 index 0000000..f82f4d1 --- /dev/null +++ b/cmd/ca-signed/testdata/goog-wr2.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw +MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl +cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc ++MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji +aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc +LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX +xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX +FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk +rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG +GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw +Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq +hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS +TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe +SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT +DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu +ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB +vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl +Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG +iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr +Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw +qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU +/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0= +-----END CERTIFICATE----- diff --git a/cmd/ca-signed/testdata/gts-r1.pem b/cmd/ca-signed/testdata/gts-r1.pem new file mode 100644 index 0000000..410b1f1 --- /dev/null +++ b/cmd/ca-signed/testdata/gts-r1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- diff --git a/cmd/ca-signed/testdata/isrg-root-x1.pem b/cmd/ca-signed/testdata/isrg-root-x1.pem new file mode 100644 index 0000000..30aa936 --- /dev/null +++ b/cmd/ca-signed/testdata/isrg-root-x1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/cmd/ca-signed/testdata/le-e7.pem b/cmd/ca-signed/testdata/le-e7.pem new file mode 100644 index 0000000..e41019e --- /dev/null +++ b/cmd/ca-signed/testdata/le-e7.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw +WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST +CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef +QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw +gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4 +wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB +AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g +BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu +Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD +aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF +h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG +yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr +OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o +yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S +M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ +UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq +Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I +tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ +YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty ++VUwFj9tmWxyR/M= +-----END CERTIFICATE-----