diff --git a/Makefile b/Makefile index efb0d19..5d146e4 100644 --- a/Makefile +++ b/Makefile @@ -45,10 +45,16 @@ MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 LICENSE_PATH ?= hack/boilerplate.go.txt -.PHONY: generate -generate: controller-gen client-gen informer-gen lister-gen ## Generate code containing DeepCopy, DeepCopyInto, ClientSet, Client Informer and DeepCopyObject method implementations. - # $(CONTROLLER_GEN) object:headerFile="$(LICENSE_PATH)" paths="./..." +.PHONY: generate +generate: generate-crd generate-client + +.PHONY: generate-crd +generate-crd: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="$(LICENSE_PATH)" paths="./..." + +.PHONY: generate-client +generate-client: client-gen informer-gen lister-gen ## Generate code containing ClientSet, Client Informer and Client Lister method implementations. rm -rf ./pkg/client/clientset rm -rf ./pkg/client/listers rm -rf ./pkg/client/informers @@ -75,6 +81,7 @@ --output-package $(MODULE_NAME)/pkg/client/informers \ --output-base ./ + .PHONY: fmt fmt: ## Run go fmt against code. go fmt ./... diff --git a/go.mod b/go.mod index bb729a4..7895144 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,14 @@ github.com/go-logr/logr v1.2.3 github.com/onsi/ginkgo/v2 v2.1.4 github.com/onsi/gomega v1.19.0 + github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 + k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.0 k8s.io/client-go v0.25.0 k8s.io/klog/v2 v2.70.1 k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed sigs.k8s.io/controller-runtime v0.13.0 + software.sslmate.com/src/go-pkcs12 v0.2.0 ) require ( @@ -59,7 +62,7 @@ go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect @@ -72,7 +75,6 @@ gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.25.0 // indirect k8s.io/apiextensions-apiserver v0.25.0 // indirect k8s.io/component-base v0.25.0 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect diff --git a/go.sum b/go.sum index 032c1a6..df924a3 100644 --- a/go.sum +++ b/go.sum @@ -282,6 +282,8 @@ github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 h1:y9azNmMzvkNBPyczpNRwaV4bm0U6e7Oyrj7gi2/SNFI= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -360,8 +362,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -795,3 +797,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/pkg/api/util/issuers.go b/pkg/api/util/issuers.go new file mode 100644 index 0000000..15461e2 --- /dev/null +++ b/pkg/api/util/issuers.go @@ -0,0 +1,14 @@ +package util + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" +) + +// IssuerKind returns the kind of issuer for a certificate. +func IssuerKind(ref acmmeta.ObjectReference) string { + if ref.Kind == "" { + return acmapi.IssuerKind + } + return ref.Kind +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index e45d54f..ee10d28 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -50,8 +50,55 @@ PKCS8 PrivateKeyEncoding = "PKCS8" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// CertificateKeystores configures additional keystore output formats to be +// created in the Certificate's output Secret. +type CertificateKeystores struct { + // JKS configures options for storing a JKS keystore in the + // `spec.secretName` Secret resource. + // +optional + JKS *JKSKeystore `json:"jks,omitempty"` + + // PKCS12 configures options for storing a PKCS12 keystore in the + // `spec.secretName` Secret resource. + // +optional + PKCS12 *PKCS12Keystore `json:"pkcs12,omitempty"` +} + +// JKS configures options for storing a JKS keystore in the `spec.secretName` +// Secret resource. +type JKSKeystore struct { + // Create enables JKS keystore creation for the Certificate. + // If true, a file named `keystore.jks` will be created in the target + // Secret resource, encrypted using the password stored in + // `passwordSecretRef`. + // The keystore file will only be updated upon re-issuance. + // A file named `truststore.jks` will also be created in the target + // Secret resource, encrypted using the password stored in + // `passwordSecretRef` containing the issuing Certificate Authority + Create bool `json:"create"` + + // PasswordSecretRef is a reference to a key in a Secret resource + // containing the password used to encrypt the JKS keystore. + PasswordSecretRef acmmeta.SecretKeySelector `json:"passwordSecretRef"` +} + +// PKCS12 configures options for storing a PKCS12 keystore in the +// `spec.secretName` Secret resource. +type PKCS12Keystore struct { + // Create enables PKCS12 keystore creation for the Certificate. + // If true, a file named `keystore.p12` will be created in the target + // Secret resource, encrypted using the password stored in + // `passwordSecretRef`. + // The keystore file will only be updated upon re-issuance. + // A file named `truststore.p12` will also be created in the target + // Secret resource, encrypted using the password stored in + // `passwordSecretRef` containing the issuing Certificate Authority + Create bool `json:"create"` + + // PasswordSecretRef is a reference to a key in a Secret resource + // containing the password used to encrypt the PKCS12 keystore. + PasswordSecretRef acmmeta.SecretKeySelector `json:"passwordSecretRef"` +} // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { @@ -123,6 +170,11 @@ // set of annotations cert-manager sets on the Certificate's Secret. // +optional SecretTemplate *CertificateSecretTemplate `json:"secretTemplate,omitempty"` + + // Keystores configures additional keystore output formats stored in the + // `secretName` Secret resource. + // +optional + Keystores *CertificateKeystores `json:"keystores,omitempty"` } // CertificatePrivateKey contains configuration options for private keys diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 6001ee4..4448bce 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -98,6 +98,31 @@ } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateKeystores) DeepCopyInto(out *CertificateKeystores) { + *out = *in + if in.JKS != nil { + in, out := &in.JKS, &out.JKS + *out = new(JKSKeystore) + **out = **in + } + if in.PKCS12 != nil { + in, out := &in.PKCS12, &out.PKCS12 + *out = new(PKCS12Keystore) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateKeystores. +func (in *CertificateKeystores) DeepCopy() *CertificateKeystores { + if in == nil { + return nil + } + out := new(CertificateKeystores) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CertificateList) DeepCopyInto(out *CertificateList) { *out = *in out.TypeMeta = in.TypeMeta @@ -377,6 +402,11 @@ *out = new(CertificateSecretTemplate) (*in).DeepCopyInto(*out) } + if in.Keystores != nil { + in, out := &in.Keystores, &out.Keystores + *out = new(CertificateKeystores) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. @@ -643,6 +673,38 @@ } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JKSKeystore) DeepCopyInto(out *JKSKeystore) { + *out = *in + out.PasswordSecretRef = in.PasswordSecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JKSKeystore. +func (in *JKSKeystore) DeepCopy() *JKSKeystore { + if in == nil { + return nil + } + out := new(JKSKeystore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PKCS12Keystore) DeepCopyInto(out *PKCS12Keystore) { + *out = *in + out.PasswordSecretRef = in.PasswordSecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PKCS12Keystore. +func (in *PKCS12Keystore) DeepCopy() *PKCS12Keystore { + if in == nil { + return nil + } + out := new(PKCS12Keystore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SelfSignedIssuer) DeepCopyInto(out *SelfSignedIssuer) { *out = *in if in.CRLDistributionPoints != nil { diff --git a/pkg/controller/certificates/certificate_controller.go b/pkg/controller/certificates/certificate_controller.go index 91d3e9a..8856d49 100644 --- a/pkg/controller/certificates/certificate_controller.go +++ b/pkg/controller/certificates/certificate_controller.go @@ -14,7 +14,7 @@ limitations under the License. */ -package controllers +package certificates import ( "context" diff --git a/pkg/controller/certificates/issuing/internal/keystore.go b/pkg/controller/certificates/issuing/internal/keystore.go new file mode 100644 index 0000000..5a51539 --- /dev/null +++ b/pkg/controller/certificates/issuing/internal/keystore.go @@ -0,0 +1,145 @@ +package internal + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "time" + + utilpki "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + jks "github.com/pavlo-v-chernykh/keystore-go/v4" + "software.sslmate.com/src/go-pkcs12" +) + +const ( + // pkcs12SecretKey is the name of the data entry in the Secret resource + // used to store the p12 file. + pkcs12SecretKey = "keystore.p12" + // Data Entry Name in the Secret resource for PKCS12 containing Certificate Authority + pkcs12TruststoreKey = "truststore.p12" + + // jksSecretKey is the name of the data entry in the Secret resource + // used to store the jks file. + jksSecretKey = "keystore.jks" + // Data Entry Name in the Secret resource for JKS containing Certificate Authority + jksTruststoreKey = "truststore.jks" +) + +// 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 := utilpki.DecodePrivateKeyBytes(rawKey) + if err != nil { + return nil, err + } + certs, err := utilpki.DecodeX509CertificateChainBytes(certPem) + if err != nil { + return nil, err + } + var cas []*x509.Certificate + if len(caPem) > 0 { + cas, err = utilpki.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...) + } + return pkcs12.Encode(rand.Reader, key, certs[0], cas, password) +} + +// encode the ca certificate +func encodePKCS12Truststore(password string, caPem []byte) ([]byte, error) { + ca, err := utilpki.DecodeX509CertificateBytes(caPem) + if err != nil { + return nil, err + } + var cas = []*x509.Certificate{ca} + return pkcs12.EncodeTrustStore(rand.Reader, cas, password) +} + +// encode java key store. Input the PEM data and return the raw data of the DER file being protected by the given password +func encodeJKSKeystore(password []byte, rawKey []byte, certPem []byte, caPem []byte) ([]byte, error) { + ks := jks.New() + // add the private key + if err := setJKSPrivateKey(&ks, password, rawKey, certPem); err != nil { + return nil, err + } + + // add the CA certificate if set + if len(caPem) > 0 { + if err := setJKSTruststore(&ks, caPem); err != nil { + return nil, err + } + } + + buf := &bytes.Buffer{} + if err := ks.Store(buf, password); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func encodeJKSTruststore(password []byte, caPem []byte) ([]byte, error) { + ks := jks.New() + if err := setJKSTruststore(&ks, caPem); err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + if err := ks.Store(buf, password); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func setJKSTruststore(ks *jks.KeyStore, caPem []byte) error { + ca, err := utilpki.DecodeX509CertificateBytes(caPem) + if err != nil { + return err + } + ks.SetTrustedCertificateEntry("ca", jks.TrustedCertificateEntry{ + CreationTime: time.Now(), + Certificate: jks.Certificate{ + Type: "X509", + Content: ca.Raw, + }, + }) + return nil +} + +func setJKSPrivateKey(ks *jks.KeyStore, password []byte, rawKey []byte, certPem []byte) error { + // convert the private key to PKCS8 + key, err := utilpki.DecodePrivateKeyBytes(rawKey) + if err != nil { + return err + } + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return err + } + // convert the certificate chain + chain, err := utilpki.DecodeX509CertificateChainBytes(certPem) + if err != nil { + return err + } + certs := make([]jks.Certificate, len(chain)) + + for i, cert := range chain { + certs[i] = jks.Certificate{Type: "X509", Content: cert.Raw} + } + + ks.SetPrivateKeyEntry("certificate", jks.PrivateKeyEntry{ + CreationTime: time.Now(), + PrivateKey: keyDER, + CertificateChain: certs, + }, password) + + return nil +} diff --git a/pkg/controller/certificates/issuing/internal/secret.go b/pkg/controller/certificates/issuing/internal/secret.go index 2019a68..02d0c21 100644 --- a/pkg/controller/certificates/issuing/internal/secret.go +++ b/pkg/controller/certificates/issuing/internal/secret.go @@ -1,10 +1,22 @@ package internal import ( - coreclient "k8s.io/client-go/kubernetes/typed/core/v1" - corelisters "k8s.io/client-go/listers/core/v1" + "context" + "crypto/x509" + "fmt" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + certs "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + utilpki "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + applycorev1 "k8s.io/client-go/applyconfigurations/core/v1" + applymetav1 "k8s.io/client-go/applyconfigurations/meta/v1" + coreclient "k8s.io/client-go/kubernetes/typed/core/v1" + corelisters "k8s.io/client-go/listers/core/v1" ) var ( @@ -48,214 +60,214 @@ } } -// // UpdateData will ensure the Secret resource contains the given secret data as -// // well as appropriate metadata using an Apply call. -// // If the Secret resource does not exist, it will be created on Apply. -// // UpdateData will also update deprecated annotations if they exist. -// func (s *SecretsManager) UpdateData(ctx context.Context, crt *acmapi.Certificate, data SecretData) error { -// secret, err := s.getCertificateSecret(ctx, crt) -// if err != nil { -// return err -// } +// UpdateData will ensure the Secret resource contains the given secret data as +// well as appropriate metadata using an Apply call. +// If the Secret resource does not exist, it will be created on Apply. +// UpdateData will also update deprecated annotations if they exist. +func (s *SecretsManager) UpdateData(ctx context.Context, crt *acmapi.Certificate, data SecretData) error { + secret, err := s.getCertificateSecret(ctx, crt) + if err != nil { + return err + } -// log := logf.FromContext(ctx).WithName("secrets_manager") -// log = logf.WithResource(log, secret) + log := logf.FromContext(ctx).WithName("secrets_manager") + log = logf.WithResource(log, secret) -// if err := s.setValues(crt, secret, data); err != nil { -// return err -// } + if err := s.setValues(crt, secret, data); err != nil { + return err + } -// // Build Secret apply configuration and options. -// applyOpts := metav1.ApplyOptions{FieldManager: s.fieldManager, Force: true} -// applyCnf := applycorev1.Secret(secret.Name, secret.Namespace). -// WithAnnotations(secret.Annotations).WithLabels(secret.Labels). -// WithData(secret.Data).WithType(secret.Type) + // Build Secret apply configuration and options. + applyOpts := metav1.ApplyOptions{FieldManager: s.fieldManager, Force: true} + applyCnf := applycorev1.Secret(secret.Name, secret.Namespace). + WithAnnotations(secret.Annotations).WithLabels(secret.Labels). + WithData(secret.Data).WithType(secret.Type) -// // If Secret owner reference is enabled, set it on the Secret. This results -// // in a no-op if the Secret already exists and has the owner reference set, -// // and visa-versa. -// if s.enableSecretOwnerReferences { -// ref := *metav1.NewControllerRef(crt, certificateGvk) -// applyCnf = applyCnf.WithOwnerReferences(&applymetav1.OwnerReferenceApplyConfiguration{ -// APIVersion: &ref.APIVersion, Kind: &ref.Kind, -// Name: &ref.Name, UID: &ref.UID, -// Controller: ref.Controller, BlockOwnerDeletion: ref.BlockOwnerDeletion, -// }) -// } + // If Secret owner reference is enabled, set it on the Secret. This results + // in a no-op if the Secret already exists and has the owner reference set, + // and visa-versa. + if s.enableSecretOwnerReferences { + ref := *metav1.NewControllerRef(crt, certificateGvk) + applyCnf = applyCnf.WithOwnerReferences(&applymetav1.OwnerReferenceApplyConfiguration{ + APIVersion: &ref.APIVersion, Kind: &ref.Kind, + Name: &ref.Name, UID: &ref.UID, + Controller: ref.Controller, BlockOwnerDeletion: ref.BlockOwnerDeletion, + }) + } -// log.V(logf.DebugLevel).Info("applying secret") + log.V(logf.DebugLevel).Info("applying secret") -// _, err = s.secretClient.Secrets(secret.Namespace).Apply(ctx, applyCnf, applyOpts) -// if err != nil { -// return fmt.Errorf("failed to apply secret %s/%s: %w", secret.Namespace, secret.Name, err) -// } + _, err = s.secretClient.Secrets(secret.Namespace).Apply(ctx, applyCnf, applyOpts) + if err != nil { + return fmt.Errorf("failed to apply secret %s/%s: %w", secret.Namespace, secret.Name, err) + } -// return nil -// } + return nil +} -// // setValues will update the Secret resource 'secret' with the data contained -// // in the given secretData. -// // It will update labels and annotations on the Secret resource appropriately. -// // The Secret resource 's' must be non-nil, although may be a resource that does -// // not exist in the Kubernetes apiserver yet. -// // setValues will NOT actually update the resource in the apiserver. -// // It will also update depreciated issuer name and kind annotations if they -// // exist. -// func (s *SecretsManager) setValues(crt *acmapi.Certificate, secret *corev1.Secret, data SecretData) error { -// if err := s.setKeystores(crt, secret, data); err != nil { -// return fmt.Errorf("failed to add keystores to Secret: %w", err) -// } +// setValues will update the Secret resource 'secret' with the data contained +// in the given secretData. +// It will update labels and annotations on the Secret resource appropriately. +// The Secret resource 's' must be non-nil, although may be a resource that does +// not exist in the Kubernetes apiserver yet. +// setValues will NOT actually update the resource in the apiserver. +// It will also update depreciated issuer name and kind annotations if they +// exist. +func (s *SecretsManager) setValues(crt *acmapi.Certificate, secret *corev1.Secret, data SecretData) error { + if err := s.setKeystores(crt, secret, data); err != nil { + return fmt.Errorf("failed to add keystores to Secret: %w", err) + } -// // Add additional output formats if feature enabled. -// if utilfeature.DefaultFeatureGate.Enabled(feature.AdditionalCertificateOutputFormats) { -// if err := setAdditionalOutputFormats(crt, secret, data); err != nil { -// return fmt.Errorf("failed to add additional output formats to Secret: %w", err) -// } -// } + // // Add additional output formats if feature enabled. + // if utilfeature.DefaultFeatureGate.Enabled(feature.AdditionalCertificateOutputFormats) { + // if err := setAdditionalOutputFormats(crt, secret, data); err != nil { + // return fmt.Errorf("failed to add additional output formats to Secret: %w", err) + // } + // } -// secret.Data[corev1.TLSPrivateKeyKey] = data.PrivateKey -// secret.Data[corev1.TLSCertKey] = data.Certificate -// if len(data.CA) > 0 { -// secret.Data[acmmeta.TLSCAKey] = data.CA -// } + secret.Data[corev1.TLSPrivateKeyKey] = data.PrivateKey + secret.Data[corev1.TLSCertKey] = data.Certificate + if len(data.CA) > 0 { + secret.Data[acmmeta.TLSCAKey] = data.CA + } -// var certificate *x509.Certificate -// if len(data.Certificate) > 0 { -// var err error -// certificate, err = utilpki.DecodeX509CertificateBytes(data.Certificate) -// // TODO: handle InvalidData here? -// if err != nil { -// return err -// } -// } + var certificate *x509.Certificate + if len(data.Certificate) > 0 { + var err error + certificate, err = utilpki.DecodeX509CertificateBytes(data.Certificate) + // TODO: handle InvalidData here. + if err != nil { + return err + } + } -// secret.Annotations = certificates.AnnotationsForCertificateSecret(crt, certificate) -// if secret.Labels == nil { -// secret.Labels = make(map[string]string) -// } + secret.Annotations = certs.AnnotationsForCertificateSecret(crt, certificate) + if secret.Labels == nil { + secret.Labels = make(map[string]string) + } -// if crt.Spec.SecretTemplate != nil { -// for k, v := range crt.Spec.SecretTemplate.Labels { -// secret.Labels[k] = v -// } -// for k, v := range crt.Spec.SecretTemplate.Annotations { -// secret.Annotations[k] = v -// } -// } + if crt.Spec.SecretTemplate != nil { + for k, v := range crt.Spec.SecretTemplate.Labels { + secret.Labels[k] = v + } + for k, v := range crt.Spec.SecretTemplate.Annotations { + secret.Annotations[k] = v + } + } -// return nil -// } + return nil +} -// // getCertificateSecret will return a secret which is ready for fields to be -// // applied. Only the Secret Type will be persisted from the original Secret. -// func (s *SecretsManager) getCertificateSecret(ctx context.Context, crt *cmapi.Certificate) (*corev1.Secret, error) { -// // Get existing secret if it exists. -// existingSecret, err := s.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) +// getCertificateSecret will return a secret which is ready for fields to be +// applied. Only the Secret Type will be persisted from the original Secret. +func (s *SecretsManager) getCertificateSecret(ctx context.Context, crt *acmapi.Certificate) (*corev1.Secret, error) { + // Get existing secret if it exists. + existingSecret, err := s.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) -// // If secret doesn't exist yet, return an empty secret that should be -// // created. -// if apierrors.IsNotFound(err) { -// return &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: crt.Spec.SecretName, -// Namespace: crt.Namespace, -// }, -// Data: make(map[string][]byte), -// Type: corev1.SecretTypeTLS, -// }, nil -// } + // If secret doesn't exist yet, return an empty secret that should be + // created. + if apierrors.IsNotFound(err) { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: crt.Spec.SecretName, + Namespace: crt.Namespace, + }, + Data: make(map[string][]byte), + Type: corev1.SecretTypeTLS, + }, nil + } -// // Transient error. -// if err != nil { -// return nil, err -// } + // Transient error. + if err != nil { + return nil, err + } -// // Only copy Secret Type to not take ownership of annotations or labels on -// // Apply. -// return &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: crt.Spec.SecretName, -// Namespace: crt.Namespace, -// }, -// Data: make(map[string][]byte), -// // Use the existing Secret's type since this may not be of type -// // `kubernetes.io/tls`, if for example it was created beforehand. Type is -// // immutable, so we must keep it to its original value. -// Type: existingSecret.Type, -// }, nil -// } + // Only copy Secret Type to not take ownership of annotations or labels on + // Apply. + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: crt.Spec.SecretName, + Namespace: crt.Namespace, + }, + Data: make(map[string][]byte), + // Use the existing Secret's type since this may not be of type + // `kubernetes.io/tls`, if for example it was created beforehand. Type is + // immutable, so we must keep it to its original value. + Type: existingSecret.Type, + }, nil +} -// // setKeystores will set extra Secret Data keys according to any Keystores -// // which have been configured. -// func (s *SecretsManager) setKeystores(crt *acmapi.Certificate, secret *corev1.Secret, data SecretData) error { -// // Handle the experimental PKCS12 support -// if crt.Spec.Keystores != nil && crt.Spec.Keystores.PKCS12 != nil && crt.Spec.Keystores.PKCS12.Create { -// ref := crt.Spec.Keystores.PKCS12.PasswordSecretRef -// pwSecret, err := s.secretLister.Secrets(crt.Namespace).Get(ref.Name) -// if err != nil { -// return fmt.Errorf("fetching PKCS12 keystore password from Secret: %v", err) -// } -// if pwSecret.Data == nil || len(pwSecret.Data[ref.Key]) == 0 { -// return fmt.Errorf("PKCS12 keystore password Secret contains no data for key %q", ref.Key) -// } -// pw := pwSecret.Data[ref.Key] -// keystoreData, err := encodePKCS12Keystore(string(pw), data.PrivateKey, data.Certificate, data.CA) -// if err != nil { -// return fmt.Errorf("error encoding PKCS12 bundle: %w", err) -// } -// // always overwrite the keystore entry for now -// secret.Data[pkcs12SecretKey] = keystoreData +// setKeystores will set extra Secret Data keys according to any Keystores +// which have been configured. +func (s *SecretsManager) setKeystores(crt *acmapi.Certificate, secret *corev1.Secret, data SecretData) error { + // Handle the experimental PKCS12 support + if crt.Spec.Keystores != nil && crt.Spec.Keystores.PKCS12 != nil && crt.Spec.Keystores.PKCS12.Create { + ref := crt.Spec.Keystores.PKCS12.PasswordSecretRef + pwSecret, err := s.secretLister.Secrets(crt.Namespace).Get(ref.Name) + if err != nil { + return fmt.Errorf("fetching PKCS12 keystore password from Secret: %v", err) + } + if pwSecret.Data == nil || len(pwSecret.Data[ref.Key]) == 0 { + return fmt.Errorf("PKCS12 keystore password Secret contains no data for key %q", ref.Key) + } + pw := pwSecret.Data[ref.Key] + keystoreData, err := encodePKCS12Keystore(string(pw), data.PrivateKey, data.Certificate, data.CA) + if err != nil { + return fmt.Errorf("error encoding PKCS12 bundle: %w", err) + } + // always overwrite the keystore entry for now + secret.Data[pkcs12SecretKey] = keystoreData -// if len(data.CA) > 0 { -// truststoreData, err := encodePKCS12Truststore(string(pw), data.CA) -// if err != nil { -// return fmt.Errorf("error encoding PKCS12 trust store bundle: %w", err) -// } -// // always overwrite the truststore entry -// secret.Data[pkcs12TruststoreKey] = truststoreData -// } -// } + if len(data.CA) > 0 { + truststoreData, err := encodePKCS12Truststore(string(pw), data.CA) + if err != nil { + return fmt.Errorf("error encoding PKCS12 trust store bundle: %w", err) + } + // always overwrite the truststore entry + secret.Data[pkcs12TruststoreKey] = truststoreData + } + } -// // Handle the experimental JKS support -// if crt.Spec.Keystores != nil && crt.Spec.Keystores.JKS != nil && crt.Spec.Keystores.JKS.Create { -// ref := crt.Spec.Keystores.JKS.PasswordSecretRef -// pwSecret, err := s.secretLister.Secrets(crt.Namespace).Get(ref.Name) -// if err != nil { -// return fmt.Errorf("fetching JKS keystore password from Secret: %v", err) -// } -// if pwSecret.Data == nil || len(pwSecret.Data[ref.Key]) == 0 { -// return fmt.Errorf("JKS keystore password Secret contains no data for key %q", ref.Key) -// } -// pw := pwSecret.Data[ref.Key] -// keystoreData, err := encodeJKSKeystore(pw, data.PrivateKey, data.Certificate, data.CA) -// if err != nil { -// return fmt.Errorf("error encoding JKS bundle: %w", err) -// } -// // always overwrite the keystore entry -// secret.Data[jksSecretKey] = keystoreData + // Handle the experimental JKS support + if crt.Spec.Keystores != nil && crt.Spec.Keystores.JKS != nil && crt.Spec.Keystores.JKS.Create { + ref := crt.Spec.Keystores.JKS.PasswordSecretRef + pwSecret, err := s.secretLister.Secrets(crt.Namespace).Get(ref.Name) + if err != nil { + return fmt.Errorf("fetching JKS keystore password from Secret: %v", err) + } + if pwSecret.Data == nil || len(pwSecret.Data[ref.Key]) == 0 { + return fmt.Errorf("JKS keystore password Secret contains no data for key %q", ref.Key) + } + pw := pwSecret.Data[ref.Key] + keystoreData, err := encodeJKSKeystore(pw, data.PrivateKey, data.Certificate, data.CA) + if err != nil { + return fmt.Errorf("error encoding JKS bundle: %w", err) + } + // always overwrite the keystore entry + secret.Data[jksSecretKey] = keystoreData -// if len(data.CA) > 0 { -// truststoreData, err := encodeJKSTruststore(pw, data.CA) -// if err != nil { -// return fmt.Errorf("error encoding JKS trust store bundle: %w", err) -// } -// // always overwrite the keystore entry -// secret.Data[jksTruststoreKey] = truststoreData -// } -// } + if len(data.CA) > 0 { + truststoreData, err := encodeJKSTruststore(pw, data.CA) + if err != nil { + return fmt.Errorf("error encoding JKS trust store bundle: %w", err) + } + // always overwrite the keystore entry + secret.Data[jksTruststoreKey] = truststoreData + } + } -// return nil -// } + return nil +} // // setAdditionalOutputFormat will set extra Secret Data keys with additional // // output formats according to any OutputFormats which have been configured. // func setAdditionalOutputFormats(crt *acmapi.Certificate, secret *corev1.Secret, data SecretData) error { // for _, format := range crt.Spec.AdditionalOutputFormats { // switch format.Type { -// case cmapi.CertificateOutputFormatDER: +// case acmapi.CertificateOutputFormatDER: // // Store binary format of the private key // secret.Data[cmapi.CertificateOutputFormatDERKey] = certificates.OutputFormatDER(data.PrivateKey) -// case cmapi.CertificateOutputFormatCombinedPEM: +// case acmapi.CertificateOutputFormatCombinedPEM: // // Combine tls.key and tls.crt // secret.Data[cmapi.CertificateOutputFormatCombinedPEMKey] = certificates.OutputFormatCombinedPEM(data.PrivateKey, data.Certificate) // default: diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1ab56dc..8fcfd58 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -5,7 +5,9 @@ "time" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmClient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" acmInformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing/internal" @@ -15,6 +17,7 @@ "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "k8s.io/utils/clock" @@ -28,10 +31,11 @@ // which will then copy the signed certificates and private key to the target // Secret resource. type controller struct { - secretLister corelisters.SecretLister - clock clock.Clock - - client kubernetes.Interface + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + clock clock.Clock + client acmClient.Interface // secretsUpdateData is used by the SecretTemplate controller for // re-reconciling Secrets where the SecretTemplate is not up to date with a @@ -42,6 +46,7 @@ func NewController( log logr.Logger, kubeClient kubernetes.Interface, + client acmClient.Interface, factory informers.SharedInformerFactory, acmFactory acmInformers.SharedInformerFactory, clock clock.Clock, @@ -66,5 +71,26 @@ WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), predicate.ResourceOwnerOf, predicate.ExtractResourceName(predicate.CertificateNextPrivateKeySecretName)), }) - return nil, nil, nil + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + certificateInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + secretsManager := internal.NewSecretsManager( + kubeClient.CoreV1(), secretsInformer.Lister(), + fieldManager, certificateControllerOptions.EnableOwnerRef, + ) + + return &controller{ + + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + clock: clock, + secretsUpdateData: secretsManager.UpdateData, + client: client, + }, queue, mustSync } diff --git a/pkg/controller/certificates/secrets.go b/pkg/controller/certificates/secrets.go new file mode 100644 index 0000000..db1e31a --- /dev/null +++ b/pkg/controller/certificates/secrets.go @@ -0,0 +1,50 @@ +package certificates + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "strings" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + utilpki "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" +) + +// AnnotationsForCertificateSecret returns a map which is set on all +// Certificate Secret's Annotations when issued. These annotations contain +// information about the Issuer and Certificate. +// If the X.509 certificate is not-nil, additional annotations will be added +// relating to its Common Name and Subject Alternative Names. +func AnnotationsForCertificateSecret(crt *acmapi.Certificate, certificate *x509.Certificate) map[string]string { + annotations := make(map[string]string) + + annotations[acmapi.CertificateNameKey] = crt.Name + annotations[acmapi.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name + annotations[acmapi.IssuerKindAnnotationKey] = apiutil.IssuerKind(crt.Spec.IssuerRef) + annotations[acmapi.IssuerGroupAnnotationKey] = crt.Spec.IssuerRef.Group + + // Only add certificate data if certificate is non-nil. + if certificate != nil { + annotations[acmapi.CommonNameAnnotationKey] = certificate.Subject.CommonName + annotations[acmapi.AltNamesAnnotationKey] = strings.Join(certificate.DNSNames, ",") + annotations[acmapi.IPSANAnnotationKey] = strings.Join(utilpki.IPAddressesToString(certificate.IPAddresses), ",") + annotations[acmapi.URISANAnnotationKey] = strings.Join(utilpki.URLsToString(certificate.URIs), ",") + } + + return annotations +} + +// OutputFormatDER returns the byte slice of the private key in DER format. To +// be used for Certificate's Additional Output Format DER. +func OutputFormatDER(privateKey []byte) []byte { + block, _ := pem.Decode(privateKey) + return block.Bytes +} + +// OutputFormatCombinedPEM returns the byte slice of the PEM encoded private +// key and signed certificate chain, concatenated. To be used for Certificate's +// Additional Output Format Combined PEM. +func OutputFormatCombinedPEM(privateKey, certificate []byte) []byte { + return bytes.Join([][]byte{privateKey, certificate}, []byte("\n")) +} diff --git a/pkg/util/errors/errors.go b/pkg/util/errors/errors.go new file mode 100644 index 0000000..8998b18 --- /dev/null +++ b/pkg/util/errors/errors.go @@ -0,0 +1,16 @@ +package errors + +import "fmt" + +type invalidDataError struct{ error } + +func NewInvalidData(str string, obj ...interface{}) error { + return &invalidDataError{error: fmt.Errorf(str, obj...)} +} + +func IsInvalidData(err error) bool { + if _, ok := err.(*invalidDataError); !ok { + return false + } + return true +} diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go new file mode 100644 index 0000000..e29bd42 --- /dev/null +++ b/pkg/util/pki/csr.go @@ -0,0 +1,50 @@ +package pki + +import ( + "errors" + "net" + "net/url" + "strings" +) + +// URLsFromString parses the urls from the string array +func URLsFromString(urlStrs []string) ([]*url.URL, error) { + var urls []*url.URL + var errs []string + for _, urlStr := range urlStrs { + url, err := url.Parse(urlStr) + if err != nil { + errs = append(errs, err.Error()) + continue + } + urls = append(urls, url) + } + + if len(errs) > 0 { + return nil, errors.New(strings.Join(errs, ", ")) + } + return urls, nil +} + +// URLsToString converts the array of *url.URL object to the string array +func URLsToString(urls []*url.URL) []string { + var urlStrs []string + for _, url := range urls { + if urls == nil { + panic("provided url to string is nil") + } + + urlStrs = append(urlStrs, url.String()) + } + + return urlStrs +} + +// IPAddressesToString converts the ip address to the string +func IPAddressesToString(ipAddresses []net.IP) []string { + var ipNames []string + for _, ip := range ipAddresses { + ipNames = append(ipNames, ip.String()) + } + return ipNames +} diff --git a/pkg/util/pki/parse.go b/pkg/util/pki/parse.go new file mode 100644 index 0000000..e8ead01 --- /dev/null +++ b/pkg/util/pki/parse.go @@ -0,0 +1,101 @@ +package pki + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + + errors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" +) + +// DecodeX509CertificateBytes will decode a PEM encoded x509 Certificate. +func DecodeX509CertificateBytes(certBytes []byte) (*x509.Certificate, error) { + certs, err := DecodeX509CertificateChainBytes(certBytes) + if err != nil { + return nil, err + } + return certs[0], nil +} + +// DecodeX509CertificateRequestBytes will decode a PEM encoded x509 Certificate Request. +func DecodeX509CertificateRequestBytes(csrBytes []byte) (*x509.CertificateRequest, error) { + block, _ := pem.Decode(csrBytes) + if block == nil { + return nil, errors.NewInvalidData("error decoding certificate request PEM block") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, err + } + + return csr, nil +} + +func DecodeX509CertificateChainBytes(certBytes []byte) ([]*x509.Certificate, error) { + certs := []*x509.Certificate{} + var block *pem.Block + + for { + block, certBytes = pem.Decode(certBytes) + if block == nil { + break + } + + // parse the tls certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.NewInvalidData("error parsing TLS certificate: %s", err.Error()) + } + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, errors.NewInvalidData("error decoding certificate PEM block") + } + return certs, nil +} + +// DecodePrivateKeyBytes will decode a PEM encoded private key into a crypto.Signer. +// It supports ECDSA and RSA private keys only. All other types will return err. +func DecodePrivateKeyBytes(keyBytes []byte) (crypto.Signer, error) { + block, _ := pem.Decode(keyBytes) + if block == nil { + return nil, errors.NewInvalidData("error decoding private key PEM block") + } + + switch block.Type { + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, errors.NewInvalidData("error parsing pks#8 private key: %s", err.Error()) + } + + signer, ok := key.(crypto.Signer) + if !ok { + return nil, errors.NewInvalidData("error parsing pkcs#8 private key: invalid key type") + } + return signer, nil + case "EC PRIVATE KEY": + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, errors.NewInvalidData("error parsing ecdsa private key: %s", err.Error()) + } + return key, nil + + case "PSA PRIVATE KEY": + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, errors.NewInvalidData("error parsing rsa private key: %s", err.Error()) + } + err = key.Validate() + if err != nil { + return nil, errors.NewInvalidData("rsa private key failed validation: %s", err.Error()) + } + + return key, nil + default: + return nil, errors.NewInvalidData("unknown private key type: %s", block.Type) + } + +}