Skip to content

Commit 80bd281

Browse files
committed
Fix cryptographic certificates for post go 1.19
Go 1.19 changed the way cryptographic certificates were verified, which broke a certain edge case of root CA rotation. This edge case is now disallowed. Signed-off-by: Drew Erny <[email protected]>
1 parent 7eb5046 commit 80bd281

File tree

5 files changed

+62
-61
lines changed

5 files changed

+62
-61
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# syntax=docker/dockerfile:1
22

3-
ARG GO_VERSION=1.18.9
3+
ARG GO_VERSION=1.21.6
44
ARG PROTOC_VERSION=3.11.4
55
ARG GOLANGCI_LINT_VERSION=v1.50.1
66
ARG DEBIAN_FRONTEND=noninteractive

ca/config_test.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -460,18 +460,19 @@ func TestSecurityConfigUpdateRootCA(t *testing.T) {
460460
// dialer, so that grpc does not attempt to load balance/retry the connection - this way the x509 errors can be
461461
// surfaced.
462462
_, actualErrChan, err := tlsGRPCDial(tc.Context, tc.Addr, secConfig.ClientTLSCreds)
463-
defer close(actualErrChan)
463+
// defer close(actualErrChan)
464464
require.Error(t, err)
465465
err = <-actualErrChan
466466
require.Error(t, err)
467-
require.IsType(t, x509.UnknownAuthorityError{}, err)
467+
require.ErrorAs(t, err, &x509.UnknownAuthorityError{})
468+
// require.IsType(t, x509.UnknownAuthorityError{}, err)
468469

469470
_, actualErrChan, err = tlsGRPCDial(tc.Context, l.Addr().String(), tcConfig.ClientTLSCreds)
470-
defer close(actualErrChan)
471+
// defer close(actualErrChan)
471472
require.Error(t, err)
472473
err = <-actualErrChan
473474
require.Error(t, err)
474-
require.IsType(t, x509.UnknownAuthorityError{}, err)
475+
require.ErrorAs(t, err, &x509.UnknownAuthorityError{})
475476

476477
// update the root CA on the "original security config to support both the old root
477478
// and the "new root" (the testing CA root). Also make sure this root CA has an
@@ -640,7 +641,7 @@ func TestRenewTLSConfigUpdatesRootOnUnknownAuthError(t *testing.T) {
640641
default:
641642
crossSigneds[i], err = cas[i-1].CrossSignCACertificate(certs[i])
642643
require.NoError(t, err)
643-
cas[i], err = ca.NewRootCA(certs[i-1], certs[i], keys[i], ca.DefaultNodeCertExpiration, crossSigneds[i])
644+
cas[i], err = ca.NewRootCA(certs[i-1], crossSigneds[i], keys[i], ca.DefaultNodeCertExpiration, crossSigneds[i])
644645
require.NoError(t, err)
645646
}
646647
}
@@ -652,7 +653,7 @@ func TestRenewTLSConfigUpdatesRootOnUnknownAuthError(t *testing.T) {
652653
CACert: certs[0],
653654
CAKey: keys[0],
654655
RootRotation: &api.RootRotation{
655-
CACert: certs[1],
656+
CACert: crossSigneds[1],
656657
CAKey: keys[1],
657658
CrossSignedCACert: crossSigneds[1],
658659
},

ca/server_test.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,9 @@ type clusterObjToUpdate struct {
429429
externalCertSignedBy []byte
430430
}
431431

432-
// When the SecurityConfig is updated with a new TLS keypair, the server automatically uses that keypair to contact
433-
// the external CA
432+
// TestServerExternalCAGetsTLSKeypairUpdates tests that when the SecurityConfig
433+
// is updated with a new TLS keypair, the server automatically uses that
434+
// keypair to contact the external CA
434435
func TestServerExternalCAGetsTLSKeypairUpdates(t *testing.T) {
435436
t.Parallel()
436437

@@ -473,12 +474,13 @@ func TestServerExternalCAGetsTLSKeypairUpdates(t *testing.T) {
473474
require.NoError(t, testutils.PollFuncWithTimeout(nil, func() error {
474475
externalCA := tc.CAServer.ExternalCA()
475476
// wait for the credentials for the external CA to update
477+
log.G(tc.Context).Warn("making external CA sign request")
476478
if _, err = externalCA.Sign(tc.Context, req); err == nil {
477479
return errors.New("external CA creds haven't updated yet to be invalid")
478480
}
479481
return nil
480482
}, 2*time.Second))
481-
require.Contains(t, errors.Cause(err).Error(), "remote error: tls: bad certificate")
483+
require.Contains(t, errors.Cause(err).Error(), "remote error: tls: expired certificate")
482484
}
483485

484486
func TestCAServerUpdateRootCA(t *testing.T) {

manager/controlapi/ca_rotation.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func hasSigningKey(a interface{}) bool {
3737
// Creates a cross-signed intermediate and new api.RootRotation object.
3838
// This function assumes that the root cert and key and the external CAs have already been validated.
3939
func newRootRotationObject(ctx context.Context, securityConfig *ca.SecurityConfig, apiRootCA *api.RootCA, newCARootCA ca.RootCA, extCAs []*api.ExternalCA, version uint64) (*api.RootCA, error) {
40+
log.G(ctx).Info("calls newRootRotationObject")
4041
var (
4142
rootCert, rootKey, crossSignedCert []byte
4243
newRootHasSigner bool
@@ -53,6 +54,7 @@ func newRootRotationObject(ctx context.Context, securityConfig *ca.SecurityConfi
5354
// a root rotation is already in progress)
5455
switch {
5556
case hasSigningKey(apiRootCA):
57+
log.G(ctx).Info("takes hasSigningKey branch")
5658
var oldRootCA ca.RootCA
5759
oldRootCA, err = ca.NewRootCA(apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil)
5860
if err == nil {
@@ -175,8 +177,14 @@ func getNormalizedExtCAs(caConfig *api.CAConfig, normalizedCurrentRootCACert []b
175177
// object as is
176178
// - we want to generate a new internal CA cert and key (force rotation value has changed), and we return the updated RootCA
177179
// object
178-
// 3. Signing cert and key have been provided: validate that these match (the cert and key match). Otherwise, return an error.
179-
// 4. Return the updated RootCA object according to the following criteria:
180+
// 3. Check if the cert is the same key. We cannot rotate to a cert with the same key. As of go 1.19, the logic for certificate
181+
// trust chain validation changed, and a chain including two certs with the same key will not validate. This case would
182+
// usually occur when reissuing the same cert with a later expiration date. Because of this validation failure, our root
183+
// rotation algorithm fails. While it might be possible to adjust the rotation procedure to accommodate such a cert change,
184+
// it is somewhat of an edge case, and, more importantly, we do not currently possess the cryptographic expertise to safely
185+
// make such a change. So, as a result, this operation is disallowed. The new root cert must have a new key.
186+
// 4. Signing cert and key have been provided: validate that these match (the cert and key match). Otherwise, return an error.
187+
// 5. Return the updated RootCA object according to the following criteria:
180188
// - If the desired cert is the same as the current CA cert then abort any outstanding rotations. The current signing key
181189
// is replaced with the desired signing key (this could lets us switch between external->internal or internal->external
182190
// without an actual CA rotation, which is not needed because any leaf cert issued with one CA cert can be validated using
@@ -289,6 +297,12 @@ func validateCAConfig(ctx context.Context, securityConfig *ca.SecurityConfig, cl
289297
return copied, nil
290298
}
291299

300+
// See step 3 in the doc comment. We cannot upgrade a cert with the same
301+
// key.
302+
if bytes.Equal(newConfig.SigningCAKey, cluster.RootCA.CAKey) {
303+
return nil, status.Errorf(codes.InvalidArgument, "Cannot update to a cert with an identical key")
304+
}
305+
292306
// check if this is the same desired cert as an existing root rotation
293307
if r := cluster.RootCA.RootRotation; r != nil && bytes.Equal(ca.NormalizePEMs(r.CACert), newConfig.SigningCACert) {
294308
copied := cluster.RootCA.Copy()

manager/controlapi/ca_rotation_test.go

+33-49
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/x509"
66
"encoding/pem"
7+
"os"
78
"testing"
89
"time"
910

@@ -12,10 +13,13 @@ import (
1213
"github.com/moby/swarmkit/v2/api"
1314
"github.com/moby/swarmkit/v2/ca"
1415
"github.com/moby/swarmkit/v2/ca/testutils"
16+
"github.com/sirupsen/logrus"
1517
"github.com/stretchr/testify/assert"
1618
"github.com/stretchr/testify/require"
1719
"google.golang.org/grpc/codes"
1820
"google.golang.org/grpc/status"
21+
22+
"github.com/moby/swarmkit/v2/log"
1923
)
2024

2125
type rootCARotationTestCase struct {
@@ -315,15 +319,19 @@ func TestValidateCAConfigInvalidValues(t *testing.T) {
315319
}
316320

317321
func runValidTestCases(t *testing.T, testcases []*rootCARotationTestCase, localRootCA *ca.RootCA) {
322+
logrus.SetLevel(logrus.DebugLevel)
323+
logrus.SetOutput(os.Stdout)
324+
ctx := log.WithLogger(context.Background(), log.L.WithField("testname", t.Name()))
318325
for _, valid := range testcases {
326+
casectx := log.WithField(ctx, "testcase", valid.description)
319327
cluster := &api.Cluster{
320328
RootCA: *valid.rootCA.Copy(),
321329
Spec: api.ClusterSpec{
322330
CAConfig: valid.caConfig,
323331
},
324332
}
325333
secConfig := getSecurityConfig(t, localRootCA, cluster)
326-
result, err := validateCAConfig(context.Background(), secConfig, cluster)
334+
result, err := validateCAConfig(casectx, secConfig, cluster)
327335
require.NoError(t, err, valid.description)
328336

329337
// ensure that the cluster was not mutated
@@ -346,8 +354,12 @@ func runValidTestCases(t *testing.T, testcases []*rootCARotationTestCase, localR
346354
// make sure the cross-signed cert is signed by the current root CA (and not an intermediate, if a root rotation is in progress)
347355
parsedCross, err := helpers.ParseCertificatePEM(result.RootRotation.CrossSignedCACert) // there should just be one
348356
require.NoError(t, err)
357+
358+
log.G(casectx).Debugf("localRootCA:%s", localRootCA.Certs)
359+
log.G(casectx).Debugf("CACert:%s", result.RootRotation.CACert)
360+
log.G(casectx).Debugf("CrossSigned:%s", result.RootRotation.CrossSignedCACert)
349361
_, err = parsedCross.Verify(x509.VerifyOptions{Roots: localRootCA.Pool})
350-
require.NoError(t, err, valid.description)
362+
assert.NoError(t, err, valid.description)
351363

352364
// if we are expecting generated certs or root rotation, we can expect the expected root CA has a root rotation
353365
result.RootRotation.CrossSignedCACert = valid.expectRootCA.RootRotation.CrossSignedCACert
@@ -365,14 +377,30 @@ func runValidTestCases(t *testing.T, testcases []*rootCARotationTestCase, localR
365377
}
366378
}
367379

380+
func printCert(t *testing.T, pemData []byte) {
381+
t.Helper()
382+
383+
block, _ := pem.Decode(pemData)
384+
cert, err := x509.ParseCertificate(block.Bytes)
385+
if err != nil {
386+
t.Error(err)
387+
}
388+
389+
cert.RawSubject = nil
390+
cert.Raw = nil
391+
cert.RawIssuer = nil
392+
cert.RawSubjectPublicKeyInfo = nil
393+
cert.RawTBSCertificate = nil
394+
cert.Signature = nil
395+
t.Logf("%+v", cert)
396+
}
397+
368398
func TestValidateCAConfigValidValues(t *testing.T) {
369399
t.Parallel()
370400
localRootCA, err := ca.NewRootCA(testutils.ECDSA256SHA256Cert, testutils.ECDSA256SHA256Cert, testutils.ECDSA256Key,
371401
ca.DefaultNodeCertExpiration, nil)
372402
require.NoError(t, err)
373403

374-
parsedCert, err := helpers.ParseCertificatePEM(testutils.ECDSA256SHA256Cert)
375-
require.NoError(t, err)
376404
parsedKey, err := helpers.ParsePrivateKeyPEM(testutils.ECDSA256Key)
377405
require.NoError(t, err)
378406

@@ -536,8 +564,7 @@ func TestValidateCAConfigValidValues(t *testing.T) {
536564

537565
// These all require a new root rotation because the desired cert is different, even if it has the same key and/or subject as the current
538566
// cert or the current-to-be-rotated cert.
539-
renewedInitialCert, err := initca.RenewFromSigner(parsedCert, parsedKey)
540-
require.NoError(t, err)
567+
time.Sleep(5 * time.Second)
541568
parsedRotationCert, err := helpers.ParseCertificatePEM(rotationCert)
542569
require.NoError(t, err)
543570
parsedRotationKey, err := helpers.ParsePrivateKeyPEM(rotationKey)
@@ -554,49 +581,6 @@ func TestValidateCAConfigValidValues(t *testing.T) {
554581
defer differentExtServer.Stop()
555582
require.NoError(t, differentExtServer.EnableCASigning())
556583
testcases = []*rootCARotationTestCase{
557-
{
558-
description: "desired cert being a renewed current cert and key results in a root rotation because the cert has changed",
559-
rootCA: initialLocalRootCA,
560-
caConfig: api.CAConfig{
561-
SigningCACert: uglifyOnePEM(renewedInitialCert),
562-
SigningCAKey: initialLocalRootCA.CAKey,
563-
ForceRotate: 5,
564-
},
565-
expectRootCA: getRootCAWithRotation(expectedBaseRootCA, renewedInitialCert, initialLocalRootCA.CAKey, nil),
566-
expectGeneratedCross: true,
567-
},
568-
{
569-
description: "desired cert being a renewed current cert, external->internal results in a root rotation because the cert has changed",
570-
rootCA: initialExternalRootCA,
571-
caConfig: api.CAConfig{
572-
SigningCACert: uglifyOnePEM(renewedInitialCert),
573-
SigningCAKey: initialLocalRootCA.CAKey,
574-
ForceRotate: 5,
575-
ExternalCAs: []*api.ExternalCA{
576-
{
577-
URL: initExtServer.URL,
578-
},
579-
},
580-
},
581-
expectRootCA: getRootCAWithRotation(getExpectedRootCA(false), renewedInitialCert, initialLocalRootCA.CAKey, nil),
582-
expectGeneratedCross: true,
583-
},
584-
{
585-
description: "desired cert being a renewed current cert, internal->external results in a root rotation because the cert has changed",
586-
rootCA: initialLocalRootCA,
587-
caConfig: api.CAConfig{
588-
SigningCACert: append([]byte("\n\n"), renewedInitialCert...),
589-
ForceRotate: 5,
590-
ExternalCAs: []*api.ExternalCA{
591-
{
592-
URL: initExtServer.URL,
593-
CACert: uglifyOnePEM(renewedInitialCert),
594-
},
595-
},
596-
},
597-
expectRootCA: getRootCAWithRotation(expectedBaseRootCA, renewedInitialCert, nil, nil),
598-
expectGeneratedCross: true,
599-
},
600584
{
601585
description: "desired cert being a renewed rotation RootCA cert + rotation key results in replaced root rotation because the cert has changed",
602586
rootCA: getRootCAWithRotation(initialLocalRootCA, rotationCert, rotationKey, crossSigned),

0 commit comments

Comments
 (0)