github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/ca/testutils/cautils.go (about) 1 package testutils 2 3 import ( 4 "context" 5 "crypto" 6 cryptorand "crypto/rand" 7 "crypto/tls" 8 "crypto/x509" 9 "encoding/pem" 10 "io/ioutil" 11 "net" 12 "os" 13 "strings" 14 "testing" 15 "time" 16 17 cfcsr "github.com/cloudflare/cfssl/csr" 18 "github.com/cloudflare/cfssl/helpers" 19 "github.com/cloudflare/cfssl/initca" 20 "github.com/docker/swarmkit/api" 21 "github.com/docker/swarmkit/ca" 22 "github.com/docker/swarmkit/ca/pkcs8" 23 "github.com/docker/swarmkit/connectionbroker" 24 "github.com/docker/swarmkit/identity" 25 "github.com/docker/swarmkit/ioutils" 26 "github.com/docker/swarmkit/log" 27 "github.com/docker/swarmkit/manager/state/store" 28 stateutils "github.com/docker/swarmkit/manager/state/testutils" 29 "github.com/docker/swarmkit/remotes" 30 "github.com/sirupsen/logrus" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 "google.golang.org/grpc" 34 "google.golang.org/grpc/credentials" 35 ) 36 37 // TestCA is a structure that encapsulates everything needed to test a CA Server 38 type TestCA struct { 39 RootCA ca.RootCA 40 ExternalSigningServer *ExternalSigningServer 41 MemoryStore *store.MemoryStore 42 Addr, TempDir, Organization string 43 Paths *ca.SecurityConfigPaths 44 Server *grpc.Server 45 ServingSecurityConfig *ca.SecurityConfig 46 CAServer *ca.Server 47 Context context.Context 48 NodeCAClients []api.NodeCAClient 49 CAClients []api.CAClient 50 Conns []*grpc.ClientConn 51 WorkerToken string 52 ManagerToken string 53 ConnBroker *connectionbroker.Broker 54 KeyReadWriter *ca.KeyReadWriter 55 ctxCancel func() 56 securityConfigCleanups []func() error 57 } 58 59 // Stop cleans up after TestCA 60 func (tc *TestCA) Stop() { 61 tc.ctxCancel() 62 for _, qClose := range tc.securityConfigCleanups { 63 qClose() 64 } 65 os.RemoveAll(tc.TempDir) 66 for _, conn := range tc.Conns { 67 conn.Close() 68 } 69 if tc.ExternalSigningServer != nil { 70 tc.ExternalSigningServer.Stop() 71 } 72 tc.CAServer.Stop() 73 tc.Server.Stop() 74 tc.MemoryStore.Close() 75 } 76 77 // NewNodeConfig returns security config for a new node, given a role 78 func (tc *TestCA) NewNodeConfig(role string) (*ca.SecurityConfig, error) { 79 return tc.NewNodeConfigOrg(role, tc.Organization) 80 } 81 82 // WriteNewNodeConfig returns security config for a new node, given a role 83 // saving the generated key and certificates to disk 84 func (tc *TestCA) WriteNewNodeConfig(role string) (*ca.SecurityConfig, error) { 85 return tc.NewNodeConfigOrg(role, tc.Organization) 86 } 87 88 // NewNodeConfigOrg returns security config for a new node, given a role and an org 89 func (tc *TestCA) NewNodeConfigOrg(role, org string) (*ca.SecurityConfig, error) { 90 withNonSigningRoot := tc.ExternalSigningServer != nil 91 s, qClose, err := genSecurityConfig(tc.MemoryStore, tc.RootCA, tc.KeyReadWriter, role, org, tc.TempDir, withNonSigningRoot) 92 if err != nil { 93 tc.securityConfigCleanups = append(tc.securityConfigCleanups, qClose) 94 } 95 return s, err 96 } 97 98 // External controls whether or not NewTestCA() will create a TestCA server 99 // configured to use an external signer or not. 100 var External bool 101 102 // NewTestCA is a helper method that creates a TestCA and a bunch of default 103 // connections and security configs. 104 func NewTestCA(t *testing.T, krwGenerators ...func(ca.CertPaths) *ca.KeyReadWriter) *TestCA { 105 tempdir, err := ioutil.TempDir("", "swarm-ca-test-") 106 if t != nil { 107 require.NoError(t, err) 108 } 109 110 cert, key, err := CreateRootCertAndKey("swarm-test-CA") 111 if t != nil { 112 require.NoError(t, err) 113 } 114 apiRootCA := api.RootCA{ 115 CACert: cert, 116 CAKey: key, 117 } 118 119 return newTestCA(t, tempdir, apiRootCA, krwGenerators, false) 120 } 121 122 // NewFIPSTestCA is a helper method that creates a mandatory fips TestCA and a bunch of default 123 // connections and security configs. 124 func NewFIPSTestCA(t *testing.T) *TestCA { 125 tempdir, err := ioutil.TempDir("", "swarm-ca-test-") 126 if t != nil { 127 require.NoError(t, err) 128 } 129 130 cert, key, err := CreateRootCertAndKey("swarm-test-CA") 131 if t != nil { 132 require.NoError(t, err) 133 } 134 apiRootCA := api.RootCA{ 135 CACert: cert, 136 CAKey: key, 137 } 138 139 return newTestCA(t, tempdir, apiRootCA, nil, true) 140 } 141 142 // NewTestCAFromAPIRootCA is a helper method that creates a TestCA and a bunch of default 143 // connections and security configs, given a temp directory and an api.RootCA to use for creating 144 // a cluster and for signing. 145 func NewTestCAFromAPIRootCA(t *testing.T, tempBaseDir string, apiRootCA api.RootCA, krwGenerators []func(ca.CertPaths) *ca.KeyReadWriter) *TestCA { 146 return newTestCA(t, tempBaseDir, apiRootCA, krwGenerators, false) 147 } 148 149 func newTestCA(t *testing.T, tempBaseDir string, apiRootCA api.RootCA, krwGenerators []func(ca.CertPaths) *ca.KeyReadWriter, fips bool) *TestCA { 150 s := store.NewMemoryStore(&stateutils.MockProposer{}) 151 152 paths := ca.NewConfigPaths(tempBaseDir) 153 organization := identity.NewID() 154 if fips { 155 organization = "FIPS." + organization 156 } 157 158 var ( 159 externalSigningServer *ExternalSigningServer 160 externalCAs []*api.ExternalCA 161 err error 162 rootCA ca.RootCA 163 ) 164 165 if apiRootCA.RootRotation != nil { 166 rootCA, err = ca.NewRootCA( 167 apiRootCA.CACert, apiRootCA.RootRotation.CACert, apiRootCA.RootRotation.CAKey, ca.DefaultNodeCertExpiration, apiRootCA.RootRotation.CrossSignedCACert) 168 } else { 169 rootCA, err = ca.NewRootCA( 170 apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil) 171 172 } 173 if t != nil { 174 require.NoError(t, err) 175 } 176 177 // Write the root certificate to disk, using decent permissions 178 err = ioutils.AtomicWriteFile(paths.RootCA.Cert, apiRootCA.CACert, 0644) 179 if t != nil { 180 require.NoError(t, err) 181 } 182 183 if External { 184 // Start the CA API server - ensure that the external server doesn't have any intermediates 185 var extRootCA ca.RootCA 186 if apiRootCA.RootRotation != nil { 187 extRootCA, err = ca.NewRootCA( 188 apiRootCA.RootRotation.CACert, apiRootCA.RootRotation.CACert, apiRootCA.RootRotation.CAKey, ca.DefaultNodeCertExpiration, nil) 189 // remove the key from the API root CA so that once the CA server starts up, it won't have a local signer 190 apiRootCA.RootRotation.CAKey = nil 191 } else { 192 extRootCA, err = ca.NewRootCA( 193 apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil) 194 // remove the key from the API root CA so that once the CA server starts up, it won't have a local signer 195 apiRootCA.CAKey = nil 196 } 197 if t != nil { 198 require.NoError(t, err) 199 } 200 201 externalSigningServer, err = NewExternalSigningServer(extRootCA, tempBaseDir) 202 if t != nil { 203 require.NoError(t, err) 204 } 205 206 externalCAs = []*api.ExternalCA{ 207 { 208 Protocol: api.ExternalCA_CAProtocolCFSSL, 209 URL: externalSigningServer.URL, 210 CACert: extRootCA.Certs, 211 }, 212 } 213 } 214 215 krw := ca.NewKeyReadWriter(paths.Node, nil, nil) 216 if len(krwGenerators) > 0 { 217 krw = krwGenerators[0](paths.Node) 218 } 219 220 managerConfig, qClose1, err := genSecurityConfig(s, rootCA, krw, ca.ManagerRole, organization, "", External) 221 if t != nil { 222 assert.NoError(t, err) 223 } 224 225 managerDiffOrgConfig, qClose2, err := genSecurityConfig(s, rootCA, krw, ca.ManagerRole, "swarm-test-org-2", "", External) 226 if t != nil { 227 assert.NoError(t, err) 228 } 229 230 workerConfig, qClose3, err := genSecurityConfig(s, rootCA, krw, ca.WorkerRole, organization, "", External) 231 if t != nil { 232 assert.NoError(t, err) 233 } 234 235 l, err := net.Listen("tcp", "127.0.0.1:0") 236 if t != nil { 237 assert.NoError(t, err) 238 } 239 240 baseOpts := []grpc.DialOption{grpc.WithTimeout(10 * time.Second)} 241 insecureClientOpts := append(baseOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) 242 clientOpts := append(baseOpts, grpc.WithTransportCredentials(workerConfig.ClientTLSCreds)) 243 managerOpts := append(baseOpts, grpc.WithTransportCredentials(managerConfig.ClientTLSCreds)) 244 managerDiffOrgOpts := append(baseOpts, grpc.WithTransportCredentials(managerDiffOrgConfig.ClientTLSCreds)) 245 246 conn1, err := grpc.Dial(l.Addr().String(), insecureClientOpts...) 247 if t != nil { 248 assert.NoError(t, err) 249 } 250 251 conn2, err := grpc.Dial(l.Addr().String(), clientOpts...) 252 if t != nil { 253 assert.NoError(t, err) 254 } 255 256 conn3, err := grpc.Dial(l.Addr().String(), managerOpts...) 257 if t != nil { 258 assert.NoError(t, err) 259 } 260 261 conn4, err := grpc.Dial(l.Addr().String(), managerDiffOrgOpts...) 262 if t != nil { 263 assert.NoError(t, err) 264 } 265 266 serverOpts := []grpc.ServerOption{grpc.Creds(managerConfig.ServerTLSCreds)} 267 grpcServer := grpc.NewServer(serverOpts...) 268 269 clusterObj := createClusterObject(t, s, organization, apiRootCA, &rootCA, externalCAs...) 270 271 caServer := ca.NewServer(s, managerConfig) 272 caServer.SetReconciliationRetryInterval(50 * time.Millisecond) 273 caServer.SetRootReconciliationInterval(50 * time.Millisecond) 274 api.RegisterCAServer(grpcServer, caServer) 275 api.RegisterNodeCAServer(grpcServer, caServer) 276 277 fields := logrus.Fields{"testHasExternalCA": External} 278 if t != nil { 279 fields["testname"] = t.Name() 280 } 281 ctx, ctxCancel := context.WithCancel(log.WithLogger(context.Background(), log.L.WithFields(fields))) 282 283 go grpcServer.Serve(l) 284 go caServer.Run(ctx) 285 286 // Wait for caServer to be ready to serve 287 <-caServer.Ready() 288 remotes := remotes.NewRemotes(api.Peer{Addr: l.Addr().String()}) 289 290 caClients := []api.CAClient{api.NewCAClient(conn1), api.NewCAClient(conn2), api.NewCAClient(conn3)} 291 nodeCAClients := []api.NodeCAClient{api.NewNodeCAClient(conn1), api.NewNodeCAClient(conn2), api.NewNodeCAClient(conn3), api.NewNodeCAClient(conn4)} 292 conns := []*grpc.ClientConn{conn1, conn2, conn3, conn4} 293 294 return &TestCA{ 295 RootCA: rootCA, 296 ExternalSigningServer: externalSigningServer, 297 MemoryStore: s, 298 TempDir: tempBaseDir, 299 Organization: organization, 300 Paths: paths, 301 Context: ctx, 302 CAClients: caClients, 303 NodeCAClients: nodeCAClients, 304 Conns: conns, 305 Addr: l.Addr().String(), 306 Server: grpcServer, 307 ServingSecurityConfig: managerConfig, 308 CAServer: caServer, 309 WorkerToken: clusterObj.RootCA.JoinTokens.Worker, 310 ManagerToken: clusterObj.RootCA.JoinTokens.Manager, 311 ConnBroker: connectionbroker.New(remotes), 312 KeyReadWriter: krw, 313 ctxCancel: ctxCancel, 314 securityConfigCleanups: []func() error{qClose1, qClose2, qClose3}, 315 } 316 } 317 318 func createNode(s *store.MemoryStore, nodeID, role string, csr, cert []byte) error { 319 apiRole, _ := ca.FormatRole(role) 320 321 err := s.Update(func(tx store.Tx) error { 322 node := &api.Node{ 323 ID: nodeID, 324 Certificate: api.Certificate{ 325 CSR: csr, 326 CN: nodeID, 327 Role: apiRole, 328 Status: api.IssuanceStatus{ 329 State: api.IssuanceStateIssued, 330 }, 331 Certificate: cert, 332 }, 333 Spec: api.NodeSpec{ 334 DesiredRole: apiRole, 335 Membership: api.NodeMembershipAccepted, 336 }, 337 Role: apiRole, 338 } 339 340 return store.CreateNode(tx, node) 341 }) 342 343 return err 344 } 345 346 func genSecurityConfig(s *store.MemoryStore, rootCA ca.RootCA, krw *ca.KeyReadWriter, role, org, tmpDir string, nonSigningRoot bool) (*ca.SecurityConfig, func() error, error) { 347 req := &cfcsr.CertificateRequest{ 348 KeyRequest: cfcsr.NewBasicKeyRequest(), 349 } 350 351 csr, key, err := cfcsr.ParseRequest(req) 352 if err != nil { 353 return nil, nil, err 354 } 355 356 key, err = pkcs8.ConvertECPrivateKeyPEM(key) 357 if err != nil { 358 return nil, nil, err 359 } 360 361 // Obtain a signed Certificate 362 nodeID := identity.NewID() 363 364 certChain, err := rootCA.ParseValidateAndSignCSR(csr, nodeID, role, org) 365 if err != nil { 366 return nil, nil, err 367 } 368 369 // If we were instructed to persist the files 370 if tmpDir != "" { 371 paths := ca.NewConfigPaths(tmpDir) 372 if err := ioutil.WriteFile(paths.Node.Cert, certChain, 0644); err != nil { 373 return nil, nil, err 374 } 375 if err := ioutil.WriteFile(paths.Node.Key, key, 0600); err != nil { 376 return nil, nil, err 377 } 378 } 379 380 // Load a valid tls.Certificate from the chain and the key 381 nodeCert, err := tls.X509KeyPair(certChain, key) 382 if err != nil { 383 return nil, nil, err 384 } 385 386 err = createNode(s, nodeID, role, csr, certChain) 387 if err != nil { 388 return nil, nil, err 389 } 390 391 signingCert := rootCA.Certs 392 if len(rootCA.Intermediates) > 0 { 393 signingCert = rootCA.Intermediates 394 } 395 parsedCert, err := helpers.ParseCertificatePEM(signingCert) 396 if err != nil { 397 return nil, nil, err 398 } 399 400 if nonSigningRoot { 401 rootCA = ca.RootCA{ 402 Certs: rootCA.Certs, 403 Digest: rootCA.Digest, 404 Pool: rootCA.Pool, 405 Intermediates: rootCA.Intermediates, 406 } 407 } 408 409 return ca.NewSecurityConfig(&rootCA, krw, &nodeCert, &ca.IssuerInfo{ 410 PublicKey: parsedCert.RawSubjectPublicKeyInfo, 411 Subject: parsedCert.RawSubject, 412 }) 413 } 414 415 func createClusterObject(t *testing.T, s *store.MemoryStore, clusterID string, apiRootCA api.RootCA, caRootCA *ca.RootCA, externalCAs ...*api.ExternalCA) *api.Cluster { 416 fips := strings.HasPrefix(clusterID, "FIPS.") 417 cluster := &api.Cluster{ 418 ID: clusterID, 419 Spec: api.ClusterSpec{ 420 Annotations: api.Annotations{ 421 Name: store.DefaultClusterName, 422 }, 423 CAConfig: api.CAConfig{ 424 ExternalCAs: externalCAs, 425 }, 426 }, 427 RootCA: apiRootCA, 428 FIPS: fips, 429 } 430 if cluster.RootCA.JoinTokens.Worker == "" { 431 cluster.RootCA.JoinTokens.Worker = ca.GenerateJoinToken(caRootCA, fips) 432 } 433 if cluster.RootCA.JoinTokens.Manager == "" { 434 cluster.RootCA.JoinTokens.Manager = ca.GenerateJoinToken(caRootCA, fips) 435 } 436 err := s.Update(func(tx store.Tx) error { 437 store.CreateCluster(tx, cluster) 438 return nil 439 }) 440 if t != nil { 441 assert.NoError(t, err) 442 } 443 return cluster 444 } 445 446 // CreateRootCertAndKey returns a generated certificate and key for a root CA 447 func CreateRootCertAndKey(rootCN string) ([]byte, []byte, error) { 448 // Create a simple CSR for the CA using the default CA validator and policy 449 req := cfcsr.CertificateRequest{ 450 CN: rootCN, 451 KeyRequest: cfcsr.NewBasicKeyRequest(), 452 CA: &cfcsr.CAConfig{Expiry: ca.RootCAExpiration}, 453 } 454 455 // Generate the CA and get the certificate and private key 456 cert, _, key, err := initca.New(&req) 457 if err != nil { 458 return nil, nil, err 459 } 460 461 key, err = pkcs8.ConvertECPrivateKeyPEM(key) 462 if err != nil { 463 return nil, nil, err 464 } 465 466 return cert, key, err 467 } 468 469 // ReDateCert takes an existing cert and changes the not before and not after date, to make it easier 470 // to test expiry 471 func ReDateCert(t *testing.T, cert, signerCert, signerKey []byte, notBefore, notAfter time.Time) []byte { 472 signee, err := helpers.ParseCertificatePEM(cert) 473 require.NoError(t, err) 474 signer, err := helpers.ParseCertificatePEM(signerCert) 475 require.NoError(t, err) 476 key, err := helpers.ParsePrivateKeyPEM(signerKey) 477 require.NoError(t, err) 478 signee.NotBefore = notBefore 479 signee.NotAfter = notAfter 480 481 derBytes, err := x509.CreateCertificate(cryptorand.Reader, signee, signer, signee.PublicKey, key) 482 require.NoError(t, err) 483 return pem.EncodeToMemory(&pem.Block{ 484 Type: "CERTIFICATE", 485 Bytes: derBytes, 486 }) 487 } 488 489 // CreateCertFromSigner creates a Certificate authority for a new Swarm Cluster given an existing key only. 490 func CreateCertFromSigner(rootCN string, priv crypto.Signer) ([]byte, error) { 491 req := cfcsr.CertificateRequest{ 492 CN: rootCN, 493 KeyRequest: &cfcsr.BasicKeyRequest{A: ca.RootKeyAlgo, S: ca.RootKeySize}, 494 CA: &cfcsr.CAConfig{Expiry: ca.RootCAExpiration}, 495 } 496 cert, _, err := initca.NewFromSigner(&req, priv) 497 return cert, err 498 }