commit 077eb13a904f2c62496f31b158135d9743526f82 Author: Darren Shepherd Date: Thu May 9 12:36:03 2019 -0700 Initial Commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e454a52 --- /dev/null +++ b/LICENSE @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/cert/cert.go b/cert/cert.go new file mode 100644 index 0000000..3429c82 --- /dev/null +++ b/cert/cert.go @@ -0,0 +1,269 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "math" + "math/big" + "net" + "path" + "strings" + "time" +) + +const ( + rsaKeySize = 2048 + duration365d = time.Hour * 24 * 365 +) + +// Config contains the basic fields required for creating a certificate +type Config struct { + CommonName string + Organization []string + AltNames AltNames + Usages []x509.ExtKeyUsage +} + +// AltNames contains the domain names and IP addresses that will be added +// to the API Server's x509 certificate SubAltNames field. The values will +// be passed directly to the x509.Certificate object. +type AltNames struct { + DNSNames []string + IPs []net.IP +} + +// NewPrivateKey creates an RSA private key +func NewPrivateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(cryptorand.Reader, rsaKeySize) +} + +// NewSelfSignedCACert creates a CA certificate +func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, error) { + now := time.Now() + tmpl := x509.Certificate{ + SerialNumber: new(big.Int).SetInt64(0), + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + NotBefore: now.UTC(), + NotAfter: now.Add(duration365d * 10).UTC(), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + + certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key) + if err != nil { + return nil, err + } + return x509.ParseCertificate(certDERBytes) +} + +// NewSignedCert creates a signed certificate using the given CA certificate and key +func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) { + serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) + if err != nil { + return nil, err + } + if len(cfg.CommonName) == 0 { + return nil, errors.New("must specify a CommonName") + } + if len(cfg.Usages) == 0 { + return nil, errors.New("must specify at least one ExtKeyUsage") + } + + certTmpl := x509.Certificate{ + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: cfg.AltNames.IPs, + SerialNumber: serial, + NotBefore: caCert.NotBefore, + NotAfter: time.Now().Add(duration365d).UTC(), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: cfg.Usages, + } + certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &certTmpl, caCert, key.Public(), caKey) + if err != nil { + return nil, err + } + return x509.ParseCertificate(certDERBytes) +} + +// MakeEllipticPrivateKeyPEM creates an ECDSA private key +func MakeEllipticPrivateKeyPEM() ([]byte, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) + if err != nil { + return nil, err + } + + derBytes, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + + privateKeyPemBlock := &pem.Block{ + Type: ECPrivateKeyBlockType, + Bytes: derBytes, + } + return pem.EncodeToMemory(privateKeyPemBlock), nil +} + +// GenerateSelfSignedCertKey creates a self-signed certificate and key for the given host. +// Host may be an IP or a DNS name +// You may also specify additional subject alt names (either ip or dns names) for the certificate. +func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, error) { + return GenerateSelfSignedCertKeyWithFixtures(host, alternateIPs, alternateDNS, "") +} + +// GenerateSelfSignedCertKeyWithFixtures creates a self-signed certificate and key for the given host. +// Host may be an IP or a DNS name. You may also specify additional subject alt names (either ip or dns names) +// for the certificate. +// +// If fixtureDirectory is non-empty, it is a directory path which can contain pre-generated certs. The format is: +// _-_-.crt +// _-_-.key +// Certs/keys not existing in that directory are created. +func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, alternateDNS []string, fixtureDirectory string) ([]byte, []byte, error) { + validFrom := time.Now().Add(-time.Hour) // valid an hour earlier to avoid flakes due to clock skew + maxAge := time.Hour * 24 * 365 // one year self-signed certs + + baseName := fmt.Sprintf("%s_%s_%s", host, strings.Join(ipsToStrings(alternateIPs), "-"), strings.Join(alternateDNS, "-")) + certFixturePath := path.Join(fixtureDirectory, baseName+".crt") + keyFixturePath := path.Join(fixtureDirectory, baseName+".key") + if len(fixtureDirectory) > 0 { + cert, err := ioutil.ReadFile(certFixturePath) + if err == nil { + key, err := ioutil.ReadFile(keyFixturePath) + if err == nil { + return cert, key, nil + } + return nil, nil, fmt.Errorf("cert %s can be read, but key %s cannot: %v", certFixturePath, keyFixturePath, err) + } + maxAge = 100 * time.Hour * 24 * 365 // 100 years fixtures + } + + caKey, err := rsa.GenerateKey(cryptorand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + caTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: fmt.Sprintf("%s-ca@%d", host, time.Now().Unix()), + }, + NotBefore: validFrom, + NotAfter: validFrom.Add(maxAge), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + + caDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + caCertificate, err := x509.ParseCertificate(caDERBytes) + if err != nil { + return nil, nil, err + } + + priv, err := rsa.GenerateKey(cryptorand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()), + }, + NotBefore: validFrom, + NotAfter: validFrom.Add(maxAge), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + if ip := net.ParseIP(host); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, host) + } + + template.IPAddresses = append(template.IPAddresses, alternateIPs...) + template.DNSNames = append(template.DNSNames, alternateDNS...) + + derBytes, err := x509.CreateCertificate(cryptorand.Reader, &template, caCertificate, &priv.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + // Generate cert, followed by ca + certBuffer := bytes.Buffer{} + if err := pem.Encode(&certBuffer, &pem.Block{Type: CertificateBlockType, Bytes: derBytes}); err != nil { + return nil, nil, err + } + if err := pem.Encode(&certBuffer, &pem.Block{Type: CertificateBlockType, Bytes: caDERBytes}); err != nil { + return nil, nil, err + } + + // Generate key + keyBuffer := bytes.Buffer{} + if err := pem.Encode(&keyBuffer, &pem.Block{Type: RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { + return nil, nil, err + } + + if len(fixtureDirectory) > 0 { + if err := ioutil.WriteFile(certFixturePath, certBuffer.Bytes(), 0644); err != nil { + return nil, nil, fmt.Errorf("failed to write cert fixture to %s: %v", certFixturePath, err) + } + if err := ioutil.WriteFile(keyFixturePath, keyBuffer.Bytes(), 0644); err != nil { + return nil, nil, fmt.Errorf("failed to write key fixture to %s: %v", certFixturePath, err) + } + } + + return certBuffer.Bytes(), keyBuffer.Bytes(), nil +} + +func ipsToStrings(ips []net.IP) []string { + ss := make([]string, 0, len(ips)) + for _, ip := range ips { + ss = append(ss, ip.String()) + } + return ss +} diff --git a/cert/csr.go b/cert/csr.go new file mode 100644 index 0000000..39a6751 --- /dev/null +++ b/cert/csr.go @@ -0,0 +1,75 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import ( + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "net" +) + +// MakeCSR generates a PEM-encoded CSR using the supplied private key, subject, and SANs. +// All key types that are implemented via crypto.Signer are supported (This includes *rsa.PrivateKey and *ecdsa.PrivateKey.) +func MakeCSR(privateKey interface{}, subject *pkix.Name, dnsSANs []string, ipSANs []net.IP) (csr []byte, err error) { + template := &x509.CertificateRequest{ + Subject: *subject, + DNSNames: dnsSANs, + IPAddresses: ipSANs, + } + + return MakeCSRFromTemplate(privateKey, template) +} + +// MakeCSRFromTemplate generates a PEM-encoded CSR using the supplied private +// key and certificate request as a template. All key types that are +// implemented via crypto.Signer are supported (This includes *rsa.PrivateKey +// and *ecdsa.PrivateKey.) +func MakeCSRFromTemplate(privateKey interface{}, template *x509.CertificateRequest) ([]byte, error) { + t := *template + t.SignatureAlgorithm = sigType(privateKey) + + csrDER, err := x509.CreateCertificateRequest(cryptorand.Reader, &t, privateKey) + if err != nil { + return nil, err + } + + csrPemBlock := &pem.Block{ + Type: CertificateRequestBlockType, + Bytes: csrDER, + } + + return pem.EncodeToMemory(csrPemBlock), nil +} + +func sigType(privateKey interface{}) x509.SignatureAlgorithm { + // Customize the signature for RSA keys, depending on the key size + if privateKey, ok := privateKey.(*rsa.PrivateKey); ok { + keySize := privateKey.N.BitLen() + switch { + case keySize >= 4096: + return x509.SHA512WithRSA + case keySize >= 3072: + return x509.SHA384WithRSA + default: + return x509.SHA256WithRSA + } + } + return x509.UnknownSignatureAlgorithm +} diff --git a/cert/io.go b/cert/io.go new file mode 100644 index 0000000..a57bf09 --- /dev/null +++ b/cert/io.go @@ -0,0 +1,193 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +// CanReadCertAndKey returns true if the certificate and key files already exists, +// otherwise returns false. If lost one of cert and key, returns error. +func CanReadCertAndKey(certPath, keyPath string) (bool, error) { + certReadable := canReadFile(certPath) + keyReadable := canReadFile(keyPath) + + if certReadable == false && keyReadable == false { + return false, nil + } + + if certReadable == false { + return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", certPath) + } + + if keyReadable == false { + return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", keyPath) + } + + return true, nil +} + +// If the file represented by path exists and +// readable, returns true otherwise returns false. +func canReadFile(path string) bool { + f, err := os.Open(path) + if err != nil { + return false + } + + defer f.Close() + + return true +} + +// WriteCert writes the pem-encoded certificate data to certPath. +// The certificate file will be created with file mode 0644. +// If the certificate file already exists, it will be overwritten. +// The parent directory of the certPath will be created as needed with file mode 0755. +func WriteCert(certPath string, data []byte) error { + if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil { + return err + } + return ioutil.WriteFile(certPath, data, os.FileMode(0644)) +} + +// WriteKey writes the pem-encoded key data to keyPath. +// The key file will be created with file mode 0600. +// If the key file already exists, it will be overwritten. +// The parent directory of the keyPath will be created as needed with file mode 0755. +func WriteKey(keyPath string, data []byte) error { + if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil { + return err + } + return ioutil.WriteFile(keyPath, data, os.FileMode(0600)) +} + +// LoadOrGenerateKeyFile looks for a key in the file at the given path. If it +// can't find one, it will generate a new key and store it there. +func LoadOrGenerateKeyFile(keyPath string) (data []byte, wasGenerated bool, err error) { + loadedData, err := ioutil.ReadFile(keyPath) + // Call verifyKeyData to ensure the file wasn't empty/corrupt. + if err == nil && verifyKeyData(loadedData) { + return loadedData, false, err + } + if !os.IsNotExist(err) { + return nil, false, fmt.Errorf("error loading key from %s: %v", keyPath, err) + } + + generatedData, err := MakeEllipticPrivateKeyPEM() + if err != nil { + return nil, false, fmt.Errorf("error generating key: %v", err) + } + if err := WriteKey(keyPath, generatedData); err != nil { + return nil, false, fmt.Errorf("error writing key to %s: %v", keyPath, err) + } + return generatedData, true, nil +} + +// MarshalPrivateKeyToPEM converts a known private key type of RSA or ECDSA to +// a PEM encoded block or returns an error. +func MarshalPrivateKeyToPEM(privateKey crypto.PrivateKey) ([]byte, error) { + switch t := privateKey.(type) { + case *ecdsa.PrivateKey: + derBytes, err := x509.MarshalECPrivateKey(t) + if err != nil { + return nil, err + } + privateKeyPemBlock := &pem.Block{ + Type: ECPrivateKeyBlockType, + Bytes: derBytes, + } + return pem.EncodeToMemory(privateKeyPemBlock), nil + case *rsa.PrivateKey: + return EncodePrivateKeyPEM(t), nil + default: + return nil, fmt.Errorf("private key is not a recognized type: %T", privateKey) + } +} + +// NewPool returns an x509.CertPool containing the certificates in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates +func NewPool(filename string) (*x509.CertPool, error) { + certs, err := CertsFromFile(filename) + if err != nil { + return nil, err + } + pool := x509.NewCertPool() + for _, cert := range certs { + pool.AddCert(cert) + } + return pool, nil +} + +// CertsFromFile returns the x509.Certificates contained in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates +func CertsFromFile(file string) ([]*x509.Certificate, error) { + pemBlock, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + certs, err := ParseCertsPEM(pemBlock) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", file, err) + } + return certs, nil +} + +// PrivateKeyFromFile returns the private key in rsa.PrivateKey or ecdsa.PrivateKey format from a given PEM-encoded file. +// Returns an error if the file could not be read or if the private key could not be parsed. +func PrivateKeyFromFile(file string) (interface{}, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + key, err := ParsePrivateKeyPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading private key file %s: %v", file, err) + } + return key, nil +} + +// PublicKeysFromFile returns the public keys in rsa.PublicKey or ecdsa.PublicKey format from a given PEM-encoded file. +// Reads public keys from both public and private key files. +func PublicKeysFromFile(file string) ([]interface{}, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + keys, err := ParsePublicKeysPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading public key file %s: %v", file, err) + } + return keys, nil +} + +// verifyKeyData returns true if the provided data appears to be a valid private key. +func verifyKeyData(data []byte) bool { + if len(data) == 0 { + return false + } + _, err := ParsePrivateKeyPEM(data) + return err == nil +} diff --git a/cert/pem.go b/cert/pem.go new file mode 100644 index 0000000..b99e366 --- /dev/null +++ b/cert/pem.go @@ -0,0 +1,269 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" +) + +const ( + // ECPrivateKeyBlockType is a possible value for pem.Block.Type. + ECPrivateKeyBlockType = "EC PRIVATE KEY" + // RSAPrivateKeyBlockType is a possible value for pem.Block.Type. + RSAPrivateKeyBlockType = "RSA PRIVATE KEY" + // PrivateKeyBlockType is a possible value for pem.Block.Type. + PrivateKeyBlockType = "PRIVATE KEY" + // PublicKeyBlockType is a possible value for pem.Block.Type. + PublicKeyBlockType = "PUBLIC KEY" + // CertificateBlockType is a possible value for pem.Block.Type. + CertificateBlockType = "CERTIFICATE" + // CertificateRequestBlockType is a possible value for pem.Block.Type. + CertificateRequestBlockType = "CERTIFICATE REQUEST" +) + +// EncodePublicKeyPEM returns PEM-encoded public data +func EncodePublicKeyPEM(key *rsa.PublicKey) ([]byte, error) { + der, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return []byte{}, err + } + block := pem.Block{ + Type: PublicKeyBlockType, + Bytes: der, + } + return pem.EncodeToMemory(&block), nil +} + +// EncodePrivateKeyPEM returns PEM-encoded private key data +func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte { + block := pem.Block{ + Type: RSAPrivateKeyBlockType, + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + return pem.EncodeToMemory(&block) +} + +// EncodeCertPEM returns PEM-endcoded certificate data +func EncodeCertPEM(cert *x509.Certificate) []byte { + block := pem.Block{ + Type: CertificateBlockType, + Bytes: cert.Raw, + } + return pem.EncodeToMemory(&block) +} + +// ParsePrivateKeyPEM returns a private key parsed from a PEM block in the supplied data. +// Recognizes PEM blocks for "EC PRIVATE KEY", "RSA PRIVATE KEY", or "PRIVATE KEY" +func ParsePrivateKeyPEM(keyData []byte) (interface{}, error) { + var privateKeyPemBlock *pem.Block + for { + privateKeyPemBlock, keyData = pem.Decode(keyData) + if privateKeyPemBlock == nil { + break + } + + switch privateKeyPemBlock.Type { + case ECPrivateKeyBlockType: + // ECDSA Private Key in ASN.1 format + if key, err := x509.ParseECPrivateKey(privateKeyPemBlock.Bytes); err == nil { + return key, nil + } + case RSAPrivateKeyBlockType: + // RSA Private Key in PKCS#1 format + if key, err := x509.ParsePKCS1PrivateKey(privateKeyPemBlock.Bytes); err == nil { + return key, nil + } + case PrivateKeyBlockType: + // RSA or ECDSA Private Key in unencrypted PKCS#8 format + if key, err := x509.ParsePKCS8PrivateKey(privateKeyPemBlock.Bytes); err == nil { + return key, nil + } + } + + // tolerate non-key PEM blocks for compatibility with things like "EC PARAMETERS" blocks + // originally, only the first PEM block was parsed and expected to be a key block + } + + // we read all the PEM blocks and didn't recognize one + return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA private key") +} + +// ParsePublicKeysPEM is a helper function for reading an array of rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded byte array. +// Reads public keys from both public and private key files. +func ParsePublicKeysPEM(keyData []byte) ([]interface{}, error) { + var block *pem.Block + keys := []interface{}{} + for { + // read the next block + block, keyData = pem.Decode(keyData) + if block == nil { + break + } + + // test block against parsing functions + if privateKey, err := parseRSAPrivateKey(block.Bytes); err == nil { + keys = append(keys, &privateKey.PublicKey) + continue + } + if publicKey, err := parseRSAPublicKey(block.Bytes); err == nil { + keys = append(keys, publicKey) + continue + } + if privateKey, err := parseECPrivateKey(block.Bytes); err == nil { + keys = append(keys, &privateKey.PublicKey) + continue + } + if publicKey, err := parseECPublicKey(block.Bytes); err == nil { + keys = append(keys, publicKey) + continue + } + + // tolerate non-key PEM blocks for backwards compatibility + // originally, only the first PEM block was parsed and expected to be a key block + } + + if len(keys) == 0 { + return nil, fmt.Errorf("data does not contain any valid RSA or ECDSA public keys") + } + return keys, nil +} + +// ParseCertsPEM returns the x509.Certificates contained in the given PEM-encoded byte array +// Returns an error if a certificate could not be parsed, or if the data does not contain any certificates +func ParseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) { + ok := false + certs := []*x509.Certificate{} + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + // Only use PEM "CERTIFICATE" blocks without extra headers + if block.Type != CertificateBlockType || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, err + } + + certs = append(certs, cert) + ok = true + } + + if !ok { + return certs, errors.New("data does not contain any valid RSA or ECDSA certificates") + } + return certs, nil +} + +// parseRSAPublicKey parses a single RSA public key from the provided data +func parseRSAPublicKey(data []byte) (*rsa.PublicKey, error) { + var err error + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(data); err != nil { + if cert, err := x509.ParseCertificate(data); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + // Test if parsed key is an RSA Public Key + var pubKey *rsa.PublicKey + var ok bool + if pubKey, ok = parsedKey.(*rsa.PublicKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid RSA Public Key") + } + + return pubKey, nil +} + +// parseRSAPrivateKey parses a single RSA private key from the provided data +func parseRSAPrivateKey(data []byte) (*rsa.PrivateKey, error) { + var err error + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS1PrivateKey(data); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(data); err != nil { + return nil, err + } + } + + // Test if parsed key is an RSA Private Key + var privKey *rsa.PrivateKey + var ok bool + if privKey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid RSA Private Key") + } + + return privKey, nil +} + +// parseECPublicKey parses a single ECDSA public key from the provided data +func parseECPublicKey(data []byte) (*ecdsa.PublicKey, error) { + var err error + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(data); err != nil { + if cert, err := x509.ParseCertificate(data); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + // Test if parsed key is an ECDSA Public Key + var pubKey *ecdsa.PublicKey + var ok bool + if pubKey, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid ECDSA Public Key") + } + + return pubKey, nil +} + +// parseECPrivateKey parses a single ECDSA private key from the provided data +func parseECPrivateKey(data []byte) (*ecdsa.PrivateKey, error) { + var err error + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParseECPrivateKey(data); err != nil { + return nil, err + } + + // Test if parsed key is an ECDSA Private Key + var privKey *ecdsa.PrivateKey + var ok bool + if privKey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid ECDSA Private Key") + } + + return privKey, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5e6d2f --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/rancher/dynamiclistener + +go 1.12 + +require ( + github.com/hashicorp/golang-lru v0.5.1 + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/sirupsen/logrus v1.4.1 + github.com/stretchr/testify v1.3.0 // indirect + golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 + golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c // indirect + golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect + golang.org/x/text v0.3.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d117bd2 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 h1:rlLehGeYg6jfoyz/eDqDU1iRXLKfR42nnNh57ytKEWo= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/read.go b/read.go new file mode 100644 index 0000000..b078703 --- /dev/null +++ b/read.go @@ -0,0 +1,55 @@ +package dynamiclistener + +import ( + "fmt" + "io/ioutil" + "path/filepath" +) + +func ReadTLSConfig(userConfig *UserConfig) error { + var err error + + path := userConfig.CertPath + + userConfig.CACerts, err = readPEM(filepath.Join(path, "cacerts.pem")) + if err != nil { + return err + } + + userConfig.Key, err = readPEM(filepath.Join(path, "key.pem")) + if err != nil { + return err + } + + userConfig.Cert, err = readPEM(filepath.Join(path, "cert.pem")) + if err != nil { + return err + } + + userConfig.Mode = "https" + if len(userConfig.Domains) > 0 { + userConfig.Mode = "acme" + } + + valid := false + if userConfig.Key != "" && userConfig.Cert != "" { + valid = true + } else if userConfig.Key == "" && userConfig.Cert == "" { + valid = true + } + + if !valid { + return fmt.Errorf("invalid SSL configuration found, please set cert/key, cert/key/cacerts, cacerts only, or none") + } + + return nil +} + +func readPEM(path string) (string, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return "", nil + } + + return string(content), nil +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..4a7877f --- /dev/null +++ b/server.go @@ -0,0 +1,645 @@ +package dynamiclistener + +import ( + "bytes" + "context" + "crypto/md5" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "log" + "net" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "time" + + lru "github.com/hashicorp/golang-lru" + cert "github.com/rancher/dynamiclistener/cert" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/acme/autocert" +) + +const ( + httpsMode = "https" + acmeMode = "acme" +) + +type server struct { + sync.Mutex + + userConfig UserConfig + listenConfigStorage ListenerConfigStorage + certs map[string]*tls.Certificate + ips *lru.Cache + + listeners []net.Listener + servers []*http.Server + + // dynamic config change on refresh + activeCert *tls.Certificate + activeCA *x509.Certificate + activeCAKey *rsa.PrivateKey + activeCAKeyString string + domains map[string]bool +} + +func NewServer(listenConfigStorage ListenerConfigStorage, config UserConfig) (ServerInterface, error) { + s := &server{ + userConfig: config, + listenConfigStorage: listenConfigStorage, + certs: map[string]*tls.Certificate{}, + } + + s.ips, _ = lru.New(20) + + if err := s.userConfigure(); err != nil { + return nil, err + } + + lc, err := listenConfigStorage.Get() + if err != nil { + return nil, err + } + + return s, s.Update(lc) +} + +func (s *server) CACert() (string, error) { + if s.userConfig.NoCACerts { + return "", nil + } + if s.userConfig.CACerts != "" { + return s.userConfig.CACerts, nil + } + status, err := s.listenConfigStorage.Get() + if err != nil { + return "", err + } + + if status.CACert == "" { + return "", fmt.Errorf("ca cert not found") + } + + return status.CACert, nil +} + +func (s *server) save() { + if s.activeCert != nil { + return + } + + s.Lock() + defer s.Unlock() + + changed := false + cfg, err := s.listenConfigStorage.Get() + if err != nil { + return + } + + if cfg.GeneratedCerts == nil { + cfg.GeneratedCerts = map[string]string{} + } + + if cfg.KnownIPs == nil { + cfg.KnownIPs = map[string]bool{} + } + + for key, cert := range s.certs { + certStr := certToString(cert) + if cfg.GeneratedCerts[key] != certStr { + cfg.GeneratedCerts[key] = certStr + changed = true + } + } + + for _, obj := range s.ips.Keys() { + ip, _ := obj.(string) + if !cfg.KnownIPs[ip] { + cfg.KnownIPs[ip] = true + changed = true + } + } + + if cfg.CAKey == "" && s.activeCAKey != nil && s.activeCA != nil { + caCertBuffer := bytes.Buffer{} + if err := pem.Encode(&caCertBuffer, &pem.Block{ + Type: cert.CertificateBlockType, + Bytes: s.activeCA.Raw, + }); err != nil { + return + } + + caKeyBuffer := bytes.Buffer{} + if err := pem.Encode(&caKeyBuffer, &pem.Block{ + Type: cert.RSAPrivateKeyBlockType, + Bytes: x509.MarshalPKCS1PrivateKey(s.activeCAKey), + }); err != nil { + return + } + + cfg.CACert = string(caCertBuffer.Bytes()) + cfg.CAKey = string(caKeyBuffer.Bytes()) + s.activeCAKeyString = cfg.CAKey + changed = true + } + + if changed { + s.listenConfigStorage.Set(cfg) + } +} + +func (s *server) userConfigure() error { + if s.userConfig.HTTPSPort == 0 { + s.userConfig.HTTPSPort = 8443 + } + + if s.userConfig.Mode == "" { + if len(s.userConfig.Domains) > 0 { + s.userConfig.Mode = acmeMode + } else { + s.userConfig.Mode = httpsMode + } + } + + s.domains = map[string]bool{} + for _, d := range s.userConfig.Domains { + s.domains[d] = true + } + + if s.userConfig.Key != "" && s.userConfig.Cert != "" { + cert, err := tls.X509KeyPair([]byte(s.userConfig.Cert), []byte(s.userConfig.Key)) + if err != nil { + return err + } + s.activeCert = &cert + s.userConfig.Mode = httpsMode + return s.reload() + } + + for _, ip := range s.userConfig.KnownIPs { + netIP := net.ParseIP(ip) + if netIP != nil { + s.ips.Add(ip, netIP) + } + } + bindAddress := net.ParseIP(s.userConfig.BindAddress) + if bindAddress != nil { + s.ips.Add(s.userConfig.BindAddress, bindAddress) + } + return nil +} + +func genCA() (*x509.Certificate, *rsa.PrivateKey, error) { + caKey, err := cert.NewPrivateKey() + if err != nil { + return nil, nil, err + } + + caCert, err := cert.NewSelfSignedCACert(cert.Config{ + CommonName: "k3s-ca", + Organization: []string{"k3s-org"}, + }, caKey) + if err != nil { + return nil, nil, err + } + + return caCert, caKey, nil +} + +func (s *server) Update(status *ListenerStatus) error { + s.Lock() + defer s.getCertificate(&tls.ClientHelloInfo{ServerName: "localhost"}) + + if status.CACert != "" && status.CAKey != "" && s.activeCAKeyString != status.CAKey { + cert, err := tls.X509KeyPair([]byte(status.CACert), []byte(status.CAKey)) + if err != nil { + s.Unlock() + return err + } + s.activeCAKey = cert.PrivateKey.(*rsa.PrivateKey) + s.activeCAKeyString = status.CAKey + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + s.Unlock() + return err + } + s.activeCA = x509Cert + s.certs = map[string]*tls.Certificate{} + } + + for ipStr := range status.KnownIPs { + ip := net.ParseIP(ipStr) + if len(ip) > 0 { + s.ips.ContainsOrAdd(ipStr, ip) + } + } + + for key, certString := range status.GeneratedCerts { + cert := stringToCert(certString) + if cert != nil { + s.certs[key] = cert + } + } + + s.Unlock() + return s.reload() +} + +func (s *server) hostPolicy(ctx context.Context, host string) error { + s.Lock() + defer s.Unlock() + + if s.domains[host] { + return nil + } + + return errors.New("acme/autocert: host not configured") +} + +func (s *server) prompt(tos string) bool { + return true +} + +func (s *server) shutdown() error { + for _, listener := range s.listeners { + if err := listener.Close(); err != nil { + return err + } + } + s.listeners = nil + + for _, server := range s.servers { + go server.Shutdown(context.Background()) + } + s.servers = nil + + return nil +} + +func (s *server) reload() error { + if len(s.listeners) > 0 { + return nil + } + + if err := s.shutdown(); err != nil { + return err + } + + switch s.userConfig.Mode { + case acmeMode: + if err := s.serveACME(); err != nil { + return err + } + case httpsMode: + if err := s.serveHTTPS(); err != nil { + return err + } + } + + return nil +} + +func (s *server) ipMapKey() string { + len := s.ips.Len() + keys := s.ips.Keys() + if len == 0 { + return fmt.Sprintf("local/%d", len) + } else if len == 1 { + return fmt.Sprintf("local/%s", keys[0]) + } + + sort.Slice(keys, func(i, j int) bool { + l, _ := keys[i].(string) + r, _ := keys[j].(string) + return l < r + }) + if len < 6 { + return fmt.Sprintf("local/%v", keys) + } + + digest := md5.New() + for _, k := range keys { + s, _ := k.(string) + digest.Write([]byte(s)) + } + + return fmt.Sprintf("local/%v", hex.EncodeToString(digest.Sum(nil))) +} + +func (s *server) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + s.Lock() + if s.activeCert != nil { + s.Unlock() + return s.activeCert, nil + } + + changed := false + defer func() { + if changed { + s.save() + } + }() + defer s.Unlock() + + mapKey := hello.ServerName + cn := hello.ServerName + dnsNames := []string{cn} + ipBased := false + var ips []net.IP + + if cn == "" { + mapKey = s.ipMapKey() + ipBased = true + } + + serverNameCert, ok := s.certs[mapKey] + if ok { + return serverNameCert, nil + } + + if ipBased { + cn = "cattle" + for _, ipStr := range s.ips.Keys() { + ip := net.ParseIP(ipStr.(string)) + if len(ip) > 0 { + ips = append(ips, ip) + } + } + } + + changed = true + + if s.activeCA == nil { + ca, key, err := genCA() + if err != nil { + return nil, err + } + s.activeCA = ca + s.activeCAKey = key + } + + cfg := cert.Config{ + CommonName: cn, + Organization: s.activeCA.Subject.Organization, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + AltNames: cert.AltNames{ + DNSNames: dnsNames, + IPs: ips, + }, + } + + key, err := cert.NewPrivateKey() + if err != nil { + return nil, err + } + + cert, err := cert.NewSignedCert(cfg, key, s.activeCA, s.activeCAKey) + if err != nil { + return nil, err + } + + tlsCert := &tls.Certificate{ + Certificate: [][]byte{ + cert.Raw, + }, + PrivateKey: key, + } + + s.certs[mapKey] = tlsCert + return tlsCert, nil +} + +func (s *server) cacheIPHandler(handler http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + h, _, err := net.SplitHostPort(req.Host) + if err != nil { + h = req.Host + } + + ip := net.ParseIP(h) + if len(ip) > 0 { + if ok, _ := s.ips.ContainsOrAdd(h, ip); ok { + go s.save() + } + } + + handler.ServeHTTP(resp, req) + }) +} + +func (s *server) serveHTTPS() error { + conf := &tls.Config{ + GetCertificate: s.getCertificate, + PreferServerCipherSuites: true, + } + + listener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPSPort, conf) + if err != nil { + return err + } + + logger := logrus.StandardLogger() + server := &http.Server{ + Handler: s.cacheIPHandler(s.Handler()), + ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags), + } + + s.servers = append(s.servers, server) + s.startServer(listener, server) + + if s.userConfig.HTTPPort > 0 { + httpListener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPPort, nil) + if err != nil { + return err + } + + httpServer := &http.Server{ + Handler: s.cacheIPHandler(httpRedirect(s.Handler())), + ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags), + } + + s.servers = append(s.servers, httpServer) + s.startServer(httpListener, httpServer) + } + + return nil +} + +// Approach taken from letsencrypt, except manglePort is specific to us +func httpRedirect(next http.Handler) http.Handler { + return http.HandlerFunc( + func(rw http.ResponseWriter, r *http.Request) { + if r.Header.Get("x-Forwarded-Proto") == "https" || + strings.HasPrefix(r.URL.Path, "/ping") || + strings.HasPrefix(r.URL.Path, "/health") { + next.ServeHTTP(rw, r) + return + } + if r.Method != "GET" && r.Method != "HEAD" { + http.Error(rw, "Use HTTPS", http.StatusBadRequest) + return + } + target := "https://" + manglePort(r.Host) + r.URL.RequestURI() + http.Redirect(rw, r, target, http.StatusFound) + }) +} + +func manglePort(hostport string) string { + host, port, err := net.SplitHostPort(hostport) + if err != nil { + return hostport + } + + portInt, err := strconv.Atoi(port) + if err != nil { + return hostport + } + + portInt = ((portInt / 1000) * 1000) + 443 + + return net.JoinHostPort(host, strconv.Itoa(portInt)) +} + +func (s *server) startServer(listener net.Listener, server *http.Server) { + go func() { + if err := server.Serve(listener); err != nil { + logrus.Errorf("server on %v returned err: %v", listener.Addr(), err) + } + }() +} + +func (s *server) Handler() http.Handler { + return s.userConfig.Handler +} + +func (s *server) newListener(ip string, port int, config *tls.Config) (net.Listener, error) { + addr := fmt.Sprintf("%s:%d", ip, port) + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + l = tcpKeepAliveListener{l.(*net.TCPListener)} + + if config != nil { + l = tls.NewListener(l, config) + } + + s.listeners = append(s.listeners, l) + logrus.Info("Listening on ", addr) + return l, nil +} + +func (s *server) serveACME() error { + manager := autocert.Manager{ + Cache: autocert.DirCache("certs-cache"), + Prompt: s.prompt, + HostPolicy: s.hostPolicy, + } + conf := &tls.Config{ + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if hello.ServerName == "localhost" || hello.ServerName == "" { + newHello := *hello + newHello.ServerName = s.userConfig.Domains[0] + return manager.GetCertificate(&newHello) + } + return manager.GetCertificate(hello) + }, + NextProtos: []string{"h2", "http/1.1"}, + } + + if s.userConfig.HTTPPort > 0 { + httpListener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPPort, nil) + if err != nil { + return err + } + + httpServer := &http.Server{ + Handler: manager.HTTPHandler(nil), + ErrorLog: log.New(logrus.StandardLogger().Writer(), "", log.LstdFlags), + } + s.servers = append(s.servers, httpServer) + go func() { + if err := httpServer.Serve(httpListener); err != nil { + logrus.Errorf("http server returned err: %v", err) + } + }() + + } + + httpsListener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPSPort, conf) + if err != nil { + return err + } + + httpsServer := &http.Server{ + Handler: s.Handler(), + ErrorLog: log.New(logrus.StandardLogger().Writer(), "", log.LstdFlags), + } + s.servers = append(s.servers, httpsServer) + go func() { + if err := httpsServer.Serve(httpsListener); err != nil { + logrus.Errorf("https server returned err: %v", err) + } + }() + + return nil +} + +func stringToCert(certString string) *tls.Certificate { + parts := strings.Split(certString, "#") + if len(parts) != 2 { + return nil + } + + cert, key := parts[0], parts[1] + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil + } + + rsaKey, err := x509.ParsePKCS1PrivateKey(keyBytes) + if err != nil { + return nil + } + + certBytes, err := base64.StdEncoding.DecodeString(cert) + if err != nil { + return nil + } + + return &tls.Certificate{ + Certificate: [][]byte{certBytes}, + PrivateKey: rsaKey, + } +} + +func certToString(cert *tls.Certificate) string { + certString := base64.StdEncoding.EncodeToString(cert.Certificate[0]) + keyString := base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(cert.PrivateKey.(*rsa.PrivateKey))) + return certString + "#" + keyString +} + +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..87527bd --- /dev/null +++ b/types.go @@ -0,0 +1,62 @@ +package dynamiclistener + +import ( + "net/http" +) + +type ListenerConfigStorage interface { + Set(*ListenerStatus) (*ListenerStatus, error) + Get() (*ListenerStatus, error) +} + +type ServerInterface interface { + Update(status *ListenerStatus) error + CACert() (string, error) +} + +type UserConfig struct { + // Required fields + + Handler http.Handler + HTTPPort int + HTTPSPort int + CertPath string + + // Optional fields + + KnownIPs []string + Domains []string + Mode string + NoCACerts bool + CACerts string + Cert string + Key string + BindAddress string +} + +type ListenerStatus struct { + Revision string `json:"revision,omitempty"` + CACert string `json:"caCert,omitempty"` + CAKey string `json:"caKey,omitempty"` + GeneratedCerts map[string]string `json:"generatedCerts" norman:"nocreate,noupdate"` + KnownIPs map[string]bool `json:"knownIps" norman:"nocreate,noupdate"` +} + +func (l *ListenerStatus) DeepCopyInto(t *ListenerStatus) { + t.Revision = l.Revision + t.CACert = l.CACert + t.CAKey = l.CAKey + t.GeneratedCerts = copyMap(t.GeneratedCerts) + t.KnownIPs = map[string]bool{} + for k, v := range l.KnownIPs { + t.KnownIPs[k] = v + } +} + +func copyMap(m map[string]string) map[string]string { + ret := map[string]string{} + for k, v := range m { + ret[k] = v + } + return ret +}