Skip to content

Commit 222a505

Browse files
Manage the HTTPS certificate from the menu and ask Safari users to install 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 <[email protected]>
1 parent bf5b8e4 commit 222a505

File tree

10 files changed

+285
-37
lines changed

10 files changed

+285
-37
lines changed

Diff for: certificates/certificates.go

+48
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import (
3030
"math/big"
3131
"net"
3232
"os"
33+
"strings"
3334
"time"
3435

36+
"github.com/arduino/arduino-create-agent/utilities"
3537
"github.com/arduino/go-paths-helper"
3638
log "github.com/sirupsen/logrus"
3739
)
@@ -267,3 +269,49 @@ func DeleteCertificates(certDir *paths.Path) {
267269
certDir.Join("cert.pem").Remove()
268270
certDir.Join("cert.cer").Remove()
269271
}
272+
273+
// isExpired checks if a certificate is expired or about to expire (less than 1 month)
274+
func isExpired() (bool, error) {
275+
bound := time.Now().AddDate(0, 1, 0)
276+
dateS, err := GetExpirationDate()
277+
if err != nil {
278+
return false, err
279+
}
280+
date, _ := time.Parse(time.DateTime, dateS)
281+
return date.Before(bound), nil
282+
}
283+
284+
// PromptInstallCertsSafari prompts the user to install the HTTPS certificates if they are using Safari
285+
func PromptInstallCertsSafari() bool {
286+
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\"")
287+
return strings.Contains(string(buttonPressed), "button returned:Install the certificate for Safari")
288+
}
289+
290+
// PromptExpiredCerts prompts the user to update the HTTPS certificates if they are using Safari
291+
func PromptExpiredCerts(certDir *paths.Path) {
292+
if expired, err := isExpired(); err != nil {
293+
log.Errorf("cannot check if certificates are expired something went wrong: %s", err)
294+
} else if expired {
295+
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\"")
296+
if strings.Contains(string(buttonPressed), "button returned:Update the certificate for Safari") {
297+
err := UninstallCertificates()
298+
if err != nil {
299+
log.Errorf("cannot uninstall certificates something went wrong: %s", err)
300+
} else {
301+
DeleteCertificates(certDir)
302+
GenerateAndInstallCertificates(certDir)
303+
}
304+
}
305+
}
306+
}
307+
308+
// GenerateAndInstallCertificates generates and installs the certificates
309+
func GenerateAndInstallCertificates(certDir *paths.Path) {
310+
GenerateCertificates(certDir)
311+
err := InstallCertificate(certDir.Join("ca.cert.cer"))
312+
// if something goes wrong during the cert install we remove them, so the user is able to retry
313+
if err != nil {
314+
log.Errorf("cannot install certificates something went wrong: %s", err)
315+
DeleteCertificates(certDir)
316+
}
317+
}

Diff for: certificates/install_darwin.go

+88-6
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,77 @@ const char *uninstallCert() {
8989
}
9090
return "";
9191
}
92+
93+
const char *getExpirationDate(char *expirationDate){
94+
// Create a key-value dictionary used to query the Keychain and look for the "Arduino" root certificate.
95+
NSDictionary *getquery = @{
96+
(id)kSecClass: (id)kSecClassCertificate,
97+
(id)kSecAttrLabel: @"Arduino",
98+
(id)kSecReturnRef: @YES,
99+
};
100+
101+
OSStatus err = noErr;
102+
SecCertificateRef cert = NULL;
103+
104+
// Use this function to check for errors
105+
err = SecItemCopyMatching((CFDictionaryRef)getquery, (CFTypeRef *)&cert);
106+
107+
if (err != noErr){
108+
NSString *errString = [@"Error: " stringByAppendingFormat:@"%d", err];
109+
NSLog(@"%@", errString);
110+
return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];
111+
}
112+
113+
// Get data from the certificate. We just need the "invalidity date" property.
114+
CFDictionaryRef valuesDict = SecCertificateCopyValues(cert, (__bridge CFArrayRef)@[(__bridge id)kSecOIDInvalidityDate], NULL);
115+
116+
id expirationDateValue;
117+
if(valuesDict){
118+
CFDictionaryRef invalidityDateDictionaryRef = CFDictionaryGetValue(valuesDict, kSecOIDInvalidityDate);
119+
if(invalidityDateDictionaryRef){
120+
CFTypeRef invalidityRef = CFDictionaryGetValue(invalidityDateDictionaryRef, kSecPropertyKeyValue);
121+
if(invalidityRef){
122+
expirationDateValue = CFBridgingRelease(invalidityRef);
123+
}
124+
}
125+
CFRelease(valuesDict);
126+
}
127+
128+
NSString *outputString = [@"" stringByAppendingFormat:@"%@", expirationDateValue];
129+
if([outputString isEqualToString:@""]){
130+
NSString *errString = @"Error: the expiration date of the certificate could not be found";
131+
NSLog(@"%@", errString);
132+
return [errString cStringUsingEncoding:[NSString defaultCStringEncoding]];
133+
}
134+
135+
// This workaround allows to obtain the expiration date alongside the error message
136+
strncpy(expirationDate, [outputString cStringUsingEncoding:[NSString defaultCStringEncoding]], 32);
137+
expirationDate[32-1] = 0;
138+
139+
return "";
140+
}
141+
142+
const char *getDefaultBrowserName() {
143+
NSURL *defaultBrowserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:[NSURL URLWithString:@"http://"]];
144+
if (defaultBrowserURL) {
145+
NSBundle *defaultBrowserBundle = [NSBundle bundleWithURL:defaultBrowserURL];
146+
NSString *defaultBrowser = [defaultBrowserBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
147+
148+
return [defaultBrowser cStringUsingEncoding:[NSString defaultCStringEncoding]];
149+
}
150+
151+
return "";
152+
}
92153
*/
93154
import "C"
94155
import (
95156
"errors"
96-
"os/exec"
157+
"strings"
97158
"unsafe"
98159

99160
log "github.com/sirupsen/logrus"
100161

162+
"github.com/arduino/arduino-create-agent/utilities"
101163
"github.com/arduino/go-paths-helper"
102164
)
103165

@@ -110,9 +172,8 @@ func InstallCertificate(cert *paths.Path) error {
110172
p := C.installCert(ccert)
111173
s := C.GoString(p)
112174
if len(s) != 0 {
113-
oscmd := exec.Command("osascript", "-e", "display dialog \""+s+"\" buttons \"OK\" with title \"Arduino Agent: Error installing certificates\"")
114-
_ = oscmd.Run()
115-
_ = UninstallCertificates()
175+
utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error installing certificates\"")
176+
UninstallCertificates()
116177
return errors.New(s)
117178
}
118179
return nil
@@ -125,9 +186,30 @@ func UninstallCertificates() error {
125186
p := C.uninstallCert()
126187
s := C.GoString(p)
127188
if len(s) != 0 {
128-
oscmd := exec.Command("osascript", "-e", "display dialog \""+s+"\" buttons \"OK\" with title \"Arduino Agent: Error uninstalling certificates\"")
129-
_ = oscmd.Run()
189+
utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error uninstalling certificates\"")
130190
return errors.New(s)
131191
}
132192
return nil
133193
}
194+
195+
// GetExpirationDate returns the expiration date of a certificate stored in the keychain
196+
func GetExpirationDate() (string, error) {
197+
log.Infof("Retrieving certificate's expiration date")
198+
dateString := C.CString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") // 32 characters string
199+
defer C.free(unsafe.Pointer(dateString))
200+
p := C.getExpirationDate(dateString)
201+
s := C.GoString(p)
202+
if len(s) != 0 {
203+
utilities.UserPrompt("display dialog \"" + s + "\" buttons \"OK\" with title \"Arduino Agent: Error retrieving expiration date\"")
204+
return "", errors.New(s)
205+
}
206+
date := C.GoString(dateString)
207+
return strings.ReplaceAll(date, " +0000", ""), nil
208+
}
209+
210+
// GetDefaultBrowserName returns the name of the default browser
211+
func GetDefaultBrowserName() string {
212+
log.Infof("Retrieving default browser name")
213+
p := C.getDefaultBrowserName()
214+
return C.GoString(p)
215+
}

Diff for: certificates/install_default.go

+12
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,15 @@ func UninstallCertificates() error {
3636
log.Warn("platform not supported for the certificates uninstall")
3737
return errors.New("platform not supported for the certificates uninstall")
3838
}
39+
40+
// GetExpirationDate won't do anything on unsupported Operative Systems
41+
func GetExpirationDate() (string, error) {
42+
log.Warn("platform not supported for retrieving certificates expiration date")
43+
return "", errors.New("platform not supported for retrieving certificates expiration date")
44+
}
45+
46+
// GetDefaultBrowserName won't do anything on unsupported Operative Systems
47+
func GetDefaultBrowserName() string {
48+
log.Warn("platform not supported for retrieving default browser name")
49+
return ""
50+
}

Diff for: config/config.go

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222

2323
"github.com/arduino/go-paths-helper"
24+
"github.com/go-ini/ini"
2425
log "github.com/sirupsen/logrus"
2526
)
2627

@@ -124,3 +125,20 @@ func GenerateConfig(destDir *paths.Path) *paths.Path {
124125
log.Infof("generated config in %s", configPath)
125126
return configPath
126127
}
128+
129+
// SetInstallCertsIni sets installCerts value to true in the config
130+
func SetInstallCertsIni(filename string, value string) error {
131+
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: false, AllowPythonMultilineValues: true}, filename)
132+
if err != nil {
133+
return err
134+
}
135+
_, err = cfg.Section("").NewKey("installCerts", value)
136+
if err != nil {
137+
return err
138+
}
139+
err = cfg.SaveTo(filename)
140+
if err != nil {
141+
return err
142+
}
143+
return nil
144+
}

Diff for: main.go

+55-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"html/template"
2626
"io"
2727
"os"
28-
"os/exec"
2928
"regexp"
3029
"runtime"
3130
"runtime/debug"
@@ -40,6 +39,7 @@ import (
4039
"github.com/arduino/arduino-create-agent/systray"
4140
"github.com/arduino/arduino-create-agent/tools"
4241
"github.com/arduino/arduino-create-agent/updater"
42+
"github.com/arduino/arduino-create-agent/utilities"
4343
v2 "github.com/arduino/arduino-create-agent/v2"
4444
paths "github.com/arduino/go-paths-helper"
4545
cors "github.com/gin-contrib/cors"
@@ -86,6 +86,7 @@ var (
8686
verbose = iniConf.Bool("v", true, "show debug logging")
8787
crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
8888
autostartMacOS = iniConf.Bool("autostartMacOS", true, "the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)")
89+
installCerts = iniConf.Bool("installCerts", false, "install the HTTPS certificate for Safari and keep it updated")
8990
)
9091

9192
// the ports filter provided by the user via the -regex flag, if any
@@ -177,7 +178,7 @@ func loop() {
177178
// If we are updating manually from 1.2.7 to 1.3.0 we have to uninstall the old agent manually first.
178179
// This check will inform the user if he needs to run the uninstall first
179180
if runtime.GOOS == "darwin" && oldInstallExists() {
180-
printDialog("Old agent installation of the Arduino Create Agent found, please uninstall it before launching the new one")
181+
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\"")
181182
os.Exit(0)
182183
}
183184

@@ -220,6 +221,32 @@ func loop() {
220221
configPath = config.GenerateConfig(configDir)
221222
}
222223

224+
// if the default browser is Safari, prompt the user to install HTTPS certificates
225+
// and eventually install them
226+
if runtime.GOOS == "darwin" && cert.GetDefaultBrowserName() == "Safari" {
227+
if exist, err := installCertsKeyExists(configPath.String()); err != nil {
228+
log.Panicf("config.ini cannot be parsed: %s", err)
229+
} else if !exist {
230+
if config.CertsExist() {
231+
err = config.SetInstallCertsIni(configPath.String(), "true")
232+
if err != nil {
233+
log.Panicf("config.ini cannot be parsed: %s", err)
234+
}
235+
} else if cert.PromptInstallCertsSafari() {
236+
err = config.SetInstallCertsIni(configPath.String(), "true")
237+
if err != nil {
238+
log.Panicf("config.ini cannot be parsed: %s", err)
239+
}
240+
cert.GenerateAndInstallCertificates(config.GetCertificatesDir())
241+
} else {
242+
err = config.SetInstallCertsIni(configPath.String(), "false")
243+
if err != nil {
244+
log.Panicf("config.ini cannot be parsed: %s", err)
245+
}
246+
}
247+
}
248+
}
249+
223250
// Parse the config.ini
224251
args, err := parseIni(configPath.String())
225252
if err != nil {
@@ -342,6 +369,24 @@ func loop() {
342369
}
343370
}
344371

372+
// check if the HTTPS certificates are expired and prompt the user to update them on macOS
373+
if runtime.GOOS == "darwin" && cert.GetDefaultBrowserName() == "Safari" {
374+
if *installCerts {
375+
if config.CertsExist() {
376+
cert.PromptExpiredCerts(config.GetCertificatesDir())
377+
} else if cert.PromptInstallCertsSafari() {
378+
// installing the certificates from scratch at this point should only happen if
379+
// something went wrong during previous installation attempts
380+
cert.GenerateAndInstallCertificates(config.GetCertificatesDir())
381+
} else {
382+
err = config.SetInstallCertsIni(configPath.String(), "false")
383+
if err != nil {
384+
log.Panicf("config.ini cannot be parsed: %s", err)
385+
}
386+
}
387+
}
388+
}
389+
345390
// launch the discoveries for the running system
346391
go serialPorts.Run()
347392
// launch the hub routine which is the singleton for the websocket server
@@ -457,12 +502,6 @@ func oldInstallExists() bool {
457502
return oldAgentPath.Join("ArduinoCreateAgent.app").Exist()
458503
}
459504

460-
// printDialog will print a GUI error dialog on macos
461-
func printDialog(dialogText string) {
462-
oscmd := exec.Command("osascript", "-e", "display dialog \""+dialogText+"\" buttons \"OK\" with title \"Error\"")
463-
_ = oscmd.Run()
464-
}
465-
466505
func parseIni(filename string) (args []string, err error) {
467506
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: false, AllowPythonMultilineValues: true}, filename)
468507
if err != nil {
@@ -487,3 +526,11 @@ func parseIni(filename string) (args []string, err error) {
487526

488527
return args, nil
489528
}
529+
530+
func installCertsKeyExists(filename string) (bool, error) {
531+
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: false, AllowPythonMultilineValues: true}, filename)
532+
if err != nil {
533+
return false, err
534+
}
535+
return cfg.Section("").HasKey("installCerts"), nil
536+
}

0 commit comments

Comments
 (0)