github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/initializer/ca/ca.go (about)

     1  /*
     2   * Copyright contributors to the Hyperledger Fabric Operator project
     3   *
     4   * SPDX-License-Identifier: Apache-2.0
     5   *
     6   * Licensed under the Apache License, Version 2.0 (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at:
     9   *
    10   * 	  http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  package initializer
    20  
    21  import (
    22  	"context"
    23  	"database/sql"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"time"
    30  
    31  	_ "github.com/lib/pq"
    32  
    33  	v1 "github.com/IBM-Blockchain/fabric-operator/pkg/apis/ca/v1"
    34  	"github.com/IBM-Blockchain/fabric-operator/pkg/initializer/ca/config"
    35  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    36  	"github.com/IBM-Blockchain/fabric-operator/pkg/util/merge"
    37  	"github.com/IBM-Blockchain/fabric-operator/pkg/util/pointer"
    38  	"github.com/hyperledger/fabric-ca/lib"
    39  	"github.com/pkg/errors"
    40  	"github.com/spf13/viper"
    41  	"sigs.k8s.io/yaml"
    42  )
    43  
    44  //go:generate counterfeiter -o mocks/config.go -fake-name CAConfig . CAConfig
    45  
    46  type CAConfig interface {
    47  	GetServerConfig() *v1.ServerConfig
    48  	ParseCABlock() (map[string][]byte, error)
    49  	ParseDBBlock() (map[string][]byte, error)
    50  	ParseTLSBlock() (map[string][]byte, error)
    51  	ParseOperationsBlock() (map[string][]byte, error)
    52  	ParseIntermediateBlock() (map[string][]byte, error)
    53  	SetServerConfig(*v1.ServerConfig)
    54  	SetMountPaths(config.Type)
    55  	GetHomeDir() string
    56  	SetUpdate(bool)
    57  	UsingPKCS11() bool
    58  }
    59  
    60  type CA struct {
    61  	CN            string
    62  	Config        CAConfig
    63  	Viper         *viper.Viper
    64  	Type          config.Type
    65  	SqliteDir     string
    66  	UsingHSMProxy bool
    67  
    68  	configFile string
    69  }
    70  
    71  func LoadConfigFromFile(file string) (*v1.ServerConfig, error) {
    72  	serverConfig := &v1.ServerConfig{}
    73  	bytes, err := ioutil.ReadFile(filepath.Clean(file))
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	err = yaml.Unmarshal(bytes, serverConfig)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	err = yaml.Unmarshal(bytes, &serverConfig.CAConfig)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return serverConfig, nil
    89  }
    90  
    91  func NewCA(config CAConfig, caType config.Type, sqliteDir string, hsmProxy bool, cn string) *CA {
    92  	return &CA{
    93  		CN:            cn,
    94  		Config:        config,
    95  		Viper:         viper.New(),
    96  		Type:          caType,
    97  		configFile:    fmt.Sprintf("%s/fabric-ca-server-config.yaml", config.GetHomeDir()),
    98  		SqliteDir:     sqliteDir,
    99  		UsingHSMProxy: hsmProxy,
   100  	}
   101  }
   102  
   103  func (ca *CA) OverrideServerConfig(newConfig *v1.ServerConfig) (err error) {
   104  	serverConfig := ca.Config.GetServerConfig()
   105  
   106  	log.Info("Overriding config values from ca initializer")
   107  	// If newConfig isn't passed, we want to make sure serverConfig.CAConfig.CSR.Cn is set
   108  	// to ca.CN by default; if newConfig is passed for an intermediate CA, the logic below
   109  	// will handle setting CN to blank if ParentServer.URL is set
   110  	serverConfig.CAConfig.CSR.CN = ca.CN
   111  
   112  	if newConfig != nil {
   113  		log.Info("Overriding config values from spec")
   114  		err = merge.WithOverwrite(ca.Config.GetServerConfig(), newConfig)
   115  		if err != nil {
   116  			return errors.Wrapf(err, "failed to merge override configuration")
   117  		}
   118  
   119  		if ca.Config.UsingPKCS11() {
   120  			ca.SetPKCS11Defaults(serverConfig)
   121  		}
   122  
   123  		// Passing in CN when enrolling an intermediate CA will cause the fabric-ca
   124  		// server to error out, a CN cannot be passed for intermediate CA. Setting
   125  		// CN to blank if ParentServer.URL is set
   126  		if serverConfig.CAConfig.Intermediate.ParentServer.URL != "" {
   127  			serverConfig.CAConfig.CSR.CN = ""
   128  		}
   129  	}
   130  
   131  	ca.setDefaults(serverConfig)
   132  
   133  	return nil
   134  }
   135  
   136  func (ca *CA) WriteConfig() (err error) {
   137  	dir := ca.Config.GetHomeDir()
   138  	log.Info(fmt.Sprintf("Writing config to file: '%s'", dir))
   139  
   140  	bytes, err := ca.ConfigToBytes()
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	err = util.EnsureDir(dir)
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	err = ioutil.WriteFile(filepath.Clean(ca.configFile), bytes, 0600)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func (ca *CA) Init() (err error) {
   159  	if ca.Config.UsingPKCS11() && ca.UsingHSMProxy {
   160  		env := os.Getenv("PKCS11_PROXY_SOCKET")
   161  		if env == "" {
   162  			return errors.New("ca configured to use PKCS11, but no PKCS11 proxy endpoint set")
   163  		}
   164  		if !util.IsTCPReachable(env) {
   165  			return errors.New(fmt.Sprintf("Unable to reach PKCS11 proxy: %s", env))
   166  		}
   167  	}
   168  
   169  	cfg, err := ca.ViperUnmarshal(ca.configFile)
   170  	if err != nil {
   171  		return errors.Wrap(err, "viper unmarshal failed")
   172  	}
   173  
   174  	dir := filepath.Dir(ca.configFile)
   175  	// TODO check if this is required!!
   176  	cfg.Metrics.Provider = "disabled"
   177  
   178  	if cfg.CAcfg.DB.Type == "postgres" {
   179  		if !ca.IsPostgresReachable(cfg.CAcfg.DB) {
   180  			return errors.New("Cannot initialize CA. Postgres is not reachable")
   181  		}
   182  	}
   183  
   184  	parentURL := cfg.CAcfg.Intermediate.ParentServer.URL
   185  	if parentURL != "" {
   186  		log.Info(fmt.Sprintf("Request received to enroll with parent server: %s", parentURL))
   187  
   188  		err = ca.HealthCheck(parentURL, cfg.CAcfg.Intermediate.TLS.CertFiles[0])
   189  		if err != nil {
   190  			return errors.Wrap(err, "could not connect to parent CA")
   191  		}
   192  	}
   193  
   194  	caserver := &lib.Server{
   195  		HomeDir: dir,
   196  		Config:  cfg,
   197  		CA: lib.CA{
   198  			Config: &cfg.CAcfg,
   199  		},
   200  	}
   201  
   202  	err = caserver.Init(false)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	serverConfig := ca.Config.GetServerConfig()
   207  	serverConfig.CA.Certfile = caserver.CA.Config.CA.Certfile
   208  	serverConfig.CA.Keyfile = caserver.CA.Config.CA.Keyfile
   209  	serverConfig.CA.Chainfile = caserver.CA.Config.CA.Chainfile
   210  
   211  	if ca.Type.Is(config.EnrollmentCA) {
   212  		serverConfig.CAfiles = []string{"/data/tlsca/fabric-ca-server-config.yaml"}
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  func (ca *CA) IsPostgresReachable(db lib.CAConfigDB) bool {
   219  
   220  	datasource := db.Datasource
   221  	if db.TLS.CertFiles != nil && len(db.TLS.CertFiles) > 0 {
   222  		// The first cert because that is what hyperledger/fabric-ca uses
   223  		datasource = fmt.Sprintf("%s sslrootcert=%s", datasource, db.TLS.CertFiles[0])
   224  	}
   225  
   226  	if db.TLS.Client.CertFile != "" {
   227  		datasource = fmt.Sprintf("%s sslcert=%s", datasource, db.TLS.Client.CertFile)
   228  	}
   229  
   230  	if db.TLS.Client.KeyFile != "" {
   231  		datasource = fmt.Sprintf("%s sslkey=%s", datasource, db.TLS.Client.KeyFile)
   232  	}
   233  
   234  	sqldb, err := sql.Open(db.Type, datasource)
   235  	if err != nil {
   236  		return false
   237  	}
   238  	defer sqldb.Close()
   239  
   240  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   241  	defer cancel()
   242  
   243  	err = sqldb.PingContext(ctx)
   244  	if err != nil {
   245  		return false
   246  	}
   247  
   248  	return true
   249  }
   250  
   251  // ViperUnmarshal as this is what fabric-ca uses when it reads it's configuration
   252  // file
   253  func (ca *CA) ViperUnmarshal(configFile string) (*lib.ServerConfig, error) {
   254  	ca.Viper.SetConfigFile(configFile)
   255  	err := ca.Viper.ReadInConfig()
   256  	if err != nil {
   257  		return nil, errors.Wrapf(err, "viper unable to read in config: %s", configFile)
   258  	}
   259  
   260  	config := &lib.ServerConfig{}
   261  	err = ca.Viper.Unmarshal(config)
   262  	if err != nil {
   263  		return nil, errors.Wrap(err, "viper unable to unmarshal into server level config")
   264  	}
   265  
   266  	err = ca.Viper.Unmarshal(&config.CAcfg)
   267  	if err != nil {
   268  		return nil, errors.Wrap(err, "viper unable to unmarshal into CA level config")
   269  	}
   270  
   271  	return config, nil
   272  }
   273  
   274  func (ca *CA) ParseCrypto() (map[string][]byte, error) {
   275  	switch ca.Type {
   276  	case config.EnrollmentCA:
   277  		return ca.ParseEnrollmentCACrypto()
   278  	case config.TLSCA:
   279  		return ca.ParseTLSCACrypto()
   280  	}
   281  
   282  	return nil, fmt.Errorf("unsupported ca type '%s'", ca.Type)
   283  }
   284  
   285  func (ca *CA) ParseEnrollmentCACrypto() (map[string][]byte, error) {
   286  	serverConfig := ca.Config.GetServerConfig()
   287  	if serverConfig.TLS.IsEnabled() {
   288  		// TLS cert and key file must always be set. Operator should auto generate
   289  		// TLS cert and key if none are provided.
   290  		if serverConfig.TLS.CertFile == "" && serverConfig.TLS.KeyFile == "" {
   291  			return nil, errors.New("no TLS cert and key file provided")
   292  		}
   293  	}
   294  
   295  	if serverConfig.Operations.TLS.IsEnabled() {
   296  		// Same set of TLS certificate that are used for CA endpoint is also used for operations endpoint
   297  		serverConfig.Operations.TLS.CertFile = serverConfig.TLS.CertFile
   298  		serverConfig.Operations.TLS.KeyFile = serverConfig.TLS.KeyFile
   299  	}
   300  
   301  	crypto, err := ca.Config.ParseCABlock()
   302  	if err != nil {
   303  		return nil, errors.Wrap(err, "failed to parse ca block")
   304  	}
   305  
   306  	tlsCrypto, err := ca.Config.ParseTLSBlock()
   307  	if err != nil {
   308  		return nil, errors.Wrap(err, "failed to parse tls block")
   309  	}
   310  	crypto = util.JoinMaps(crypto, tlsCrypto)
   311  
   312  	dbCrypto, err := ca.Config.ParseDBBlock()
   313  	if err != nil {
   314  		return nil, errors.Wrap(err, "failed to parse db block")
   315  	}
   316  	crypto = util.JoinMaps(crypto, dbCrypto)
   317  
   318  	opsCrypto, err := ca.Config.ParseOperationsBlock()
   319  	if err != nil {
   320  		return nil, errors.Wrap(err, "failed to parse operations block")
   321  	}
   322  	crypto = util.JoinMaps(crypto, opsCrypto)
   323  
   324  	intCrypto, err := ca.Config.ParseIntermediateBlock()
   325  	if err != nil {
   326  		return nil, errors.Wrap(err, "failed to parse intermediate block")
   327  	}
   328  	crypto = util.JoinMaps(crypto, intCrypto)
   329  
   330  	return crypto, nil
   331  }
   332  
   333  func (ca *CA) ParseTLSCACrypto() (map[string][]byte, error) {
   334  	crypto, err := ca.ParseCABlock()
   335  	if err != nil {
   336  		return nil, errors.Wrap(err, "failed to parse ca block")
   337  	}
   338  
   339  	tlsCrypto, err := ca.Config.ParseTLSBlock()
   340  	if err != nil {
   341  		return nil, errors.Wrap(err, "failed to parse tls block")
   342  	}
   343  	crypto = util.JoinMaps(crypto, tlsCrypto)
   344  
   345  	dbCrypto, err := ca.Config.ParseDBBlock()
   346  	if err != nil {
   347  		return nil, errors.Wrap(err, "failed to parse db block")
   348  	}
   349  	crypto = util.JoinMaps(crypto, dbCrypto)
   350  
   351  	intCrypto, err := ca.Config.ParseIntermediateBlock()
   352  	if err != nil {
   353  		return nil, errors.Wrap(err, "failed to parse intermediate block")
   354  	}
   355  	crypto = util.JoinMaps(crypto, intCrypto)
   356  
   357  	return crypto, nil
   358  }
   359  
   360  func (ca *CA) ParseCABlock() (map[string][]byte, error) {
   361  	crypto, err := ca.Config.ParseCABlock()
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	return crypto, nil
   367  }
   368  
   369  func (ca *CA) ConfigToBytes() ([]byte, error) {
   370  
   371  	bytes, err := yaml.Marshal(ca.Config.GetServerConfig())
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  
   376  	return bytes, nil
   377  }
   378  
   379  func (ca *CA) SetMountPaths() {
   380  	ca.Config.SetMountPaths(ca.Type)
   381  }
   382  
   383  func (ca *CA) SetPKCS11Defaults(serverConfig *v1.ServerConfig) {
   384  	if serverConfig.CAConfig.CSP.PKCS11 == nil {
   385  		serverConfig.CAConfig.CSP.PKCS11 = &v1.PKCS11Opts{}
   386  	}
   387  
   388  	if ca.UsingHSMProxy {
   389  		serverConfig.CAConfig.CSP.PKCS11.Library = "/usr/local/lib/libpkcs11-proxy.so"
   390  	}
   391  
   392  	serverConfig.CAConfig.CSP.PKCS11.FileKeyStore.KeyStorePath = "msp/keystore"
   393  
   394  	if serverConfig.CAConfig.CSP.PKCS11.HashFamily == "" {
   395  		serverConfig.CAConfig.CSP.PKCS11.HashFamily = "SHA2"
   396  	}
   397  
   398  	if serverConfig.CAConfig.CSP.PKCS11.SecLevel == 0 {
   399  		serverConfig.CAConfig.CSP.PKCS11.SecLevel = 256
   400  	}
   401  }
   402  
   403  func (ca *CA) GetHomeDir() string {
   404  	return ca.Config.GetHomeDir()
   405  }
   406  
   407  func (ca *CA) GetServerConfig() *v1.ServerConfig {
   408  	return ca.Config.GetServerConfig()
   409  }
   410  
   411  func (ca *CA) RemoveHomeDir() error {
   412  	err := os.RemoveAll(ca.GetHomeDir())
   413  	if err != nil {
   414  		return err
   415  	}
   416  	return nil
   417  }
   418  
   419  func (ca *CA) IsBeingUpdated() {
   420  	ca.Config.SetUpdate(true)
   421  }
   422  
   423  func (ca *CA) IsHSMEnabled() bool {
   424  	if ca.Config.UsingPKCS11() {
   425  		return true
   426  	}
   427  	return false
   428  }
   429  
   430  func (ca *CA) HealthCheck(parentURL, certPath string) error {
   431  	parsedURL, err := url.Parse(parentURL)
   432  	if err != nil {
   433  		return errors.Wrapf(err, "invalid CA url")
   434  	}
   435  
   436  	healthURL := getHealthCheckEndpoint(parsedURL)
   437  	log.Info(fmt.Sprintf("Health checking parent server, pinging %s", healthURL))
   438  
   439  	// Make sure that parent server is running before trying to enroll
   440  	// intermediate CA. Retry 5 times for a total of 5 seconds to make
   441  	// sure parent server is up. If parent server is found, bail early
   442  	// and continue with enrollment
   443  	cert, err := ioutil.ReadFile(filepath.Clean(certPath))
   444  	if err != nil {
   445  		return errors.Wrap(err, "failed to read TLS cert for intermediate enrollment")
   446  	}
   447  
   448  	for i := 0; i < 5; i++ {
   449  		err = util.HealthCheck(healthURL, cert, 30*time.Second)
   450  		if err != nil {
   451  			log.Info(fmt.Sprintf("Health check error: %s", err))
   452  			time.Sleep(1 * time.Second)
   453  			log.Info("Health check failed, retrying")
   454  			continue
   455  		}
   456  		log.Info("Health check successfull")
   457  		break
   458  	}
   459  
   460  	return nil
   461  }
   462  
   463  func (ca *CA) GetType() config.Type {
   464  	return ca.Type
   465  }
   466  
   467  func getHealthCheckEndpoint(u *url.URL) string {
   468  	return fmt.Sprintf("%s://%s/cainfo", u.Scheme, u.Host)
   469  }
   470  
   471  func (ca *CA) setDefaults(serverConfig *v1.ServerConfig) {
   472  	serverConfig.CAConfig.Cfg.Identities.AllowRemove = pointer.True()
   473  	serverConfig.CAConfig.Cfg.Affiliations.AllowRemove = pointer.True()
   474  	// Ignore Certificate Expiry for re-enroll
   475  	serverConfig.CA.ReenrollIgnoreCertExpiry = pointer.True()
   476  }