Skip to content

Commit

Permalink
Merge #16: CryptoAPI TLS certificate injection
Browse files Browse the repository at this point in the history
271a0c7 tlshook: Fix linter warning about shadowed variable. (JeremyRand)
978116d Travis: Disable gometalinter warnings on the portion of x509 that is copied verbatim from the Go standard library. (JeremyRand)
05afcd4 tlshook: Remove unused imports. (JeremyRand)
81fb477 tlshook: Removed commented-out code for non-dehydrated certificates; I plan to re-add that code once it's properly tested. (JeremyRand)
e16ad6f TLS dehydrated certificate injection for CryptoAPI trust store (triggered by hooking DNS lookups). (JeremyRand)

Pull request description:

  Add the ability to inject TLS certs into CryptoAPI's trust store before replying to DNS queries.

  Please review but do not merge yet.

  TODO before merging:

  - [x] Make the x509 build script use `go generate`.
  - [x] Make the x509 build script source `go env` and use `$GOROOT` from it.
  - [x] Update the `d/` spec to match the current dehydrated certificate format.  (It's changed slightly since I submitted the spec.)
  - [x] Look into using Errore instead of Fatal.
  - [x] Fix `.gitignore`.
  - [x] Squash commits.

Tree-SHA512: 1ce4e650e142aa1630f51b09497d85ad0626ae46ccc63c2e72fafa97d99bf340b3583db5ac76b5cc339228e56be8e9db673348d2d8f1f6173f1bca5306971629
  • Loading branch information
JeremyRand committed Jul 30, 2017
2 parents 6ae1223 + 271a0c7 commit dbc5d47
Show file tree
Hide file tree
Showing 15 changed files with 1,288 additions and 41 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions .travis/script
Expand Up @@ -14,6 +14,8 @@ fi

gometalinter.v1 --install

# The --exclude line disables warnings on the portion of x509 that is copied
# verbatim from the Go standard library.
echo ""
echo "gometalinter critical (should be no warnings):"
gometalinter.v1 --enable-all \
Expand All @@ -37,6 +39,7 @@ gometalinter.v1 --enable-all \
--disable=unused \
--concurrency=3 \
--deadline=10m \
--exclude='^x509/([a-wy-z]|x509.go|x509_[a-rt-z])' \
./...
STATICRESULT1=$?

Expand All @@ -45,6 +48,7 @@ echo "gometalinter non-critical (warnings expected):"
gometalinter.v1 --enable-all \
--concurrency=3 \
--deadline=10m \
--exclude='^x509/([a-wy-z]|x509.go|x509_[a-rt-z])' \
./...
STATICRESULT2=$?

Expand Down
168 changes: 168 additions & 0 deletions BorderlessBlockParty2015.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions backend/backend.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
236 changes: 236 additions & 0 deletions certdehydrate/certdehydrate.go
@@ -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

}
39 changes: 39 additions & 0 deletions certdehydrate/certdehydrate_test.go
@@ -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)
}
}

15 changes: 15 additions & 0 deletions certinject/certinject_misc.go
@@ -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() {

}
36 changes: 36 additions & 0 deletions certinject/certinject_windows.go
@@ -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 comments on commit dbc5d47

Please sign in to comment.