Browse Source

Support installing into Java's root store (#38)

Adam Shannon 6 years ago
parent
commit
ae768be874
2 changed files with 129 additions and 0 deletions
  1. 22 0
      main.go
  2. 107 0
      truststore_java.go

+ 22 - 0
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("")

+ 107 - 0
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
+}