From 520c485f3b367adf7f7b6b2a3702c515ca88417d Mon Sep 17 00:00:00 2001 From: Fotis Nikolaidis Date: Mon, 26 Dec 2022 15:44:21 +0200 Subject: [PATCH] Add support for OCSP verification Signed-off-by: Fotis Nikolaidis --- CHANGELOG.md | 2 + CONTRIBUTORS.md | 1 + cmd/internal/cli/verify.go | 16 ++ e2e/verify/ocspcertificates/download_chain.sh | 7 + e2e/verify/ocspcertificates/intermediate.pem | 63 +++++ e2e/verify/ocspcertificates/leaf.pem | 35 +++ e2e/verify/ocspresponder/index.txt | 3 + e2e/verify/ocspresponder/ocsp_responder.go | 62 +++++ .../standalone/add_cert_to_index.sh | 9 + e2e/verify/ocspresponder/standalone/main.go | 15 ++ e2e/verify/verify.go | 83 +++++- go.mod | 2 +- internal/app/singularity/verify.go | 41 ++- internal/app/singularity/verify_ocsp.go | 254 ++++++++++++++++++ test/certs/gen_certs.go | 3 + test/certs/intermediate.pem | 13 +- test/certs/leaf.pem | 11 +- test/certs/root.pem | 11 +- 18 files changed, 604 insertions(+), 27 deletions(-) create mode 100755 e2e/verify/ocspcertificates/download_chain.sh create mode 100644 e2e/verify/ocspcertificates/intermediate.pem create mode 100644 e2e/verify/ocspcertificates/leaf.pem create mode 100644 e2e/verify/ocspresponder/index.txt create mode 100644 e2e/verify/ocspresponder/ocsp_responder.go create mode 100755 e2e/verify/ocspresponder/standalone/add_cert_to_index.sh create mode 100644 e2e/verify/ocspresponder/standalone/main.go create mode 100755 internal/app/singularity/verify_ocsp.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab3c8c390..45b4de3af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,8 @@ - A new `--reproducible` flag for `./mconfig` will configure Singularity so that its binaries do not contain non-reproducible paths. This disables plugin functionality. +- Support for online verification checks of x509 certificates using OCSP protocol. + (introduced flag: `verify --ocsp-verify`) ### Bug Fixes diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5a11953b37..0fa3a5c616 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -44,6 +44,7 @@ The following have contributed code and/or documentation to this repository. - Eng Zer Jun - Eric Müller - Felix Abecassis +- Fotis Nikolaidis - Geoffroy Vallee , - George Hartzell - Gert Hulselmans diff --git a/cmd/internal/cli/verify.go b/cmd/internal/cli/verify.go index e31e09183a..eac2ba5c46 100644 --- a/cmd/internal/cli/verify.go +++ b/cmd/internal/cli/verify.go @@ -25,6 +25,7 @@ var ( certificatePath string // --certificate flag certificateIntermediatesPath string // --certificate-intermediates flag certificateRootsPath string // --certificate-roots flag + ocspVerify bool // --ocsp-verify flag pubKeyPath string // --key flag localVerify bool // -l flag jsonVerify bool // -j flag @@ -113,6 +114,16 @@ var verifyCertificateRootsFlag = cmdline.Flag{ EnvKeys: []string{"VERIFY_ROOTS"}, } +// --ocsp-verify +var verifyOCSPFlag = cmdline.Flag{ + ID: "ocspVerifyFlag", + Value: &ocspVerify, + DefaultValue: false, + Name: "ocsp-verify", + Usage: "enable online revocation check for certificates", + EnvKeys: []string{"VERIFY_OCSP"}, +} + // --key var verifyPublicKeyFlag = cmdline.Flag{ ID: "publicKeyFlag", @@ -175,6 +186,7 @@ func init() { cmdManager.RegisterFlagForCmd(&verifyCertificateFlag, VerifyCmd) cmdManager.RegisterFlagForCmd(&verifyCertificateIntermediatesFlag, VerifyCmd) cmdManager.RegisterFlagForCmd(&verifyCertificateRootsFlag, VerifyCmd) + cmdManager.RegisterFlagForCmd(&verifyOCSPFlag, VerifyCmd) cmdManager.RegisterFlagForCmd(&verifyPublicKeyFlag, VerifyCmd) cmdManager.RegisterFlagForCmd(&verifyLocalFlag, VerifyCmd) cmdManager.RegisterFlagForCmd(&verifyJSONFlag, VerifyCmd) @@ -228,6 +240,10 @@ func doVerifyCmd(cmd *cobra.Command, cpath string) { opts = append(opts, singularity.OptVerifyWithRoots(p)) } + if cmd.Flag(verifyOCSPFlag.Name).Changed { + opts = append(opts, singularity.OptVerifyWithOCSP()) + } + case cmd.Flag(verifyPublicKeyFlag.Name).Changed: sylog.Infof("Verifying image with key material from '%v'", pubKeyPath) diff --git a/e2e/verify/ocspcertificates/download_chain.sh b/e2e/verify/ocspcertificates/download_chain.sh new file mode 100755 index 0000000000..a1f6e9c79d --- /dev/null +++ b/e2e/verify/ocspcertificates/download_chain.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Step 1: Get the leaf certificate +openssl s_client -connect www.akamai.com:443 < /dev/null 2>&1 | sed -n '/-----BEGIN/,/-----END/p' > leaf.pem + +# Step 2: Get the intermediate certificate +openssl s_client -showcerts -connect www.akamai.com:443 < /dev/null 2>&1 | sed -n '/-----BEGIN/,/-----END/p' > intermediate.pem diff --git a/e2e/verify/ocspcertificates/intermediate.pem b/e2e/verify/ocspcertificates/intermediate.pem new file mode 100644 index 0000000000..96343a93ea --- /dev/null +++ b/e2e/verify/ocspcertificates/intermediate.pem @@ -0,0 +1,63 @@ +-----BEGIN CERTIFICATE----- +MIIGATCCBOmgAwIBAgIQCh5LiVVv3nEde7Li3/+ATzANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE +aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMjA2MjIwMDAwMDBa +Fw0yMzA2MjMyMzU5NTlaMHYxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNo +dXNldHRzMRIwEAYDVQQHEwlDYW1icmlkZ2UxIjAgBgNVBAoTGUFrYW1haSBUZWNo +bm9sb2dpZXMsIEluYy4xFzAVBgNVBAMTDnd3dy5ha2FtYWkuY29tMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEiuc+zlu43bv55+s0Fj6RiBW+olZmc/AkoTP48CFC +IGP1DET7Oufx6oe63GIuBzdVfR5D6R2z818b5gY1o2lBxqOCA3swggN3MB8GA1Ud +IwQYMBaAFLdrouqoqoSMeeq02g+YssWVdrn0MB0GA1UdDgQWBBT86pFIu848aqiQ +L73R3pUDjECAnDAlBgNVHREEHjAcgg53d3cuYWthbWFpLmNvbYIKYWthbWFpLmNv +bTAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MIGPBgNVHR8EgYcwgYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E +aWdpQ2VydFRMU1JTQVNIQTI1NjIwMjBDQTEtNC5jcmwwQKA+oDyGOmh0dHA6Ly9j +cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNIQTI1NjIwMjBDQTEtNC5j +cmwwPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3 +dy5kaWdpY2VydC5jb20vQ1BTMH8GCCsGAQUFBwEBBHMwcTAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEkGCCsGAQUFBzAChj1odHRwOi8vY2Fj +ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUTFNSU0FTSEEyNTYyMDIwQ0ExLTEu +Y3J0MAkGA1UdEwQCMAAwggF/BgorBgEEAdZ5AgQCBIIBbwSCAWsBaQB3AOg+0No+ +9QY1MudXKLyJa8kD08vREWvs62nhd31tBr1uAAABgYi+UIwAAAQDAEgwRgIhAJ1R +BJT/F3l27vLAd65f6bsRlLdKe2B0g5iaNWn9qUOIAiEAzcfo21akKVUHdySdtIz/ +4xCXsm5hZlIr2AdtGVxiR0IAdgA1zxkbv7FsV78PrUxtQsu7ticgJlHqP+Eq76gD +wzvWTAAAAYGIvlCWAAAEAwBHMEUCIQCflW6g5is3DB5BOoiOIck7g1GDZFQPvxYA +2ssOnVqqkgIgIKBzdY0933/Bvdv69uhR9YGgNjF6pqCqcEFX2IhmqGkAdgC3Pvsk +35xNunXyOcW6WPRsXfxCz3qfNcSeHQmBJe20mQAAAYGIvlCBAAAEAwBHMEUCIQD6 +l++PTabZ98GPjyGmbbCRsgJEJut6I7J5eolQfKQhAQIgP7sOIn+mhH5HNgU/6cS0 +T/dL3qVKI/DK2VVq2iLHQo8wDQYJKoZIhvcNAQELBQADggEBAEjCX4PWIZW//UGo +7tBLfNdP3XOo7WbWZNqam9I+hXnlNhV8rl7kNnkhzXMMpF4ljOZ9dOblXT1aFGib +kc8ucHcBXxd8wO8UB5R8FUeYDhE4BVJWAsdzRL8PT+RuY9xKfntXFKjpUI7FD4Cb +LhpSh/cnBVfapUTY8RbDjb6SiLEkwWrppWUSEtDG0tSsYPuPHvZM+YTDCAfA2gdt +yDsiPlNrBjo0h2YAt5kbzj5UoMkRmCGiA0qj/Mo2Cp31OUU45eaEELq0ilgLUlI+ +bjo+7eCvoiEWltwceoWazetWLW7fDTme29dZ4olGdPuGVq9G2Qj4x90sqvJOw+0/ +1to7zMw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEvjCCA6agAwIBAgIQBtjZBNVYQ0b2ii+nVCJ+xDANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaME8xCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxKTAnBgNVBAMTIERpZ2lDZXJ0IFRMUyBS +U0EgU0hBMjU2IDIwMjAgQ0ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAwUuzZUdwvN1PWNvsnO3DZuUfMRNUrUpmRh8sCuxkB+Uu3Ny5CiDt3+PE0J6a +qXodgojlEVbbHp9YwlHnLDQNLtKS4VbL8Xlfs7uHyiUDe5pSQWYQYE9XE0nw6Ddn +g9/n00tnTCJRpt8OmRDtV1F0JuJ9x8piLhMbfyOIJVNvwTRYAIuE//i+p1hJInuW +raKImxW8oHzf6VGo1bDtN+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB +Afr5yjK7tI4nhyfFK3TUqNaX3sNk+crOU6JWvHgXjkkDKa77SU+kFbnO8lwZV21r +eacroicgE7XQPUDTITAHk+qZ9QIDAQABo4IBgjCCAX4wEgYDVR0TAQH/BAgwBgEB +/wIBADAdBgNVHQ4EFgQUt2ui6qiqhIx56rTaD5iyxZV2ufQwHwYDVR0jBBgwFoAU +A95QNVbRTLtm8KPiGxvDl7I90VUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGG +GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2Nh +Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNydDBCBgNV +HR8EOzA5MDegNaAzhjFodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH +bG9iYWxSb290Q0EuY3JsMD0GA1UdIAQ2MDQwCwYJYIZIAYb9bAIBMAcGBWeBDAEB +MAgGBmeBDAECATAIBgZngQwBAgIwCAYGZ4EMAQIDMA0GCSqGSIb3DQEBCwUAA4IB +AQCAMs5eC91uWg0Kr+HWhMvAjvqFcO3aXbMM9yt1QP6FCvrzMXi3cEsaiVi6gL3z +ax3pfs8LulicWdSQ0/1s/dCYbbdxglvPbQtaCdB73sRD2Cqk3p5BJl+7j5nL3a7h +qG+fh/50tx8bIKuxT8b1Z11dmzzp/2n3YWzW2fP9NsarA4h20ksudYbj/NhVfSbC +EXffPgK2fPOre3qGNm+499iTcc+G33Mw+nur7SpZyEKEOxEXGlLzyQ4UfaJbcme6 +ce1XR2bFuAJKZTRei9AqPCCcUZlM51Ke92sRKw2Sfh3oius2FkOH6ipjv3U/697E +A7sKPPcw7+uvTPyLNhBzPvOk +-----END CERTIFICATE----- diff --git a/e2e/verify/ocspcertificates/leaf.pem b/e2e/verify/ocspcertificates/leaf.pem new file mode 100644 index 0000000000..cfc2ee483e --- /dev/null +++ b/e2e/verify/ocspcertificates/leaf.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGATCCBOmgAwIBAgIQCh5LiVVv3nEde7Li3/+ATzANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE +aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMjA2MjIwMDAwMDBa +Fw0yMzA2MjMyMzU5NTlaMHYxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNo +dXNldHRzMRIwEAYDVQQHEwlDYW1icmlkZ2UxIjAgBgNVBAoTGUFrYW1haSBUZWNo +bm9sb2dpZXMsIEluYy4xFzAVBgNVBAMTDnd3dy5ha2FtYWkuY29tMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEiuc+zlu43bv55+s0Fj6RiBW+olZmc/AkoTP48CFC +IGP1DET7Oufx6oe63GIuBzdVfR5D6R2z818b5gY1o2lBxqOCA3swggN3MB8GA1Ud +IwQYMBaAFLdrouqoqoSMeeq02g+YssWVdrn0MB0GA1UdDgQWBBT86pFIu848aqiQ +L73R3pUDjECAnDAlBgNVHREEHjAcgg53d3cuYWthbWFpLmNvbYIKYWthbWFpLmNv +bTAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MIGPBgNVHR8EgYcwgYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E +aWdpQ2VydFRMU1JTQVNIQTI1NjIwMjBDQTEtNC5jcmwwQKA+oDyGOmh0dHA6Ly9j +cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNIQTI1NjIwMjBDQTEtNC5j +cmwwPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3 +dy5kaWdpY2VydC5jb20vQ1BTMH8GCCsGAQUFBwEBBHMwcTAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEkGCCsGAQUFBzAChj1odHRwOi8vY2Fj +ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUTFNSU0FTSEEyNTYyMDIwQ0ExLTEu +Y3J0MAkGA1UdEwQCMAAwggF/BgorBgEEAdZ5AgQCBIIBbwSCAWsBaQB3AOg+0No+ +9QY1MudXKLyJa8kD08vREWvs62nhd31tBr1uAAABgYi+UIwAAAQDAEgwRgIhAJ1R +BJT/F3l27vLAd65f6bsRlLdKe2B0g5iaNWn9qUOIAiEAzcfo21akKVUHdySdtIz/ +4xCXsm5hZlIr2AdtGVxiR0IAdgA1zxkbv7FsV78PrUxtQsu7ticgJlHqP+Eq76gD +wzvWTAAAAYGIvlCWAAAEAwBHMEUCIQCflW6g5is3DB5BOoiOIck7g1GDZFQPvxYA +2ssOnVqqkgIgIKBzdY0933/Bvdv69uhR9YGgNjF6pqCqcEFX2IhmqGkAdgC3Pvsk +35xNunXyOcW6WPRsXfxCz3qfNcSeHQmBJe20mQAAAYGIvlCBAAAEAwBHMEUCIQD6 +l++PTabZ98GPjyGmbbCRsgJEJut6I7J5eolQfKQhAQIgP7sOIn+mhH5HNgU/6cS0 +T/dL3qVKI/DK2VVq2iLHQo8wDQYJKoZIhvcNAQELBQADggEBAEjCX4PWIZW//UGo +7tBLfNdP3XOo7WbWZNqam9I+hXnlNhV8rl7kNnkhzXMMpF4ljOZ9dOblXT1aFGib +kc8ucHcBXxd8wO8UB5R8FUeYDhE4BVJWAsdzRL8PT+RuY9xKfntXFKjpUI7FD4Cb +LhpSh/cnBVfapUTY8RbDjb6SiLEkwWrppWUSEtDG0tSsYPuPHvZM+YTDCAfA2gdt +yDsiPlNrBjo0h2YAt5kbzj5UoMkRmCGiA0qj/Mo2Cp31OUU45eaEELq0ilgLUlI+ +bjo+7eCvoiEWltwceoWazetWLW7fDTme29dZ4olGdPuGVq9G2Qj4x90sqvJOw+0/ +1to7zMw= +-----END CERTIFICATE----- diff --git a/e2e/verify/ocspresponder/index.txt b/e2e/verify/ocspresponder/index.txt new file mode 100644 index 0000000000..4bf93169a3 --- /dev/null +++ b/e2e/verify/ocspresponder/index.txt @@ -0,0 +1,3 @@ +V 300401030000Z 01 unknown = US, O = Sylabs Inc., CN = root +V 300401030000Z 02 unknown = US, O = Sylabs Inc., CN = intermediate +V 300401030000Z 03 unknown = US, O = Sylabs Inc., CN = leaf diff --git a/e2e/verify/ocspresponder/ocsp_responder.go b/e2e/verify/ocspresponder/ocsp_responder.go new file mode 100644 index 0000000000..c327d13db0 --- /dev/null +++ b/e2e/verify/ocspresponder/ocsp_responder.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file +// distributed with the sources of this project regarding your rights to use or distribute this +// software. + +package ocspresponder + +import ( + "os" + "os/exec" + "path/filepath" +) + +var DefaultOCSPResponderArgs = ResponderArgs{ + IndexFile: "./index.txt", + ServerPort: "9999", + OCSPKeyPath: filepath.Join("..", "test", "keys", "ecdsa-private.pem"), // see test/gen_certs.go + OCSPCertPath: filepath.Join("..", "test", "certs", "root.pem"), // see test/gen_certs.go + CACertPath: filepath.Join("..", "test", "certs", "root.pem"), +} + +// ResponderArgs specifies the arguments for the OCSP Responder. +type ResponderArgs struct { + // IndexFile is the Certificate status index file + IndexFile string + + // ServerPort is the Port to run responder on. + ServerPort string + + // OCSPKeyPath is the Responder key to sign responses with. + OCSPKeyPath string + + // OCSPCertPath is the Responder certificate to sign responses with. + OCSPCertPath string + + // CACertPath is CA certificate filename. + CACertPath string +} + +// StartOCSPResponder runs the OCSP responder. +func StartOCSPResponder(args ResponderArgs) error { + // ensure that the index file exists. + // if not, create is using the ./add_cert_to_index.sh + _, err := os.Stat(args.IndexFile) + if err != nil { + return err + } + + cmd := exec.Command("openssl", []string{ + "ocsp", "-text", + "-index", args.IndexFile, + "-port", args.ServerPort, + "-rsigner", args.OCSPCertPath, + "-rkey", args.OCSPKeyPath, + "-CA", args.CACertPath, + }...) + + // cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/e2e/verify/ocspresponder/standalone/add_cert_to_index.sh b/e2e/verify/ocspresponder/standalone/add_cert_to_index.sh new file mode 100755 index 0000000000..95a412b886 --- /dev/null +++ b/e2e/verify/ocspresponder/standalone/add_cert_to_index.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# pass the path to the PEM-encoded certificate as first argument to the script, and then append to index.txt + +crt=$1 +exp=$(date -d "$(openssl x509 -enddate -noout -in $crt | cut -d= -f 2)" +"%y%m%d%H%M%SZ") +ser=$(openssl x509 -serial -noout -in $crt | cut -d= -f 2) +sub=$(openssl x509 -subject -noout -in $crt | cut -d= -f 2- | cut -d' ' -f 2-) +echo -e "V\t$exp\t\t$ser\tunknown\t$sub" diff --git a/e2e/verify/ocspresponder/standalone/main.go b/e2e/verify/ocspresponder/standalone/main.go new file mode 100644 index 0000000000..2d03821905 --- /dev/null +++ b/e2e/verify/ocspresponder/standalone/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "github.com/sylabs/singularity/e2e/verify/ocspresponder" +) + +func main() { + if err := ocspresponder.StartOCSPResponder(ocspresponder.DefaultOCSPResponderArgs); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } +} diff --git a/e2e/verify/verify.go b/e2e/verify/verify.go index 38294e1db0..62dedcb33b 100644 --- a/e2e/verify/verify.go +++ b/e2e/verify/verify.go @@ -6,9 +6,13 @@ package verify import ( + "fmt" "os" "path/filepath" "testing" + "time" + + "github.com/sylabs/singularity/e2e/verify/ocspresponder" "github.com/sylabs/singularity/e2e/internal/e2e" "github.com/sylabs/singularity/e2e/internal/testhelper" @@ -19,11 +23,15 @@ type ctx struct { } func (c *ctx) verify(t *testing.T) { - keyPath := filepath.Join("..", "test", "keys", "ed25519-public.pem") + pubKeyPath := filepath.Join("..", "test", "keys", "ed25519-public.pem") + priKeyPath := filepath.Join("..", "test", "keys", "ed25519-private.pem") + certPath := filepath.Join("..", "test", "certs", "leaf.pem") intPath := filepath.Join("..", "test", "certs", "intermediate.pem") rootPath := filepath.Join("..", "test", "certs", "root.pem") + c.startOCSPResponder(priKeyPath, rootPath) + tests := []struct { name string envs []string @@ -113,19 +121,19 @@ func (c *ctx) verify(t *testing.T) { }, { name: "KeyFlag", - flags: []string{"--key", keyPath}, + flags: []string{"--key", pubKeyPath}, imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"), expectOps: []e2e.SingularityCmdResultOp{ - e2e.ExpectError(e2e.ContainMatch, "Verifying image with key material from '"+keyPath+"'"), + e2e.ExpectError(e2e.ContainMatch, "Verifying image with key material from '"+pubKeyPath+"'"), e2e.ExpectError(e2e.ContainMatch, "Verified signature(s) from image"), }, }, { name: "KeyEnvVar", - envs: []string{"SINGULARITY_VERIFY_KEY=" + keyPath}, + envs: []string{"SINGULARITY_VERIFY_KEY=" + pubKeyPath}, imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"), expectOps: []e2e.SingularityCmdResultOp{ - e2e.ExpectError(e2e.ContainMatch, "Verifying image with key material from '"+keyPath+"'"), + e2e.ExpectError(e2e.ContainMatch, "Verifying image with key material from '"+pubKeyPath+"'"), e2e.ExpectError(e2e.ContainMatch, "Verified signature(s) from image"), }, }, @@ -155,6 +163,52 @@ func (c *ctx) verify(t *testing.T) { e2e.ExpectError(e2e.ContainMatch, "Verified signature(s) from image"), }, }, + { + name: "OCSPFlags", + flags: []string{ + "--certificate", certPath, + "--certificate-intermediates", intPath, + "--certificate-roots", rootPath, + "--ocsp-verify", + }, + imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"), + expectCode: 255, + expectOps: []e2e.SingularityCmdResultOp{ + // Expect OCSP to fail due to https://github.com/sylabs/singularity/issues/1152 + e2e.ExpectError(e2e.ContainMatch, "Failed to verify container: OCSP verification has failed"), + }, + }, + { + name: "OCSPEnvVars", + envs: []string{ + "SINGULARITY_VERIFY_CERTIFICATE=" + certPath, + "SINGULARITY_VERIFY_INTERMEDIATES=" + intPath, + "SINGULARITY_VERIFY_ROOTS=" + rootPath, + "SINGULARITY_VERIFY_OCSP=true", + }, + imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"), + expectCode: 255, + expectOps: []e2e.SingularityCmdResultOp{ + // Expect OCSP to fail due to https://github.com/sylabs/singularity/issues/1152 + e2e.ExpectError(e2e.ContainMatch, "Failed to verify container: OCSP verification has failed"), + }, + }, + { + name: "OCSPThirdPartyChain", + flags: []string{ + "--certificate", filepath.Join("./verify", "ocspcertificates", "leaf.pem"), + "--certificate-intermediates", filepath.Join("./verify", "ocspcertificates", "intermediate.pem"), + "--ocsp-verify", + }, + imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"), + expectCode: 255, + expectOps: []e2e.SingularityCmdResultOp{ + e2e.ExpectError(e2e.ContainMatch, "Failed to verify container: x509: certificate specifies an incompatible key usage"), + // https://github.com/sylabs/singularity/pull/1213#pullrequestreview-1240524316 + // Error Expect OCSP to succeed, but signature verification to fail. + // e2e.ExpectError(e2e.ContainMatch, "Failed to verify container: integrity: signature object 3 not valid: dsse: verify envelope failed: Accepted signatures do not match threshold, Found: 0, Expected 1"), + }, + }, } for _, tt := range tests { @@ -179,6 +233,25 @@ func (c *ctx) importPGPKeypairs(t *testing.T) { ) } +func (c *ctx) startOCSPResponder(rootKeyPath string, rootCertPath string) { + // initiate OCSP responder to validate the singularity certificate chain + go func() { + args := ocspresponder.ResponderArgs{ + IndexFile: filepath.Join("./verify", "ocspresponder", "index.txt"), + ServerPort: "9999", + OCSPKeyPath: rootKeyPath, + OCSPCertPath: rootCertPath, + CACertPath: rootCertPath, + } + + if err := ocspresponder.StartOCSPResponder(args); err != nil { + panic(fmt.Errorf("responder initialization has failed due to '%s'", err)) + } + }() + + time.Sleep(5 * time.Second) +} + // E2ETests is the main func to trigger the test suite func E2ETests(env e2e.TestEnv) testhelper.Tests { c := ctx{ diff --git a/go.mod b/go.mod index 76bbbe34ef..bf07c15c13 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/sylabs/scs-library-client v1.4.1 github.com/sylabs/sif/v2 v2.9.0 github.com/vbauerster/mpb/v8 v8.1.4 + golang.org/x/crypto v0.4.0 golang.org/x/sys v0.3.0 golang.org/x/term v0.3.0 golang.org/x/text v0.5.0 @@ -151,7 +152,6 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.4.0 // indirect golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.3.0 // indirect golang.org/x/sync v0.1.0 // indirect diff --git a/internal/app/singularity/verify.go b/internal/app/singularity/verify.go index 9be06b9d1d..8a06ba31e5 100644 --- a/internal/app/singularity/verify.go +++ b/internal/app/singularity/verify.go @@ -11,16 +11,18 @@ import ( "crypto" "crypto/x509" "encoding/hex" - "errors" + "fmt" "os" "strings" "github.com/ProtonMail/go-crypto/openpgp" + "github.com/pkg/errors" "github.com/sigstore/sigstore/pkg/signature" "github.com/sylabs/scs-key-client/client" "github.com/sylabs/sif/v2/pkg/integrity" "github.com/sylabs/sif/v2/pkg/sif" "github.com/sylabs/singularity/internal/pkg/buildcfg" + "github.com/sylabs/singularity/pkg/sylog" "github.com/sylabs/singularity/pkg/sypgp" ) @@ -33,6 +35,7 @@ type verifier struct { certs []*x509.Certificate intermediates *x509.CertPool roots *x509.CertPool + ocsp bool svs []signature.Verifier pgp bool pgpOpts []client.Option @@ -90,6 +93,16 @@ func OptVerifyWithPGP(opts ...client.Option) VerifyOpt { } } +// OptVerifyWithOCSP subjects the x509 certificate chains to online revocation checks, +// before the leaf certificate is deemed as trusted for validating the signature. +func OptVerifyWithOCSP() VerifyOpt { + return func(v *verifier) error { + v.ocsp = true + + return nil + } +} + // OptVerifyGroup adds a verification task for the group with the specified groupID. This may be // called multiple times to request verification of more than one group. func OptVerifyGroup(groupID uint32) VerifyOpt { @@ -148,7 +161,7 @@ func newVerifier(opts []VerifyOpt) (verifier, error) { // verifyCertificate attempts to verify c is a valid code signing certificate by building one or // more chains from c to a certificate in roots, using certificates in intermediates if needed. // This function does not do any revocation checking. -func verifyCertificate(c *x509.Certificate, intermediates, roots *x509.CertPool) error { +func verifyCertificate(c *x509.Certificate, intermediates, roots *x509.CertPool) (chains [][]*x509.Certificate, err error) { opts := x509.VerifyOptions{ Intermediates: intermediates, Roots: roots, @@ -157,8 +170,7 @@ func verifyCertificate(c *x509.Certificate, intermediates, roots *x509.CertPool) }, } - _, err := c.Verify(opts) - return err + return c.Verify(opts) } // getOpts returns integrity.VerifierOpt necessary to validate f. @@ -167,10 +179,28 @@ func (v verifier) getOpts(ctx context.Context, f *sif.FileImage) ([]integrity.Ve // Add key material from certificate(s). for _, c := range v.certs { - if err := verifyCertificate(c, v.intermediates, v.roots); err != nil { + // verify that the leaf certificate is not tampered and that is adequate for signing purposes. + chain, err := verifyCertificate(c, v.intermediates, v.roots) + if err != nil { return nil, err } + // Verify that the certificate is issued by a trustworthy CA (i.e the certificate chain is not revoked or expired). + if v.ocsp { + if len(chain) != 1 { + return nil, fmt.Errorf("unhandled OCSP condition, chain length %d != 1", len(chain)) + } + + ocspErr := OCSPVerify(chain[0]...) + if ocspErr != nil { + // TODO: We need to decide whether this should be strict or permissive. + return nil, ocspErr + } + + sylog.Debugf("OCSP validation has passed") + } + + // verify the signature by using the certificate. sv, err := signature.LoadVerifier(c.PublicKey, crypto.SHA256) if err != nil { return nil, err @@ -287,6 +317,7 @@ func Verify(ctx context.Context, path string, opts ...VerifyOpt) error { if err != nil { return err } + return iv.Verify() } diff --git a/internal/app/singularity/verify_ocsp.go b/internal/app/singularity/verify_ocsp.go new file mode 100755 index 0000000000..e2202095e0 --- /dev/null +++ b/internal/app/singularity/verify_ocsp.go @@ -0,0 +1,254 @@ +// Copyright (c) 2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2020-2022, ICS-FORTH. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file +// distributed with the sources of this project regarding your rights to use or distribute this +// software. + +package singularity + +import ( + "bytes" + "crypto" + "crypto/x509" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/pkg/errors" + "github.com/sylabs/singularity/pkg/sylog" + "golang.org/x/crypto/ocsp" +) + +/* +Online Certificate Status Protocol - OCSP + +OCSP responder is used to provide real-time verification of the revocation status of an X.509 certificate. +RFC: https://www.rfc-editor.org/rfc/rfc6960 +*/ + +const ( + // PKIXOCSPNoCheck refers to the Revocation Checking of an Authorized Responder. + // More more info check https://oidref.com/1.3.6.1.5.5.7.48.1.5 + PKIXOCSPNoCheck = "1.3.6.1.5.5.7.48.1.5" +) + +var errOCSP = errors.New("OCSP verification has failed") + +func OCSPVerify(chain ...*x509.Certificate) error { + // use the pool as an index for certificate issuers. + // fixme: we can drop this lookup if we assume that certificate N is always signed by certificate N+1. + pool := map[string]*x509.Certificate{} + + for _, cert := range chain { + pool[string(cert.SubjectKeyId)] = cert + } + + // recursively validate the certificate chain + for _, cert := range chain { + if err := revocationCheck(cert, pool); err != nil { + sylog.Warningf("OCSP verification has failed. Err: %s", err) + return errOCSP + } + } + + return nil +} + +func revocationCheck(cert *x509.Certificate, pool map[string]*x509.Certificate) error { + if len(cert.AuthorityKeyId) == 0 || string(cert.SubjectKeyId) == string(cert.AuthorityKeyId) { + sylog.Infof("skip self-signed certificate (%s)", cert.Subject.String()) + + return nil + } + + // Get the CA who issued the certificate in question. + issuer, exists := pool[string(cert.AuthorityKeyId)] + if !exists { + return fmt.Errorf("cannot find issuer '%s'", cert.Issuer) + } + + sylog.Infof("Validate: cert:%s issuer:%s", cert.Subject.CommonName, issuer.Subject.CommonName) + + // Ask OCSP for the validity of signer's certificate. + ocspCertificate, err := checkOCSPResponse(cert, issuer) + if err != nil { + return fmt.Errorf("OCSP Query err: %w", err) + } + + // 4.2.2.2 Authorized Responders + if ocspCertificate != nil { + // MUST reject the response if the certificate required to validate the signature on the + // response fails to meet at least one of the following criteria: + // 1. Matches a local configuration of OCSP signing authority for the + // certificate in question; or + // + // 2. Is the certificate of the CA that issued the certificate in + // question; or + // + // 3. Includes a value of id-ad-ocspSigning in an ExtendedKeyUsage + // extension and is issued by the CA that issued the certificate in + // question. + isCase1 := func() bool { + // Systems MAY provide a means of locally configuring one + // or more OCSP signing authorities, and specifying the set of CAs for + // which each signing authority is trusted. + // + // This is not our case so we always return false. + return false + } + + isCase2 := func() bool { + return string(ocspCertificate.SubjectKeyId) == string(cert.AuthorityKeyId) + } + + isCase3 := func() bool { + for _, usage := range cert.ExtKeyUsage { + if usage == x509.ExtKeyUsageOCSPSigning { + if string(ocspCertificate.AuthorityKeyId) == string(cert.AuthorityKeyId) { + return true + } + } + } + return false + } + + if !(isCase1() || isCase2() || isCase3()) { + return fmt.Errorf("ocsp response is rejected") + } + + // 4.2.2.2.1 Revocation Checking of an Authorized Responder + + // A CA may specify that an OCSP client can trust a responder for the + // lifetime of the responder's certificate. The CA does so by including + // the extension id-pkix-ocsp-nocheck. + for _, extension := range cert.Extensions { + if extension.Id.String() == PKIXOCSPNoCheck { + goto skipOCSPVerification + } + } + + // A CA may specify how the responder's certificate be checked for + // revocation. This can be done using CRL Distribution Points if the + // check should be done using CRLs or CRL Distribution Points, or + // Authority Information Access if the check should be done in some + // other way. Details for specifying either of these two mechanisms are + // available in [RFC2459]. + if err := revocationCheck(ocspCertificate, pool); err != nil { + return fmt.Errorf("cannot verify OCSP server's certificate. err: %w", err) + } + + // A CA may choose not to specify any method of revocation checking + // for the responder's certificate, in which case, it would be up to the + // OCSP client's local security policy to decide whether that + // certificate should be checked for revocation or not. + // -- Our current policy is to pass validation -- + skipOCSPVerification: + } + + return nil +} + +// checkOCSPResponse submit a revocation check request to the OCSP responder. +// If the certificate is ok to use, it returns nil. +// If the function cannot perform the check, or if the certificate is not ok for use (revoked or unknown), it returns +// with an error. +func checkOCSPResponse(cert, issuer *x509.Certificate) (needsValidation *x509.Certificate, err error) { + sylog.Debugf("cert:[%s] issuer:[%s]", cert.Subject.String(), issuer.Subject.String()) + + if !issuer.IsCA { + return nil, fmt.Errorf("signer's certificates can only belong to a CA") + } + + // Extract OCSP Server from the certificate in question + if len(cert.OCSPServer) == 0 { + return nil, fmt.Errorf("certificate does not support OCSP") + } + + // RFC 5280, 4.2.2.1 (Authority Information Access) + ocspURL, err := url.Parse(cert.OCSPServer[0]) + if err != nil { + return nil, fmt.Errorf("cannot parse OCSP Server from certificate. err: %w", err) + } + + // Create OCSP Request + + // Hash contains the hash function that should be used when + // constructing the OCSP request. If zero, SHA-1 will be used. + opts := &ocsp.RequestOptions{Hash: crypto.SHA1} + + buffer, err := ocsp.CreateRequest(cert, issuer, opts) + if err != nil { + return nil, fmt.Errorf("OCSP Create Request err: %w", err) + } + + httpRequest, err := http.NewRequest(http.MethodPost, cert.OCSPServer[0], bytes.NewBuffer(buffer)) + if err != nil { + return nil, fmt.Errorf("HTTP Create Request err: %w", err) + } + + // Submit OCSP Request + httpRequest.Header.Add("Content-Type", "application/ocsp-request") + httpRequest.Header.Add("Accept", "application/ocsp-response") + httpRequest.Header.Add("host", ocspURL.Host) + + httpClient := &http.Client{} + httpResponse, err := httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("OCSP Send Request err: %w", err) + } + + defer httpResponse.Body.Close() + + // Parse OCSP Response + output, err := io.ReadAll(httpResponse.Body) + if err != nil { + return nil, fmt.Errorf("cannot read response body. err: %w", err) + } + + ocspResponse, err := ocsp.ParseResponseForCert(output, cert, issuer) + if err != nil { + return nil, fmt.Errorf("OCSP response err: %w", err) + } + + // Handle OCSP Response + + // 4.2.2.1 Time + // + // - Responses whose thisUpdate time is later than the local system time + // SHOULD be considered unreliable. + // - Responses whose nextUpdate value is earlier than + // the local system time value SHOULD be considered unreliable. + // - Responses where the nextUpdate value is not set are equivalent to a CRL + // with no time for nextUpdate (see Section 2.4). + if ocspResponse.ThisUpdate.After(time.Now()) { + return nil, fmt.Errorf("unreliable OCSP response") + } + + if !ocspResponse.NextUpdate.IsZero() { + if ocspResponse.NextUpdate.Before(time.Now()) { + return nil, fmt.Errorf("unreliable OCSP response") + } + } + // If nextUpdate is not set, the responder is indicating that newer + // revocation information is available all the time. + + // The OCSP's certificate is signed by a third-party issuer that we need to verify. + if ocspResponse.Certificate != nil { + needsValidation = ocspResponse.Certificate + } + + // Check validity + switch ocspResponse.Status { + case ocsp.Good: // means the certificate is still valid + return needsValidation, nil + + case ocsp.Revoked: // says the certificate was revoked and cannot be trusted + return needsValidation, fmt.Errorf("certificate revoked at '%s'. Revocation reason code: '%d'", + ocspResponse.RevokedAt, ocspResponse.RevocationReason) + + default: // states that the server does not know about the requested certificate, + return needsValidation, fmt.Errorf("status unknown. certificate cannot be trusted") + } +} diff --git a/test/certs/gen_certs.go b/test/certs/gen_certs.go index 62df0fa777..975a4f4e2b 100644 --- a/test/certs/gen_certs.go +++ b/test/certs/gen_certs.go @@ -73,6 +73,7 @@ func createRoot(start time.Time) (crypto.PrivateKey, *x509.Certificate, error) { BasicConstraintsValid: true, IsCA: true, MaxPathLen: 2, + OCSPServer: []string{"http://localhost:9999"}, } c, err := createCertificate(tmpl, tmpl, key.(crypto.Signer).Public(), key) @@ -99,6 +100,7 @@ func createIntermediate(start time.Time, parentKey crypto.PrivateKey, parent *x5 BasicConstraintsValid: true, IsCA: true, MaxPathLen: 1, + OCSPServer: []string{"http://localhost:9999"}, } c, err := createCertificate(tmpl, parent, key.(crypto.Signer).Public(), parentKey) @@ -126,6 +128,7 @@ func createLeaf(start time.Time, parentKey crypto.PrivateKey, parent *x509.Certi x509.ExtKeyUsageCodeSigning, }, MaxPathLenZero: true, + OCSPServer: []string{"http://localhost:9999"}, } return createCertificate(tmpl, parent, key.(crypto.Signer).Public(), parentKey) diff --git a/test/certs/intermediate.pem b/test/certs/intermediate.pem index b61cc85ae9..4b9d881bfe 100644 --- a/test/certs/intermediate.pem +++ b/test/certs/intermediate.pem @@ -1,12 +1,13 @@ -----BEGIN CERTIFICATE----- -MIIBsDCCAWKgAwIBAgIBAjAFBgMrZXAwMjELMAkGA1UEBhMCVVMxFDASBgNVBAoT +MIIB5TCCAZegAwIBAgIBAjAFBgMrZXAwMjELMAkGA1UEBhMCVVMxFDASBgNVBAoT C1N5bGFicyBJbmMuMQ0wCwYDVQQDEwRyb290MB4XDTIwMDQwMTAwMDAwMFoXDTMw MDQwMTAwMDAwMFowOjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1N5bGFicyBJbmMu MRUwEwYDVQQDEwxpbnRlcm1lZGlhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AASaChR3vOdhomt23dAwin2/NaPnKY0IyYgyG7mOvS+EMkuZ9STlWVgmWUOyc/mq -adBKI1Rj6oAYgIhZ3uuStcOIo2YwZDAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/ -BAgwBgEB/wIBATAdBgNVHQ4EFgQUvHQF8F3OfyfrQJVSuAHwYe83DEowHwYDVR0j -BBgwFoAUmh5LmUbEGS5tTqdXdK5nPamCnvEwBQYDK2VwA0EAuajgsJen1KclDfA+ -57RJT+CXsuZInp/jiY8C9qWp4oq4iw5ajbx7f5s9vnnV1wEnD9AqyffsgBlD5kFA -ycZPBA== +adBKI1Rj6oAYgIhZ3uuStcOIo4GaMIGXMA4GA1UdDwEB/wQEAwICBDASBgNVHRMB +Af8ECDAGAQH/AgEBMB0GA1UdDgQWBBS8dAXwXc5/J+tAlVK4AfBh7zcMSjAfBgNV +HSMEGDAWgBSaHkuZRsQZLm1Op1d0rmc9qYKe8TAxBggrBgEFBQcBAQQlMCMwIQYI +KwYBBQUHMAGGFWh0dHA6Ly9sb2NhbGhvc3Q6OTk5OTAFBgMrZXADQQDuo2DEHdzg +bxTp4t+29tjJUPCUQgxebDxtmzhnGPxMrYJu6BTBK8qD7YwCI+wKs35frgdNr9HU +CiCOcyiFY20K -----END CERTIFICATE----- diff --git a/test/certs/leaf.pem b/test/certs/leaf.pem index 7d92d0f472..03034cd9cd 100644 --- a/test/certs/leaf.pem +++ b/test/certs/leaf.pem @@ -1,5 +1,5 @@ -----BEGIN CERTIFICATE----- -MIIDbjCCAxSgAwIBAgIBAzAKBggqhkjOPQQDAjA6MQswCQYDVQQGEwJVUzEUMBIG +MIIDojCCA0egAwIBAgIBAzAKBggqhkjOPQQDAjA6MQswCQYDVQQGEwJVUzEUMBIG A1UEChMLU3lsYWJzIEluYy4xFTATBgNVBAMTDGludGVybWVkaWF0ZTAeFw0yMDA0 MDEwMDAwMDBaFw0zMDA0MDEwMDAwMDBaMDIxCzAJBgNVBAYTAlVTMRQwEgYDVQQK EwtTeWxhYnMgSW5jLjENMAsGA1UEAxMEbGVhZjCCAiIwDQYJKoZIhvcNAQEBBQAD @@ -14,8 +14,9 @@ jF3c5jNse8+MluAcvLbO34kN9CI1XDw2AgXKd8qzhRaOLrs641ImwyJDpEem/7uM pYrIkkGSUxQlMssYUFTL0sEltpZOWOXoeXLOjVzqkQcaD1PXH9CO0n+fmCJfgHFW QFYJkSKWO6U2OavkA8ooqDUGL+sC+D449PZcIxBqTBfxa1Jaj898novrJ953Zd0Q QbV2mVyCQQzozQUPefLX2zGlQBvZNnA5wNkMbic/e7lVOoCQGOmL/7ZQFqvPAgMB -AAGjSDBGMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNV -HSMEGDAWgBS8dAXwXc5/J+tAlVK4AfBh7zcMSjAKBggqhkjOPQQDAgNIADBFAiEA -optRPfuz01AQ0sd2/SJBq203Gl7/DJH07eOARd2x6u8CIA+bRDno/9eq3xvUPYkM -D1djKUDwjLGUyYfVKmYvBUW8 +AAGjezB5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNV +HSMEGDAWgBS8dAXwXc5/J+tAlVK4AfBh7zcMSjAxBggrBgEFBQcBAQQlMCMwIQYI +KwYBBQUHMAGGFWh0dHA6Ly9sb2NhbGhvc3Q6OTk5OTAKBggqhkjOPQQDAgNJADBG +AiEAw8ZBg0zPU/0WTOHL4jwk+m7wpUqG1bKKuU2R7OiDrwACIQDCGHq4Nuptl5CM +pHIoOkAf1Ns4NS2CNzYE3SCgXWXqSw== -----END CERTIFICATE----- diff --git a/test/certs/root.pem b/test/certs/root.pem index 80518e8ff0..ca126c66b8 100644 --- a/test/certs/root.pem +++ b/test/certs/root.pem @@ -1,10 +1,11 @@ -----BEGIN CERTIFICATE----- -MIIBWDCCAQqgAwIBAgIBATAFBgMrZXAwMjELMAkGA1UEBhMCVVMxFDASBgNVBAoT +MIIBizCCAT2gAwIBAgIBATAFBgMrZXAwMjELMAkGA1UEBhMCVVMxFDASBgNVBAoT C1N5bGFicyBJbmMuMQ0wCwYDVQQDEwRyb290MB4XDTIwMDQwMTAwMDAwMFoXDTMw MDQwMTAwMDAwMFowMjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1N5bGFicyBJbmMu MQ0wCwYDVQQDEwRyb290MCowBQYDK2VwAyEA4LypVa0tjUB5eUQeeGjllrBG7gWC -IOSymuMc6fg8GB6jRTBDMA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/ -AgECMB0GA1UdDgQWBBSaHkuZRsQZLm1Op1d0rmc9qYKe8TAFBgMrZXADQQBQBMHM -l5xCuf7tmeW6BlW6U91+XxKkKIZ6xQSDQsEH6ygURWVDQhXJXw7I9iKc16BXTV6o -UfBN/8aKZcomxCQO +IOSymuMc6fg8GB6jeDB2MA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/ +AgECMB0GA1UdDgQWBBSaHkuZRsQZLm1Op1d0rmc9qYKe8TAxBggrBgEFBQcBAQQl +MCMwIQYIKwYBBQUHMAGGFWh0dHA6Ly9sb2NhbGhvc3Q6OTk5OTAFBgMrZXADQQAP +ri18/2kD2bGZPMt9tZg9w7jWBFJ3EAQyRSlvWgOMCDBQye/vaJDK+7vBfFQS0v9b ++41cevyA+E8Q2r9v810C -----END CERTIFICATE-----