1
0
Fork 0
mirror of https://github.com/arangodb/kube-arangodb.git synced 2024-12-14 11:57:37 +00:00

Merge pull request #43 from arangodb/tls

TLS support
This commit is contained in:
Ewout Prangsma 2018-03-12 14:40:18 +01:00 committed by GitHub
commit 6c3688a435
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 3946 additions and 154 deletions

View file

@ -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 \

View file

@ -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.

View file

@ -0,0 +1,4 @@
# go-certificates
Library for golang code related to creating certificates.

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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
)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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-----

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

42
docs/user/tls.md Normal file
View file

@ -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 <deploy-name> --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

View file

@ -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"]

View file

@ -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"))

View file

@ -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"
}
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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() {

View file

@ -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{

View file

@ -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{

View file

@ -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{

View file

@ -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{

View file

@ -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() {

129
pkg/deployment/tls.go Normal file
View file

@ -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
}

View file

@ -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`)
)

View file

@ -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{

View file

@ -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{})

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}
}