diff --git a/.gitignore b/.gitignore index 3997bea..23a76e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -*.db \ No newline at end of file +*.db +*.pem +*config.yaml + +.DS_Store \ No newline at end of file diff --git a/cmd/gocert/main.go b/cmd/gocert/main.go index a3d179a..5c43364 100644 --- a/cmd/gocert/main.go +++ b/cmd/gocert/main.go @@ -5,8 +5,7 @@ import ( "log" "os" - server "github.com/canonical/gocert/api" - "github.com/canonical/gocert/internal/certdb" + server "github.com/canonical/gocert/internal/api" ) func main() { @@ -17,15 +16,7 @@ func main() { if *configFilePtr == "" { log.Fatalf("Providing a valid config file is required.") } - config, err := server.ValidateConfigFile(*configFilePtr) - if err != nil { - log.Fatalf("Config file validation failed: %s.", err) - } - _, err = certdb.NewCertificateRequestsRepository(config.DBPath, "CertificateRequests") - if err != nil { - log.Fatalf("Couldn't connect to database: %s", err) - } - srv, err := server.NewServer(config.Cert, config.Key, config.Port) + srv, err := server.NewServer(*configFilePtr) if err != nil { log.Fatalf("Couldn't create server: %s", err) } diff --git a/cmd/gocert/main_test.go b/cmd/gocert/main_test.go index 3a784a9..6822761 100644 --- a/cmd/gocert/main_test.go +++ b/cmd/gocert/main_test.go @@ -139,7 +139,7 @@ func TestGoCertFail(t *testing.T) { ExpectedOutput string }{ {"flags not set", []string{}, validConfig, "Providing a valid config file is required."}, - {"config file not valid", []string{"-config", "config.yaml"}, invalidConfig, "Config file validation failed:"}, + {"config file not valid", []string{"-config", "config.yaml"}, invalidConfig, "config file validation failed:"}, {"database not connectable", []string{"-config", "config.yaml"}, invalidDBConfig, "Couldn't connect to database:"}, } for _, tc := range cases { diff --git a/internal/api/handlers.go b/internal/api/handlers.go new file mode 100644 index 0000000..c41545f --- /dev/null +++ b/internal/api/handlers.go @@ -0,0 +1,194 @@ +package server + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strconv" + "strings" +) + +// NewGoCertRouter takes in an environment struct, passes it along to any handlers that will need +// access to it, then builds and returns it for a server to consume +func NewGoCertRouter(env *Environment) http.Handler { + router := http.NewServeMux() + router.HandleFunc("GET /certificate_requests", GetCertificateRequests(env)) + router.HandleFunc("POST /certificate_requests", PostCertificateRequest(env)) + router.HandleFunc("GET /certificate_requests/{id}", GetCertificateRequest(env)) + router.HandleFunc("DELETE /certificate_requests/{id}", DeleteCertificateRequest(env)) + router.HandleFunc("POST /certificate_requests/{id}/certificate", PostCertificate(env)) + router.HandleFunc("DELETE /certificate_requests/{id}/certificate", DeleteCertificate(env)) + + v1 := http.NewServeMux() + v1.HandleFunc("GET /status", HealthCheck) + v1.Handle("/api/v1/", http.StripPrefix("/api/v1", router)) + + return logging(v1) +} + +// the health check endpoint simply returns a http.StatusOK +func HealthCheck(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) //nolint:errcheck +} + +// GetCertificateRequests returns all of the Certificate Requests +func GetCertificateRequests(env *Environment) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + certs, err := env.DB.RetrieveAll() + if err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + body, err := json.Marshal(certs) + if err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + if _, err := w.Write(body); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } + } +} + +// PostCertificateRequest creates a new Certificate Request, and returns the id of the created row +func PostCertificateRequest(env *Environment) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + csr, err := io.ReadAll(r.Body) + if err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + id, err := env.DB.Create(string(csr)) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + logErrorAndWriteResponse("given csr already recorded", http.StatusBadRequest, w) + return + } + if strings.Contains(err.Error(), "csr validation failed") { + logErrorAndWriteResponse(err.Error(), http.StatusBadRequest, w) + return + } + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + w.WriteHeader(http.StatusCreated) + if _, err := w.Write([]byte(strconv.FormatInt(id, 10))); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } + } +} + +// GetCertificateRequests receives an id as a path parameter, and +// returns the corresponding Certificate Request +func GetCertificateRequest(env *Environment) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + cert, err := env.DB.Retrieve(id) + if err != nil { + if err.Error() == "csr id not found" { + logErrorAndWriteResponse(err.Error(), http.StatusBadRequest, w) + return + } + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + body, err := json.Marshal(cert) + if err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + if _, err := w.Write(body); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } + } +} + +// DeleteCertificateRequest handler receives an id as a path parameter, +// deletes the corresponding Certificate Request, and returns a http.StatusNoContent on success +func DeleteCertificateRequest(env *Environment) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + insertId, err := env.DB.Delete(id) + if err != nil { + if err.Error() == "csr id not found" { + logErrorAndWriteResponse(err.Error(), http.StatusBadRequest, w) + return + } + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + w.WriteHeader(http.StatusAccepted) + if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } + } +} + +// PostCertificate handler receives an id as a path parameter, +// and attempts to add a given certificate to the corresponding certificate request +func PostCertificate(env *Environment) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cert, err := io.ReadAll(r.Body) + if err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusBadRequest, w) + return + } + id := r.PathValue("id") + insertId, err := env.DB.Update(id, string(cert)) + if err != nil { + if err.Error() == "csr id not found" || + err.Error() == "certificate does not match CSR" || + strings.Contains(err.Error(), "cert validation failed") { + logErrorAndWriteResponse(err.Error(), http.StatusBadRequest, w) + return + } + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + w.WriteHeader(http.StatusCreated) + if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } + } +} + +// DeleteCertificate handler receives an id as a path parameter, +// and attempts to add a given certificate to the corresponding certificate request +func DeleteCertificate(env *Environment) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + insertId, err := env.DB.Update(id, "") + if err != nil { + if err.Error() == "csr id not found" { + logErrorAndWriteResponse(err.Error(), http.StatusBadRequest, w) + return + } + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + return + } + w.WriteHeader(http.StatusAccepted) + if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } + } +} + +// The logging middleware captures any http request coming through, and logs it +func logging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + log.Println(r.Method, r.URL.Path) + }) +} + +// logErrorAndWriteResponse is a helper function that logs any error and writes it back as an http response +func logErrorAndWriteResponse(msg string, status int, w http.ResponseWriter) { + errMsg := fmt.Sprintf("error: %s", msg) + log.Println(errMsg) + w.WriteHeader(status) + if _, err := w.Write([]byte(errMsg)); err != nil { + logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w) + } +} diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go new file mode 100644 index 0000000..397e116 --- /dev/null +++ b/internal/api/handlers_test.go @@ -0,0 +1,329 @@ +package server_test + +import ( + "io" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + + server "github.com/canonical/gocert/internal/api" + "github.com/canonical/gocert/internal/certdb" +) + +const ( + validCSR1 = `-----BEGIN CERTIFICATE REQUEST----- +MIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDC5KgrADpuOUPwSh0YLmpWF66VTcciIGC2HcGn +oJknL7pm5q9qhfWGIdvKKlIA6cBB32jPd0QcYDsx7+AvzEvBuO7mq7v2Q1sPU4Q+ +L0s2pLJges6/cnDWvk/p5eBjDLOqHhUNzpMUga9SgIod8yymTZm3eqQvt1ABdwTg +FzBs5QdSm2Ny1fEbbcRE+Rv5rqXyJb2isXSujzSuS22VqslDIyqnY5WaLg+pjZyR ++0j13ecJsdh6/MJMUZWheimV2Yv7SFtxzFwbzBMO9YFS098sy4F896eBHLNe9cUC ++d1JDtLaewlMogjHBHAxmP54dhe6vvc78anElKKP4hm5N5nlAgMBAAGgWDBWBgkq +hkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL +BQADggEBACP1VKEGVYKoVLMDJS+EZ0CPwIYWsO4xBXgK6atHe8WIChVn/8I7eo60 +cuMDiy4LR70G++xL1tpmYGRbx21r9d/shL2ehp9VdClX06qxlcGxiC/F8eThRuS5 +zHcdNqSVyMoLJ0c7yWHJahN5u2bn1Lov34yOEqGGpWCGF/gT1nEvM+p/v30s89f2 +Y/uPl4g3jpGqLCKTASWJDGnZLroLICOzYTVs5P3oj+VueSUwYhGK5tBnS2x5FHID +uMNMgwl0fxGMQZjrlXyCBhXBm1k6PmwcJGJF5LQ31c+5aTTMFU7SyZhlymctB8mS +y+ErBQsRpcQho6Ok+HTXQQUcx7WNcwI= +-----END CERTIFICATE REQUEST-----` + validCSR2 = `-----BEGIN CERTIFICATE REQUEST----- +MIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk +MzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi +pUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI +69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW +XcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO +yae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW +Kl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ +KoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs +dC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3 +DQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V +FGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa +uHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87 +cAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+ +RSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1 +H9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI +-----END CERTIFICATE REQUEST-----` + validCSR3 = `-----BEGIN CERTIFICATE REQUEST----- +MIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDN7tHggWTtxiT5Sh5Npoif8J2BdpJjtMdpZ7Vu +NVzMxW/eojSRlq0p3nafmpjnSdSH1k/XMmPsgmv9txxEHMw1LIUJUef2QVrQTI6J +4ueu9NvexZWXZ+UxFip63PKyn/CkZRFiHCRIGzDDPxM2aApjghXy9ISMtGqDVSnr +5hQDu2U1CEiUWKMoTpyk/KlBZliDDOzaGm3cQuzKWs6Stjzpq+uX4ecJAXZg5Cj+ ++JUETH93A/VOfsiiHXoKeTnFMCsmJgEHz2DZixw8EN8XgpOp5BA2n8Y/xS+Ren5R +ZH7uNJI/SmQ0yrR+2bYR6hm+4bCzspyCfzbiuI5IS9+2eXA/AgMBAAGgWDBWBgkq +hkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL +BQADggEBAB/aPfYLbnCubYyKnxLRipoLr3TBSYFnRfcxiZR1o+L3/tuv2NlrXJjY +K13xzzPhwuZwd6iKfX3xC33sKgnUNFawyE8IuAmyhJ2cl97iA2lwoYcyuWP9TOEx +LT60zxp7PHsKo53gqaqRJ5B9RZtiv1jYdUZvynHP4J5JG7Zwaa0VNi/Cx5cwGW8K +rfvNABPUAU6xIqqYgd2heDPF6kjvpoNiOl056qIAbk0dbmpqOJf/lxKBRfqlHhSC +0qRScGu70l2Oxl89YSsfGtUyQuzTkLshI2VkEUM+W/ZauXbxLd8SyWveH3/7mDC+ +Sgi7T+lz+c1Tw+XFgkqryUwMeG2wxt8= +-----END CERTIFICATE REQUEST-----` + validCert2 = `-----BEGIN CERTIFICATE----- +MIIDrDCCApSgAwIBAgIURKr+jf7hj60SyAryIeN++9wDdtkwDQYJKoZIhvcNAQEL +BQAwOTELMAkGA1UEBhMCVVMxKjAoBgNVBAMMIXNlbGYtc2lnbmVkLWNlcnRpZmlj +YXRlcy1vcGVyYXRvcjAeFw0yNDAzMjcxMjQ4MDRaFw0yNTAzMjcxMjQ4MDRaMEcx +FjAUBgNVBAMMDTEwLjE1Mi4xODMuNTMxLTArBgNVBC0MJDM5YWNlMTk1LWRjNWEt +NDMyYi04MDkwLWFmZTZhYjRiNDljZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAIzOVs/h0bQ73kc3g5BAzOKK5yGnn3vHfJ5hYqVGrfaglN1Pe/fBUeOt +Ax4wixsRccZqEYrcx7WkeHsOR9N+h5RgdtMmrPwjA4ElCOvQ8gLMj8BCajlgRjtS +y+dKl3Zef4zpDtr6APY+VLA9QrVkpJcoWzYVjKWKo9jm1l3GmI/Rsf08GIcPMbCf +0EVsgq7441cx/PuH63w4QVTsssDe7KviN1sAm8gQXDfLjsmntG/6uflCVIqoKXjg +If+GO6iN2GqzI+HJu3cJoglY3nVMIeuc1ch7UFHy3fJxVipfe++ZjQXNconLyg1N +2QeYWdTGbaWzAcGgNcjEhK7P34eSml8CAwEAAaOBnTCBmjAhBgNVHSMEGjAYgBYE +FN/vgl9cAapV7hH9lEyM7qYS958aMB0GA1UdDgQWBBRJJDZkHr64VqTC24DPQVld +Ba3iPDAMBgNVHRMBAf8EAjAAMEgGA1UdEQRBMD+CN3ZhdWx0LWs4cy0wLnZhdWx0 +LWs4cy1lbmRwb2ludHMudmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWyHBAqYtzUwDQYJ +KoZIhvcNAQELBQADggEBAEH9NTwDiSsoQt/QXkWPMBrB830K0dlwKl5WBNgVxFP+ +hSfQ86xN77jNSp2VxOksgzF9J9u/ubAXvSFsou4xdP8MevBXoFJXeqMERq5RW3gc +WyhXkzguv3dwH+n43GJFP6MQ+n9W/nPZCUQ0Iy7ueAvj0HFhGyZzAE2wxNFZdvCs +gCX3nqYpp70oZIFDrhmYwE5ij5KXlHD4/1IOfNUKCDmQDgGPLI1tVtwQLjeRq7Hg +XVelpl/LXTQawmJyvDaVT/Q9P+WqoDiMjrqF6Sy7DzNeeccWVqvqX5TVS6Ky56iS +Mvo/+PAJHkBciR5Xn+Wg2a+7vrZvT6CBoRSOTozlLSM= +-----END CERTIFICATE-----` +) + +const ( + expectedGetAllCertsResponseBody1 = "[{\"ID\":1,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDC5KgrADpuOUPwSh0YLmpWF66VTcciIGC2HcGn\\noJknL7pm5q9qhfWGIdvKKlIA6cBB32jPd0QcYDsx7+AvzEvBuO7mq7v2Q1sPU4Q+\\nL0s2pLJges6/cnDWvk/p5eBjDLOqHhUNzpMUga9SgIod8yymTZm3eqQvt1ABdwTg\\nFzBs5QdSm2Ny1fEbbcRE+Rv5rqXyJb2isXSujzSuS22VqslDIyqnY5WaLg+pjZyR\\n+0j13ecJsdh6/MJMUZWheimV2Yv7SFtxzFwbzBMO9YFS098sy4F896eBHLNe9cUC\\n+d1JDtLaewlMogjHBHAxmP54dhe6vvc78anElKKP4hm5N5nlAgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBACP1VKEGVYKoVLMDJS+EZ0CPwIYWsO4xBXgK6atHe8WIChVn/8I7eo60\\ncuMDiy4LR70G++xL1tpmYGRbx21r9d/shL2ehp9VdClX06qxlcGxiC/F8eThRuS5\\nzHcdNqSVyMoLJ0c7yWHJahN5u2bn1Lov34yOEqGGpWCGF/gT1nEvM+p/v30s89f2\\nY/uPl4g3jpGqLCKTASWJDGnZLroLICOzYTVs5P3oj+VueSUwYhGK5tBnS2x5FHID\\nuMNMgwl0fxGMQZjrlXyCBhXBm1k6PmwcJGJF5LQ31c+5aTTMFU7SyZhlymctB8mS\\ny+ErBQsRpcQho6Ok+HTXQQUcx7WNcwI=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}]" + expectedGetAllCertsResponseBody2 = "[{\"ID\":1,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDC5KgrADpuOUPwSh0YLmpWF66VTcciIGC2HcGn\\noJknL7pm5q9qhfWGIdvKKlIA6cBB32jPd0QcYDsx7+AvzEvBuO7mq7v2Q1sPU4Q+\\nL0s2pLJges6/cnDWvk/p5eBjDLOqHhUNzpMUga9SgIod8yymTZm3eqQvt1ABdwTg\\nFzBs5QdSm2Ny1fEbbcRE+Rv5rqXyJb2isXSujzSuS22VqslDIyqnY5WaLg+pjZyR\\n+0j13ecJsdh6/MJMUZWheimV2Yv7SFtxzFwbzBMO9YFS098sy4F896eBHLNe9cUC\\n+d1JDtLaewlMogjHBHAxmP54dhe6vvc78anElKKP4hm5N5nlAgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBACP1VKEGVYKoVLMDJS+EZ0CPwIYWsO4xBXgK6atHe8WIChVn/8I7eo60\\ncuMDiy4LR70G++xL1tpmYGRbx21r9d/shL2ehp9VdClX06qxlcGxiC/F8eThRuS5\\nzHcdNqSVyMoLJ0c7yWHJahN5u2bn1Lov34yOEqGGpWCGF/gT1nEvM+p/v30s89f2\\nY/uPl4g3jpGqLCKTASWJDGnZLroLICOzYTVs5P3oj+VueSUwYhGK5tBnS2x5FHID\\nuMNMgwl0fxGMQZjrlXyCBhXBm1k6PmwcJGJF5LQ31c+5aTTMFU7SyZhlymctB8mS\\ny+ErBQsRpcQho6Ok+HTXQQUcx7WNcwI=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"},{\"ID\":2,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk\\nMzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG\\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi\\npUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI\\n69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW\\nXcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO\\nyae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW\\nKl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ\\nKoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs\\ndC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3\\nDQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V\\nFGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa\\nuHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87\\ncAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+\\nRSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1\\nH9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}]" + expectedGetAllCertsResponseBody3 = "[{\"ID\":2,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk\\nMzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG\\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi\\npUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI\\n69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW\\nXcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO\\nyae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW\\nKl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ\\nKoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs\\ndC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3\\nDQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V\\nFGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa\\nuHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87\\ncAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+\\nRSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1\\nH9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIDrDCCApSgAwIBAgIURKr+jf7hj60SyAryIeN++9wDdtkwDQYJKoZIhvcNAQEL\\nBQAwOTELMAkGA1UEBhMCVVMxKjAoBgNVBAMMIXNlbGYtc2lnbmVkLWNlcnRpZmlj\\nYXRlcy1vcGVyYXRvcjAeFw0yNDAzMjcxMjQ4MDRaFw0yNTAzMjcxMjQ4MDRaMEcx\\nFjAUBgNVBAMMDTEwLjE1Mi4xODMuNTMxLTArBgNVBC0MJDM5YWNlMTk1LWRjNWEt\\nNDMyYi04MDkwLWFmZTZhYjRiNDljZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\\nAQoCggEBAIzOVs/h0bQ73kc3g5BAzOKK5yGnn3vHfJ5hYqVGrfaglN1Pe/fBUeOt\\nAx4wixsRccZqEYrcx7WkeHsOR9N+h5RgdtMmrPwjA4ElCOvQ8gLMj8BCajlgRjtS\\ny+dKl3Zef4zpDtr6APY+VLA9QrVkpJcoWzYVjKWKo9jm1l3GmI/Rsf08GIcPMbCf\\n0EVsgq7441cx/PuH63w4QVTsssDe7KviN1sAm8gQXDfLjsmntG/6uflCVIqoKXjg\\nIf+GO6iN2GqzI+HJu3cJoglY3nVMIeuc1ch7UFHy3fJxVipfe++ZjQXNconLyg1N\\n2QeYWdTGbaWzAcGgNcjEhK7P34eSml8CAwEAAaOBnTCBmjAhBgNVHSMEGjAYgBYE\\nFN/vgl9cAapV7hH9lEyM7qYS958aMB0GA1UdDgQWBBRJJDZkHr64VqTC24DPQVld\\nBa3iPDAMBgNVHRMBAf8EAjAAMEgGA1UdEQRBMD+CN3ZhdWx0LWs4cy0wLnZhdWx0\\nLWs4cy1lbmRwb2ludHMudmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWyHBAqYtzUwDQYJ\\nKoZIhvcNAQELBQADggEBAEH9NTwDiSsoQt/QXkWPMBrB830K0dlwKl5WBNgVxFP+\\nhSfQ86xN77jNSp2VxOksgzF9J9u/ubAXvSFsou4xdP8MevBXoFJXeqMERq5RW3gc\\nWyhXkzguv3dwH+n43GJFP6MQ+n9W/nPZCUQ0Iy7ueAvj0HFhGyZzAE2wxNFZdvCs\\ngCX3nqYpp70oZIFDrhmYwE5ij5KXlHD4/1IOfNUKCDmQDgGPLI1tVtwQLjeRq7Hg\\nXVelpl/LXTQawmJyvDaVT/Q9P+WqoDiMjrqF6Sy7DzNeeccWVqvqX5TVS6Ky56iS\\nMvo/+PAJHkBciR5Xn+Wg2a+7vrZvT6CBoRSOTozlLSM=\\n-----END CERTIFICATE-----\"},{\"ID\":3,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDN7tHggWTtxiT5Sh5Npoif8J2BdpJjtMdpZ7Vu\\nNVzMxW/eojSRlq0p3nafmpjnSdSH1k/XMmPsgmv9txxEHMw1LIUJUef2QVrQTI6J\\n4ueu9NvexZWXZ+UxFip63PKyn/CkZRFiHCRIGzDDPxM2aApjghXy9ISMtGqDVSnr\\n5hQDu2U1CEiUWKMoTpyk/KlBZliDDOzaGm3cQuzKWs6Stjzpq+uX4ecJAXZg5Cj+\\n+JUETH93A/VOfsiiHXoKeTnFMCsmJgEHz2DZixw8EN8XgpOp5BA2n8Y/xS+Ren5R\\nZH7uNJI/SmQ0yrR+2bYR6hm+4bCzspyCfzbiuI5IS9+2eXA/AgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBAB/aPfYLbnCubYyKnxLRipoLr3TBSYFnRfcxiZR1o+L3/tuv2NlrXJjY\\nK13xzzPhwuZwd6iKfX3xC33sKgnUNFawyE8IuAmyhJ2cl97iA2lwoYcyuWP9TOEx\\nLT60zxp7PHsKo53gqaqRJ5B9RZtiv1jYdUZvynHP4J5JG7Zwaa0VNi/Cx5cwGW8K\\nrfvNABPUAU6xIqqYgd2heDPF6kjvpoNiOl056qIAbk0dbmpqOJf/lxKBRfqlHhSC\\n0qRScGu70l2Oxl89YSsfGtUyQuzTkLshI2VkEUM+W/ZauXbxLd8SyWveH3/7mDC+\\nSgi7T+lz+c1Tw+XFgkqryUwMeG2wxt8=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"},{\"ID\":4,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDC5KgrADpuOUPwSh0YLmpWF66VTcciIGC2HcGn\\noJknL7pm5q9qhfWGIdvKKlIA6cBB32jPd0QcYDsx7+AvzEvBuO7mq7v2Q1sPU4Q+\\nL0s2pLJges6/cnDWvk/p5eBjDLOqHhUNzpMUga9SgIod8yymTZm3eqQvt1ABdwTg\\nFzBs5QdSm2Ny1fEbbcRE+Rv5rqXyJb2isXSujzSuS22VqslDIyqnY5WaLg+pjZyR\\n+0j13ecJsdh6/MJMUZWheimV2Yv7SFtxzFwbzBMO9YFS098sy4F896eBHLNe9cUC\\n+d1JDtLaewlMogjHBHAxmP54dhe6vvc78anElKKP4hm5N5nlAgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBACP1VKEGVYKoVLMDJS+EZ0CPwIYWsO4xBXgK6atHe8WIChVn/8I7eo60\\ncuMDiy4LR70G++xL1tpmYGRbx21r9d/shL2ehp9VdClX06qxlcGxiC/F8eThRuS5\\nzHcdNqSVyMoLJ0c7yWHJahN5u2bn1Lov34yOEqGGpWCGF/gT1nEvM+p/v30s89f2\\nY/uPl4g3jpGqLCKTASWJDGnZLroLICOzYTVs5P3oj+VueSUwYhGK5tBnS2x5FHID\\nuMNMgwl0fxGMQZjrlXyCBhXBm1k6PmwcJGJF5LQ31c+5aTTMFU7SyZhlymctB8mS\\ny+ErBQsRpcQho6Ok+HTXQQUcx7WNcwI=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}]" + expectedGetAllCertsResponseBody4 = "[{\"ID\":2,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk\\nMzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG\\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi\\npUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI\\n69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW\\nXcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO\\nyae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW\\nKl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ\\nKoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs\\ndC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3\\nDQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V\\nFGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa\\nuHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87\\ncAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+\\nRSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1\\nH9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"},{\"ID\":3,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDN7tHggWTtxiT5Sh5Npoif8J2BdpJjtMdpZ7Vu\\nNVzMxW/eojSRlq0p3nafmpjnSdSH1k/XMmPsgmv9txxEHMw1LIUJUef2QVrQTI6J\\n4ueu9NvexZWXZ+UxFip63PKyn/CkZRFiHCRIGzDDPxM2aApjghXy9ISMtGqDVSnr\\n5hQDu2U1CEiUWKMoTpyk/KlBZliDDOzaGm3cQuzKWs6Stjzpq+uX4ecJAXZg5Cj+\\n+JUETH93A/VOfsiiHXoKeTnFMCsmJgEHz2DZixw8EN8XgpOp5BA2n8Y/xS+Ren5R\\nZH7uNJI/SmQ0yrR+2bYR6hm+4bCzspyCfzbiuI5IS9+2eXA/AgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBAB/aPfYLbnCubYyKnxLRipoLr3TBSYFnRfcxiZR1o+L3/tuv2NlrXJjY\\nK13xzzPhwuZwd6iKfX3xC33sKgnUNFawyE8IuAmyhJ2cl97iA2lwoYcyuWP9TOEx\\nLT60zxp7PHsKo53gqaqRJ5B9RZtiv1jYdUZvynHP4J5JG7Zwaa0VNi/Cx5cwGW8K\\nrfvNABPUAU6xIqqYgd2heDPF6kjvpoNiOl056qIAbk0dbmpqOJf/lxKBRfqlHhSC\\n0qRScGu70l2Oxl89YSsfGtUyQuzTkLshI2VkEUM+W/ZauXbxLd8SyWveH3/7mDC+\\nSgi7T+lz+c1Tw+XFgkqryUwMeG2wxt8=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"},{\"ID\":4,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDC5KgrADpuOUPwSh0YLmpWF66VTcciIGC2HcGn\\noJknL7pm5q9qhfWGIdvKKlIA6cBB32jPd0QcYDsx7+AvzEvBuO7mq7v2Q1sPU4Q+\\nL0s2pLJges6/cnDWvk/p5eBjDLOqHhUNzpMUga9SgIod8yymTZm3eqQvt1ABdwTg\\nFzBs5QdSm2Ny1fEbbcRE+Rv5rqXyJb2isXSujzSuS22VqslDIyqnY5WaLg+pjZyR\\n+0j13ecJsdh6/MJMUZWheimV2Yv7SFtxzFwbzBMO9YFS098sy4F896eBHLNe9cUC\\n+d1JDtLaewlMogjHBHAxmP54dhe6vvc78anElKKP4hm5N5nlAgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBACP1VKEGVYKoVLMDJS+EZ0CPwIYWsO4xBXgK6atHe8WIChVn/8I7eo60\\ncuMDiy4LR70G++xL1tpmYGRbx21r9d/shL2ehp9VdClX06qxlcGxiC/F8eThRuS5\\nzHcdNqSVyMoLJ0c7yWHJahN5u2bn1Lov34yOEqGGpWCGF/gT1nEvM+p/v30s89f2\\nY/uPl4g3jpGqLCKTASWJDGnZLroLICOzYTVs5P3oj+VueSUwYhGK5tBnS2x5FHID\\nuMNMgwl0fxGMQZjrlXyCBhXBm1k6PmwcJGJF5LQ31c+5aTTMFU7SyZhlymctB8mS\\ny+ErBQsRpcQho6Ok+HTXQQUcx7WNcwI=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}]" + expectedGetCertReqResponseBody1 = "{\"ID\":2,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk\\nMzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG\\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi\\npUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI\\n69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW\\nXcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO\\nyae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW\\nKl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ\\nKoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs\\ndC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3\\nDQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V\\nFGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa\\nuHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87\\ncAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+\\nRSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1\\nH9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}" + expectedGetCertReqResponseBody2 = "{\"ID\":4,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICszCCAZsCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3\\nDQEBAQUAA4IBDwAwggEKAoIBAQDC5KgrADpuOUPwSh0YLmpWF66VTcciIGC2HcGn\\noJknL7pm5q9qhfWGIdvKKlIA6cBB32jPd0QcYDsx7+AvzEvBuO7mq7v2Q1sPU4Q+\\nL0s2pLJges6/cnDWvk/p5eBjDLOqHhUNzpMUga9SgIod8yymTZm3eqQvt1ABdwTg\\nFzBs5QdSm2Ny1fEbbcRE+Rv5rqXyJb2isXSujzSuS22VqslDIyqnY5WaLg+pjZyR\\n+0j13ecJsdh6/MJMUZWheimV2Yv7SFtxzFwbzBMO9YFS098sy4F896eBHLNe9cUC\\n+d1JDtLaewlMogjHBHAxmP54dhe6vvc78anElKKP4hm5N5nlAgMBAAGgWDBWBgkq\\nhkiG9w0BCQ4xSTBHMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD\\nAQYIKwYBBQUHAwIwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEL\\nBQADggEBACP1VKEGVYKoVLMDJS+EZ0CPwIYWsO4xBXgK6atHe8WIChVn/8I7eo60\\ncuMDiy4LR70G++xL1tpmYGRbx21r9d/shL2ehp9VdClX06qxlcGxiC/F8eThRuS5\\nzHcdNqSVyMoLJ0c7yWHJahN5u2bn1Lov34yOEqGGpWCGF/gT1nEvM+p/v30s89f2\\nY/uPl4g3jpGqLCKTASWJDGnZLroLICOzYTVs5P3oj+VueSUwYhGK5tBnS2x5FHID\\nuMNMgwl0fxGMQZjrlXyCBhXBm1k6PmwcJGJF5LQ31c+5aTTMFU7SyZhlymctB8mS\\ny+ErBQsRpcQho6Ok+HTXQQUcx7WNcwI=\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}" + expectedGetCertReqResponseBody3 = "{\"ID\":2,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk\\nMzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG\\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi\\npUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI\\n69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW\\nXcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO\\nyae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW\\nKl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ\\nKoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs\\ndC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3\\nDQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V\\nFGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa\\nuHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87\\ncAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+\\nRSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1\\nH9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIDrDCCApSgAwIBAgIURKr+jf7hj60SyAryIeN++9wDdtkwDQYJKoZIhvcNAQEL\\nBQAwOTELMAkGA1UEBhMCVVMxKjAoBgNVBAMMIXNlbGYtc2lnbmVkLWNlcnRpZmlj\\nYXRlcy1vcGVyYXRvcjAeFw0yNDAzMjcxMjQ4MDRaFw0yNTAzMjcxMjQ4MDRaMEcx\\nFjAUBgNVBAMMDTEwLjE1Mi4xODMuNTMxLTArBgNVBC0MJDM5YWNlMTk1LWRjNWEt\\nNDMyYi04MDkwLWFmZTZhYjRiNDljZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\\nAQoCggEBAIzOVs/h0bQ73kc3g5BAzOKK5yGnn3vHfJ5hYqVGrfaglN1Pe/fBUeOt\\nAx4wixsRccZqEYrcx7WkeHsOR9N+h5RgdtMmrPwjA4ElCOvQ8gLMj8BCajlgRjtS\\ny+dKl3Zef4zpDtr6APY+VLA9QrVkpJcoWzYVjKWKo9jm1l3GmI/Rsf08GIcPMbCf\\n0EVsgq7441cx/PuH63w4QVTsssDe7KviN1sAm8gQXDfLjsmntG/6uflCVIqoKXjg\\nIf+GO6iN2GqzI+HJu3cJoglY3nVMIeuc1ch7UFHy3fJxVipfe++ZjQXNconLyg1N\\n2QeYWdTGbaWzAcGgNcjEhK7P34eSml8CAwEAAaOBnTCBmjAhBgNVHSMEGjAYgBYE\\nFN/vgl9cAapV7hH9lEyM7qYS958aMB0GA1UdDgQWBBRJJDZkHr64VqTC24DPQVld\\nBa3iPDAMBgNVHRMBAf8EAjAAMEgGA1UdEQRBMD+CN3ZhdWx0LWs4cy0wLnZhdWx0\\nLWs4cy1lbmRwb2ludHMudmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWyHBAqYtzUwDQYJ\\nKoZIhvcNAQELBQADggEBAEH9NTwDiSsoQt/QXkWPMBrB830K0dlwKl5WBNgVxFP+\\nhSfQ86xN77jNSp2VxOksgzF9J9u/ubAXvSFsou4xdP8MevBXoFJXeqMERq5RW3gc\\nWyhXkzguv3dwH+n43GJFP6MQ+n9W/nPZCUQ0Iy7ueAvj0HFhGyZzAE2wxNFZdvCs\\ngCX3nqYpp70oZIFDrhmYwE5ij5KXlHD4/1IOfNUKCDmQDgGPLI1tVtwQLjeRq7Hg\\nXVelpl/LXTQawmJyvDaVT/Q9P+WqoDiMjrqF6Sy7DzNeeccWVqvqX5TVS6Ky56iS\\nMvo/+PAJHkBciR5Xn+Wg2a+7vrZvT6CBoRSOTozlLSM=\\n-----END CERTIFICATE-----\"}" + expectedGetCertReqResponseBody4 = "{\"ID\":2,\"CSR\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk\\nMzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG\\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi\\npUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI\\n69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW\\nXcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO\\nyae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW\\nKl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ\\nKoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs\\ndC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3\\nDQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V\\nFGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa\\nuHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87\\ncAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+\\nRSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1\\nH9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI\\n-----END CERTIFICATE REQUEST-----\",\"Certificate\":\"\"}" +) + +func TestGoCertRouter(t *testing.T) { + testdb, err := certdb.NewCertificateRequestsRepository(":memory:", "CertificateRequests") + if err != nil { + log.Fatalf("couldn't create test sqlite db: %s", err) + } + env := &server.Environment{} + env.DB = testdb + ts := httptest.NewTLSServer(server.NewGoCertRouter(env)) + defer ts.Close() + + client := ts.Client() + + testCases := []struct { + desc string + method string + path string + data string + response string + status int + }{ + { + desc: "healthcheck success", + method: "GET", + path: "/status", + data: "", + response: "", + status: http.StatusOK, + }, + { + desc: "empty get csrs success", + method: "GET", + path: "/api/v1/certificate_requests", + data: "", + response: "null", + status: http.StatusOK, + }, + { + desc: "post csr1 fail", + method: "POST", + path: "/api/v1/certificate_requests", + data: "this is very clearly not a csr", + response: "error: csr validation failed: PEM Certificate Request string not found or malformed", + status: http.StatusBadRequest, + }, + { + desc: "post csr1 success", + method: "POST", + path: "/api/v1/certificate_requests", + data: validCSR1, + response: "1", + status: http.StatusCreated, + }, + { + desc: "get csrs 1 success", + method: "GET", + path: "/api/v1/certificate_requests", + data: "", + response: expectedGetAllCertsResponseBody1, + status: http.StatusOK, + }, + { + desc: "post csr2 success", + method: "POST", + path: "/api/v1/certificate_requests", + data: validCSR2, + response: "2", + status: http.StatusCreated, + }, + { + desc: "get csrs 2 success", + method: "GET", + path: "/api/v1/certificate_requests", + data: "", + response: expectedGetAllCertsResponseBody2, + status: http.StatusOK, + }, + { + desc: "post csr2 fail", + method: "POST", + path: "/api/v1/certificate_requests", + data: validCSR2, + response: "error: given csr already recorded", + status: http.StatusBadRequest, + }, + { + desc: "post csr3 success", + method: "POST", + path: "/api/v1/certificate_requests", + data: validCSR3, + response: "3", + status: http.StatusCreated, + }, + { + desc: "delete csr1 success", + method: "DELETE", + path: "/api/v1/certificate_requests/1", + data: "", + response: "1", + status: http.StatusAccepted, + }, + { + desc: "delete csr5 fail", + method: "DELETE", + path: "/api/v1/certificate_requests/5", + data: "", + response: "error: csr id not found", + status: http.StatusBadRequest, + }, + { + desc: "get csr1 fail", + method: "GET", + path: "/api/v1/certificate_requests/1", + data: "", + response: "error: csr id not found", + status: http.StatusBadRequest, + }, + { + desc: "get csr2 success", + method: "GET", + path: "/api/v1/certificate_requests/2", + data: "", + response: expectedGetCertReqResponseBody1, + status: http.StatusOK, + }, + { + desc: "post csr4 success", + method: "POST", + path: "/api/v1/certificate_requests", + data: validCSR1, + response: "4", + status: http.StatusCreated, + }, + { + desc: "get csr4 success", + method: "GET", + path: "/api/v1/certificate_requests/4", + data: "", + response: expectedGetCertReqResponseBody2, + status: http.StatusOK, + }, + { + desc: "post cert2 fail 1", + method: "POST", + path: "/api/v1/certificate_requests/4/certificate", + data: validCert2, + response: "error: cert validation failed: certificate does not match CSR", + status: http.StatusBadRequest, + }, + { + desc: "post cert2 fail 2", + method: "POST", + path: "/api/v1/certificate_requests/4/certificate", + data: "some random data that's clearly not a cert", + response: "error: cert validation failed: PEM Certificate string not found or malformed", + status: http.StatusBadRequest, + }, + { + desc: "post cert2 success", + method: "POST", + path: "/api/v1/certificate_requests/2/certificate", + data: validCert2, + response: "4", + status: http.StatusCreated, + }, + { + desc: "get csr2 success", + method: "GET", + path: "/api/v1/certificate_requests/2", + data: "", + response: expectedGetCertReqResponseBody3, + status: http.StatusOK, + }, + { + desc: "delete csr2 cert success", + method: "DELETE", + path: "/api/v1/certificate_requests/2/certificate", + data: "", + response: "4", + status: http.StatusAccepted, + }, + { + desc: "get csr2 success", + method: "GET", + path: "/api/v1/certificate_requests/2", + data: "", + response: expectedGetCertReqResponseBody4, + status: http.StatusOK, + }, + { + desc: "get csrs 3 success", + method: "GET", + path: "/api/v1/certificate_requests", + data: "", + response: expectedGetAllCertsResponseBody4, + status: http.StatusOK, + }, + { + desc: "healthcheck success", + method: "GET", + path: "/status", + data: "", + response: "", + status: http.StatusOK, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader(tC.data)) + if err != nil { + t.Fatal(err) + } + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + resBody, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != tC.status || string(resBody) != tC.response { + t.Errorf("expected response did not match.\nExpected vs Received status code: %d vs %d\nExpected vs Received body: \n%s\nvs\n%s\n", tC.status, res.StatusCode, tC.response, string(resBody)) + } + }) + } + +} diff --git a/api/server.go b/internal/api/server.go similarity index 62% rename from api/server.go rename to internal/api/server.go index 386b7d7..edbb233 100644 --- a/api/server.go +++ b/internal/api/server.go @@ -5,10 +5,12 @@ import ( "crypto/tls" "errors" "fmt" + "log" "net/http" "os" "time" + "github.com/canonical/gocert/internal/certdb" "gopkg.in/yaml.v3" ) @@ -26,7 +28,12 @@ type Config struct { Port int } -func ValidateConfigFile(filePath string) (Config, error) { +type Environment struct { + DB *certdb.CertificateRequestsRepository +} + +// validateConfigFile opens and processes the given yaml file, and catches errors in the process +func validateConfigFile(filePath string) (Config, error) { validationErr := errors.New("config file validation failed: ") config := Config{} configYaml, err := os.ReadFile(filePath) @@ -61,24 +68,31 @@ func ValidateConfigFile(filePath string) (Config, error) { return config, nil } -// NewServer creates a new http server with handlers that Go can start listening to -func NewServer(certificate, key []byte, port int) (*http.Server, error) { - serverCerts, err := tls.X509KeyPair(certificate, key) +// NewServer creates an environment and an http server with handlers that Go can start listening to +func NewServer(configFile string) (*http.Server, error) { + config, err := validateConfigFile(configFile) + if err != nil { + return nil, err + } + serverCerts, err := tls.X509KeyPair(config.Cert, config.Key) if err != nil { return nil, err } - router := http.NewServeMux() - router.HandleFunc("GET /", HealthCheck) + db, err := certdb.NewCertificateRequestsRepository(config.DBPath, "CertificateRequests") + if err != nil { + log.Fatalf("Couldn't connect to database: %s", err) + } - v1 := http.NewServeMux() - v1.Handle("/v1/", http.StripPrefix("/v1", router)) + env := &Environment{} + env.DB = db + router := NewGoCertRouter(env) s := &http.Server{ - Addr: fmt.Sprintf(":%d", port), + Addr: fmt.Sprintf(":%d", config.Port), ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, - Handler: v1, + Handler: router, MaxHeaderBytes: 1 << 20, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{serverCerts}, @@ -87,7 +101,3 @@ func NewServer(certificate, key []byte, port int) (*http.Server, error) { return s, nil } - -func HealthCheck(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Server Alive")) //nolint:errcheck -} diff --git a/api/server_test.go b/internal/api/server_test.go similarity index 86% rename from api/server_test.go rename to internal/api/server_test.go index 336c055..1415acf 100644 --- a/api/server_test.go +++ b/internal/api/server_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - server "github.com/canonical/gocert/api" + server "github.com/canonical/gocert/internal/api" ) const ( @@ -88,6 +88,14 @@ D7DC34n8CH9+avz9sCRwxpjxKnYW/BeyK0c4n9uZpjI8N4sOVqy6yWBUseww validConfig = `keypath: "./key_test.pem" certpath: "./cert_test.pem" dbpath: "./certs.db" +port: 8000` + wrongCertConfig = `keypath: "./key_test.pem" +certpath: "./cert_test_wrong.pem" +dbpath: "./certs.db" +port: 8000` + wrongKeyConfig = `keypath: "./key_test_wrong.pem" +certpath: "./cert_test.pem" +dbpath: "./certs.db" port: 8000` invalidYAMLConfig = `wrong: fields every: where` @@ -102,10 +110,9 @@ func TestMain(m *testing.M) { if err != nil { log.Fatalf("couldn't create temp directory") } - writeConfigErr := os.WriteFile(testfolder+"/config.yaml", []byte(validConfig), 0644) writeCertErr := os.WriteFile(testfolder+"/cert_test.pem", []byte(validCert), 0644) writeKeyErr := os.WriteFile(testfolder+"/key_test.pem", []byte(validPK), 0644) - if writeConfigErr != nil || writeCertErr != nil || writeKeyErr != nil { + if writeCertErr != nil || writeKeyErr != nil { log.Fatalf("couldn't create temp testing file") } if err := os.Chdir(testfolder); err != nil { @@ -124,7 +131,11 @@ func TestMain(m *testing.M) { } func TestNewServerSuccess(t *testing.T) { - s, err := server.NewServer([]byte(validCert), []byte(validPK), 8000) + writeConfigErr := os.WriteFile("config.yaml", []byte(validConfig), 0644) + if writeConfigErr != nil { + log.Fatalf("Error writing config file") + } + s, err := server.NewServer("config.yaml") if err != nil { t.Errorf("Error occured: %s", err) } @@ -135,52 +146,28 @@ func TestNewServerSuccess(t *testing.T) { func TestNewServerFail(t *testing.T) { testCases := []struct { - desc string - cert string - key string + desc string + config string }{ { - desc: "wrong certificate", - cert: "some cert", - key: validPK, + desc: "wrong certificate", + config: wrongCertConfig, }, { - desc: "wrong key", - cert: validCert, - key: "some pk", + desc: "wrong key", + config: wrongKeyConfig, }, } for _, tC := range testCases { + writeConfigErr := os.WriteFile("config.yaml", []byte(tC.config), 0644) + if writeConfigErr != nil { + log.Fatalf("Error writing config file") + } t.Run(tC.desc, func(t *testing.T) { - _, err := server.NewServer([]byte(tC.cert), []byte(tC.key), 8000) + _, err := server.NewServer("config.yaml") if err == nil { t.Errorf("Expected error") } }) } } - -func TestConfigFileSuccess(t *testing.T) { - config, err := server.ValidateConfigFile("./config.yaml") - if err != nil { - t.Errorf("Error occured: %s", err) - } - if config.Cert == nil || config.Key == nil || config.DBPath == "" { - t.Errorf("Expected values were not read: %s", err) - } -} - -func TestConfigFileFail(t *testing.T) { - testCases := []struct { - desc string - }{ - { - desc: "", - }, - } - for _, tC := range testCases { - t.Run(tC.desc, func(t *testing.T) { - - }) - } -} diff --git a/internal/certdb/certdb.go b/internal/certdb/certdb.go index 297db38..1cb59fe 100644 --- a/internal/certdb/certdb.go +++ b/internal/certdb/certdb.go @@ -3,6 +3,7 @@ package certdb import ( "database/sql" + "errors" "fmt" _ "github.com/mattn/go-sqlite3" @@ -11,10 +12,10 @@ import ( const queryCreateTable = "CREATE TABLE IF NOT EXISTS %s (CSR VARCHAR PRIMARY KEY UNIQUE NOT NULL, Certificate VARCHAR DEFAULT '')" const queryGetAllCSRs = "SELECT rowid, * FROM %s" -const queryGetCSR = "SELECT rowid, * FROM %s WHERE CSR=?" +const queryGetCSR = "SELECT rowid, * FROM %s WHERE rowid=?" const queryCreateCSR = "INSERT INTO %s (CSR) VALUES (?)" -const queryUpdateCSR = "UPDATE %s SET Certificate=? WHERE CSR=?" -const queryDeleteCSR = "DELETE FROM %s WHERE CSR=?" +const queryUpdateCSR = "UPDATE %s SET Certificate=? WHERE rowid=?" +const queryDeleteCSR = "DELETE FROM %s WHERE rowid=?" // CertificateRequestRepository is the object used to communicate with the established repository. type CertificateRequestsRepository struct { @@ -51,10 +52,13 @@ func (db *CertificateRequestsRepository) RetrieveAll() ([]CertificateRequest, er // Retrieve gets a given CSR from the repository. // It returns the row id and matching certificate alongside the CSR in a CertificateRequest object. -func (db *CertificateRequestsRepository) Retrieve(csr string) (CertificateRequest, error) { +func (db *CertificateRequestsRepository) Retrieve(id string) (CertificateRequest, error) { var newCSR CertificateRequest - row := db.conn.QueryRow(fmt.Sprintf(queryGetCSR, db.table), csr) + row := db.conn.QueryRow(fmt.Sprintf(queryGetCSR, db.table), id) if err := row.Scan(&newCSR.ID, &newCSR.CSR, &newCSR.Certificate); err != nil { + if err.Error() == "sql: no rows in result set" { + return newCSR, errors.New("csr id not found") + } return newCSR, err } return newCSR, nil @@ -64,7 +68,7 @@ func (db *CertificateRequestsRepository) Retrieve(csr string) (CertificateReques // The given CSR must be valid and unique func (db *CertificateRequestsRepository) Create(csr string) (int64, error) { if err := ValidateCertificateRequest(csr); err != nil { - return 0, err + return 0, errors.New("csr validation failed: " + err.Error()) } result, err := db.conn.Exec(fmt.Sprintf(queryCreateCSR, db.table), csr) if err != nil { @@ -79,31 +83,44 @@ func (db *CertificateRequestsRepository) Create(csr string) (int64, error) { // Update adds a new cert to the given CSR in the repository. // The given certificate must share the public key of the CSR and must be valid. -func (db *CertificateRequestsRepository) Update(csr string, cert string) (int64, error) { - if err := ValidateCertificate(cert); err != nil { +func (db *CertificateRequestsRepository) Update(id string, cert string) (int64, error) { + csr, err := db.Retrieve(id) + if err != nil { return 0, err } - if err := CertificateMatchesCSR(cert, csr); err != nil { - return 0, err + err = ValidateCertificate(cert) + if cert != "" && err != nil { + return 0, errors.New("cert validation failed: " + err.Error()) } - result, err := db.conn.Exec(fmt.Sprintf(queryUpdateCSR, db.table), cert, csr) + err = CertificateMatchesCSR(cert, csr.CSR) + if cert != "" && err != nil { + return 0, errors.New("cert validation failed: " + err.Error()) + } + result, err := db.conn.Exec(fmt.Sprintf(queryUpdateCSR, db.table), cert, csr.ID) if err != nil { return 0, err } - id, err := result.LastInsertId() + insertId, err := result.LastInsertId() if err != nil { return 0, err } - return id, nil + return insertId, nil } // Delete removes a CSR from the database alongside the certificate that may have been generated for it. -func (db *CertificateRequestsRepository) Delete(csr string) error { - _, err := db.conn.Exec(fmt.Sprintf(queryDeleteCSR, db.table), csr) +func (db *CertificateRequestsRepository) Delete(id string) (int64, error) { + result, err := db.conn.Exec(fmt.Sprintf(queryDeleteCSR, db.table), id) if err != nil { - return err + return 0, err } - return nil + deleteId, err := result.RowsAffected() + if err != nil { + return 0, err + } + if deleteId == 0 { + return 0, errors.New("csr id not found") + } + return deleteId, nil } // Close closes the connection to the repository cleanly. diff --git a/internal/certdb/certdb_test.go b/internal/certdb/certdb_test.go index 4dfe946..6f58ff4 100644 --- a/internal/certdb/certdb_test.go +++ b/internal/certdb/certdb_test.go @@ -2,6 +2,7 @@ package certdb_test import ( "log" + "strconv" "strings" "testing" @@ -23,13 +24,16 @@ func TestEndToEnd(t *testing.T) { } defer db.Close() - if _, err := db.Create(ValidCSR1); err != nil { + id1, err := db.Create(ValidCSR1) + if err != nil { t.Fatalf("Couldn't complete Create: %s", err) } - if _, err := db.Create(ValidCSR2); err != nil { + id2, err := db.Create(ValidCSR2) + if err != nil { t.Fatalf("Couldn't complete Create: %s", err) } - if _, err := db.Create(ValidCSR3); err != nil { + _, err = db.Create(ValidCSR3) + if err != nil { t.Fatalf("Couldn't complete Create: %s", err) } @@ -40,7 +44,7 @@ func TestEndToEnd(t *testing.T) { if len(res) != 3 { t.Fatalf("One or more CSRs weren't found in DB") } - retrievedCSR, err := db.Retrieve(ValidCSR1) + retrievedCSR, err := db.Retrieve(strconv.FormatInt(id1, 10)) if err != nil { t.Fatalf("Couldn't complete Retrieve: %s", err) } @@ -48,7 +52,7 @@ func TestEndToEnd(t *testing.T) { t.Fatalf("The CSR from the database doesn't match the CSR that was given") } - if err = db.Delete(ValidCSR1); err != nil { + if _, err = db.Delete(strconv.FormatInt(id1, 10)); err != nil { t.Fatalf("Couldn't complete Delete: %s", err) } res, _ = db.RetrieveAll() @@ -56,13 +60,21 @@ func TestEndToEnd(t *testing.T) { t.Fatalf("CSR's weren't deleted from the DB properly") } - _, err = db.Update(ValidCSR2, ValidCert2) + _, err = db.Update(strconv.FormatInt(id2, 10), ValidCert2) if err != nil { t.Fatalf("Couldn't complete Update: %s", err) } - retrievedCSR, _ = db.Retrieve(ValidCSR2) + retrievedCSR, _ = db.Retrieve(strconv.FormatInt(id2, 10)) if retrievedCSR.Certificate != ValidCert2 { - t.Fatalf("The certificate that was uploaded does not match the certificate that was given: Retrieved: %s\nGiven: %s", retrievedCSR.Certificate, ValidCert2) + t.Fatalf("The certificate that was uploaded does not match the certificate that was given.\n Retrieved: %s\nGiven: %s", retrievedCSR.Certificate, ValidCert2) + } + _, err = db.Update(strconv.FormatInt(id2, 10), "") + if err != nil { + t.Fatalf("Couldn't complete Update: %s", err) + } + retrievedCSR, _ = db.Retrieve(strconv.FormatInt(id2, 10)) + if retrievedCSR.Certificate != "" { + t.Fatalf("Couldn't delete certificate") } } @@ -85,13 +97,13 @@ func TestUpdateFails(t *testing.T) { db, _ := certdb.NewCertificateRequestsRepository(":memory:", "CertificateRequests") //nolint:errcheck defer db.Close() - db.Create(ValidCSR1) //nolint:errcheck - db.Create(ValidCSR2) //nolint:errcheck + id1, _ := db.Create(ValidCSR1) //nolint:errcheck + id2, _ := db.Create(ValidCSR2) //nolint:errcheck InvalidCert := strings.ReplaceAll(ValidCert2, "/", "+") - if _, err := db.Update(ValidCSR2, InvalidCert); err == nil { + if _, err := db.Update(strconv.FormatInt(id2, 10), InvalidCert); err == nil { t.Fatalf("Expected updating with invalid cert to fail") } - if _, err := db.Update(ValidCSR1, ValidCert2); err == nil { + if _, err := db.Update(strconv.FormatInt(id1, 10), ValidCert2); err == nil { t.Fatalf("Expected updating with mismatched cert to fail") } } @@ -101,7 +113,7 @@ func TestRetrieve(t *testing.T) { defer db.Close() db.Create(ValidCSR1) //nolint:errcheck - if _, err := db.Retrieve(ValidCSR2); err == nil { + if _, err := db.Retrieve("this is definitely not an id"); err == nil { t.Fatalf("Expected failure looking for nonexistent CSR") }