Skip to content

Commit dbc5d47

Browse files
committed
Merge #16: CryptoAPI TLS certificate injection
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
2 parents 6ae1223 + 271a0c7 commit dbc5d47

15 files changed

+1288
-41
lines changed

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ addons:
1414
sudo: false
1515

1616
install:
17+
- go generate -v ./...
1718
- go get -v -t ./...
1819
- env GOOS=windows GOARCH=amd64 go get -d -v -t ./...
1920
script:

.travis/script

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ fi
1414

1515
gometalinter.v1 --install
1616

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

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

BorderlessBlockParty2015.md

+168
Large diffs are not rendered by default.

backend/backend.go

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "gopkg.in/hlandau/madns.v1/merr"
66
import "github.com/namecoin/ncdns/namecoin"
77
import "github.com/namecoin/ncdns/util"
88
import "github.com/namecoin/ncdns/ncdomain"
9+
import "github.com/namecoin/ncdns/tlshook"
910
import "github.com/hlandau/xlog"
1011
import "sync"
1112
import "fmt"
@@ -421,6 +422,12 @@ func (tx *btx) _findNCValue(ncv *ncdomain.Value, isubname, subname string, depth
421422

422423
func (tx *btx) addAnswersUnderNCValueActual(ncv *ncdomain.Value, sn string) (rrs []dns.RR, err error) {
423424
rrs, err = ncv.RRs(nil, dns.Fqdn(tx.qname), dns.Fqdn(tx.basename+"."+tx.rootname))
425+
426+
// TODO: add callback variable "OnValueReferencedFunc" to backend options so that we don't pollute this function with every hook that we want
427+
// might need to add the other attributes of tx, and sn, to the callback variable for flexibility's sake
428+
// This doesn't normally return errors, but any errors during execution will be logged.
429+
tlshook.DomainValueHookTLS(tx.qname, ncv)
430+
424431
return
425432
}
426433

certdehydrate/certdehydrate.go

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package certdehydrate
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"crypto/x509/pkix"
7+
"encoding/base64"
8+
"encoding/binary"
9+
"encoding/json"
10+
"fmt"
11+
"math/big"
12+
"time"
13+
)
14+
15+
import "github.com/namecoin/ncdns/x509"
16+
17+
// TODO: add a version field
18+
type DehydratedCertificate struct {
19+
PubkeyB64 string
20+
NotBeforeScaled int64
21+
NotAfterScaled int64
22+
SignatureAlgorithm int64
23+
SignatureB64 string
24+
}
25+
26+
func (dehydrated DehydratedCertificate) SerialNumber(name string) ([]byte, error){
27+
28+
nameHash := sha256.Sum256([]byte(name))
29+
30+
pubkeyBytes, err := base64.StdEncoding.DecodeString(dehydrated.PubkeyB64)
31+
if err != nil {
32+
return nil, fmt.Errorf("Dehydrated cert pubkey is not valid base64: %s", err)
33+
}
34+
pubkeyHash := sha256.Sum256(pubkeyBytes)
35+
36+
notBeforeScaledBuf := new(bytes.Buffer)
37+
err = binary.Write(notBeforeScaledBuf, binary.BigEndian, dehydrated.NotBeforeScaled)
38+
if err != nil {
39+
return nil, fmt.Errorf("binary.Write of notBefore failed: %s", err)
40+
}
41+
notBeforeHash := sha256.Sum256(notBeforeScaledBuf.Bytes())
42+
43+
notAfterScaledBuf := new(bytes.Buffer)
44+
err = binary.Write(notAfterScaledBuf, binary.BigEndian, dehydrated.NotAfterScaled)
45+
if err != nil {
46+
return nil, fmt.Errorf("binary.Write of notAfter failed: %s", err)
47+
}
48+
notAfterHash := sha256.Sum256(notAfterScaledBuf.Bytes())
49+
50+
serialHash := sha256.New()
51+
serialHash.Write(nameHash[:])
52+
serialHash.Write(pubkeyHash[:])
53+
serialHash.Write(notBeforeHash[:])
54+
serialHash.Write(notAfterHash[:])
55+
56+
// 19 bytes will be less than 2^159, see https://crypto.stackexchange.com/a/260
57+
return serialHash.Sum(nil)[0:19], nil
58+
}
59+
60+
func (dehydrated DehydratedCertificate) String() string {
61+
output := []interface{}{1, dehydrated.PubkeyB64, dehydrated.NotBeforeScaled, dehydrated.NotAfterScaled, dehydrated.SignatureAlgorithm, dehydrated.SignatureB64}
62+
binOutput, _ := json.Marshal(output)
63+
return string(binOutput)
64+
}
65+
66+
func ParseDehydratedCert(data interface{}) (*DehydratedCertificate, error) {
67+
dehydrated, ok := data.([]interface{})
68+
if !ok {
69+
return nil, fmt.Errorf("Dehydrated cert is not a list")
70+
}
71+
72+
if len(dehydrated) < 1 {
73+
return nil, fmt.Errorf("Dehydrated cert must have a version field")
74+
}
75+
76+
version, ok := dehydrated[0].(float64)
77+
if !ok {
78+
return nil, fmt.Errorf("Dehydrated cert version must be an integer")
79+
}
80+
81+
if version != 1 {
82+
return nil, fmt.Errorf("Dehydrated cert has an unrecognized version")
83+
}
84+
85+
if len(dehydrated) < 6 {
86+
return nil, fmt.Errorf("Dehydrated cert must have 6 items")
87+
}
88+
89+
pubkeyB64, ok := dehydrated[1].(string)
90+
if !ok {
91+
return nil, fmt.Errorf("Dehydrated cert pubkey must be a string")
92+
}
93+
94+
notBeforeScaled, ok := dehydrated[2].(float64)
95+
if !ok {
96+
return nil, fmt.Errorf("Dehydrated cert notBefore must be an integer")
97+
}
98+
99+
notAfterScaled, ok := dehydrated[3].(float64)
100+
if !ok {
101+
return nil, fmt.Errorf("Dehydrated cert notAfter must be an integer")
102+
}
103+
104+
signatureAlgorithm, ok := dehydrated[4].(float64)
105+
if !ok {
106+
return nil, fmt.Errorf("Dehydrated cert signature algorithm must be an integer")
107+
}
108+
109+
signatureB64, ok := dehydrated[5].(string)
110+
if !ok {
111+
return nil, fmt.Errorf("Dehydrated cert signature must be a string")
112+
}
113+
114+
result := DehydratedCertificate {
115+
PubkeyB64: pubkeyB64,
116+
NotBeforeScaled: int64(notBeforeScaled),
117+
NotAfterScaled: int64(notAfterScaled),
118+
SignatureAlgorithm: int64(signatureAlgorithm),
119+
SignatureB64: signatureB64,
120+
}
121+
122+
return &result, nil
123+
}
124+
125+
func DehydrateCert(cert *x509.Certificate) (*DehydratedCertificate, error) {
126+
127+
pubkeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
128+
if err != nil {
129+
return nil, fmt.Errorf("failed to marshal parsed public key: %s", err)
130+
}
131+
132+
pubkeyB64 := base64.StdEncoding.EncodeToString(pubkeyBytes)
133+
134+
notBeforeInt := cert.NotBefore.Unix()
135+
notAfterInt := cert.NotAfter.Unix()
136+
137+
timestampPrecision := int64(5 * 60) // 5 minute precision
138+
139+
notBeforeScaled := notBeforeInt / timestampPrecision
140+
notAfterScaled := notAfterInt / timestampPrecision
141+
142+
signatureAlgorithm := int64(cert.SignatureAlgorithm)
143+
signatureBytes := cert.Signature
144+
signatureB64 := base64.StdEncoding.EncodeToString(signatureBytes)
145+
146+
result := DehydratedCertificate{
147+
PubkeyB64: pubkeyB64,
148+
NotBeforeScaled: notBeforeScaled,
149+
NotAfterScaled: notAfterScaled,
150+
SignatureAlgorithm: signatureAlgorithm,
151+
SignatureB64: signatureB64,
152+
}
153+
154+
return &result, nil
155+
}
156+
157+
// Accepts as input the bare minimum data needed to produce a valid cert.
158+
// The input is untrusted.
159+
// The output is safe.
160+
// The timestamps are in 5-minute increments.
161+
func RehydrateCert(dehydrated *DehydratedCertificate) (*x509.Certificate, error) {
162+
163+
pubkeyBin, err := base64.StdEncoding.DecodeString(dehydrated.PubkeyB64)
164+
if err != nil {
165+
return nil, fmt.Errorf("Dehydrated cert pubkey must be valid base64: %s", err)
166+
}
167+
168+
pubkey, err := x509.ParsePKIXPublicKey(pubkeyBin)
169+
if err != nil {
170+
return nil, fmt.Errorf("Dehydrated cert pubkey is invalid: %s", err)
171+
}
172+
173+
timestampPrecision := int64(5 * 60) // 5 minute precision
174+
175+
notBeforeInt := dehydrated.NotBeforeScaled * timestampPrecision
176+
notAfterInt := dehydrated.NotAfterScaled * timestampPrecision
177+
178+
notBefore := time.Unix(int64(notBeforeInt), 0)
179+
notAfter := time.Unix(int64(notAfterInt), 0)
180+
181+
signatureAlgorithm := x509.SignatureAlgorithm(dehydrated.SignatureAlgorithm)
182+
183+
signature, err := base64.StdEncoding.DecodeString(dehydrated.SignatureB64)
184+
if err != nil {
185+
return nil, fmt.Errorf("Dehydrated cert signature must be valid base64: %s", err)
186+
}
187+
188+
template := x509.Certificate{
189+
SerialNumber: big.NewInt(1),
190+
NotBefore: notBefore,
191+
NotAfter: notAfter,
192+
193+
// 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.
194+
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
195+
KeyUsage: x509.KeyUsageDigitalSignature,
196+
197+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
198+
BasicConstraintsValid: true,
199+
200+
SignatureAlgorithm: signatureAlgorithm,
201+
PublicKey: pubkey,
202+
Signature: signature,
203+
}
204+
205+
return &template, nil
206+
}
207+
208+
func FillRehydratedCertTemplate(template x509.Certificate, name string) ([]byte, error) {
209+
210+
template.Subject = pkix.Name{
211+
CommonName: name,
212+
SerialNumber: "Namecoin TLS Certificate",
213+
}
214+
215+
// DNS name
216+
template.DNSNames = append(template.DNSNames, name)
217+
218+
// Serial number
219+
dehydrated, err := DehydrateCert(&template)
220+
if err != nil {
221+
return nil, fmt.Errorf("Error dehydrating filled cert template: %s", err)
222+
}
223+
serialNumberBytes, err := dehydrated.SerialNumber(name)
224+
if err != nil {
225+
return nil, fmt.Errorf("Error calculating serial number: %s", err)
226+
}
227+
template.SerialNumber.SetBytes(serialNumberBytes)
228+
229+
derBytes, err := x509.CreateCertificateWithSplicedSignature(&template, &template)
230+
if err != nil {
231+
return nil, fmt.Errorf("Error splicing signature: %s", err)
232+
}
233+
234+
return derBytes, nil
235+
236+
}

certdehydrate/certdehydrate_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package certdehydrate_test
2+
3+
import (
4+
"testing"
5+
"encoding/json"
6+
"reflect"
7+
"github.com/namecoin/ncdns/certdehydrate"
8+
)
9+
10+
func TestDehydratedCertIdentityOperation(t *testing.T) {
11+
bytesJson := []byte(`[1, "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/hy1t4jB14ronx6n1m8VQh02jblRfu2cV3/LcyomfVljypUQMGjmuxWNbPI0a3cF6miNOijSCutqTZdb7TLvig==",4944096,5049216,10,"MEQCIGXXk6gYx95vQoknRwiQ4e27I+DXUWkE8L6dmLwAiGncAiBbtEX1nnZINx1YGzT5Fx8SxpjLwNDTUBkq22NpazHLIA=="]`)
12+
13+
var parsedJson []interface{}
14+
15+
if err := json.Unmarshal(bytesJson, &parsedJson); err != nil {
16+
t.Error("Error parsing JSON:", err)
17+
}
18+
19+
dehydrated, err := certdehydrate.ParseDehydratedCert(parsedJson)
20+
if err != nil {
21+
t.Error("Error parsing dehydrated certificate:", err)
22+
}
23+
24+
template, err := certdehydrate.RehydrateCert(dehydrated)
25+
if err != nil {
26+
t.Error("Error rehydrating certificate:", err)
27+
}
28+
29+
dehydrated2, err := certdehydrate.DehydrateCert(template)
30+
if err != nil {
31+
t.Error("Error dehydrating certificate:", err)
32+
}
33+
34+
// Test to make sure that rehydrating and then dehydrating a cert doesn't change it.
35+
if !reflect.DeepEqual(dehydrated, dehydrated2) {
36+
t.Error(dehydrated, "!=", dehydrated2)
37+
}
38+
}
39+

certinject/certinject_misc.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// +build !windows
2+
3+
package certinject
4+
5+
import "github.com/hlandau/xlog"
6+
7+
var log, Log = xlog.New("ncdns.certinject")
8+
9+
func InjectCert(derBytes []byte) {
10+
11+
}
12+
13+
func CleanCerts() {
14+
15+
}

certinject/certinject_windows.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package certinject
2+
3+
import (
4+
"gopkg.in/hlandau/easyconfig.v1/cflag"
5+
"github.com/hlandau/xlog"
6+
)
7+
8+
9+
// This package is used to add and remove certificates to the system trust
10+
// store.
11+
// Currently only supports Windows CryptoAPI.
12+
13+
var log, Log = xlog.New("ncdns.certinject")
14+
15+
var (
16+
flagGroup = cflag.NewGroup(nil, "certstore")
17+
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.")
18+
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.")
19+
)
20+
21+
// Injects the given cert into all configured trust stores.
22+
func InjectCert(derBytes []byte) {
23+
24+
if cryptoApiFlag.Value() {
25+
injectCertCryptoApi(derBytes)
26+
}
27+
}
28+
29+
// Cleans expired certs from all configured trust stores.
30+
func CleanCerts() {
31+
32+
if cryptoApiFlag.Value() {
33+
cleanCertsCryptoApi()
34+
}
35+
36+
}

0 commit comments

Comments
 (0)