Skip to content

Commit

Permalink
Manage the HTTPS certificate from the menu and ask Safari users to in…
Browse files Browse the repository at this point in the history
…stall it at startup (#941)

* Add function to retrieve certificates expiration date

* Check the certificate expiration date

* Obtain certificates info using the systray icon

* Manage errors that may occur retrieving certificates expiration date

* Obtain default browser name on macOS

* Prompt Safari users to install HTTPS certificates and check if they are outdated when the Agent is started

* Skip some tests on macOS because the user is prompted to install certificates

* Set installCerts value in config.ini if certicates are manually installed

* Always set installCerts if the certificates exist

* Add "Arduino Agent" to the title of dialogs

* Fix check for pressed buttons

* Move osascript execution function to Utilities to avoid code duplication

* Modify certificate management from the systray menu

* Install certificates if they are missing and the flag inside the config is set to true

* Avoid code duplication

* Fix button order and title

* Do not restart the Agent if no action is performed on the certificate

* Do not modify the config if the default browser is not Safari

* Small messages/titles fixes

---------

Co-authored-by: Xayton <30591904+Xayton@users.noreply.github.com>
  • Loading branch information
MatteoPologruto and Xayton committed May 8, 2024
1 parent bf5b8e4 commit 222a505
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 37 deletions.
48 changes: 48 additions & 0 deletions certificates/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import (
"math/big"
"net"
"os"
"strings"
"time"

"github.com/arduino/arduino-create-agent/utilities"
"github.com/arduino/go-paths-helper"
log "github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -267,3 +269,49 @@ func DeleteCertificates(certDir *paths.Path) {
certDir.Join("cert.pem").Remove()
certDir.Join("cert.cer").Remove()
}

// isExpired checks if a certificate is expired or about to expire (less than 1 month)
func isExpired() (bool, error) {
bound := time.Now().AddDate(0, 1, 0)
dateS, err := GetExpirationDate()
if err != nil {
return false, err
}
date, _ := time.Parse(time.DateTime, dateS)
return date.Before(bound), nil
}

// PromptInstallCertsSafari prompts the user to install the HTTPS certificates if they are using Safari
func PromptInstallCertsSafari() bool {
buttonPressed := utilities.UserPrompt("display dialog \"The Arduino Agent needs a local HTTPS certificate to work correctly with Safari.\nIf you use Safari, you need to install it.\" buttons {\"Do not install\", \"Install the certificate for Safari\"} default button 2 with title \"Arduino Agent: Install certificate\"")
return strings.Contains(string(buttonPressed), "button returned:Install the certificate for Safari")
}

// PromptExpiredCerts prompts the user to update the HTTPS certificates if they are using Safari
func PromptExpiredCerts(certDir *paths.Path) {
if expired, err := isExpired(); err != nil {
log.Errorf("cannot check if certificates are expired something went wrong: %s", err)
} else if expired {
buttonPressed := utilities.UserPrompt("display dialog \"The Arduino Agent needs a local HTTPS certificate to work correctly with Safari.\nYour certificate is expired or close to expiration. Do you want to update it?\" buttons {\"Do not update\", \"Update the certificate for Safari\"} default button 2 with title \"Arduino Agent: Update certificate\"")
if strings.Contains(string(buttonPressed), "button returned:Update the certificate for Safari") {
err := UninstallCertificates()
if err != nil {
log.Errorf("cannot uninstall certificates something went wrong: %s", err)
} else {
DeleteCertificates(certDir)
GenerateAndInstallCertificates(certDir)
}
}
}
}

// GenerateAndInstallCertificates generates and installs the certificates
func GenerateAndInstallCertificates(certDir *paths.Path) {
GenerateCertificates(certDir)
err := InstallCertificate(certDir.Join("ca.cert.cer"))
// if something goes wrong during the cert install we remove them, so the user is able to retry
if err != nil {
log.Errorf("cannot install certificates something went wrong: %s", err)
DeleteCertificates(certDir)
}
}
94 changes: 88 additions & 6 deletions certificates/install_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,77 @@ const char *uninstallCert() {
}
return "";
}
const char *getExpirationDate(char *expirationDate){
// Create a key-value dictionary used to query the Keychain and look for the "Arduino" root certificate.
NSDictionary *getquery = @{
(id)kSecClass: (id)kSecClassCertificate,
(id)kSecAttrLabel: @"Arduino",
(id)kSecReturnRef: @YES,
};
OSStatus err = noErr;
SecCertificateRef cert = NULL;
// Use this function to check for errors
err = SecItemCopyMatching((CFDictionaryRef)getquery, (CFTypeRef *)&cert);
if (err != noErr){
NSString *errString = [@"Error: " stringByAppendingFormat:@"%d", err];
NSLog(@"%@", errString);
return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];
}
// Get data from the certificate. We just need the "invalidity date" property.
CFDictionaryRef valuesDict = SecCertificateCopyValues(cert, (__bridge CFArrayRef)@[(__bridge id)kSecOIDInvalidityDate], NULL);
id expirationDateValue;
if(valuesDict){
CFDictionaryRef invalidityDateDictionaryRef = CFDictionaryGetValue(valuesDict, kSecOIDInvalidityDate);
if(invalidityDateDictionaryRef){
CFTypeRef invalidityRef = CFDictionaryGetValue(invalidityDateDictionaryRef, kSecPropertyKeyValue);
if(invalidityRef){
expirationDateValue = CFBridgingRelease(invalidityRef);
}
}
CFRelease(valuesDict);
}
NSString *outputString = [@"" stringByAppendingFormat:@"%@", expirationDateValue];
if([outputString isEqualToString:@""]){
NSString *errString = @"Error: the expiration date of the certificate could not be found";
NSLog(@"%@", errString);
return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];
}
// This workaround allows to obtain the expiration date alongside the error message
strncpy(expirationDate, [outputString cStringUsingEncoding:[NSString defaultCStringEncoding]], 32);
expirationDate[32-1] = 0;
return "";
}
const char *getDefaultBrowserName() {
NSURL *defaultBrowserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:[NSURL URLWithString:@"http://"]];
if (defaultBrowserURL) {
NSBundle *defaultBrowserBundle = [NSBundle bundleWithURL:defaultBrowserURL];
NSString *defaultBrowser = [defaultBrowserBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
return [defaultBrowser cStringUsingEncoding:[NSString defaultCStringEncoding]];
}
return "";
}
*/
import "C"
import (
"errors"
"os/exec"
"strings"
"unsafe"

log "github.com/sirupsen/logrus"

"github.com/arduino/arduino-create-agent/utilities"
"github.com/arduino/go-paths-helper"
)

Expand All @@ -110,9 +172,8 @@ func InstallCertificate(cert *paths.Path) error {
p := C.installCert(ccert)
s := C.GoString(p)
if len(s) != 0 {
oscmd := exec.Command("osascript", "-e", "display dialog \""+s+"\" buttons \"OK\" with title \"Arduino Agent: Error installing certificates\"")
_ = oscmd.Run()
_ = UninstallCertificates()
utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error installing certificates\"")
UninstallCertificates()
return errors.New(s)
}
return nil
Expand All @@ -125,9 +186,30 @@ func UninstallCertificates() error {
p := C.uninstallCert()
s := C.GoString(p)
if len(s) != 0 {
oscmd := exec.Command("osascript", "-e", "display dialog \""+s+"\" buttons \"OK\" with title \"Arduino Agent: Error uninstalling certificates\"")
_ = oscmd.Run()
utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error uninstalling certificates\"")
return errors.New(s)
}
return nil
}

// GetExpirationDate returns the expiration date of a certificate stored in the keychain
func GetExpirationDate() (string, error) {
log.Infof("Retrieving certificate's expiration date")
dateString := C.CString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") // 32 characters string
defer C.free(unsafe.Pointer(dateString))
p := C.getExpirationDate(dateString)
s := C.GoString(p)
if len(s) != 0 {
utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error retrieving expiration date\"")
return "", errors.New(s)
}
date := C.GoString(dateString)
return strings.ReplaceAll(date, " +0000", ""), nil
}

// GetDefaultBrowserName returns the name of the default browser
func GetDefaultBrowserName() string {
log.Infof("Retrieving default browser name")
p := C.getDefaultBrowserName()
return C.GoString(p)
}
12 changes: 12 additions & 0 deletions certificates/install_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ func UninstallCertificates() error {
log.Warn("platform not supported for the certificates uninstall")
return errors.New("platform not supported for the certificates uninstall")
}

// GetExpirationDate won't do anything on unsupported Operative Systems
func GetExpirationDate() (string, error) {
log.Warn("platform not supported for retrieving certificates expiration date")
return "", errors.New("platform not supported for retrieving certificates expiration date")
}

// GetDefaultBrowserName won't do anything on unsupported Operative Systems
func GetDefaultBrowserName() string {
log.Warn("platform not supported for retrieving default browser name")
return ""
}
18 changes: 18 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"os"

"github.com/arduino/go-paths-helper"
"github.com/go-ini/ini"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -124,3 +125,20 @@ func GenerateConfig(destDir *paths.Path) *paths.Path {
log.Infof("generated config in %s", configPath)
return configPath
}

// SetInstallCertsIni sets installCerts value to true in the config
func SetInstallCertsIni(filename string, value string) error {
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: false, AllowPythonMultilineValues: true}, filename)
if err != nil {
return err
}
_, err = cfg.Section("").NewKey("installCerts", value)
if err != nil {
return err
}
err = cfg.SaveTo(filename)
if err != nil {
return err
}
return nil
}
63 changes: 55 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"html/template"
"io"
"os"
"os/exec"
"regexp"
"runtime"
"runtime/debug"
Expand All @@ -40,6 +39,7 @@ import (
"github.com/arduino/arduino-create-agent/systray"
"github.com/arduino/arduino-create-agent/tools"
"github.com/arduino/arduino-create-agent/updater"
"github.com/arduino/arduino-create-agent/utilities"
v2 "github.com/arduino/arduino-create-agent/v2"
paths "github.com/arduino/go-paths-helper"
cors "github.com/gin-contrib/cors"
Expand Down Expand Up @@ -86,6 +86,7 @@ var (
verbose = iniConf.Bool("v", true, "show debug logging")
crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
autostartMacOS = iniConf.Bool("autostartMacOS", true, "the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)")
installCerts = iniConf.Bool("installCerts", false, "install the HTTPS certificate for Safari and keep it updated")
)

// the ports filter provided by the user via the -regex flag, if any
Expand Down Expand Up @@ -177,7 +178,7 @@ func loop() {
// If we are updating manually from 1.2.7 to 1.3.0 we have to uninstall the old agent manually first.
// This check will inform the user if he needs to run the uninstall first
if runtime.GOOS == "darwin" && oldInstallExists() {
printDialog("Old agent installation of the Arduino Create Agent found, please uninstall it before launching the new one")
utilities.UserPrompt("display dialog \"Old agent installation of the Arduino Create Agent found, please uninstall it before launching the new one\" buttons \"OK\" with title \"Error\"")
os.Exit(0)
}

Expand Down Expand Up @@ -220,6 +221,32 @@ func loop() {
configPath = config.GenerateConfig(configDir)
}

// if the default browser is Safari, prompt the user to install HTTPS certificates
// and eventually install them
if runtime.GOOS == "darwin" && cert.GetDefaultBrowserName() == "Safari" {
if exist, err := installCertsKeyExists(configPath.String()); err != nil {
log.Panicf("config.ini cannot be parsed: %s", err)
} else if !exist {
if config.CertsExist() {
err = config.SetInstallCertsIni(configPath.String(), "true")
if err != nil {
log.Panicf("config.ini cannot be parsed: %s", err)
}
} else if cert.PromptInstallCertsSafari() {
err = config.SetInstallCertsIni(configPath.String(), "true")
if err != nil {
log.Panicf("config.ini cannot be parsed: %s", err)
}
cert.GenerateAndInstallCertificates(config.GetCertificatesDir())
} else {
err = config.SetInstallCertsIni(configPath.String(), "false")
if err != nil {
log.Panicf("config.ini cannot be parsed: %s", err)
}
}
}
}

// Parse the config.ini
args, err := parseIni(configPath.String())
if err != nil {
Expand Down Expand Up @@ -342,6 +369,24 @@ func loop() {
}
}

// check if the HTTPS certificates are expired and prompt the user to update them on macOS
if runtime.GOOS == "darwin" && cert.GetDefaultBrowserName() == "Safari" {
if *installCerts {
if config.CertsExist() {
cert.PromptExpiredCerts(config.GetCertificatesDir())
} else if cert.PromptInstallCertsSafari() {
// installing the certificates from scratch at this point should only happen if
// something went wrong during previous installation attempts
cert.GenerateAndInstallCertificates(config.GetCertificatesDir())
} else {
err = config.SetInstallCertsIni(configPath.String(), "false")
if err != nil {
log.Panicf("config.ini cannot be parsed: %s", err)
}
}
}
}

// launch the discoveries for the running system
go serialPorts.Run()
// launch the hub routine which is the singleton for the websocket server
Expand Down Expand Up @@ -457,12 +502,6 @@ func oldInstallExists() bool {
return oldAgentPath.Join("ArduinoCreateAgent.app").Exist()
}

// printDialog will print a GUI error dialog on macos
func printDialog(dialogText string) {
oscmd := exec.Command("osascript", "-e", "display dialog \""+dialogText+"\" buttons \"OK\" with title \"Error\"")
_ = oscmd.Run()
}

func parseIni(filename string) (args []string, err error) {
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: false, AllowPythonMultilineValues: true}, filename)
if err != nil {
Expand All @@ -487,3 +526,11 @@ func parseIni(filename string) (args []string, err error) {

return args, nil
}

func installCertsKeyExists(filename string) (bool, error) {
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: false, AllowPythonMultilineValues: true}, filename)
if err != nil {
return false, err
}
return cfg.Section("").HasKey("installCerts"), nil
}
Loading

0 comments on commit 222a505

Please sign in to comment.