-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
15 changed files
with
1,288 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
|
||
} |
Oops, something went wrong.