From 43f7a989beac3b7a06fa9769e669672b01098434 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Fri, 18 Aug 2023 09:01:40 +0200 Subject: [PATCH] feat(GODT-2771): added CLI commands for cert install/uninstall/status check on macOS. --- internal/certs/cert_store_darwin_test.go | 4 +- internal/certs/installer.go | 34 +++++++++++++--- internal/frontend/cli/accounts.go | 12 ++++++ internal/frontend/cli/frontend.go | 52 ++++++++++++++++++------ internal/frontend/cli/system.go | 45 ++++++++++++++++++++ 5 files changed, 128 insertions(+), 19 deletions(-) diff --git a/internal/certs/cert_store_darwin_test.go b/internal/certs/cert_store_darwin_test.go index 2f6fa84f..21cddbac 100644 --- a/internal/certs/cert_store_darwin_test.go +++ b/internal/certs/cert_store_darwin_test.go @@ -40,7 +40,7 @@ func TestCertInKeychain(t *testing.T) { } // This test require human interaction (macOS security prompts), and is disabled by default. -func _TestCertificateTrust(t *testing.T) { +func _TestCertificateTrust(t *testing.T) { //nolint:unused certPEM := generatePEMCertificate(t) require.False(t, isCertTrusted(certPEM)) require.NoError(t, addCertToKeychain(certPEM)) @@ -52,7 +52,7 @@ func _TestCertificateTrust(t *testing.T) { } // This test require human interaction (macOS security prompts), and is disabled by default. -func _TestInstallAndRemove(t *testing.T) { +func _TestInstallAndRemove(t *testing.T) { //nolint:unused certPEM := generatePEMCertificate(t) // fresh install diff --git a/internal/certs/installer.go b/internal/certs/installer.go index 7d164740..39ab0cf5 100644 --- a/internal/certs/installer.go +++ b/internal/certs/installer.go @@ -17,24 +17,48 @@ package certs -import "errors" +import ( + "errors" + + "github.com/sirupsen/logrus" +) var ( ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog") ) -type Installer struct{} +type Installer struct { + log *logrus.Entry +} func NewInstaller() *Installer { - return &Installer{} + return &Installer{ + log: logrus.WithField("pkg", "certs"), + } } func (installer *Installer) InstallCert(certPEM []byte) error { - return installCert(certPEM) + installer.log.Info("Installing the Bridge TLS certificate in the OS keychain") + + if err := installCert(certPEM); err != nil { + installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain") + return err + } + + installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain") + return nil } func (installer *Installer) UninstallCert(certPEM []byte) error { - return uninstallCert(certPEM) + installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain") + + if err := uninstallCert(certPEM); err != nil { + installer.log.WithError(err).Error("The Bridge TLS certificate could not be uninstalled from the OS keychain") + return err + } + + installer.log.Info("The Bridge TLS certificate was successfully uninstalled from the OS keychain") + return nil } func (installer *Installer) IsCertInstalled(certPEM []byte) bool { diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index cabb7413..bd2a1fa8 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -23,6 +23,7 @@ import ( "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" + "github.com/ProtonMail/proton-bridge/v3/internal/certs" "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/abiosoft/ishell" @@ -297,6 +298,17 @@ func (f *frontendCLI) configureAppleMail(c *ishell.Context) { return } + cert, _ := f.bridge.GetBridgeTLSCert() + installer := certs.NewInstaller() + if !installer.IsCertInstalled(cert) { + f.Println("Apple Mail requires that a TLS certificate for bridge IMAP and SMTP server is installed in your system keychain.") + f.Println("Please provide your credentials in the system popup dialog in order to continue.") + if err := installer.InstallCert(cert); err != nil { + f.printAndLogError(err) + return + } + } + if err := f.bridge.ConfigureAppleMail(context.Background(), user.UserID, user.Addresses[0]); err != nil { f.printAndLogError(err) return diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go index 3e84ca68..a3ff74b7 100644 --- a/internal/frontend/cli/frontend.go +++ b/internal/frontend/cli/frontend.go @@ -21,6 +21,7 @@ package cli import ( "errors" "os" + "runtime" "github.com/ProtonMail/gluon/async" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" @@ -145,25 +146,52 @@ func New( }) fe.AddCmd(dohCmd) - // Apple Mail commands. - configureCmd := &ishell.Cmd{ - Name: "configure-apple-mail", - Help: "Configures Apple Mail to use ProtonMail Bridge", - Func: fe.configureAppleMail, + //goland:noinspection GoBoolExpressions + if runtime.GOOS == "darwin" { + // Apple Mail commands. + configureCmd := &ishell.Cmd{ + Name: "configure-apple-mail", + Help: "Configures Apple Mail to use ProtonMail Bridge", + Func: fe.configureAppleMail, + } + fe.AddCmd(configureCmd) } - fe.AddCmd(configureCmd) // TLS commands. - fe.AddCmd(&ishell.Cmd{ - Name: "export-tls-cert", - Help: "Export the TLS certificate used by the Bridge", + certCmd := &ishell.Cmd{ + Name: "cert", + Help: "Manage the TLS certificate used by Bridge", + } + + //goland:noinspection GoBoolExpressions + if runtime.GOOS == "darwin" { + certCmd.AddCmd(&ishell.Cmd{ + Name: "status", + Help: "Check if the TLS certificate used by Bridge is installed in the OS keychain", + Func: fe.tlsCertStatus, + }) + certCmd.AddCmd(&ishell.Cmd{ + Name: "install", + Help: "Install TLS certificate used by Bridge in the OS keychain", + Func: fe.installTLSCert, + }) + certCmd.AddCmd(&ishell.Cmd{ + Name: "uninstall", + Help: "Uninstall the TLS certificate used by Bridge from the OS keychain", + Func: fe.uninstallTLSCert, + }) + } + certCmd.AddCmd(&ishell.Cmd{ + Name: "export", + Help: "Export the TLS certificate used by Bridge", Func: fe.exportTLSCerts, }) - fe.AddCmd(&ishell.Cmd{ - Name: "import-tls-cert", - Help: "Import a TLS certificate to be used by the Bridge", + certCmd.AddCmd(&ishell.Cmd{ + Name: "import", + Help: "Import a TLS certificate to be used by Bridge", Func: fe.importTLSCerts, }) + fe.AddCmd(certCmd) // All mail visibility commands. allMailCmd := &ishell.Cmd{ diff --git a/internal/frontend/cli/system.go b/internal/frontend/cli/system.go index d163db1f..be6d03e4 100644 --- a/internal/frontend/cli/system.go +++ b/internal/frontend/cli/system.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" + "github.com/ProtonMail/proton-bridge/v3/internal/certs" "github.com/ProtonMail/proton-bridge/v3/pkg/ports" "github.com/abiosoft/ishell" ) @@ -240,6 +241,50 @@ func (f *frontendCLI) setGluonLocation(c *ishell.Context) { } } +func (f *frontendCLI) tlsCertStatus(_ *ishell.Context) { + cert, _ := f.bridge.GetBridgeTLSCert() + installer := certs.NewInstaller() + if installer.IsCertInstalled(cert) { + f.Println("The Bridge TLS certificate is already installed in the OS keychain.") + } else { + f.Println("The Bridge TLS certificate is not installed in the OS keychain.") + } +} + +func (f *frontendCLI) installTLSCert(_ *ishell.Context) { + cert, _ := f.bridge.GetBridgeTLSCert() + installer := certs.NewInstaller() + if installer.IsCertInstalled(cert) { + f.printAndLogError(errors.New("the Bridge TLS certificate is already installed in the OS keychain")) + return + } + + f.Println("Please provide your credentials in the system popup dialog in order to continue.") + if err := installer.InstallCert(cert); err != nil { + f.printAndLogError(err) + return + } + + f.Println("The Bridge TLS certificate was successfully installed in the OS keychain.") +} + +func (f *frontendCLI) uninstallTLSCert(_ *ishell.Context) { + cert, _ := f.bridge.GetBridgeTLSCert() + installer := certs.NewInstaller() + if !installer.IsCertInstalled(cert) { + f.printAndLogError(errors.New("the Bridge TLS certificate is not installed in the OS keychain")) + return + } + + f.Println("Please provide your credentials in the system popup dialog in order to continue.") + if err := installer.UninstallCert(cert); err != nil { + f.printAndLogError(err) + return + } + + f.Println("The Bridge TLS certificate was successfully uninstalled from the OS keychain.") +} + func (f *frontendCLI) exportTLSCerts(c *ishell.Context) { if location := f.readStringInAttempts("Enter a path to which to export the TLS certificate used for IMAP and SMTP", c.ReadLine, f.isCacheLocationUsable); location != "" { cert, key := f.bridge.GetBridgeTLSCert()