From e73c23e462ecb41698d8245a08731c33373f6cd9 Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Tue, 16 Mar 2021 23:11:13 -0500 Subject: [PATCH] Add support for external SSL providers. Also adds support for ZeroSSL from the beginning --- cert/selfsigned.go | 63 ++++++--- cert/selfsigned_test.go | 10 +- cert/tls.go | 37 ++++- certprovider/zerossl.go | 289 ++++++++++++++++++++++++++++++++++++++++ config.go | 33 +++++ lnd.go | 266 +++++++++++++++++++++++++++++++++--- 6 files changed, 654 insertions(+), 44 deletions(-) create mode 100644 certprovider/zerossl.go diff --git a/cert/selfsigned.go b/cert/selfsigned.go index c3f1f1e0d3..71bb4871ca 100644 --- a/cert/selfsigned.go +++ b/cert/selfsigned.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" @@ -16,7 +17,7 @@ import ( "time" "github.com/lightningnetwork/lnd/keychain" - "github.com/lightningnetwork/lnd/lnencrypt" + "github.com/lightningnetwork/lnd/lnencrypt" ) const ( @@ -210,7 +211,7 @@ func IsOutdated(cert *x509.Certificate, tlsExtraIPs, func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, tlsExtraDomains []string, tlsDisableAutofill bool, certValidity time.Duration, encryptKey bool, - keyRing keychain.KeyRing) ([]byte, []byte, error) { + keyRing keychain.KeyRing, keyType string) ([]byte, []byte, error) { now := time.Now() validUntil := now.Add(certValidity) @@ -234,12 +235,6 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, return nil, nil, err } - // Generate a private key for the certificate. - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, nil, err - } - // Construct the certificate template. template := x509.Certificate{ SerialNumber: serialNumber, @@ -260,10 +255,46 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, IPAddresses: ipAddresses, } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, - &template, &priv.PublicKey, priv) - if err != nil { - return nil, nil, fmt.Errorf("failed to create certificate: %v", err) + // Generate a private key for the certificate. + var derBytes []byte + var keyBytes []byte + var encodeString string + if keyType == "ec" { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + derBytes, err = x509.CreateCertificate(rand.Reader, &template, + &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, fmt.Errorf("failed to create certificate: %v", err) + } + + keyBytes, err = x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) + } + encodeString = "EC PRIVATE KEY" + } else if keyType == "rsa" { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + derBytes, err = x509.CreateCertificate(rand.Reader, &template, + &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, fmt.Errorf("failed to create certificate: %v", err) + } + + keyBytes = x509.MarshalPKCS1PrivateKey(priv) + if err != nil { + return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) + } + encodeString = "RSA PRIVATE KEY" + } else { + return nil, nil, fmt.Errorf("Unknown keyType: %s", keyType) } certBuf := &bytes.Buffer{} @@ -273,13 +304,9 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, return nil, nil, fmt.Errorf("failed to encode certificate: %v", err) } - keybytes, err := x509.MarshalECPrivateKey(priv) - if err != nil { - return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) - } keyBuf := &bytes.Buffer{} - err = pem.Encode(keyBuf, &pem.Block{Type: "EC PRIVATE KEY", - Bytes: keybytes}) + err = pem.Encode(keyBuf, &pem.Block{Type: encodeString, + Bytes: keyBytes}) if err != nil { return nil, nil, fmt.Errorf("failed to encode private key: %v", err) } diff --git a/cert/selfsigned_test.go b/cert/selfsigned_test.go index c9fc7572ce..94ec706710 100644 --- a/cert/selfsigned_test.go +++ b/cert/selfsigned_test.go @@ -43,7 +43,7 @@ func TestIsOutdatedCert(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], extraDomains[:2], false, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) if err != nil { t.Fatal(err) @@ -108,7 +108,7 @@ func TestIsOutdatedPermutation(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:], extraDomains[:], false, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) if err != nil { t.Fatal(err) @@ -185,7 +185,7 @@ func TestTLSDisableAutofill(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], extraDomains[:2], true, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) require.NoError( t, err, @@ -253,7 +253,7 @@ func TestTlsConfig(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, []string{extraIPs[0]}, []string{extraDomains[0]}, false, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) if err != nil { t.Fatal(err) @@ -325,7 +325,7 @@ func TestEncryptedTlsConfig(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, []string{extraIPs[0]}, []string{extraDomains[0]}, false, cert.DefaultAutogenValidity, - true, keyRing, + true, keyRing, "ec", ) if err != nil { t.Fatal(err) diff --git a/cert/tls.go b/cert/tls.go index 6d90f2896d..a28fc5858b 100644 --- a/cert/tls.go +++ b/cert/tls.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "io/ioutil" + "strings" "sync" ) @@ -24,6 +25,10 @@ var ( tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, } + tlsRSACipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } ) type TlsReloader struct { @@ -72,12 +77,34 @@ func LoadCert(certBytes, keyBytes []byte) (tls.Certificate, *x509.Certificate, // TLSConfFromCert returns the default TLS configuration used for a server, // using the given certificate as identity. -func TLSConfFromCert(certData tls.Certificate) *tls.Config { - return &tls.Config{ - Certificates: []tls.Certificate{certData}, - CipherSuites: tlsCipherSuites, - MinVersion: tls.VersionTLS12, +func TLSConfFromCert(certData []tls.Certificate) *tls.Config { + var config *tls.Config + + getCertificate := func(h *tls.ClientHelloInfo) (*tls.Certificate, error) { + defaultCertList := []string{"localhost", "127.0.0.1"} + for _, host := range defaultCertList { + if strings.Contains(h.ServerName, host) { + return &certData[0], nil + } + } + return &certData[1], nil + } + + if len(certData) > 1 { + config = &tls.Config{ + Certificates: []tls.Certificate{certData[0]}, + GetCertificate: getCertificate, + CipherSuites: tlsRSACipherSuites, + MinVersion: tls.VersionTLS12, + } + } else { + config = &tls.Config{ + Certificates: []tls.Certificate{certData[0]}, + CipherSuites: tlsCipherSuites, + MinVersion: tls.VersionTLS12, + } } + return config } // NewTLSReloader is used to create a new TLS Reloader that will be used diff --git a/certprovider/zerossl.go b/certprovider/zerossl.go new file mode 100644 index 0000000000..d785507393 --- /dev/null +++ b/certprovider/zerossl.go @@ -0,0 +1,289 @@ +package certprovider + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" +) + +var ( + zeroSSLBaseUrl = "https://api.zerossl.com" +) + +type ZeroSSLError struct { + Code string `json:"code"` + Type string `json:"type"` +} + +type ZeroSSLApiError struct { + Success string `json:"success"` + Error ZeroSSLError `json:"error"` +} + +type ZeroSSLValidationMethod struct { + FileValidationUrlHttp string `json:"file_validation_url_http"` + FileValidationUrlHttps string `json:"file_validation_url_https"` + FileValidationContent []string `json:"file_validation_content"` + CnameValidationP1 string `json:"cname_validation_p1"` + CnameValidationP2 string `json:"cname_validation_p2"` +} + +type ZeroSSLValidation struct { + EmailValidation map[string][]string `json:"email_validation"` + OtherValidation map[string]ZeroSSLValidationMethod `json:"other_methods"` +} + +type ZeroSSLExternalCert struct { + Id string `json:"id"` + Type string `json:"type"` + CommonName string `json:"common_name"` + AdditionalDomains string `json:"additional_domains"` + Created string `json:"created"` + Expires string `json:"expires"` + Status string `json:"status"` + ValidationType string `json:"validation_type"` + ValidationEmails string `json:"validation_emails"` + ReplacementFor string `json:"replacement_for"` + Validation ZeroSSLValidation `json:"validation"` +} + +type ZeroSSLCertResponse struct { + Certificate string `json:"certificate.crt"` + CaBundle string `json:"ca_bundle.crt"` +} + +type ZeroSSLCertRevoke struct { + Success int `json:"success"` +} + +func ZeroSSLGenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, err error) { + block, _ := pem.Decode(keyBytes) + x509Encoded := block.Bytes + privKey, err := x509.ParsePKCS1PrivateKey(x509Encoded) + if err != nil { + return csrBuffer, err + } + subj := pkix.Name{ + CommonName: domain, + } + rawSubj := subj.ToRDNSequence() + asn1Subj, _ := asn1.Marshal(rawSubj) + template := x509.CertificateRequest{ + RawSubject: asn1Subj, + SignatureAlgorithm: x509.SHA256WithRSA, + } + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, privKey) + if err != nil { + return csrBuffer, err + } + pem.Encode(&csrBuffer, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) + return csrBuffer, nil +} + +func ZeroSSLRequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExternalCert, err error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return certificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + parsedCsr := strings.Replace(csr.String(), "\n", "", -1) + data := url.Values{} + data.Set("certificate_domains", domain) + data.Set("certificate_validity_days", "90") + data.Set("certificate_csr", parsedCsr) + apiUrl := fmt.Sprintf( + "%s/certificates?access_key=%s", + zeroSSLBaseUrl, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("POST", apiUrl, strings.NewReader(data.Encode())) + if err != nil { + return certificate, err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return certificate, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return certificate, fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, _ := ioutil.ReadAll(resp.Body) + err = json.Unmarshal(body, &certificate) + if err != nil || certificate.Id == "" { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + return certificate, fmt.Errorf("Unknown error occured: %v", string(body)) + } + return certificate, fmt.Errorf("There was a problem requesting a certificate: %v", apiError.Error.Type) + } + return certificate, nil +} + +func ZeroSSLValidateCert(certificate ZeroSSLExternalCert) error { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s/challenges?access_key=%s", + zeroSSLBaseUrl, certificate.Id, apiKey, + ) + data := url.Values{} + data.Set("validation_method", "HTTP_CSR_HASH") + client := &http.Client{} + request, err := http.NewRequest("POST", apiUrl, strings.NewReader(data.Encode())) + if err != nil { + return err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + return nil +} + +func ZeroSSLGetCert(certificate ZeroSSLExternalCert) (newCertificate ZeroSSLExternalCert, err error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return newCertificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s?access_key=%s", + zeroSSLBaseUrl, certificate.Id, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("GET", apiUrl, nil) + if err != nil { + return newCertificate, err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return newCertificate, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return newCertificate, fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return newCertificate, err + } + err = json.Unmarshal(body, &newCertificate) + if err != nil { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + fmt.Printf("Unknown error occured: %v\n", string(body)) + } + fmt.Printf("There was a problem requesting a certificate: %s", apiError.Error.Type) + } + return newCertificate, nil +} + +func ZeroSSLDownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return "", "", fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s/download/return?access_key=%s", + zeroSSLBaseUrl, certificate.Id, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("GET", apiUrl, nil) + if err != nil { + return "", "", err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return "", "", fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", "", err + } + var certResponse ZeroSSLCertResponse + err = json.Unmarshal(body, &certResponse) + if err != nil { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + return "", "", fmt.Errorf("Unknown error occured: %v", string(body)) + } + return "", "", fmt.Errorf("There was a problem requesting a certificate: %s", apiError.Error.Type) + } + return certResponse.Certificate, certResponse.CaBundle, nil +} + +func ZeroSSLRevokeCert(certificateId string) (err error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s/revoke?access_key=%s", + zeroSSLBaseUrl, certificateId, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("POST", apiUrl, nil) + if err != nil { + return err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + var revokeCert ZeroSSLCertRevoke + err = json.Unmarshal(body, &revokeCert) + if err != nil { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + fmt.Printf("Unknown error occured: %v\n", string(body)) + return err + } + return fmt.Errorf("There was a problem requesting a certificate: %s", apiError.Error.Type) + } + if revokeCert.Success != 1 { + fmt.Printf("Unknown error occured: %v\n", string(body)) + return fmt.Errorf("There was a problem requesting a certificate: %s", revokeCert) + } + return nil +} diff --git a/config.go b/config.go index cdc6c69142..2ec60f9d80 100644 --- a/config.go +++ b/config.go @@ -55,6 +55,7 @@ const ( defaultRPCPort = 10009 defaultRESTPort = 8080 defaultPeerPort = 9735 + defaultExternalSSLPort = 8787 defaultRPCHost = "localhost" defaultNoSeedBackup = false @@ -206,6 +207,10 @@ type Config struct { LetsEncryptListen string `long:"letsencryptlisten" description:"The IP:port on which lnd will listen for Let's Encrypt challenges. Let's Encrypt will always try to contact on port 80. Often non-root processes are not allowed to bind to ports lower than 1024. This configuration option allows a different port to be used, but must be used in combination with port forwarding from port 80. This configuration can also be used to specify another IP address to listen on, for example an IPv6 address."` LetsEncryptDomain string `long:"letsencryptdomain" description:"Request a Let's Encrypt certificate for this domain. Note that the certicate is only requested and stored when the first rpc connection comes in."` + ExternalSSLProvider string `long:"externalsslprovider" description:"The provider to use when requesting SSL Certificates"` + ExternalSSLPort int `long:"externalsslport" description:"The port on which lnd will listen for certificate validation challenges."` + ExternalSSLDomain string `long:"externalssldomain" description:"Request an external certificate for this domain"` + // We'll parse these 'raw' string arguments into real net.Addrs in the // loadConfig function. We need to expose the 'raw' strings so the // command line library can access them. @@ -358,6 +363,7 @@ func DefaultConfig() Config { TLSKeyPath: defaultTLSKeyPath, LetsEncryptDir: defaultLetsEncryptDir, LetsEncryptListen: defaultLetsEncryptListen, + ExternalSSLPort: defaultExternalSSLPort, LogDir: defaultLogDir, MaxLogFiles: defaultMaxLogFiles, MaxLogFileSize: defaultMaxLogFileSize, @@ -567,6 +573,15 @@ func LoadConfig() (*Config, error) { return cleanCfg, nil } +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + // ValidateConfig check the given configuration to be sane. This makes sure no // illegal values or combination of values are set. All file system paths are // normalized. The cleaned up config is returned on success. @@ -592,6 +607,24 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) { } } + if cfg.ExternalSSLProvider != "" { + if cfg.ExternalSSLDomain == "" { + return nil, fmt.Errorf("you must supply a domain when requesting external certificates") + } + + supportedSSLProviders := []string{"zerossl"} + isSupported := contains(supportedSSLProviders, cfg.ExternalSSLProvider) + if !isSupported { + return nil, fmt.Errorf("Received unsupported external ssl provider: %s", cfg.ExternalSSLProvider) + } + + if cfg.ExternalSSLProvider != "" { + if err := os.MkdirAll(fmt.Sprintf("%s/%s/", lndDir, cfg.ExternalSSLProvider), 0700); err != nil { + return nil, err + } + } + } + funcName := "loadConfig" makeDirectory := func(dir string) error { err := os.MkdirAll(dir, 0700) diff --git a/lnd.go b/lnd.go index 6d1cea0d4a..5151bbeb28 100644 --- a/lnd.go +++ b/lnd.go @@ -37,6 +37,7 @@ import ( "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" + "github.com/lightningnetwork/lnd/certprovider" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chanacceptor" "github.com/lightningnetwork/lnd/channeldb" @@ -270,6 +271,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { var restDialOpts []grpc.DialOption var restListen func(net.Addr) (net.Listener, error) var tlsReloader *cert.TlsReloader + var certId string // The real KeyRing isn't available until after the wallet is unlocked, // but we need one now. Because we aren't encrypting anything here it can @@ -285,6 +287,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { restListen, cleanUp, tlsReloader, + certId, err = getEphemeralTLSConfig(cfg, emptyKeyRing) } else { serverOpts, @@ -763,13 +766,17 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { // The wallet is unlocked at this point so we can use the real KeyRing if cfg.TLSEncryptKey { tmpCertPath := cfg.TLSCertPath + ".tmp" + tmpExternalCertPath := fmt.Sprintf("%s/%s/tls.cert.tmp", cfg.LndDir, cfg.ExternalSSLProvider) err = os.Remove(tmpCertPath) if err != nil { ltndLog.Warn("unable to delete temp cert at %v", tmpCertPath) } - // Ensure the persistent TLS credentials are created - _, _, _, _, _, err = getTLSConfig(cfg, activeChainControl.KeyRing) + err = os.Remove(tmpExternalCertPath) + if err != nil { + ltndLog.Warn("unable to delete temp external cert at %v", tmpExternalCertPath) + } + _, _, _, _, _, err = getTLSConfig(cfg, emptyKeyRing) if err != nil { err := fmt.Errorf("unable to load TLS credentials: %v", err) ltndLog.Error(err) @@ -784,6 +791,11 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { if err != nil { return err } + err = certprovider.ZeroSSLRevokeCert(certId) + if err != nil { + ltndLog.Error("Failed to revoke temporary certifiate:") + ltndLog.Error(err) + } // Switch the server's TLS certificate to the persisntent one err = tlsReloader.AttemptReload(certBytes, keyBytes) if err != nil { @@ -898,46 +910,186 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { return nil } +// createExternalCert creates an Externally provisioned SSL Certificate +func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (returnCert tls.Certificate, certId string, err error) { + var certServer *http.Server + if cfg.ExternalSSLProvider == "zerossl" { + csr, err := certprovider.ZeroSSLGenerateCsr(keyBytes, cfg.ExternalSSLDomain) + if err != nil { + return returnCert, certId, err + } + rpcsLog.Debugf("created csr for %s", cfg.ExternalSSLDomain) + externalCert, err := certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + if err != nil { + return returnCert, certId, err + } + rpcsLog.Infof("received cert request with id %s", externalCert.Id) + domain := externalCert.CommonName + path := externalCert.Validation.OtherValidation[domain].FileValidationUrlHttp + path = strings.Replace(path, "http://"+domain, "", -1) + content := strings.Join(externalCert.Validation.OtherValidation[domain].FileValidationContent[:], "\n") + rpcsLog.Debugf("using cert path: %s", path) + rpcsLog.Debugf("using cert content: %s", content) + go func() { + addr := fmt.Sprintf(":%v", cfg.ExternalSSLPort) + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte(content)) + }) + certServer = &http.Server{ + Addr: addr, + Handler: http.DefaultServeMux, + } + rpcsLog.Infof("starting certificate validator server at %s", + addr) + err := certServer.ListenAndServe() + if err != nil { + rpcsLog.Errorf("there was a problem starting external cert validation server: %v", + err) + return + } + }() + err = certprovider.ZeroSSLValidateCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Debug("requested certificate to be validated") + checkCount := 0 + retries := 0 + for { + newCert, err := certprovider.ZeroSSLGetCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + status := newCert.Status + rpcsLog.Debugf("found certificate in state %s", status) + if status == "issued" { + rpcsLog.Infof("found certificate in state %s", status) + break + } else if status == "draft" { + err = certprovider.ZeroSSLValidateCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + } + if retries > 3 { + rpcsLog.Error("Still can't get a certificate after 3 retries. Failing...") + certServer.Close() + return returnCert, "", fmt.Errorf("Timed out trying to create SSL Certificate") + } + if checkCount > 15 { + rpcsLog.Warn("Timed out waiting for cert. Requesting a new one.") + externalCert, err = certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Infof("received cert request with id %s", externalCert.Id) + retries += 1 + checkCount = 0 + } + checkCount += 1 + time.Sleep(2 * time.Second) + } + certId = externalCert.Id + certificate, caBundle, err := certprovider.ZeroSSLDownloadCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + externalCertBytes := []byte(certificate + "\n" + caBundle) + if err = ioutil.WriteFile(certLocation, externalCertBytes, 0644); err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Infof("successfully wrote external SSL certificate to %s", + certLocation) + externalCertData, _, err := cert.LoadCert( + externalCertBytes, keyBytes, + ) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Info("shutting down certificate validator server") + certServer.Close() + return externalCertData, certId, nil + } else { + return returnCert, certId, fmt.Errorf("Unknown external certificate provider: %s", cfg.ExternalSSLProvider) + } +} + // getEphemeralTLSConfig returns a temporary TLS configuration with the TLS // key and cert for the gRPC server and credentials and a proxy destination // for the REST reverse proxy. The key is not written to disk. func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( []grpc.ServerOption, []grpc.DialOption, - func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, string, error) { rpcsLog.Infof("Generating ephemeral TLS certificates...") tmpValidity := 24 * time.Hour // Append .tmp to the end of the cert for differentiation. tmpCertPath := cfg.TLSCertPath + ".tmp" + var externalSSLCertPath string + keyType := "ec" + if cfg.ExternalSSLProvider != "" { + keyType = "rsa" + externalSSLCertPath = fmt.Sprintf("%s/%s/tls.cert.tmp", cfg.LndDir, cfg.ExternalSSLProvider) + } + // Pass in a blank string for the key path so the // function doesn't write them to disk. certBytes, keyBytes, err := cert.GenCertPair( "lnd temporary autogenerated cert", tmpCertPath, "", cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, tmpValidity, false, keyRing, + keyType, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } + + var externalCertData tls.Certificate + var certId string + var failedProvision bool + if cfg.ExternalSSLProvider != "" { + externalCertData, certId, err = createExternalCert( + cfg, keyBytes, externalSSLCertPath, + ) + if err != nil { + rpcsLog.Warn(err) + failedProvision = true + } + } + rpcsLog.Infof("Done generating ephemeral TLS certificates") certData, _, err := cert.LoadCert( certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err + } + certList := []tls.Certificate{certData} + if cfg.ExternalSSLProvider != "" && !failedProvision { + certList = append(certList, externalCertData) } - tlsCfg := cert.TLSConfFromCert(certData) + + tlsCfg := cert.TLSConfFromCert(certList) tlsCfg.GetCertificate = tlsr.GetCertificateFunc() restCreds, err := credentials.NewClientTLSFromFile(tmpCertPath, "") if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } cleanUp := func() {} @@ -968,7 +1120,7 @@ func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return lncfg.TLSListenOnAddress(addr, tlsCfg) } - return serverOpts, restDialOpts, restListen, cleanUp, tlsr, nil + return serverOpts, restDialOpts, restListen, cleanUp, tlsr, certId, nil } // getTLSConfig returns a TLS configuration for the gRPC server and credentials @@ -978,6 +1130,14 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( []grpc.ServerOption, []grpc.DialOption, func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + externalSSLCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) + keyType := "ec" + privateKeyPrefix := []byte("-----BEGIN EC PRIVATE KEY-----") + if cfg.ExternalSSLProvider != "" { + keyType = "rsa" + privateKeyPrefix = []byte("-----BEGIN RSA PRIVATE KEY-----") + } + // Ensure we create TLS key and certificate if they don't exist. if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) { rpcsLog.Infof("Generating TLS certificates...") @@ -985,11 +1145,19 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( "lnd autogenerated cert", cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, - cfg.TLSEncryptKey, keyRing, + cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { return nil, nil, nil, nil, nil, err } + + // If the external ssl provider is supplied and there was a key rotation + // then we need to rotate the external SSL too. Just delete here so it + // can be regenerated a little farther down + if cfg.ExternalSSLProvider != "" { + os.Remove(externalSSLCertPath) + } + rpcsLog.Infof("Done generating TLS certificates") } @@ -998,10 +1166,9 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return nil, nil, nil, nil, nil, err } - // We check to see if the private key is encrypted or plaintext. - // If it's encrypted we need to try to decrypt it so we can use it - // in the gRPC server. - privateKeyPrefix := []byte("-----BEGIN EC PRIVATE KEY-----") + // Do a check to see if the TLS private key is encrypted. If it's encrypted, + // try to decrypt it. If it's in plaintext but should be encrypted, + // then encrypt it. if !bytes.HasPrefix(keyBytes, privateKeyPrefix) { // If the private key is encrypted but the user didn't pass // --tlsencryptkey we error out. This is because the wallet is not @@ -1031,6 +1198,35 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( } } + var externalCertData tls.Certificate + var failedProvision bool + if cfg.ExternalSSLProvider != "" { + // Ensure we create external TLS certificate if they don't exist. + if !fileExists(externalSSLCertPath) { + ltndLog.Infof("Requesting external certificate for domain %v", + cfg.ExternalSSLDomain) + _, _, err = createExternalCert( + cfg, keyBytes, externalSSLCertPath, + ) + if err != nil { + rpcsLog.Info(err) + failedProvision = true + } + } + if !failedProvision { + externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + externalCertData, _, err = cert.LoadCert( + externalCertBytes, keyBytes, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + } + certData, parsedCert, err := cert.LoadCert( certBytes, keyBytes, ) @@ -1069,12 +1265,19 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return nil, nil, nil, nil, nil, err } + if cfg.ExternalSSLProvider != "" { + err = os.Remove(externalSSLCertPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + rpcsLog.Infof("Renewing TLS certificates...") _, _, err = cert.GenCertPair( "lnd autogenerated cert", cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, - cfg.TLSEncryptKey, keyRing, + cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { return nil, nil, nil, nil, nil, err @@ -1087,6 +1290,33 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return nil, nil, nil, nil, nil, err } + if cfg.ExternalSSLProvider != "" { + // Ensure we create external TLS certificate if they don't exist. + if !fileExists(externalSSLCertPath) { + ltndLog.Infof("Requesting external certificate for domain %v", + cfg.ExternalSSLDomain) + _, _, err = createExternalCert( + cfg, keyBytes, externalSSLCertPath, + ) + if err != nil { + rpcsLog.Info(err) + failedProvision = true + } + } + if !failedProvision { + externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + externalCertData, _, err = cert.LoadCert( + externalCertBytes, keyBytes, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + } + // If key encryption is set, then decrypt the file. // We don't need to do a file type check here because GenCertPair // has been ran with the same value for cfg.TLSEncryptKey. @@ -1110,7 +1340,11 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( if err != nil { return nil, nil, nil, nil, nil, err } - tlsCfg := cert.TLSConfFromCert(certData) + certList := []tls.Certificate{certData} + if cfg.ExternalSSLProvider != "" && !failedProvision { + certList = append(certList, externalCertData) + } + tlsCfg := cert.TLSConfFromCert(certList) tlsCfg.GetCertificate = tlsr.GetCertificateFunc() restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "")