From 3190a1a1326bd841454d5fbb7efd7adc2f6d697e Mon Sep 17 00:00:00 2001 From: MatteoPologruto <109663225+MatteoPologruto@users.noreply.github.com> Date: Mon, 13 May 2024 15:51:29 +0200 Subject: [PATCH] Fix buttons and improve handling of certificates when Safari is not the default browser (#949) * Fix check on buttons returning the correct message * Update certificates regardless of the default browser * Set installCerts when the certificate is installed from previous versions of the Agent regardless of the default browser * Do not set installCerts to false if the default browser is not Safari * Do not ask again to update the certificate if the user refuses once * Fix user script on macOS * Check for the presence of the certificate in the keychain to determine if it is installed * Fix getExpirationDate breaking when the certificate is expired * Fix return value in case of error * getExpirationDate rewritten to use the correct expiration field. * Separate osascript default button from the one to press * Fix leftover buttons * Small text fixes in the "manage certificate" dialog * Simplify error management in getExpirationDate * Fix compiler warnings and move obj-c code into a separate file. * certInKeychain returns a bool * Fix building errors caused by objective-c files on Ubuntu and Windows * Build objective-c files only on Darwin * Remove -ld_classic library because XCode is not up to date on the CI --------- Co-authored-by: Xayton <30591904+Xayton@users.noreply.github.com> --- certificates/certificates.go | 34 +----- certificates/certificates_darwin.h | 7 ++ certificates/certificates_darwin.m | 137 +++++++++++++++++++++++ certificates/install_darwin.go | 172 +++++------------------------ certificates/install_default.go | 11 +- main.go | 75 ++++++++----- systray/systray_real.go | 50 +++++---- utilities/utilities.go | 6 +- 8 files changed, 264 insertions(+), 228 deletions(-) create mode 100644 certificates/certificates_darwin.h create mode 100644 certificates/certificates_darwin.m diff --git a/certificates/certificates.go b/certificates/certificates.go index baac7c33..8a8c50d8 100644 --- a/certificates/certificates.go +++ b/certificates/certificates.go @@ -30,16 +30,13 @@ 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" ) var ( - host = "localhost" validFrom = "" validFor = 365 * 24 * time.Hour * 2 // 2 years rsaBits = 2048 @@ -270,41 +267,16 @@ func DeleteCertificates(certDir *paths.Path) { certDir.Join("cert.cer").Remove() } -// isExpired checks if a certificate is expired or about to expire (less than 1 month) -func isExpired() (bool, error) { +// 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() + date, 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) diff --git a/certificates/certificates_darwin.h b/certificates/certificates_darwin.h new file mode 100644 index 00000000..78ba0ae5 --- /dev/null +++ b/certificates/certificates_darwin.h @@ -0,0 +1,7 @@ +const char *getDefaultBrowserName(); + +const char *installCert(const char *path); +const char *uninstallCert(); +const bool certInKeychain(); + +const char *getExpirationDate(long *expirationDate); \ No newline at end of file diff --git a/certificates/certificates_darwin.m b/certificates/certificates_darwin.m new file mode 100644 index 00000000..0ac51183 --- /dev/null +++ b/certificates/certificates_darwin.m @@ -0,0 +1,137 @@ +#import +#import +#include "certificates_darwin.h" + +// Used to return error strings (as NSString) as a C-string to the Go code. +const char *toErrorString(NSString *errString) { + NSLog(@"%@", errString); + return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; +} + +// Returns a string describing the name of the default browser set for the user, nil in case of error. +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 ""; +} + +// inspired by https://stackoverflow.com/questions/12798950/ios-install-ssl-certificate-programmatically +const char *installCert(const char *path) { + NSURL *url = [NSURL fileURLWithPath:@(path) isDirectory:NO]; + NSData *rootCertData = [NSData dataWithContentsOfURL:url]; + + OSStatus err = noErr; + SecCertificateRef rootCert = SecCertificateCreateWithData(kCFAllocatorDefault, (CFDataRef) rootCertData); + + CFTypeRef result; + + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: + (id)kSecClassCertificate, kSecClass, + rootCert, kSecValueRef, + nil]; + + err = SecItemAdd((CFDictionaryRef)dict, &result); + + if (err == noErr) { + NSLog(@"Install root certificate success"); + } else if (err == errSecDuplicateItem) { + NSString *errString = [@"duplicate root certificate entry. Error: " stringByAppendingFormat:@"%d", err]; + NSLog(@"%@", errString); + return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; + } else { + NSString *errString = [@"install root certificate failure. Error: " stringByAppendingFormat:@"%d", err]; + NSLog(@"%@", errString); + return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; + } + + NSDictionary *newTrustSettings = @{(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot]}; + err = SecTrustSettingsSetTrustSettings(rootCert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(newTrustSettings)); + if (err != errSecSuccess) { + NSString *errString = [@"Could not change the trust setting for a certificate. Error: " stringByAppendingFormat:@"%d", err]; + NSLog(@"%@", errString); + return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; + } + + return ""; +} + +const char *uninstallCert() { + // Each line is a key-value of the dictionary. Note: the the inverted order, value first then key. + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: + (id)kSecClassCertificate, kSecClass, + CFSTR("Arduino"), kSecAttrLabel, + kSecMatchLimitOne, kSecMatchLimit, + kCFBooleanTrue, kSecReturnAttributes, + nil]; + + OSStatus err = noErr; + // Use this function to check for errors + err = SecItemCopyMatching((CFDictionaryRef)dict, nil); + if (err == noErr) { + err = SecItemDelete((CFDictionaryRef)dict); + if (err != noErr) { + NSString *errString = [@"Could not delete the certificates. Error: " stringByAppendingFormat:@"%d", err]; + NSLog(@"%@", errString); + return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; + } + } else if (err != errSecItemNotFound){ + NSString *errString = [@"Error: " stringByAppendingFormat:@"%d", err]; + NSLog(@"%@", errString); + return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; + } + return ""; +} + +const bool certInKeychain() { + // 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 = SecItemCopyMatching((CFDictionaryRef)getquery, nil); + return (err == noErr); // No error means the certificate was found, otherwise err will be "errSecItemNotFound". +} + +// Returns the expiration date "kSecOIDX509V1ValidityNotAfter" of the Arduino certificate. +// The value is returned as a CFAbsoluteTime: a long number of seconds from the date of 1 Jan 2001 00:00:00 GMT. +const char *getExpirationDate(long *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, + }; + + SecCertificateRef cert = NULL; + + // Search the keychain for certificates matching the query above. + OSStatus err = SecItemCopyMatching((CFDictionaryRef)getquery, (CFTypeRef *)&cert); + if (err != noErr) return toErrorString([@"Error getting the certificate: " stringByAppendingFormat:@"%d", err]); + + // Get data from the certificate, as a dictionary of properties. We just need the "invalidity not after" property. + CFDictionaryRef certDict = SecCertificateCopyValues(cert, + (__bridge CFArrayRef)@[(__bridge id)kSecOIDX509V1ValidityNotAfter], NULL); + if (certDict == NULL) return toErrorString(@"SecCertificateCopyValues failed"); + + + // Get the "validity not after" property as a dictionary, and get the "value" key (that is a number). + CFDictionaryRef validityNotAfterDict = CFDictionaryGetValue(certDict, kSecOIDX509V1ValidityNotAfter); + if (validityNotAfterDict == NULL) return toErrorString(@"CFDictionaryGetValue (validity) failed"); + + CFNumberRef number = (CFNumberRef)CFDictionaryGetValue(validityNotAfterDict, kSecPropertyKeyValue); + if (number == NULL) return toErrorString(@"CFDictionaryGetValue (keyValue) failed"); + + CFNumberGetValue(number, kCFNumberSInt64Type, expirationDate); + // NSLog(@"Certificate validity not after: %ld", *expirationDate); + + CFRelease(certDict); + return ""; // No error. +} \ No newline at end of file diff --git a/certificates/install_darwin.go b/certificates/install_darwin.go index 892c390b..515c9f7d 100644 --- a/certificates/install_darwin.go +++ b/certificates/install_darwin.go @@ -15,146 +15,20 @@ package certificates -//inspired by https://stackoverflow.com/questions/12798950/ios-install-ssl-certificate-programmatically - /* // Explicitly tell the GCC compiler that the language is Objective-C. #cgo CFLAGS: -x objective-c -// Pass the list of macOS frameworks needed by this piece of Objective-C code. -#cgo LDFLAGS: -framework Cocoa -#import - -const char *installCert(const char *path) { - NSURL *url = [NSURL fileURLWithPath:@(path) isDirectory:NO]; - NSData *rootCertData = [NSData dataWithContentsOfURL:url]; - - OSStatus err = noErr; - SecCertificateRef rootCert = SecCertificateCreateWithData(kCFAllocatorDefault, (CFDataRef) rootCertData); - - CFTypeRef result; - - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: - (id)kSecClassCertificate, kSecClass, - rootCert, kSecValueRef, - nil]; - - err = SecItemAdd((CFDictionaryRef)dict, &result); - - if (err == noErr) { - NSLog(@"Install root certificate success"); - } else if (err == errSecDuplicateItem) { - NSString *errString = [@"duplicate root certificate entry. Error: " stringByAppendingFormat:@"%d", err]; - NSLog(@"%@", errString); - return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];; - } else { - NSString *errString = [@"install root certificate failure. Error: " stringByAppendingFormat:@"%d", err]; - NSLog(@"%@", errString); - return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; - } - - NSDictionary *newTrustSettings = @{(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot]}; - err = SecTrustSettingsSetTrustSettings(rootCert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(newTrustSettings)); - if (err != errSecSuccess) { - NSString *errString = [@"Could not change the trust setting for a certificate. Error: " stringByAppendingFormat:@"%d", err]; - NSLog(@"%@", errString); - return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]]; - } - - return ""; -} - -const char *uninstallCert() { - // Each line is a key-value of the dictionary. Note: the the inverted order, value first then key. - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: - (id)kSecClassCertificate, kSecClass, - CFSTR("Arduino"), kSecAttrLabel, - kSecMatchLimitOne, kSecMatchLimit, - kCFBooleanTrue, kSecReturnAttributes, - nil]; - - OSStatus err = noErr; - // Use this function to check for errors - err = SecItemCopyMatching((CFDictionaryRef)dict, nil); - if (err == noErr) { - err = SecItemDelete((CFDictionaryRef)dict); - if (err != noErr) { - NSString *errString = [@"Could not delete the certificates. Error: " stringByAppendingFormat:@"%d", err]; - NSLog(@"%@", errString); - return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];; - } - } else if (err != errSecItemNotFound){ - NSString *errString = [@"Error: " stringByAppendingFormat:@"%d", err]; - NSLog(@"%@", errString); - return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];; - } - 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]]; - } +// Pass the list of macOS frameworks needed by this piece of Objective-C code. +#cgo LDFLAGS: -framework Foundation -framework Security -framework AppKit - return ""; -} +#import +#include "certificates_darwin.h" */ import "C" import ( "errors" - "strings" + "time" "unsafe" log "github.com/sirupsen/logrus" @@ -172,7 +46,7 @@ func InstallCertificate(cert *paths.Path) error { p := C.installCert(ccert) s := C.GoString(p) if len(s) != 0 { - utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error installing certificates\"") + utilities.UserPrompt(s, "\"OK\"", "OK", "OK", "Arduino Agent: Error installing certificates") UninstallCertificates() return errors.New(s) } @@ -186,25 +60,29 @@ func UninstallCertificates() error { p := C.uninstallCert() s := C.GoString(p) if len(s) != 0 { - utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error uninstalling certificates\"") + utilities.UserPrompt(s, "\"OK\"", "OK", "OK", "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) { +func GetExpirationDate() (time.Time, 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) + + expirationDateLong := C.long(0) + + err := C.getExpirationDate(&expirationDateLong) + errString := C.GoString(err) + if len(errString) > 0 { + utilities.UserPrompt(errString, "\"OK\"", "OK", "OK", "Arduino Agent: Error retrieving expiration date") + return time.Time{}, errors.New(errString) } - date := C.GoString(dateString) - return strings.ReplaceAll(date, " +0000", ""), nil + + // The expirationDate is the number of seconds from the date of 1 Jan 2001 00:00:00 GMT. + // Add 31 years to convert it to Unix Epoch. + expirationDate := int64(expirationDateLong) + return time.Unix(expirationDate, 0).AddDate(31, 0, 0), nil } // GetDefaultBrowserName returns the name of the default browser @@ -213,3 +91,11 @@ func GetDefaultBrowserName() string { p := C.getDefaultBrowserName() return C.GoString(p) } + +// CertInKeychain checks if the certificate is stored inside the keychain +func CertInKeychain() bool { + log.Infof("Checking if the Arduino certificate is in the keychain") + + certInKeychain := C.certInKeychain() + return bool(certInKeychain) +} diff --git a/certificates/install_default.go b/certificates/install_default.go index 8013c018..533574d6 100644 --- a/certificates/install_default.go +++ b/certificates/install_default.go @@ -19,6 +19,7 @@ package certificates import ( "errors" + "time" log "github.com/sirupsen/logrus" @@ -38,9 +39,9 @@ func UninstallCertificates() error { } // GetExpirationDate won't do anything on unsupported Operative Systems -func GetExpirationDate() (string, error) { +func GetExpirationDate() (time.Time, error) { log.Warn("platform not supported for retrieving certificates expiration date") - return "", errors.New("platform not supported for retrieving certificates expiration date") + return time.Time{}, errors.New("platform not supported for retrieving certificates expiration date") } // GetDefaultBrowserName won't do anything on unsupported Operative Systems @@ -48,3 +49,9 @@ func GetDefaultBrowserName() string { log.Warn("platform not supported for retrieving default browser name") return "" } + +// CertInKeychain won't do anything on unsupported Operative Systems +func CertInKeychain() bool { + log.Warn("platform not supported for verifying the certificate existence") + return false +} diff --git a/main.go b/main.go index 0231548d..1ca857b0 100755 --- a/main.go +++ b/main.go @@ -178,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() { - 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\"") + utilities.UserPrompt("Old agent installation of the Arduino Create Agent found, please uninstall it before launching the new one", "\"OK\"", "OK", "OK", "Error") os.Exit(0) } @@ -223,25 +223,27 @@ func loop() { // 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 runtime.GOOS == "darwin" { 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() { + if cert.CertInKeychain() || config.CertsExist() { 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) + } else if cert.GetDefaultBrowserName() == "Safari" { + if 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) + } } } } @@ -369,21 +371,38 @@ 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) + // check if the HTTPS certificates are expired or expiring and prompt the user to update them on macOS + if runtime.GOOS == "darwin" && *installCerts { + if cert.CertInKeychain() || config.CertsExist() { + certDir := config.GetCertificatesDir() + if expired, err := cert.IsExpired(); err != nil { + log.Errorf("cannot check if certificates are expired something went wrong: %s", err) + } else if expired { + buttonPressed := utilities.UserPrompt("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?", "{\"Do not update\", \"Update the certificate for Safari\"}", "Update the certificate for Safari", "Update the certificate for Safari", "Arduino Agent: Update certificate") + if buttonPressed { + err := cert.UninstallCertificates() + if err != nil { + log.Errorf("cannot uninstall certificates something went wrong: %s", err) + } else { + cert.DeleteCertificates(certDir) + cert.GenerateAndInstallCertificates(certDir) + } + } else { + err = config.SetInstallCertsIni(configPath.String(), "false") + if err != nil { + log.Panicf("config.ini cannot be parsed: %s", err) + } } } + } else if 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) + } } } @@ -534,3 +553,7 @@ func installCertsKeyExists(filename string) (bool, error) { } return cfg.Section("").HasKey("installCerts"), nil } + +func promptInstallCertsSafari() bool { + return utilities.UserPrompt("The Arduino Agent needs a local HTTPS certificate to work correctly with Safari.\nIf you use Safari, you need to install it.", "{\"Do not install\", \"Install the certificate for Safari\"}", "Install the certificate for Safari", "Install the certificate for Safari", "Arduino Agent: Install certificate") +} diff --git a/systray/systray_real.go b/systray/systray_real.go index 503373d8..5ae79f08 100644 --- a/systray/systray_real.go +++ b/systray/systray_real.go @@ -22,7 +22,7 @@ package systray import ( "os" "runtime" - "strings" + "time" "fyne.io/systray" cert "github.com/arduino/arduino-create-agent/certificates" @@ -95,38 +95,42 @@ func (s *Systray) start() { s.updateMenuItem(mRmCrashes, config.LogsIsEmpty()) case <-mManageCerts.ClickedCh: infoMsg := "The Arduino Agent needs a local HTTPS certificate to work correctly with Safari.\n\nYour HTTPS certificate status:\n" - buttons := "{\"Install the certificate for Safari\", \"OK\"} default button \"OK\"" + buttons := "{\"Install the certificate for Safari\", \"OK\"}" + toPress := "Install the certificate for Safari" certDir := config.GetCertificatesDir() - if config.CertsExist() { + if cert.CertInKeychain() || config.CertsExist() { expDate, err := cert.GetExpirationDate() if err != nil { log.Errorf("cannot get certificates expiration date, something went wrong: %s", err) } - infoMsg = infoMsg + "- Certificate installed: Yes\n- Certificate trusted: Yes\n- Certificate expiration date: " + expDate - buttons = "{\"Uninstall the certificate for Safari\", \"OK\"} default button \"OK\"" - } else { - infoMsg = infoMsg + "- Certificate installed: No\n- Certificate trusted: N/A\n- Certificate expiration date: N/A" - } - pressedButton := utilities.UserPrompt("display dialog \"" + infoMsg + "\" buttons " + buttons + " with title \"Arduino Agent: Manage HTTPS certificate\"") - if strings.Contains(pressedButton, "Install certificate for Safari") { - cert.GenerateAndInstallCertificates(certDir) - err := config.SetInstallCertsIni(s.currentConfigFilePath.String(), "true") - if err != nil { - log.Errorf("cannot set installCerts value in config.ini: %s", err) + infoMsg = infoMsg + "- Certificate installed:\t\tYes\n- Certificate trusted:\t\tYes\n- Certificate expiration:\t" + expDate.Format(time.DateTime) + buttons = "{\"Uninstall the certificate for Safari\", \"OK\"}" + toPress = "Uninstall the certificate for Safari" + pressedButton := utilities.UserPrompt(infoMsg, buttons, "OK", toPress, "Arduino Agent: Manage HTTPS certificate") + if pressedButton { + err := cert.UninstallCertificates() + if err != nil { + log.Errorf("cannot uninstall certificates something went wrong: %s", err) + } else { + cert.DeleteCertificates(certDir) + err = config.SetInstallCertsIni(s.currentConfigFilePath.String(), "false") + if err != nil { + log.Errorf("cannot set installCerts value in config.ini: %s", err) + } + } + s.Restart() } - s.Restart() - } else if strings.Contains(pressedButton, "Uninstall certificate for Safari") { - err := cert.UninstallCertificates() - if err != nil { - log.Errorf("cannot uninstall certificates something went wrong: %s", err) - } else { - cert.DeleteCertificates(certDir) - err = config.SetInstallCertsIni(s.currentConfigFilePath.String(), "false") + } else { + infoMsg = infoMsg + "- Certificate installed:\t\tNo\n- Certificate trusted:\t\tN/A\n- Certificate expiration:\tN/A" + pressedButton := utilities.UserPrompt(infoMsg, buttons, "OK", toPress, "Arduino Agent: Manage HTTPS certificate") + if pressedButton { + cert.GenerateAndInstallCertificates(certDir) + err := config.SetInstallCertsIni(s.currentConfigFilePath.String(), "true") if err != nil { log.Errorf("cannot set installCerts value in config.ini: %s", err) } + s.Restart() } - s.Restart() } case <-mPause.ClickedCh: s.Pause() diff --git a/utilities/utilities.go b/utilities/utilities.go index 63f09103..5979732d 100644 --- a/utilities/utilities.go +++ b/utilities/utilities.go @@ -151,8 +151,8 @@ func VerifyInput(input string, signature string) error { } // UserPrompt executes an osascript and returns the pressed button -func UserPrompt(dialog string) string { - oscmd := exec.Command("osascript", "-e", dialog) +func UserPrompt(dialog string, buttons string, defaultButton string, toPress string, title string) bool { + oscmd := exec.Command("osascript", "-e", "display dialog \""+dialog+"\" buttons "+buttons+" default button\""+defaultButton+"\" with title \""+title+"\"") pressedButton, _ := oscmd.Output() - return string(pressedButton) + return strings.Contains(string(pressedButton), "button returned:"+toPress) }