From 8f2e616e078da56ade2b8ee31bd92eb519b75207 Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Fri, 15 Jul 2022 17:46:11 +0200 Subject: [PATCH] GODT-1673: TLS certs generation for gRPC service Wait for Bridge certificate and use it for gRPC connection Other: add README file for Bridge-GUI prerequisites GODT-1673: Configure Client/Server to make use of the bridge cert Other : comments + todo on known issue Other: fix go import alias [skip-ci] --- internal/app/bridge/bridge.go | 1 + internal/frontend/.gitignore | 2 + .../frontend/bridge-gui/GRPC/GRPCClient.cpp | 111 +++++++++++++----- .../frontend/bridge-gui/GRPC/GRPCClient.h | 5 +- internal/frontend/bridge-gui/README.md | 28 +++++ internal/frontend/frontend.go | 3 + internal/frontend/grpc/certs.go | 69 ----------- internal/frontend/grpc/service.go | 17 +-- 8 files changed, 131 insertions(+), 105 deletions(-) create mode 100644 internal/frontend/bridge-gui/README.md delete mode 100644 internal/frontend/grpc/certs.go diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go index 5a044e02..a3422a09 100644 --- a/internal/app/bridge/bridge.go +++ b/internal/app/bridge/bridge.go @@ -162,6 +162,7 @@ func mailLoop(b *base.Base, c *cli.Context) error { //nolint:funlen frontendMode, !c.Bool(base.FlagNoWindow), b.CrashHandler, + b.TLS, b.Locations, b.Settings, b.Listener, diff --git a/internal/frontend/.gitignore b/internal/frontend/.gitignore index e2c7a202..3cb70d44 100644 --- a/internal/frontend/.gitignore +++ b/internal/frontend/.gitignore @@ -9,3 +9,5 @@ rcc.qrc rcc_cgo_*.go *.qmlc +# QtCreator env +CMakeLists.txt.user diff --git a/internal/frontend/bridge-gui/GRPC/GRPCClient.cpp b/internal/frontend/bridge-gui/GRPC/GRPCClient.cpp index 43ea8e12..6fbf284b 100644 --- a/internal/frontend/bridge-gui/GRPC/GRPCClient.cpp +++ b/internal/frontend/bridge-gui/GRPC/GRPCClient.cpp @@ -30,37 +30,94 @@ using namespace grpc; namespace { - - -/// \todo GODT-1673 Decide how to generate/store/share this certificate. - -std::string const cert = R"(-----BEGIN CERTIFICATE----- -MIIC5TCCAc2gAwIBAgIJAMUQK0VGexMsMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV -BAMMCWxvY2FsaG9zdDAeFw0yMjA2MTQxNjUyNTVaFw0yMjA3MTQxNjUyNTVaMBQx -EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL6T1JQ0jptq512PBLASpCLFB0px7KIzEml0oMUCkVgUF+2cayrvdBXJZnaO -SG+/JPnHDcQ/ecgqkh2Ii6a2x2kWA5KqWiV+bSHp0drXyUGJfM85muLsnrhYwJ83 -HHtweoUVebRZvHn66KjaH8nBJ+YVWyYbSUhJezcg6nBSEtkW+I/XUHu4S2C7FUc5 -DXPO3yWWZuZ22OZz70DY3uYE/9COuilotuKdj7XgeKDyKIvRXjPFyqGxwnnp6bXC -vWvrQdcxy0wM+vZxew3QtA/Ag9uKJU9owP6noauXw95l49lEVIA5KXVNtdaldVht -MO/QoelLZC7h79PK22zbii3x930CAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo -b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B -AQsFAAOCAQEAW/9PE8dcAN+0C3K96Xd6Y3qOOtQhRw+WlZXhtiqMtlJfTjvuGKs9 -58xuKcTvU5oobxLv+i5+4gpqLjUZZ9FBnYXZIACNVzq4PEXf+YdzcA+y6RS/rqT4 -dUjsuYrScAmdXK03Duw3HWYrTp8gsJzIaYGTltUrOn0E4k/TsZb/tZ6z+oH7Fi+p -wdsI6Ut6Zwm3Z7WLn5DDk8KvFjHjZkdsCb82SFSAUVrzWo5EtbLIY/7y3A5rGp9D -t0AVpuGPo5Vn+MW1WA9HT8lhjz0v5wKGMOBi3VYW+Yx8FWHDpacvbZwVM0MjMSAd -M7SXYbNDiLF4LwPLsunoLsW133Ky7s99MA== ------END CERTIFICATE-----)"; - - Empty empty; // re-used across client calls. +QString const configFolder = "protonmail/bridge"; +QString const certFile = "cert.pem"; int const maxConnectionTimeSecs = 60; ///< Amount of time after which we consider connection attemps to the server have failed. - +int const maxCertificateWaitMsecs = 60 * 1000; ///< Ammount of time we wait for he server to generate the certificate. } +//**************************************************************************************************************************************************** +/// \return user configuration directory used by bridge (based on Golang OS/File::UserConfigDir). +//**************************************************************************************************************************************************** +static const QString _userConfigDir(){ + QString dir; +#ifdef Q_OS_WIN + dir = qgetenv ("AppData"); + if (dir.isEmpty()) + throw Exception("%AppData% is not defined."); +#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN) + dir = qgetenv ("HOME"); + if (dir.isEmpty()) + throw Exception("$HOME is not defined."); + dir += "/Library/Application Support"; +#else + dir = qgetenv ("XDG_CONFIG_HOME"); + if (dir.isEmpty()) + dir = qgetenv ("HOME"); + if (dir.isEmpty()) + throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined"); + dir += "/.config"; +#endif + QString folder = dir + "/" + configFolder; + QDir().mkpath(folder); + + return folder; +} + +//**************************************************************************************************************************************************** +/// \brief wait for certificate generation by Bridge +/// \return server certificate generated by Bridge +//**************************************************************************************************************************************************** +std::string GRPCClient::getServerCertificate() +{ + const QString filename = _userConfigDir() + "/" + certFile; + QFile file(filename); + // TODO : the certificate can exist but still be invalid. + // If the certificate is close to its limit, the bridge will generate a new one. + // If we read the certificate before the bridge rewrites it the certificate will be invalid. + if (!file.exists()) + { + // wait for file creation + QFileSystemWatcher watcher(this); + if (!watcher.addPath(_userConfigDir())) + throw Exception("Failed to watch User Config Directory"); + connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &GRPCClient::configFolderChanged); + + // set up an eventLoop to wait for the certIsReady signal or timeout. + QTimer timer; + timer.setSingleShot(true); + QEventLoop loop; + connect(this, &GRPCClient::certIsReady, &loop, &QEventLoop::quit); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(maxCertificateWaitMsecs); + loop.exec(); + + // timeout case. + if(!timer.isActive()) + throw Exception("Server failed to generate certificate on time"); + //else certIsReadySignal. + } + + if (!file.open(QFile::ReadOnly)) + throw Exception("Failed to read the server certificate"); + QByteArray qbaCert = file.readAll(); + std::string cert(qbaCert.constData(), qbaCert.length()); + file.close(); + return cert; +} + +//**************************************************************************************************************************************************** +/// \brief Action on UserConfig directory changes, looking for the certificate creation +//**************************************************************************************************************************************************** +void GRPCClient::configFolderChanged() +{ + QFile cert(_userConfigDir() + "/" + certFile); + if (cert.exists()) + emit certIsReady(); +} //**************************************************************************************************************************************************** /// \param[out] outError If the function returns false, this variable contains a description of the error. @@ -71,9 +128,9 @@ bool GRPCClient::connectToServer(QString &outError) try { SslCredentialsOptions opts; - opts.pem_root_certs += cert; + opts.pem_root_certs += this->getServerCertificate(); - channel_ = CreateChannel("localhost:9292", grpc::SslCredentials(opts)); + channel_ = CreateChannel("127.0.0.1:9292", grpc::SslCredentials(opts)); if (!channel_) throw Exception("Channel creation failed."); diff --git a/internal/frontend/bridge-gui/GRPC/GRPCClient.h b/internal/frontend/bridge-gui/GRPC/GRPCClient.h index 5ef5b583..0c6561c6 100644 --- a/internal/frontend/bridge-gui/GRPC/GRPCClient.h +++ b/internal/frontend/bridge-gui/GRPC/GRPCClient.h @@ -171,6 +171,7 @@ signals: void changeKeychainFinished(); void hasNoKeychain(); void rebuildKeychain(); + void certIsReady(); signals: // mail releated events void noActiveKeyForRecipient(QString const &email); // _ func(email string) `signal:noActiveKeyForRecipient` @@ -182,7 +183,8 @@ public: grpc::Status startEventStream(); ///< Retrieve and signal the events in the event stream. grpc::Status stopEventStream(); ///< Stop the event stream. - +private slots: + void configFolderChanged(); private: grpc::Status simpleMethod(SimpleMethod method); ///< perform a gRPC call to a bool setter. @@ -196,6 +198,7 @@ private: grpc::Status getURL(StringGetter getter, QUrl& outValue); ///< Perform a gRPC call to a string getter, with resulted converted to QUrl. grpc::Status methodWithStringParam(StringParamMethod method, QString const& str); ///< Perfom a gRPC call that takes a string as a parameter and returns an Empty. + std::string getServerCertificate(); ///< Wait until server certificates is generated and retrieve it. void processAppEvent(grpc::AppEvent const &event); ///< Process an 'App' event. void processLoginEvent(grpc::LoginEvent const &event); ///< Process a 'Login' event. void processUpdateEvent(grpc::UpdateEvent const &event); ///< Process an 'Update' event. diff --git a/internal/frontend/bridge-gui/README.md b/internal/frontend/bridge-gui/README.md new file mode 100644 index 00000000..d0c23301 --- /dev/null +++ b/internal/frontend/bridge-gui/README.md @@ -0,0 +1,28 @@ +## Prerequisite + +```` bash +sudo apt install build-essential +sudo apt install tar curl zip unzip +sudo apt install linux-headers-$(uname -r) +sudo apt install mesa-common-dev libglu1-mesa-dev +```` + +## Define Qt5DIR + +```` bash +export QT5DIR=/opt/Qt/5.13.0/gcc_64 +```` + +## install vcpkg and define VCPKG_ROOT + +```` bash +git clone https://github.com/Microsoft/vcpkg.git +./vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$PWD/vcpkg +```` + +## install grpc & protobuf + +```` bash +./vcpkg install grpc +```` diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index 964d5687..be74865a 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -21,6 +21,7 @@ package frontend import ( "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" + "github.com/ProtonMail/proton-bridge/v2/internal/config/tls" "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc" @@ -47,6 +48,7 @@ func New( frontendType string, showWindowOnStart bool, panicHandler types.PanicHandler, + tls *tls.TLS, locations *locations.Locations, settings *settings.Settings, eventListener listener.Listener, @@ -64,6 +66,7 @@ func New( programName, showWindowOnStart, panicHandler, + tls, locations, settings, eventListener, diff --git a/internal/frontend/grpc/certs.go b/internal/frontend/grpc/certs.go deleted file mode 100644 index b83a0cfb..00000000 --- a/internal/frontend/grpc/certs.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package grpc - -//goland:noinspection SpellCheckingInspection -const ( - serverCert = `-----BEGIN CERTIFICATE----- -MIIC5TCCAc2gAwIBAgIJAMUQK0VGexMsMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV -BAMMCWxvY2FsaG9zdDAeFw0yMjA2MTQxNjUyNTVaFw0yMjA3MTQxNjUyNTVaMBQx -EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL6T1JQ0jptq512PBLASpCLFB0px7KIzEml0oMUCkVgUF+2cayrvdBXJZnaO -SG+/JPnHDcQ/ecgqkh2Ii6a2x2kWA5KqWiV+bSHp0drXyUGJfM85muLsnrhYwJ83 -HHtweoUVebRZvHn66KjaH8nBJ+YVWyYbSUhJezcg6nBSEtkW+I/XUHu4S2C7FUc5 -DXPO3yWWZuZ22OZz70DY3uYE/9COuilotuKdj7XgeKDyKIvRXjPFyqGxwnnp6bXC -vWvrQdcxy0wM+vZxew3QtA/Ag9uKJU9owP6noauXw95l49lEVIA5KXVNtdaldVht -MO/QoelLZC7h79PK22zbii3x930CAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo -b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B -AQsFAAOCAQEAW/9PE8dcAN+0C3K96Xd6Y3qOOtQhRw+WlZXhtiqMtlJfTjvuGKs9 -58xuKcTvU5oobxLv+i5+4gpqLjUZZ9FBnYXZIACNVzq4PEXf+YdzcA+y6RS/rqT4 -dUjsuYrScAmdXK03Duw3HWYrTp8gsJzIaYGTltUrOn0E4k/TsZb/tZ6z+oH7Fi+p -wdsI6Ut6Zwm3Z7WLn5DDk8KvFjHjZkdsCb82SFSAUVrzWo5EtbLIY/7y3A5rGp9D -t0AVpuGPo5Vn+MW1WA9HT8lhjz0v5wKGMOBi3VYW+Yx8FWHDpacvbZwVM0MjMSAd -M7SXYbNDiLF4LwPLsunoLsW133Ky7s99MA== ------END CERTIFICATE-----` - - serverKey = `-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+k9SUNI6baudd -jwSwEqQixQdKceyiMxJpdKDFApFYFBftnGsq73QVyWZ2jkhvvyT5xw3EP3nIKpId -iIumtsdpFgOSqlolfm0h6dHa18lBiXzPOZri7J64WMCfNxx7cHqFFXm0Wbx5+uio -2h/JwSfmFVsmG0lISXs3IOpwUhLZFviP11B7uEtguxVHOQ1zzt8llmbmdtjmc+9A -2N7mBP/QjropaLbinY+14Hig8iiL0V4zxcqhscJ56em1wr1r60HXMctMDPr2cXsN -0LQPwIPbiiVPaMD+p6Grl8PeZePZRFSAOSl1TbXWpXVYbTDv0KHpS2Qu4e/Tytts -24ot8fd9AgMBAAECggEBAJFkGpOOnRU4s5YO3BavwgS8p9lFnLAJooxNa7GhSd0W -R0MBSEkTMU7FvaPI3L5T5xOfpoMHohLxV1Osrk3bt7oWD1e/GtLr5routejtIx8a -kttNKTriJhyhqSJOWy5ZGz+YqKbMpxuwLftTnVjAQX4o4MbrnjbFyHjAZdqW4sY2 -jLulfEdOave6nxaEocmIkoXEjuX90LB+yNG6ncSYM3GV+IyCVw7DsoU4dLd/IRDa -4iJVF7tVdAsZqN6/EVYXpGqG0t1HI8ddacHa1qWgCG3kBB+3faxXZcDJdlRrXLUQ -4jLH8oEfXOb5YgCwyYzW2EynXEpG5vjsPmsCWJY/mIECgYEA52av81+lui97KLg+ -T07XtR8zJPMkHnBNfc6ooWku/+0NuQPpUq14vqzRVut9jBHUDP3xSvrPnXsp15ZA -/mipLQLNKssTYtk90cyGqLUkrd/NPLFZLXToBfWBlfazdcJQQRIxZ2dTy5MH+HIU -Oio3LZi+iDIbdzzSlmL8PaLit20CgYEA0tYsswhq6OaWx25iu4hBMRlt6hr9qGVW -jlzCFjBhlh3YtoBti2w2fsJdU+hUpeXU327fhFmdCQFXtf+Om5CSHihmJ+mHj9O1 -5Jd6zn4o8szdg5je9T4gt7KG6QdXaFJ2aMuq+SxZl1NIE+9qnf/qom4GHHZ/Nj41 -vwlQu+zS5lECgYAOzSK0DoorPp5CHIbfy8tAap563pKQ394VDgL7UB8Rf7hA/V8P -SslOaP9679U4AGvv6M5mXWSqThZ/E71UiJ1Jo8Q72IGE8SBjKxHx+KQ/+vDF0RJD -NhchSnLfhMg14BgCEYfXdWSGwQDhg2qHzet5nyuQyqO3HMzbkblQt/qIgQKBgHLv -nPiQmy+SHRplO9+93MQ2d6wKwMNfUztSp9/OyjQ62xxKkO1TtbWOobAPVK4Hx+9y -EtmkvK3fFIC763M08eMM5PvXHDa1FFCkn6cYMZyDQDLwUINjNhTOdytr/CN76N8i -QHeLzN9o4D814mp1y+R2lFBJ7PmWGlilbGS2KxaxAoGAFMsb1MER+eTOUO3z05Di -lts4VRWQhq2frd/on6AcTv4idQox1RcOrKWQbRVgeQVY1SkkHhg8lN0jX3W3EfuQ -aOfyky04GbLiwO8NRHZMlORWLxlCkrUrb6Va+LQlT0JvpQbqdbu6Ix8NomG9K697 -aScKmY7bGC0ki2IIdt2YZ5I= ------END PRIVATE KEY-----` -) diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index cd4df72c..d3400d2a 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -20,7 +20,7 @@ package grpc //go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative bridge.proto import ( - "crypto/tls" + cryptotls "crypto/tls" "net" "runtime" "strings" @@ -29,6 +29,7 @@ import ( "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" + bridgetls "github.com/ProtonMail/proton-bridge/v2/internal/config/tls" "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" "github.com/ProtonMail/proton-bridge/v2/internal/events" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" @@ -54,6 +55,7 @@ type Service struct { // nolint:structcheck programName string programVersion string panicHandler types.PanicHandler + tls *bridgetls.TLS locations *locations.Locations settings *settings.Settings eventListener listener.Listener @@ -78,6 +80,7 @@ func NewService( programName string, showOnStartup bool, panicHandler types.PanicHandler, + tls *bridgetls.TLS, locations *locations.Locations, settings *settings.Settings, eventListener listener.Listener, @@ -93,6 +96,7 @@ func NewService( programName: programName, programVersion: version, panicHandler: panicHandler, + tls: tls, locations: locations, settings: settings, eventListener: eventListener, @@ -109,19 +113,16 @@ func NewService( } s.userAgent.SetPlatform(runtime.GOOS) // TO-DO GODT-1672 In the previous Qt frontend, this routine used QSysInfo::PrettyProductName to return a more accurate description, e.g. "Windows 10" or "MacOS 10.12" - - cert, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey)) + config, err := tls.GetConfig() + config.ClientAuth = cryptotls.NoClientCert // skip client auth if the certificate allow it. if err != nil { - s.log.WithError(err).Error("could not create key pair") + s.log.WithError(err).Error("could not get TLS config") panic(err) } s.initAutostart() - s.grpcServer = grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{ - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS13, - }))) + s.grpcServer = grpc.NewServer(grpc.Creds(credentials.NewTLS(config))) RegisterBridgeServer(s.grpcServer, &s)