mirror of https://github.com/namecoin/ncdns
Merge #17: NSS TLS certificate injection
pull/61/head375ff45
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
commit
49b181854b
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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…
Reference in New Issue