http: add client certificate user auth middleware

This populates the authenticated user from the client certificate
common name.

Also added tests for the existing client certificate functionality.
This commit is contained in:
Peter Fern 2023-05-26 15:26:13 +10:00 committed by Nick Craig-Wood
parent 7751d5a00b
commit 1cfed18aa7
14 changed files with 458 additions and 29 deletions

View File

@ -19,6 +19,10 @@ By default this will serve files without needing a login.
You can either use an htpasswd file which can take lots of users, or You can either use an htpasswd file which can take lots of users, or
set a single username and password with the ` + "`--{{ .Prefix }}user` and `--{{ .Prefix }}pass`" + ` flags. set a single username and password with the ` + "`--{{ .Prefix }}user` and `--{{ .Prefix }}pass`" + ` flags.
If no static users are configured by either of the above methods, and client
certificates are required by the ` + "`--client-ca`" + ` flag passed to the server, the
client certificate common name will be considered as the username.
Use ` + "`--{{ .Prefix }}htpasswd /path/to/htpasswd`" + ` to provide an htpasswd file. This is Use ` + "`--{{ .Prefix }}htpasswd /path/to/htpasswd`" + ` to provide an htpasswd file. This is
in standard apache format and supports MD5, SHA1 and BCrypt for basic in standard apache format and supports MD5, SHA1 and BCrypt for basic
authentication. Bcrypt is recommended. authentication. Bcrypt is recommended.

View File

@ -74,6 +74,24 @@ func basicAuth(authenticator *LoggedBasicAuth) func(next http.Handler) http.Hand
} }
} }
// MiddlewareAuthCertificateUser instantiates middleware that extracts the authenticated user via client certificate common name
func MiddlewareAuthCertificateUser() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, cert := range r.TLS.PeerCertificates {
if cert.Subject.CommonName != "" {
r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, cert.Subject.CommonName))
next.ServeHTTP(w, r)
return
}
}
code := http.StatusUnauthorized
w.Header().Set("Content-Type", "text/plain")
http.Error(w, http.StatusText(code), code)
})
}
}
// MiddlewareAuthHtpasswd instantiates middleware that authenticates against the passed htpasswd file // MiddlewareAuthHtpasswd instantiates middleware that authenticates against the passed htpasswd file
func MiddlewareAuthHtpasswd(path, realm string) Middleware { func MiddlewareAuthHtpasswd(path, realm string) Middleware {
fs.Infof(nil, "Using %q as htpasswd storage", path) fs.Infof(nil, "Using %q as htpasswd storage", path)
@ -97,7 +115,7 @@ func MiddlewareAuthBasic(user, pass, realm, salt string) Middleware {
} }
// MiddlewareAuthCustom instantiates middleware that authenticates using a custom function // MiddlewareAuthCustom instantiates middleware that authenticates using a custom function
func MiddlewareAuthCustom(fn CustomAuthFn, realm string) Middleware { func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) Middleware {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// skip auth for unix socket // skip auth for unix socket
@ -107,6 +125,10 @@ func MiddlewareAuthCustom(fn CustomAuthFn, realm string) Middleware {
} }
user, pass, ok := parseAuthorization(r) user, pass, ok := parseAuthorization(r)
if !ok && userFromContext {
user, ok = CtxGetUser(r.Context())
}
if !ok { if !ok {
code := http.StatusUnauthorized code := http.StatusUnauthorized
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")

View File

@ -2,6 +2,7 @@ package http
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -159,6 +160,167 @@ func TestMiddlewareAuth(t *testing.T) {
} }
} }
func TestMiddlewareAuthCertificateUser(t *testing.T) {
serverCertBytes := testReadTestdataFile(t, "local.crt")
serverKeyBytes := testReadTestdataFile(t, "local.key")
clientCertBytes := testReadTestdataFile(t, "client.crt")
clientKeyBytes := testReadTestdataFile(t, "client.key")
clientCert, err := tls.X509KeyPair(clientCertBytes, clientKeyBytes)
require.NoError(t, err)
emptyCertBytes := testReadTestdataFile(t, "emptyclient.crt")
emptyKeyBytes := testReadTestdataFile(t, "emptyclient.key")
emptyCert, err := tls.X509KeyPair(emptyCertBytes, emptyKeyBytes)
require.NoError(t, err)
invalidCert, err := tls.X509KeyPair(serverCertBytes, serverKeyBytes)
require.NoError(t, err)
servers := []struct {
name string
wantErr bool
status int
result string
http Config
auth AuthConfig
clientCerts []tls.Certificate
}{
{
name: "Missing",
wantErr: true,
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
},
{
name: "Invalid",
wantErr: true,
clientCerts: []tls.Certificate{invalidCert},
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
},
{
name: "EmptyCommonName",
status: http.StatusUnauthorized,
result: fmt.Sprintf("%s\n", http.StatusText(http.StatusUnauthorized)),
clientCerts: []tls.Certificate{emptyCert},
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
},
{
name: "Valid",
status: http.StatusOK,
result: "rclone-dev-client",
clientCerts: []tls.Certificate{clientCert},
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
},
{
name: "CustomAuth/Invalid",
status: http.StatusUnauthorized,
result: fmt.Sprintf("%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)),
clientCerts: []tls.Certificate{clientCert},
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
auth: AuthConfig{
Realm: "test",
CustomAuthFn: func(user, pass string) (value interface{}, err error) {
if user == "custom" && pass == "custom" {
return true, nil
}
return nil, errors.New("invalid credentials")
},
},
},
{
name: "CustomAuth/Valid",
status: http.StatusOK,
result: "rclone-dev-client",
clientCerts: []tls.Certificate{clientCert},
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
auth: AuthConfig{
Realm: "test",
CustomAuthFn: func(user, pass string) (value interface{}, err error) {
fmt.Println("CUSTOMAUTH", user, pass)
if user == "rclone-dev-client" && pass == "" {
return true, nil
}
return nil, errors.New("invalid credentials")
},
},
},
}
for _, ss := range servers {
t.Run(ss.name, func(t *testing.T) {
s, err := NewServer(context.Background(), WithConfig(ss.http), WithAuth(ss.auth))
require.NoError(t, err)
defer func() {
require.NoError(t, s.Shutdown())
}()
s.Router().Mount("/", testAuthUserHandler())
s.Serve()
url := testGetServerURL(t, s)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: ss.clientCerts,
InsecureSkipVerify: true,
},
},
}
req, err := http.NewRequest("GET", url, nil)
require.NoError(t, err)
resp, err := client.Do(req)
if ss.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
defer func() {
_ = resp.Body.Close()
}()
require.Equal(t, ss.status, resp.StatusCode, fmt.Sprintf("should return status %d", ss.status))
testExpectRespBody(t, resp, []byte(ss.result))
})
}
}
var _testCORSHeaderKeys = []string{ var _testCORSHeaderKeys = []string{
"Access-Control-Allow-Origin", "Access-Control-Allow-Origin",
"Access-Control-Request-Method", "Access-Control-Request-Method",

View File

@ -226,8 +226,6 @@ func NewServer(ctx context.Context, options ...Option) (*Server, error) {
s.mux.Use(MiddlewareStripPrefix(s.cfg.BaseURL)) s.mux.Use(MiddlewareStripPrefix(s.cfg.BaseURL))
} }
s.initAuth()
err := s.initTemplate() err := s.initTemplate()
if err != nil { if err != nil {
return nil, err return nil, err
@ -238,6 +236,8 @@ func NewServer(ctx context.Context, options ...Option) (*Server, error) {
return nil, err return nil, err
} }
s.initAuth()
for _, addr := range s.cfg.ListenAddr { for _, addr := range s.cfg.ListenAddr {
var url string var url string
var network = "tcp" var network = "tcp"
@ -293,9 +293,17 @@ func NewServer(ctx context.Context, options ...Option) (*Server, error) {
} }
func (s *Server) initAuth() { func (s *Server) initAuth() {
s.usingAuth = false
authCertificateUserEnabled := s.tlsConfig != nil && s.tlsConfig.ClientAuth != tls.NoClientCert && s.auth.HtPasswd == "" && s.auth.BasicUser == ""
if authCertificateUserEnabled {
s.usingAuth = true
s.mux.Use(MiddlewareAuthCertificateUser())
}
if s.auth.CustomAuthFn != nil { if s.auth.CustomAuthFn != nil {
s.usingAuth = true s.usingAuth = true
s.mux.Use(MiddlewareAuthCustom(s.auth.CustomAuthFn, s.auth.Realm)) s.mux.Use(MiddlewareAuthCustom(s.auth.CustomAuthFn, s.auth.Realm, authCertificateUserEnabled))
return return
} }
@ -310,7 +318,6 @@ func (s *Server) initAuth() {
s.mux.Use(MiddlewareAuthBasic(s.auth.BasicUser, s.auth.BasicPass, s.auth.Realm, s.auth.Salt)) s.mux.Use(MiddlewareAuthBasic(s.auth.BasicUser, s.auth.BasicPass, s.auth.Realm, s.auth.Salt))
return return
} }
s.usingAuth = false
} }
func (s *Server) initTemplate() error { func (s *Server) initTemplate() error {

View File

@ -20,6 +20,16 @@ func testEchoHandler(data []byte) http.Handler {
}) })
} }
func testAuthUserHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := CtxGetUser(r.Context())
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
_, _ = w.Write([]byte(userID))
})
}
func testExpectRespBody(t *testing.T, resp *http.Response, expected []byte) { func testExpectRespBody(t *testing.T, resp *http.Response, expected []byte) {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
require.NoError(t, err) require.NoError(t, err)
@ -234,19 +244,22 @@ func TestNewServerBaseURL(t *testing.T) {
} }
func TestNewServerTLS(t *testing.T) { func TestNewServerTLS(t *testing.T) {
certBytes := testReadTestdataFile(t, "local.crt") serverCertBytes := testReadTestdataFile(t, "local.crt")
keyBytes := testReadTestdataFile(t, "local.key") serverKeyBytes := testReadTestdataFile(t, "local.key")
clientCertBytes := testReadTestdataFile(t, "client.crt")
clientKeyBytes := testReadTestdataFile(t, "client.key")
clientCert, err := tls.X509KeyPair(clientCertBytes, clientKeyBytes)
require.NoError(t, err)
// TODO: generate a proper cert with SAN // TODO: generate a proper cert with SAN
// TODO: generate CA, test mTLS
// clientCert, err := tls.X509KeyPair(certBytes, keyBytes)
// require.NoError(t, err, "should be testing with a valid self signed certificate")
servers := []struct { servers := []struct {
name string name string
wantErr bool clientCerts []tls.Certificate
err error wantErr bool
http Config wantClientErr bool
err error
http Config
}{ }{
{ {
name: "FromFile/Valid", name: "FromFile/Valid",
@ -303,8 +316,8 @@ func TestNewServerTLS(t *testing.T) {
name: "FromBody/Valid", name: "FromBody/Valid",
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0", MinTLSVersion: "tls1.0",
}, },
}, },
@ -315,7 +328,7 @@ func TestNewServerTLS(t *testing.T) {
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: nil, TLSCertBody: nil,
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0", MinTLSVersion: "tls1.0",
}, },
}, },
@ -325,7 +338,7 @@ func TestNewServerTLS(t *testing.T) {
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: []byte("JUNK DATA"), TLSCertBody: []byte("JUNK DATA"),
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0", MinTLSVersion: "tls1.0",
}, },
}, },
@ -335,7 +348,7 @@ func TestNewServerTLS(t *testing.T) {
err: ErrTLSBodyMismatch, err: ErrTLSBodyMismatch,
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: nil, TLSKeyBody: nil,
MinTLSVersion: "tls1.0", MinTLSVersion: "tls1.0",
}, },
@ -345,7 +358,7 @@ func TestNewServerTLS(t *testing.T) {
wantErr: true, wantErr: true,
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: []byte("JUNK DATA"), TLSKeyBody: []byte("JUNK DATA"),
MinTLSVersion: "tls1.0", MinTLSVersion: "tls1.0",
}, },
@ -354,8 +367,8 @@ func TestNewServerTLS(t *testing.T) {
name: "MinTLSVersion/Valid/1.1", name: "MinTLSVersion/Valid/1.1",
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.1", MinTLSVersion: "tls1.1",
}, },
}, },
@ -363,8 +376,8 @@ func TestNewServerTLS(t *testing.T) {
name: "MinTLSVersion/Valid/1.2", name: "MinTLSVersion/Valid/1.2",
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.2", MinTLSVersion: "tls1.2",
}, },
}, },
@ -372,8 +385,8 @@ func TestNewServerTLS(t *testing.T) {
name: "MinTLSVersion/Valid/1.3", name: "MinTLSVersion/Valid/1.3",
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.3", MinTLSVersion: "tls1.3",
}, },
}, },
@ -383,11 +396,46 @@ func TestNewServerTLS(t *testing.T) {
err: ErrInvalidMinTLSVersion, err: ErrInvalidMinTLSVersion,
http: Config{ http: Config{
ListenAddr: []string{"127.0.0.1:0"}, ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: certBytes, TLSCertBody: serverCertBytes,
TLSKeyBody: keyBytes, TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls0.9", MinTLSVersion: "tls0.9",
}, },
}, },
{
name: "MutualTLS/InvalidCA",
clientCerts: []tls.Certificate{clientCert},
wantErr: true,
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt.invalid",
},
},
{
name: "MutualTLS/InvalidClient",
clientCerts: []tls.Certificate{},
wantClientErr: true,
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
},
{
name: "MutualTLS/Valid",
clientCerts: []tls.Certificate{clientCert},
http: Config{
ListenAddr: []string{"127.0.0.1:0"},
TLSCertBody: serverCertBytes,
TLSKeyBody: serverKeyBytes,
MinTLSVersion: "tls1.0",
ClientCA: "./testdata/client-ca.crt",
},
},
} }
for _, ss := range servers { for _, ss := range servers {
@ -422,7 +470,7 @@ func TestNewServerTLS(t *testing.T) {
return net.Dial("tcp", dest) return net.Dial("tcp", dest)
}, },
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
// Certificates: []tls.Certificate{clientCert}, Certificates: ss.clientCerts,
InsecureSkipVerify: true, InsecureSkipVerify: true,
}, },
}, },
@ -431,6 +479,12 @@ func TestNewServerTLS(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
resp, err := client.Do(req) resp, err := client.Do(req)
if ss.wantClientErr {
require.Error(t, err, "new server client should return error")
return
}
require.NoError(t, err) require.NoError(t, err)
defer func() { defer func() {
_ = resp.Body.Close() _ = resp.Body.Close()

22
lib/http/testdata/client-ca.crt vendored Normal file
View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIURSp4/DIspJDvwXq6udbAqg2Yk9IwDQYJKoZIhvcNAQEL
BQAwZzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xDzANBgNVBAoM
BnJjbG9uZTETMBEGA1UECwwKcmNsb25lLWRldjEdMBsGA1UEAwwUcmNsb25lLWRl
di1jbGllbnQtY2EwHhcNMjMwNTI1MjM1NzE3WhcNMzMwNTIyMjM1NzE3WjBnMQsw
CQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEPMA0GA1UECgwGcmNsb25l
MRMwEQYDVQQLDApyY2xvbmUtZGV2MR0wGwYDVQQDDBRyY2xvbmUtZGV2LWNsaWVu
dC1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxufFA5IrPWTnk+
EEUttdGNystKNkJyuRV2Mgso9VX5HrwIGyNCOVeRX2Uds2Piaz8uSET8/fWMg3h4
ZNTImoomMUcaqL7lmBsPxXZUguo9M5ZlrlLW9VrwuSIZs9+W26Gy44WliivZl3f5
RNYzk+0UyZ0hp7h81LufUiVQhT8MX3XoVdDFAgoDipEI07ROipB2KXPa1lr1Mdhu
kAEKu0gLb7RoGrIEADyCKMr4waE5SovXWfC0jTVbMdB9DLCYmR5/zmR9ykUzWaWJ
ug7iIv6a/q+r+sB/Xh+PHY0CTieYGkd4rweFQk5ImwspOIx0RdtWj4pyxDoKiIuK
vhLyzl0CAwEAAaNTMFEwHQYDVR0OBBYEFDcCPxWKtwaU8DCpAdGuZ8X07x67MB8G
A1UdIwQYMBaAFDcCPxWKtwaU8DCpAdGuZ8X07x67MA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBACgU9YDjoqrOAhfhYz8MNkb8yWofCGse8fDrhXft
ZdJjlVBd343Ja97o6P5IaS9H8mM2ZRVtw0p/JoBvnsF74GMbSAR/M1VovxPa/Ze9
SjGKdJAwxjhx3+YCU61psR0DZO1By1+mjKfvbOMOHD+HVvvG7x1/iv3buPId935+
53dt36d1OYx7Z6BbYYjL8/CXtkaOhgdKbCtng2frU3OvOtSs1OBLBimtc9ja+evk
Nn/NiG14F962ZWRPXEKQAH1tpqzWJ6okjY3F58hPb5c//p/kgOiWefCZ+XF/75HL
vZAQjtmpn0pv4ObCHWp/6ZFWdmnxGDuOigcC09R74P+HNuU=
-----END CERTIFICATE-----

28
lib/http/testdata/client-ca.key vendored Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDcbnxQOSKz1k55
PhBFLbXRjcrLSjZCcrkVdjILKPVV+R68CBsjQjlXkV9lHbNj4ms/LkhE/P31jIN4
eGTUyJqKJjFHGqi+5ZgbD8V2VILqPTOWZa5S1vVa8LkiGbPfltuhsuOFpYor2Zd3
+UTWM5PtFMmdIae4fNS7n1IlUIU/DF916FXQxQIKA4qRCNO0ToqQdilz2tZa9THY
bpABCrtIC2+0aBqyBAA8gijK+MGhOUqL11nwtI01WzHQfQywmJkef85kfcpFM1ml
iboO4iL+mv6vq/rAf14fjx2NAk4nmBpHeK8HhUJOSJsLKTiMdEXbVo+KcsQ6CoiL
ir4S8s5dAgMBAAECggEATNrw2P+yy8USw08SWSxg0ll/tXWAiZZ6VbNKK33yXDFp
t+GTpK14VMHI4vaCD3doMTUv2W3kFfMR+7TuYwo2Z6h9Ue9HmpduezD6hhFdO9Ju
5Cc7qoJsNXLs+ajAgFqW5T/7+CMJk9Rf7WKpz41YLDctPG35jmdnvKsF9yCl9J70
Fd8ZGq7l6WDbTpAlb+cbEEb/dXuBgesgp0qlnzBd8E4Ib5IhAuex1c+lY7gHZBn4
9f8fEowusjidFtvKZxK5cXhumZ6qQdXwk2rBPOcND73k8ftYjX6uFMvhCpoDkrGA
uIAPHOq3/DpgH1fOXe/KxZCvFNyX8OgW8QwI+ASU+QKBgQDxTb0+52m+mGvcx3ZF
6D9PQAOoMgxMPP0e6rKHDxBPdroJE1PevbfEvU6mdrqzvNhGbOsxlQpDqDWqgNKK
asBiPw0Tm4+Tcnytol86OCd0Nhgkat/QwGx5PpwOQwNkeCJv3igazaGrsUxfGkEK
h3NjL8G1vgCTLJdSZPjgM/1/swKBgQDp21JKczoMs0f969x/O2TQzFt5NSLS1ZCd
SYV8bskLwFVywX7ij6j3eYm9fZy7283hO8Jmw+kxNnvqT624qte87oeLgWplJ7k4
wglOqMTsU47sGyl7tY6Eqz2FH0D3euzx2Em0Uw3Qv7CP0Eh9AdHj17T5pOEB9Wgw
YRu8nkvxrwKBgD4uQi4Lg/xRWroxzBCHoIjTfh3Bh9m9fZyR7h9Pimxvs9DS4jHr
wYc5ISNURRg7+Z9sQc8tENAOcIXXXGm+yISIqt36oCzmu6oixVdDUSdpKR95SuOI
Mmur7preOemR643YOY1un9KWhY+cPFZyQRG2JLyokY1bWEMrMdbUjuZxAoGAA+Zx
f+ZeEHoo+DYnzkNqUgUmfWYCd6uyJr1kKYgbeEOz6R8LA7JLqhzvzCY9J/DphRkf
C+G2kOiMtoKvrgXDZVZBEnWNFbTM5QJvb01nQ129Y3isf3CuuM22T/MOfVIig4IM
8KH1+AZKZoudud/+5SLi1MsIKaUzIKNt9/5X2+cCgYAitKJmHLgLnXlRbswioHy1
l9Anqrh+sDjLzfcuSKaNucivqNProC/K45o33p61BTVrKr8aFWHfmPyLrXAbKWOI
c+5YcUBh6dqsk96nbNsKH+pqvWYTdEsbfaqOjVv5R7Y8/rGt3TEeBfIYvht6ILkQ
+waZBbneFx9gdQ2Q/x0yhg==
-----END PRIVATE KEY-----

1
lib/http/testdata/client-ca.srl vendored Normal file
View File

@ -0,0 +1 @@
607C74360F5500750AA949B367AC937975C32987

20
lib/http/testdata/client.crt vendored Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUjCCAjoCFGB8dDYPVQB1CqlJs2esk3l1wymGMA0GCSqGSIb3DQEBCwUAMGcx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMQ8wDQYDVQQKDAZyY2xv
bmUxEzARBgNVBAsMCnJjbG9uZS1kZXYxHTAbBgNVBAMMFHJjbG9uZS1kZXYtY2xp
ZW50LWNhMB4XDTIzMDUyNTIzNTgzMloXDTMzMDUyMjIzNTgzMlowZDELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xDzANBgNVBAoMBnJjbG9uZTETMBEG
A1UECwwKcmNsb25lLWRldjEaMBgGA1UEAwwRcmNsb25lLWRldi1jbGllbnQwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0hCo8Q7MsmdZMh+5t4+NtiU2D
h/QQYlQsXybJYtjuO4KMWvx4rqIrJkP5P/RBBQKbVcv2KvjfIDddHfHxdkFBT5bN
l/RHg6dzrvv18cFwk4Ho9e+LZ5Uq7aLCUkYxdrBwCvrd7+RfzGSlcpdupLtlWDX2
5QvC0G3B2Cb0jCUl18vxO6u9XRcKJREjcjZ0kmORLX4kKWQQDtOc3Lb51q0zpRjf
Ev4otSugyo3B4VNpOnJdXxVTDQXDDkmvErxMrylKe4KMm3v608obODLFd49CCXI8
g9TSHQGeMQBtyq7u/7B6Z11/iEyrGxxqUpe/bTK7vtUXG1xXF8CF4f2oJ9CJAgMB
AAEwDQYJKoZIhvcNAQELBQADggEBADML6IM1HPaQM7xF2tIrEyVzzLf1wlPS914a
CUI2Ck8q9wIwZ02P605dzgzwvAXyq5SZx+A0tYKeyai01lgL5dGF1Bo38E4yGwbW
2Zk00yGB2fdiCXKjj4yWhVnbCH7/GNIrFlCb7KZOWhkf1Ia66iDuZW6RZID37ai2
qZxvDk7oW316pAVfl5hEPsKJcQXQJBX/+jJzjN/VSpGv6vuk1VzCHNROg6M5aEdh
D7r/kX/yEF5eVaPlE42S7YpzfgQpT3HKU/PLdq51MrRZ2B4Jm3daaGwty1bvymFv
zcEADCCnFdKlg0QfLkF/gsonMqlohaL0KKP6/7xO/YMD0Sb9Fe8=
-----END CERTIFICATE-----

17
lib/http/testdata/client.csr vendored Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICqTCCAZECAQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24x
DzANBgNVBAoMBnJjbG9uZTETMBEGA1UECwwKcmNsb25lLWRldjEaMBgGA1UEAwwR
cmNsb25lLWRldi1jbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC0hCo8Q7MsmdZMh+5t4+NtiU2Dh/QQYlQsXybJYtjuO4KMWvx4rqIrJkP5P/RB
BQKbVcv2KvjfIDddHfHxdkFBT5bNl/RHg6dzrvv18cFwk4Ho9e+LZ5Uq7aLCUkYx
drBwCvrd7+RfzGSlcpdupLtlWDX25QvC0G3B2Cb0jCUl18vxO6u9XRcKJREjcjZ0
kmORLX4kKWQQDtOc3Lb51q0zpRjfEv4otSugyo3B4VNpOnJdXxVTDQXDDkmvErxM
rylKe4KMm3v608obODLFd49CCXI8g9TSHQGeMQBtyq7u/7B6Z11/iEyrGxxqUpe/
bTK7vtUXG1xXF8CF4f2oJ9CJAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAZy97
RGpwNyBJVLM10lgZjUMPlUOC3smGBfiJ75W0lQ/YlRhRMr2IeO15c7rD5Z4t4G06
HI+1X8KObH61RvIRdDp+ypaqMGhu5UdHH8khyqsmB98e4gI1nhRKfWoAay+vrV3y
vwK/qgpsgst98HHzkEYIlSZtdHzpY5APjEqoTwWlAoRcwGbFj7Cnme5ga5NIn0W8
jZ8S4ue0BompGNGwlE8T8swd14/IDNI9VLL1iHcOiClKX15ldHKlZPM1u1RXciQJ
1btA6JElqdLvuIA8DqwKiMniQotSz/X4xigEm7CLJQqVwj8brHJCNDDbx2qcGr1S
wiU3dzpA4NOuC635DA==
-----END CERTIFICATE REQUEST-----

28
lib/http/testdata/client.key vendored Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC0hCo8Q7MsmdZM
h+5t4+NtiU2Dh/QQYlQsXybJYtjuO4KMWvx4rqIrJkP5P/RBBQKbVcv2KvjfIDdd
HfHxdkFBT5bNl/RHg6dzrvv18cFwk4Ho9e+LZ5Uq7aLCUkYxdrBwCvrd7+RfzGSl
cpdupLtlWDX25QvC0G3B2Cb0jCUl18vxO6u9XRcKJREjcjZ0kmORLX4kKWQQDtOc
3Lb51q0zpRjfEv4otSugyo3B4VNpOnJdXxVTDQXDDkmvErxMrylKe4KMm3v608ob
ODLFd49CCXI8g9TSHQGeMQBtyq7u/7B6Z11/iEyrGxxqUpe/bTK7vtUXG1xXF8CF
4f2oJ9CJAgMBAAECgf985XTTfYPauBWtnd856RLSFs2q08XqEB5tFOihLeMp8cLB
mbJVTX6mnDMroTQ+SFklYJdeGx1WQ9QKeU2M42UC6y5L0XcSg+S4BboO0NYmLekU
ZhT3PxPWP9T83i/yyUwKOY6ZQAGixqhcUIy14QRHemDcEl2wzMUj+Yn6aXzKUPqw
3BQgtTVBItoBMOPz2OaK22JzSxI7TXLi8E6+Rqb+uXDE2flgcjSSO4VwVrhUzL4r
kxnTWhmwEn3p9GlSSRqL1vJIU7pQu1h+/d5PjYRAy1hFZikALe/U//V9uLetzMP/
98ybqcCD/wUXu5ae5IYBpZ/E7D3t1Va1oskhilECgYEAytmZ78+5mxAlN50v8jM/
1lHj1dd3VQGuEjvwdoyxcPX6sOooEXHz8+BchDomLO3aDpaaazGi5mom6VUNhvBE
GaWhK7VgFQGcfn13GIE+p5GFsZ3zs1eakNGSf7qN9yrDQLa/0MShUoflwxC2+fxl
2nIONerrSO7bXlc60JQ2d/ECgYEA49CApQCOOQFFQHrQx+6YrkmTHL6eJ5ckVW4X
31Ppli5OiqdaOSp+nXCqm0IyhRWiZ8teMqz0b92fZLUm4jfR939bV+3H8YRimjyc
n7Nxs8IhsPzgzlZtku2XZctdqI3U0OOn1w2zVDYAAIQPVydTA+IaqcWvEOlNKNmY
6R/BuhkCgYAHnTtmAQoag/ShrcjK8pmG1fQTZs8X5cQ+8vkHuig+8TzDv0ZZwUlC
8j0GyZf9P8Bbo9OQCoDu3TUwtPyZABPOUqVGGrzMjQ7uwI7j4JYVfCTkkeU/6h3n
KbayDLKfgH9rwnBYyci0bF13gP0dTRgVpwpZg8PpLO4XEHcotSeGQQKBgQDeu3z7
Vea3bzmRCELWJr3aMQ8HHIsudARPDjuC2tzXO2EJCQQaPiTas0vqTjdsjLFjP59S
dmzqbkknwkFJDYBYtYjOGCnTRTbOS5JqRZxWPuiHzUXSFwg8jdTm7oUchcbbkKkJ
hlidbcpktrj04fq1IjwlXqSCKUeKN+zbiHP1CQKBgB6jZ7Vk6CGBSZTS5fJB8kgD
3IEX3K7mZF/4B2C48gvfysASR2hdDFfLzg2WTZhiavjWM91UDcvfPsIMAnFSA29I
oVYaZdBlkFJCGwImVOynn0GZQL5oTCN4He5k20Y0mSOX995ngXHSYsCNSbyyNwSQ
uOlbvNTIooMkhu2Tpr52
-----END PRIVATE KEY-----

20
lib/http/testdata/emptyclient.crt vendored Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDNjCCAh4CFGB8dDYPVQB1CqlJs2esk3l1wymHMA0GCSqGSIb3DQEBCwUAMGcx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMQ8wDQYDVQQKDAZyY2xv
bmUxEzARBgNVBAsMCnJjbG9uZS1kZXYxHTAbBgNVBAMMFHJjbG9uZS1kZXYtY2xp
ZW50LWNhMB4XDTIzMDUyNjAzNTIzMVoXDTMzMDUyMzAzNTIzMVowSDELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xDzANBgNVBAoMBnJjbG9uZTETMBEG
A1UECwwKcmNsb25lLWRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AJqTaR9wVpkpalY972W102Fj5LL+cSvqte4kSzp2RTlRW5CXa5AJat+IXSeUln/6
TJdwnpnRyHP12XSWWlqTeBG1Q6cDBMt7GRrIqK5qEitDNihlSVElJkeFHDStT79a
YJbyZ86IJXGKXP42TZGv56NkC/UCLbpRV7lq7zNgrCptZH+ZClRcNq7UGGsxEgzy
iISQ2ALf9MFtVxq85J76pi5nJ1WYc6d3usSBPk9uLWQvTPNNoVf35SRCWUPNHM0O
cOXIMicUIlpm6Ksh7KfiEEuuHlNH6F9YnlmEQkXzR+90KfSwwBH+jlBAUuuSkI4s
a5lES42sOEdjgno8lThXPgcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAikhwkc1d
O+9fO9fMqi7iqdMBzF8c5F004IH1JRVHnkaMEOo2I+dUgVwwGj3iVQA7PdJce3Ij
GPwef1Wc5RdyhuwigjPO8Z4zRMBAASFnwerVS31HhO42ZS9sdfJwldBnBX4h/uKy
4ib3XVj+VfCBKDuKbV4Z6cjp9r81vO96xE5PVbNqK+DH1qNUIS+jDPJDK8nAyL7z
YjyluIR8ECbJje2WGIaNK/kNlz/8sRO64z8FZZbI67sL1fsauNcq8EFURq55tcfA
llwvwVB9Guns70pEsgvZLO5Vy+Gzq+veFdbmJ+aa2ayCor6hdZ8N7r8/Mj6KeEXZ
Eq03flwdARJKJQ==
-----END CERTIFICATE-----

16
lib/http/testdata/emptyclient.csr vendored Normal file
View File

@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICjTCCAXUCAQAwSDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24x
DzANBgNVBAoMBnJjbG9uZTETMBEGA1UECwwKcmNsb25lLWRldjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAJqTaR9wVpkpalY972W102Fj5LL+cSvqte4k
Szp2RTlRW5CXa5AJat+IXSeUln/6TJdwnpnRyHP12XSWWlqTeBG1Q6cDBMt7GRrI
qK5qEitDNihlSVElJkeFHDStT79aYJbyZ86IJXGKXP42TZGv56NkC/UCLbpRV7lq
7zNgrCptZH+ZClRcNq7UGGsxEgzyiISQ2ALf9MFtVxq85J76pi5nJ1WYc6d3usSB
Pk9uLWQvTPNNoVf35SRCWUPNHM0OcOXIMicUIlpm6Ksh7KfiEEuuHlNH6F9YnlmE
QkXzR+90KfSwwBH+jlBAUuuSkI4sa5lES42sOEdjgno8lThXPgcCAwEAAaAAMA0G
CSqGSIb3DQEBCwUAA4IBAQBtGAtDmIdSZOpKLNHMqruN2J/ZP/W7N00wEViu3Etu
3GS5UofXoqVfeRVp6phbp8KdXBiU/VkMAWIAC8ZDqvQGArD/pr4mrIaqiWrzBQDG
NxuyXz3aRjkR9CVjRNyWiodQPY2lSkKlgVg0Erbb5TaWWzt9AHbDO1pUhg748CkY
AGZoLZvxWIR0XivCyFqYbhFOW6yzgXgqxrCr5wd2OGyrzaZBQUoydp1EVGZHkgGp
d8ZUH7cb497SAPcGImCgB1RQdFAHmUI6DjPJmsTe+4dcATDBL+IayUOGedWLu3yZ
PZc1O8/f50YjdLIeHuNqiIBb4hlKCoikZ1cdp/7J2hrq
-----END CERTIFICATE REQUEST-----

28
lib/http/testdata/emptyclient.key vendored Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCak2kfcFaZKWpW
Pe9ltdNhY+Sy/nEr6rXuJEs6dkU5UVuQl2uQCWrfiF0nlJZ/+kyXcJ6Z0chz9dl0
llpak3gRtUOnAwTLexkayKiuahIrQzYoZUlRJSZHhRw0rU+/WmCW8mfOiCVxilz+
Nk2Rr+ejZAv1Ai26UVe5au8zYKwqbWR/mQpUXDau1BhrMRIM8oiEkNgC3/TBbVca
vOSe+qYuZydVmHOnd7rEgT5Pbi1kL0zzTaFX9+UkQllDzRzNDnDlyDInFCJaZuir
Ieyn4hBLrh5TR+hfWJ5ZhEJF80fvdCn0sMAR/o5QQFLrkpCOLGuZREuNrDhHY4J6
PJU4Vz4HAgMBAAECggEAL+ExUretW0vk0Enm+Y5Up3oVwQvnaj8Nk3JSiw1Pa+2z
exosCzWfkRXgJP51j7asOsx7lBHTEXg5n09jNWMwceu/xN++gHjk0dMNzNi2QAhV
ojWdfDERpl2o2vhEF3WbLaZwWRz63CyLmYKgjFv8WDQJMB84otnHXnutFDEBozI7
0+QQLacPVCuqid48x/ydDAzUdggmSkaB4WoIzYzEHHa1abC+giZZSxy9tMKAHHJH
rKuAANGC18cGeeQcGYxDz5FDYQiEZu/NEEv1gGbbOaMBu5pCZ6+43jzydv7BYiXJ
fzrCryeFihVzEG2Ri28JYYKi01YkOE3k2zsLSQUF4QKBgQDTcFsA8cveO03mZDFh
m6lf3K9EuDkJmGiusG/tYb+zfTkBwLNRQysQynjgq/5nxAVz/XawuvMSX+DBxvUR
IEWX2TCt62gb6eQW3q6S4RIBbp8qwQ75tZXXOfc/G3ho5zmJDikWMfor1kUnZwKI
+y8RmlCIc4MubVuIOj9sVTjr+wKBgQC7JydjAE9PyOMT8Zncc5atXSagOC6bbd0B
5xk7vxojjmOkvoi4OPUzxP22P5tLKZdor8jBv1oGwgmKKLjlnqfkJhpHa35bFx9y
NQ6koUdCqaDkWtXv5lEFZWRYniGU62LJMSHlLBu7L6PwX+Hfz+r62lSCGzUZnA9g
yX+s5YksZQKBgAmgjAQ2/jlYKevblAQFumiK+8/9M1ukfN+3WOFOGhRqFzZlN8Tz
cfqJvYc9TZAb9MObPtQ9LuQfSXSJQo9NEN4hHX5NwafDtob0DK7TYKaACu8/axcj
lXb/RKqy7YCZRp1e76/7BpEIaI2quwrRpQsAI7qSx95NTGWfgVPFbZoRAoGBALFZ
cSGH8aCRpV4I3NzjTC4Mz8WUd9YiTgS3klnjxklbbWF4jObGUtY0HpjNvcOELk6u
BXhUdGNjDNc3r78okcDJuq1jV+HKD6qSTMYFbxnk1OqQiZtEjhKm+mhfsUMFrB8r
yAr7uWuwwZHPyqPky6/bpamFTtRt5sS5LZwSB+NhAoGBALah1gCxay3pZLHg2Yhx
r5r4cUTaQmSs8NBqYeXHNx388ObQP01XxrD22XnyBKOqev4k9jSzz+RNRxl6kI8w
7IQhx2/dk5f032xmzIy/6nNrYI3qq0hwHkoPkG1g4VRDGBVscSQ5/IeZ6ysZbo5s
fOG8ouxBmrh03LCmnvYVGluA
-----END PRIVATE KEY-----