github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/manager/controlapi/ca_rotation_test.go (about)

     1  package controlapi
     2  
     3  import (
     4  	"context"
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"io/ioutil"
     8  	"os"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/cloudflare/cfssl/helpers"
    13  	"github.com/cloudflare/cfssl/initca"
    14  	"github.com/docker/swarmkit/api"
    15  	"github.com/docker/swarmkit/ca"
    16  	"github.com/docker/swarmkit/ca/testutils"
    17  	"github.com/stretchr/testify/require"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/status"
    20  )
    21  
    22  type rootCARotationTestCase struct {
    23  	rootCA   api.RootCA
    24  	caConfig api.CAConfig
    25  
    26  	// what to expect if the validate and update succeeds - we can't always check that everything matches, for instance if
    27  	// random values for join tokens or cross signed certs, or generated root rotation cert/key,
    28  	// are expected
    29  	expectRootCA                api.RootCA
    30  	expectJoinTokenChange       bool
    31  	expectGeneratedRootRotation bool
    32  	expectGeneratedCross        bool
    33  	description                 string // in case an expectation fails
    34  
    35  	// what error string to expect if the validate fails
    36  	expectErrorString string
    37  }
    38  
    39  var initialLocalRootCA = api.RootCA{
    40  	CACert:     testutils.ECDSA256SHA256Cert,
    41  	CAKey:      testutils.ECDSA256Key,
    42  	CACertHash: "DEADBEEF",
    43  	JoinTokens: api.JoinTokens{
    44  		Worker:  "SWMTKN-1-worker",
    45  		Manager: "SWMTKN-1-manager",
    46  	},
    47  }
    48  var rotationCert, rotationKey = testutils.ECDSACertChain[2], testutils.ECDSACertChainKeys[2]
    49  
    50  func uglifyOnePEM(pemBytes []byte) []byte {
    51  	pemBlock, _ := pem.Decode(pemBytes)
    52  	pemBlock.Headers = map[string]string{
    53  		"this": "should",
    54  		"be":   "removed",
    55  	}
    56  	return append(append([]byte("\n\t   "), pem.EncodeToMemory(pemBlock)...), []byte("   \t")...)
    57  }
    58  
    59  func getSecurityConfig(t *testing.T, localRootCA *ca.RootCA, cluster *api.Cluster) *ca.SecurityConfig {
    60  	tempdir, err := ioutil.TempDir("", "test-validate-CA")
    61  	require.NoError(t, err)
    62  	defer os.RemoveAll(tempdir)
    63  	paths := ca.NewConfigPaths(tempdir)
    64  	secConfig, cancel, err := localRootCA.CreateSecurityConfig(context.Background(), ca.NewKeyReadWriter(paths.Node, nil, nil), ca.CertificateRequestConfig{})
    65  	require.NoError(t, err)
    66  	cancel()
    67  	return secConfig
    68  }
    69  
    70  func TestValidateCAConfigInvalidValues(t *testing.T) {
    71  	t.Parallel()
    72  	localRootCA, err := ca.NewRootCA(initialLocalRootCA.CACert, initialLocalRootCA.CACert, initialLocalRootCA.CAKey,
    73  		ca.DefaultNodeCertExpiration, nil)
    74  	require.NoError(t, err)
    75  
    76  	initialExternalRootCA := initialLocalRootCA
    77  	initialExternalRootCA.CAKey = nil
    78  
    79  	crossSigned, err := localRootCA.CrossSignCACertificate(rotationCert)
    80  	require.NoError(t, err)
    81  
    82  	initExternalRootCAWithRotation := initialExternalRootCA
    83  	initExternalRootCAWithRotation.RootRotation = &api.RootRotation{
    84  		CACert:            rotationCert,
    85  		CAKey:             rotationKey,
    86  		CrossSignedCACert: crossSigned,
    87  	}
    88  
    89  	initWithExternalRootRotation := initialLocalRootCA
    90  	initWithExternalRootRotation.RootRotation = &api.RootRotation{
    91  		CACert:            rotationCert,
    92  		CrossSignedCACert: crossSigned,
    93  	}
    94  
    95  	// set up 2 external CAs that can be contacted for signing
    96  	tempdir, err := ioutil.TempDir("", "test-validate-CA")
    97  	require.NoError(t, err)
    98  	defer os.RemoveAll(tempdir)
    99  
   100  	initExtServer, err := testutils.NewExternalSigningServer(localRootCA, tempdir)
   101  	require.NoError(t, err)
   102  	defer initExtServer.Stop()
   103  
   104  	// we need to accept client certs from the original cert
   105  	rotationRootCA, err := ca.NewRootCA(append(initialLocalRootCA.CACert, rotationCert...), rotationCert, rotationKey,
   106  		ca.DefaultNodeCertExpiration, nil)
   107  	require.NoError(t, err)
   108  	rotateExtServer, err := testutils.NewExternalSigningServer(rotationRootCA, tempdir)
   109  	require.NoError(t, err)
   110  	defer rotateExtServer.Stop()
   111  
   112  	for _, invalid := range []rootCARotationTestCase{
   113  		{
   114  			rootCA: initialLocalRootCA,
   115  			caConfig: api.CAConfig{
   116  				SigningCAKey: initialLocalRootCA.CAKey,
   117  			},
   118  			expectErrorString: "the signing CA cert must also be provided",
   119  		},
   120  		{
   121  			rootCA: initExternalRootCAWithRotation, // even if a root rotation is already in progress, the current CA external URL must be present
   122  			caConfig: api.CAConfig{
   123  				ExternalCAs: []*api.ExternalCA{
   124  					{
   125  						URL:      initExtServer.URL,
   126  						CACert:   initialLocalRootCA.CACert,
   127  						Protocol: 3, // wrong protocol
   128  					},
   129  					{
   130  						URL:    initExtServer.URL,
   131  						CACert: rotationCert, // wrong cert
   132  					},
   133  				},
   134  			},
   135  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the current CA certificate",
   136  		},
   137  		{
   138  			rootCA: initialExternalRootCA,
   139  			caConfig: api.CAConfig{
   140  				SigningCACert: rotationCert, // even if there's a desired cert, the current CA external URL must be present
   141  				ExternalCAs: []*api.ExternalCA{ // right certs, but invalid URLs in several ways
   142  					{
   143  						URL:    rotateExtServer.URL,
   144  						CACert: initialExternalRootCA.CACert,
   145  					},
   146  					{
   147  						URL:    "invalidurl",
   148  						CACert: initialExternalRootCA.CACert,
   149  					},
   150  					{
   151  						URL:    "https://too:many:colons:1:2:3",
   152  						CACert: initialExternalRootCA.CACert,
   153  					},
   154  				},
   155  			},
   156  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the current CA certificate",
   157  		},
   158  		{
   159  			rootCA: initialLocalRootCA,
   160  			caConfig: api.CAConfig{
   161  				SigningCACert: rotationCert,
   162  				ExternalCAs: []*api.ExternalCA{
   163  					{
   164  						URL:      rotateExtServer.URL,
   165  						CACert:   rotationCert,
   166  						Protocol: 3, // wrong protocol
   167  					},
   168  					{
   169  						URL: rotateExtServer.URL,
   170  						// wrong cert because no cert is assumed to be the current root CA cert
   171  					},
   172  				},
   173  			},
   174  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the desired CA certificate",
   175  		},
   176  		{
   177  			rootCA: initialLocalRootCA,
   178  			caConfig: api.CAConfig{
   179  				SigningCACert: rotationCert,
   180  				ExternalCAs: []*api.ExternalCA{ // right certs, but invalid URLs in several ways
   181  					{
   182  						URL:    initExtServer.URL,
   183  						CACert: rotationCert,
   184  					},
   185  					{
   186  						URL:    "invalidurl",
   187  						CACert: rotationCert,
   188  					},
   189  					{
   190  						URL:    "https://too:many:colons:1:2:3",
   191  						CACert: initialExternalRootCA.CACert,
   192  					},
   193  				},
   194  			},
   195  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the desired CA certificate",
   196  		},
   197  		{
   198  			rootCA: initWithExternalRootRotation,
   199  			caConfig: api.CAConfig{ // no forceRotate change, no explicit signing cert change
   200  				ExternalCAs: []*api.ExternalCA{
   201  					{
   202  						URL:      rotateExtServer.URL,
   203  						CACert:   rotationCert,
   204  						Protocol: 3, // wrong protocol
   205  					},
   206  					{
   207  						URL:    rotateExtServer.URL,
   208  						CACert: initialLocalRootCA.CACert, // wrong cert
   209  					},
   210  				},
   211  			},
   212  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the next CA certificate",
   213  		},
   214  		{
   215  			rootCA: initWithExternalRootRotation,
   216  			caConfig: api.CAConfig{ // no forceRotate change, no explicit signing cert change
   217  				ExternalCAs: []*api.ExternalCA{
   218  					{
   219  						URL:    initExtServer.URL,
   220  						CACert: rotationCert,
   221  						// right CA cert, but the server cert is not signed by this CA cert
   222  					},
   223  					{
   224  						URL:    "invalidurl",
   225  						CACert: rotationCert,
   226  						// right CA cert, but invalid URL
   227  					},
   228  				},
   229  			},
   230  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the next CA certificate",
   231  		},
   232  		{
   233  			rootCA:            initialExternalRootCA,
   234  			caConfig:          api.CAConfig{}, // removing the current external CA is not supported
   235  			expectErrorString: "there must be at least one valid, reachable external CA corresponding to the current CA certificate",
   236  		},
   237  		{
   238  			rootCA: initialExternalRootCA,
   239  			caConfig: api.CAConfig{
   240  				SigningCACert: rotationCert,
   241  				ExternalCAs: []*api.ExternalCA{
   242  					{
   243  						URL:    initExtServer.URL,
   244  						CACert: initialLocalRootCA.CACert, // current cert
   245  					},
   246  					{
   247  						URL:    rotateExtServer.URL,
   248  						CACert: rotationCert, //new cert
   249  					},
   250  				},
   251  			},
   252  			expectErrorString: "rotating from one external CA to a different external CA is not supported",
   253  		},
   254  		{
   255  			rootCA: initialExternalRootCA,
   256  			caConfig: api.CAConfig{
   257  				SigningCACert: rotationCert,
   258  				ExternalCAs: []*api.ExternalCA{
   259  					{
   260  						URL: initExtServer.URL,
   261  						// no cert means the current cert
   262  					},
   263  					{
   264  						URL:    rotateExtServer.URL,
   265  						CACert: rotationCert, //new cert
   266  					},
   267  				},
   268  			},
   269  			expectErrorString: "rotating from one external CA to a different external CA is not supported",
   270  		},
   271  		{
   272  			rootCA: initialLocalRootCA,
   273  			caConfig: api.CAConfig{
   274  				SigningCACert: append(rotationCert, initialLocalRootCA.CACert...),
   275  				SigningCAKey:  rotationKey,
   276  			},
   277  			expectErrorString: "cannot contain multiple certificates",
   278  		},
   279  		{
   280  			rootCA: initialLocalRootCA,
   281  			caConfig: api.CAConfig{
   282  				SigningCACert: testutils.ReDateCert(t, rotationCert, rotationCert, rotationKey,
   283  					time.Now().Add(-1*time.Minute), time.Now().Add(364*helpers.OneDay)),
   284  				SigningCAKey: rotationKey,
   285  			},
   286  			expectErrorString: "expires too soon",
   287  		},
   288  		{
   289  			rootCA: initialLocalRootCA,
   290  			caConfig: api.CAConfig{
   291  				SigningCACert: initialLocalRootCA.CACert,
   292  				SigningCAKey:  testutils.ExpiredKey, // same cert but mismatching key
   293  			},
   294  			expectErrorString: "certificate key mismatch",
   295  		},
   296  		{
   297  			// this is just one class of failures caught by NewRootCA, not going to bother testing others, since they are
   298  			// extensively tested in NewRootCA
   299  			rootCA: initialLocalRootCA,
   300  			caConfig: api.CAConfig{
   301  				SigningCACert: testutils.ExpiredCert,
   302  				SigningCAKey:  testutils.ExpiredKey,
   303  			},
   304  			expectErrorString: "expired",
   305  		},
   306  	} {
   307  		cluster := &api.Cluster{
   308  			RootCA: invalid.rootCA,
   309  			Spec: api.ClusterSpec{
   310  				CAConfig: invalid.caConfig,
   311  			},
   312  		}
   313  		secConfig := getSecurityConfig(t, &localRootCA, cluster)
   314  		_, err := validateCAConfig(context.Background(), secConfig, cluster)
   315  		require.Error(t, err, invalid.expectErrorString)
   316  		s, _ := status.FromError(err)
   317  		require.Equal(t, codes.InvalidArgument, s.Code(), invalid.expectErrorString)
   318  		require.Contains(t, s.Message(), invalid.expectErrorString)
   319  	}
   320  }
   321  
   322  func runValidTestCases(t *testing.T, testcases []*rootCARotationTestCase, localRootCA *ca.RootCA) {
   323  	for _, valid := range testcases {
   324  		cluster := &api.Cluster{
   325  			RootCA: *valid.rootCA.Copy(),
   326  			Spec: api.ClusterSpec{
   327  				CAConfig: valid.caConfig,
   328  			},
   329  		}
   330  		secConfig := getSecurityConfig(t, localRootCA, cluster)
   331  		result, err := validateCAConfig(context.Background(), secConfig, cluster)
   332  		require.NoError(t, err, valid.description)
   333  
   334  		// ensure that the cluster was not mutated
   335  		require.Equal(t, valid.rootCA, cluster.RootCA)
   336  
   337  		// Because join tokens are random, we can't predict exactly what it is, so this needs to be manually checked
   338  		if valid.expectJoinTokenChange {
   339  			require.NotEmpty(t, result.JoinTokens, valid.rootCA.JoinTokens, valid.description)
   340  		} else {
   341  			require.Equal(t, result.JoinTokens, valid.rootCA.JoinTokens, valid.description)
   342  		}
   343  		result.JoinTokens = valid.expectRootCA.JoinTokens
   344  
   345  		// If a cross-signed certificates is generated, we cant know what it is ahead of time.  All we can do is check that it's
   346  		// correctly generated.
   347  		if valid.expectGeneratedCross || valid.expectGeneratedRootRotation { // both generate cross signed certs
   348  			require.NotNil(t, result.RootRotation, valid.description)
   349  			require.NotEmpty(t, result.RootRotation.CrossSignedCACert, valid.description)
   350  
   351  			// make sure the cross-signed cert is signed by the current root CA (and not an intermediate, if a root rotation is in progress)
   352  			parsedCross, err := helpers.ParseCertificatePEM(result.RootRotation.CrossSignedCACert) // there should just be one
   353  			require.NoError(t, err)
   354  			_, err = parsedCross.Verify(x509.VerifyOptions{Roots: localRootCA.Pool})
   355  			require.NoError(t, err, valid.description)
   356  
   357  			// if we are expecting generated certs or root rotation, we can expect the expected root CA has a root rotation
   358  			result.RootRotation.CrossSignedCACert = valid.expectRootCA.RootRotation.CrossSignedCACert
   359  		}
   360  
   361  		// If a root rotation cert is generated, we can't assert what the cert and key are.  So if we expect it to be generated,
   362  		// just assert that the value has changed.
   363  		if valid.expectGeneratedRootRotation {
   364  			require.NotNil(t, result.RootRotation, valid.description)
   365  			require.NotEqual(t, valid.rootCA.RootRotation, result.RootRotation, valid.description)
   366  			result.RootRotation = valid.expectRootCA.RootRotation
   367  		}
   368  
   369  		require.Equal(t, result, &valid.expectRootCA, valid.description)
   370  	}
   371  }
   372  
   373  func TestValidateCAConfigValidValues(t *testing.T) {
   374  	t.Parallel()
   375  	localRootCA, err := ca.NewRootCA(testutils.ECDSA256SHA256Cert, testutils.ECDSA256SHA256Cert, testutils.ECDSA256Key,
   376  		ca.DefaultNodeCertExpiration, nil)
   377  	require.NoError(t, err)
   378  
   379  	parsedCert, err := helpers.ParseCertificatePEM(testutils.ECDSA256SHA256Cert)
   380  	require.NoError(t, err)
   381  	parsedKey, err := helpers.ParsePrivateKeyPEM(testutils.ECDSA256Key)
   382  	require.NoError(t, err)
   383  
   384  	initialExternalRootCA := initialLocalRootCA
   385  	initialExternalRootCA.CAKey = nil
   386  
   387  	// set up 2 external CAs that can be contacted for signing
   388  	tempdir, err := ioutil.TempDir("", "test-validate-CA")
   389  	require.NoError(t, err)
   390  	defer os.RemoveAll(tempdir)
   391  
   392  	initExtServer, err := testutils.NewExternalSigningServer(localRootCA, tempdir)
   393  	require.NoError(t, err)
   394  	defer initExtServer.Stop()
   395  	require.NoError(t, initExtServer.EnableCASigning())
   396  
   397  	// we need to accept client certs from the original cert
   398  	rotationRootCA, err := ca.NewRootCA(append(initialLocalRootCA.CACert, rotationCert...), rotationCert, rotationKey,
   399  		ca.DefaultNodeCertExpiration, nil)
   400  	require.NoError(t, err)
   401  	rotateExtServer, err := testutils.NewExternalSigningServer(rotationRootCA, tempdir)
   402  	require.NoError(t, err)
   403  	defer rotateExtServer.Stop()
   404  	require.NoError(t, rotateExtServer.EnableCASigning())
   405  
   406  	getExpectedRootCA := func(hasKey bool) api.RootCA {
   407  		result := initialLocalRootCA
   408  		result.LastForcedRotation = 5
   409  		result.JoinTokens = api.JoinTokens{}
   410  		if !hasKey {
   411  			result.CAKey = nil
   412  		}
   413  		return result
   414  	}
   415  	getRootCAWithRotation := func(base api.RootCA, cert, key, cross []byte) api.RootCA {
   416  		init := base
   417  		init.RootRotation = &api.RootRotation{
   418  			CACert:            cert,
   419  			CAKey:             key,
   420  			CrossSignedCACert: cross,
   421  		}
   422  		return init
   423  	}
   424  
   425  	// no change in the CAConfig spec means no rotation
   426  	runValidTestCases(t, []*rootCARotationTestCase{
   427  		{
   428  			description:  "no specified config changes results no root rotation",
   429  			rootCA:       initialLocalRootCA,
   430  			caConfig:     api.CAConfig{},
   431  			expectRootCA: initialLocalRootCA,
   432  		},
   433  	}, &localRootCA)
   434  
   435  	// These require no rotation, because the cert is exactly the same or there is no change specified.
   436  	testcases := []*rootCARotationTestCase{
   437  		{
   438  			description: "same desired cert and key as current Root CA results in no root rotation",
   439  			rootCA:      initialLocalRootCA,
   440  			caConfig: api.CAConfig{
   441  				SigningCACert: uglifyOnePEM(initialLocalRootCA.CACert),
   442  				SigningCAKey:  initialLocalRootCA.CAKey,
   443  				ForceRotate:   5,
   444  			},
   445  			expectRootCA: getExpectedRootCA(true),
   446  		},
   447  		{
   448  			description: "same desired cert as current Root CA but external->internal (remove external CA is ok) results in no root rotation and no key -> key",
   449  			rootCA:      initialExternalRootCA,
   450  			caConfig: api.CAConfig{
   451  				SigningCACert: uglifyOnePEM(initialLocalRootCA.CACert),
   452  				SigningCAKey:  initialLocalRootCA.CAKey,
   453  				ForceRotate:   5,
   454  			},
   455  			expectRootCA: getExpectedRootCA(true),
   456  		},
   457  		{
   458  			description: "same desired cert as current Root CA but internal->external results in no root rotation and key -> no key",
   459  			rootCA:      initialLocalRootCA,
   460  			caConfig: api.CAConfig{
   461  				SigningCACert: initialLocalRootCA.CACert,
   462  				ExternalCAs: []*api.ExternalCA{
   463  					{
   464  						URL:    initExtServer.URL,
   465  						CACert: uglifyOnePEM(initialLocalRootCA.CACert),
   466  					},
   467  				},
   468  				ForceRotate: 5,
   469  			},
   470  			expectRootCA: getExpectedRootCA(false),
   471  		},
   472  		{
   473  			description: "same desired cert and key as current Root CA but adding an external CA results in no root rotation and no key change",
   474  			rootCA:      initialLocalRootCA,
   475  			caConfig: api.CAConfig{
   476  				SigningCACert: initialLocalRootCA.CACert,
   477  				SigningCAKey:  initialLocalRootCA.CAKey,
   478  				ExternalCAs: []*api.ExternalCA{
   479  					{
   480  						URL:    initExtServer.URL,
   481  						CACert: uglifyOnePEM(initialLocalRootCA.CACert),
   482  					},
   483  				},
   484  				ForceRotate: 5,
   485  			},
   486  			expectRootCA: getExpectedRootCA(true),
   487  		},
   488  	}
   489  	runValidTestCases(t, testcases, &localRootCA)
   490  
   491  	// These are the same test cases as above, but we are testing that it will abort root rotation because
   492  	// the desired cert is the same as the current RootCA cert
   493  	crossSigned, err := localRootCA.CrossSignCACertificate(rotationCert)
   494  	require.NoError(t, err)
   495  	for _, testcase := range testcases {
   496  		testcase.rootCA = getRootCAWithRotation(testcase.rootCA, rotationCert, rotationKey, crossSigned)
   497  	}
   498  	testcases[0].description = "same desired cert and key as current RootCA results in aborting root rotation"
   499  	testcases[1].description = "same desired cert as current Root CA but external->internal (remove external CA is ok) results in aborting root rotation and no key -> key"
   500  	testcases[2].description = "same desired cert, even if internal->external, as current RootCA results in aborting root rotation and key -> no key"
   501  	testcases[3].description = "same desired cert and key as current Root CA but adding an external CA results in aborting root rotation and no key change"
   502  	runValidTestCases(t, testcases, &localRootCA)
   503  
   504  	// These will not change the root rotation because the desired cert is the same as the current to-be-rotated-to cert
   505  	expectedBaseRootCA := getExpectedRootCA(true) // the main root CA expected will always have a signing key
   506  	testcases = []*rootCARotationTestCase{
   507  		{
   508  			description: "same desired cert and key as current root rotation results in no change in root rotation",
   509  			rootCA:      getRootCAWithRotation(initialLocalRootCA, rotationCert, rotationKey, crossSigned),
   510  			caConfig: api.CAConfig{
   511  				SigningCACert: testutils.ECDSACertChain[2],
   512  				SigningCAKey:  testutils.ECDSACertChainKeys[2],
   513  				ForceRotate:   5,
   514  			},
   515  			expectRootCA: getRootCAWithRotation(expectedBaseRootCA, rotationCert, rotationKey, crossSigned),
   516  		},
   517  		{
   518  			description: "same desired cert as current root rotation but external->internal results minor change in root rotation (no key -> key)",
   519  			rootCA:      getRootCAWithRotation(initialLocalRootCA, rotationCert, nil, crossSigned),
   520  			caConfig: api.CAConfig{
   521  				SigningCACert: testutils.ECDSACertChain[2],
   522  				SigningCAKey:  testutils.ECDSACertChainKeys[2],
   523  				ForceRotate:   5,
   524  			},
   525  			expectRootCA: getRootCAWithRotation(expectedBaseRootCA, rotationCert, rotationKey, crossSigned),
   526  		},
   527  		{
   528  			description: "same desired cert as current root rotation but internal->external results minor change in root rotation (key -> no key)",
   529  			rootCA:      getRootCAWithRotation(initialLocalRootCA, rotationCert, rotationKey, crossSigned),
   530  			caConfig: api.CAConfig{
   531  				SigningCACert: testutils.ECDSACertChain[2],
   532  				ForceRotate:   5,
   533  				ExternalCAs: []*api.ExternalCA{
   534  					{
   535  						URL:    rotateExtServer.URL,
   536  						CACert: append(testutils.ECDSACertChain[2], ' '),
   537  					},
   538  				},
   539  			},
   540  			expectRootCA: getRootCAWithRotation(expectedBaseRootCA, rotationCert, nil, crossSigned),
   541  		},
   542  	}
   543  	runValidTestCases(t, testcases, &localRootCA)
   544  
   545  	// 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
   546  	// cert or the current-to-be-rotated cert.
   547  	renewedInitialCert, err := initca.RenewFromSigner(parsedCert, parsedKey)
   548  	require.NoError(t, err)
   549  	parsedRotationCert, err := helpers.ParseCertificatePEM(rotationCert)
   550  	require.NoError(t, err)
   551  	parsedRotationKey, err := helpers.ParsePrivateKeyPEM(rotationKey)
   552  	require.NoError(t, err)
   553  	renewedRotationCert, err := initca.RenewFromSigner(parsedRotationCert, parsedRotationKey)
   554  	require.NoError(t, err)
   555  	differentInitialCert, err := testutils.CreateCertFromSigner("otherRootCN", parsedKey)
   556  	require.NoError(t, err)
   557  	differentRootCA, err := ca.NewRootCA(append(initialLocalRootCA.CACert, differentInitialCert...), differentInitialCert,
   558  		initialLocalRootCA.CAKey, ca.DefaultNodeCertExpiration, nil)
   559  	require.NoError(t, err)
   560  	differentExtServer, err := testutils.NewExternalSigningServer(differentRootCA, tempdir)
   561  	require.NoError(t, err)
   562  	defer differentExtServer.Stop()
   563  	require.NoError(t, differentExtServer.EnableCASigning())
   564  	testcases = []*rootCARotationTestCase{
   565  		{
   566  			description: "desired cert being a renewed current cert and key results in a root rotation because the cert has changed",
   567  			rootCA:      initialLocalRootCA,
   568  			caConfig: api.CAConfig{
   569  				SigningCACert: uglifyOnePEM(renewedInitialCert),
   570  				SigningCAKey:  initialLocalRootCA.CAKey,
   571  				ForceRotate:   5,
   572  			},
   573  			expectRootCA:         getRootCAWithRotation(expectedBaseRootCA, renewedInitialCert, initialLocalRootCA.CAKey, nil),
   574  			expectGeneratedCross: true,
   575  		},
   576  		{
   577  			description: "desired cert being a renewed current cert, external->internal results in a root rotation because the cert has changed",
   578  			rootCA:      initialExternalRootCA,
   579  			caConfig: api.CAConfig{
   580  				SigningCACert: uglifyOnePEM(renewedInitialCert),
   581  				SigningCAKey:  initialLocalRootCA.CAKey,
   582  				ForceRotate:   5,
   583  				ExternalCAs: []*api.ExternalCA{
   584  					{
   585  						URL: initExtServer.URL,
   586  					},
   587  				},
   588  			},
   589  			expectRootCA:         getRootCAWithRotation(getExpectedRootCA(false), renewedInitialCert, initialLocalRootCA.CAKey, nil),
   590  			expectGeneratedCross: true,
   591  		},
   592  		{
   593  			description: "desired cert being a renewed current cert, internal->external results in a root rotation because the cert has changed",
   594  			rootCA:      initialLocalRootCA,
   595  			caConfig: api.CAConfig{
   596  				SigningCACert: append([]byte("\n\n"), renewedInitialCert...),
   597  				ForceRotate:   5,
   598  				ExternalCAs: []*api.ExternalCA{
   599  					{
   600  						URL:    initExtServer.URL,
   601  						CACert: uglifyOnePEM(renewedInitialCert),
   602  					},
   603  				},
   604  			},
   605  			expectRootCA:         getRootCAWithRotation(expectedBaseRootCA, renewedInitialCert, nil, nil),
   606  			expectGeneratedCross: true,
   607  		},
   608  		{
   609  			description: "desired cert being a renewed rotation RootCA cert + rotation key results in replaced root rotation because the cert has changed",
   610  			rootCA:      getRootCAWithRotation(initialLocalRootCA, rotationCert, rotationKey, crossSigned),
   611  			caConfig: api.CAConfig{
   612  				SigningCACert: uglifyOnePEM(renewedRotationCert),
   613  				SigningCAKey:  rotationKey,
   614  				ForceRotate:   5,
   615  			},
   616  			expectRootCA:         getRootCAWithRotation(expectedBaseRootCA, renewedRotationCert, rotationKey, nil),
   617  			expectGeneratedCross: true,
   618  		},
   619  		{
   620  			description: "desired cert being a different rotation rootCA cert results in replaced root rotation (only new external CA required, not old rotation external CA)",
   621  			rootCA:      getRootCAWithRotation(initialLocalRootCA, rotationCert, nil, crossSigned),
   622  			caConfig: api.CAConfig{
   623  				SigningCACert: uglifyOnePEM(differentInitialCert),
   624  				ForceRotate:   5,
   625  				ExternalCAs: []*api.ExternalCA{
   626  					{
   627  						// we need a different external server, because otherwise the external server's cert will fail to validate
   628  						// (not signed by the right cert - note that there's a bug in go 1.7 where this is not needed, because the
   629  						// subject names of cert names aren't checked, but go 1.8 fixes this.)
   630  						URL:    differentExtServer.URL,
   631  						CACert: append([]byte("\n\t"), differentInitialCert...),
   632  					},
   633  				},
   634  			},
   635  			expectRootCA:         getRootCAWithRotation(expectedBaseRootCA, differentInitialCert, nil, nil),
   636  			expectGeneratedCross: true,
   637  		},
   638  	}
   639  	runValidTestCases(t, testcases, &localRootCA)
   640  
   641  	// These require rotation because the cert and key are generated and hence completely different.
   642  	testcases = []*rootCARotationTestCase{
   643  		{
   644  			description:                 "generating cert and key results in root rotation",
   645  			rootCA:                      initialLocalRootCA,
   646  			caConfig:                    api.CAConfig{ForceRotate: 5},
   647  			expectRootCA:                getRootCAWithRotation(getExpectedRootCA(true), nil, nil, nil),
   648  			expectGeneratedRootRotation: true,
   649  		},
   650  		{
   651  			description: "generating cert for external->internal results in root rotation",
   652  			rootCA:      initialExternalRootCA,
   653  			caConfig: api.CAConfig{
   654  				ForceRotate: 5,
   655  				ExternalCAs: []*api.ExternalCA{
   656  					{
   657  						URL:    initExtServer.URL,
   658  						CACert: uglifyOnePEM(initialExternalRootCA.CACert),
   659  					},
   660  				},
   661  			},
   662  			expectRootCA:                getRootCAWithRotation(getExpectedRootCA(false), nil, nil, nil),
   663  			expectGeneratedRootRotation: true,
   664  		},
   665  		{
   666  			description:                 "generating cert and key results in replacing root rotation",
   667  			rootCA:                      getRootCAWithRotation(initialLocalRootCA, rotationCert, rotationKey, crossSigned),
   668  			caConfig:                    api.CAConfig{ForceRotate: 5},
   669  			expectRootCA:                getRootCAWithRotation(getExpectedRootCA(true), nil, nil, nil),
   670  			expectGeneratedRootRotation: true,
   671  		},
   672  		{
   673  			description:                 "generating cert and key results in replacing root rotation; external CAs required by old root rotation are no longer necessary",
   674  			rootCA:                      getRootCAWithRotation(initialLocalRootCA, rotationCert, nil, crossSigned),
   675  			caConfig:                    api.CAConfig{ForceRotate: 5},
   676  			expectRootCA:                getRootCAWithRotation(getExpectedRootCA(true), nil, nil, nil),
   677  			expectGeneratedRootRotation: true,
   678  		},
   679  	}
   680  	runValidTestCases(t, testcases, &localRootCA)
   681  
   682  	// These require no change at all because the force rotate value hasn't changed, and there is no desired cert specified
   683  	testcases = []*rootCARotationTestCase{
   684  		{
   685  			description:  "no desired certificate specified, no force rotation: no change to internal signer root (which has no outstanding rotation)",
   686  			rootCA:       initialLocalRootCA,
   687  			expectRootCA: initialLocalRootCA,
   688  		},
   689  		{
   690  			description: "no desired certificate specified, no force rotation: no change to external CA root (which has no outstanding rotation)",
   691  			rootCA:      initialExternalRootCA,
   692  			caConfig: api.CAConfig{
   693  				ExternalCAs: []*api.ExternalCA{
   694  					{
   695  						URL:    initExtServer.URL,
   696  						CACert: uglifyOnePEM(initialExternalRootCA.CACert),
   697  					},
   698  				},
   699  			},
   700  			expectRootCA: initialExternalRootCA,
   701  		},
   702  	}
   703  	runValidTestCases(t, testcases, &localRootCA)
   704  
   705  	for _, testcase := range testcases {
   706  		testcase.rootCA = getRootCAWithRotation(testcase.rootCA, rotationCert, rotationKey, crossSigned)
   707  		testcase.expectRootCA = testcase.rootCA
   708  	}
   709  	testcases[0].description = "no desired certificate specified, no force rotation: no change to internal signer root or to outstanding rotation"
   710  	testcases[1].description = "no desired certificate specified, no force rotation: no change to external CA root or to outstanding rotation"
   711  	runValidTestCases(t, testcases, &localRootCA)
   712  }