diff --git a/docs/HSM-PKCS11.md b/docs/HSM-PKCS11.md index ae4b9977c49..2092b461660 100644 --- a/docs/HSM-PKCS11.md +++ b/docs/HSM-PKCS11.md @@ -28,7 +28,7 @@ To import the server test key into SoftHSM, for example: To launch ghostunnel with the SoftHSM-backed PKCS11 key (on macOS): ghostunnel server \ - --keystore test-keys/server-cert.pem \ + --cert test-keys/server-cert.pem \ --pkcs11-module /usr/local/Cellar/softhsm/2.4.0/lib/softhsm/libsofthsm2.so \ --pkcs11-token-label ghostunnel-server \ --pkcs11-pin 1234 \ @@ -43,7 +43,7 @@ to use environment variables to set PKCS11 options instead of flags (via `PKCS11_MODULE`, `PKCS11_TOKEN_LABEL` and `PKCS11_PIN`), useful if you don't want to show the PIN on the command line. -Note that `--keystore` needs to point to the certificate chain that corresponds +Note that `--cert` needs to point to the certificate chain that corresponds to the private key in the PKCS#11 module, with the leaf certificate being the first certificate in the chain. Ghostunnel doesn't have the ability to read the certificate chain directly from the module at this point in time. diff --git a/main.go b/main.go index 559c94f7683..25e39ee4305 100644 --- a/main.go +++ b/main.go @@ -29,18 +29,18 @@ import ( "strings" "time" - "github.com/cyberdelia/go-metrics-graphite" - "github.com/hashicorp/go-syslog" - "github.com/kavu/go_reuseport" - "github.com/mwitkow/go-http-dialer" + graphite "github.com/cyberdelia/go-metrics-graphite" + gsyslog "github.com/hashicorp/go-syslog" + reuseport "github.com/kavu/go_reuseport" + http_dialer "github.com/mwitkow/go-http-dialer" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/rcrowley/go-metrics" + metrics "github.com/rcrowley/go-metrics" "github.com/square/ghostunnel/auth" "github.com/square/ghostunnel/certloader" "github.com/square/ghostunnel/proxy" "github.com/square/ghostunnel/wildcard" - "github.com/square/go-sq-metrics" - "gopkg.in/alecthomas/kingpin.v2" + sqmetrics "github.com/square/go-sq-metrics" + kingpin "gopkg.in/alecthomas/kingpin.v2" prometheusmetrics "github.com/deathowl/go-metrics-prometheus" "github.com/prometheus/client_golang/prometheus" @@ -90,10 +90,10 @@ var ( clientDisableAuth = clientCommand.Flag("disable-authentication", "Disable client authentication, no certificate will be provided to the server.").Default("false").Bool() // TLS options - keystorePath = app.Flag("keystore", "Path to certificate and keystore (PEM with certificate/key, or PKCS12).").PlaceHolder("PATH").String() - cert = app.Flag("cert", "Path to certificate (PEM without key).").PlaceHolder("PATH").String() - key = app.Flag("key", "Path to certificate private key (PEM key).").PlaceHolder("PATH").String() - keystorePass = app.Flag("storepass", "Password for certificate and keystore (optional).").PlaceHolder("PASS").String() + keystorePath = app.Flag("keystore", "Path to keystore (combined PEM with cert/key, or PKCS12 keystore).").PlaceHolder("PATH").String() + certPath = app.Flag("cert", "Path to certificate (PEM with certificate chain).").PlaceHolder("PATH").String() + keyPath = app.Flag("key", "Path to certificate private key (PEM with private key).").PlaceHolder("PATH").String() + keystorePass = app.Flag("storepass", "Password for keystore (if using PKCS keystore, optional).").PlaceHolder("PASS").String() caBundlePath = app.Flag("cacert", "Path to CA bundle file (PEM/X509). Uses system trust store by default.").String() enabledCipherSuites = app.Flag("cipher-suites", "Set of cipher suites to enable, comma-separated, in order of preference (AES, CHACHA).").Default("AES,CHACHA").String() @@ -217,14 +217,14 @@ func serverValidateFlags() error { len(*serverAllowedIPs) > 0 || len(*serverAllowedURIs) > 0 - if (*key != "" && *cert == "") || (*key == "" && *cert != "") { - return errors.New("both key and cert are required") + if ((*keyPath != "" && *certPath == "") || (*keyPath == "" && *certPath != "")) && !hasPKCS11() { + return errors.New("when using --cert, must also specify --key") } - if *key != "" && *cert != "" && *keystorePath != "" { - return errors.New("Cannot specificy both key/cert and keystorePath") + if *keyPath != "" && *certPath != "" && *keystorePath != "" { + return errors.New("--key/--cert and --keystore are mutually exclusive") } - if *keystorePath == "" && !hasKeychainIdentity() { - return errors.New("at least one of --keystore/cert/key or --keychain-identity (if supported) flags is required") + if *keystorePath == "" && !hasKeychainIdentity() && *certPath == "" { + return errors.New("at least one of --keystore, --cert/--key or --keychain-identity (if supported) flags is required") } if *keystorePath != "" && hasKeychainIdentity() { return errors.New("--keystore and --keychain-identity flags are mutually exclusive") @@ -333,7 +333,7 @@ func run(args []string) error { } metrics := sqmetrics.NewMetrics(*metricsURL, *metricsPrefix, client, *metricsInterval, metrics.DefaultRegistry, logger) - cert, err := buildCertificate(*keystorePath, *cert, *key, *keystorePass) + cert, err := buildCertificate(*keystorePath, *certPath, *keyPath, *keystorePass) if err != nil { fmt.Fprintf(os.Stderr, "error: unable to load certificates: %s\n", err) return err diff --git a/tests/test-client-reloads-certificate.py b/tests/test-client-reloads-keystore.py similarity index 100% rename from tests/test-client-reloads-certificate.py rename to tests/test-client-reloads-keystore.py diff --git a/tests/test-server-pkcs11-module.py b/tests/test-server-pkcs11-module.py index de12ff23b75..7f536134cff 100755 --- a/tests/test-server-pkcs11-module.py +++ b/tests/test-server-pkcs11-module.py @@ -26,7 +26,7 @@ '--listen={0}:13001'.format(LOCALHOST), '--target={0}:{1}'.format(LOCALHOST, STATUS_PORT), - '--keystore=../test-keys/server-cert.pem', + '--cert=../test-keys/server-cert.pem', '--pkcs11-module={0}'.format(os.environ['GHOSTUNNEL_TEST_PKCS11_MODULE']), '--pkcs11-token-label={0}'.format(os.environ['GHOSTUNNEL_TEST_PKCS11_LABEL']), '--pkcs11-pin={0}'.format(os.environ['GHOSTUNNEL_TEST_PKCS11_PIN']), diff --git a/tests/test-server-reloads-certificate.py b/tests/test-server-reloads-keystore.py similarity index 100% rename from tests/test-server-reloads-certificate.py rename to tests/test-server-reloads-keystore.py diff --git a/tests/test-server-reloads-split-cert-key.py b/tests/test-server-reloads-split-cert-key.py new file mode 100755 index 00000000000..23c9212a545 --- /dev/null +++ b/tests/test-server-reloads-split-cert-key.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +""" +Ensures that tunnel sees & reloads a certificate +change. +""" + +from common import LOCALHOST, RootCert, STATUS_PORT, SocketPair, TcpServer, TlsClient, print_ok, run_ghostunnel, terminate +import os +import signal + +if __name__ == "__main__": + ghostunnel = None + try: + # create certs + root = RootCert('root') + root.create_signed_cert('server') + root.create_signed_cert('new_server') + root.create_signed_cert('client') + + # start ghostunnel + ghostunnel = run_ghostunnel(['server', + '--listen={0}:13001'.format(LOCALHOST), + '--target={0}:13002'.format(LOCALHOST), + '--key=server.key', + '--cert=server.crt', + '--cacert=root.crt', + '--allow-ou=client', + '--status={0}:{1}'.format(LOCALHOST, + STATUS_PORT)]) + + # create connections with client + pair1 = SocketPair( + TlsClient('client', 'root', 13001), TcpServer(13002)) + pair1.validate_can_send_from_client("toto", "pair1 works") + pair1.validate_tunnel_ou("server", "pair1 -> ou=server") + + # Replace keystore and trigger reload + os.rename('new_server.crt', 'server.crt') + os.rename('new_server.key', 'server.key') + ghostunnel.send_signal(signal.SIGUSR1) + + TlsClient(None, 'root', STATUS_PORT).connect(20, 'new_server') + print_ok("reload done") + + # create connections with client + pair2 = SocketPair( + TlsClient('client', 'root', 13001), TcpServer(13002)) + pair2.validate_can_send_from_client("toto", "pair2 works") + pair2.validate_tunnel_ou("new_server", "pair2 -> ou=new_server") + pair2.cleanup() + + # ensure that pair1 is still alive + pair1.validate_can_send_from_client("toto", "pair1 still works") + pair1.cleanup() + + print_ok("OK") + finally: + terminate(ghostunnel) diff --git a/tests/test-server-split-cert-key.py b/tests/test-server-split-cert-key.py new file mode 100755 index 00000000000..7d189d666d5 --- /dev/null +++ b/tests/test-server-split-cert-key.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +""" +Test that ensures that we can use the --cert/--key flags. +""" + +from common import LOCALHOST, STATUS_PORT, TcpClient, print_ok, run_ghostunnel, terminate, RootCert, SocketPair, TlsClient, TcpServer +import urllib.request +import urllib.error +import urllib.parse +import os +import signal +import json +import sys + +if __name__ == "__main__": + ghostunnel = None + try: + root = RootCert('root') + root.create_signed_cert('server') + root.create_signed_cert('client') + + # start ghostunnel + ghostunnel = run_ghostunnel(['server', + '--listen={0}:13001'.format(LOCALHOST), + '--target={0}:13002'.format(LOCALHOST), + '--cert=server.crt', + '--key=server.key', + '--cacert=root.crt', + '--status={0}:{1}'.format(LOCALHOST, + STATUS_PORT), + '--allow-ou=client']) + + # connect with client, confirm that the tunnel is up + pair = SocketPair( + TlsClient('client', 'root', 13001), TcpServer(13002)) + pair.validate_can_send_from_client( + "hello world", "1: client -> server") + pair.validate_can_send_from_server( + "hello world", "1: server -> client") + pair.validate_closing_client_closes_server( + "1: client closed -> server closed") + + print_ok("OK") + finally: + terminate(ghostunnel) diff --git a/tls.go b/tls.go index 2bd424b8f7e..8dd9d74bc3c 100644 --- a/tls.go +++ b/tls.go @@ -43,7 +43,11 @@ var cipherSuites = map[string][]uint16{ // Build reloadable certificate func buildCertificate(keystorePath, certPath, keyPath, keystorePass string) (certloader.Certificate, error) { if hasPKCS11() { - return buildCertificateFromPKCS11(keystorePath) + if keystorePath != "" { + return buildCertificateFromPKCS11(keystorePath) + } else { + return buildCertificateFromPKCS11(certPath) + } } if hasKeychainIdentity() { return buildCertificateFromCertstore()