diff --git a/certinject/certinject.go b/certinject/certinject.go new file mode 100644 index 0000000..c4da3be --- /dev/null +++ b/certinject/certinject.go @@ -0,0 +1,14 @@ +package certinject + +import ( + "gopkg.in/hlandau/easyconfig.v1/cflag" +) + +var ( + flagGroup = cflag.NewGroup(nil, "certstore") + nssFlag = cflag.Bool(flagGroup, "nss", false, nssExplain) + certExpirePeriod = cflag.Int(flagGroup, "expire", 60*30, "Duration "+ + "(in seconds) after which TLS certs will be removed from the "+ + "trust store. Making this smaller than the DNS TTL (default "+ + "600) may cause TLS errors.") +) diff --git a/certinject/certinject_linux.go b/certinject/certinject_linux.go new file mode 100644 index 0000000..ede3d1c --- /dev/null +++ b/certinject/certinject_linux.go @@ -0,0 +1,11 @@ +package certinject + +var ( + nssExplain = "Synchronize TLS certs to an NSS sqlite3 trust store? " + + "This enables HTTPS to work with NSS web browsers such as " + + "Chromium/Chrome. Only use if you've set up NUMS HPKP in " + + "Chromium/Chrome as per documentation. If you haven't set " + + "up NUMS HPKP, or if you access the configured NSS sqlite3 " + + "trust store from browsers not based on Chromium, this is " + + "unsafe and should not be used." +) diff --git a/certinject/certinject_misc.go b/certinject/certinject_misc.go deleted file mode 100644 index ff40bc5..0000000 --- a/certinject/certinject_misc.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build !windows - -package certinject - -import "github.com/hlandau/xlog" - -var log, Log = xlog.New("ncdns.certinject") - -func InjectCert(derBytes []byte) { - -} - -func CleanCerts() { - -} diff --git a/certinject/certinject_notlinux.go b/certinject/certinject_notlinux.go new file mode 100644 index 0000000..28d7f19 --- /dev/null +++ b/certinject/certinject_notlinux.go @@ -0,0 +1,10 @@ +// +build !linux + +package certinject + +var ( + nssExplain = "(Unsafe and experimental!) Synchronize TLS certs to " + + "an NSS sqlite3 trust store? This enables HTTPS to work " + + "with some NSS-based software. This is currently unsafe " + + "and should not be used." +) diff --git a/certinject/certinject_notwindows.go b/certinject/certinject_notwindows.go new file mode 100644 index 0000000..ee70a3e --- /dev/null +++ b/certinject/certinject_notwindows.go @@ -0,0 +1,28 @@ +// +build !windows + +package certinject + +import "github.com/hlandau/xlog" + +// This package is used to add and remove certificates to the system trust +// store. +// Currently only supports NSS sqlite3 stores. + +var log, Log = xlog.New("ncdns.certinject") + +// InjectCert injects the given cert into all configured trust stores. +func InjectCert(derBytes []byte) { + + if nssFlag.Value() { + injectCertNss(derBytes) + } +} + +// CleanCerts cleans expired certs from all configured trust stores. +func CleanCerts() { + + if nssFlag.Value() { + cleanCertsNss() + } + +} diff --git a/certinject/certinject_windows.go b/certinject/certinject_windows.go index 8811560..2670ff6 100644 --- a/certinject/certinject_windows.go +++ b/certinject/certinject_windows.go @@ -7,29 +7,40 @@ import ( // This package is used to add and remove certificates to the system trust // store. -// Currently only supports Windows CryptoAPI. +// Currently only supports Windows CryptoAPI and NSS sqlite3 stores. var log, Log = xlog.New("ncdns.certinject") var ( - flagGroup = cflag.NewGroup(nil, "certstore") - cryptoApiFlag = cflag.Bool(flagGroup, "cryptoapi", false, "Synchronize TLS certs to the CryptoAPI trust store? This enables HTTPS to work with Chromium/Chrome. Only use if you've set up null HPKP in Chromium/Chrome as per documentation. If you haven't set up null HPKP, or if you access ncdns from browsers not based on Chromium or Firefox, this is unsafe and should not be used.") - certExpirePeriod = cflag.Int(flagGroup, "expire", 60*30, "Duration (in seconds) after which TLS certs will be removed from the trust store. Making this smaller than the DNS TTL (default 600) may cause TLS errors.") + cryptoApiFlag = cflag.Bool(flagGroup, "cryptoapi", false, + "Synchronize TLS certs to the CryptoAPI trust store? This "+ + "enables HTTPS to work with Chromium/Chrome. Only "+ + "use if you've set up NUMS HPKP in Chromium/Chrome "+ + "as per documentation. If you haven't set up NUMS "+ + "HPKP, or if you access ncdns from browsers not "+ + "based on Chromium or Firefox, this is unsafe and "+ + "should not be used.") ) -// Injects the given cert into all configured trust stores. +// InjectCert injects the given cert into all configured trust stores. func InjectCert(derBytes []byte) { if cryptoApiFlag.Value() { injectCertCryptoApi(derBytes) } + if nssFlag.Value() { + injectCertNss(derBytes) + } } -// Cleans expired certs from all configured trust stores. +// CleanCerts cleans expired certs from all configured trust stores. func CleanCerts() { if cryptoApiFlag.Value() { cleanCertsCryptoApi() } + if nssFlag.Value() { + cleanCertsNss() + } } diff --git a/certinject/cryptoapi_windows.go b/certinject/cryptoapi_windows.go index 5451e50..5e52106 100644 --- a/certinject/cryptoapi_windows.go +++ b/certinject/cryptoapi_windows.go @@ -4,10 +4,11 @@ import ( "crypto/sha1" "encoding/hex" "fmt" - "golang.org/x/sys/windows/registry" "math" "strings" "time" + + "golang.org/x/sys/windows/registry" ) // In 64-bit Windows, this key is shared between 64-bit and 32-bit applications. diff --git a/certinject/nss.go b/certinject/nss.go new file mode 100644 index 0000000..0f2a348 --- /dev/null +++ b/certinject/nss.go @@ -0,0 +1,136 @@ +package certinject + +import "crypto/sha256" +import "encoding/hex" +import "io/ioutil" +import "os" +import "os/exec" +import "strings" +import "math" +import "time" +import "gopkg.in/hlandau/easyconfig.v1/cflag" + +var certDir = cflag.String(flagGroup, "nsscertdir", "", "Directory to store "+ + "certificate files. Only use a directory that only ncdns can write "+ + "to. (Required if nss is set.)") +var nssDir = cflag.String(flagGroup, "nssdbdir", "", "Directory that "+ + "contains NSS's cert9.db. (Required if nss is set.)") + +func injectCertNss(derBytes []byte) { + + if certDir.Value() == "" { + log.Fatal("Empty nsscertdir configuration.") + } + if nssDir.Value() == "" { + log.Fatal("Empty nssdbdir configuration.") + } + + fingerprint := sha256.Sum256(derBytes) + + fingerprintHex := hex.EncodeToString(fingerprint[:]) + + path := certDir.Value() + "/" + fingerprintHex + ".pem" + + injectCertFile(derBytes, path) + + nickname := nicknameFromFingerprintHexNss(fingerprintHex) + + // TODO: check whether we can replace CP with just P. + cmd := exec.Command(nssCertutilName, "-d", "sql:"+nssDir.Value(), "-A", + "-t", "CP,,", "-n", nickname, "-a", "-i", path) + + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + if strings.Contains(string(stdoutStderr), "SEC_ERROR_PKCS11_GENERAL_ERROR") { + log.Warn("Temporary SEC_ERROR_PKCS11_GENERAL_ERROR injecting certificate to NSS database; retrying in 1ms...") + time.Sleep(1 * time.Millisecond) + injectCertNss(derBytes) + } else { + log.Errorf("Error injecting cert to NSS database: %s\n%s", err, stdoutStderr) + } + } + +} + +func cleanCertsNss() { + + if certDir.Value() == "" { + log.Fatal("Empty nsscertdir configuration.") + } + if nssDir.Value() == "" { + log.Fatal("Empty nssdbdir configuration.") + } + + certFiles, err := ioutil.ReadDir(certDir.Value() + "/") + if err != nil { + log.Fatalf("Error enumerating files in cert directory: %s", err) + } + + // for all Namecoin certs in the folder + for _, f := range certFiles { + + // Check if the cert is expired + expired, err := checkCertExpiredNss(f) + if err != nil { + log.Fatalf("Error checking if NSS cert is expired: %s", err) + } + + // delete the cert if it's expired + if expired { + + filename := f.Name() + + fingerprintHex := strings.Replace(filename, ".pem", "", + -1) + + nickname := nicknameFromFingerprintHexNss( + fingerprintHex) + + // Delete the cert from NSS + cmd := exec.Command(nssCertutilName, "-d", "sql:"+ + nssDir.Value(), "-D", "-n", nickname) + + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + if strings.Contains(string(stdoutStderr), "SEC_ERROR_UNRECOGNIZED_OID") { + log.Warn("Tried to delete certificate from NSS database, but the certificate was already not present in NSS database") + } else if strings.Contains(string(stdoutStderr), "SEC_ERROR_PKCS11_GENERAL_ERROR") { + log.Warn("Temporary SEC_ERROR_PKCS11_GENERAL_ERROR deleting certificate from NSS database; retrying in 1ms...") + time.Sleep(1 * time.Millisecond) + cleanCertsNss() + } else { + log.Fatalf("Error deleting cert from NSS database: %s\n%s", err, stdoutStderr) + } + } + + // Also delete the cert from the filesystem + err = os.Remove(certDir.Value() + "/" + filename) + if err != nil { + log.Fatalf("Error deleting NSS cert from filesystem: %s", err) + } + } + } + +} + +func checkCertExpiredNss(certFile os.FileInfo) (bool, error) { + + // Get the last modified time + certFileModTime := certFile.ModTime() + + age := time.Since(certFileModTime) + ageSeconds := age.Seconds() + + // If the cert's last modified timestamp differs too much from the + // current time in either direction, consider it expired + expired := math.Abs(ageSeconds) > float64(certExpirePeriod.Value()) + + log.Debugf("Age of certificate: %s = %f seconds; expired = %t", age, ageSeconds, expired) + + return expired, nil + +} + +func nicknameFromFingerprintHexNss(fingerprintHex string) string { + return "Namecoin-" + fingerprintHex +} diff --git a/certinject/nss_internal_test.go b/certinject/nss_internal_test.go new file mode 100644 index 0000000..2a6dc16 --- /dev/null +++ b/certinject/nss_internal_test.go @@ -0,0 +1,48 @@ +package certinject + +import ( + "os" + "testing" + "time" +) + +func TestCheckCertExpired(t *testing.T) { + testFilename := "test_cert_file.pem" + + certExpirePeriod.SetValue(5.0) + + bytesDummy := []byte(`TEST DATA`) + + injectCertFile(bytesDummy, testFilename) + defer os.Remove(testFilename) + + info1, err := os.Stat(testFilename) + if err != nil { + t.Errorf("Error getting file info 1: %s", err) + } + + expired1, err := checkCertExpiredNss(info1) + if err != nil { + t.Errorf("Error checking if file info 1 expired: %s", err) + } + + if expired1 { + t.Errorf("Cert expired instantly") + } + + time.Sleep(10 * time.Second) + + info2, err := os.Stat(testFilename) + if err != nil { + t.Errorf("Error getting file info 2: %s", err) + } + + expired2, err := checkCertExpiredNss(info2) + if err != nil { + t.Errorf("Error checking if file info 2 expired: %s", err) + } + + if !expired2 { + t.Errorf("Cert never expired") + } +} diff --git a/certinject/nss_notwindows.go b/certinject/nss_notwindows.go new file mode 100644 index 0000000..d242f5f --- /dev/null +++ b/certinject/nss_notwindows.go @@ -0,0 +1,7 @@ +// +build !windows + +package certinject + +const ( + nssCertutilName = "certutil" +) diff --git a/certinject/nss_windows.go b/certinject/nss_windows.go new file mode 100644 index 0000000..b24bef3 --- /dev/null +++ b/certinject/nss_windows.go @@ -0,0 +1,5 @@ +package certinject + +const ( + nssCertutilName = "nss-certutil" +)