diff --git a/.travis.yml b/.travis.yml index bb438884..cb0aabc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: go sudo: false go: - - 1.11.x + - 1.12.x - 1.x os: - linux @@ -38,7 +38,7 @@ matrix: # Release build configuration - name: release go: - - 1.11.x + - 1.12.x os: - linux @@ -66,7 +66,7 @@ matrix: - name: docker if: type != pull_request AND (branch = master OR tag IS present) go: - - 1.11.x + - 1.12.x os: - linux services: diff --git a/README.md b/README.md index 054fc951..84f1a194 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/ You will need: - * [go](https://golang.org/dl/) v1.11 or later. + * [go](https://golang.org/dl/) v1.12 or later. * [node.js](https://nodejs.org/en/download/) v10 or later. You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac: diff --git a/app.go b/app.go index a9ced17f..18d45b2e 100644 --- a/app.go +++ b/app.go @@ -69,6 +69,9 @@ func run(args options) { // configure log level and output configureLogger(args) + // enable TLS 1.3 + enableTLS13() + // print the first message after logger is configured log.Printf("AdGuard Home, version %s\n", VersionString) log.Debug("Current working directory is %s", config.ourWorkingDir) @@ -176,13 +179,13 @@ func run(args options) { } address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS)) // validate current TLS config and update warnings (it could have been loaded from file) - data := validateCertificates(config.TLS) - if !data.usable { + data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName) + if !data.ValidPair { log.Fatal(data.WarningValidation) os.Exit(1) } config.Lock() - config.TLS = data // update warnings + config.TLS.tlsConfigStatus = data // update warnings config.Unlock() // prepare certs for HTTPS server @@ -293,6 +296,14 @@ func configureLogger(args options) { } } +// TODO after GO 1.13 release TLS 1.3 will be enabled by default. Remove this afterward +func enableTLS13() { + err := os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1") + if err != nil { + log.Fatalf("Failed to enable TLS 1.3: %s", err) + } +} + func cleanup() { log.Info("Stopping AdGuard Home") diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js index f4b01560..fd43f933 100644 --- a/client/src/components/Settings/Encryption/Form.js +++ b/client/src/components/Settings/Encryption/Form.js @@ -57,6 +57,7 @@ let Form = (props) => { valid_chain, valid_key, valid_cert, + valid_pair, dns_names, key_type, issuer, @@ -65,6 +66,15 @@ let Form = (props) => { setTlsConfig, } = props; + const isSavingDisabled = invalid + || submitting + || processingConfig + || processingValidate + || (isEnabled && (!privateKey || !certificateChain)) + || (privateKey && !valid_key) + || (certificateChain && !valid_cert) + || (privateKey && certificateChain && !valid_pair); + return (
@@ -291,15 +301,7 @@ let Form = (props) => { @@ -334,6 +336,7 @@ Form.propTypes = { valid_chain: PropTypes.bool, valid_key: PropTypes.bool, valid_cert: PropTypes.bool, + valid_pair: PropTypes.bool, dns_names: PropTypes.string, key_type: PropTypes.string, issuer: PropTypes.string, diff --git a/client/src/reducers/encryption.js b/client/src/reducers/encryption.js index 3f51b217..f861701b 100644 --- a/client/src/reducers/encryption.js +++ b/client/src/reducers/encryption.js @@ -70,6 +70,7 @@ const encryption = handleActions({ valid_chain: false, valid_key: false, valid_cert: false, + valid_pair: false, status_cert: '', status_key: '', certificate_chain: '', diff --git a/config.go b/config.go index d6178c38..706dff25 100644 --- a/config.go +++ b/config.go @@ -88,7 +88,7 @@ type tlsConfigStatus struct { KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA // is usable? set by validator - usable bool + ValidPair bool `yaml:"-" json:"valid_pair"` // ValidPair is true if both certificate and private key are correct // warnings WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description diff --git a/control.go b/control.go index fe340ecd..d6e5cbe0 100644 --- a/control.go +++ b/control.go @@ -3,21 +3,12 @@ package main import ( "bytes" "context" - "crypto" - "crypto/ecdsa" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/base64" "encoding/json" - "encoding/pem" - "errors" "fmt" "io/ioutil" "net" "net/http" "os" - "reflect" "sort" "strconv" "strings" @@ -26,7 +17,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" - "github.com/joomcode/errorx" "github.com/miekg/dns" govalidator "gopkg.in/asaskevich/govalidator.v4" ) @@ -993,295 +983,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) { } } -// --- -// TLS -// --- -func handleTLSStatus(w http.ResponseWriter, r *http.Request) { - log.Tracef("%s %v", r.Method, r.URL) - marshalTLS(w, config.TLS) -} - -func handleTLSValidate(w http.ResponseWriter, r *http.Request) { - log.Tracef("%s %v", r.Method, r.URL) - data, err := unmarshalTLS(r) - if err != nil { - httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err) - return - } - - // check if port is available - // BUT: if we are already using this port, no need - alreadyRunning := false - if httpsServer.server != nil { - alreadyRunning = true - } - if !alreadyRunning { - err = checkPortAvailable(config.BindHost, data.PortHTTPS) - if err != nil { - httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS) - return - } - } - - data = validateCertificates(data) - marshalTLS(w, data) -} - -func handleTLSConfigure(w http.ResponseWriter, r *http.Request) { - log.Tracef("%s %v", r.Method, r.URL) - data, err := unmarshalTLS(r) - if err != nil { - httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err) - return - } - - // check if port is available - // BUT: if we are already using this port, no need - alreadyRunning := false - if httpsServer.server != nil { - alreadyRunning = true - } - if !alreadyRunning { - err = checkPortAvailable(config.BindHost, data.PortHTTPS) - if err != nil { - httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS) - return - } - } - - restartHTTPS := false - data = validateCertificates(data) - if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) { - log.Printf("tls config settings have changed, will restart HTTPS server") - restartHTTPS = true - } - config.TLS = data - err = writeAllConfigsAndReloadDNS() - if err != nil { - httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err) - return - } - marshalTLS(w, data) - // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block - // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely - if restartHTTPS { - go func() { - time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server - httpsServer.cond.L.Lock() - httpsServer.cond.Broadcast() - if httpsServer.server != nil { - httpsServer.server.Shutdown(context.TODO()) - } - httpsServer.cond.L.Unlock() - }() - } -} - -func validateCertificates(data tlsConfig) tlsConfig { - var err error - - // clear out status for certificates - data.tlsConfigStatus = tlsConfigStatus{} - - // check only public certificate separately from the key - if data.CertificateChain != "" { - log.Tracef("got certificate: %s", data.CertificateChain) - - // now do a more extended validation - var certs []*pem.Block // PEM-encoded certificates - var skippedBytes []string // skipped bytes - - pemblock := []byte(data.CertificateChain) - for { - var decoded *pem.Block - decoded, pemblock = pem.Decode(pemblock) - if decoded == nil { - break - } - if decoded.Type == "CERTIFICATE" { - certs = append(certs, decoded) - } else { - skippedBytes = append(skippedBytes, decoded.Type) - } - } - - var parsedCerts []*x509.Certificate - - for _, cert := range certs { - parsed, err := x509.ParseCertificate(cert.Bytes) - if err != nil { - data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err) - return data - } - parsedCerts = append(parsedCerts, parsed) - } - - if len(parsedCerts) == 0 { - data.WarningValidation = fmt.Sprintf("You have specified an empty certificate") - return data - } - - data.ValidCert = true - - // spew.Dump(parsedCerts) - - opts := x509.VerifyOptions{ - DNSName: data.ServerName, - } - - log.Printf("number of certs - %d", len(parsedCerts)) - if len(parsedCerts) > 1 { - // set up an intermediate - pool := x509.NewCertPool() - for _, cert := range parsedCerts[1:] { - log.Printf("got an intermediate cert") - pool.AddCert(cert) - } - opts.Intermediates = pool - } - - // TODO: save it as a warning rather than error it out -- shouldn't be a big problem - mainCert := parsedCerts[0] - _, err := mainCert.Verify(opts) - if err != nil { - // let self-signed certs through - data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err) - } else { - data.ValidChain = true - } - // spew.Dump(chains) - - // update status - if mainCert != nil { - notAfter := mainCert.NotAfter - data.Subject = mainCert.Subject.String() - data.Issuer = mainCert.Issuer.String() - data.NotAfter = notAfter - data.NotBefore = mainCert.NotBefore - data.DNSNames = mainCert.DNSNames - } - } - - // validate private key (right now the only validation possible is just parsing it) - if data.PrivateKey != "" { - // now do a more extended validation - var key *pem.Block // PEM-encoded certificates - var skippedBytes []string // skipped bytes - - // go through all pem blocks, but take first valid pem block and drop the rest - pemblock := []byte(data.PrivateKey) - for { - var decoded *pem.Block - decoded, pemblock = pem.Decode(pemblock) - if decoded == nil { - break - } - if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") { - key = decoded - break - } else { - skippedBytes = append(skippedBytes, decoded.Type) - } - } - - if key == nil { - data.WarningValidation = "No valid keys were found" - return data - } - - // parse the decoded key - _, keytype, err := parsePrivateKey(key.Bytes) - if err != nil { - data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err) - return data - } - - data.ValidKey = true - data.KeyType = keytype - } - - // if both are set, validate both in unison - if data.PrivateKey != "" && data.CertificateChain != "" { - _, err = tls.X509KeyPair([]byte(data.CertificateChain), []byte(data.PrivateKey)) - if err != nil { - data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err) - return data - } - data.usable = true - } - - return data -} - -// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates -// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys. -// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three. -func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) { - if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { - return key, "RSA", nil - } - if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { - switch key := key.(type) { - case *rsa.PrivateKey: - return key, "RSA", nil - case *ecdsa.PrivateKey: - return key, "ECDSA", nil - default: - return nil, "", errors.New("tls: found unknown private key type in PKCS#8 wrapping") - } - } - if key, err := x509.ParseECPrivateKey(der); err == nil { - return key, "ECDSA", nil - } - - return nil, "", errors.New("tls: failed to parse private key") -} - -// unmarshalTLS handles base64-encoded certificates transparently -func unmarshalTLS(r *http.Request) (tlsConfig, error) { - data := tlsConfig{} - err := json.NewDecoder(r.Body).Decode(&data) - if err != nil { - return data, errorx.Decorate(err, "Failed to parse new TLS config json") - } - - if data.CertificateChain != "" { - certPEM, err := base64.StdEncoding.DecodeString(data.CertificateChain) - if err != nil { - return data, errorx.Decorate(err, "Failed to base64-decode certificate chain") - } - data.CertificateChain = string(certPEM) - } - - if data.PrivateKey != "" { - keyPEM, err := base64.StdEncoding.DecodeString(data.PrivateKey) - if err != nil { - return data, errorx.Decorate(err, "Failed to base64-decode private key") - } - - data.PrivateKey = string(keyPEM) - } - - return data, nil -} - -func marshalTLS(w http.ResponseWriter, data tlsConfig) { - w.Header().Set("Content-Type", "application/json") - if data.CertificateChain != "" { - encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain)) - data.CertificateChain = encoded - } - if data.PrivateKey != "" { - encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey)) - data.PrivateKey = encoded - } - err := json.NewEncoder(w).Encode(data) - if err != nil { - httpError(w, http.StatusInternalServerError, "Failed to marshal json with TLS status: %s", err) - return - } -} - // -------------- // DNS-over-HTTPS // -------------- @@ -1347,9 +1048,7 @@ func registerControlHandlers() { http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig)))) http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer)))) - http.HandleFunc("/control/tls/status", postInstall(optionalAuth(ensureGET(handleTLSStatus)))) - http.HandleFunc("/control/tls/configure", postInstall(optionalAuth(ensurePOST(handleTLSConfigure)))) - http.HandleFunc("/control/tls/validate", postInstall(optionalAuth(ensurePOST(handleTLSValidate)))) + RegisterTLSHandlers() http.HandleFunc("/dns-query", postInstall(handleDOH)) } diff --git a/control_test.go b/control_test.go new file mode 100644 index 00000000..b823b252 --- /dev/null +++ b/control_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "testing" + "time" +) + +/* Tests performed: +. Bad certificate +. Bad private key +. Valid certificate & private key */ +func TestValidateCertificates(t *testing.T) { + var data tlsConfigStatus + + // bad cert + data = validateCertificates("bad cert", "", "") + if !(data.WarningValidation != "" && + !data.ValidCert && + !data.ValidChain) { + t.Fatalf("bad cert: validateCertificates(): %v", data) + } + + // bad priv key + data = validateCertificates("", "bad priv key", "") + if !(data.WarningValidation != "" && + !data.ValidKey) { + t.Fatalf("bad priv key: validateCertificates(): %v", data) + } + + // valid cert & priv key + CertificateChain := `-----BEGIN CERTIFICATE----- +MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV +BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3 +MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV +MBMGA1UEAwwMQWRHdWFyZCBIb21lMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQCwvwUnPJiOvLcOaWmGu6Y68ksFr13nrXBcsDlhxlXy8PaohVi3XxEmt2OrVjKW +QFw/bdV4fZ9tdWFAVRRkgeGbIZzP7YBD1Ore/O5SQ+DbCCEafvjJCcXQIrTeKFE6 +i9G3aSMHs0Pwq2LgV8U5mYotLrvyFiE8QPInJbDDMpaFYwIDAQABo1MwUTAdBgNV +HQ4EFgQUdLUmQpEqrhn4eKO029jYd2AAZEQwHwYDVR0jBBgwFoAUdLUmQpEqrhn4 +eKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8 +LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna +b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq +Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ== +-----END CERTIFICATE-----` + PrivateKey := `-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p +aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV +FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX +xTmZii0uu/IWITxA8iclsMMyloVjAgMBAAECgYEAmjzoG1h27UDkIlB9BVWl95TP +QVPLB81D267xNFDnWk1Lgr5zL/pnNjkdYjyjgpkBp1yKyE4gHV4skv5sAFWTcOCU +QCgfPfUn/rDFcxVzAdJVWAa/CpJNaZgjTPR8NTGU+Ztod+wfBESNCP5tbnuw0GbL +MuwdLQJGbzeJYpsNysECQQDfFHYoRNfgxHwMbX24GCoNZIgk12uDmGTA9CS5E+72 +9t3V1y4CfXxSkfhqNbd5RWrUBRLEw9BKofBS7L9NMDKDAkEAytQoIueE1vqEAaRg +a3A1YDUekKesU5wKfKfKlXvNgB7Hwh4HuvoQS9RCvVhf/60Dvq8KSu6hSjkFRquj +FQ5roQJBAMwKwyiCD5MfJPeZDmzcbVpiocRQ5Z4wPbffl9dRTDnIA5AciZDthlFg +An/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp +O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG +O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU +kXS9jgARhhiWXJrk +-----END PRIVATE KEY-----` + data = validateCertificates(CertificateChain, PrivateKey, "") + notBefore, _ := time.Parse(time.RFC3339, "2019-02-27T09:24:23Z") + notAfter, _ := time.Parse(time.RFC3339, "2046-07-14T09:24:23Z") + if !(data.WarningValidation != "" /* self signed */ && + data.ValidCert && + !data.ValidChain && + data.ValidKey && + data.KeyType == "RSA" && + data.Subject == "CN=AdGuard Home,O=AdGuard Ltd" && + data.Issuer == "CN=AdGuard Home,O=AdGuard Ltd" && + data.NotBefore == notBefore && + data.NotAfter == notAfter && + // data.DNSNames[0] == && + data.ValidPair) { + t.Fatalf("valid cert & priv key: validateCertificates(): %v", data) + } +} diff --git a/control_tls.go b/control_tls.go new file mode 100644 index 00000000..0546c5f1 --- /dev/null +++ b/control_tls.go @@ -0,0 +1,338 @@ +// Control: TLS configuring handlers + +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + "time" + + "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" +) + +// RegisterTLSHandlers registers HTTP handlers for TLS configuration +func RegisterTLSHandlers() { + http.HandleFunc("/control/tls/status", postInstall(optionalAuth(ensureGET(handleTLSStatus)))) + http.HandleFunc("/control/tls/configure", postInstall(optionalAuth(ensurePOST(handleTLSConfigure)))) + http.HandleFunc("/control/tls/validate", postInstall(optionalAuth(ensurePOST(handleTLSValidate)))) +} + +func handleTLSStatus(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + marshalTLS(w, config.TLS) +} + +func handleTLSValidate(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + data, err := unmarshalTLS(r) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err) + return + } + + // check if port is available + // BUT: if we are already using this port, no need + alreadyRunning := false + if httpsServer.server != nil { + alreadyRunning = true + } + if !alreadyRunning { + err = checkPortAvailable(config.BindHost, data.PortHTTPS) + if err != nil { + httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS) + return + } + } + + data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName) + marshalTLS(w, data) +} + +func handleTLSConfigure(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + data, err := unmarshalTLS(r) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err) + return + } + + // check if port is available + // BUT: if we are already using this port, no need + alreadyRunning := false + if httpsServer.server != nil { + alreadyRunning = true + } + if !alreadyRunning { + err = checkPortAvailable(config.BindHost, data.PortHTTPS) + if err != nil { + httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS) + return + } + } + + restartHTTPS := false + data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName) + if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) { + log.Printf("tls config settings have changed, will restart HTTPS server") + restartHTTPS = true + } + config.TLS = data + err = writeAllConfigsAndReloadDNS() + if err != nil { + httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err) + return + } + marshalTLS(w, data) + // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block + // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely + if restartHTTPS { + go func() { + time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server + httpsServer.cond.L.Lock() + httpsServer.cond.Broadcast() + if httpsServer.server != nil { + httpsServer.server.Shutdown(context.TODO()) + } + httpsServer.cond.L.Unlock() + }() + } +} + +func verifyCertChain(data *tlsConfigStatus, certChain string, serverName string) error { + log.Tracef("got certificate: %s", certChain) + + // now do a more extended validation + var certs []*pem.Block // PEM-encoded certificates + var skippedBytes []string // skipped bytes + + pemblock := []byte(certChain) + for { + var decoded *pem.Block + decoded, pemblock = pem.Decode(pemblock) + if decoded == nil { + break + } + if decoded.Type == "CERTIFICATE" { + certs = append(certs, decoded) + } else { + skippedBytes = append(skippedBytes, decoded.Type) + } + } + + var parsedCerts []*x509.Certificate + + for _, cert := range certs { + parsed, err := x509.ParseCertificate(cert.Bytes) + if err != nil { + data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err) + return errors.New(data.WarningValidation) + } + parsedCerts = append(parsedCerts, parsed) + } + + if len(parsedCerts) == 0 { + data.WarningValidation = fmt.Sprintf("You have specified an empty certificate") + return errors.New(data.WarningValidation) + } + + data.ValidCert = true + + // spew.Dump(parsedCerts) + + opts := x509.VerifyOptions{ + DNSName: serverName, + } + + log.Printf("number of certs - %d", len(parsedCerts)) + if len(parsedCerts) > 1 { + // set up an intermediate + pool := x509.NewCertPool() + for _, cert := range parsedCerts[1:] { + log.Printf("got an intermediate cert") + pool.AddCert(cert) + } + opts.Intermediates = pool + } + + // TODO: save it as a warning rather than error it out -- shouldn't be a big problem + mainCert := parsedCerts[0] + _, err := mainCert.Verify(opts) + if err != nil { + // let self-signed certs through + data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err) + } else { + data.ValidChain = true + } + // spew.Dump(chains) + + // update status + if mainCert != nil { + notAfter := mainCert.NotAfter + data.Subject = mainCert.Subject.String() + data.Issuer = mainCert.Issuer.String() + data.NotAfter = notAfter + data.NotBefore = mainCert.NotBefore + data.DNSNames = mainCert.DNSNames + } + + return nil +} + +func validatePkey(data *tlsConfigStatus, pkey string) error { + // now do a more extended validation + var key *pem.Block // PEM-encoded certificates + var skippedBytes []string // skipped bytes + + // go through all pem blocks, but take first valid pem block and drop the rest + pemblock := []byte(pkey) + for { + var decoded *pem.Block + decoded, pemblock = pem.Decode(pemblock) + if decoded == nil { + break + } + if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") { + key = decoded + break + } else { + skippedBytes = append(skippedBytes, decoded.Type) + } + } + + if key == nil { + data.WarningValidation = "No valid keys were found" + return errors.New(data.WarningValidation) + } + + // parse the decoded key + _, keytype, err := parsePrivateKey(key.Bytes) + if err != nil { + data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err) + return errors.New(data.WarningValidation) + } + + data.ValidKey = true + data.KeyType = keytype + return nil +} + +// Process certificate data and its private key. +// All parameters are optional. +// On error, return partially set object +// with 'WarningValidation' field containing error description. +func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus { + var data tlsConfigStatus + + // check only public certificate separately from the key + if certChain != "" { + if verifyCertChain(&data, certChain, serverName) != nil { + return data + } + } + + // validate private key (right now the only validation possible is just parsing it) + if pkey != "" { + if validatePkey(&data, pkey) != nil { + return data + } + } + + // if both are set, validate both in unison + if pkey != "" && certChain != "" { + _, err := tls.X509KeyPair([]byte(certChain), []byte(pkey)) + if err != nil { + data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err) + return data + } + data.ValidPair = true + } + + return data +} + +// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates +// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys. +// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three. +func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, "RSA", nil + } + + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey: + return key, "RSA", nil + case *ecdsa.PrivateKey: + return key, "ECDSA", nil + default: + return nil, "", errors.New("tls: found unknown private key type in PKCS#8 wrapping") + } + } + + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, "ECDSA", nil + } + + return nil, "", errors.New("tls: failed to parse private key") +} + +// unmarshalTLS handles base64-encoded certificates transparently +func unmarshalTLS(r *http.Request) (tlsConfig, error) { + data := tlsConfig{} + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + return data, errorx.Decorate(err, "Failed to parse new TLS config json") + } + + if data.CertificateChain != "" { + certPEM, err := base64.StdEncoding.DecodeString(data.CertificateChain) + if err != nil { + return data, errorx.Decorate(err, "Failed to base64-decode certificate chain") + } + data.CertificateChain = string(certPEM) + } + + if data.PrivateKey != "" { + keyPEM, err := base64.StdEncoding.DecodeString(data.PrivateKey) + if err != nil { + return data, errorx.Decorate(err, "Failed to base64-decode private key") + } + + data.PrivateKey = string(keyPEM) + } + + return data, nil +} + +func marshalTLS(w http.ResponseWriter, data tlsConfig) { + w.Header().Set("Content-Type", "application/json") + + if data.CertificateChain != "" { + encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain)) + data.CertificateChain = encoded + } + + if data.PrivateKey != "" { + encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey)) + data.PrivateKey = encoded + } + + err := json.NewEncoder(w).Encode(data) + if err != nil { + httpError(w, http.StatusInternalServerError, "Failed to marshal json with TLS status: %s", err) + return + } +} diff --git a/dnsforward/querylog_top.go b/dnsforward/querylog_top.go index b381984c..43d16699 100644 --- a/dnsforward/querylog_top.go +++ b/dnsforward/querylog_top.go @@ -158,6 +158,11 @@ func (d *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error { hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, ".")) + // if question hostname is empty, do nothing + if hostname == "" { + return nil + } + // get value, if not set, crate one d.hoursReadLock() defer d.hoursReadUnlock() diff --git a/go.mod b/go.mod index d824b5b7..589d0982 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/AdguardTeam/AdGuardHome +go 1.12 + require ( github.com/AdguardTeam/dnsproxy v0.11.2 github.com/AdguardTeam/golibs v0.1.0 diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index c40648bc..d8b1072c 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1263,6 +1263,10 @@ definitions: type: "string" example: "You have specified an empty certificate" description: "warning_validation is a validation warning message with the issue description" + valid_pair: + type: "boolean" + example: "true" + description: "valid_pair is true if both certificate and private key are correct" NetInterface: type: "object" description: "Network interface info"