diff --git a/certinject/certinject.go b/certinject/certinject.go new file mode 100644 index 0000000..513a9a0 --- /dev/null +++ b/certinject/certinject.go @@ -0,0 +1,11 @@ +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 index 71ddb30..fb175f4 100644 --- a/certinject/certinject_linux.go +++ b/certinject/certinject_linux.go @@ -1,34 +1,5 @@ package certinject -import "gopkg.in/hlandau/easyconfig.v1/cflag" -import "github.com/hlandau/xlog" - - -// This package is used to add and remove certificates to the system trust -// store. -// Currently only supports the system NSS store. - -var log, Log = xlog.New("ncdns.certinject") - var ( - flagGroup = cflag.NewGroup(nil, "certstore") - nssSharedFlag = cflag.Bool(flagGroup, "nss-shared", false, "Synchronize TLS certs to the NSS shared 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, 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.") + 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." ) - -// Injects the given cert into all configured trust stores. -func InjectCert(derBytes []byte) { - - if nssSharedFlag.Value() { - injectCertNssShared(derBytes) - } -} - -// Cleans expired certs from all configured trust stores. -func CleanCerts() { - - if nssSharedFlag.Value() { - cleanCertsNssShared() - } - -} diff --git a/certinject/certinject_misc.go b/certinject/certinject_misc.go deleted file mode 100644 index c017530..0000000 --- a/certinject/certinject_misc.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build !windows,!linux - -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..5368a65 --- /dev/null +++ b/certinject/certinject_notlinux.go @@ -0,0 +1,7 @@ +// +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..23f7f51 --- /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") + +// Injects the given cert into all configured trust stores. +func InjectCert(derBytes []byte) { + + if nssFlag.Value() { + injectCertNss(derBytes) + } +} + +// 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..288b8ad 100644 --- a/certinject/certinject_windows.go +++ b/certinject/certinject_windows.go @@ -7,14 +7,12 @@ 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. @@ -23,6 +21,9 @@ func InjectCert(derBytes []byte) { if cryptoApiFlag.Value() { injectCertCryptoApi(derBytes) } + if nssFlag.Value() { + injectCertNss(derBytes) + } } // Cleans expired certs from all configured trust stores. @@ -31,5 +32,8 @@ func CleanCerts() { if cryptoApiFlag.Value() { cleanCertsCryptoApi() } + if nssFlag.Value() { + cleanCertsNss() + } } diff --git a/certinject/nss.go b/certinject/nss.go new file mode 100644 index 0000000..a6c54ed --- /dev/null +++ b/certinject/nss.go @@ -0,0 +1,103 @@ +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) + + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + +} + +func cleanCertsNss() { + + if certDir.Value() == "" { + log.Fatal("Empty nsscertdir configuration.") + } + if nssDir.Value() == "" { + log.Fatal("Empty nssdbdir configuration.") + } + + certFiles, _ := ioutil.ReadDir(certDir.Value() + "/") + + // 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.Fatal(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) + + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + + // Also delete the cert from the filesystem + err = os.Remove(certDir.Value() + "/" + filename) + } + } + +} + +func checkCertExpiredNss(certFile os.FileInfo) (bool, error) { + + // Get the last modified time + certFileModTime := certFile.ModTime() + + // If the cert's last modified timestamp differs too much from the current time in either direction, consider it expired + expired := math.Abs( time.Since(certFileModTime).Seconds() ) > float64(certExpirePeriod.Value()) + + return expired, nil + +} + +func nicknameFromFingerprintHexNss(fingerprintHex string) string { + return "Namecoin-" + fingerprintHex +} diff --git a/certinject/nss_notwindows.go b/certinject/nss_notwindows.go new file mode 100644 index 0000000..930bf90 --- /dev/null +++ b/certinject/nss_notwindows.go @@ -0,0 +1,7 @@ +// +build !windows + +package certinject + +var ( + nssCertutilName = "certutil" +) diff --git a/certinject/nss_shared_linux.go b/certinject/nss_shared_linux.go deleted file mode 100644 index 1d60885..0000000 --- a/certinject/nss_shared_linux.go +++ /dev/null @@ -1,89 +0,0 @@ -package certinject - -import "crypto/sha256" -import "encoding/hex" -import "io/ioutil" -import "os" -import "os/exec" -import "strings" -import "math" -import "time" - -var homeDir = os.Getenv("HOME") - -var certDir = homeDir + "/.ncdns/certs" -var nssDir = "sql:" + homeDir + "/.pki/nssdb" - -func injectCertNssShared(derBytes []byte) { - - fingerprint := sha256.Sum256(derBytes) - - fingerprintHex := hex.EncodeToString(fingerprint[:]) - - path := certDir + "/" + fingerprintHex + ".pem" - - injectCertFile(derBytes, path) - - nickname := nicknameFromFingerprintHexNssShared(fingerprintHex) - - cmd := exec.Command("certutil", "-d", nssDir, "-A", "-t", "CP,,", "-n", nickname, "-a", "-i", path) - - err := cmd.Run() - if err != nil { - log.Fatal(err) - } - -} - -func cleanCertsNssShared() { - - certFiles, _ := ioutil.ReadDir(certDir + "/") - - // for all Namecoin certs in the folder - for _, f := range certFiles { - - // Check if the cert is expired - expired, err := checkCertExpiredNssShared(f) - if err != nil { - log.Fatal(err) - } - - // delete the cert if it's expired - if expired { - - filename := f.Name() - - fingerprintHex := strings.Replace(filename, ".pem", "", -1) - - nickname := nicknameFromFingerprintHexNssShared(fingerprintHex) - - // Delete the cert from NSS - cmd := exec.Command("certutil", "-d", nssDir, "-D", "-n", nickname) - - err := cmd.Run() - if err != nil { - log.Fatal(err) - } - - // Also delete the cert from the filesystem - err = os.Remove(certDir + "/" + filename) - } - } - -} - -func checkCertExpiredNssShared(certFile os.FileInfo) (bool, error) { - - // Get the last modified time - certFileModTime := certFile.ModTime() - - // If the cert's last modified timestamp differs too much from the current time in either direction, consider it expired - expired := math.Abs( time.Since(certFileModTime).Seconds() ) > float64(certExpirePeriod.Value()) - - return expired, nil - -} - -func nicknameFromFingerprintHexNssShared(fingerprintHex string) string { - return "Namecoin-" + fingerprintHex -} diff --git a/certinject/nss_windows.go b/certinject/nss_windows.go new file mode 100644 index 0000000..3a5c223 --- /dev/null +++ b/certinject/nss_windows.go @@ -0,0 +1,5 @@ +package certinject + +var ( + nssCertutilName = "nss-certutil" +)