From 3e35acfa5258fc2d06fba8fcc457a916d36118ce Mon Sep 17 00:00:00 2001 From: Wesley Date: Fri, 3 Oct 2025 22:12:21 +0200 Subject: [PATCH] Avoid creating certs that violate Apple requirements for macOS 10.15 (#208) * Prevent creating non-standards compliant certs. Changes generated certificates to have a NotBefore based on either the CA NotBefore or the current time. This prevents creation of certificates that are valid for too long making them return errors on platforms like MacOS. * Add license header and add test cases --- cert/cert.go | 23 ++++++------- cert/validity.go | 39 ++++++++++++++++++++++ cert/validity_test.go | 77 +++++++++++++++++++++++++++++++++++++++++++ factory/cert_utils.go | 17 ++++++---- go.mod | 2 +- go.sum | 4 +-- 6 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 cert/validity.go create mode 100644 cert/validity_test.go diff --git a/cert/cert.go b/cert/cert.go index 35c2bf7..3149051 100644 --- a/cert/cert.go +++ b/cert/cert.go @@ -73,15 +73,15 @@ func NewPrivateKey() (*rsa.PrivateKey, error) { // NewSelfSignedCACert creates a CA certificate func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, error) { - now := time.Now() + notBefore := CalculateNotBefore(nil) tmpl := x509.Certificate{ SerialNumber: new(big.Int).SetInt64(0), Subject: pkix.Name{ CommonName: cfg.CommonName, Organization: cfg.Organization, }, - NotBefore: now.UTC(), - NotAfter: now.Add(duration365d * 10).UTC(), + NotBefore: notBefore, + NotAfter: notBefore.Add(duration365d * 10), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, @@ -125,6 +125,7 @@ func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKe } } + notBefore := CalculateNotBefore(caCert) certTmpl := x509.Certificate{ Subject: pkix.Name{ CommonName: cfg.CommonName, @@ -133,8 +134,8 @@ func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKe DNSNames: cfg.AltNames.DNSNames, IPAddresses: cfg.AltNames.IPs, SerialNumber: serial, - NotBefore: caCert.NotBefore, - NotAfter: time.Now().Add(expiresAt).UTC(), + NotBefore: notBefore, + NotAfter: notBefore.Add(expiresAt), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: cfg.Usages, } @@ -186,8 +187,8 @@ func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS // _-_-.key // Certs/keys not existing in that directory are created. func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, alternateDNS []string, fixtureDirectory string) ([]byte, []byte, error) { - validFrom := time.Now().Add(-time.Hour) // valid an hour earlier to avoid flakes due to clock skew - maxAge := time.Hour * 24 * 365 // one year self-signed certs + notBefore := CalculateNotBefore(nil) + maxAge := time.Hour * 24 * 365 // one year self-signed certs baseName := fmt.Sprintf("%s_%s_%s", host, strings.Join(ipsToStrings(alternateIPs), "-"), strings.Join(alternateDNS, "-")) certFixturePath := filepath.Join(fixtureDirectory, baseName+".crt") @@ -214,8 +215,8 @@ func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, a Subject: pkix.Name{ CommonName: fmt.Sprintf("%s-ca@%d", host, time.Now().Unix()), }, - NotBefore: validFrom, - NotAfter: validFrom.Add(maxAge), + NotBefore: notBefore, + NotAfter: notBefore.Add(maxAge), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, @@ -242,8 +243,8 @@ func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, a Subject: pkix.Name{ CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()), }, - NotBefore: validFrom, - NotAfter: validFrom.Add(maxAge), + NotBefore: notBefore, + NotAfter: notBefore.Add(maxAge), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, diff --git a/cert/validity.go b/cert/validity.go new file mode 100644 index 0000000..eb7b496 --- /dev/null +++ b/cert/validity.go @@ -0,0 +1,39 @@ +/* +Copyright 2014 The Kubernetes Authors. + +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. +*/ + +package cert + +import ( + "crypto/x509" + "time" + + clockutil "k8s.io/utils/clock" +) + +var clock clockutil.PassiveClock = &clockutil.RealClock{} + +// CalculateNotBefore calculates a NotBefore time of 1 hour in the past, or the +// NotBefore time of the optionally provided *x509.Certificate, whichever is greater. +func CalculateNotBefore(ca *x509.Certificate) time.Time { + // Subtract 1 hour for clock skew + now := clock.Now().UTC().Add(-time.Hour) + + // It makes no sense to return a time before the CA itself is valid. + if ca != nil && now.Before(ca.NotBefore) { + return ca.NotBefore + } + return now +} diff --git a/cert/validity_test.go b/cert/validity_test.go new file mode 100644 index 0000000..d2511c5 --- /dev/null +++ b/cert/validity_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2014 The Kubernetes Authors. + +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. +*/ + +package cert + +import ( + "crypto/x509" + "testing" + "time" + + clocktest "k8s.io/utils/clock/testing" +) + +func TestCalculateNotBefore(t *testing.T) { + baseTime := time.Date(2025, 9, 29, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + ca *x509.Certificate + now time.Time + expected time.Time + }{ + { + name: "nil CA returns 1h ago", + ca: nil, + now: baseTime, + expected: baseTime.Add(-time.Hour), + }, + { + name: "CA notBefore before now returns 1h ago", + ca: &x509.Certificate{ + NotBefore: baseTime.Add(-2 * time.Hour), + }, + now: baseTime, + expected: baseTime.Add(-time.Hour), + }, + { + name: "CA notBefore after now returns CA.NotBefore", + ca: &x509.Certificate{ + NotBefore: baseTime.Add(2 * time.Hour), + }, + now: baseTime, + expected: baseTime.Add(2 * time.Hour), + }, + { + name: "CA notBefore equal to now returns now", + ca: &x509.Certificate{ + NotBefore: baseTime, + }, + now: baseTime, + expected: baseTime, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clock = clocktest.NewFakePassiveClock(tt.now) + result := CalculateNotBefore(tt.ca) + if !result.Equal(tt.expected) { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/factory/cert_utils.go b/factory/cert_utils.go index 1f21593..522a6d6 100644 --- a/factory/cert_utils.go +++ b/factory/cert_utils.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/rancher/dynamiclistener/cert" "github.com/sirupsen/logrus" ) @@ -24,13 +25,13 @@ const ( ) func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Certificate, error) { - now := time.Now() + notBefore := cert.CalculateNotBefore(nil) tmpl := x509.Certificate{ BasicConstraintsValid: true, IsCA: true, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - NotAfter: now.Add(time.Hour * 24 * 365 * 10).UTC(), - NotBefore: now.UTC(), + NotBefore: notBefore, + NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10), SerialNumber: new(big.Int).SetInt64(0), Subject: pkix.Name{ CommonName: cn, @@ -55,11 +56,12 @@ func NewSignedClientCert(signer crypto.Signer, caCert *x509.Certificate, caKey c return nil, err } + notBefore := cert.CalculateNotBefore(caCert) parent := x509.Certificate{ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(), - NotBefore: caCert.NotBefore, + NotBefore: notBefore, + NotAfter: notBefore.Add(time.Hour * 24 * 365), SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: cn, @@ -98,13 +100,14 @@ func NewSignedCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto. } } + notBefore := cert.CalculateNotBefore(caCert) parent := x509.Certificate{ DNSNames: domains, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPAddresses: ips, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - NotAfter: time.Now().Add(time.Hour * 24 * time.Duration(expirationDays)).UTC(), - NotBefore: caCert.NotBefore, + NotBefore: notBefore, + NotAfter: notBefore.Add(time.Hour * 24 * time.Duration(expirationDays)), SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: cn, diff --git a/go.mod b/go.mod index e683804..c71a44c 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( k8s.io/api v0.34.1 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 ) require ( @@ -56,7 +57,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect diff --git a/go.sum b/go.sum index f73d7d4..1f4ade1 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=