Skip to content

Commit

Permalink
login via certificates (#4857)
Browse files Browse the repository at this point in the history
* login via certificates
  • Loading branch information
tiloKo authored Mar 11, 2024
1 parent 2c69c4c commit 2330993
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 2 deletions.
7 changes: 6 additions & 1 deletion pkg/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -550,6 +554,7 @@ func (c *Client) configureTLSToTrustCertificates(transport *TransportWrapper) er
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false,
RootCAs: rootCAs,
Certificates: c.certificates,
},
},
doLogRequestBodyOnDebug: c.doLogRequestBodyOnDebug,
Expand Down
173 changes: 173 additions & 0 deletions pkg/http/http_cert_logon_test.go
Original file line number Diff line number Diff line change
@@ -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()
})
}
11 changes: 10 additions & 1 deletion pkg/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down

0 comments on commit 2330993

Please sign in to comment.