diff --git a/Makefile b/Makefile index 5b1258a5d..220c6f9ee 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,7 @@ update-vendor: k8s.io/client-go/... \ k8s.io/gengo/args \ k8s.io/apiextensions-apiserver \ + github.com/arangodb-helper/go-certificates \ github.com/arangodb/go-driver \ github.com/cenkalti/backoff \ github.com/dchest/uniuri \ diff --git a/deps/github.com/arangodb-helper/go-certificates/LICENSE b/deps/github.com/arangodb-helper/go-certificates/LICENSE new file mode 100644 index 000000000..b8ff39b5a --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/LICENSE @@ -0,0 +1,202 @@ + + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 ArangoDB GmbH + + 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. \ No newline at end of file diff --git a/deps/github.com/arangodb-helper/go-certificates/README.md b/deps/github.com/arangodb-helper/go-certificates/README.md new file mode 100644 index 000000000..571b6f8f5 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/README.md @@ -0,0 +1,4 @@ +# go-certificates + +Library for golang code related to creating certificates. + diff --git a/deps/github.com/arangodb-helper/go-certificates/ca.go b/deps/github.com/arangodb-helper/go-certificates/ca.go new file mode 100644 index 000000000..a3dcc85df --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/ca.go @@ -0,0 +1,101 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "strings" +) + +type CA struct { + Certificate []*x509.Certificate + PrivateKey interface{} +} + +// LoadCAFromPEM parses the given certificate & key into a CA instance. +func LoadCAFromPEM(cert, key string) (CA, error) { + certs, privKey, err := LoadFromPEM(cert, key) + if err != nil { + return CA{}, maskAny(err) + } + return CA{ + Certificate: certs, + PrivateKey: privKey, + }, nil +} + +// LoadFromPEM parses the given certificate & key into a certificate slice & private key. +func LoadFromPEM(cert, key string) ([]*x509.Certificate, interface{}, error) { + var certs []*x509.Certificate + + // Parse certificate + pemCerts := []byte(cert) + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, maskAny(err) + } + + certs = append(certs, c) + } + if len(certs) == 0 { + return nil, nil, maskAny(fmt.Errorf("No CERTIFICATE's found in '%s'", cert)) + } + + // Parse key + pemKey := []byte(key) + var privKey interface{} + for len(pemKey) > 0 { + var block *pem.Block + block, pemKey = pem.Decode(pemKey) + if block == nil { + break + } + + if block.Type == "PRIVATE KEY" || strings.HasSuffix(block.Type, " PRIVATE KEY") { + if privKey == nil { + var err error + privKey, err = parsePrivateKey(block.Bytes) + if err != nil { + return nil, nil, maskAny(err) + } + } + } + } + if privKey == nil { + return nil, nil, maskAny(fmt.Errorf("No PRIVATE KEY found in '%s'", key)) + } + + return certs, privKey, nil +} diff --git a/deps/github.com/arangodb-helper/go-certificates/cli/certificates.go b/deps/github.com/arangodb-helper/go-certificates/cli/certificates.go new file mode 100644 index 000000000..f749808e8 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/cli/certificates.go @@ -0,0 +1,403 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package cli + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + certificates "github.com/arangodb-helper/go-certificates" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + // TLS valid for defaults + defaultTLSValidFor = time.Hour * 24 * 365 * 2 // 2 years + defaultTLSCAValidFor = time.Hour * 24 * 365 * 15 // 15 years + // Client authentication valid for defaults + defaultClientAuthValidFor = time.Hour * 24 * 365 * 1 // 1 years + defaultClientAuthCAValidFor = time.Hour * 24 * 365 * 15 // 15 years +) + +var ( + logFatal func(error, string) + showUsage func(cmd *cobra.Command, args []string) + cmdCreate = &cobra.Command{ + Use: "create", + Short: "Create certificates", + Run: cmdShowUsage, + } + cmdCreateJWTSecret = &cobra.Command{ + Use: "jwt-secret", + Short: "Create a JWT secret and save it in a plain text file", + Run: cmdCreateJWTSecretRun, + } + cmdCreateTLS = &cobra.Command{ + Use: "tls", + Short: "Create TLS certificates", + Run: cmdShowUsage, + } + cmdCreateTLSCA = &cobra.Command{ + Use: "ca", + Short: "Create a CA certificate used to sign TLS certificates", + Run: cmdCreateTLSCARun, + } + cmdCreateTLSKeyFile = &cobra.Command{ + Use: "keyfile", + Short: "Create a TLS certificate and save it as keyfile", + Run: cmdCreateTLSKeyFileRun, + } + cmdCreateTLSCertificate = &cobra.Command{ + Use: "certificate", + Short: "Create a TLS certificate and save it as crt, key files", + Run: cmdCreateTLSCertificateRun, + } + cmdCreateTLSKeystore = &cobra.Command{ + Use: "keystore", + Short: "Create a TLS certificate and save it as java keystore files", + Run: cmdCreateTLSKeystoreRun, + } + cmdCreateClientAuth = &cobra.Command{ + Use: "client-auth", + Short: "Create client authentication certificates", + Run: cmdShowUsage, + } + cmdCreateClientAuthCA = &cobra.Command{ + Use: "ca", + Short: "Create a CA certificate used to sign client authentication certificates", + Run: cmdCreateClientAuthCARun, + } + cmdCreateClientAuthKeyFile = &cobra.Command{ + Use: "keyfile", + Short: "Create a client authentication certificate and save it as keyfile", + Run: cmdCreateClientAuthKeyFileRun, + } + + createOptions struct { + jwtsecret createJWTSecretOptions + tls struct { + ca createCAOptions + keyFile createKeyFileOptions + certificate createCertificateOptions + keystore createKeystoreOptions + } + clientAuth struct { + ca createCAOptions + keyFile createKeyFileOptions + } + } +) + +type createJWTSecretOptions struct { + secretFile string + secretLength int +} + +func (o *createJWTSecretOptions) ConfigureFlags(f *pflag.FlagSet) { + f.StringVar(&o.secretFile, "secret", "secret.jwt", "Filename of the generated JWT secret") + f.IntVar(&o.secretLength, "length", 32, "Number of bytes in the secret") +} + +func (o *createJWTSecretOptions) CreateSecret() { + if o.secretFile == "" { + logFatal(nil, "--secret missing") + } + + // Create secrey + secret := make([]byte, o.secretLength) + rand.Read(secret) + + // Encode as hex & store + encoded := hex.EncodeToString(secret) + mustWriteFile(encoded, o.secretFile, 0600, "secret") + + fmt.Printf("Created JWT secret in %s\n", o.secretFile) + fmt.Println("Make sure to store this files in a secure location.") +} + +type createCAOptions struct { + certFile string + keyFile string + validFor time.Duration + ecdsaCurve string +} + +func (o *createCAOptions) ConfigureFlags(f *pflag.FlagSet, defaultFName string, defaultValidFor time.Duration) { + f.StringVar(&o.certFile, "cert", defaultFName+".crt", "Filename of the generated CA certificate") + f.StringVar(&o.keyFile, "key", defaultFName+".key", "Filename of the generated CA private key") + f.DurationVar(&o.validFor, "validfor", defaultValidFor, "Lifetime of the certificate until expiration") + f.StringVar(&o.ecdsaCurve, "curve", "P521", "ECDSA curve used for private key") +} + +func (o *createCAOptions) CreateCA() { + // Create certificate + options := certificates.CreateCertificateOptions{ + ValidFor: o.validFor, + ECDSACurve: o.ecdsaCurve, + IsCA: true, + } + cert, key, err := certificates.CreateCertificate(options, nil) + if err != nil { + logFatal(err, "Failed to create certificate") + } + + // Save certificate + mustWriteFile(cert, o.certFile, 0644, "cert") + mustWriteFile(key, o.keyFile, 0600, "key") + + fmt.Printf("Created CA certificate & key in %s, %s\n", o.certFile, o.keyFile) + fmt.Println("Make sure to store these files in a secure location.") +} + +type createCertificateBaseOptions struct { + caCertFile string + caKeyFile string + hosts []string + emailAddresses []string + validFor time.Duration + ecdsaCurve string +} + +func (o *createCertificateBaseOptions) ConfigureFlags(f *pflag.FlagSet, defaultCAFName, defaultFName string, defaultValidFor time.Duration) { + f.StringVar(&o.caCertFile, "cacert", defaultCAFName+".crt", "File containing TLS CA certificate") + f.StringVar(&o.caKeyFile, "cakey", defaultCAFName+".key", "File containing TLS CA private key") + f.StringSliceVar(&o.hosts, "host", nil, "Host name to include in the certificate") + f.StringSliceVar(&o.emailAddresses, "email", nil, "Email address to include in the certificate") + f.DurationVar(&o.validFor, "validfor", defaultValidFor, "Lifetime of the certificate until expiration") + f.StringVar(&o.ecdsaCurve, "curve", "P521", "ECDSA curve used for private key") +} + +// Create a certificate from given options. +// Returns: certificate content, key content, ca-certificate content +func (o *createCertificateBaseOptions) CreateCertificate(isClientAuth bool) (string, string, string) { + // Load data + caCert := mustReadFile(o.caCertFile, "cacert") + caKey := mustReadFile(o.caKeyFile, "cakey") + ca, err := certificates.LoadCAFromPEM(caCert, caKey) + if err != nil { + logFatal(err, "Failed to load CA") + } + + // Create certificate + options := certificates.CreateCertificateOptions{ + Hosts: o.hosts, + EmailAddresses: o.emailAddresses, + ValidFor: o.validFor, + ECDSACurve: o.ecdsaCurve, + IsClientAuth: isClientAuth, + } + cert, key, err := certificates.CreateCertificate(options, &ca) + if err != nil { + logFatal(err, "Failed to create certificate") + } + + return cert, key, caCert +} + +type createKeyFileOptions struct { + createCertificateBaseOptions + keyFile string +} + +func (o *createKeyFileOptions) ConfigureFlags(f *pflag.FlagSet, defaultCAFName, defaultFName string, defaultValidFor time.Duration) { + o.createCertificateBaseOptions.ConfigureFlags(f, defaultCAFName, defaultFName, defaultValidFor) + f.StringVar(&o.keyFile, "keyfile", defaultFName+".keyfile", "Filename of keyfile to generate") +} + +func (o *createKeyFileOptions) CreateKeyFile(isClientAuth bool) { + // Create certificate + key + cert, key, _ := o.createCertificateBaseOptions.CreateCertificate(isClientAuth) + + // Save certificate + mustWriteKeyFile(cert, key, o.keyFile, "keyfile") + + fmt.Printf("Created keyfile in %s\n", o.keyFile) + fmt.Println("Make sure to store this file in a secure location.") +} + +type createCertificateOptions struct { + createCertificateBaseOptions + certFile string + keyFile string +} + +func (o *createCertificateOptions) ConfigureFlags(f *pflag.FlagSet, defaultCAFName, defaultFName string, defaultValidFor time.Duration) { + o.createCertificateBaseOptions.ConfigureFlags(f, defaultCAFName, defaultFName, defaultValidFor) + f.StringVar(&o.certFile, "cert", defaultFName+".crt", "Filename of the generated certificate") + f.StringVar(&o.keyFile, "key", defaultFName+".key", "Filename of the generated private key") +} + +func (o *createCertificateOptions) CreateCertificate(isClientAuth bool) { + // Create certificate + key + cert, key, _ := o.createCertificateBaseOptions.CreateCertificate(isClientAuth) + + // Save certificate + mustWriteFile(cert, o.certFile, 0644, "cert") + mustWriteFile(key, o.keyFile, 0600, "key") + + fmt.Printf("Created certificate & key in %s, %s\n", o.certFile, o.keyFile) + fmt.Println("Make sure to store these files in a secure location.") +} + +type createKeystoreOptions struct { + createCertificateBaseOptions + keystoreFile string + keystorePassword string + alias string +} + +func (o *createKeystoreOptions) ConfigureFlags(f *pflag.FlagSet, defaultCAFName, defaultFName string, defaultValidFor time.Duration) { + o.createCertificateBaseOptions.ConfigureFlags(f, defaultCAFName, defaultFName, defaultValidFor) + f.StringVar(&o.keystoreFile, "keystore", defaultFName+".jks", "Filename of the generated keystore") + f.StringVar(&o.keystorePassword, "keystore-password", "", "Password of the generated keystore") + f.StringVar(&o.alias, "alias", "", "Aliases use to store the certificate under in the keystore") +} + +func (o *createKeystoreOptions) CreateCertificate(isClientAuth bool) { + if o.alias == "" { + logFatal(nil, "--alias missing") + } + if o.keystorePassword == "" { + logFatal(nil, "--keystore-password missing") + } + // Create certificate + key + cert, key, caCert := o.createCertificateBaseOptions.CreateCertificate(isClientAuth) + + // Encode as keystore + ks, err := certificates.CreateKeystore(cert, key, caCert, o.alias, []byte(o.keystorePassword)) + if err != nil { + logFatal(err, "Failed to create keystore") + } + mustWriteFile(string(ks), o.keystoreFile, 0600, "keystore") + + fmt.Printf("Created keystore in %s\n", o.keystoreFile) + fmt.Println("Make sure to store this files in a secure location.") +} + +// AddCommands adds all creations commands to the given root command. +func AddCommands(cmd *cobra.Command, logFatalFunc func(error, string), showUsageFunc func(cmd *cobra.Command, args []string)) { + logFatal = logFatalFunc + showUsage = showUsageFunc + + cmd.AddCommand(cmdCreate) + cmdCreate.AddCommand(cmdCreateJWTSecret) + cmdCreate.AddCommand(cmdCreateTLS) + cmdCreateTLS.AddCommand(cmdCreateTLSCA) + cmdCreateTLS.AddCommand(cmdCreateTLSKeyFile) + cmdCreateTLS.AddCommand(cmdCreateTLSCertificate) + cmdCreateTLS.AddCommand(cmdCreateTLSKeystore) + cmdCreate.AddCommand(cmdCreateClientAuth) + cmdCreateClientAuth.AddCommand(cmdCreateClientAuthCA) + cmdCreateClientAuth.AddCommand(cmdCreateClientAuthKeyFile) + + createOptions.jwtsecret.ConfigureFlags(cmdCreateJWTSecret.Flags()) + createOptions.tls.ca.ConfigureFlags(cmdCreateTLSCA.Flags(), "tls-ca", defaultTLSCAValidFor) + createOptions.tls.keyFile.ConfigureFlags(cmdCreateTLSKeyFile.Flags(), "tls-ca", "tls", defaultTLSValidFor) + createOptions.tls.certificate.ConfigureFlags(cmdCreateTLSCertificate.Flags(), "tls-ca", "tls", defaultTLSValidFor) + createOptions.tls.keystore.ConfigureFlags(cmdCreateTLSKeystore.Flags(), "tls-ca", "tls", defaultTLSValidFor) + createOptions.clientAuth.ca.ConfigureFlags(cmdCreateClientAuthCA.Flags(), "client-auth-ca", defaultClientAuthCAValidFor) + createOptions.clientAuth.keyFile.ConfigureFlags(cmdCreateClientAuthKeyFile.Flags(), "client-auth-ca", "client-auth", defaultClientAuthValidFor) +} + +// Cobra run function using the usage of the given command +func cmdShowUsage(cmd *cobra.Command, args []string) { + showUsage(cmd, args) +} + +// cmdCreateJWTSecretRun creates a JWT secret and writes it to file +func cmdCreateJWTSecretRun(cmd *cobra.Command, args []string) { + createOptions.jwtsecret.CreateSecret() +} + +// cmdCreateTLSCARun creates a CA used to sign TLS certificates +func cmdCreateTLSCARun(cmd *cobra.Command, args []string) { + createOptions.tls.ca.CreateCA() +} + +// cmdCreateTLSKeyFileRun creates a TLS certificate and save it as keyfile +func cmdCreateTLSKeyFileRun(cmd *cobra.Command, args []string) { + isClientAuth := false + createOptions.tls.keyFile.CreateKeyFile(isClientAuth) +} + +// cmdCreateTLSCertificateRun creates a TLS certificate and save it as crt+key file +func cmdCreateTLSCertificateRun(cmd *cobra.Command, args []string) { + isClientAuth := false + createOptions.tls.certificate.CreateCertificate(isClientAuth) +} + +// cmdCreateTLSKeystoreRun creates a TLS certificate and save it as java keystore file. +func cmdCreateTLSKeystoreRun(cmd *cobra.Command, args []string) { + isClientAuth := false + createOptions.tls.keystore.CreateCertificate(isClientAuth) +} + +// cmdCreateClientAuthCARun creates a CA used to sign client authentication certificates +func cmdCreateClientAuthCARun(cmd *cobra.Command, args []string) { + createOptions.clientAuth.ca.CreateCA() +} + +// cmdCreateClientAuthKeyFileRun creates a client authentication certificate and save it as keyfile +func cmdCreateClientAuthKeyFileRun(cmd *cobra.Command, args []string) { + isClientAuth := true + createOptions.clientAuth.keyFile.CreateKeyFile(isClientAuth) +} + +func mustWriteKeyFile(cert, key string, filename string, flagName string) { + if filename == "" { + logFatal(nil, fmt.Sprintf("Missing filename for option --%s", flagName)) + } + if err := certificates.SaveKeyFile(cert, key, filename); err != nil { + logFatal(err, fmt.Sprintf("Failed to write %s", filename)) + } +} + +func mustWriteFile(content string, filename string, mode os.FileMode, flagName string) { + if filename == "" { + logFatal(nil, fmt.Sprintf("Missing filename for option --%s", flagName)) + } + folder := filepath.Dir(filename) + if folder != "" { + os.MkdirAll(folder, 0755) + } + if err := ioutil.WriteFile(filename, []byte(content), mode); err != nil { + logFatal(err, fmt.Sprintf("Failed to write %s", filename)) + } +} + +func mustReadFile(filename string, flagName string) string { + if filename == "" { + logFatal(nil, fmt.Sprintf("Missing filename for option --%s", flagName)) + } + content, err := ioutil.ReadFile(filename) + if err != nil { + logFatal(err, fmt.Sprintf("Failed to read %s", filename)) + } + return string(content) +} diff --git a/deps/github.com/arangodb-helper/go-certificates/create.go b/deps/github.com/arangodb-helper/go-certificates/create.go new file mode 100644 index 000000000..0cd489bee --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/create.go @@ -0,0 +1,195 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "time" +) + +const ( + defaultValidFor = time.Hour * 24 * 365 + defaultRSABits = 2048 +) + +type CreateCertificateOptions struct { + CommonName string // Common name set in the certificate. If not specified, defaults to first email address, then first host and if all not set 'ArangoDB'. + Hosts []string // Comma-separated hostnames and IPs to generate a certificate for + EmailAddresses []string // List of email address to include in the certificate as alternative name + ValidFrom time.Time // Creation data of the certificate + ValidFor time.Duration // Duration that certificate is valid for + IsCA bool // Whether this cert should be its own Certificate Authority + IsClientAuth bool // Whether this cert can be used for client authentication + RSABits int // Size of RSA key to generate. Ignored if ECDSACurve is set + ECDSACurve string // ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521 +} + +// CreateCertificate creates a certificate according to the given configuration. +// If ca is nil, the certificate will be self-signed, otherwise the certificate +// will be signed by the given CA certificate+key. +// The resulting certificate + private key will be PEM encoded and returned as string (cert, priv, error). +func CreateCertificate(options CreateCertificateOptions, ca *CA) (string, string, error) { + // Create private key + var priv interface{} + var err error + switch options.ECDSACurve { + case "": + if options.RSABits == 0 { + options.RSABits = defaultRSABits + } + priv, err = rsa.GenerateKey(rand.Reader, options.RSABits) + case "P224": + priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + case "P256": + priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case "P384": + priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case "P521": + priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + default: + return "", "", maskAny(fmt.Errorf("Unknown curve '%s'", options.ECDSACurve)) + } + + notBefore := time.Now() + if options.ValidFor == 0 { + options.ValidFor = defaultValidFor + } + notAfter := notBefore.Add(options.ValidFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return "", "", maskAny(fmt.Errorf("failed to generate serial number: %v", err)) + } + + commonName := "ArangoDB" + if options.CommonName != "" { + commonName = options.CommonName + } else if len(options.EmailAddresses) > 0 { + commonName = options.EmailAddresses[0] + } else if len(options.Hosts) > 0 { + commonName = options.Hosts[0] + } + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: commonName, + Organization: []string{"ArangoDB"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + BasicConstraintsValid: true, + } + + for _, h := range options.Hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + template.EmailAddresses = append(template.EmailAddresses, options.EmailAddresses...) + + if options.IsCA { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + if options.IsClientAuth { + template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + } else { + template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + } + + // Create the certificate + var derBytes []byte + if ca != nil { + derBytes, err = x509.CreateCertificate(rand.Reader, &template, ca.Certificate[0], publicKey(priv), ca.PrivateKey) + if err != nil { + return "", "", maskAny(fmt.Errorf("Failed to create signed certificate: %v", err)) + } + } else { + derBytes, err = x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) + if err != nil { + return "", "", maskAny(fmt.Errorf("Failed to create self-signed certificate: %v", err)) + } + } + + // Encode certificate + // Public key + buf := &bytes.Buffer{} + pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if ca != nil { + for _, c := range ca.Certificate { + pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}) + } + } + certPem := buf.String() + + // Private key + buf = &bytes.Buffer{} + pem.Encode(buf, pemBlockForKey(priv)) + privPem := buf.String() + + return certPem, privPem, nil +} + +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return nil + } +} + +func pemBlockForKey(priv interface{}) *pem.Block { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} + case *ecdsa.PrivateKey: + b, err := x509.MarshalECPrivateKey(k) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to marshal ECDSA private key: %v", err) + os.Exit(2) + } + return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + default: + return nil + } +} diff --git a/deps/github.com/arangodb-helper/go-certificates/error.go b/deps/github.com/arangodb-helper/go-certificates/error.go new file mode 100644 index 000000000..62a4021b3 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/error.go @@ -0,0 +1,29 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import "github.com/pkg/errors" + +var ( + maskAny = errors.WithStack +) diff --git a/deps/github.com/arangodb-helper/go-certificates/expiration_date.go b/deps/github.com/arangodb-helper/go-certificates/expiration_date.go new file mode 100644 index 000000000..534492894 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/expiration_date.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "crypto/tls" + "crypto/x509" + "time" +) + +// GetCertificateExpirationDate returns the expiration date of the TLS certificate +// found in the given config. +// Returns: ExpirationDate, FoundExpirationDate +func GetCertificateExpirationDate(config *tls.Config) (time.Time, bool) { + if config == nil || len(config.Certificates) == 0 { + return time.Time{}, false + } + var expDate time.Time + found := false + for _, raw := range config.Certificates[0].Certificate { + if c, err := x509.ParseCertificate(raw); err == nil { + d := c.NotAfter + if !found || d.Before(expDate) { + expDate = d + } + found = true + } + } + return expDate, found +} diff --git a/deps/github.com/arangodb-helper/go-certificates/keyfile.go b/deps/github.com/arangodb-helper/go-certificates/keyfile.go new file mode 100644 index 000000000..1f57e2c91 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/keyfile.go @@ -0,0 +1,164 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// LoadKeyFile loads a SSL keyfile formatted for the arangod server. +func LoadKeyFile(keyFile string) (tls.Certificate, error) { + raw, err := ioutil.ReadFile(keyFile) + if err != nil { + return tls.Certificate{}, maskAny(err) + } + + result := tls.Certificate{} + for { + var derBlock *pem.Block + derBlock, raw = pem.Decode(raw) + if derBlock == nil { + break + } + if derBlock.Type == "CERTIFICATE" { + result.Certificate = append(result.Certificate, derBlock.Bytes) + } else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { + if result.PrivateKey == nil { + result.PrivateKey, err = parsePrivateKey(derBlock.Bytes) + if err != nil { + return tls.Certificate{}, maskAny(err) + } + } + } + } + + if len(result.Certificate) == 0 { + return tls.Certificate{}, maskAny(fmt.Errorf("No certificates found in %s", keyFile)) + } + if result.PrivateKey == nil { + return tls.Certificate{}, maskAny(fmt.Errorf("No private key found in %s", keyFile)) + } + + return result, nil +} + +// ExtractCACertificateFromKeyFile loads a SSL keyfile formatted for the arangod server and +// extracts the CA certificate(s) from it (if any). +func ExtractCACertificateFromKeyFile(keyFile string) (string, error) { + raw, err := ioutil.ReadFile(keyFile) + if err != nil { + return "", maskAny(err) + } + + buf := &bytes.Buffer{} + certificatesFound := 0 + for { + var derBlock *pem.Block + derBlock, raw = pem.Decode(raw) + if derBlock == nil { + break + } + if derBlock.Type == "CERTIFICATE" { + certificatesFound++ + c, err := x509.ParseCertificate(derBlock.Bytes) + if err != nil { + return "", maskAny(err) + } + if c.IsCA { + pem.Encode(buf, derBlock) + } + } + } + + certPem := buf.String() + if certificatesFound == 0 { + return "", maskAny(fmt.Errorf("No certificates found in %s", keyFile)) + } + return certPem, nil +} + +// SaveKeyFile creates a keyfile with given certificate & key data +func SaveKeyFile(cert, key string, filename string) error { + folder := filepath.Dir(filename) + if folder != "" { + os.MkdirAll(folder, 0755) + } + content := strings.TrimSpace(cert) + "\n" + strings.TrimSpace(key) + if err := ioutil.WriteFile(filename, []byte(content), 0600); err != nil { + return maskAny(err) + } + return nil +} + +// 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, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey: + return key, nil + default: + return nil, maskAny(errors.New("tls: found unknown private key type in PKCS#8 wrapping")) + } + } + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + + return nil, maskAny(errors.New("tls: failed to parse private key")) +} + +// EncodeToString encodes the given certification information into +// 2 strings. The first containing all certificates (PEM encoded), +// the second containing the private key (PEM encoded). +func EncodeToString(c tls.Certificate) (cert, key string) { + // Encode certificates + buf := &bytes.Buffer{} + for _, x := range c.Certificate { + pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: x}) + } + certPem := buf.String() + + // Private key + buf = &bytes.Buffer{} + pem.Encode(buf, pemBlockForKey(c.PrivateKey)) + privPem := buf.String() + + return certPem, privPem +} diff --git a/deps/github.com/arangodb-helper/go-certificates/keystore.go b/deps/github.com/arangodb-helper/go-certificates/keystore.go new file mode 100644 index 000000000..20ec57bc7 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/keystore.go @@ -0,0 +1,181 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + "time" + + keystore "github.com/pavel-v-chernykh/keystore-go" + + "github.com/pkg/errors" +) + +// CreateKeystore creates a java keystore containing the given certificate, +// private key & ca certificate(s). +func CreateKeystore(cert, key, caCert string, alias string, keystorePassword []byte) ([]byte, error) { + ks := make(keystore.KeyStore) + + // Decode CA cert + ksCACerts, err := decodeCACertificates(caCert) + if err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to decode CA certificates")) + } + for alias, ksCACert := range ksCACerts { + ks[alias] = &keystore.TrustedCertificateEntry{ + Entry: keystore.Entry{CreationDate: time.Now()}, + Certificate: ksCACert, + } + } + + // Decode certificate + ksCerts, err := decodeCertificates(cert) + if err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to decode certificate")) + } + + // Decode private key + pk, err := decodePrivateKey(key) + if err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to decode private key")) + } + encPK, err := convertPrivateKeyToPKCS8(pk) + if err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to encode private key")) + } + ks[alias] = &keystore.PrivateKeyEntry{ + Entry: keystore.Entry{CreationDate: time.Now()}, + PrivKey: encPK, + CertChain: ksCerts, + } + + // Encode keystore + buf := &bytes.Buffer{} + if err := keystore.Encode(buf, ks, keystorePassword); err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to encode keystore")) + } + + return buf.Bytes(), nil +} + +// decodeCACertificates takes a PEM encoded string and decodes all certificates +// in it into a map of alias+certificate pairs. +func decodeCACertificates(pemContent string) (map[string]keystore.Certificate, error) { + ksCerts, err := decodeCertificates(pemContent) + if err != nil { + return nil, maskAny(err) + } + + result := map[string]keystore.Certificate{} + for _, ksCert := range ksCerts { + caCerts, err := x509.ParseCertificates(ksCert.Content) + if err != nil { + return nil, maskAny(err) + } + if len(caCerts) == 0 { + return nil, maskAny(errors.New("Failed to parse CA certificate")) + } + + for _, caCert := range caCerts { + commonName := caCert.Subject.CommonName + if commonName == "" { + return nil, maskAny(fmt.Errorf("Missing common name in CA certificate '%s'", caCert.Subject)) + } + alias := strings.Replace(strings.ToLower(commonName), " ", "", -1) + result[alias] = ksCert + } + } + return result, nil +} + +// decodeCertificates takes a PEM encoded string and decodes it a list of +// keystore certificates. +func decodeCertificates(pemContent string) ([]keystore.Certificate, error) { + if pemContent == "" { + return nil, nil + } + blocks, err := decodePEMString(pemContent) + if err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to decode certificates")) + } + var result []keystore.Certificate + for _, b := range blocks { + if b.Type == "CERTIFICATE" { + result = append(result, keystore.Certificate{ + Type: "X509", + Content: b.Bytes, + }) + } else { + return nil, maskAny(fmt.Errorf("Unexpected block of type '%s' in CA certificates", b.Type)) + } + } + return result, nil +} + +// decodePrivateKey takes a PEM encoded string and decodes its private key entry. +func decodePrivateKey(pemContent string) (interface{}, error) { + blocks, err := decodePEMString(pemContent) + if err != nil { + return nil, maskAny(errors.Wrap(err, "Failed to decode private key")) + } + var result interface{} + for _, b := range blocks { + if b.Type == "PRIVATE KEY" || strings.HasSuffix(b.Type, " PRIVATE KEY") { + if result != nil { + return nil, maskAny(errors.New("Found multiple private keys")) + } + privKey, err := parsePrivateKey(b.Bytes) + if err != nil { + return nil, maskAny(err) + } + result = privKey + } else { + return nil, maskAny(fmt.Errorf("Unexpected block of type '%s' in CA certificates", b.Type)) + } + } + return result, nil +} + +// decodePEMString takes a PEM encoded string and decodes it into pem blocks. +func decodePEMString(pemContent string) ([]*pem.Block, error) { + var blocks []*pem.Block + content := []byte(pemContent) + for { + b, remaining := pem.Decode(content) + if b == nil { + if len(blocks) > 0 { + return blocks, nil + } + return nil, maskAny(errors.New("failed to decode PEM blocks")) + } + blocks = append(blocks, b) + if len(remaining) == 0 { + return blocks, nil + } + content = remaining + } +} diff --git a/deps/github.com/arangodb-helper/go-certificates/pkcs8.go b/deps/github.com/arangodb-helper/go-certificates/pkcs8.go new file mode 100644 index 000000000..0fb80e13d --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/pkcs8.go @@ -0,0 +1,148 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + + "github.com/pkg/errors" +) + +// Unecrypted PKCS8 +var ( + oidPKCS5PBKDF2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 12} + oidPBES2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + oidAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} +) + +// Copy from crypto/x509 +var ( + oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} +) + +// Copy from crypto/x509 +var ( + oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33} + oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} + oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34} + oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35} +) + +// Copy from crypto/x509 +func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, bool) { + switch curve { + case elliptic.P224(): + return oidNamedCurveP224, true + case elliptic.P256(): + return oidNamedCurveP256, true + case elliptic.P384(): + return oidNamedCurveP384, true + case elliptic.P521(): + return oidNamedCurveP521, true + } + + return nil, false +} + +type privateKeyInfo struct { + Version int + PrivateKeyAlgorithm []asn1.ObjectIdentifier + PrivateKey []byte +} + +// Encrypted PKCS8 +/*type pbkdf2Params struct { + Salt []byte + IterationCount int +} + +type pbkdf2Algorithms struct { + IDPBKDF2 asn1.ObjectIdentifier + PBKDF2Params pbkdf2Params +} + +type pbkdf2Encs struct { + EncryAlgo asn1.ObjectIdentifier + IV []byte +} + +type pbes2Params struct { + KeyDerivationFunc pbkdf2Algorithms + EncryptionScheme pbkdf2Encs +} + +type pbes2Algorithms struct { + IDPBES2 asn1.ObjectIdentifier + PBES2Params pbes2Params +} + +type encryptedPrivateKeyInfo struct { + EncryptionAlgorithm pbes2Algorithms + EncryptedData []byte +}*/ + +func convertPrivateKeyToPKCS8(priv interface{}) (der []byte, err error) { + var rb []byte + var pkey privateKeyInfo + + switch priv := priv.(type) { + case *ecdsa.PrivateKey: + eckey, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, maskAny(err) + } + + oidNamedCurve, ok := oidFromNamedCurve(priv.Curve) + if !ok { + return nil, maskAny(errors.New("pkcs8: unknown elliptic curve")) + } + + // Per RFC5958, if publicKey is present, then version is set to v2(1) else version is set to v1(0). + // But openssl set to v1 even publicKey is present + pkey.Version = 0 + pkey.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 2) + pkey.PrivateKeyAlgorithm[0] = oidPublicKeyECDSA + pkey.PrivateKeyAlgorithm[1] = oidNamedCurve + pkey.PrivateKey = eckey + case *rsa.PrivateKey: + + // Per RFC5958, if publicKey is present, then version is set to v2(1) else version is set to v1(0). + // But openssl set to v1 even publicKey is present + pkey.Version = 0 + pkey.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 1) + pkey.PrivateKeyAlgorithm[0] = oidPublicKeyRSA + pkey.PrivateKey = x509.MarshalPKCS1PrivateKey(priv) + } + + rb, err = asn1.Marshal(pkey) + if err != nil { + return nil, maskAny(err) + } + + return rb, nil +} diff --git a/deps/github.com/arangodb-helper/go-certificates/pool.go b/deps/github.com/arangodb-helper/go-certificates/pool.go new file mode 100644 index 000000000..d22de43e3 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/pool.go @@ -0,0 +1,41 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "crypto/x509" + "fmt" +) + +// LoadCertPool creates a certificate pool from the certificate(s) given in the +// given PEM encoded string. +func LoadCertPool(certificate string) (*x509.CertPool, error) { + if certificate == "" { + return nil, nil + } + certpool := x509.NewCertPool() + if success := certpool.AppendCertsFromPEM([]byte(certificate)); !success { + return nil, maskAny(fmt.Errorf("Invalid certificate")) + } + return certpool, nil +} diff --git a/deps/github.com/arangodb-helper/go-certificates/tls_authentication.go b/deps/github.com/arangodb-helper/go-certificates/tls_authentication.go new file mode 100644 index 000000000..1e5698b81 --- /dev/null +++ b/deps/github.com/arangodb-helper/go-certificates/tls_authentication.go @@ -0,0 +1,53 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package certificates + +import ( + "crypto/tls" +) + +type TLSAuthentication interface { + CACertificate() string + ClientCertificate() string + ClientKey() string +} + +// CreateTLSConfigFromAuthentication creates a tls.Config object from given configuration. +func CreateTLSConfigFromAuthentication(a TLSAuthentication, insecureSkipVerify bool) (*tls.Config, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + var err error + tlsConfig.RootCAs, err = LoadCertPool(a.CACertificate()) + if err != nil { + return nil, maskAny(err) + } + if a.ClientCertificate() != "" && a.ClientKey() != "" { + clientCert, err := tls.X509KeyPair([]byte(a.ClientCertificate()), []byte(a.ClientKey())) + if err != nil { + return nil, maskAny(err) + } + tlsConfig.Certificates = []tls.Certificate{clientCert} + } + return tlsConfig, nil +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/.gitignore b/deps/github.com/pavel-v-chernykh/keystore-go/.gitignore new file mode 100644 index 000000000..edaa03578 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/.gitignore @@ -0,0 +1,18 @@ +*.o +*.a +*.so +_obj +_test +*.[568vq] +[568vq].out +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* +_testmain.go +*.exe +*.test +*.prof +*.iml +.idea diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/LICENSE b/deps/github.com/pavel-v-chernykh/keystore-go/LICENSE new file mode 100644 index 000000000..b6f8c3a14 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Pavel Chernykh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/README.md b/deps/github.com/pavel-v-chernykh/keystore-go/README.md new file mode 100644 index 000000000..6425310b9 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/README.md @@ -0,0 +1,64 @@ +# Keystore +A go (golang) implementation of Java [KeyStore][1] encoder/decoder + +Take into account that JKS assumes that private keys are PKCS8 encoded. + +### Example + +```go +package main + +import ( + "github.com/pavel-v-chernykh/keystore-go" + "log" + "os" + "reflect" +) + +func readKeyStore(filename string, password []byte) keystore.KeyStore { + f, err := os.Open(filename) + defer f.Close() + if err != nil { + log.Fatal(err) + } + keyStore, err := keystore.Decode(f, password) + if err != nil { + log.Fatal(err) + } + return keyStore +} + +func writeKeyStore(keyStore keystore.KeyStore, filename string, password []byte) { + o, err := os.Create(filename) + defer o.Close() + if err != nil { + log.Fatal(err) + } + err = keystore.Encode(o, keyStore, password) + if err != nil { + log.Fatal(err) + } +} + +func zeroing(s []byte) { + for i := 0; i < len(s); i++ { + s[i] = 0 + } +} + +func main() { + password := []byte{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'} + defer zeroing(password) + ks1 := readKeyStore("keystore.jks", password) + + writeKeyStore(ks1, "keystore2.jks", password) + + ks2 := readKeyStore("keystore2.jks", password) + + log.Printf("Is equal: %v\n", reflect.DeepEqual(ks1, ks2)) +} +``` + +For more examples explore [examples](examples) dir + +[1]: https://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html#KeyManagement diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/common.go b/deps/github.com/pavel-v-chernykh/keystore-go/common.go new file mode 100644 index 000000000..48e7604a0 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/common.go @@ -0,0 +1,43 @@ +package keystore + +import ( + "encoding/binary" + "time" +) + +const magic uint32 = 0xfeedfeed +const ( + version01 uint32 = 1 + version02 uint32 = 2 +) +const ( + privateKeyTag uint32 = 1 + trustedCertificateTag uint32 = 2 +) +const bufSize = 1024 + +var order = binary.BigEndian + +var whitenerMessage = []byte("Mighty Aphrodite") + +func passwordBytes(password []byte) []byte { + passwdBytes := make([]byte, 0, len(password)*2) + for _, b := range password { + passwdBytes = append(passwdBytes, 0, b) + } + return passwdBytes +} + +func zeroing(s []byte) { + for i := 0; i < len(s); i++ { + s[i] = 0 + } +} + +func millisecondsToTime(ms int64) time.Time { + return time.Unix(0, ms*int64(time.Millisecond)) +} + +func timeToMilliseconds(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/common_test.go b/deps/github.com/pavel-v-chernykh/keystore-go/common_test.go new file mode 100644 index 000000000..65c1660fe --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/common_test.go @@ -0,0 +1,54 @@ +package keystore + +import ( + "crypto/rand" + "reflect" + "testing" +) + +func TestZeroing(t *testing.T) { + type zeroingItem struct { + input []byte + } + type zeroingTable []zeroingItem + + var table zeroingTable + for i := 0; i < 20; i++ { + buf := make([]byte, 4096) + rand.Read(buf) + table = append(table, zeroingItem{input: buf}) + } + + for _, tt := range table { + zeroing(tt.input) + for i := range tt.input { + if tt.input[i] != 0 { + t.Errorf("Invalid zeroing '%v'", tt.input) + } + } + } +} + +func TestPasswordBytes(t *testing.T) { + type passwordBytesItem struct { + input []byte + output []byte + } + var table []passwordBytesItem + for i := 0; i < 20; i++ { + ibuf := make([]byte, 1024) + rand.Read(ibuf) + obuf := make([]byte, len(ibuf)*2) + for j, k := 0, 0; j < len(obuf); j, k = j+2, k+1 { + obuf[j] = 0 + obuf[j+1] = ibuf[k] + } + table = append(table, passwordBytesItem{input: ibuf, output: obuf}) + } + for _, tt := range table { + output := passwordBytes(tt.input) + if !reflect.DeepEqual(output, tt.output) { + t.Errorf("Invalid output '%v', '%v'", output, tt.output) + } + } +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/decoder.go b/deps/github.com/pavel-v-chernykh/keystore-go/decoder.go new file mode 100644 index 000000000..3bd2ce9f6 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/decoder.go @@ -0,0 +1,273 @@ +package keystore + +import ( + "crypto/sha1" + "errors" + "hash" + "io" +) + +const defaultCertificateType = "X509" + +// ErrIo indicates i/o error +var ErrIo = errors.New("keystore: invalid keystore format") + +// ErrIncorrectMagic indicates incorrect file magic +var ErrIncorrectMagic = errors.New("keystore: invalid keystore format") + +// ErrIncorrectVersion indicates incorrect keystore version format +var ErrIncorrectVersion = errors.New("keystore: invalid keystore format") + +// ErrIncorrectTag indicates incorrect keystore entry tag +var ErrIncorrectTag = errors.New("keystore: invalid keystore format") + +// ErrIncorrectPrivateKey indicates incorrect private key entry content +var ErrIncorrectPrivateKey = errors.New("keystore: invalid private key format") + +// ErrInvalidDigest indicates that keystore was tampered or password was incorrect +var ErrInvalidDigest = errors.New("keystore: invalid digest") + +type keyStoreDecoder struct { + r io.Reader + b [bufSize]byte + md hash.Hash +} + +func (ksd *keyStoreDecoder) readUint16() (uint16, error) { + const blockSize = 2 + _, err := io.ReadFull(ksd.r, ksd.b[:blockSize]) + if err != nil { + return 0, ErrIo + } + _, err = ksd.md.Write(ksd.b[:blockSize]) + if err != nil { + return 0, err + } + return order.Uint16(ksd.b[:blockSize]), nil +} + +func (ksd *keyStoreDecoder) readUint32() (uint32, error) { + const blockSize = 4 + _, err := io.ReadFull(ksd.r, ksd.b[:blockSize]) + if err != nil { + return 0, ErrIo + } + _, err = ksd.md.Write(ksd.b[:blockSize]) + if err != nil { + return 0, err + } + return order.Uint32(ksd.b[:blockSize]), nil +} + +func (ksd *keyStoreDecoder) readUint64() (uint64, error) { + const blockSize = 8 + _, err := io.ReadFull(ksd.r, ksd.b[:blockSize]) + if err != nil { + return 0, ErrIo + } + _, err = ksd.md.Write(ksd.b[:blockSize]) + if err != nil { + return 0, err + } + return order.Uint64(ksd.b[:blockSize]), nil +} + +func (ksd *keyStoreDecoder) readBytes(num uint32) ([]byte, error) { + var result []byte + for lenToRead := num; lenToRead > 0; { + blockSize := lenToRead + if blockSize > bufSize { + blockSize = bufSize + } + _, err := io.ReadFull(ksd.r, ksd.b[:blockSize]) + if err != nil { + return result, ErrIo + } + result = append(result, ksd.b[:blockSize]...) + lenToRead -= blockSize + } + _, err := ksd.md.Write(result) + if err != nil { + return nil, err + } + return result, nil +} + +func (ksd *keyStoreDecoder) readString() (string, error) { + strLen, err := ksd.readUint16() + if err != nil { + return "", err + } + bytes, err := ksd.readBytes(uint32(strLen)) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func (ksd *keyStoreDecoder) readCertificate(version uint32) (*Certificate, error) { + var certType string + switch version { + case version01: + certType = defaultCertificateType + case version02: + readCertType, err := ksd.readString() + if err != nil { + return nil, err + } + certType = readCertType + default: + return nil, ErrIncorrectVersion + } + certLen, err := ksd.readUint32() + if err != nil { + return nil, err + } + certContent, err := ksd.readBytes(certLen) + if err != nil { + return nil, err + } + certificate := Certificate{ + Type: certType, + Content: certContent, + } + return &certificate, nil +} + +func (ksd *keyStoreDecoder) readPrivateKeyEntry(version uint32, password []byte) (*PrivateKeyEntry, error) { + creationDateTimeStamp, err := ksd.readUint64() + if err != nil { + return nil, err + } + privKeyLen, err := ksd.readUint32() + if err != nil { + return nil, err + } + encodedPrivateKeyContent, err := ksd.readBytes(privKeyLen) + if err != nil { + return nil, err + } + certCount, err := ksd.readUint32() + if err != nil { + return nil, err + } + var chain []Certificate + for i := certCount; i > 0; i-- { + cert, err := ksd.readCertificate(version) + if err != nil { + return nil, err + } + chain = append(chain, *cert) + } + plainPrivateKeyContent, err := recoverKey(encodedPrivateKeyContent, password) + if err != nil { + return nil, err + } + creationDateTime := millisecondsToTime(int64(creationDateTimeStamp)) + privateKeyEntry := PrivateKeyEntry{ + Entry: Entry{ + CreationDate: creationDateTime, + }, + PrivKey: plainPrivateKeyContent, + CertChain: chain, + } + return &privateKeyEntry, nil +} + +func (ksd *keyStoreDecoder) readTrustedCertificateEntry(version uint32) (*TrustedCertificateEntry, error) { + creationDateTimeStamp, err := ksd.readUint64() + if err != nil { + return nil, err + } + cert, err := ksd.readCertificate(version) + if err != nil { + return nil, err + } + creationDateTime := millisecondsToTime(int64(creationDateTimeStamp)) + trustedCertificateEntry := TrustedCertificateEntry{ + Entry: Entry{ + CreationDate: creationDateTime, + }, + Certificate: *cert, + } + return &trustedCertificateEntry, nil +} + +func (ksd *keyStoreDecoder) readEntry(version uint32, password []byte) (string, interface{}, error) { + tag, err := ksd.readUint32() + if err != nil { + return "", nil, err + } + alias, err := ksd.readString() + if err != nil { + return "", nil, err + } + switch tag { + case privateKeyTag: + entry, err := ksd.readPrivateKeyEntry(version, password) + if err != nil { + return "", nil, err + } + return alias, entry, nil + case trustedCertificateTag: + entry, err := ksd.readTrustedCertificateEntry(version) + if err != nil { + return "", nil, err + } + return alias, entry, nil + } + return "", nil, ErrIncorrectTag +} + +// Decode reads keystore representation from r then decrypts and check signature using password +// It is strongly recommended to fill password slice with zero after usage +func Decode(r io.Reader, password []byte) (KeyStore, error) { + ksd := keyStoreDecoder{ + r: r, + md: sha1.New(), + } + passwordBytes := passwordBytes(password) + defer zeroing(passwordBytes) + _, err := ksd.md.Write(passwordBytes) + if err != nil { + return nil, err + } + _, err = ksd.md.Write(whitenerMessage) + if err != nil { + return nil, err + } + + readMagic, err := ksd.readUint32() + if err != nil { + return nil, err + } + if readMagic != magic { + return nil, ErrIncorrectMagic + } + version, err := ksd.readUint32() + if err != nil { + return nil, err + } + count, err := ksd.readUint32() + if err != nil { + return nil, err + } + keyStore := KeyStore{} + for entitiesCount := count; entitiesCount > 0; entitiesCount-- { + alias, entry, err := ksd.readEntry(version, password) + if err != nil { + return nil, err + } + keyStore[alias] = entry + } + + computedDigest := ksd.md.Sum(nil) + actualDigest, err := ksd.readBytes(uint32(ksd.md.Size())) + for i := 0; i < len(actualDigest); i++ { + if actualDigest[i] != computedDigest[i] { + return nil, ErrInvalidDigest + } + } + + return keyStore, nil +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/decoder_test.go b/deps/github.com/pavel-v-chernykh/keystore-go/decoder_test.go new file mode 100644 index 000000000..d7cf01eb5 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/decoder_test.go @@ -0,0 +1,436 @@ +package keystore + +import ( + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/binary" + "reflect" + "testing" +) + +func TestReadUint16(t *testing.T) { + type readUint16Item struct { + input []byte + number uint16 + err error + hash [sha1.Size]byte + } + var readUint32Table = func() []readUint16Item { + var table []readUint16Item + table = append(table, readUint16Item{ + input: nil, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readUint16Item{ + input: []byte{}, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readUint16Item{ + input: []byte{1}, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + buf := make([]byte, 2) + var number uint16 = 10 + binary.BigEndian.PutUint16(buf, number) + table = append(table, readUint16Item{ + input: buf, + number: number, + err: nil, + hash: sha1.Sum(buf), + }) + buf = make([]byte, 2) + number = 0 + binary.BigEndian.PutUint16(buf, number) + table = append(table, readUint16Item{ + input: buf, + number: number, + err: nil, + hash: sha1.Sum(buf), + }) + return table + }() + + for _, tt := range readUint32Table { + ksd := keyStoreDecoder{ + r: bytes.NewReader(tt.input), + md: sha1.New(), + } + number, err := ksd.readUint16() + hash := ksd.md.Sum(nil) + if err != tt.err { + t.Errorf("Invalid error '%v' '%v'", err, tt.err) + } + if number != tt.number { + t.Errorf("Invalid uint16 '%v' '%v'", number, tt.number) + } + if !reflect.DeepEqual(hash, tt.hash[:]) { + t.Errorf("Invalid hash '%v' '%v'", hash, tt.hash) + } + } +} + +func TestReadUint32(t *testing.T) { + type readUint32Item struct { + input []byte + number uint32 + err error + hash [sha1.Size]byte + } + var readUint32Table = func() []readUint32Item { + var table []readUint32Item + table = append(table, readUint32Item{ + input: nil, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readUint32Item{ + input: []byte{}, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readUint32Item{ + input: []byte{1, 2, 3}, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + buf := make([]byte, 4) + var number uint32 = 10 + binary.BigEndian.PutUint32(buf, number) + table = append(table, readUint32Item{ + input: buf, + number: number, + err: nil, + hash: sha1.Sum(buf), + }) + buf = make([]byte, 4) + number = 0 + binary.BigEndian.PutUint32(buf, number) + table = append(table, readUint32Item{ + input: buf, + number: number, + err: nil, + hash: sha1.Sum(buf), + }) + return table + }() + + for _, tt := range readUint32Table { + ksd := keyStoreDecoder{ + r: bytes.NewReader(tt.input), + md: sha1.New(), + } + number, err := ksd.readUint32() + hash := ksd.md.Sum(nil) + if err != tt.err { + t.Errorf("Invalid error '%v' '%v'", err, tt.err) + } + if number != tt.number { + t.Errorf("Invalid uint32 '%v' '%v'", number, tt.number) + } + if !reflect.DeepEqual(hash, tt.hash[:]) { + t.Errorf("Invalid hash '%v' '%v'", hash, tt.hash) + } + } +} + +func TestReadUint64(t *testing.T) { + type readUint64Item struct { + input []byte + number uint64 + err error + hash [sha1.Size]byte + } + var readUint64Table = func() []readUint64Item { + var table []readUint64Item + table = append(table, readUint64Item{ + input: nil, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readUint64Item{ + input: []byte{}, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readUint64Item{ + input: []byte{1, 2, 3}, + number: 0, + err: ErrIo, + hash: sha1.Sum(nil), + }) + buf := make([]byte, 8) + var number uint64 = 10 + binary.BigEndian.PutUint64(buf, number) + table = append(table, readUint64Item{ + input: buf, + number: number, + err: nil, + hash: sha1.Sum(buf), + }) + buf = make([]byte, 8) + number = 0 + binary.BigEndian.PutUint64(buf, number) + table = append(table, readUint64Item{ + input: buf, + number: number, + err: nil, + hash: sha1.Sum(buf), + }) + return table + }() + + for _, tt := range readUint64Table { + ksd := keyStoreDecoder{ + r: bytes.NewReader(tt.input), + md: sha1.New(), + } + number, err := ksd.readUint64() + hash := ksd.md.Sum(nil) + + if err != tt.err { + t.Errorf("Invalid error '%v' '%v'", err, tt.err) + } + if number != tt.number { + t.Errorf("Invalid uint64 '%v' '%v'", number, tt.number) + } + if !reflect.DeepEqual(hash, tt.hash[:]) { + t.Errorf("Invalid hash '%v' '%v'", hash, tt.hash) + } + } +} + +func TestReadBytes(t *testing.T) { + type readBytesItem struct { + input []byte + readLen uint32 + bytes []byte + err error + hash [sha1.Size]byte + } + var readUint32Table = func() []readBytesItem { + var table []readBytesItem + table = append(table, readBytesItem{ + input: nil, + readLen: 0, + bytes: nil, + err: nil, + hash: sha1.Sum(nil), + }) + table = append(table, readBytesItem{ + input: []byte{1, 2, 3}, + readLen: 3, + bytes: []byte{1, 2, 3}, + err: nil, + hash: sha1.Sum([]byte{1, 2, 3}), + }) + table = append(table, readBytesItem{ + input: []byte{1, 2, 3}, + readLen: 2, + bytes: []byte{1, 2}, + err: nil, + hash: sha1.Sum([]byte{1, 2}), + }) + buf := func() []byte { + buf := make([]byte, 10*1024) + _, err := rand.Read(buf) + if err != nil { + t.Errorf("Error: %v", err) + } + return buf + }() + table = append(table, readBytesItem{ + input: buf, + readLen: 9 * 1024, + bytes: buf[:9*1024], + err: nil, + hash: sha1.Sum(buf[:9*1024]), + }) + return table + }() + + for _, tt := range readUint32Table { + ksd := keyStoreDecoder{ + r: bytes.NewReader(tt.input), + md: sha1.New(), + } + bts, err := ksd.readBytes(tt.readLen) + hash := ksd.md.Sum(nil) + if err != tt.err { + t.Errorf("Invalid error '%v' '%v'", err, tt.err) + } + if !reflect.DeepEqual(bts, tt.bytes) { + t.Errorf("Invalid bytes '%v' '%v'", bts, tt.bytes) + } + if !reflect.DeepEqual(hash, tt.hash[:]) { + t.Errorf("Invalid hash '%v' '%v'", hash, tt.hash) + } + } +} + +func TestReadString(t *testing.T) { + type readStringItem struct { + input []byte + string string + err error + hash [sha1.Size]byte + } + var readUint32Table = func() []readStringItem { + var table []readStringItem + table = append(table, readStringItem{ + input: nil, + string: "", + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readStringItem{ + input: []byte{}, + string: "", + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readStringItem{ + input: []byte{1, 2, 3}, + string: "", + err: ErrIo, + hash: sha1.Sum([]byte{1, 2}), + }) + str := "some string to read" + buf := make([]byte, 2) + binary.BigEndian.PutUint16(buf, uint16(len(str))) + buf = append(buf, []byte(str)...) + table = append(table, readStringItem{ + input: buf, + string: str, + err: nil, + hash: sha1.Sum(buf), + }) + return table + }() + + for _, tt := range readUint32Table { + ksd := keyStoreDecoder{ + r: bytes.NewReader(tt.input), + md: sha1.New(), + } + str, err := ksd.readString() + hash := ksd.md.Sum(nil) + if err != tt.err { + t.Errorf("Invalid error '%v' '%v'", err, tt.err) + } + if str != tt.string { + t.Errorf("Invalid string '%v' '%v'", str, tt.string) + } + if !reflect.DeepEqual(hash, tt.hash[:]) { + t.Errorf("Invalid hash '%v' '%v'", hash, tt.hash) + } + } +} + +func TestReadCertificate(t *testing.T) { + type readCertificateItem struct { + input []byte + version uint32 + cert *Certificate + err error + hash [sha1.Size]byte + } + + var readCertificateTable = func() []readCertificateItem { + var table []readCertificateItem + table = append(table, readCertificateItem{ + input: nil, + version: version01, + cert: nil, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readCertificateItem{ + input: nil, + version: version02, + cert: nil, + err: ErrIo, + hash: sha1.Sum(nil), + }) + table = append(table, readCertificateItem{ + input: nil, + version: 3, + cert: nil, + err: ErrIncorrectVersion, + hash: sha1.Sum(nil), + }) + table = append(table, func() readCertificateItem { + input := []byte{0, 0, 0, 0} + return readCertificateItem{ + input: input, + version: version01, + cert: &Certificate{ + Type: defaultCertificateType, + Content: nil, + }, + err: nil, + hash: sha1.Sum(input), + } + }()) + table = append(table, func() readCertificateItem { + buf := make([]byte, 2) + order.PutUint16(buf, uint16(len(defaultCertificateType))) + buf = append(buf, []byte(defaultCertificateType)...) + buf = append(buf, 0, 0, 0, 0) + return readCertificateItem{ + input: buf, + version: version02, + cert: &Certificate{ + Type: defaultCertificateType, + Content: nil, + }, + err: nil, + hash: sha1.Sum(buf), + } + }()) + table = append(table, func() readCertificateItem { + buf := make([]byte, 2) + order.PutUint16(buf, uint16(len(defaultCertificateType))) + buf = append(buf, []byte(defaultCertificateType)...) + buf = append(buf, 0, 0, 0, 1) + return readCertificateItem{ + input: buf, + version: version02, + cert: nil, + err: ErrIo, + hash: sha1.Sum(buf), + } + }()) + + return table + }() + + for _, tt := range readCertificateTable { + ksd := keyStoreDecoder{ + r: bytes.NewReader(tt.input), + md: sha1.New(), + } + cert, err := ksd.readCertificate(tt.version) + hash := ksd.md.Sum(nil) + if err != tt.err { + t.Errorf("Invalid error '%v' '%v'", err, tt.err) + } + if cert != nil && tt.cert != nil && !reflect.DeepEqual(cert, tt.cert) { + t.Errorf("Invalid certificate '%v' '%v'", cert, tt.cert) + } + if !reflect.DeepEqual(hash, tt.hash[:]) { + t.Errorf("Invalid hash '%v' '%v'", hash, tt.hash) + } + } +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/encoder.go b/deps/github.com/pavel-v-chernykh/keystore-go/encoder.go new file mode 100644 index 000000000..017706650 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/encoder.go @@ -0,0 +1,241 @@ +package keystore + +import ( + "crypto/rand" + "crypto/sha1" + "errors" + "hash" + "io" + "math" +) + +// ErrEncodedSequenceTooLong indicates that size of string or bytes trying to encode too big +var ErrEncodedSequenceTooLong = errors.New("keystore: encoded sequence too long") + +// ErrIncorrectEntryType indicates incorrect entry type addressing +var ErrIncorrectEntryType = errors.New("keystore: incorrect entry type") + +type keyStoreEncoder struct { + w io.Writer + b [bufSize]byte + md hash.Hash + rand io.Reader +} + +func (kse *keyStoreEncoder) writeUint16(value uint16) error { + const blockSize = 2 + order.PutUint16(kse.b[:blockSize], value) + _, err := kse.w.Write(kse.b[:blockSize]) + if err != nil { + return err + } + _, err = kse.md.Write(kse.b[:blockSize]) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writeUint32(value uint32) error { + const blockSize = 4 + order.PutUint32(kse.b[:blockSize], value) + _, err := kse.w.Write(kse.b[:blockSize]) + if err != nil { + return err + } + _, err = kse.md.Write(kse.b[:blockSize]) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writeUint64(value uint64) error { + const blockSize = 8 + order.PutUint64(kse.b[:blockSize], value) + _, err := kse.w.Write(kse.b[:blockSize]) + if err != nil { + return err + } + _, err = kse.md.Write(kse.b[:blockSize]) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writeBytes(value []byte) error { + _, err := kse.w.Write(value) + if err != nil { + return err + } + _, err = kse.md.Write(value) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writeString(value string) error { + strLen := len(value) + if strLen > math.MaxUint16 { + return ErrEncodedSequenceTooLong + } + err := kse.writeUint16(uint16(strLen)) + if err != nil { + return err + } + err = kse.writeBytes([]byte(value)) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writeCertificate(cert *Certificate) error { + err := kse.writeString(cert.Type) + if err != nil { + return err + } + certLen := uint64(len(cert.Content)) + if certLen > math.MaxUint32 { + return ErrEncodedSequenceTooLong + } + err = kse.writeUint32(uint32(certLen)) + if err != nil { + return err + } + err = kse.writeBytes(cert.Content) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writeTrustedCertificateEntry(alias string, tce *TrustedCertificateEntry) error { + err := kse.writeUint32(trustedCertificateTag) + if err != nil { + return err + } + err = kse.writeString(alias) + if err != nil { + return err + } + err = kse.writeUint64(uint64(timeToMilliseconds(tce.CreationDate))) + if err != nil { + return err + } + err = kse.writeCertificate(&tce.Certificate) + if err != nil { + return err + } + return nil +} + +func (kse *keyStoreEncoder) writePrivateKeyEntry(alias string, pke *PrivateKeyEntry, password []byte) error { + err := kse.writeUint32(privateKeyTag) + if err != nil { + return err + } + err = kse.writeString(alias) + if err != nil { + return err + } + err = kse.writeUint64(uint64(timeToMilliseconds(pke.CreationDate))) + if err != nil { + return err + } + encodedPrivKeyContent, err := protectKey(kse.rand, pke.PrivKey, password) + if err != nil { + return err + } + privKeyLen := uint64(len(encodedPrivKeyContent)) + if privKeyLen > math.MaxUint32 { + return ErrEncodedSequenceTooLong + } + err = kse.writeUint32(uint32(privKeyLen)) + if err != nil { + return err + } + err = kse.writeBytes(encodedPrivKeyContent) + if err != nil { + return err + } + certCount := uint64(len(pke.CertChain)) + if certCount > math.MaxUint32 { + return ErrEncodedSequenceTooLong + } + err = kse.writeUint32(uint32(certCount)) + if err != nil { + return err + } + for _, cert := range pke.CertChain { + err = kse.writeCertificate(&cert) + if err != nil { + return err + } + } + return nil +} + +// Encode encrypts and signs keystore using password and writes its representation into w +// It is strongly recommended to fill password slice with zero after usage +func Encode(w io.Writer, ks KeyStore, password []byte) error { + return EncodeWithRand(rand.Reader, w, ks, password) +} + +// Encode encrypts and signs keystore using password and writes its representation into w +// Random bytes are read from rand, which must be a cryptographically secure source of randomness +// It is strongly recommended to fill password slice with zero after usage +func EncodeWithRand(rand io.Reader, w io.Writer, ks KeyStore, password []byte) error { + kse := keyStoreEncoder{ + w: w, + md: sha1.New(), + rand: rand, + } + passwordBytes := passwordBytes(password) + defer zeroing(passwordBytes) + _, err := kse.md.Write(passwordBytes) + if err != nil { + return err + } + _, err = kse.md.Write(whitenerMessage) + if err != nil { + return err + } + + err = kse.writeUint32(magic) + if err != nil { + return err + } + // always write latest version + err = kse.writeUint32(version02) + if err != nil { + return err + } + err = kse.writeUint32(uint32(len(ks))) + if err != nil { + return err + } + for alias, entry := range ks { + switch typedEntry := entry.(type) { + case *PrivateKeyEntry: + err = kse.writePrivateKeyEntry(alias, typedEntry, password) + if err != nil { + return err + } + case *TrustedCertificateEntry: + err = kse.writeTrustedCertificateEntry(alias, typedEntry) + if err != nil { + return err + } + default: + return ErrIncorrectEntryType + } + } + err = kse.writeBytes(kse.md.Sum(nil)) + if err != nil { + return err + } + return nil +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/examples/compare/keystore.jks b/deps/github.com/pavel-v-chernykh/keystore-go/examples/compare/keystore.jks new file mode 100644 index 000000000..1b9960b1e Binary files /dev/null and b/deps/github.com/pavel-v-chernykh/keystore-go/examples/compare/keystore.jks differ diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/examples/compare/main.go b/deps/github.com/pavel-v-chernykh/keystore-go/examples/compare/main.go new file mode 100644 index 000000000..9221506f7 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/examples/compare/main.go @@ -0,0 +1,53 @@ +// +build ignore + +package main + +import ( + "github.com/pavel-v-chernykh/keystore-go" + "log" + "os" + "reflect" +) + +func readKeyStore(filename string, password []byte) keystore.KeyStore { + f, err := os.Open(filename) + defer f.Close() + if err != nil { + log.Fatal(err) + } + keyStore, err := keystore.Decode(f, password) + if err != nil { + log.Fatal(err) + } + return keyStore +} + +func writeKeyStore(keyStore keystore.KeyStore, filename string, password []byte) { + o, err := os.Create(filename) + defer o.Close() + if err != nil { + log.Fatal(err) + } + err = keystore.Encode(o, keyStore, password) + if err != nil { + log.Fatal(err) + } +} + +func zeroing(s []byte) { + for i := 0; i < len(s); i++ { + s[i] = 0 + } +} + +func main() { + password := []byte{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'} + defer zeroing(password) + ks1 := readKeyStore("keystore.jks", password) + + writeKeyStore(ks1, "keystore2.jks", password) + + ks2 := readKeyStore("keystore2.jks", password) + + log.Printf("Is equal: %v\n", reflect.DeepEqual(ks1, ks2)) +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/examples/pem/main.go b/deps/github.com/pavel-v-chernykh/keystore-go/examples/pem/main.go new file mode 100644 index 000000000..f3ae6f05c --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/examples/pem/main.go @@ -0,0 +1,83 @@ +// +build ignore + +package main + +import ( + "crypto/x509" + "encoding/pem" + "github.com/pavel-v-chernykh/keystore-go" + "io/ioutil" + "log" + "os" + "time" +) + +func readKeyStore(filename string, password []byte) keystore.KeyStore { + f, err := os.Open(filename) + defer f.Close() + if err != nil { + log.Fatal(err) + } + keyStore, err := keystore.Decode(f, password) + if err != nil { + log.Fatal(err) + } + return keyStore +} + +func writeKeyStore(keyStore keystore.KeyStore, filename string, password []byte) { + o, err := os.Create(filename) + defer o.Close() + if err != nil { + log.Fatal(err) + } + err = keystore.Encode(o, keyStore, password) + if err != nil { + log.Fatal(err) + } +} + +func zeroing(s []byte) { + for i := 0; i < len(s); i++ { + s[i] = 0 + } +} + +func main() { + // openssl genrsa 1024 | openssl pkcs8 -topk8 -inform pem -outform pem -nocrypt -out privkey.pem + pke, err := ioutil.ReadFile("./privkey.pem") + if err != nil { + log.Fatal(err) + } + p, _ := pem.Decode(pke) + if p == nil { + log.Fatal("Should have at least one pem block") + } + if p.Type != "PRIVATE KEY" { + log.Fatal("Should be a rsa private key") + } + + keyStore := keystore.KeyStore{ + "alias": &keystore.PrivateKeyEntry{ + Entry: keystore.Entry{ + CreationDate: time.Now(), + }, + PrivKey: p.Bytes, + }, + } + + password := []byte{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'} + defer zeroing(password) + writeKeyStore(keyStore, "keystore.jks", password) + + ks := readKeyStore("keystore.jks", password) + + entry := ks["alias"] + privKeyEntry := entry.(*keystore.PrivateKeyEntry) + key, err := x509.ParsePKCS8PrivateKey(privKeyEntry.PrivKey) + if err != nil { + log.Fatal(err) + } + + log.Printf("%v", key) +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/examples/pem/privkey.pem b/deps/github.com/pavel-v-chernykh/keystore-go/examples/pem/privkey.pem new file mode 100644 index 000000000..323021d74 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/examples/pem/privkey.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMiiH2T1BHnHiaU8 +EGAIa6JHkOZCfb9xDLyfqL73m/kFTFAJAhJ9gGusVIk5NCS0aGgASsNnGiqmhFbE +MiYuvxRzUJIKL9hh1ttnGhyZQ0hwlyeTNVJKCWNX6rcLZ+blO7kpF6YI6fqYsmWc +QeyxgW2NAFH9HaiqF5F0V0DUXh4dAgMBAAECgYEAwRS0ndXmbsQGxUuefqzb2JqC +6fWHSpujJEuKe+2S3v2oSUXCBsVct0JrQHwaoFA2QhA14wLv/aeuqEm78V7/ZxsF +vi+PFsYGsag05N83vZcJi/fHbLzkWFOANAnr7i/4u1sd2fIqFkm5xY5lw02lW5JN +daVDAo/njAEsTYn2OEkCQQD3cx/50LWmsQnsmSy4RdfERYWMuxDjSlvixfabEb3X +usyZ8CcK1RUUdEi7m+H+3KXJvuNaZiQ5WdQ7nXX34plXAkEAz5DfPLcC40sQlXxM +G0pteLNDemV81Okj4yfzukRZXwt54JjPd0AO1GQbmB4K8Zqag7p0ekU/5Y7oL+JQ +fXA3qwJAPudRNZxM0TcoIrE9oQqAMzDJJmFXhbAdc6SHcBwuemzOHkPiaOqKFU0K +QEb8SGGm84ZHHW/hvYKMZSs+FenQuQJBAMC6T83cUH4j0P48L56XeRY9vUYEvegj +ogLlsdUeaa1qxnvY56pefGaRnV2dZ6P2Xco6crSlYDMSgl0T0pDmhYkCQDZ1dn3F ++MXxmJT9uRSfwLj4cNPHICZKrlp20lqDYvEZtIiouY4c/Y7cYykUSotzUgigng9D +zK3idGFIejk+Ezo= +-----END PRIVATE KEY----- diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/keyprotector.go b/deps/github.com/pavel-v-chernykh/keystore-go/keyprotector.go new file mode 100644 index 000000000..2c6b42e7f --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/keyprotector.go @@ -0,0 +1,165 @@ +package keystore + +import ( + "crypto/sha1" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "io" +) + +const saltLen = 20 + +var supportedPrivateKeyAlgorithmOid = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 42, 2, 17, 1, 1}) + +// ErrUnsupportedPrivateKeyAlgorithm indicates unsupported private key algorithm +var ErrUnsupportedPrivateKeyAlgorithm = errors.New("keystore: unsupported private key algorithm") + +// ErrUnrecoverablePrivateKey indicates unrecoverable private key content (often means wrong password usage) +var ErrUnrecoverablePrivateKey = errors.New("keystore: unrecoverable private key") + +type keyInfo struct { + Algo pkix.AlgorithmIdentifier + PrivateKey []byte +} + +func recoverKey(encodedKey []byte, password []byte) ([]byte, error) { + var keyInfo keyInfo + asn1Rest, err := asn1.Unmarshal(encodedKey, &keyInfo) + if err != nil || len(asn1Rest) > 0 { + return nil, ErrIncorrectPrivateKey + } + if !keyInfo.Algo.Algorithm.Equal(supportedPrivateKeyAlgorithmOid) { + return nil, ErrUnsupportedPrivateKeyAlgorithm + } + + md := sha1.New() + passwordBytes := passwordBytes(password) + defer zeroing(passwordBytes) + salt := make([]byte, saltLen) + copy(salt, keyInfo.PrivateKey) + encrKeyLen := len(keyInfo.PrivateKey) - saltLen - md.Size() + numRounds := encrKeyLen / md.Size() + + if encrKeyLen%md.Size() != 0 { + numRounds++ + } + + encrKey := make([]byte, encrKeyLen) + copy(encrKey, keyInfo.PrivateKey[saltLen:]) + + xorKey := make([]byte, encrKeyLen) + + digest := salt + for i, xorOffset := 0, 0; i < numRounds; i++ { + _, err := md.Write(passwordBytes) + if err != nil { + return nil, ErrUnrecoverablePrivateKey + } + _, err = md.Write(digest) + if err != nil { + return nil, ErrUnrecoverablePrivateKey + } + digest = md.Sum(nil) + md.Reset() + copy(xorKey[xorOffset:], digest) + xorOffset += md.Size() + } + + plainKey := make([]byte, encrKeyLen) + for i := 0; i < len(plainKey); i++ { + plainKey[i] = encrKey[i] ^ xorKey[i] + } + + _, err = md.Write(passwordBytes) + if err != nil { + return nil, ErrUnrecoverablePrivateKey + } + _, err = md.Write(plainKey) + if err != nil { + return nil, ErrUnrecoverablePrivateKey + } + digest = md.Sum(nil) + md.Reset() + + digestOffset := saltLen + encrKeyLen + for i := 0; i < len(digest); i++ { + if digest[i] != keyInfo.PrivateKey[digestOffset+i] { + return nil, ErrUnrecoverablePrivateKey + } + } + + return plainKey, nil +} + +func protectKey(rand io.Reader, plainKey []byte, password []byte) ([]byte, error) { + md := sha1.New() + passwdBytes := passwordBytes(password) + defer zeroing(passwdBytes) + plainKeyLen := len(plainKey) + numRounds := plainKeyLen / md.Size() + + if plainKeyLen%md.Size() != 0 { + numRounds++ + } + + salt := make([]byte, saltLen) + _, err := rand.Read(salt) + if err != nil { + return nil, err + } + + xorKey := make([]byte, plainKeyLen) + + digest := salt + for i, xorOffset := 0, 0; i < numRounds; i++ { + _, err = md.Write(passwdBytes) + if err != nil { + return nil, err + } + _, err = md.Write(digest) + if err != nil { + return nil, err + } + digest = md.Sum(nil) + md.Reset() + copy(xorKey[xorOffset:], digest) + xorOffset += md.Size() + } + + tmpKey := make([]byte, plainKeyLen) + for i := 0; i < plainKeyLen; i++ { + tmpKey[i] = plainKey[i] ^ xorKey[i] + } + + encrKey := make([]byte, saltLen+plainKeyLen+md.Size()) + encrKeyOffset := 0 + copy(encrKey[encrKeyOffset:], salt) + encrKeyOffset += saltLen + copy(encrKey[encrKeyOffset:], tmpKey) + encrKeyOffset += plainKeyLen + + _, err = md.Write(passwdBytes) + if err != nil { + return nil, err + } + _, err = md.Write(plainKey) + if err != nil { + return nil, err + } + digest = md.Sum(nil) + md.Reset() + copy(encrKey[encrKeyOffset:], digest) + keyInfo := keyInfo{ + Algo: pkix.AlgorithmIdentifier{ + Algorithm: supportedPrivateKeyAlgorithmOid, + Parameters: asn1.RawValue{Tag: 5}, + }, + PrivateKey: encrKey, + } + encodedKey, err := asn1.Marshal(keyInfo) + if err != nil { + return nil, err + } + return encodedKey, nil +} diff --git a/deps/github.com/pavel-v-chernykh/keystore-go/keystore.go b/deps/github.com/pavel-v-chernykh/keystore-go/keystore.go new file mode 100644 index 000000000..d262bc3c7 --- /dev/null +++ b/deps/github.com/pavel-v-chernykh/keystore-go/keystore.go @@ -0,0 +1,32 @@ +package keystore + +import ( + "time" +) + +// KeyStore is a mapping of alias to pointer to PrivateKeyEntry or TrustedCertificateEntry +type KeyStore map[string]interface{} + +// Certificate describes type of certificate +type Certificate struct { + Type string + Content []byte +} + +// Entry is a basis of entries types supported by keystore +type Entry struct { + CreationDate time.Time +} + +// PrivateKeyEntry is an entry for private keys and associated certificates +type PrivateKeyEntry struct { + Entry + PrivKey []byte + CertChain []Certificate +} + +// TrustedCertificateEntry is an entry for certificates only +type TrustedCertificateEntry struct { + Entry + Certificate Certificate +} diff --git a/docs/user/README.md b/docs/user/README.md index 0f52da4e2..22815172b 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -8,4 +8,5 @@ - [Services & Load balancer](./services_and_loadbalancer.md) - [Storage](./storage.md) - [Storage Resource](./storage_resource.md) +- [TLS](./tls.md) - [Upgrading](./upgrading.md) diff --git a/docs/user/tls.md b/docs/user/tls.md new file mode 100644 index 000000000..e320eeaa8 --- /dev/null +++ b/docs/user/tls.md @@ -0,0 +1,42 @@ +# TLS + +The ArangoDB operator allows you to create ArangoDB deployments that use +secure TLS connections. + +It uses a single CA certificate (stored in a Kubernetes secret) and +one certificate per ArangoDB server (stored in a Kubernetes secret per server). + +## Install CA certificate + +If the CA certificate is self-signed, it will not be trusted by browsers, +until you install it in the local operating system or browser. +This process differs per operating system. + +To do so, you first have to fetch the CA certificate from its Kubernetes +secret. + +```bash +kubectl get secret --template='{{index .data "ca.crt"}}' | base64 -D > ca.crt +``` + +### Windows + +TODO + +### MacOS + +To install a CA certificate in MacOS, run: + +```bash +sudo /usr/bin/security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt +``` + +To uninstall a CA certificate in MacOS, run: + +```bash +sudo /usr/bin/security remove-trusted-cert -d ca.crt +``` + +### Linux + +TODO \ No newline at end of file diff --git a/examples/simple-cluster-tls.yaml b/examples/simple-cluster-tls.yaml new file mode 100644 index 000000000..18f97aa56 --- /dev/null +++ b/examples/simple-cluster-tls.yaml @@ -0,0 +1,9 @@ +apiVersion: "database.arangodb.com/v1alpha" +kind: "ArangoDeployment" +metadata: + name: "example-simple-cluster-tls" +spec: + mode: cluster + tls: + caSecretName: example-simple-cluster-tls + altNames: ["kube-01", "kube-02", "kube-03"] diff --git a/pkg/apis/arangodb/v1alpha/deployment_spec.go b/pkg/apis/arangodb/v1alpha/deployment_spec.go index df33b2e03..99973a314 100644 --- a/pkg/apis/arangodb/v1alpha/deployment_spec.go +++ b/pkg/apis/arangodb/v1alpha/deployment_spec.go @@ -52,7 +52,7 @@ type DeploymentSpec struct { RocksDB RocksDBSpec `json:"rocksdb"` Authentication AuthenticationSpec `json:"auth"` - SSL SSLSpec `json:"ssl"` + TLS TLSSpec `json:"tls"` Sync SyncSpec `json:"sync"` Single ServerGroupSpec `json:"single"` @@ -70,7 +70,7 @@ func (s DeploymentSpec) IsAuthenticated() bool { // IsSecure returns true when SSL is enabled func (s DeploymentSpec) IsSecure() bool { - return s.SSL.IsSecure() + return s.TLS.IsSecure() } // SetDefaults fills in default values when a field is not specified. @@ -92,8 +92,8 @@ func (s *DeploymentSpec) SetDefaults(deploymentName string) { } s.RocksDB.SetDefaults() s.Authentication.SetDefaults(deploymentName + "-jwt") - s.SSL.SetDefaults() - s.Sync.SetDefaults(s.Image, s.ImagePullPolicy, deploymentName+"-sync-jwt") + s.TLS.SetDefaults("") + s.Sync.SetDefaults(s.Image, s.ImagePullPolicy, deploymentName+"-sync-jwt", deploymentName+"-sync-ca") s.Single.SetDefaults(ServerGroupSingle, s.Mode.HasSingleServers(), s.Mode) s.Agents.SetDefaults(ServerGroupAgents, s.Mode.HasAgents(), s.Mode) s.DBServers.SetDefaults(ServerGroupDBServers, s.Mode.HasDBServers(), s.Mode) @@ -126,8 +126,8 @@ func (s *DeploymentSpec) Validate() error { if err := s.Authentication.Validate(false); err != nil { return maskAny(errors.Wrap(err, "spec.auth")) } - if err := s.SSL.Validate(); err != nil { - return maskAny(errors.Wrap(err, "spec.ssl")) + if err := s.TLS.Validate(); err != nil { + return maskAny(errors.Wrap(err, "spec.tls")) } if err := s.Sync.Validate(s.Mode); err != nil { return maskAny(errors.Wrap(err, "spec.sync")) diff --git a/pkg/apis/arangodb/v1alpha/ssl_spec.go b/pkg/apis/arangodb/v1alpha/ssl_spec.go deleted file mode 100644 index 27e95b86e..000000000 --- a/pkg/apis/arangodb/v1alpha/ssl_spec.go +++ /dev/null @@ -1,54 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2018 ArangoDB GmbH, Cologne, Germany -// -// 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. -// -// Copyright holder is ArangoDB GmbH, Cologne, Germany -// -// Author Ewout Prangsma -// - -package v1alpha - -import ( - "github.com/arangodb/k8s-operator/pkg/util/k8sutil" -) - -// SSLSpec holds SSL specific configuration settings -type SSLSpec struct { - KeySecretName string `json:"keySecretName,omitempty"` - OrganizationName string `json:"organizationName,omitempty"` - ServerName string `json:"serverName,omitempty"` -} - -// IsSecure returns true when a key secret has been set, false otherwise. -func (s SSLSpec) IsSecure() bool { - return s.KeySecretName != "" -} - -// Validate the given spec -func (s SSLSpec) Validate() error { - if err := k8sutil.ValidateOptionalResourceName(s.KeySecretName); err != nil { - return maskAny(err) - } - return nil -} - -// SetDefaults fills in missing defaults -func (s *SSLSpec) SetDefaults() { - if s.OrganizationName == "" { - s.OrganizationName = "ArangoDB" - } -} diff --git a/pkg/apis/arangodb/v1alpha/ssl_spec_test.go b/pkg/apis/arangodb/v1alpha/ssl_spec_test.go deleted file mode 100644 index e6dacb000..000000000 --- a/pkg/apis/arangodb/v1alpha/ssl_spec_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2018 ArangoDB GmbH, Cologne, Germany -// -// 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. -// -// Copyright holder is ArangoDB GmbH, Cologne, Germany -// -// Author Ewout Prangsma -// - -package v1alpha - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSSLSpecValidate(t *testing.T) { - // Valid - assert.Nil(t, SSLSpec{KeySecretName: ""}.Validate()) - assert.Nil(t, SSLSpec{KeySecretName: "foo"}.Validate()) - - // Not valid - assert.Error(t, SSLSpec{KeySecretName: "Foo"}.Validate()) -} - -func TestSSLSpecIsSecure(t *testing.T) { - assert.False(t, SSLSpec{KeySecretName: ""}.IsSecure()) - assert.True(t, SSLSpec{KeySecretName: "foo"}.IsSecure()) -} - -func TestSSLSpecSetDefaults(t *testing.T) { - def := func(spec SSLSpec) SSLSpec { - spec.SetDefaults() - return spec - } - - assert.Equal(t, "", def(SSLSpec{}).KeySecretName) - assert.Equal(t, "foo", def(SSLSpec{KeySecretName: "foo"}).KeySecretName) - assert.Equal(t, "ArangoDB", def(SSLSpec{}).OrganizationName) - assert.Equal(t, "foo", def(SSLSpec{OrganizationName: "foo"}).OrganizationName) - assert.Equal(t, "", def(SSLSpec{}).ServerName) - assert.Equal(t, "foo", def(SSLSpec{ServerName: "foo"}).ServerName) -} diff --git a/pkg/apis/arangodb/v1alpha/sync_spec.go b/pkg/apis/arangodb/v1alpha/sync_spec.go index dcd195273..07b83fc11 100644 --- a/pkg/apis/arangodb/v1alpha/sync_spec.go +++ b/pkg/apis/arangodb/v1alpha/sync_spec.go @@ -34,6 +34,7 @@ type SyncSpec struct { ImagePullPolicy v1.PullPolicy `json:"imagePullPolicy,omitempty"` Authentication AuthenticationSpec `json:"auth"` + TLS TLSSpec `json:"tls"` Monitoring MonitoringSpec `json:"monitoring"` } @@ -48,6 +49,11 @@ func (s SyncSpec) Validate(mode DeploymentMode) error { if err := s.Authentication.Validate(s.Enabled); err != nil { return maskAny(err) } + if s.Enabled { + if err := s.TLS.Validate(); err != nil { + return maskAny(err) + } + } if err := s.Monitoring.Validate(); err != nil { return maskAny(err) } @@ -55,7 +61,7 @@ func (s SyncSpec) Validate(mode DeploymentMode) error { } // SetDefaults fills in missing defaults -func (s *SyncSpec) SetDefaults(defaultImage string, defaulPullPolicy v1.PullPolicy, defaultJWTSecretName string) { +func (s *SyncSpec) SetDefaults(defaultImage string, defaulPullPolicy v1.PullPolicy, defaultJWTSecretName, defaultCASecretName string) { if s.Image == "" { s.Image = defaultImage } @@ -63,6 +69,7 @@ func (s *SyncSpec) SetDefaults(defaultImage string, defaulPullPolicy v1.PullPoli s.ImagePullPolicy = defaulPullPolicy } s.Authentication.SetDefaults(defaultJWTSecretName) + s.TLS.SetDefaults(defaultCASecretName) s.Monitoring.SetDefaults() } diff --git a/pkg/apis/arangodb/v1alpha/sync_spec_test.go b/pkg/apis/arangodb/v1alpha/sync_spec_test.go index 0a664b8ad..33a024908 100644 --- a/pkg/apis/arangodb/v1alpha/sync_spec_test.go +++ b/pkg/apis/arangodb/v1alpha/sync_spec_test.go @@ -47,7 +47,7 @@ func TestSyncSpecValidate(t *testing.T) { func TestSyncSpecSetDefaults(t *testing.T) { def := func(spec SyncSpec) SyncSpec { - spec.SetDefaults("test-image", v1.PullAlways, "test-jwt") + spec.SetDefaults("test-image", v1.PullAlways, "test-jwt", "test-ca") return spec } diff --git a/pkg/apis/arangodb/v1alpha/tls_spec.go b/pkg/apis/arangodb/v1alpha/tls_spec.go new file mode 100644 index 000000000..c38d05540 --- /dev/null +++ b/pkg/apis/arangodb/v1alpha/tls_spec.go @@ -0,0 +1,86 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "fmt" + "net" + "time" + + "github.com/arangodb/k8s-operator/pkg/util/k8sutil" + "github.com/arangodb/k8s-operator/pkg/util/validation" +) + +const ( + defaultTLSTTL = time.Hour * 2160 // About 3 month +) + +// TLSSpec holds TLS specific configuration settings +type TLSSpec struct { + CASecretName string `json:"caSecretName,omitempty"` + AltNames []string `json:"altNames,omitempty"` + TTL time.Duration `json:"ttl,omitempty"` +} + +// IsSecure returns true when a CA secret has been set, false otherwise. +func (s TLSSpec) IsSecure() bool { + return s.CASecretName != "" +} + +// GetAltNames splits the list of AltNames into DNS names, IP addresses & email addresses. +// When an entry is not valid for any of those categories, an error is returned. +func (s TLSSpec) GetAltNames() (dnsNames, ipAddresses, emailAddresses []string, err error) { + for _, name := range s.AltNames { + if net.ParseIP(name) != nil { + ipAddresses = append(ipAddresses, name) + } else if validation.IsValidDNSName(name) { + dnsNames = append(dnsNames, name) + } else if validation.IsValidEmailAddress(name) { + emailAddresses = append(emailAddresses, name) + } else { + return nil, nil, nil, maskAny(fmt.Errorf("'%s' is not a valid alternate name", name)) + } + } + return dnsNames, ipAddresses, emailAddresses, nil +} + +// Validate the given spec +func (s TLSSpec) Validate() error { + if err := k8sutil.ValidateOptionalResourceName(s.CASecretName); err != nil { + return maskAny(err) + } + if _, _, _, err := s.GetAltNames(); err != nil { + return maskAny(err) + } + return nil +} + +// SetDefaults fills in missing defaults +func (s *TLSSpec) SetDefaults(defaultCASecretName string) { + if s.CASecretName == "" { + s.CASecretName = defaultCASecretName + } + if s.TTL == 0 { + s.TTL = defaultTLSTTL + } +} diff --git a/pkg/apis/arangodb/v1alpha/tls_spec_test.go b/pkg/apis/arangodb/v1alpha/tls_spec_test.go new file mode 100644 index 000000000..fcd30bb07 --- /dev/null +++ b/pkg/apis/arangodb/v1alpha/tls_spec_test.go @@ -0,0 +1,62 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTLSSpecValidate(t *testing.T) { + // Valid + assert.Nil(t, TLSSpec{CASecretName: ""}.Validate()) + assert.Nil(t, TLSSpec{CASecretName: "foo"}.Validate()) + assert.Nil(t, TLSSpec{AltNames: []string{}}.Validate()) + assert.Nil(t, TLSSpec{AltNames: []string{"foo"}}.Validate()) + assert.Nil(t, TLSSpec{AltNames: []string{"email@example.com", "127.0.0.1"}}.Validate()) + + // Not valid + assert.Error(t, TLSSpec{CASecretName: "Foo"}.Validate()) + assert.Error(t, TLSSpec{AltNames: []string{"@@"}}.Validate()) +} + +func TestTLSSpecIsSecure(t *testing.T) { + assert.False(t, TLSSpec{CASecretName: ""}.IsSecure()) + assert.True(t, TLSSpec{CASecretName: "foo"}.IsSecure()) +} + +func TestTLSSpecSetDefaults(t *testing.T) { + def := func(spec TLSSpec) TLSSpec { + spec.SetDefaults("") + return spec + } + + assert.Equal(t, "", def(TLSSpec{}).CASecretName) + assert.Equal(t, "foo", def(TLSSpec{CASecretName: "foo"}).CASecretName) + assert.Len(t, def(TLSSpec{}).AltNames, 0) + assert.Len(t, def(TLSSpec{AltNames: []string{"foo.local"}}).AltNames, 1) + assert.Equal(t, defaultTLSTTL, def(TLSSpec{}).TTL) + assert.Equal(t, time.Hour, def(TLSSpec{TTL: time.Hour}).TTL) +} diff --git a/pkg/apis/arangodb/v1alpha/zz_generated.deepcopy.go b/pkg/apis/arangodb/v1alpha/zz_generated.deepcopy.go index 7d628ae21..c810cead6 100644 --- a/pkg/apis/arangodb/v1alpha/zz_generated.deepcopy.go +++ b/pkg/apis/arangodb/v1alpha/zz_generated.deepcopy.go @@ -154,8 +154,8 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) { *out = *in out.RocksDB = in.RocksDB out.Authentication = in.Authentication - out.SSL = in.SSL - out.Sync = in.Sync + in.TLS.DeepCopyInto(&out.TLS) + in.Sync.DeepCopyInto(&out.Sync) in.Single.DeepCopyInto(&out.Single) in.Agents.DeepCopyInto(&out.Agents) in.DBServers.DeepCopyInto(&out.DBServers) @@ -336,22 +336,6 @@ func (in *RocksDBSpec) DeepCopy() *RocksDBSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SSLSpec) DeepCopyInto(out *SSLSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSLSpec. -func (in *SSLSpec) DeepCopy() *SSLSpec { - if in == nil { - return nil - } - out := new(SSLSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerGroupSpec) DeepCopyInto(out *ServerGroupSpec) { *out = *in @@ -378,6 +362,7 @@ func (in *ServerGroupSpec) DeepCopy() *ServerGroupSpec { func (in *SyncSpec) DeepCopyInto(out *SyncSpec) { *out = *in out.Authentication = in.Authentication + in.TLS.DeepCopyInto(&out.TLS) out.Monitoring = in.Monitoring return } @@ -391,3 +376,24 @@ func (in *SyncSpec) DeepCopy() *SyncSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + if in.AltNames != nil { + in, out := &in.AltNames, &out.AltNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/deployment/pod_creator.go b/pkg/deployment/pod_creator.go index e8875c97e..0af17af03 100644 --- a/pkg/deployment/pod_creator.go +++ b/pkg/deployment/pod_creator.go @@ -65,6 +65,9 @@ func createArangodArgs(apiObject metav1.Object, deplSpec api.DeploymentSpec, gro }*/ //scheme := NewURLSchemes(bsCfg.SslKeyFile != "").Arangod scheme := "tcp" + if deplSpec.IsSecure() { + scheme = "ssl" + } options = append(options, optionPair{"--server.endpoint", fmt.Sprintf("%s://%s:%d", scheme, listenAddr, k8sutil.ArangoPort)}, ) @@ -93,23 +96,24 @@ func createArangodArgs(apiObject metav1.Object, deplSpec api.DeploymentSpec, gro optionPair{"--log.level", "INFO"}, ) - // SSL - /*if bsCfg.SslKeyFile != "" { - sslSection := &configSection{ - Name: "ssl", - Settings: map[string]string{ - "keyfile": bsCfg.SslKeyFile, - }, - } - if bsCfg.SslCAFile != "" { - sslSection.Settings["cafile"] = bsCfg.SslCAFile - } - config = append(config, sslSection) - }*/ + // TLS + if deplSpec.IsSecure() { + keyPath := filepath.Join(k8sutil.TLSKeyfileVolumeMountDir, constants.SecretTLSKeyfile) + options = append(options, + optionPair{"--ssl.keyfile", keyPath}, + optionPair{"--ssl.ecdh-curve", ""}, // This way arangod accepts curves other than P256 as well. + ) + /*if bsCfg.SslKeyFile != "" { + if bsCfg.SslCAFile != "" { + sslSection.Settings["cafile"] = bsCfg.SslCAFile + } + config = append(config, sslSection) + }*/ + } // RocksDB if deplSpec.RocksDB.IsEncrypted() { - keyPath := filepath.Join(k8sutil.RocksDBEncryptionVolumeMountDir, "key") + keyPath := filepath.Join(k8sutil.RocksDBEncryptionVolumeMountDir, constants.SecretEncryptionKey) options = append(options, optionPair{"--rocksdb.encryption-keyfile", keyPath}, ) @@ -298,6 +302,7 @@ func (d *Deployment) createReadinessProbe(apiObject *api.ArangoDeployment, group // ensurePods creates all Pods listed in member status func (d *Deployment) ensurePods(apiObject *api.ArangoDeployment) error { kubecli := d.deps.KubeCli + log := d.deps.Log ns := apiObject.GetNamespace() if err := apiObject.ForeachServerGroup(func(group api.ServerGroup, spec api.ServerGroupSpec, status *api.MemberStatusList) error { @@ -318,6 +323,18 @@ func (d *Deployment) ensurePods(apiObject *api.ArangoDeployment) error { if err != nil { return maskAny(err) } + tlsKeyfileSecretName := "" + if apiObject.Spec.IsSecure() { + tlsKeyfileSecretName = k8sutil.CreateTLSKeyfileSecretName(apiObject.GetName(), role, m.ID) + serverNames := []string{ + k8sutil.CreateDatabaseClientServiceDNSName(apiObject), + k8sutil.CreatePodDNSName(apiObject, role, m.ID), + } + owner := apiObject.AsOwner() + if err := createServerCertificate(log, kubecli.CoreV1(), serverNames, apiObject.Spec.TLS, tlsKeyfileSecretName, ns, &owner); err != nil && !k8sutil.IsAlreadyExists(err) { + return maskAny(errors.Wrapf(err, "Failed to create TLS keyfile secret")) + } + } rocksdbEncryptionSecretName := "" if apiObject.Spec.RocksDB.IsEncrypted() { rocksdbEncryptionSecretName = apiObject.Spec.RocksDB.Encryption.KeySecretName @@ -331,7 +348,7 @@ func (d *Deployment) ensurePods(apiObject *api.ArangoDeployment) error { SecretKey: constants.SecretKeyJWT, } } - if err := k8sutil.CreateArangodPod(kubecli, apiObject.Spec.IsDevelopment(), apiObject, role, m.ID, m.PersistentVolumeClaimName, apiObject.Spec.Image, apiObject.Spec.ImagePullPolicy, args, env, livenessProbe, readinessProbe, rocksdbEncryptionSecretName); err != nil { + if err := k8sutil.CreateArangodPod(kubecli, apiObject.Spec.IsDevelopment(), apiObject, role, m.ID, m.PersistentVolumeClaimName, apiObject.Spec.Image, apiObject.Spec.ImagePullPolicy, args, env, livenessProbe, readinessProbe, tlsKeyfileSecretName, rocksdbEncryptionSecretName); err != nil { return maskAny(err) } } else if group.IsArangosync() { diff --git a/pkg/deployment/pod_creator_agent_args_test.go b/pkg/deployment/pod_creator_agent_args_test.go index dace0e19d..a7260ab4b 100644 --- a/pkg/deployment/pod_creator_agent_args_test.go +++ b/pkg/deployment/pod_creator_agent_args_test.go @@ -74,6 +74,52 @@ func TestCreateArangodArgsAgent(t *testing.T) { ) } + // Default+TLS deployment + { + apiObject := &api.ArangoDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "ns", + }, + Spec: api.DeploymentSpec{ + Mode: api.DeploymentModeCluster, + TLS: api.TLSSpec{ + CASecretName: "test-ca", + }, + }, + } + apiObject.Spec.SetDefaults("test") + agents := api.MemberStatusList{ + api.MemberStatus{ID: "a1"}, + api.MemberStatus{ID: "a2"}, + api.MemberStatus{ID: "a3"}, + } + cmdline := createArangodArgs(apiObject, apiObject.Spec, api.ServerGroupAgents, apiObject.Spec.Agents, agents, "a1") + assert.Equal(t, + []string{ + "--agency.activate=true", + "--agency.endpoint=ssl://name-agent-a2.name-int.ns.svc:8529", + "--agency.endpoint=ssl://name-agent-a3.name-int.ns.svc:8529", + "--agency.my-address=ssl://name-agent-a1.name-int.ns.svc:8529", + "--agency.size=3", + "--agency.supervision=true", + "--cluster.my-id=a1", + "--database.directory=/data", + "--foxx.queues=false", + "--log.level=INFO", + "--log.output=+", + "--server.authentication=true", + "--server.endpoint=ssl://[::]:8529", + "--server.jwt-secret=$(ARANGOD_JWT_SECRET)", + "--server.statistics=false", + "--server.storage-engine=rocksdb", + "--ssl.ecdh-curve=", + "--ssl.keyfile=/secrets/tls/tls.keyfile", + }, + cmdline, + ) + } + // No authentication, mmfiles { apiObject := &api.ArangoDeployment{ diff --git a/pkg/deployment/pod_creator_coordinator_args_test.go b/pkg/deployment/pod_creator_coordinator_args_test.go index d9cd5406e..b4050dd8b 100644 --- a/pkg/deployment/pod_creator_coordinator_args_test.go +++ b/pkg/deployment/pod_creator_coordinator_args_test.go @@ -73,6 +73,51 @@ func TestCreateArangodArgsCoordinator(t *testing.T) { ) } + // Default+TLS deployment + { + apiObject := &api.ArangoDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "ns", + }, + Spec: api.DeploymentSpec{ + Mode: api.DeploymentModeCluster, + TLS: api.TLSSpec{ + CASecretName: "test-ca", + }, + }, + } + apiObject.Spec.SetDefaults("test") + agents := api.MemberStatusList{ + api.MemberStatus{ID: "a1"}, + api.MemberStatus{ID: "a2"}, + api.MemberStatus{ID: "a3"}, + } + cmdline := createArangodArgs(apiObject, apiObject.Spec, api.ServerGroupCoordinators, apiObject.Spec.Coordinators, agents, "id1") + assert.Equal(t, + []string{ + "--cluster.agency-endpoint=ssl://name-agent-a1.name-int.ns.svc:8529", + "--cluster.agency-endpoint=ssl://name-agent-a2.name-int.ns.svc:8529", + "--cluster.agency-endpoint=ssl://name-agent-a3.name-int.ns.svc:8529", + "--cluster.my-address=ssl://name-coordinator-id1.name-int.ns.svc:8529", + "--cluster.my-id=id1", + "--cluster.my-role=COORDINATOR", + "--database.directory=/data", + "--foxx.queues=true", + "--log.level=INFO", + "--log.output=+", + "--server.authentication=true", + "--server.endpoint=ssl://[::]:8529", + "--server.jwt-secret=$(ARANGOD_JWT_SECRET)", + "--server.statistics=true", + "--server.storage-engine=rocksdb", + "--ssl.ecdh-curve=", + "--ssl.keyfile=/secrets/tls/tls.keyfile", + }, + cmdline, + ) + } + // No authentication { apiObject := &api.ArangoDeployment{ diff --git a/pkg/deployment/pod_creator_dbserver_args_test.go b/pkg/deployment/pod_creator_dbserver_args_test.go index e8ca64f84..e323d066e 100644 --- a/pkg/deployment/pod_creator_dbserver_args_test.go +++ b/pkg/deployment/pod_creator_dbserver_args_test.go @@ -73,6 +73,51 @@ func TestCreateArangodArgsDBServer(t *testing.T) { ) } + // Default+TLS deployment + { + apiObject := &api.ArangoDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "ns", + }, + Spec: api.DeploymentSpec{ + Mode: api.DeploymentModeCluster, + TLS: api.TLSSpec{ + CASecretName: "test-ca", + }, + }, + } + apiObject.Spec.SetDefaults("test") + agents := api.MemberStatusList{ + api.MemberStatus{ID: "a1"}, + api.MemberStatus{ID: "a2"}, + api.MemberStatus{ID: "a3"}, + } + cmdline := createArangodArgs(apiObject, apiObject.Spec, api.ServerGroupDBServers, apiObject.Spec.DBServers, agents, "id1") + assert.Equal(t, + []string{ + "--cluster.agency-endpoint=ssl://name-agent-a1.name-int.ns.svc:8529", + "--cluster.agency-endpoint=ssl://name-agent-a2.name-int.ns.svc:8529", + "--cluster.agency-endpoint=ssl://name-agent-a3.name-int.ns.svc:8529", + "--cluster.my-address=ssl://name-dbserver-id1.name-int.ns.svc:8529", + "--cluster.my-id=id1", + "--cluster.my-role=PRIMARY", + "--database.directory=/data", + "--foxx.queues=false", + "--log.level=INFO", + "--log.output=+", + "--server.authentication=true", + "--server.endpoint=ssl://[::]:8529", + "--server.jwt-secret=$(ARANGOD_JWT_SECRET)", + "--server.statistics=true", + "--server.storage-engine=rocksdb", + "--ssl.ecdh-curve=", + "--ssl.keyfile=/secrets/tls/tls.keyfile", + }, + cmdline, + ) + } + // No authentication { apiObject := &api.ArangoDeployment{ diff --git a/pkg/deployment/pod_creator_single_args_test.go b/pkg/deployment/pod_creator_single_args_test.go index a02b04ee0..da26ac0cc 100644 --- a/pkg/deployment/pod_creator_single_args_test.go +++ b/pkg/deployment/pod_creator_single_args_test.go @@ -58,6 +58,36 @@ func TestCreateArangodArgsSingle(t *testing.T) { ) } + // Default+TLS deployment + { + apiObject := &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Mode: api.DeploymentModeSingle, + TLS: api.TLSSpec{ + CASecretName: "test-ca", + }, + }, + } + apiObject.Spec.SetDefaults("test") + cmdline := createArangodArgs(apiObject, apiObject.Spec, api.ServerGroupSingle, apiObject.Spec.Single, nil, "id1") + assert.Equal(t, + []string{ + "--database.directory=/data", + "--foxx.queues=true", + "--log.level=INFO", + "--log.output=+", + "--server.authentication=true", + "--server.endpoint=ssl://[::]:8529", + "--server.jwt-secret=$(ARANGOD_JWT_SECRET)", + "--server.statistics=true", + "--server.storage-engine=rocksdb", + "--ssl.ecdh-curve=", + "--ssl.keyfile=/secrets/tls/tls.keyfile", + }, + cmdline, + ) + } + // Default deployment with mmfiles { apiObject := &api.ArangoDeployment{ diff --git a/pkg/deployment/secrets.go b/pkg/deployment/secrets.go index 48ef783ef..89b582d95 100644 --- a/pkg/deployment/secrets.go +++ b/pkg/deployment/secrets.go @@ -40,6 +40,16 @@ func (d *Deployment) createSecrets(apiObject *api.ArangoDeployment) error { return maskAny(err) } } + if apiObject.Spec.IsSecure() { + if err := d.ensureCACertificateSecret(apiObject.Spec.TLS); err != nil { + return maskAny(err) + } + } + if apiObject.Spec.Sync.Enabled { + if err := d.ensureCACertificateSecret(apiObject.Spec.Sync.TLS); err != nil { + return maskAny(err) + } + } return nil } @@ -72,6 +82,30 @@ func (d *Deployment) ensureJWTSecret(secretName string) error { return nil } +// ensureCACertificateSecret checks if a secret with given name exists in the namespace +// of the deployment. If not, it will add such a secret with a generated CA certificate. +// JWT token. +func (d *Deployment) ensureCACertificateSecret(spec api.TLSSpec) error { + kubecli := d.deps.KubeCli + ns := d.apiObject.GetNamespace() + if _, err := kubecli.CoreV1().Secrets(ns).Get(spec.CASecretName, metav1.GetOptions{}); k8sutil.IsNotFound(err) { + // Secret not found, create it + owner := d.apiObject.AsOwner() + deploymentName := d.apiObject.GetName() + if err := createCACertificate(d.deps.Log, kubecli.CoreV1(), spec, deploymentName, ns, &owner); k8sutil.IsAlreadyExists(err) { + // Secret added while we tried it also + return nil + } else if err != nil { + // Failed to create secret + return maskAny(err) + } + } else if err != nil { + // Failed to get secret for other reasons + return maskAny(err) + } + return nil +} + // getJWTSecret loads the JWT secret from a Secret configured in apiObject.Spec.Authentication.JWTSecretName. func (d *Deployment) getJWTSecret(apiObject *api.ArangoDeployment) (string, error) { if !apiObject.Spec.IsAuthenticated() { diff --git a/pkg/deployment/tls.go b/pkg/deployment/tls.go new file mode 100644 index 000000000..2bca95432 --- /dev/null +++ b/pkg/deployment/tls.go @@ -0,0 +1,129 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package deployment + +import ( + "fmt" + "strings" + "time" + + certificates "github.com/arangodb-helper/go-certificates" + "github.com/rs/zerolog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/typed/core/v1" + + api "github.com/arangodb/k8s-operator/pkg/apis/arangodb/v1alpha" + "github.com/arangodb/k8s-operator/pkg/util/k8sutil" +) + +const ( + caTTL = time.Hour * 24 * 365 * 10 // 10 year + tlsECDSACurve = "P256" // This curve is the default that ArangoDB accepts and plenty strong +) + +// createCACertificate creates a CA certificate and stores it in a secret with name +// specified in the given spec. +func createCACertificate(log zerolog.Logger, cli v1.CoreV1Interface, spec api.TLSSpec, deploymentName, namespace string, ownerRef *metav1.OwnerReference) error { + log = log.With().Str("secret", spec.CASecretName).Logger() + dnsNames, ipAddresses, emailAddress, err := spec.GetAltNames() + if err != nil { + log.Debug().Err(err).Msg("Failed to get alternate names") + return maskAny(err) + } + + options := certificates.CreateCertificateOptions{ + CommonName: fmt.Sprintf("%s Root Certificate", deploymentName), + Hosts: append(dnsNames, ipAddresses...), + EmailAddresses: emailAddress, + ValidFrom: time.Now(), + ValidFor: caTTL, + IsCA: true, + ECDSACurve: tlsECDSACurve, + } + cert, priv, err := certificates.CreateCertificate(options, nil) + if err != nil { + log.Debug().Err(err).Msg("Failed to create CA certificate") + return maskAny(err) + } + if err := k8sutil.CreateCASecret(cli, spec.CASecretName, namespace, cert, priv, ownerRef); err != nil { + if k8sutil.IsAlreadyExists(err) { + log.Debug().Msg("CA Secret already exists") + } else { + log.Debug().Err(err).Msg("Failed to create CA Secret") + } + return maskAny(err) + } + log.Debug().Msg("Created CA Secret") + return nil +} + +// createServerCertificate creates a TLS certificate for a specific server and stores +// it in a secret with the given name. +func createServerCertificate(log zerolog.Logger, cli v1.CoreV1Interface, serverNames []string, spec api.TLSSpec, secretName, namespace string, ownerRef *metav1.OwnerReference) error { + log = log.With().Str("secret", secretName).Logger() + // Load alt names + dnsNames, ipAddresses, emailAddress, err := spec.GetAltNames() + if err != nil { + log.Debug().Err(err).Msg("Failed to get alternate names") + return maskAny(err) + } + + // Load CA certificate + caCert, caKey, err := k8sutil.GetCASecret(cli, spec.CASecretName, namespace) + if err != nil { + log.Debug().Err(err).Msg("Failed to load CA certificate") + return maskAny(err) + } + ca, err := certificates.LoadCAFromPEM(caCert, caKey) + if err != nil { + log.Debug().Err(err).Msg("Failed to decode CA certificate") + return maskAny(err) + } + + options := certificates.CreateCertificateOptions{ + CommonName: serverNames[0], + Hosts: append(append(serverNames, dnsNames...), ipAddresses...), + EmailAddresses: emailAddress, + ValidFrom: time.Now(), + ValidFor: spec.TTL, + IsCA: false, + ECDSACurve: tlsECDSACurve, + } + cert, priv, err := certificates.CreateCertificate(options, &ca) + if err != nil { + log.Debug().Err(err).Msg("Failed to create server certificate") + return maskAny(err) + } + keyfile := strings.TrimSpace(cert) + "\n" + + strings.TrimSpace(priv) + if err := k8sutil.CreateTLSKeyfileSecret(cli, secretName, namespace, keyfile, ownerRef); err != nil { + if k8sutil.IsAlreadyExists(err) { + log.Debug().Msg("Server Secret already exists") + } else { + log.Debug().Err(err).Msg("Failed to create server Secret") + } + return maskAny(err) + } + log.Debug().Msg("Created server Secret") + return nil +} diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 7a12ffe5d..d0a646844 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -32,4 +32,9 @@ const ( SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key SecretKeyJWT = "token" // Key inside a Secret used to hold a JW token + + SecretCACertificate = "ca.crt" // Key in Secret.data used to store a PEM encoded CA certificate (public key) + SecretCAKey = "ca.key" // Key in Secret.data used to store a PEM encoded CA private key + + SecretTLSKeyfile = "tls.keyfile" // Key in Secret.data used to store a PEM encoded TLS certificate in the format used by ArangoDB (`--ssl.keyfile`) ) diff --git a/pkg/util/k8sutil/pods.go b/pkg/util/k8sutil/pods.go index 2e8612e34..e3a33ec89 100644 --- a/pkg/util/k8sutil/pods.go +++ b/pkg/util/k8sutil/pods.go @@ -30,9 +30,11 @@ import ( const ( arangodVolumeName = "arangod-data" + tlsKeyfileVolumeName = "tls-keyfile" rocksdbEncryptionVolumeName = "rocksdb-encryption" ArangodVolumeMountDir = "/data" RocksDBEncryptionVolumeMountDir = "/secrets/rocksdb/encryption" + TLSKeyfileVolumeMountDir = "/secrets/tls" ) // EnvValue is a helper structure for environment variable sources. @@ -98,6 +100,12 @@ func CreatePodName(deploymentName, role, id string) string { return deploymentName + "-" + role + "-" + id } +// CreateTLSKeyfileSecretName returns the name of the Secret that holds the TLS keyfile for a member with +// a given id in a deployment with a given name. +func CreateTLSKeyfileSecretName(deploymentName, role, id string) string { + return CreatePodName(deploymentName, role, id) + "-tls-keyfile" +} + // arangodVolumeMounts creates a volume mount structure for arangod. func arangodVolumeMounts() []v1.VolumeMount { return []v1.VolumeMount{ @@ -105,7 +113,17 @@ func arangodVolumeMounts() []v1.VolumeMount { } } -// arangodVolumeMounts creates a volume mount structure for arangod. +// tlsKeyfileVolumeMounts creates a volume mount structure for a TLS keyfile. +func tlsKeyfileVolumeMounts() []v1.VolumeMount { + return []v1.VolumeMount{ + { + Name: tlsKeyfileVolumeName, + MountPath: TLSKeyfileVolumeMountDir, + }, + } +} + +// rocksdbEncryptionVolumeMounts creates a volume mount structure for a RocksDB encryption key. func rocksdbEncryptionVolumeMounts() []v1.VolumeMount { return []v1.VolumeMount{ { @@ -193,12 +211,15 @@ func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deploy role, id, pvcName, image string, imagePullPolicy v1.PullPolicy, args []string, env map[string]EnvValue, livenessProbe *HTTPProbeConfig, readinessProbe *HTTPProbeConfig, - rocksdbEncryptionSecretName string) error { + tlsKeyfileSecretName, rocksdbEncryptionSecretName string) error { // Prepare basic pod p := newPod(deployment.GetName(), deployment.GetNamespace(), role, id) // Add arangod container c := arangodContainer(p.GetName(), image, imagePullPolicy, args, env, livenessProbe, readinessProbe) + if tlsKeyfileSecretName != "" { + c.VolumeMounts = append(c.VolumeMounts, tlsKeyfileVolumeMounts()...) + } if rocksdbEncryptionSecretName != "" { c.VolumeMounts = append(c.VolumeMounts, rocksdbEncryptionVolumeMounts()...) } @@ -227,6 +248,19 @@ func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deploy p.Spec.Volumes = append(p.Spec.Volumes, vol) } + // TLS keyfile secret mount (if any) + if tlsKeyfileSecretName != "" { + vol := v1.Volume{ + Name: tlsKeyfileVolumeName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: tlsKeyfileSecretName, + }, + }, + } + p.Spec.Volumes = append(p.Spec.Volumes, vol) + } + // RocksDB encryption secret mount (if any) if rocksdbEncryptionSecretName != "" { vol := v1.Volume{ diff --git a/pkg/util/k8sutil/secrets.go b/pkg/util/k8sutil/secrets.go index d26ca626b..c9d2d596a 100644 --- a/pkg/util/k8sutil/secrets.go +++ b/pkg/util/k8sutil/secrets.go @@ -71,6 +71,70 @@ func CreateEncryptionKeySecret(cli corev1.CoreV1Interface, secretName, namespace return nil } +// GetCASecret loads a secret with given name in the given namespace +// and extracts the `ca.crt` & `ca.key` field. +// If the secret does not exists or one of the fields is missing, +// an error is returned. +// Returns: certificate, private-key, error +func GetCASecret(cli corev1.CoreV1Interface, secretName, namespace string) (string, string, error) { + s, err := cli.Secrets(namespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + return "", "", maskAny(err) + } + // Load `ca.crt` field + cert, found := s.Data[constants.SecretCACertificate] + if !found { + return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretCACertificate, secretName)) + } + priv, found := s.Data[constants.SecretCAKey] + if !found { + return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretCAKey, secretName)) + } + return string(cert), string(priv), nil +} + +// CreateCASecret creates a secret used to store a PEM encoded CA certificate & private key. +func CreateCASecret(cli corev1.CoreV1Interface, secretName, namespace string, certificate, key string, ownerRef *metav1.OwnerReference) error { + // Create secret + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + Data: map[string][]byte{ + constants.SecretCACertificate: []byte(certificate), + constants.SecretCAKey: []byte(key), + }, + } + // Attach secret to owner + addOwnerRefToObject(secret, ownerRef) + if _, err := cli.Secrets(namespace).Create(secret); err != nil { + // Failed to create secret + return maskAny(err) + } + return nil +} + +// CreateTLSKeyfileSecret creates a secret used to store a PEM encoded keyfile +// in the format ArangoDB accepts it for its `--ssl.keyfile` option. +func CreateTLSKeyfileSecret(cli corev1.CoreV1Interface, secretName, namespace string, keyfile string, ownerRef *metav1.OwnerReference) error { + // Create secret + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + Data: map[string][]byte{ + constants.SecretTLSKeyfile: []byte(keyfile), + }, + } + // Attach secret to owner + addOwnerRefToObject(secret, ownerRef) + if _, err := cli.Secrets(namespace).Create(secret); err != nil { + // Failed to create secret + return maskAny(err) + } + return nil +} + // GetJWTSecret loads the JWT secret from a Secret with given name. func GetJWTSecret(cli corev1.CoreV1Interface, secretName, namespace string) (string, error) { s, err := cli.Secrets(namespace).Get(secretName, metav1.GetOptions{}) diff --git a/pkg/util/validation/dns_name.go b/pkg/util/validation/dns_name.go new file mode 100644 index 000000000..835bec0ee --- /dev/null +++ b/pkg/util/validation/dns_name.go @@ -0,0 +1,42 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package validation + +import ( + "regexp" + "strings" +) + +var ( + dnsNameRegexp = regexp.MustCompile(`^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`) +) + +// IsValidDNSName returns true when the given input is a valid DNS name +func IsValidDNSName(input string) bool { + // IsDNSName will validate the given string as a DNS name + if input == "" || len(strings.Replace(input, ".", "", -1)) > 255 { + // constraints already violated + return false + } + return dnsNameRegexp.MatchString(input) +} diff --git a/pkg/util/validation/dns_name_test.go b/pkg/util/validation/dns_name_test.go new file mode 100644 index 000000000..7847f8536 --- /dev/null +++ b/pkg/util/validation/dns_name_test.go @@ -0,0 +1,48 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidDNSName(t *testing.T) { + valid := []string{ + "foo.example.com", + } + invalid := []string{ + "verylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylongverylong", + "@.com", + " .arangodb.com", + "invalid.arangodb . com", + } + + for _, x := range valid { + assert.True(t, IsValidDNSName(x), x) + } + for _, x := range invalid { + assert.False(t, IsValidDNSName(x), x) + } +} diff --git a/pkg/util/validation/email_address.go b/pkg/util/validation/email_address.go new file mode 100644 index 000000000..948e72800 --- /dev/null +++ b/pkg/util/validation/email_address.go @@ -0,0 +1,34 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package validation + +import "regexp" + +var ( + emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") +) + +// IsValidEmailAddress returns true when the given input is a valid email address +func IsValidEmailAddress(input string) bool { + return emailRegexp.MatchString(input) +} diff --git a/pkg/util/validation/email_address_test.go b/pkg/util/validation/email_address_test.go new file mode 100644 index 000000000..e55a107b1 --- /dev/null +++ b/pkg/util/validation/email_address_test.go @@ -0,0 +1,46 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidEmailAddress(t *testing.T) { + valid := []string{ + "foo@example.com", + } + invalid := []string{ + "ç$?§/az@gmail.com", + "foo@example_underscore.com", + } + + for _, x := range valid { + assert.True(t, IsValidEmailAddress(x), x) + } + for _, x := range invalid { + assert.False(t, IsValidEmailAddress(x), x) + } +}