diff --git a/main.go b/main.go index 90747a9..1b10b0c 100644 --- a/main.go +++ b/main.go @@ -105,6 +105,10 @@ func (m *mkcert) Run(args []string) { warning = true log.Printf("Warning: the local CA is not installed in the %s trust store! ⚠️", NSSBrowsers) } + if hasJava && !m.checkJava() { + warning = true + log.Println("Warning: the local CA is not installed in the Java trust store! ⚠️") + } if warning { log.Println("Run \"mkcert -install\" to avoid verification errors ‼️") } @@ -179,6 +183,15 @@ func (m *mkcert) install() { } printed = true } + if hasJava && !m.checkJava() { + if hasKeytool { + m.installJava() + log.Println("The local CA is now installed in Java's trust store! ☕️") + } else { + log.Println(`Warning: "keytool" is not available, so the CA can't be automatically installed in Java's trust store! ⚠️`) + } + printed = true + } if printed { log.Print("") } @@ -195,6 +208,15 @@ func (m *mkcert) uninstall() { log.Print("") } } + if hasJava { + if hasKeytool { + m.uninstallJava() + } else { + log.Print("") + log.Println(`Warning: "keytool" is not available, so the CA can't be automatically uninstalled from Java's trust store (if it was ever installed)! ⚠️`) + log.Print("") + } + } if m.uninstallPlatform() { log.Print("The local CA is now uninstalled from the system trust store(s)! 👋") log.Print("") diff --git a/truststore_java.go b/truststore_java.go new file mode 100644 index 0000000..7d84f5f --- /dev/null +++ b/truststore_java.go @@ -0,0 +1,107 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "hash" + "os" + "os/exec" + "path" + "path/filepath" + "strings" +) + +var ( + hasJava bool + hasKeytool bool + + javaHome string + cacertsPath string + keytoolPath string + storePass string = "changeit" +) + +func init() { + if v := os.Getenv("JAVA_HOME"); v != "" { + hasJava = true + javaHome = v + + _, err := os.Stat(path.Join(v, "bin/keytool")) + if err == nil { + hasKeytool = true + keytoolPath = path.Join(v, "bin/keytool") + } + + cacertsPath = path.Join(v, "jre/lib/security/cacerts") + } +} + +func (m *mkcert) checkJava() bool { + // exists returns true if the given x509.Certificate's fingerprint + // is in the keytool -list output + exists := func(c *x509.Certificate, h hash.Hash, keytoolOutput []byte) bool { + h.Write(c.Raw) + fp := strings.ToUpper(hex.EncodeToString(h.Sum(nil))) + return bytes.Contains(keytoolOutput, []byte(fp)) + } + + keytoolOutput, err := exec.Command(keytoolPath, "-list", "-keystore", cacertsPath, "-storepass", storePass).CombinedOutput() + fatalIfCmdErr(err, "keytool -list", keytoolOutput) + // keytool outputs SHA1 and SHA256 (Java 9+) certificates in uppercase hex + // with each octet pair delimitated by ":". Drop them from the keytool output + keytoolOutput = bytes.Replace(keytoolOutput, []byte(":"), nil, -1) + + // pre-Java 9 uses SHA1 fingerprints + s1, s256 := sha1.New(), sha256.New() + return exists(m.caCert, s1, keytoolOutput) || exists(m.caCert, s256, keytoolOutput) +} + +func (m *mkcert) installJava() { + args := []string{ + "-importcert", "-noprompt", + "-keystore", cacertsPath, + "-storepass", storePass, + "-file", filepath.Join(m.CAROOT, rootName), + "-alias", m.caUniqueName(), + } + + out, err := m.execKeytool(exec.Command(keytoolPath, args...)) + fatalIfCmdErr(err, "keytool -importcert", out) +} + +func (m *mkcert) uninstallJava() { + args := []string{ + "-delete", + "-alias", m.caUniqueName(), + "-keystore", cacertsPath, + "-storepass", storePass, + } + out, err := m.execKeytool(exec.Command(keytoolPath, args...)) + if bytes.Contains(out, []byte("does not exist")) { + return // cert didn't exist + } + fatalIfCmdErr(err, "keytool -delete", out) +} + +// execKeytool will execute a "keytool" command and if needed re-execute +// the command wrapped in 'sudo' to work around file permissions. +func (m *mkcert) execKeytool(cmd *exec.Cmd) ([]byte, error) { + out, err := cmd.CombinedOutput() + if err != nil && bytes.Contains(out, []byte("java.io.FileNotFoundException")) { + origArgs := cmd.Args[1:] + cmd = exec.Command("sudo", keytoolPath) + cmd.Args = append(cmd.Args, origArgs...) + cmd.Env = []string{ + "JAVA_HOME=" + javaHome, + } + out, err = cmd.CombinedOutput() + } + return out, err +}