diff --git a/LICENSES b/LICENSES index 897a523a8..b24b717af 100644 --- a/LICENSES +++ b/LICENSES @@ -10550,6 +10550,35 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ +================================================================================ += vendor/github.com/pavel-v-chernykh/keystore-go licensed under: = + +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. + += vendor/github.com/pavel-v-chernykh/keystore-go/LICENSE 1ef2fe9f6c2064290158c50854f690dc +================================================================================ + + ================================================================================ = vendor/github.com/pierrec/lz4 licensed under: = diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 22de88aae..ae9be5b23 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -106,6 +106,18 @@ type ControllerOptions struct { // decrypt PKCS#12 bundles stored in Secret resources. // This option only has any affect is ExperimentalIssuePKCS12 is true. ExperimentalPKCS12KeystorePassword string + + // ExperimentalIssueJKS, if true, will make the certificates controller + // create a `keystore.jks` in the Secret resource for each Certificate. + // This can only be toggled globally, and the keystore will be encrypted + // with the supplied ExperimentalJKSPassword. + // This flag is likely to be removed in future in favour of native JKS + // keystore bundle support. + ExperimentalIssueJKS bool + // ExperimentalJKSPassword is the password used to encrypt and + // decrypt JKS bundles stored in Secret resources. + // This option only has any affect is ExperimentalIssueJKS is true. + ExperimentalJKSPassword string } const ( @@ -307,6 +319,11 @@ func (s *ControllerOptions) AddFlags(fs *pflag.FlagSet) { "If true, --experimental-pkcs12-keystore-password must be provided.") fs.StringVar(&s.ExperimentalPKCS12KeystorePassword, "experimental-pkcs12-keystore-password", "", "The password used to encrypt and decrypt PKCS#12 "+ "bundles stored in Secret resources. This field is required if --experimental-issue-pkcs12 is enabled.") + fs.BoolVar(&s.ExperimentalIssueJKS, "experimental-issue-jks", false, "If true, the certificate controller will create 'keystore.jks' files in Secret resources it "+ + "manages, containing a copy of the certificate data encrypted using the provided --experimental-jks-password. "+ + "If true, --experimental-jks-password must be provided.") + fs.StringVar(&s.ExperimentalJKSPassword, "experimental-jks-password", "", "The password used to encrypt and decrypt JKS "+ + "bundles stored in Secret resources. This field is required if --experimental-issue-jks is enabled.") } func (o *ControllerOptions) Validate() error { diff --git a/go.mod b/go.mod index 13732d44a..14ba62626 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/munnerz/crd-schema-fuzz v0.0.0-20191114184610-fbd148d44a0a github.com/onsi/ginkgo v1.10.1 github.com/onsi/gomega v1.7.0 + github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.0.0 github.com/spf13/cobra v0.0.5 diff --git a/go.sum b/go.sum index 108344fee..404bbfc2f 100644 --- a/go.sum +++ b/go.sum @@ -401,6 +401,8 @@ github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible h1:Jd6xfriVlJ6hWPvYOE0Ni0QWcNTLRehfGPFxr3eSL80= +github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible/go.mod h1:xlUlxe/2ItGlQyMTstqeDv9r3U4obH7xYd26TbDQutY= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/hack/build/repos.bzl b/hack/build/repos.bzl index 0e24ead27..cbc9b2caa 100644 --- a/hack/build/repos.bzl +++ b/hack/build/repos.bzl @@ -2451,3 +2451,11 @@ def go_repositories(): sum = "h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=", version = "v1.0.0-20181015200546-f715ec2f112d", ) + go_repository( + name = "com_github_pavel_v_chernykh_keystore_go", + build_file_generation = "on", + build_file_proto_mode = "disable", + importpath = "github.com/pavel-v-chernykh/keystore-go", + sum = "h1:Jd6xfriVlJ6hWPvYOE0Ni0QWcNTLRehfGPFxr3eSL80=", + version = "v2.1.0+incompatible", + ) diff --git a/pkg/controller/certificates/BUILD.bazel b/pkg/controller/certificates/BUILD.bazel index dcc15d00a..868dee8dc 100644 --- a/pkg/controller/certificates/BUILD.bazel +++ b/pkg/controller/certificates/BUILD.bazel @@ -27,6 +27,7 @@ go_library( "//pkg/util/pki:go_default_library", "@com_github_go_logr_logr//:go_default_library", "@com_github_kr_pretty//:go_default_library", + "@com_github_pavel_v_chernykh_keystore_go//:go_default_library", "@com_sslmate_software_src_go_pkcs12//:go_default_library", "@io_k8s_api//core/v1:go_default_library", "@io_k8s_apimachinery//pkg/api/errors:go_default_library", diff --git a/pkg/controller/certificates/controller.go b/pkg/controller/certificates/controller.go index c8113361e..4ca3402f9 100644 --- a/pkg/controller/certificates/controller.go +++ b/pkg/controller/certificates/controller.go @@ -95,6 +95,17 @@ type certificateRequestManager struct { // decrypt PKCS#12 bundles stored in Secret resources. // This option only has any affect is ExperimentalIssuePKCS12 is true. experimentalPKCS12KeystorePassword string + // experimentalIssueJKS, if true, will make the certificates controller + // create a `keystore.jks` in the Secret resource for each Certificate. + // This can only be toggled globally, and the keystore will be encrypted + // with the supplied ExperimentalJKSPassword. + // This flag is likely to be removed in future in favour of native JKS + // keystore bundle support. + experimentalIssueJKS bool + // experimentalJKSPassword is the password used to encrypt and + // decrypt JKS files stored in Secret resources. + // This option only has any affect is experimentalIssueJKS is true. + experimentalJKSPassword string } type localTemporarySignerFn func(crt *cmapi.Certificate, pk []byte) ([]byte, error) @@ -158,6 +169,12 @@ func (c *certificateRequestManager) Register(ctx *controllerpkg.Context) (workqu if c.experimentalIssuePKCS12 && len(c.experimentalPKCS12KeystorePassword) == 0 { return nil, nil, nil, fmt.Errorf("if experimental pkcs12 issuance is enabled, a keystore password must be provided") } + // Experimental JKS handling options + c.experimentalIssueJKS = ctx.CertificateOptions.ExperimentalIssueJKS + c.experimentalJKSPassword = ctx.CertificateOptions.ExperimentalJKSPassword + if c.experimentalIssueJKS && len(c.experimentalJKSPassword) == 0 { + return nil, nil, nil, fmt.Errorf("if experimental pkcs12 issuance is enabled, a keystore password must be provided") + } c.cmClient = ctx.CMClient c.kubeClient = ctx.Client diff --git a/pkg/controller/certificates/keystore.go b/pkg/controller/certificates/keystore.go index 2999a92c5..e805b441f 100644 --- a/pkg/controller/certificates/keystore.go +++ b/pkg/controller/certificates/keystore.go @@ -23,9 +23,12 @@ limitations under the License. package certificates import ( + "bytes" "crypto/rand" "crypto/x509" + "time" + jks "github.com/pavel-v-chernykh/keystore-go" "software.sslmate.com/src/go-pkcs12" "github.com/jetstack/cert-manager/pkg/util/pki" @@ -35,6 +38,10 @@ const ( // pkcs12SecretKey is the name of the data entry in the Secret resource // used to store the p12 file. pkcs12SecretKey = "keystore.p12" + + // jksSecretKey is the name of the data entry in the Secret resource + // used to store the jks file. + jksSecretKey = "keystore.jks" ) // encodePKCS12Keystore will encode a PKCS12 keystore using the password provided. @@ -69,3 +76,61 @@ func encodePKCS12Keystore(password string, rawKey []byte, certPem []byte, caPem } return keystoreData, nil } + +func encodeJKSKeystore(password string, rawKey []byte, certPem []byte, caPem []byte) ([]byte, error) { + // encode the private key to PKCS8 + key, err := pki.DecodePrivateKeyBytes(rawKey) + if err != nil { + return nil, err + } + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, err + } + + // encode the certificate chain + chain, err := pki.DecodeX509CertificateChainBytes(certPem) + if err != nil { + return nil, err + } + certs := make([]jks.Certificate, len(chain)) + for i, cert := range chain { + certs[i] = jks.Certificate{ + Type: "X509", + Content: cert.Raw, + } + } + + ks := jks.KeyStore{ + "certificate": jks.PrivateKeyEntry{ + Entry: jks.Entry{ + CreationDate: time.Now(), + }, + PrivKey: keyDER, + CertChain: certs, + }, + } + // add the CA certificate, if set + if len(caPem) > 0 { + ca, err := pki.DecodeX509CertificateBytes(caPem) + if err != nil { + return nil, err + } + + ks["ca"] = jks.TrustedCertificateEntry{ + Entry: jks.Entry{ + CreationDate: time.Now(), + }, + Certificate: jks.Certificate{ + Type: "X509", + Content: ca.Raw, + }, + } + } + + buf := &bytes.Buffer{} + if err := jks.Encode(buf, ks, []byte(password)); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index ed7cda0a5..5ad90efc7 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -831,6 +831,23 @@ func (c *certificateRequestManager) setSecretValues(ctx context.Context, crt *cm s.Data[pkcs12SecretKey] = keystoreData } } + // Handle the experimental JKS support + if c.experimentalIssueJKS { + // Only write a new JKS file if any of the private key/certificate/CA data has + // actually changed. + if data.pk != nil && data.cert != nil && + (!bytes.Equal(s.Data[corev1.TLSPrivateKeyKey], data.pk) || + !bytes.Equal(s.Data[corev1.TLSCertKey], data.cert) || + !bytes.Equal(s.Data[cmmeta.TLSCAKey], data.ca)) { + keystoreData, err := encodeJKSKeystore(c.experimentalJKSPassword, data.pk, data.cert, data.ca) + if err != nil { + return fmt.Errorf("error encoding JKS bundle: %w", err) + } + // always overwrite the keystore entry for now + s.Data[jksSecretKey] = keystoreData + } + } + s.Data[corev1.TLSPrivateKeyKey] = data.pk s.Data[corev1.TLSCertKey] = data.cert s.Data[cmmeta.TLSCAKey] = data.ca diff --git a/pkg/controller/context.go b/pkg/controller/context.go index 01054c96b..38aa276ec 100644 --- a/pkg/controller/context.go +++ b/pkg/controller/context.go @@ -144,6 +144,18 @@ type CertificateOptions struct { // decrypt PKCS#12 bundles stored in Secret resources. // This option only has any affect is ExperimentalIssuePKCS12 is true. ExperimentalPKCS12KeystorePassword string + + // ExperimentalIssueJKS, if true, will make the certificates controller + // create a `keystore.jks` in the Secret resource for each Certificate. + // This can only be toggled globally, and the keystore will be encrypted + // with the supplied ExperimentalJKSPassword. + // This flag is likely to be removed in future in favour of native JKS + // bundle support. + ExperimentalIssueJKS bool + // ExperimentalJKSPassword is the password used to encrypt and + // decrypt JKS bundles stored in Secret resources. + // This option only has any affect is ExperimentalIssueJKS is true. + ExperimentalJKSPassword string } type SchedulerOptions struct {