From 2330993615b43b0131c69ccc17c5fc80f77502b6 Mon Sep 17 00:00:00 2001 From: tiloKo <70266685+tiloKo@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:12:32 +0100 Subject: [PATCH] login via certificates (#4857) * login via certificates --- pkg/http/http.go | 7 +- pkg/http/http_cert_logon_test.go | 173 +++++++++++++++++++++++++++++++ pkg/http/http_test.go | 11 +- 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 pkg/http/http_cert_logon_test.go diff --git a/pkg/http/http.go b/pkg/http/http.go index e991a2f32d..768206bf2b 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -43,6 +43,7 @@ type Client struct { doLogResponseBodyOnDebug bool useDefaultTransport bool trustedCerts []string + certificates []tls.Certificate // contains one or more certificate chains to present to the other side of the connection (client-authentication) fileUtils piperutils.FileUtils httpClient *http.Client } @@ -68,7 +69,8 @@ type ClientOptions struct { DoLogRequestBodyOnDebug bool DoLogResponseBodyOnDebug bool UseDefaultTransport bool - TrustedCerts []string + TrustedCerts []string // defines the set of root certificate authorities that clients use when verifying server certificates + Certificates []tls.Certificate // contains one or more certificate chains to present to the other side of the connection (client-authentication) } // TransportWrapper is a wrapper for central round trip capabilities @@ -261,6 +263,7 @@ func (c *Client) SetOptions(options ClientOptions) { c.cookieJar = options.CookieJar c.trustedCerts = options.TrustedCerts c.fileUtils = &piperutils.Files{} + c.certificates = options.Certificates } // SetFileUtils can be used to overwrite the default file utils @@ -291,6 +294,7 @@ func (c *Client) initializeHttpClient() *http.Client { TLSHandshakeTimeout: c.transportTimeout, TLSClientConfig: &tls.Config{ InsecureSkipVerify: c.transportSkipVerification, + Certificates: c.certificates, }, }, doLogRequestBodyOnDebug: c.doLogRequestBodyOnDebug, @@ -550,6 +554,7 @@ func (c *Client) configureTLSToTrustCertificates(transport *TransportWrapper) er TLSClientConfig: &tls.Config{ InsecureSkipVerify: false, RootCAs: rootCAs, + Certificates: c.certificates, }, }, doLogRequestBodyOnDebug: c.doLogRequestBodyOnDebug, diff --git a/pkg/http/http_cert_logon_test.go b/pkg/http/http_cert_logon_test.go new file mode 100644 index 0000000000..c8f4ee01a9 --- /dev/null +++ b/pkg/http/http_cert_logon_test.go @@ -0,0 +1,173 @@ +//go:build unit +// +build unit + +package http + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "log" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func GenerateSelfSignedCertificate(usages []x509.ExtKeyUsage) (pemKey, pemCert []byte) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatalf("Failed to generate private key: %v", err) + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("Failed to generate serial number: %v", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"My Corp"}, + }, + DNSNames: []string{"localhost"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: usages, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + log.Fatalf("Failed to create certificate: %v", err) + } + + pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if pemCert == nil { + log.Fatal("Failed to encode certificate to PEM") + } + + privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + log.Fatalf("Unable to marshal private key: %v", err) + } + pemKey = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + if pemKey == nil { + log.Fatal("Failed to encode key to PEM") + } + + return pemKey, pemCert +} + +func GenerateSelfSignedServerAuthCertificate() (pemKey, pemCert []byte) { + return GenerateSelfSignedCertificate([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) +} + +func GenerateSelfSignedClientAuthCertificate() (pemKey, pemCert []byte) { + return GenerateSelfSignedCertificate([]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}) +} + +func TestCertificateLogon(t *testing.T) { + testOkayString := "Okidoki" + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(testOkayString)) + })) + + clientPemKey, clientPemCert := GenerateSelfSignedClientAuthCertificate() + + //server + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientPemCert) + + tlsConfig := tls.Config{ + MinVersion: tls.VersionTLS13, + PreferServerCipherSuites: true, + ClientCAs: clientCertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + } + + server.TLS = &tlsConfig + server.StartTLS() + defer server.Close() + + //client + tlsKeyPair, err := tls.X509KeyPair(clientPemCert, clientPemKey) + if err != nil { + log.Fatal("Failed to create clients tls key pair") + } + + t.Run("Success - Login with certificate", func(t *testing.T) { + c := Client{} + c.SetOptions(ClientOptions{ + TransportSkipVerification: true, + MaxRetries: 1, + Certificates: []tls.Certificate{tlsKeyPair}, + }) + + response, err := c.SendRequest("GET", server.URL, nil, nil, nil) + assert.NoError(t, err, "Error occurred but none expected") + content, err := io.ReadAll(response.Body) + assert.Equal(t, testOkayString, string(content), "Returned content incorrect") + response.Body.Close() + }) + + t.Run("Failure - Login without certificate", func(t *testing.T) { + c := Client{} + c.SetOptions(ClientOptions{ + TransportSkipVerification: true, + MaxRetries: 1, + }) + + _, err := c.SendRequest("GET", server.URL, nil, nil, nil) + assert.ErrorContains(t, err, "bad certificate") + }) + + t.Run("Failure - Login with wrong certificate", func(t *testing.T) { + otherClientPemKey, otherClientPemCert := GenerateSelfSignedClientAuthCertificate() + + otherTlsKeyPair, err := tls.X509KeyPair(otherClientPemCert, otherClientPemKey) + if err != nil { + log.Fatal("Failed to create clients tls key pair") + } + + c := Client{} + c.SetOptions(ClientOptions{ + TransportSkipVerification: true, + MaxRetries: 1, + Certificates: []tls.Certificate{otherTlsKeyPair}, + }) + + _, err = c.SendRequest("GET", server.URL, nil, nil, nil) + assert.ErrorContains(t, err, "bad certificate") + }) + + t.Run("SanityCheck", func(t *testing.T) { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + //RootCAs: certPool, + InsecureSkipVerify: true, + Certificates: []tls.Certificate{tlsKeyPair}, + }, + }, + } + + response, err := client.Get(server.URL) + assert.NoError(t, err, "Error occurred but none expected") + content, err := io.ReadAll(response.Body) + assert.Equal(t, testOkayString, string(content), "Returned content incorrect") + response.Body.Close() + }) +} diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go index 06c3e08870..df964a118b 100644 --- a/pkg/http/http_test.go +++ b/pkg/http/http_test.go @@ -210,7 +210,15 @@ func TestSendRequest(t *testing.T) { func TestSetOptions(t *testing.T) { c := Client{} transportProxy, _ := url.Parse("https://proxy.dummy.sap.com") - opts := ClientOptions{MaxRetries: -1, TransportTimeout: 10, TransportProxy: transportProxy, MaxRequestDuration: 5, Username: "TestUser", Password: "TestPassword", Token: "TestToken", Logger: log.Entry().WithField("package", "github.com/SAP/jenkins-library/pkg/http")} + opts := ClientOptions{MaxRetries: -1, + TransportTimeout: 10, + TransportProxy: transportProxy, + MaxRequestDuration: 5, + Username: "TestUser", + Password: "TestPassword", + Token: "TestToken", + Logger: log.Entry().WithField("package", "github.com/SAP/jenkins-library/pkg/http"), + Certificates: []tls.Certificate{{}}} c.SetOptions(opts) assert.Equal(t, opts.TransportTimeout, c.transportTimeout) @@ -220,6 +228,7 @@ func TestSetOptions(t *testing.T) { assert.Equal(t, opts.Username, c.username) assert.Equal(t, opts.Password, c.password) assert.Equal(t, opts.Token, c.token) + assert.Equal(t, opts.Certificates, c.certificates) } func TestApplyDefaults(t *testing.T) {