diff --git a/internal/certificate/generate.go b/internal/certificate/generate.go index c98a6aa..fa0bf95 100644 --- a/internal/certificate/generate.go +++ b/internal/certificate/generate.go @@ -29,6 +29,7 @@ import ( ) func GenerateLeaf( + certParser *pki.CertParser, leafDNSNames []string, leafDuration time.Duration, caCert *x509.Certificate, caPk crypto.PrivateKey, @@ -61,11 +62,12 @@ func GenerateLeaf( } // Sign certificate using CA - cert, err := pki.SignCertificate(template, caCert, pk.Public(), caPk) + cert, err := pki.SignCertificate(certParser, template, caCert, pk.Public(), caPk) return cert, pk, err } func GenerateCA( + certParser *pki.CertParser, caDuration time.Duration, ) (*x509.Certificate, crypto.Signer, error) { pk, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) @@ -94,6 +96,6 @@ func GenerateCA( } // self sign the root CA - cert, err := pki.SignCertificate(template, template, pk.Public(), pk) + cert, err := pki.SignCertificate(certParser, template, template, pk.Public(), pk) return cert, pk, err } diff --git a/internal/pki/cert_parser.go b/internal/pki/cert_parser.go index 0140441..1992a08 100644 --- a/internal/pki/cert_parser.go +++ b/internal/pki/cert_parser.go @@ -17,11 +17,45 @@ limitations under the License. package pki import ( + "bytes" "crypto/x509" "encoding/pem" "fmt" + "slices" ) +type CertParser struct { + parsedCerts []parsedCert +} + +func NewCertParser() *CertParser { + return &CertParser{} +} + +type parsedCert struct { + certBytes []byte + certificate *x509.Certificate + err error +} + +func (cp *CertParser) parseCertificateDER(certBytes []byte) (*x509.Certificate, error) { + i, found := slices.BinarySearchFunc(cp.parsedCerts, certBytes, func(a parsedCert, b []byte) int { + return bytes.Compare(a.certBytes, b) + }) + if found { + parsedCert := cp.parsedCerts[i] + return parsedCert.certificate, parsedCert.err + } + + certificate, err := x509.ParseCertificate(certBytes) + cp.parsedCerts = slices.Insert(cp.parsedCerts, i, parsedCert{ + certBytes: certBytes, + certificate: certificate, + err: err, + }) + return certificate, err +} + // parseCertificatePEM strictly validates a given input PEM bundle to confirm it contains // only valid CERTIFICATE PEM blocks. If successful, returns nil and invokes addCert for // each parsed certificate. Any comments or extra non-whitespace data cause an error. @@ -32,7 +66,7 @@ import ( // If the callback returns false and there remains unread non-whitespace input, // the function returns an error about extra data; if no extra data remains, the // function returns nil. -func parseCertificatePEM(pemData []byte, addCert func(*x509.Certificate) (bool, error)) error { +func (cp *CertParser) parseCertificatePEM(pemData []byte, addCert func(*x509.Certificate) (bool, error)) error { if pemData == nil { return fmt.Errorf("certificate data can't be nil") } @@ -59,7 +93,7 @@ func parseCertificatePEM(pemData []byte, addCert func(*x509.Certificate) (bool, return fmt.Errorf("invalid PEM block in bundle: PEM headers are not permitted") } - certificate, err := x509.ParseCertificate(block.Bytes) + certificate, err := cp.parseCertificateDER(block.Bytes) if err != nil { return fmt.Errorf("invalid PEM block in bundle: failed to parse certificate: %w", err) } diff --git a/internal/pki/cert_pool.go b/internal/pki/cert_pool.go index 06f520f..9d635e8 100644 --- a/internal/pki/cert_pool.go +++ b/internal/pki/cert_pool.go @@ -20,10 +20,12 @@ import ( "bytes" "crypto/sha256" "crypto/x509" + "encoding/base32" "encoding/pem" "fmt" "maps" "slices" + "strings" "time" ) @@ -69,9 +71,9 @@ func (cp *CertPool) addCert(now time.Time, cert *x509.Certificate) error { return nil } -func (cp *CertPool) AddCertificatesFromPEM(pemData []byte) error { +func (cp *CertPool) AddCertificatesFromPEM(parser *CertParser, pemData []byte) error { now := time.Now() - return parseCertificatePEM(pemData, func(cert *x509.Certificate) (bool, error) { + return parser.parseCertificatePEM(pemData, func(cert *x509.Certificate) (bool, error) { if err := cp.addCert(now, cert); err != nil { return false, err } @@ -115,3 +117,21 @@ func (cp *CertPool) Certificates() []*x509.Certificate { } return orderedCertificates } + +func (cp *CertPool) HashString() string { + return HashString(CertificatesHash(cp.Certificates()...)) +} + +func HashString(hash [sha256.Size]byte) string { + return strings.TrimRight(base32.HexEncoding.EncodeToString(hash[:]), "=") +} + +func CertificatesHash(certs ...*x509.Certificate) [sha256.Size]byte { + hash := sha256.New() + for _, cert := range certs { + _, _ = hash.Write(cert.Raw) + } + var certsHash [sha256.Size]byte + _ = hash.Sum(certsHash[:0]) + return certsHash +} diff --git a/internal/pki/csr.go b/internal/pki/csr.go index cd96d07..4c3cd13 100644 --- a/internal/pki/csr.go +++ b/internal/pki/csr.go @@ -34,7 +34,7 @@ import ( // publicKey is the public key of the signee, and signerKey is the private // key of the signer. // It returns a parsed *x509.Certificate on success. -func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey any) (*x509.Certificate, error) { +func SignCertificate(certParser *CertParser, template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey any) (*x509.Certificate, error) { typedSigner, ok := signerKey.(crypto.Signer) if !ok { return nil, fmt.Errorf("didn't get an expected Signer in call to SignCertificate") @@ -77,7 +77,7 @@ func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, p return nil, fmt.Errorf("error creating x509 certificate: %w", err) } - cert, err := x509.ParseCertificate(derBytes) + cert, err := DecodeCertificateFromDER(certParser, derBytes) if err != nil { return nil, fmt.Errorf("error decoding DER certificate bytes: %w", err) } diff --git a/internal/pki/parse.go b/internal/pki/parse.go index e9b0327..f79b855 100644 --- a/internal/pki/parse.go +++ b/internal/pki/parse.go @@ -24,23 +24,28 @@ import ( ) // DecodeCertificateFromPEM will decode a PEM encoded x509 Certificate. -func DecodeCertificateFromPEM(certBytes []byte) (*x509.Certificate, error) { +func DecodeCertificateFromPEM(parser *CertParser, certBytes []byte) (*x509.Certificate, error) { var returnedCert *x509.Certificate - return returnedCert, parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) { + return returnedCert, parser.parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) { returnedCert = cert return false, nil // stop after first cert, will error if there are more }) } // DecodeAllCertificatesFromPEM will decode a concatenated list of PEM encoded x509 Certificates. -func DecodeAllCertificatesFromPEM(certBytes []byte) ([]*x509.Certificate, error) { +func DecodeAllCertificatesFromPEM(parser *CertParser, certBytes []byte) ([]*x509.Certificate, error) { var returnedCerts []*x509.Certificate - return returnedCerts, parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) { + return returnedCerts, parser.parseCertificatePEM(certBytes, func(cert *x509.Certificate) (bool, error) { returnedCerts = append(returnedCerts, cert) return true, nil }) } +// DecodeCertificateFromDER will decode a DER encoded x509 Certificate. +func DecodeCertificateFromDER(parser *CertParser, derBytes []byte) (*x509.Certificate, error) { + return parser.parseCertificateDER(derBytes) +} + // 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) { diff --git a/pkg/authority/ca_secret_controller.go b/pkg/authority/ca_secret_controller.go index 2a129b4..0022420 100644 --- a/pkg/authority/ca_secret_controller.go +++ b/pkg/authority/ca_secret_controller.go @@ -87,12 +87,12 @@ func (r *CASecretReconciler) reconcileSecret(ctx context.Context, secret *corev1 if required, reason := caRequiresRegeneration(secret); required { log.FromContext(ctx).Info("Will regenerate CA", "reason", reason) - caCert, caPk, err = certificate.GenerateCA(r.CAOptions.Duration) + caCert, caPk, err = certificate.GenerateCA(pki.NewCertParser(), r.CAOptions.Duration) if err != nil { return caCert, err } } else { - caCert, err = pki.DecodeCertificateFromPEM(secret.Data[corev1.TLSCertKey]) + caCert, err = pki.DecodeCertificateFromPEM(pki.NewCertParser(), secret.Data[corev1.TLSCertKey]) if err != nil { return caCert, err } @@ -136,7 +136,7 @@ func (r *CASecretReconciler) reconcileSecret(ctx context.Context, secret *corev1 func addCertToCABundle(ctx context.Context, caBundleBytes []byte, caCert *x509.Certificate) []byte { certPool := pki.NewCertPool(pki.WithFilteredExpiredCerts(true)) - if err := certPool.AddCertificatesFromPEM(caBundleBytes); err != nil { + if err := certPool.AddCertificatesFromPEM(pki.NewCertParser(), caBundleBytes); err != nil { log.FromContext(ctx).Error(err, "failed to re-use existing CAs in new set of CAs") } // TODO: handle AddCertificate returning false? I expect this will never happen. diff --git a/pkg/authority/ca_secret_controller_test.go b/pkg/authority/ca_secret_controller_test.go index e93ce57..3a772e0 100644 --- a/pkg/authority/ca_secret_controller_test.go +++ b/pkg/authority/ca_secret_controller_test.go @@ -59,7 +59,7 @@ func Test__caRequiresRegeneration(t *testing.T) { if mod != nil { mod(cert) } - cert, err = pki.SignCertificate(cert, cert, pk.Public(), pk) + cert, err = pki.SignCertificate(pki.NewCertParser(), cert, cert, pk.Public(), pk) assert.NoError(t, err) certBytes, err := pki.EncodeCertificateAsPEM(cert) assert.NoError(t, err) diff --git a/pkg/authority/leaf_cert_controller.go b/pkg/authority/leaf_cert_controller.go index 50c2258..32a57d1 100644 --- a/pkg/authority/leaf_cert_controller.go +++ b/pkg/authority/leaf_cert_controller.go @@ -69,7 +69,7 @@ func (r *LeafCertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } func (r *LeafCertReconciler) generateCertificate(caSecret *corev1.Secret) (cert *x509.Certificate, pk crypto.Signer, err error) { - caCert, err := pki.DecodeCertificateFromPEM(caSecret.Data[corev1.TLSCertKey]) + caCert, err := pki.DecodeCertificateFromPEM(pki.NewCertParser(), caSecret.Data[corev1.TLSCertKey]) if err != nil { return cert, pk, err } @@ -79,6 +79,7 @@ func (r *LeafCertReconciler) generateCertificate(caSecret *corev1.Secret) (cert } cert, pk, err = certificate.GenerateLeaf( + pki.NewCertParser(), r.LeafOptions.DNSNames, r.LeafOptions.Duration, caCert, caPk, diff --git a/test/ca_secret_controller_test.go b/test/ca_secret_controller_test.go index 6a871ee..f96a67e 100644 --- a/test/ca_secret_controller_test.go +++ b/test/ca_secret_controller_test.go @@ -112,7 +112,7 @@ var _ = Describe("CA Secret Controller", Ordered, func() { It("should retain old CA if CA is rotated", func() { assertCASecret(caSecret) - caBundleCerts, err := pki.DecodeAllCertificatesFromPEM(caSecret.Data[api.TLSCABundleKey]) + caBundleCerts, err := pki.DecodeAllCertificatesFromPEM(pki.NewCertParser(), caSecret.Data[api.TLSCABundleKey]) Expect(err).ToNot(HaveOccurred()) Expect(caBundleCerts).To(HaveLen(1)) @@ -136,7 +136,7 @@ var _ = Describe("CA Secret Controller", Ordered, func() { HaveField("Data", HaveKeyWithValue(corev1.TLSCertKey, Equal(certBytes))), ) - caBundleCerts, err = pki.DecodeAllCertificatesFromPEM(caSecret.Data[api.TLSCABundleKey]) + caBundleCerts, err = pki.DecodeAllCertificatesFromPEM(pki.NewCertParser(), caSecret.Data[api.TLSCABundleKey]) Expect(err).ToNot(HaveOccurred()) Expect(caBundleCerts).To(HaveLen(2)) }) diff --git a/test/leaf_cert_controller_test.go b/test/leaf_cert_controller_test.go index cbd1729..d7d8c1d 100644 --- a/test/leaf_cert_controller_test.go +++ b/test/leaf_cert_controller_test.go @@ -61,7 +61,7 @@ var _ = Describe("Leaf Certificate Controller", Ordered, func() { ns.Name = opts.CAOptions.Namespace Expect(k8sClient.Create(ctx, ns)).To(Succeed()) - caCert, caPK, err := certificate.GenerateCA(opts.CAOptions.Duration) + caCert, caPK, err := certificate.GenerateCA(pki.NewCertParser(), opts.CAOptions.Duration) Expect(err).ToNot(HaveOccurred()) caCertBytes, err := pki.EncodeCertificateAsPEM(caCert) Expect(err).ToNot(HaveOccurred()) diff --git a/test/util_test.go b/test/util_test.go index 98ba35b..331a862 100644 --- a/test/util_test.go +++ b/test/util_test.go @@ -42,9 +42,9 @@ func assertCASecret(secret *corev1.Secret) { )), )) - cert, err := pki.DecodeCertificateFromPEM(secret.Data[corev1.TLSCertKey]) + cert, err := pki.DecodeCertificateFromPEM(pki.NewCertParser(), secret.Data[corev1.TLSCertKey]) Expect(err).ToNot(HaveOccurred()) - caBundle, err := pki.DecodeAllCertificatesFromPEM(secret.Data[api.TLSCABundleKey]) + caBundle, err := pki.DecodeAllCertificatesFromPEM(pki.NewCertParser(), secret.Data[api.TLSCABundleKey]) Expect(err).ToNot(HaveOccurred()) Expect(secretPublicKeysDiffer(secret)).To(BeFalse()) @@ -82,7 +82,7 @@ func secretPublicKeysDiffer(secret *corev1.Secret) (bool, error) { if err != nil { return true, fmt.Errorf("secret contains invalid private key data: %w", err) } - x509Cert, err := pki.DecodeCertificateFromPEM(secret.Data[corev1.TLSCertKey]) + x509Cert, err := pki.DecodeCertificateFromPEM(pki.NewCertParser(), secret.Data[corev1.TLSCertKey]) if err != nil { return true, fmt.Errorf("secret contains an invalid certificate: %w", err) }