From 2e50c752328db1650e9047b4c0064d0c07e2baaa Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Sun, 11 Feb 2018 20:56:24 +0000 Subject: [PATCH] ncdumpzone: Add Firefox mode. This mode outputs a cert_override.txt file (based on TLSA records) that Firefox will accept. --- ncdumpzone/ncdumpzone.go | 14 +++- tlsoverridefirefox/firefox.go | 124 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tlsoverridefirefox/firefox.go diff --git a/ncdumpzone/ncdumpzone.go b/ncdumpzone/ncdumpzone.go index 1ba2eb4..0ed441d 100644 --- a/ncdumpzone/ncdumpzone.go +++ b/ncdumpzone/ncdumpzone.go @@ -3,6 +3,7 @@ package main import "gopkg.in/alecthomas/kingpin.v2" import "github.com/namecoin/ncdns/ncdomain" import "github.com/namecoin/ncdns/namecoin" +import "github.com/namecoin/ncdns/tlsoverridefirefox" import "github.com/namecoin/ncdns/util" import "github.com/hlandau/xlog" import "strings" @@ -14,6 +15,9 @@ var ( rpchostFlag = kingpin.Flag("rpchost", "Namecoin RPC host:port").Default("127.0.0.1:8336").String() rpcuserFlag = kingpin.Flag("rpcuser", "Namecoin RPC username").String() rpcpassFlag = kingpin.Flag("rpcpass", "Namecoin RPC password").String() + formatFlag = kingpin.Flag("format", "Output format. \"zonefile\" = "+ + "DNS zone file. \"firefox-override\" = Firefox "+ + "cert_override.txt format.").Default("zonefile").String() ) var conn namecoin.Conn @@ -79,7 +83,15 @@ func main() { log.Warne(err, "error generating RRs") for _, rr := range rrs { - fmt.Print(rr.String(), "\n") + if *formatFlag == "zonefile" { + fmt.Print(rr.String(), "\n") + } else if *formatFlag == "firefox-override" { + result, err := tlsoverridefirefox.OverrideFromRR(rr) + if err != nil { + panic(err) + } + fmt.Print(result) + } } } diff --git a/tlsoverridefirefox/firefox.go b/tlsoverridefirefox/firefox.go new file mode 100644 index 0000000..423409a --- /dev/null +++ b/tlsoverridefirefox/firefox.go @@ -0,0 +1,124 @@ +package tlsoverridefirefox + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "strings" + + "github.com/miekg/dns" + "github.com/namecoin/ncdns/util" +) + +// OverrideFromRR returns a Firefox certificate override (in cert_override.txt +// format) derived from rr. If no such override can be derived, returns an +// empty string. +func OverrideFromRR(rr dns.RR) (string, error) { + tlsa, ok := rr.(*dns.TLSA) + + if !ok { + return "", nil + } + + portLabel, protocolLabelAndHost := util.SplitDomainTail(tlsa.Hdr.Name) + protocolLabel, hostFQDN := util.SplitDomainTail(protocolLabelAndHost) + + if protocolLabel != "_tcp" { + return "", nil + } + + if !strings.HasPrefix(portLabel, "_") { + return "", nil + } + + port := strings.TrimPrefix(portLabel, "_") + + if !strings.HasSuffix(hostFQDN, ".") { + return "", fmt.Errorf("TLSA not a FQDN") + } + + host := strings.TrimSuffix(hostFQDN, ".") + + // SHA256, as per https://dxr.mozilla.org/mozilla-central/source/security/manager/ssl/nsCertOverrideService.cpp + fingerprintAlgo := "OID.2.16.840.1.101.3.4.2.1" + + // Possible Usage values: + // 0: CA constraint. No override is necessary. + // 1: Service certificate constraint. No override is necessary. + // 2: Trust anchor assertion. Firefox doesn't support these. + // 3: Domain-issued certificate. Do an override in this case. + + if tlsa.Usage != 3 { + return "", nil + } + + // Only a full certificate selector can yield a SHA256 certificate + // fingerprint. + if tlsa.Selector != 0 { + return "", nil + } + + fingerprint, err := getFingerprint(tlsa) + if err != nil { + return "", nil + } + + overrideMask := "U" + + // Format documented in https://dxr.mozilla.org/mozilla-central/source/security/manager/ssl/nsNSSCertificate.cpp + // However, it looks empirically like we can just use 0-length serial + // number and 0-length DN, and Firefox doesn't care. + dbKey := base64.StdEncoding.EncodeToString([]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + + return host + ":" + port + "\t" + fingerprintAlgo + "\t" + + fingerprint + "\t" + overrideMask + "\t" + dbKey + "\n", nil +} + +func getFingerprint(tlsa *dns.TLSA) (string, error) { + var fingerprint string + + // SHA512 fingerprint can't yield a SHA256 fingerprint + if tlsa.MatchingType == 2 { + return "", fmt.Errorf("SHA512 fingerprint can't yield a " + + "SHA256 fingerprint") + } + + if tlsa.MatchingType == 1 { + // SHA256 fingerprint + + fingerprintBytes, err := hex.DecodeString(tlsa.Certificate) + if err != nil { + return "", err + } + + fingerprint = insertColons(fingerprintBytes) + } else if tlsa.MatchingType == 0 { + // Exact match + + certificateBytes, err := hex.DecodeString(tlsa.Certificate) + if err != nil { + return "", err + } + + fingerprintArray := sha256.Sum256(certificateBytes) + + fingerprint = insertColons(fingerprintArray[:]) + } else { + // Unknown MatchingType + return "", fmt.Errorf("Unknown MatchingType") + } + + return strings.ToUpper(fingerprint), nil +} + +// Based on FingerprintLegacyMD5 from +// https://github.com/golang/crypto/blob/master/ssh/keys.go +func insertColons(input []byte) string { + hexarray := make([]string, len(input)) + for i, c := range input { + hexarray[i] = hex.EncodeToString([]byte{c}) + } + return strings.Join(hexarray, ":") +}