TLS dehydrated certificate injection for CryptoAPI trust store (triggered by hooking DNS lookups).

pull/16/head
JeremyRand 9 years ago
parent 6ae122309b
commit e16ad6f62c
No known key found for this signature in database
GPG Key ID: B3F2D165786D6570

@ -14,6 +14,7 @@ addons:
sudo: false
install:
- go generate -v ./...
- go get -v -t ./...
- env GOOS=windows GOARCH=amd64 go get -d -v -t ./...
script:

@ -0,0 +1,168 @@
# ncdns-tls: Namecoin TLS Authentication for Web Servers
Entry by Jeremy Rand (University of Oklahoma) in the Borderless Block Party Hackathon, November 2015.
## Summary
ncdns-tls uses the Namecoin blockchain-based naming system to verify TLS certificates of web sites without certificate authorities, notaries, or other trusted third parties. It is built on top of the ncdns DNS server by Hugo Landau, and is coded in Go. It works by hooking DNS requests for Namecoin (.bit) websites and injects the correct certificate into the trust store, in a way that guarantees that only the certificates in the blockchain for a given domain name will be considered correct for that domain name.
## Feasibility
ncdns-tls is absolutely implementable at a large scale. The biggest inefficiency in its approach is the use of "dehydrated certificates" (see technical description below), which are larger than simple certificate hashes, but based on the size of the example configuration in the below documentation, if 100,000,000 websites (the approximate number of non-squatted ICANN domains) use it, then the state of the global name database (with pruning of spent and expired names) is 100,000,000 websites * 351 bytes/website = 35.1 GB, which is well within reason. The other inefficiencies and obstacles in the current implementations are the issues of removing expired certificates, requiring administrator privileges, and supporting browsers that don't use Windows CryptoAPI, but we believe that these are solvable with mild engineering (see the Future Direction sections "Support Other Trust Stores" and "Direct Registry Access to CryptoAPI Trust Store"). If for any reason Namecoin becomes no longer the preferred naming system, ncdns-tls should work (with minimal changes) with other naming systems, such as Ethereum, BitShares, or even systems which use notary trust instead of blockchains (e.g. Perspectives and Convergence for ICANN domains).
## Uniqueness
The idea of using Namecoin to verify TLS certificates has been around since at least 2012, but the idea and ncdns-tls's implementation of injecting certificates into the trust store is new. Previous approaches either involved intercepting proxies like Convergence (which are high-risk; see SuperFish for an example of what can go wrong), or browser add-ons like DNSSEC-Validator (which leak confidential information when certificate verification fails). ncdns-tls's use of "dehydrated certificates" (pieces of a certificate which can reconstruct a full certificate deterministically) to sufficiently compress the certificate to fit into a blockchain record (while also making security checks very simple) is also new. Dehydrated certificates required a fork of the Go standard library's x.509 library, but the changes are very noninvasive.
## Implementation and Execution
ncdns-tls is functional, with an example certificate deployed on https://www.veclabs.bit/ . There are definitely things that can be improved (see the Future Direction sections below), but the fundamental functionality is proven, working, and deployable (although obviously it should get more review and polishing before being deployed in a situation where security matters).
## Need
The use of certificate authorities (CA's) in TLS is an enormous problem, as disastrous security failures like DigiNotar (in which fraudulent certificates valid for hundreds of providers like Microsoft and Google were issued to an Iranian IP and then deployed for over a month without anyone noticing) make fully clear. Other proposed solutions like DNSSEC/DANE and notaries (e.g. Perspectives and Convergence) only shift around the trust to what is hoped are slightly more reliable trusted parties. A fully trust-free TLS validation system with a blockchain security model removes these attack vectors from the equation, which if widely adopted could reduce security risks and costs for businesses and ultimately save the lives of dissidents in repressive countries who rely on TLS to stay safe.
# Additional Documentation
Below is additional documentation for anyone who is interested in learning more, or perhaps running the code.
**Warning: this code is a proof of concept, and has not been subjected to careful testing. It utilizes specifications which not only are unfinalized, but have not been reviewed by anyone else. Do not use this code for any public deployments (on either servers or clients) of any kind whatsoever. We will be working to clean up and standardize this code (and its associated specifications), but we make no guarantees of the timeline associated with said cleanup and standardization.**
## Introduction to Namecoin
Namecoin is a naming system which uses a blockchain. Namecoin was the first solution to Zooko's Triangle, the long-standing problem of having a naming system which is simultaneously global, decentralized, and human-meaningful. Namecoin's first use case is the .bit top-level domain, which provides decentralized, global, human-readable DNS.
## Introduction to Namecoin TLS
Websites currently rely on TLS to prove their authenticity. The TLS certificate authority system is flawed due to centralization. DNSSEC/DANE does not solve the centralization issue. Namecoin in theory offers a good solution (by associating TLS certificates with a .bit domain name), but the task of getting mainstream web browsers to accept Namecoin certificates has, so far, remained unsolved.
Existing methods generally either leak private data (e.g. DNSSEC Validator) or rely on intercepting proxies (e.g. Convergence); both of these are deemed insufficiently safe.
## Certificate Injection
We started with the ncdns codebase. ncdns is a Namecoin authoritative DNS server (in Golang) by Hugo Landau. We added a callback to ncdns so that when it receives a request for "example.bit" it checks whether a TLSA record exists for "_443._tcp.example.bit". If such a record exists, it dumps the certificate to a file, and injects it into the local trust store before replying to the DNS request. This ensures that the web browser will be aware of the certificate prior to beginning certificate validation.
Currently the only trust store which is supported is the Windows CryptoAPI trust store, which is used by Internet Explorer, Chromium/Chrome, and most other Windows software. Firefox uses NSS instead, which we do not currently support. OS X and Linux are not supported currently.
## Getting the Certificate
A typical x.509 certificate is often in the range of 2 to 3 KB. A Namecoin name can only hold 520 bytes, and is often used to store data in addition to TLS authentication data. In addition, injecting arbitrary certificates into the local trust store is a security risk, because a website might provide a certificate which is valid for another website, or which is usable as a certificate authority. We solved both of these issues by generating a certificate on the fly of the following form:
1. Certificate serial number hardcoded to 1. Note that this may cause issues if a single website has multiple certificates.
2. SAN DNS name hardcoded to the specific domain name being looked up. Note that this means a certificate is only valid for one domain name. SNI should be used if a single web server serves more than one domain name.
3. Validity period (NotBefore and NotAfter) specified by the name value.
4. ECDSA public key specified by the name value. Note that we do not support RSA keys, because ECDSA has smaller keys and signatures.
5. ECDSA signature specified by the name value.
6. CA bit disabled; the certificate is only usable as an end entity certificate.
7. KeyUsage hardcoded to "DigitalSignature". Note that we disallow "KeyEncipherment" because that is only used for RSA key exchange (no forward secrecy), and everyone should be using DHE/ECDHE key exchange (which has forward secrecy) now.
8. Subject serial number of "Namecoin TLS Certificate". This is so that if an end user comes across one of our certificates in his or her trust store later, it will be obvious where it came from.
9. Everything else at the default best practices for TLS server certificates.
We call the data that must be stored in the Namecoin value a "dehydrated certificate", and a full certificate generated from that data is called a "rehydrated cert". An example dehydrated certificate, with base64 encoding for the public key and signature, and integer encoding for the validity period, is as follows:
["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvSJuG7K8RYX7toL8iqHnh2d+lMnbqr57pioQzicDpmGooMiKaufVoiJg1fJFUiI2EEbvYO/GjrAFsbCFmzC3Vg==",1420070400,1577836799,10,"MEQCIH/EE6yYEuAlGUIv88VSuBDz4tVWdgWHwJThnU0kiSAsAiBHkXaeFgy5btNpgOvfiIcrovuvkatn3VhrE6Ix3N7TZQ=="]
This is 252 bytes; easily enough to fit into a Namecoin value.
We have provided a command-line tool for generating these certificates, resulting in a dehydrated certificate that can be pasted into a Namecoin value, and a rehydrated certificate that can easily be imported into a web server. For example:
$ ./generate_nmc_cert --host www.example.bit --ecdsa-curve P256 --start-date "Jan 1 00:00:00 2015" --end-date "Dec 31 23:59:59 2019"
2015/11/26 02:42:22 written cert.pem
2015/11/26 02:42:22 written key.pem
2015/11/26 02:42:22 Your NMC cert is: ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvSJuG7K8RYX7toL8iqHnh2d+lMnbqr57pioQzicDpmGooMiKaufVoiJg1fJFUiI2EEbvYO/GjrAFsbCFmzC3Vg==",1420070400,1577836799,10,"MEQCIH/EE6yYEuAlGUIv88VSuBDz4tVWdgWHwJThnU0kiSAsAiBHkXaeFgy5btNpgOvfiIcrovuvkatn3VhrE6Ix3N7TZQ=="]
2015/11/26 02:42:22 SUCCESS: The cert decompressed to an identical form.
Making dehydrated certificates work properly in Golang was tricky, because the x.509 library functions which are needed to splice a signature into a certificate are private. We ended up forking the x.509 package, and adding a single file to it which exposed the necessary functionality as a public function.
Credit for the idea of dehydrated certificates is due to Ryan Castellucci.
## HPKP
We ~~abuse~~ *take advantage of* an interesting quirk in browser implementations of HPKP (HTTPS Public Key Pinning). Browsers only enforce key pins against certificates for built-in certificate authorities; user-specified certificate authorities are exempt from HPKP. This behavior is presumably to make it easier for users to intentionally intercept their own traffic (or for corporations to intercept traffic in their network, which is a less ethical version of a technologically identical concept). As such, we believe that this behavior will not go away anytime soon, and is safe to rely on. We place a key pin at the "bit" domain, with subdomains enabled, for a "nothing up my sleeve" public key hash. As a result, no public CA can sign certificates for any domain ending in ".bit", but user-specified CA's can. Windows CryptoAPI treats user-specified end-entity certificates as user-specified CA's for this purpose. As such, rehydrated certificates that we generate will be considered valid, but nothing else will. (Unless you installed another user-specified CA on your machine that is valid for .bit. But if you did that, then either you want to intercept .bit, in which case it's fine, or you did it against your will, in which case you are already screwed.)
## Instructions
### Installation
1. Install namecoind or Namecoin-Qt from https://namecoin.org .
2. Configure Namecoin to accept RPC connections (same procedure as Bitcoin).
3. Let Namecoin fully download the blockchain (takes circa 5 hours).
4. Install dnssec-trigger from https://www.nlnetlabs.nl/projects/dnssec-trigger/ or your package manager.
5. Install Go from https://golang.org/dl/
6. go get github.com/hlandau/ncdns
7. Replace the ncdns repo contents with this modified version.
8. If on Linux run "x509_build/install.sh" , on Windows create a subdirectory of ncdns called "x509", copy C:\Go\src\crypto\x509\*.go to x509, and copy the .go file from x509_build to x509.
9. go install github.com/hlandau/ncdns
10. Modify the Unbound config file that dnssec-trigger installed to include the lines at https://github.com/hlandau/ncdns.t#using-ncdns-with-a-recursive-resolver , including the domain-insecure line.
11. Create "$GOPATH/etc/ncdns.conf", with content similar to https://forum.namecoin.org/viewtopic.php?p=16072#p16072 . Make sure the bind port is the same as what you entered in Unbound's config file, and that the Namecoin RPC data is what your namecoind is set to.
12. In Chromium/Chrome, go to chrome://net-internals/#hsts
13. Under "Add domain", enter the following: domain "bit", STS unchecked, PKP checked, fingerprint "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=". The fingerprint is a "nothing up my sleeve" value; no one has a private key whose public key hashes to that.
14. Click "Add".
15. Reboot your computer to make sure that dnssec-trigger reloads your configuration.
### Creating and deploying an HTTPS certificate
1. go install github.com/hlandau/ncdns/generate_nmc_cert
2. ./generate_nmc_cert --host www.example.bit --ecdsa-curve P256 --start-date "Jan 1 00:00:00 2015" --end-date "Dec 31 23:59:59 2019"
3. Take the cert and key files generated; configure your HTTPS server to use them for your .bit domain.
4. Take the NMC cert listed in the standard output; place it in your Namecoin name using the following template:
{"map":{"www":{"ip":"YOUR IP HERE", "map":{"_tcp":{"map":{"_443":{"tls":[["c0","c0","c0", YOUR CERT HERE]]}}}}}}}
For example:
{"map":{"www":{"ip":"123.45.67.89", "map":{"_tcp":{"map":{"_443":{"tls":[["c0","c0","c0", ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvSJuG7K8RYX7toL8iqHnh2d+lMnbqr57pioQzicDpmGooMiKaufVoiJg1fJFUiI2EEbvYO/GjrAFsbCFmzC3Vg==",1420070400,1577836799,10,"MEQCIH/EE6yYEuAlGUIv88VSuBDz4tVWdgWHwJThnU0kiSAsAiBHkXaeFgy5btNpgOvfiIcrovuvkatn3VhrE6Ix3N7TZQ=="]]]}}}}}}}
A live example is at https://www.veclabs.bit (block explorer link at https://namecha.in/name/d/veclabs for people who don't have Namecoin installed).
### Running It
1. Run ncdns as an administrator. (Yes, it sucks that you need admin privileges; we think it is possible to fix this with some additional effort that we will do after the hackathon is over.)
2. Visit a .bit website that uses HTTPS. For example, https://www.veclabs.bit . (That site has broken images because of server config issues, but HTTPS should work without certificate errors.)
3. The page will load successfully. Yay!
Note: while the "correct" TLS cert will be trusted on any application that uses Windows CryptoAPI, and "incorrect" TLS certs that are not signed by a default trusted CA will be rejected on any such application, only Chrome will protect you from TLS certs that are not "correct" in the Namecoin blockchain but which have been signed by a default trusted CA (i.e. a CA that Windows comes with and is used for non-.bit domains). This is because Chrome is the only CryptoAPI browser that supports HPKP. Therefore, for maximum protection from malicious certs, you should not visit .bit websites in browsers other than Chrome.
## Future Direction
There are a number of things we would like to improve in this scheme.
### Support Other Trust Stores
Among other things, we would like to support NSS (used by Firefox, and the OSX/Linux versions of Chromium/Chrome), and OpenSSL (used by many applications on Linux).
### Direct Registry Access to CryptoAPI Trust Store
The certificates generated by ncdns-tls are stored by Windows certutil in the registry key HKLM\SOFTWARE\Wow6432Node\Microsoft\EnterpriseCertificates\Root\Certificates. We believe that it is possible to directly add certificates to this registry key without going through certutil. This means that we can apply the Windows registry permission system, which would eliminate the need for ncdns-tls to be run as an administrator. It would also allow us to delete certificates that are outdated, since the registry keeps track of a "last modified" timestamp.
### DNSSEC Dehydrated Certificate Retrieval
Right now, our ncdns callback only checks for TLSA records that exist in Namecoin. It would make more sense to perform an actual TLSA DNS lookup, so that TLSA records that have been delegated to a nameserver using DNSSEC would work as well. (This also is needed in order to allow non-.bit domains to be supported.) This would require checking the AD flag in DNS responses, to make sure that a nameserver that doesn't implement DNSSEC can't provide TLSA records (since that would be insecure). Note that DNSSEC is slightly less secure than Namecoin since DNSSEC signatures expire after about a month, while a Namecoin blockchain is considered outdated after 2 hours, so DNSSEC gives the attacker more time to perform a replay attack.
### Network Dehydrated Certificate Retrieval
In some cases, no dehydrated certificate can be obtained from Namecoin or DNSSEC. This may be because a TLSA record only contains a hash or a public key instead of the full certificate. In such cases, ncdns-tls could perform a TLS handshake with the destination to retrieve a certificate, and then consider it valid if the hash or public key matches the TLSA record. This would require carefulness, since performing a TLS handshake would usually trigger a DNS lookup, which results in an infinite recursion loop of DNS lookups and TLS handshakes. A solution might be to do a TLS handshake with "example.bit.bit-no-hook", since ncdns supports domain suffixes, and then explicitly choose "example.bit" as the SNI header.
### Proxy-Compatible Dehydrated Certificate Retrieval
Relying on DNS hooks for retrieving certificates means that browsers which use DNS over a proxy (in particular TorBrowser) won't trigger the hook. Chromium/Chrome and Firefox/TorBrowser support an API to run Javascript hooks prior to issuing an HTTP request. Using these hooks would allow users who have a proxy configured to still trigger the rehydrated certificate injection. It is also needed in order to enable non-.bit websites (which don't get passed to ncdns) to use dehydrated certificates.
### Notary Dehydrated Certificate Retrieval
Non-.bit websites which don't have a TLSA record could have TLSA records generated from notaries such as what Perspectives provides.
### Firefox cert injection / Windows
NSS on Windows does not provide any easy way to inject certificates to its trust store (unless the user builds NSS-Tools from source). However, Firefox does expose an API to extensions which want to do this. Therefore, providing this option to Firefox users would make things easier.
### More Thorough Dehydration
It may be possible to further dehydrate the certificates, e.g. by using compressed public keys, reducing the precision of the validity period, or using more compact encoding than base64.
### HSTS Simulation
A website which has a known TLSA record should be able to be automatically redirected from HTTP to HTTPS using a browser extension. This would protect against sslstrip attacks (similar to HSTS but without trusting the first use). However, it would also break a tiny subset of websites which list a TLSA record but only serve content on HTTP.
### Test for Validation Bugs
We should carefully test x.509 implementations for bugs that might interact badly with our code. For example, if a bug in a major browser caused certificates without the CA bit to be usable as CA's, that would be very bad. We think it is unlikely that such major bugs exist, given that major browser vendors have had many years to review their code. But one can't be too careful.

@ -6,6 +6,7 @@ import "gopkg.in/hlandau/madns.v1/merr"
import "github.com/namecoin/ncdns/namecoin"
import "github.com/namecoin/ncdns/util"
import "github.com/namecoin/ncdns/ncdomain"
import "github.com/namecoin/ncdns/tlshook"
import "github.com/hlandau/xlog"
import "sync"
import "fmt"
@ -421,6 +422,12 @@ func (tx *btx) _findNCValue(ncv *ncdomain.Value, isubname, subname string, depth
func (tx *btx) addAnswersUnderNCValueActual(ncv *ncdomain.Value, sn string) (rrs []dns.RR, err error) {
rrs, err = ncv.RRs(nil, dns.Fqdn(tx.qname), dns.Fqdn(tx.basename+"."+tx.rootname))
// TODO: add callback variable "OnValueReferencedFunc" to backend options so that we don't pollute this function with every hook that we want
// might need to add the other attributes of tx, and sn, to the callback variable for flexibility's sake
// This doesn't normally return errors, but any errors during execution will be logged.
tlshook.DomainValueHookTLS(tx.qname, ncv)
return
}

@ -0,0 +1,236 @@
package certdehydrate
import (
"bytes"
"crypto/sha256"
"crypto/x509/pkix"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"math/big"
"time"
)
import "github.com/namecoin/ncdns/x509"
// TODO: add a version field
type DehydratedCertificate struct {
PubkeyB64 string
NotBeforeScaled int64
NotAfterScaled int64
SignatureAlgorithm int64
SignatureB64 string
}
func (dehydrated DehydratedCertificate) SerialNumber(name string) ([]byte, error){
nameHash := sha256.Sum256([]byte(name))
pubkeyBytes, err := base64.StdEncoding.DecodeString(dehydrated.PubkeyB64)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert pubkey is not valid base64: %s", err)
}
pubkeyHash := sha256.Sum256(pubkeyBytes)
notBeforeScaledBuf := new(bytes.Buffer)
err = binary.Write(notBeforeScaledBuf, binary.BigEndian, dehydrated.NotBeforeScaled)
if err != nil {
return nil, fmt.Errorf("binary.Write of notBefore failed: %s", err)
}
notBeforeHash := sha256.Sum256(notBeforeScaledBuf.Bytes())
notAfterScaledBuf := new(bytes.Buffer)
err = binary.Write(notAfterScaledBuf, binary.BigEndian, dehydrated.NotAfterScaled)
if err != nil {
return nil, fmt.Errorf("binary.Write of notAfter failed: %s", err)
}
notAfterHash := sha256.Sum256(notAfterScaledBuf.Bytes())
serialHash := sha256.New()
serialHash.Write(nameHash[:])
serialHash.Write(pubkeyHash[:])
serialHash.Write(notBeforeHash[:])
serialHash.Write(notAfterHash[:])
// 19 bytes will be less than 2^159, see https://crypto.stackexchange.com/a/260
return serialHash.Sum(nil)[0:19], nil
}
func (dehydrated DehydratedCertificate) String() string {
output := []interface{}{1, dehydrated.PubkeyB64, dehydrated.NotBeforeScaled, dehydrated.NotAfterScaled, dehydrated.SignatureAlgorithm, dehydrated.SignatureB64}
binOutput, _ := json.Marshal(output)
return string(binOutput)
}
func ParseDehydratedCert(data interface{}) (*DehydratedCertificate, error) {
dehydrated, ok := data.([]interface{})
if !ok {
return nil, fmt.Errorf("Dehydrated cert is not a list")
}
if len(dehydrated) < 1 {
return nil, fmt.Errorf("Dehydrated cert must have a version field")
}
version, ok := dehydrated[0].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert version must be an integer")
}
if version != 1 {
return nil, fmt.Errorf("Dehydrated cert has an unrecognized version")
}
if len(dehydrated) < 6 {
return nil, fmt.Errorf("Dehydrated cert must have 6 items")
}
pubkeyB64, ok := dehydrated[1].(string)
if !ok {
return nil, fmt.Errorf("Dehydrated cert pubkey must be a string")
}
notBeforeScaled, ok := dehydrated[2].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert notBefore must be an integer")
}
notAfterScaled, ok := dehydrated[3].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert notAfter must be an integer")
}
signatureAlgorithm, ok := dehydrated[4].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert signature algorithm must be an integer")
}
signatureB64, ok := dehydrated[5].(string)
if !ok {
return nil, fmt.Errorf("Dehydrated cert signature must be a string")
}
result := DehydratedCertificate {
PubkeyB64: pubkeyB64,
NotBeforeScaled: int64(notBeforeScaled),
NotAfterScaled: int64(notAfterScaled),
SignatureAlgorithm: int64(signatureAlgorithm),
SignatureB64: signatureB64,
}
return &result, nil
}
func DehydrateCert(cert *x509.Certificate) (*DehydratedCertificate, error) {
pubkeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal parsed public key: %s", err)
}
pubkeyB64 := base64.StdEncoding.EncodeToString(pubkeyBytes)
notBeforeInt := cert.NotBefore.Unix()
notAfterInt := cert.NotAfter.Unix()
timestampPrecision := int64(5 * 60) // 5 minute precision
notBeforeScaled := notBeforeInt / timestampPrecision
notAfterScaled := notAfterInt / timestampPrecision
signatureAlgorithm := int64(cert.SignatureAlgorithm)
signatureBytes := cert.Signature
signatureB64 := base64.StdEncoding.EncodeToString(signatureBytes)
result := DehydratedCertificate{
PubkeyB64: pubkeyB64,
NotBeforeScaled: notBeforeScaled,
NotAfterScaled: notAfterScaled,
SignatureAlgorithm: signatureAlgorithm,
SignatureB64: signatureB64,
}
return &result, nil
}
// Accepts as input the bare minimum data needed to produce a valid cert.
// The input is untrusted.
// The output is safe.
// The timestamps are in 5-minute increments.
func RehydrateCert(dehydrated *DehydratedCertificate) (*x509.Certificate, error) {
pubkeyBin, err := base64.StdEncoding.DecodeString(dehydrated.PubkeyB64)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert pubkey must be valid base64: %s", err)
}
pubkey, err := x509.ParsePKIXPublicKey(pubkeyBin)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert pubkey is invalid: %s", err)
}
timestampPrecision := int64(5 * 60) // 5 minute precision
notBeforeInt := dehydrated.NotBeforeScaled * timestampPrecision
notAfterInt := dehydrated.NotAfterScaled * timestampPrecision
notBefore := time.Unix(int64(notBeforeInt), 0)
notAfter := time.Unix(int64(notAfterInt), 0)
signatureAlgorithm := x509.SignatureAlgorithm(dehydrated.SignatureAlgorithm)
signature, err := base64.StdEncoding.DecodeString(dehydrated.SignatureB64)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert signature must be valid base64: %s", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: notBefore,
NotAfter: notAfter,
// x509.KeyUsageKeyEncipherment is used for RSA key exchange, but not DHE/ECDHE key exchange. Since everyone should be using ECDHE (due to forward secrecy), we disallow x509.KeyUsageKeyEncipherment in our template.
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
SignatureAlgorithm: signatureAlgorithm,
PublicKey: pubkey,
Signature: signature,
}
return &template, nil
}
func FillRehydratedCertTemplate(template x509.Certificate, name string) ([]byte, error) {
template.Subject = pkix.Name{
CommonName: name,
SerialNumber: "Namecoin TLS Certificate",
}
// DNS name
template.DNSNames = append(template.DNSNames, name)
// Serial number
dehydrated, err := DehydrateCert(&template)
if err != nil {
return nil, fmt.Errorf("Error dehydrating filled cert template: %s", err)
}
serialNumberBytes, err := dehydrated.SerialNumber(name)
if err != nil {
return nil, fmt.Errorf("Error calculating serial number: %s", err)
}
template.SerialNumber.SetBytes(serialNumberBytes)
derBytes, err := x509.CreateCertificateWithSplicedSignature(&template, &template)
if err != nil {
return nil, fmt.Errorf("Error splicing signature: %s", err)
}
return derBytes, nil
}

@ -0,0 +1,39 @@
package certdehydrate_test
import (
"testing"
"encoding/json"
"reflect"
"github.com/namecoin/ncdns/certdehydrate"
)
func TestDehydratedCertIdentityOperation(t *testing.T) {
bytesJson := []byte(`[1, "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/hy1t4jB14ronx6n1m8VQh02jblRfu2cV3/LcyomfVljypUQMGjmuxWNbPI0a3cF6miNOijSCutqTZdb7TLvig==",4944096,5049216,10,"MEQCIGXXk6gYx95vQoknRwiQ4e27I+DXUWkE8L6dmLwAiGncAiBbtEX1nnZINx1YGzT5Fx8SxpjLwNDTUBkq22NpazHLIA=="]`)
var parsedJson []interface{}
if err := json.Unmarshal(bytesJson, &parsedJson); err != nil {
t.Error("Error parsing JSON:", err)
}
dehydrated, err := certdehydrate.ParseDehydratedCert(parsedJson)
if err != nil {
t.Error("Error parsing dehydrated certificate:", err)
}
template, err := certdehydrate.RehydrateCert(dehydrated)
if err != nil {
t.Error("Error rehydrating certificate:", err)
}
dehydrated2, err := certdehydrate.DehydrateCert(template)
if err != nil {
t.Error("Error dehydrating certificate:", err)
}
// Test to make sure that rehydrating and then dehydrating a cert doesn't change it.
if !reflect.DeepEqual(dehydrated, dehydrated2) {
t.Error(dehydrated, "!=", dehydrated2)
}
}

@ -0,0 +1,15 @@
// +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,36 @@
package certinject
import (
"gopkg.in/hlandau/easyconfig.v1/cflag"
"github.com/hlandau/xlog"
)
// This package is used to add and remove certificates to the system trust
// store.
// Currently only supports Windows CryptoAPI.
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.")
)
// Injects the given cert into all configured trust stores.
func InjectCert(derBytes []byte) {
if cryptoApiFlag.Value() {
injectCertCryptoApi(derBytes)
}
}
// Cleans expired certs from all configured trust stores.
func CleanCerts() {
if cryptoApiFlag.Value() {
cleanCertsCryptoApi()
}
}

@ -0,0 +1,191 @@
package certinject
import (
"golang.org/x/sys/windows/registry"
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"
"math"
"time"
)
// In 64-bit Windows, this key is shared between 64-bit and 32-bit applications.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa384253.aspx
const cryptoApiCertStoreRegistryBase = registry.LOCAL_MACHINE
const cryptoApiCertStoreRegistryKey = `SOFTWARE\Microsoft\EnterpriseCertificates\Root\Certificates`
const cryptoApiMagicName = "Namecoin"
const cryptoApiMagicValue = 1
func injectCertCryptoApi(derBytes []byte) {
// Format documentation of Microsoft's "Certificate Registry Blob":
// 5c 00 00 00 // propid
// 01 00 00 00 // unknown (possibly a version or flags field; value is always the same in my testing)
// 04 00 00 00 // size (little endian)
// subject public key bit length // data[size]
// 19 00 00 00
// 01 00 00 00
// 10 00 00 00
// MD5 of ECC pubkey of certificate
// 0f 00 00 00
// 01 00 00 00
// 20 00 00 00
// Signature Hash
// 03 00 00 00
// 01 00 00 00
// 14 00 00 00
// Cert SHA1 hash
// 14 00 00 00
// 01 00 00 00
// 14 00 00 00
// Key Identifier
// 04 00 00 00
// 01 00 00 00
// 10 00 00 00
// Cert MD5 hash
// 20 00 00 00
// 01 00 00 00
// cert length
// cert
// But, guess what? All you need is the "20" record.
// Windows will happily regenerate all the others for you, whenever you actually try to use the certificate.
// How cool is that?
// Length of cert
certLength := len(derBytes)
// Header for a stripped Windows Certificate Registry Blob
certBlobHeader := []byte{0x20, 0, 0, 0, 0x01, 0, 0, 0, byte( (certLength >> 0) & 0xFF), byte( (certLength >> 8) & 0xFF), byte( (certLength >> 16) & 0xFF), byte( (certLength >> 24) & 0xFF) }
// Construct the Blob
certBlob := append(certBlobHeader, derBytes...)
// Open up the cert store.
certStoreKey, err := registry.OpenKey(cryptoApiCertStoreRegistryBase, cryptoApiCertStoreRegistryKey, registry.ALL_ACCESS)
if err != nil {
log.Errorf("Couldn't open cert store: %s", err)
return
}
defer certStoreKey.Close()
// Windows CryptoAPI uses the SHA-1 fingerprint to identify a cert.
// This is probably a Bad Thing (TM) since SHA-1 is weak.
// However, that's Microsoft's problem to fix, not ours.
fingerprint := sha1.Sum(derBytes)
// Windows CryptoAPI uses a hex string to represent the fingerprint.
fingerprintHex := hex.EncodeToString(fingerprint[:])
// Windows CryptoAPI uses uppercase hex strings
fingerprintHexUpper := strings.ToUpper(fingerprintHex)
// Create the registry key in which we will store the cert.
// The 2nd result of CreateKey is openedExisting, which tells us if the cert already existed.
// This doesn't matter to us. If true, the "last modified" metadata won't update,
// but we delete and recreate the magic value inside it as a workaround.
certKey, _, err := registry.CreateKey(certStoreKey, fingerprintHexUpper, registry.ALL_ACCESS)
if err != nil {
log.Errorf("Couldn't create registry key for certificate: %s", err)
return
}
defer certKey.Close()
// Add a magic value which indicates that the certificate is a
// Namecoin cert. This will be used for deleting expired certs.
// However, we have to delete it before we create it, so that we make sure that the "last modified" metadata gets updated.
// If an error occurs during deletion, we ignore it, since it probably just means it wasn't there already.
_ = certKey.DeleteValue(cryptoApiMagicName)
err = certKey.SetDWordValue(cryptoApiMagicName, cryptoApiMagicValue)
if err != nil {
log.Errorf("Couldn't set magic registry value for certificate: %s", err)
return
}
// Create the registry value which holds the certificate.
err = certKey.SetBinaryValue("Blob", certBlob)
if err != nil {
log.Errorf("Couldn't set blob registry value for certificate: %s", err)
return
}
}
func cleanCertsCryptoApi() {
// Open up the cert store.
certStoreKey, err := registry.OpenKey(cryptoApiCertStoreRegistryBase, cryptoApiCertStoreRegistryKey, registry.ALL_ACCESS)
if err != nil {
log.Errorf("Couldn't open cert store: %s", err)
return
}
defer certStoreKey.Close()
// get all subkey names in the cert store
subKeys, err := certStoreKey.ReadSubKeyNames(0)
if err != nil {
log.Errorf("Couldn't list certs in cert store: %s", err)
return
}
// for all certs in the cert store
for _, subKeyName := range subKeys {
// Check if the cert is expired
expired, err := checkCertExpiredCryptoApi(certStoreKey, subKeyName)
if err != nil {
log.Errorf("Couldn't check if cert is expired: %s", err)
return
}
// delete the cert if it's expired
if expired {
registry.DeleteKey(certStoreKey, subKeyName)
}
}
}
func checkCertExpiredCryptoApi(certStoreKey registry.Key, subKeyName string) (bool, error) {
// Open the cert
certKey, err := registry.OpenKey(certStoreKey, subKeyName, registry.ALL_ACCESS)
if err != nil {
return false, fmt.Errorf("Couldn't open cert registry key: %s", err)
}
defer certKey.Close()
// Check for magic value
isNamecoin, _, err := certKey.GetIntegerValue(cryptoApiMagicName)
if err != nil {
// Magic value wasn't found. Therefore don't consider it expired.
return false, nil
}
if isNamecoin != cryptoApiMagicValue {
// Magic value was found but it wasn't the one we recognize. Therefore don't consider it expired.
return false, nil
}
// Get metadata about the cert key
certKeyInfo, err := certKey.Stat()
if err != nil {
return false, fmt.Errorf("Couldn't read metadata for cert registry key: %s", err)
}
// Get the last modified time
certKeyModTime := certKeyInfo.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(certKeyModTime).Seconds() ) > float64(certExpirePeriod.Value())
return expired, nil
}

@ -0,0 +1,17 @@
package certinject
import (
"encoding/pem"
"io/ioutil"
)
// Injects a certificate by writing to a file. Might be relevant for non-CryptoAPI trust stores.
func injectCertFile(derBytes []byte, fileName string) {
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
err := ioutil.WriteFile(fileName, pemBytes, 0644)
if err != nil {
log.Errore(err, "Error writing cert!")
return
}
}

@ -0,0 +1,274 @@
// Copyright 2009 The Go Authors, 2015-2016 Jeremy Rand. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Generate a self-signed X.509 certificate for a TLS server. Outputs to
// 'cert.pem' and 'key.pem' and will overwrite existing files.
// This code has been modified from the stock Go code to generate
// "dehydrated certificates", suitable for inclusion in a Namecoin name.
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
//"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"flag"
"fmt"
"log"
"math/big"
"os"
"time"
"github.com/namecoin/ncdns/certdehydrate"
"github.com/namecoin/ncdns/x509"
)
var (
host = flag.String("host", "", "Hostname to generate a certificate for (only use one)")
validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
validTo = flag.String("end-date", "", "End date formatted as Jan 1 15:04:05 2011")
ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521")
falseHost = flag.String("false-host", "", "(Optional) Generate a false cert for this host; used to test x.509 implementations for safety regarding handling of the CA flag and KeyUsage")
)
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}
func pemBlockForKey(priv interface{}) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to marshal ECDSA private key: %v", err)
os.Exit(2)
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
default:
return nil
}
}
func main() {
flag.Parse()
if len(*host) == 0 {
log.Fatalf("Missing required --host parameter")
}
if len(*validFrom) == 0 {
log.Fatalf("Missing required --start-date parameter")
}
if len(*validTo) == 0 {
log.Fatalf("Missing required --end-date parameter")
}
if len(*ecdsaCurve) == 0 {
log.Fatalf("Missing required --ecdsa-curve parameter")
}
var priv interface{}
var err error
switch *ecdsaCurve {
case "P224":
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
case "P256":
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case "P384":
priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
fmt.Fprintf(os.Stderr, "Unrecognized elliptic curve: %q", *ecdsaCurve)
os.Exit(1)
}
if err != nil {
log.Fatalf("failed to generate private key: %s", err)
}
var notBefore time.Time
notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err)
os.Exit(1)
}
var notAfter time.Time
notAfter, err = time.Parse("Jan 2 15:04:05 2006", *validTo)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse expiry date: %s\n", err)
os.Exit(1)
}
timestampPrecision := int64(5 * 60)
notBeforeFloored := time.Unix( ( notBefore.Unix() / timestampPrecision ) * timestampPrecision, 0 )
notAfterFloored := time.Unix( ( notAfter.Unix() / timestampPrecision ) * timestampPrecision, 0 )
// Serial components
pubkeyBytes, err := x509.MarshalPKIXPublicKey(publicKey(priv))
if err != nil {
log.Fatalf("failed to marshal public key: %s", err)
}
pubkeyB64 := base64.StdEncoding.EncodeToString(pubkeyBytes)
notBeforeScaled := notBeforeFloored.Unix() / timestampPrecision
notAfterScaled := notAfterFloored.Unix() / timestampPrecision
// Calculate serial
serialDehydrated := certdehydrate.DehydratedCertificate {
PubkeyB64: pubkeyB64,
NotBeforeScaled: notBeforeScaled,
NotAfterScaled: notAfterScaled,
}
serialNumber := big.NewInt(1)
serialNumberBytes, err := serialDehydrated.SerialNumber(*host)
if err != nil {
log.Fatalf("Error calculating serial number: %s", err)
}
serialNumber.SetBytes(serialNumberBytes)
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: *host,
SerialNumber: "Namecoin TLS Certificate",
},
NotBefore: notBeforeFloored,
NotAfter: notAfterFloored,
// x509.KeyUsageKeyEncipherment is used for RSA key exchange, but not DHE/ECDHE key exchange. Since everyone should be using ECDHE (due to forward secrecy), we disallow x509.KeyUsageKeyEncipherment in our template.
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
template.DNSNames = append(template.DNSNames, *host)
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
log.Fatalf("Failed to create certificate: %s", err)
}
certOut, err := os.Create("cert.pem")
if err != nil {
log.Fatalf("failed to open cert.pem for writing: %s", err)
}
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
log.Print("written cert.pem\n")
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Print("failed to open key.pem for writing:", err)
return
}
pem.Encode(keyOut, pemBlockForKey(priv))
keyOut.Close()
log.Print("written key.pem\n")
parsedResult, err := x509.ParseCertificate(derBytes)
if err != nil {
log.Fatal("failed to parse output cert: ", err)
}
dehydrated, err := certdehydrate.DehydrateCert(parsedResult)
if err != nil {
log.Fatal("failed to dehydrate result cert: ", err)
}
rehydrated, err := certdehydrate.RehydrateCert(dehydrated)
if err != nil {
log.Fatal("failed to rehydrate result cert: ", err)
}
rehydratedDerBytes, err := certdehydrate.FillRehydratedCertTemplate(*rehydrated, *host)
if err != nil {
log.Fatal("failed to fill rehydrated result cert: ", err)
}
if ! bytes.Equal(derBytes, rehydratedDerBytes) {
log.Fatal("ERROR: The cert did not rehydrate to an identical form. This is a bug; do not use the generated certificate.")
}
log.Print("Your Namecoin cert is: {\"d8\":", dehydrated, "}")
log.Print("SUCCESS: The cert rehydrated to an identical form. Place the generated files in your HTTPS server, and place the above JSON in the \"tls\" field for your Namecoin name.");
if len(*falseHost) > 0 {
var falsePriv interface{}
switch *ecdsaCurve {
case "P224":
falsePriv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
case "P256":
falsePriv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case "P384":
falsePriv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case "P521":
falsePriv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
fmt.Fprintf(os.Stderr, "Unrecognized elliptic curve: %q", *ecdsaCurve)
os.Exit(1)
}
if err != nil {
log.Fatalf("failed to generate false private key: %s", err)
}
falseSerialNumber := big.NewInt(2)
falseTemplate := x509.Certificate{
SerialNumber: falseSerialNumber,
Subject: pkix.Name{
CommonName: *falseHost,
SerialNumber: "Namecoin TLS Certificate",
},
NotBefore: notBefore,
NotAfter: notAfter,
// x509.KeyUsageKeyEncipherment is used for RSA key exchange, but not DHE/ECDHE key exchange. Since everyone should be using ECDHE (due to forward secrecy), we disallow x509.KeyUsageKeyEncipherment in our template.
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
falseTemplate.DNSNames = append(falseTemplate.DNSNames, *falseHost)
falseDerBytes, err := x509.CreateCertificate(rand.Reader, &falseTemplate, &template, publicKey(falsePriv), priv)
if err != nil {
log.Fatalf("Failed to create false certificate: %s", err)
}
falseCertOut, err := os.Create("falseCert.pem")
if err != nil {
log.Fatalf("failed to open falseCert.pem for writing: %s", err)
}
pem.Encode(falseCertOut, &pem.Block{Type: "CERTIFICATE", Bytes: falseDerBytes})
falseCertOut.Close()
log.Print("written falseCert.pem\n")
falseKeyOut, err := os.OpenFile("falseKey.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Print("failed to open falseKey.pem for writing:", err)
return
}
pem.Encode(falseKeyOut, pemBlockForKey(falsePriv))
falseKeyOut.Close()
log.Print("written falseKey.pem\n")
}
}

@ -10,6 +10,9 @@ import "github.com/namecoin/ncdns/util"
import "strings"
import "strconv"
import "github.com/namecoin/ncdns/x509"
import "github.com/namecoin/ncdns/certdehydrate"
const depthLimit = 16
const mergeDepthLimit = 4
const defaultTTL = 600
@ -41,6 +44,7 @@ type Value struct {
Hostmaster string // "hostmaster@example.com"
MX []*dns.MX // header name is left blank
TLSA []*dns.TLSA
TLSAGenerated []x509.Certificate // Certs can be dehydrated in the blockchain, they will be put here without SAN values. SAN must be filled in before use.
Map map[string]*Value // may contain and "*", will not contain ""
// set if the value is at the top level (alas necessary for relname interpretation)
@ -245,11 +249,39 @@ func (v *Value) appendSRVs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, e
return out, nil
}
func (v *Value) appendTLSA(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) {
for _, tlsa := range v.TLSA {
out = append(out, tlsa)
}
for _, cert := range v.TLSAGenerated {
template := cert
_, nameNoPort := util.SplitDomainTail(suffix)
_, nameNoPortOrProtocol := util.SplitDomainTail(nameNoPort)
derBytes, err := certdehydrate.FillRehydratedCertTemplate(template, nameNoPortOrProtocol)
if err != nil {
// TODO: add debug output here
continue
}
derBytesHex := hex.EncodeToString(derBytes)
out = append(out, &dns.TLSA{
Hdr: dns.RR_Header{Name: suffix, Rrtype: dns.TypeTLSA, Class: dns.ClassINET,
Ttl: defaultTTL},
Usage: uint8(3),
Selector: uint8(0),
MatchingType: uint8(0),
Certificate: strings.ToUpper(derBytesHex),
})
}
return out, nil
}
@ -817,6 +849,71 @@ func parseDS(rv map[string]interface{}, v *Value, errFunc ErrorFunc) {
errFunc.add(fmt.Errorf("malformed DS field format"))
}
func parseTLSADehydrated(tlsa1dehydrated interface{}, v *Value) error {
dehydrated, err := certdehydrate.ParseDehydratedCert(tlsa1dehydrated)
if err != nil {
return fmt.Errorf("Error parsing dehydrated certificate: %s", err)
}
template, err := certdehydrate.RehydrateCert(dehydrated)
if err != nil {
return fmt.Errorf("Error rehydrating certificate: %s", err)
}
v.TLSAGenerated = append(v.TLSAGenerated, *template)
return nil
}
func parseTLSADANE(tlsa1dane interface{}, v *Value) error {
if tlsa, ok := tlsa1dane.([]interface{}); ok {
// Format: ["443", "tcp", 1, 2, 3, "base64 certificate data"]
if len(tlsa) < 4 {
return fmt.Errorf("TLSA item must have six items")
}
a1, ok := tlsa[0].(float64)
if !ok {
return fmt.Errorf("Third item in TLSA value must be an integer (usage)")
}
a2, ok := tlsa[1].(float64)
if !ok {
return fmt.Errorf("Fourth item in TLSA value must be an integer (selector)")
}
a3, ok := tlsa[2].(float64)
if !ok {
return fmt.Errorf("Fifth item in TLSA value must be an integer (match type)")
}
a4, ok := tlsa[3].(string)
if !ok {
return fmt.Errorf("Sixth item in TLSA value must be a string (certificate)")
}
a4b, err := base64.StdEncoding.DecodeString(a4)
if err != nil {
return fmt.Errorf("Fourth item in DS value must be valid base64: %v", err)
}
a4h := hex.EncodeToString(a4b)
v.TLSA = append(v.TLSA, &dns.TLSA{
Hdr: dns.RR_Header{Name: "", Rrtype: dns.TypeTLSA, Class: dns.ClassINET,
Ttl: defaultTTL},
Usage: uint8(a1),
Selector: uint8(a2),
MatchingType: uint8(a3),
Certificate: strings.ToUpper(a4h),
})
return nil
} else {
return fmt.Errorf("TLSA item must be an array")
}
}
func parseTLSA(rv map[string]interface{}, v *Value, errFunc ErrorFunc) {
tlsa, ok := rv["tls"]
if !ok || tlsa == nil {
@ -827,56 +924,33 @@ func parseTLSA(rv map[string]interface{}, v *Value, errFunc ErrorFunc) {
if tlsaa, ok := tlsa.([]interface{}); ok {
for _, tlsa1 := range tlsaa {
if tlsa, ok := tlsa1.([]interface{}); ok {
// Format: ["443", "tcp", 1, 2, 3, "base64 certificate data"]
if len(tlsa) < 4 {
errFunc.add(fmt.Errorf("TLSA item must have six items"))
continue
}
a1, ok := tlsa[0].(float64)
if !ok {
errFunc.add(fmt.Errorf("Third item in TLSA value must be an integer (usage)"))
continue
}
a2, ok := tlsa[1].(float64)
if !ok {
errFunc.add(fmt.Errorf("Fourth item in TLSA value must be an integer (selector)"))
continue
}
var tlsa1m map[string]interface{}
a3, ok := tlsa[2].(float64)
if !ok {
errFunc.add(fmt.Errorf("Fifth item in TLSA value must be an integer (match type)"))
continue
if _, ok := tlsa1.([]interface{}); ok {
tlsa1m = map[string]interface{} {
"dane": tlsa1,
}
} else {
tlsa1m = tlsa1.(map[string]interface{})
}
a4, ok := tlsa[3].(string)
if !ok {
errFunc.add(fmt.Errorf("Sixth item in TLSA value must be a string (certificate)"))
if tlsa1dehydrated, ok := tlsa1m["d8"]; ok {
err := parseTLSADehydrated(tlsa1dehydrated, v)
if err == nil {
continue
}
errFunc.add(err)
}
a4b, err := base64.StdEncoding.DecodeString(a4)
if err != nil {
errFunc.add(fmt.Errorf("Fourth item in DS value must be valid base64: %v", err))
if tlsa1dane, ok := tlsa1m["dane"]; ok {
err := parseTLSADANE(tlsa1dane, v)
if err == nil {
continue
}
a4h := hex.EncodeToString(a4b)
v.TLSA = append(v.TLSA, &dns.TLSA{
Hdr: dns.RR_Header{Name: "", Rrtype: dns.TypeTLSA, Class: dns.ClassINET,
Ttl: defaultTTL},
Usage: uint8(a1),
Selector: uint8(a2),
MatchingType: uint8(a3),
Certificate: strings.ToUpper(a4h),
})
} else {
errFunc.add(fmt.Errorf("TLSA item must be an array"))
errFunc.add(err)
}
errFunc.add(fmt.Errorf("Unknown TLSA item format"))
}
return
}

@ -0,0 +1,127 @@
package tlshook
import (
"github.com/namecoin/ncdns/certdehydrate"
"github.com/namecoin/ncdns/certinject"
"github.com/namecoin/ncdns/ncdomain"
"github.com/namecoin/ncdns/x509"
"github.com/hlandau/xlog"
)
import "bytes"
import "encoding/hex"
var log, Log = xlog.New("ncdns.tlshook")
func DomainValueHookTLS(qname string, ncv *ncdomain.Value) (err error) {
log.Info("Intercepted a Value for ", qname)
if protocol, ok := ncv.Map["_tcp"]; ok { // TODO: look into allowing non-TCP protocols
log.Info("Saw a request with TCP")
if port, ok := protocol.Map["_443"]; ok { // TODO: check all ports, not just 443
log.Info("Saw a request with TCP port 443")
// For dehydrated certificates
if len(port.TLSAGenerated) > 0 {
log.Info("Just saw a TLS port 443 capable domain request for ", qname, "!")
for index, cert := range port.TLSAGenerated {
log.Info("Using dehydrated certificate # ", index)
template := cert
derBytes, err := certdehydrate.FillRehydratedCertTemplate(template, qname)
if err != nil {
log.Info("Failed to create certificate: ", err)
continue
}
// TODO: check return value
certinject.InjectCert(derBytes)
}
}
// For non-dehydrated certificates
// TODO: test this code.
// since this code has not been tested yet, it's disabled for safety reasons.
//if len(port.TLSA) > 0 {
if false {
log.Info("Just saw a TLS port 443 capable domain request for ", qname, "!")
for index, cert := range port.TLSA {
// cert usage 3 is end-entity cert that need not pass CA-based validation
// cert selector 0 is a full certificate (not just public key)
// cert matching type 0 is exact match (not hashed)
if cert.Usage == 3 && cert.Selector == 0 && cert.MatchingType == 0 {
log.Info("Certificate # ", index, " is usable with hex value ", cert.Certificate)
origCertBytes, err:= hex.DecodeString(cert.Certificate)
if err != nil {
log.Info("Failed to decode hex string of TLSA certificate, ", err)
continue
}
origCert, err := x509.ParseCertificate(origCertBytes)
if err != nil {
log.Info("Failed to parse TLSA certificate, ", err)
continue
}
// TODO: look into being a bit more flexible with cert serial number, validity period, and subject serial number.
// The uniformity in those fields is due to compression rather than security concerns.
// So we could possibly pass those through in cases like this.
// Subject serial number is also there due to transparency concerns, so maybe don't allow customizing it.
dehydrated, err := certdehydrate.DehydrateCert(origCert)
if err != nil {
log.Info("Failed to dehydrate TLSA certificate, ", err)
continue
}
rehydrated, err := certdehydrate.RehydrateCert(dehydrated)
if err != nil {
log.Info("Failed to rehydrate TLSA certificate, ", err)
continue
}
rehydratedDerBytes, err := certdehydrate.FillRehydratedCertTemplate(*rehydrated, qname)
if err != nil {
log.Info("Failed to fill rehydrated TLSA certificate, ", err)
continue
}
if ! bytes.Equal(origCertBytes, rehydratedDerBytes) {
log.Info("TLSA certificate didn't conform to dehydration template; skipping certificate.")
continue
}
// TODO: check return value
certinject.InjectCert(rehydratedDerBytes)
} else {
log.Info("Certificate # ", index, " is not usable because we cannot recover the full end-entity certificate from the TLSA record.")
}
}
}
}
}
// remove any certs that aren't valid anymore
certinject.CleanCerts()
err = nil
return
}

@ -0,0 +1,7 @@
#!/bin/bash
set -eu -o pipefail
shopt -s failglob
cp -a $(go env GOROOT)/src/crypto/x509/* ./
rm ./x509_test.go

@ -0,0 +1,120 @@
// Copyright 2009 The Go Authors, 2015-2016 Jeremy Rand. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This code is modified from the stock CreateCertificate to use a
// pre-existing signature.
// Last rebased on Go 1.6.2
//go:generate bash install.sh
// Package x509 parses X.509-encoded keys and certificates.
package x509
import (
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/asn1"
)
// CreateCertificate creates a new certificate based on a template. The
// following members of template are used: SerialNumber, Subject, NotBefore,
// NotAfter, KeyUsage, ExtKeyUsage, UnknownExtKeyUsage, BasicConstraintsValid,
// IsCA, MaxPathLen, SubjectKeyId, DNSNames, PermittedDNSDomainsCritical,
// PermittedDNSDomains, SignatureAlgorithm.
//
// The certificate is signed by parent. If parent is equal to template then the
// certificate is self-signed. The parameter pub is the public key of the
// signee and priv is the private key of the signer.
//
// The returned slice is the certificate in DER encoding.
//
// All keys types that are implemented via crypto.Signer are supported (This
// includes *rsa.PublicKey and *ecdsa.PublicKey.)
//func CreateCertificate(rand io.Reader, template, parent *Certificate, pub, priv interface{}) (cert []byte, err error) {
func CreateCertificateWithSplicedSignature(template, parent *Certificate) (cert []byte, err error) {
//key, ok := priv.(crypto.Signer)
//if !ok {
// return nil, errors.New("x509: certificate private key does not implement crypto.Signer")
//}
//hashFunc, signatureAlgorithm, err := signingParamsForPublicKey(key.Public(), template.SignatureAlgorithm)
//if err != nil {
// return nil, err
//}
// This block added
_, signatureAlgorithm, err := signingParamsForPublicKey(parent.PublicKey, template.SignatureAlgorithm)
if err != nil {
return nil, err
}
// This line added
pub := template.PublicKey
publicKeyBytes, publicKeyAlgorithm, err := marshalPublicKey(pub)
if err != nil {
return nil, err
}
if len(parent.SubjectKeyId) > 0 {
template.AuthorityKeyId = parent.SubjectKeyId
}
extensions, err := buildExtensions(template)
if err != nil {
return
}
asn1Issuer, err := subjectBytes(parent)
if err != nil {
return
}
asn1Subject, err := subjectBytes(template)
if err != nil {
return
}
encodedPublicKey := asn1.BitString{BitLength: len(publicKeyBytes) * 8, Bytes: publicKeyBytes}
c := tbsCertificate{
Version: 2,
SerialNumber: template.SerialNumber,
SignatureAlgorithm: signatureAlgorithm,
Issuer: asn1.RawValue{FullBytes: asn1Issuer},
Validity: validity{template.NotBefore.UTC(), template.NotAfter.UTC()},
Subject: asn1.RawValue{FullBytes: asn1Subject},
PublicKey: publicKeyInfo{nil, publicKeyAlgorithm, encodedPublicKey},
Extensions: extensions,
}
tbsCertContents, err := asn1.Marshal(c)
if err != nil {
return
}
c.Raw = tbsCertContents
//h := hashFunc.New()
//h.Write(tbsCertContents)
//digest := h.Sum(nil)
//var signature []byte
//signature, err = key.Sign(rand, digest, hashFunc)
//if err != nil {
// return
//}
// This line added
signature := template.Signature
return asn1.Marshal(certificate{
nil,
c,
signatureAlgorithm,
asn1.BitString{Bytes: signature, BitLength: len(signature) * 8},
})
}
Loading…
Cancel
Save