From e9374730c907b73f4d6e18b124c296d9442ead20 Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Tue, 3 Mar 2020 09:23:45 +0000 Subject: [PATCH] Add --experimental-issue-pkcs12 flag to enable PKCS12 bundle generation Signed-off-by: James Munnelly --- LICENSES | 36 ++++++++++++ cmd/controller/app/options/options.go | 23 ++++++++ go.mod | 1 + go.sum | 1 + pkg/controller/certificates/BUILD.bazel | 2 + pkg/controller/certificates/controller.go | 20 +++++++ pkg/controller/certificates/keystore.go | 71 +++++++++++++++++++++++ pkg/controller/certificates/sync.go | 23 +++++++- pkg/controller/context.go | 12 ++++ 9 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 pkg/controller/certificates/keystore.go diff --git a/LICENSES b/LICENSES index 846b98d4c38..897a523a819 100644 --- a/LICENSES +++ b/LICENSES @@ -17898,3 +17898,39 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. = vendor/sigs.k8s.io/yaml/LICENSE 0ceb9ff3b27d3a8cf451ca3785d73c71 ================================================================================ + +================================================================================ += vendor/software.sslmate.com/src/go-pkcs12 licensed under: = + +Copyright (c) 2015, 2018 Opsmate, Inc. +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + += vendor/software.sslmate.com/src/go-pkcs12/LICENSE 59ab963efbc183656060b23893ea5745 +================================================================================ + diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 78b36453e8f..22de88aae0e 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -94,6 +94,18 @@ type ControllerOptions struct { // DNSNames are the dns names that should be set on the serving certificate WebhookDNSNames []string + + // ExperimentalIssuePKCS12, if true, will make the certificates controller + // create a `keystore.p12` in the Secret resource for each Certificate. + // This can only be toggled globally, and the keystore will be encrypted + // with the supplied ExperimentalPKCS12KeystorePassword. + // This flag is likely to be removed in future in favour of native PKCS12 + // keystore bundle support. + ExperimentalIssuePKCS12 bool + // ExperimentalPKCS12KeystorePassword is the password used to encrypt and + // decrypt PKCS#12 bundles stored in Secret resources. + // This option only has any affect is ExperimentalIssuePKCS12 is true. + ExperimentalPKCS12KeystorePassword string } const ( @@ -289,6 +301,12 @@ func (s *ControllerOptions) AddFlags(fs *pflag.FlagSet) { "serving certificate.") fs.StringSliceVar(&s.WebhookDNSNames, "webhook-dns-names", defaultWebhookDNSNames, "Comma-separated list of DNS names that should be present on "+ "the webhook's serving certificate.") + + fs.BoolVar(&s.ExperimentalIssuePKCS12, "experimental-issue-pkcs12", false, "If true, the certificate controller will create 'keystore.p12' files in Secret resources it "+ + "manages, containing a copy of the certificate data encrypted using the provided --experimental-pkcs12-keystore-password. "+ + "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.") } func (o *ControllerOptions) Validate() error { @@ -306,5 +324,10 @@ func (o *ControllerOptions) Validate() error { return fmt.Errorf("invalid DNS server (%v): %v", err, server) } } + + if o.ExperimentalIssuePKCS12 && len(o.ExperimentalPKCS12KeystorePassword) == 0 { + return fmt.Errorf("--experimental-pkcs12-keystore-password must be specified if --experimental-issue-pkcs12 is enabled") + } + return nil } diff --git a/go.mod b/go.mod index b1f312b581f..13732d44a04 100644 --- a/go.mod +++ b/go.mod @@ -55,4 +55,5 @@ require ( sigs.k8s.io/controller-runtime v0.4.0 sigs.k8s.io/controller-tools v0.2.5 sigs.k8s.io/testing_frameworks v0.1.2 + software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 ) diff --git a/go.sum b/go.sum index ce6b85a49a1..108344feed2 100644 --- a/go.sum +++ b/go.sum @@ -744,4 +744,5 @@ sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 h1:iAEkCBPbRaflBgZ7o9gjVUuWuvWeV4sytFWg9o+Pj2k= software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237/go.mod h1:/xvNRWUqm0+/ZMiF4EX00vrSCMsE4/NHb+Pt3freEeQ= diff --git a/pkg/controller/certificates/BUILD.bazel b/pkg/controller/certificates/BUILD.bazel index 82960b1930d..dcc15d00adf 100644 --- a/pkg/controller/certificates/BUILD.bazel +++ b/pkg/controller/certificates/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "checks.go", "controller.go", + "keystore.go", "sync.go", "util.go", ], @@ -26,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_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", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", diff --git a/pkg/controller/certificates/controller.go b/pkg/controller/certificates/controller.go index 7428245e9dc..c8113361ecf 100644 --- a/pkg/controller/certificates/controller.go +++ b/pkg/controller/certificates/controller.go @@ -19,6 +19,7 @@ package certificates import ( "context" "crypto/x509" + "fmt" "time" "k8s.io/client-go/kubernetes" @@ -82,6 +83,18 @@ type certificateRequestManager struct { // Secret resource will be automatically deleted. // This option is disabled by default. enableSecretOwnerReferences bool + + // experimentalIssuePKCS12, if true, will make the certificates controller + // create a `keystore.p12` in the Secret resource for each Certificate. + // This can only be toggled globally, and the keystore will be encrypted + // with the supplied ExperimentalPKCS12KeystorePassword. + // This flag is likely to be removed in future in favour of native PKCS12 + // keystore bundle support. + experimentalIssuePKCS12 bool + // ExperimentalPKCS12KeystorePassword is the password used to encrypt and + // decrypt PKCS#12 bundles stored in Secret resources. + // This option only has any affect is ExperimentalIssuePKCS12 is true. + experimentalPKCS12KeystorePassword string } type localTemporarySignerFn func(crt *cmapi.Certificate, pk []byte) ([]byte, error) @@ -139,6 +152,13 @@ func (c *certificateRequestManager) Register(ctx *controllerpkg.Context) (workqu c.localTemporarySigner = generateLocallySignedTemporaryCertificate c.enableSecretOwnerReferences = ctx.CertificateOptions.EnableOwnerRef + // Experimental PKCS12 handling options + c.experimentalIssuePKCS12 = ctx.CertificateOptions.ExperimentalIssuePKCS12 + c.experimentalPKCS12KeystorePassword = ctx.CertificateOptions.ExperimentalPKCS12KeystorePassword + 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") + } + c.cmClient = ctx.CMClient c.kubeClient = ctx.Client diff --git a/pkg/controller/certificates/keystore.go b/pkg/controller/certificates/keystore.go new file mode 100644 index 00000000000..2999a92c5a8 --- /dev/null +++ b/pkg/controller/certificates/keystore.go @@ -0,0 +1,71 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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. +*/ + +// This file defines methods used for PKCS#12 support. +// This is an experimental feature and the contents of this file are intended +// to be absorbed into a more fully fledged implementing ahead of the v0.15 +// release. +// This should hopefully not exist by the next time you come to read this :) + +package certificates + +import ( + "crypto/rand" + "crypto/x509" + + "software.sslmate.com/src/go-pkcs12" + + "github.com/jetstack/cert-manager/pkg/util/pki" +) + +const ( + // pkcs12SecretKey is the name of the data entry in the Secret resource + // used to store the p12 file. + pkcs12SecretKey = "keystore.p12" +) + +// encodePKCS12Keystore will encode a PKCS12 keystore using the password provided. +// The key, certificate and CA data must be provided in PKCS1 or PKCS8 PEM format. +// If the certificate data contains multiple certificates, the first will be used +// as the keystores 'certificate' and the remaining certificates will be prepended +// to the list of CAs in the resulting keystore. +func encodePKCS12Keystore(password string, rawKey []byte, certPem []byte, caPem []byte) ([]byte, error) { + key, err := pki.DecodePrivateKeyBytes(rawKey) + if err != nil { + return nil, err + } + certs, err := pki.DecodeX509CertificateChainBytes(certPem) + if err != nil { + return nil, err + } + var cas []*x509.Certificate + if len(caPem) > 0 { + cas, err = pki.DecodeX509CertificateChainBytes(caPem) + if err != nil { + return nil, err + } + // prepend the certificate chain to the list of certificates as the PKCS12 + // library only allows setting a single certificate. + if len(certs) > 1 { + cas = append(certs[1:], cas...) + } + } + keystoreData, err := pkcs12.Encode(rand.Reader, key, certs[0], cas, password) + if err != nil { + return nil, err + } + return keystoreData, nil +} diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index 4f1cf322abb..ed7cda0a520 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -17,6 +17,7 @@ limitations under the License. package certificates import ( + "bytes" "context" "crypto/ecdsa" "crypto/rsa" @@ -536,7 +537,7 @@ func (c *certificateRequestManager) updateSecretData(ctx context.Context, crt *c } newSecret := s.DeepCopy() - err := setSecretValues(ctx, crt, newSecret, secretData{pk: data.pk, cert: data.cert, ca: data.ca}) + err := c.setSecretValues(ctx, crt, newSecret, secretData{pk: data.pk, cert: data.cert, ca: data.ca}) if err != nil { return false, err } @@ -582,7 +583,7 @@ func (c *certificateRequestManager) issueTemporaryCertificate(ctx context.Contex } newSecret := secret.DeepCopy() - err = setSecretValues(ctx, crt, newSecret, secretData{pk: key, cert: tempCertData}) + err = c.setSecretValues(ctx, crt, newSecret, secretData{pk: key, cert: tempCertData}) if err != nil { return err } @@ -808,12 +809,28 @@ type secretData struct { // If updating an existing Secret resource returned by an api client 'lister', // make sure to DeepCopy the object first to avoid modifying data in-cache. // It will also update depreciated issuer name and kind annotations if they exist. -func setSecretValues(ctx context.Context, crt *cmapi.Certificate, s *corev1.Secret, data secretData) error { +func (c *certificateRequestManager) setSecretValues(ctx context.Context, crt *cmapi.Certificate, s *corev1.Secret, data secretData) error { // initialize the `Data` field if it is nil if s.Data == nil { s.Data = make(map[string][]byte) } + // Handle the experimental PKCS12 support + if c.experimentalIssuePKCS12 { + // Only write a new PKCS12 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 := encodePKCS12Keystore(c.experimentalPKCS12KeystorePassword, data.pk, data.cert, data.ca) + if err != nil { + return fmt.Errorf("error encoding PKCS12 bundle: %w", err) + } + // always overwrite the keystore entry for now + s.Data[pkcs12SecretKey] = 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 a140f79d844..01054c96ba3 100644 --- a/pkg/controller/context.go +++ b/pkg/controller/context.go @@ -132,6 +132,18 @@ type CertificateOptions struct { // EnableOwnerRef controls whether the certificate is configured as an owner of // secret where the effective TLS certificate is stored. EnableOwnerRef bool + + // ExperimentalIssuePKCS12, if true, will make the certificates controller + // create a `keystore.p12` in the Secret resource for each Certificate. + // This can only be toggled globally, and the keystore will be encrypted + // with the supplied ExperimentalPKCS12KeystorePassword. + // This flag is likely to be removed in future in favour of native PKCS12 + // keystore bundle support. + ExperimentalIssuePKCS12 bool + // ExperimentalPKCS12KeystorePassword is the password used to encrypt and + // decrypt PKCS#12 bundles stored in Secret resources. + // This option only has any affect is ExperimentalIssuePKCS12 is true. + ExperimentalPKCS12KeystorePassword string } type SchedulerOptions struct {