mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
3ca438e3d5
Mutual TLS helps prevent untrusted clients communicating with services like Authelia. This can be utilized to reduce the attack surface. Fixes #3041
380 lines
10 KiB
Go
380 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/elliptic"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/authelia/authelia/v4/internal/logging"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/valyala/fasthttp"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
|
"github.com/authelia/authelia/v4/internal/utils"
|
|
)
|
|
|
|
// TemporaryCertificate contains the FD of 2 temporary files containing the PEM format of the certificate and private key.
|
|
type TemporaryCertificate struct {
|
|
CertFile *os.File
|
|
KeyFile *os.File
|
|
|
|
Certificate *x509.Certificate
|
|
|
|
CertificatePEM []byte
|
|
KeyPEM []byte
|
|
}
|
|
|
|
func (tc TemporaryCertificate) TLSCertificate() (tls.Certificate, error) {
|
|
return tls.LoadX509KeyPair(tc.CertFile.Name(), tc.KeyFile.Name())
|
|
}
|
|
|
|
func (tc *TemporaryCertificate) Close() {
|
|
if tc.CertFile != nil {
|
|
tc.CertFile.Close()
|
|
}
|
|
|
|
if tc.KeyFile != nil {
|
|
tc.KeyFile.Close()
|
|
}
|
|
}
|
|
|
|
type CertificateContext struct {
|
|
Certificates []TemporaryCertificate
|
|
privateKeyBuilder utils.PrivateKeyBuilder
|
|
}
|
|
|
|
// NewCertificateContext instantiate a new certificate context used to easily generate certificates within tests.
|
|
func NewCertificateContext(privateKeyBuilder utils.PrivateKeyBuilder) (*CertificateContext, error) {
|
|
certificateContext := new(CertificateContext)
|
|
certificateContext.privateKeyBuilder = privateKeyBuilder
|
|
|
|
cert, err := certificateContext.GenerateCertificate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certificateContext.Certificates = []TemporaryCertificate{*cert}
|
|
|
|
return certificateContext, nil
|
|
}
|
|
|
|
// GenerateCertificate generate a new certificate in the context.
|
|
func (cc *CertificateContext) GenerateCertificate() (*TemporaryCertificate, error) {
|
|
certBytes, keyBytes, err := utils.GenerateCertificate(cc.privateKeyBuilder,
|
|
[]string{"authelia.com", "example.org", "local.example.com"},
|
|
time.Now(), 3*time.Hour, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to generate certificate: %v", err)
|
|
}
|
|
|
|
tmpCertificate := new(TemporaryCertificate)
|
|
|
|
certFile, err := os.CreateTemp("", "cert")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create temp file for certificate: %v", err)
|
|
}
|
|
|
|
tmpCertificate.CertFile = certFile
|
|
tmpCertificate.CertificatePEM = certBytes
|
|
|
|
block, _ := pem.Decode(certBytes)
|
|
c, err := x509.ParseCertificate(block.Bytes)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse certificate: %v", err)
|
|
}
|
|
|
|
tmpCertificate.Certificate = c
|
|
|
|
err = os.WriteFile(tmpCertificate.CertFile.Name(), certBytes, 0600)
|
|
if err != nil {
|
|
tmpCertificate.Close()
|
|
return nil, fmt.Errorf("unable to write certificates in file: %v", err)
|
|
}
|
|
|
|
keyFile, err := os.CreateTemp("", "key")
|
|
if err != nil {
|
|
tmpCertificate.Close()
|
|
return nil, fmt.Errorf("unable to create temp file for private key: %v", err)
|
|
}
|
|
|
|
tmpCertificate.KeyFile = keyFile
|
|
tmpCertificate.KeyPEM = keyBytes
|
|
|
|
err = os.WriteFile(tmpCertificate.KeyFile.Name(), keyBytes, 0600)
|
|
if err != nil {
|
|
tmpCertificate.Close()
|
|
return nil, fmt.Errorf("unable to write private key in file: %v", err)
|
|
}
|
|
|
|
cc.Certificates = append(cc.Certificates, *tmpCertificate)
|
|
|
|
return tmpCertificate, nil
|
|
}
|
|
|
|
func (cc *CertificateContext) Close() {
|
|
for _, tc := range cc.Certificates {
|
|
tc.Close()
|
|
}
|
|
}
|
|
|
|
type TLSServerContext struct {
|
|
server *fasthttp.Server
|
|
port int
|
|
}
|
|
|
|
func NewTLSServerContext(configuration schema.Configuration) (*TLSServerContext, error) {
|
|
serverContext := new(TLSServerContext)
|
|
|
|
s, listener := CreateServer(configuration, middlewares.Providers{})
|
|
serverContext.server = s
|
|
|
|
go func() {
|
|
err := s.Serve(listener)
|
|
if err != nil {
|
|
logging.Logger().Fatal(err)
|
|
}
|
|
}()
|
|
|
|
addrSplit := strings.Split(listener.Addr().String(), ":")
|
|
if len(addrSplit) > 1 {
|
|
port, err := strconv.ParseInt(addrSplit[len(addrSplit)-1], 10, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse port from address: %v", err)
|
|
}
|
|
|
|
serverContext.port = int(port)
|
|
}
|
|
|
|
return serverContext, nil
|
|
}
|
|
|
|
func (sc *TLSServerContext) Port() int {
|
|
return sc.port
|
|
}
|
|
|
|
func (sc *TLSServerContext) Close() error {
|
|
return sc.server.Shutdown()
|
|
}
|
|
|
|
func TestShouldRaiseErrorWhenClientDoesNotSkipVerify(t *testing.T) {
|
|
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
|
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
|
require.NoError(t, err)
|
|
|
|
defer certificateContext.Close()
|
|
|
|
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
|
Server: schema.ServerConfiguration{
|
|
TLS: schema.ServerTLSConfiguration{
|
|
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
|
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
defer tlsServerContext.Close()
|
|
|
|
fmt.Println(tlsServerContext.Port())
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d", tlsServerContext.Port()), nil)
|
|
require.NoError(t, err)
|
|
|
|
_, err = http.DefaultClient.Do(req)
|
|
require.Error(t, err)
|
|
|
|
require.Contains(t, err.Error(), "x509: certificate signed by unknown authority")
|
|
}
|
|
|
|
func TestShouldServeOverTLSWhenClientDoesSkipVerify(t *testing.T) {
|
|
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
|
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
|
require.NoError(t, err)
|
|
|
|
defer certificateContext.Close()
|
|
|
|
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
|
Server: schema.ServerConfiguration{
|
|
TLS: schema.ServerTLSConfiguration{
|
|
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
|
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
defer tlsServerContext.Close()
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
|
require.NoError(t, err)
|
|
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // Needs to be enabled in tests. Not used in production.
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
res, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
defer res.Body.Close()
|
|
|
|
_, err = io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "404 Not Found", res.Status)
|
|
}
|
|
|
|
func TestShouldServeOverTLSWhenClientHasProperRootCA(t *testing.T) {
|
|
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
|
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
|
require.NoError(t, err)
|
|
|
|
defer certificateContext.Close()
|
|
|
|
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
|
Server: schema.ServerConfiguration{
|
|
TLS: schema.ServerTLSConfiguration{
|
|
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
|
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
defer tlsServerContext.Close()
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
|
require.NoError(t, err)
|
|
|
|
block, _ := pem.Decode(certificateContext.Certificates[0].CertificatePEM)
|
|
c, err := x509.ParseCertificate(block.Bytes)
|
|
require.NoError(t, err)
|
|
|
|
// Create a root CA for the client to properly validate server cert.
|
|
rootCAs := x509.NewCertPool()
|
|
rootCAs.AddCert(c)
|
|
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: rootCAs,
|
|
MinVersion: tls.VersionTLS13,
|
|
},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
res, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
defer res.Body.Close()
|
|
|
|
_, err = io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "404 Not Found", res.Status)
|
|
}
|
|
|
|
func TestShouldRaiseWhenMutualTLSIsConfiguredAndClientIsNotAuthenticated(t *testing.T) {
|
|
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
|
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
|
require.NoError(t, err)
|
|
|
|
defer certificateContext.Close()
|
|
|
|
clientCert, err := certificateContext.GenerateCertificate()
|
|
require.NoError(t, err)
|
|
|
|
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
|
Server: schema.ServerConfiguration{
|
|
TLS: schema.ServerTLSConfiguration{
|
|
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
|
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
|
ClientCertificates: []string{clientCert.CertFile.Name()},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
defer tlsServerContext.Close()
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
|
require.NoError(t, err)
|
|
|
|
// Create a root CA for the client to properly validate server cert.
|
|
rootCAs := x509.NewCertPool()
|
|
rootCAs.AddCert(certificateContext.Certificates[0].Certificate)
|
|
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: rootCAs,
|
|
MinVersion: tls.VersionTLS13,
|
|
},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
_, err = client.Do(req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "remote error: tls: bad certificate")
|
|
}
|
|
|
|
func TestShouldServeProperlyWhenMutualTLSIsConfiguredAndClientIsAuthenticated(t *testing.T) {
|
|
privateKeyBuilder := utils.ECDSAKeyBuilder{}.WithCurve(elliptic.P256())
|
|
certificateContext, err := NewCertificateContext(privateKeyBuilder)
|
|
require.NoError(t, err)
|
|
|
|
defer certificateContext.Close()
|
|
|
|
clientCert, err := certificateContext.GenerateCertificate()
|
|
require.NoError(t, err)
|
|
|
|
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
|
|
Server: schema.ServerConfiguration{
|
|
TLS: schema.ServerTLSConfiguration{
|
|
Certificate: certificateContext.Certificates[0].CertFile.Name(),
|
|
Key: certificateContext.Certificates[0].KeyFile.Name(),
|
|
ClientCertificates: []string{clientCert.CertFile.Name()},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
defer tlsServerContext.Close()
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://local.example.com:%d/api/notfound", tlsServerContext.Port()), nil)
|
|
require.NoError(t, err)
|
|
|
|
// Create a root CA for the client to properly validate server cert.
|
|
rootCAs := x509.NewCertPool()
|
|
rootCAs.AddCert(certificateContext.Certificates[0].Certificate)
|
|
|
|
cCert, err := certificateContext.Certificates[1].TLSCertificate()
|
|
require.NoError(t, err)
|
|
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: rootCAs,
|
|
Certificates: []tls.Certificate{cCert},
|
|
MinVersion: tls.VersionTLS13,
|
|
},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
res, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
defer res.Body.Close()
|
|
|
|
_, err = io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "404 Not Found", res.Status)
|
|
}
|