Skip to content

Commit

Permalink
Network: support TLS offloading with DER encoded client certs (#1340) (
Browse files Browse the repository at this point in the history
…#1358)

* Network: support TLS offloading with DER encoded client certs (#1340)

* release notes
  • Loading branch information
reinkrul authored Aug 18, 2022
1 parent 6f69533 commit 5afcf39
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 7 deletions.
19 changes: 17 additions & 2 deletions docs/pages/deployment/tls-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ In addition to the general TLS configuration, you need to configure the followin

* ``tls.offload`` needs to be set to ``incoming``
* ``tls.certheader`` needs to be set to the name of the header in which your proxy sets the certificate (e.g. ``X-SSl-CERT``).
The certificate must in be PEM or base64 encoded DER format.

The certificate and truststore will still need to be available to the Nuts node for making outbound connections.

For `NGINX <https://www.nginx.com/>`_ the proxy configuration could look as follows:

Expand All @@ -83,7 +86,7 @@ For `NGINX <https://www.nginx.com/>`_ the proxy configuration could look as foll
server {
server_name nuts;
listen 443 ssl http2;
listen 5555 ssl http2;
ssl_certificate /etc/nginx/ssl/server.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_client_certificate /etc/nginx/ssl/truststore.pem;
Expand All @@ -96,7 +99,19 @@ For `NGINX <https://www.nginx.com/>`_ the proxy configuration could look as foll
}
}
The certificate and truststore will still need to be available to the Nuts node for making outbound connections.
For `HAProxy <https://www.haproxy.com/>`_ the proxy configuration could look as follows:

.. code-block::
frontend grpc_service
mode http
bind :5555 proto h2 ssl crt /certificate.pem ca-file /truststore.pem verify required
default_backend grpc_servers
backend grpc_servers
mode http
http-request set-header X-SSL-CERT %{+Q}[ssl_c_der,base64]
server node1 nuts_node:5555 check proto h2
No TLS
******
Expand Down
13 changes: 12 additions & 1 deletion docs/pages/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
Release notes
#############

Whats has been changed, and how to update between versions.
What has been changed, and how to update between versions.

*****************
Chestnut update (v4.1.1)
*****************

Release date: 2022-08-18

This patch adds TLS offloading for gRPC connections with support for DER encoded client certificates.
This is required for supporting TLS offloading on HAProxy.

**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v4.1.0...v4.1.1

*****************
Chestnut update (v4.1.0)
Expand Down
32 changes: 30 additions & 2 deletions network/transport/grpc/tls_offloading.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/core"
Expand All @@ -33,6 +34,7 @@ import (
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"net/url"
"strings"
)

type tlsOffloadingAuthenticator struct {
Expand Down Expand Up @@ -78,13 +80,39 @@ func (t *tlsOffloadingAuthenticator) authenticate(serverStream grpc.ServerStream
if err != nil {
return nil, errors.New("TLS client header escaping is invalid")
}
certificates, err := core.ParseCertificates([]byte(unescaped))

var certificates []*x509.Certificate
if strings.Contains(unescaped, "-----BEGIN CERTIFICATE-----") {
certificates, err = t.parsePEMCert(unescaped)
} else {
certificates, err = t.parseDERCert(values[0])
}
if err != nil {
return nil, fmt.Errorf("invalid client certificate(s) in header: %w", err)
return nil, err
}
if len(certificates) != 1 {
return nil, fmt.Errorf("expected exactly 1 client certificate in header, found %d", len(certificates))
}
return certificates, err
}

func (t *tlsOffloadingAuthenticator) parseDERCert(data string) ([]*x509.Certificate, error) {
bytes, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, fmt.Errorf("unable to base64 decode client cert header: %w", err)
}
certificate, err := x509.ParseCertificate(bytes)
if err != nil {
return nil, fmt.Errorf("unable to DER decode client cert: %w", err)
}
return []*x509.Certificate{certificate}, nil
}

func (t *tlsOffloadingAuthenticator) parsePEMCert(data string) ([]*x509.Certificate, error) {
certificates, err := core.ParseCertificates([]byte(data))
if err != nil {
return nil, fmt.Errorf("invalid client certificate(s) in header: %w", err)
}
return certificates, nil
}

Expand Down
24 changes: 22 additions & 2 deletions network/transport/grpc/tls_offloading_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"testing"
)

const certAsDER = "MIIBLDCB06ADAgECAgkAmIRh+hEybUEwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yMTAyMjIxMjI4MDJaFw0yMzA1MjgxMjI4MDJaMBAxDjAMBgNVBAMMBW5vZGVCMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDj3lVuuswobsBm1hpLWJ3occMPnHRv31Z84t4xzTePeqHZkWgwhdoffRoWDFonBeC/pPyIdYPnyImTZTVYx6oaMUMBIwEAYDVR0RBAkwB4IFbm9kZUIwCgYIKoZIzj0EAwIDSAAwRQIhAJvWKKcU/JCjcR/Ub4XDfmbAAFacq1bqeU/3BXU+K6cIAiB20w4Tq+wb3MvK6j/MGz+DHPW0V4PGREsMS/kfnzWdTw=="
const certAsPEM = `-----BEGIN CERTIFICATE-----
MIIDJjCCAg6gAwIBAgIJAJES+D3F7kfeMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
BAMMB1Jvb3QgQ0EwHhcNMjEwMTI2MTE1MzUwWhcNMjMwNTAxMTE1MzUwWjAUMRIw
Expand All @@ -48,6 +49,9 @@ ZnECVkHdrKGz6OMFF8uU9t7N+xbzx5nFswEbJXw4AjTklXlyHeyuC0y09ZmWcUDs
16Gop6VMff6NkShfyUP3EPtvR4Mr33BDAXl8ePp6BFQFd1+IzBY//gfnNBObOqlA
zG0zvbFZM8oAu/AWf85MH4Ex06cbsimNUsJqu/cx4rDzqNF5iC2uKfKJ
-----END CERTIFICATE-----`
const invalidCertAsPEM = `-----BEGIN CERTIFICATE-----
invalid
-----END CERTIFICATE-----`

func Test_tlsOffloadingAuthenticator(t *testing.T) {
auth := tlsOffloadingAuthenticator{clientCertHeaderName: "cert"}
Expand Down Expand Up @@ -111,6 +115,14 @@ func Test_tlsOffloadingAuthenticator(t *testing.T) {

certs, err := auth.authenticate(serverStream)

assert.EqualError(t, err, "unable to base64 decode client cert header: illegal base64 data at input byte 3")
assert.Nil(t, certs)
})
t.Run("PEM: invalid certificate", func(t *testing.T) {
serverStream.ctx = contextWithMD(url.QueryEscape(invalidCertAsPEM))

certs, err := auth.authenticate(serverStream)

assert.EqualError(t, err, "invalid client certificate(s) in header: unable to decode PEM encoded data")
assert.Nil(t, certs)
})
Expand All @@ -119,17 +131,25 @@ func Test_tlsOffloadingAuthenticator(t *testing.T) {

certs, err := auth.authenticate(serverStream)

assert.EqualError(t, err, "expected exactly 1 client certificate in header, found 0")
assert.EqualError(t, err, "unable to DER decode client cert: x509: malformed certificate")
assert.Nil(t, certs)
})
t.Run("multiple certs", func(t *testing.T) {
t.Run("PEM: multiple certs", func(t *testing.T) {
serverStream.ctx = contextWithMD(url.QueryEscape(certAsPEM + "\n" + certAsPEM))

certs, err := auth.authenticate(serverStream)

assert.EqualError(t, err, "expected exactly 1 client certificate in header, found 2")
assert.Nil(t, certs)
})
t.Run("DER: ok", func(t *testing.T) {
serverStream.ctx = contextWithMD(certAsDER)

certs, err := auth.authenticate(serverStream)

assert.NoError(t, err)
assert.Len(t, certs, 1)
})
})

}

0 comments on commit 5afcf39

Please sign in to comment.