Merge #17: NSS TLS certificate injection

375ff45 certinject: NSS: Add an internal test. (JeremyRand)
ead7a20 certinject: NSS: Improve error handling. (JeremyRand)
145d1e3 certinject: Fix various issues found by static analysis. (JeremyRand)
2c8b5fe certinject: NSS improvements, now works on arbitrary NSS cert store directories. (JeremyRand)
e5c7c09 certinject: add support for the shared NSS trust store on GNU/Linux systems. (JeremyRand)

Pull request description:

  Extend #16 to support the user's shared NSS trust store on GNU/Linux systems.

  Please review but do not merge yet.

  TODO before merging:

  - [x] Get #16 merged.
  - [x] Figure out what to do in the case where ncdns isn't run by the same user as the owner of the NSS database.  Presumably it makes sense to run ncdns under its own user.  Should we require a config option that lists the users whose NSS databases are written to?

  Other issue to discuss:

  Writing to the NSS database with `certutil` is really slow, I'm seeing ~700ms latency added by this.  Is there a faster way to do it?  If we try to handle multiple NSS databases (one per user), this could easily cause DNS timeouts.  Using the system NSS database should be possible, but it would be unsafe for users who haven't installed the HPKP pin into Chromium.

Tree-SHA512: d35fcb44e6c09d6654140de8cf378b0b7523ac19d63d007064db14d5c84cd2178cad95d348baa3234843d215fb563185b98ced33c3e876876d8d42a01ba4e6a7
pull/61/head
JeremyRand 6 years ago
commit 49b181854b
No known key found for this signature in database
GPG Key ID: B3F2D165786D6570

@ -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.")
)

@ -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."
)

@ -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() {
}

@ -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."
)

@ -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()
}
}

@ -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()
}
}

@ -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.

@ -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
}

@ -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")
}
}

@ -0,0 +1,7 @@
// +build !windows
package certinject
const (
nssCertutilName = "certutil"
)

@ -0,0 +1,5 @@
package certinject
const (
nssCertutilName = "nss-certutil"
)
Loading…
Cancel
Save