github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/certificate/reenroller/reenroller.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 reenroller
    20  
    21  import (
    22  	"crypto/ecdsa"
    23  	"crypto/x509"
    24  	"encoding/hex"
    25  	"encoding/pem"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"os"
    29  	"path/filepath"
    30  	"time"
    31  
    32  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    33  	commonapi "github.com/IBM-Blockchain/fabric-operator/pkg/apis/common"
    34  	"github.com/IBM-Blockchain/fabric-operator/pkg/initializer/common/config"
    35  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    36  	"github.com/hyperledger/fabric-ca/api"
    37  	"github.com/hyperledger/fabric-ca/lib"
    38  	"github.com/hyperledger/fabric-ca/lib/client/credential"
    39  	fabricx509 "github.com/hyperledger/fabric-ca/lib/client/credential/x509"
    40  	"github.com/hyperledger/fabric-ca/lib/tls"
    41  	"github.com/hyperledger/fabric/bccsp"
    42  	"github.com/hyperledger/fabric/bccsp/utils"
    43  	"github.com/pkg/errors"
    44  	"k8s.io/apimachinery/pkg/util/wait"
    45  
    46  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    47  )
    48  
    49  var log = logf.Log.WithName("reenroller")
    50  
    51  //go:generate counterfeiter -o mocks/identity.go -fake-name Identity . Identity
    52  type Identity interface {
    53  	Reenroll(req *api.ReenrollmentRequest) (*lib.EnrollmentResponse, error)
    54  	GetECert() *fabricx509.Signer
    55  	GetClient() *lib.Client
    56  }
    57  
    58  type Reenroller struct {
    59  	Client   *lib.Client
    60  	Identity Identity
    61  
    62  	HomeDir string
    63  	Config  *current.Enrollment
    64  	BCCSP   bool
    65  	Timeout time.Duration
    66  	NewKey  bool
    67  }
    68  
    69  func New(cfg *current.Enrollment, homeDir string, bccsp *commonapi.BCCSP, timeoutstring string, newKey bool) (*Reenroller, error) {
    70  	if cfg == nil {
    71  		return nil, errors.New("unable to reenroll, Enrollment config must be passed")
    72  	}
    73  
    74  	err := EnrollmentConfigValidation(cfg)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	client := &lib.Client{
    80  		HomeDir: homeDir,
    81  		Config: &lib.ClientConfig{
    82  			TLS: tls.ClientTLSConfig{
    83  				Enabled:   true,
    84  				CertFiles: []string{"tlsCert.pem"},
    85  			},
    86  			URL: fmt.Sprintf("https://%s:%s", cfg.CAHost, cfg.CAPort),
    87  		},
    88  	}
    89  
    90  	client = GetClient(client, bccsp)
    91  
    92  	timeout, err := time.ParseDuration(timeoutstring)
    93  	if err != nil || timeoutstring == "" {
    94  		timeout = time.Duration(60 * time.Second)
    95  	}
    96  
    97  	r := &Reenroller{
    98  		Client:  client,
    99  		HomeDir: homeDir,
   100  		Config:  cfg.DeepCopy(),
   101  		Timeout: timeout,
   102  		NewKey:  newKey,
   103  	}
   104  
   105  	if bccsp != nil {
   106  		r.BCCSP = true
   107  	}
   108  
   109  	return r, nil
   110  }
   111  
   112  func (r *Reenroller) InitClient() error {
   113  	if !r.IsCAReachable() {
   114  		return errors.New("unable to init client for re-enroll, CA is not reachable")
   115  	}
   116  
   117  	tlsCertBytes, err := util.Base64ToBytes(r.Config.CATLS.CACert)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	err = os.MkdirAll(r.HomeDir, 0750)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	err = util.WriteFile(filepath.Join(r.HomeDir, "tlsCert.pem"), tlsCertBytes, 0755)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	err = r.Client.Init()
   132  	if err != nil {
   133  		return errors.Wrap(err, "failed to initialize CA client")
   134  	}
   135  	return nil
   136  }
   137  
   138  func (r *Reenroller) loadHSMIdentity(certPemBytes []byte) error {
   139  	log.Info("Loading HSM based identity...")
   140  
   141  	csp := r.Client.GetCSP()
   142  	certPubK, err := r.Client.GetCSP().KeyImport(certPemBytes, &bccsp.X509PublicKeyImportOpts{Temporary: true})
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	// Get the key given the SKI value
   148  	ski := certPubK.SKI()
   149  	privateKey, err := csp.GetKey(ski)
   150  	if err != nil {
   151  		return errors.WithMessage(err, "could not find matching private key for SKI")
   152  	}
   153  
   154  	// BCCSP returns a public key if the private key for the SKI wasn't found, so
   155  	// we need to return an error in that case.
   156  	if !privateKey.Private() {
   157  		return errors.Errorf("The private key associated with the certificate with SKI '%s' was not found", hex.EncodeToString(ski))
   158  	}
   159  
   160  	signer, err := fabricx509.NewSigner(privateKey, certPemBytes)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	cred := fabricx509.NewCredential("", "", r.Client)
   166  	err = cred.SetVal(signer)
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	r.Identity = lib.NewIdentity(r.Client, r.Config.EnrollID, []credential.Credential{cred})
   172  
   173  	return nil
   174  }
   175  
   176  func (r *Reenroller) loadIdentity(certPemBytes []byte, keyPemBytes []byte) error {
   177  	log.Info("Loading software based identity...")
   178  
   179  	client := r.Client
   180  	enrollmentID := r.Config.EnrollID
   181  
   182  	// NOTE: Utilized code from https://github.com/hyperledger/fabric-ca/blob/v2.0.0-alpha/util/csp.go#L220
   183  	// but modified to use pem bytes instead of file since we store the key in a secret, not in filesystem
   184  	var bccspKey bccsp.Key
   185  	temporary := true
   186  	key, err := utils.PEMtoPrivateKey(keyPemBytes, nil)
   187  	if err != nil {
   188  		return errors.Wrap(err, "failed to get private key from pem bytes")
   189  	}
   190  	switch key.(type) {
   191  	case *ecdsa.PrivateKey:
   192  		priv, err := utils.PrivateKeyToDER(key.(*ecdsa.PrivateKey))
   193  		if err != nil {
   194  			return errors.Wrap(err, "failed to marshal ECDSA private key to der")
   195  		}
   196  		bccspKey, err = client.GetCSP().KeyImport(priv, &bccsp.ECDSAPrivateKeyImportOpts{Temporary: temporary})
   197  		if err != nil {
   198  			return errors.Wrap(err, "failed to import ECDSA private key")
   199  		}
   200  	default:
   201  		return errors.New("failed to import key, invalid secret key type")
   202  	}
   203  
   204  	signer, err := fabricx509.NewSigner(bccspKey, certPemBytes)
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	cred := fabricx509.NewCredential("", "", client)
   210  	err = cred.SetVal(signer)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	r.Identity = lib.NewIdentity(client, enrollmentID, []credential.Credential{cred})
   216  
   217  	return nil
   218  }
   219  
   220  func (r *Reenroller) LoadIdentity(certPemBytes []byte, keyPemBytes []byte, hsmEnabled bool) error {
   221  	if hsmEnabled {
   222  		err := r.loadHSMIdentity(certPemBytes)
   223  		if err != nil {
   224  			return errors.Wrap(err, "failed to load HSM based identity")
   225  		}
   226  
   227  		return nil
   228  	}
   229  
   230  	err := r.loadIdentity(certPemBytes, keyPemBytes)
   231  	if err != nil {
   232  		return errors.Wrap(err, "failed to load identity")
   233  	}
   234  
   235  	return nil
   236  }
   237  
   238  func (r *Reenroller) IsCAReachable() bool {
   239  	timeout := r.Timeout
   240  	url := fmt.Sprintf("https://%s:%s/cainfo", r.Config.CAHost, r.Config.CAPort)
   241  
   242  	// Convert TLS certificate from base64 to file
   243  	tlsCertBytes, err := util.Base64ToBytes(r.Config.CATLS.CACert)
   244  	if err != nil {
   245  		log.Error(err, "Cannot convert TLS Certificate from base64")
   246  		return false
   247  	}
   248  
   249  	err = wait.Poll(500*time.Millisecond, timeout, func() (bool, error) {
   250  		err = util.HealthCheck(url, tlsCertBytes, timeout)
   251  		if err == nil {
   252  			return true, nil
   253  		}
   254  		return false, nil
   255  	})
   256  	if err != nil {
   257  		log.Error(err, "Health check failed")
   258  		return false
   259  	}
   260  
   261  	return true
   262  }
   263  
   264  func (r *Reenroller) Reenroll() (*config.Response, error) {
   265  	reuseKey := true
   266  	if r.NewKey {
   267  		reuseKey = false
   268  	}
   269  
   270  	reenrollReq := &api.ReenrollmentRequest{
   271  		CAName: r.Config.CAName,
   272  		CSR: &api.CSRInfo{
   273  			KeyRequest: &api.KeyRequest{
   274  				ReuseKey: reuseKey,
   275  			},
   276  		},
   277  	}
   278  
   279  	if r.Config.CSR != nil && len(r.Config.CSR.Hosts) > 0 {
   280  		reenrollReq.CSR.Hosts = r.Config.CSR.Hosts
   281  	}
   282  
   283  	log.Info(fmt.Sprintf("Re-enrolling with CA '%s' with request %+v, csr %+v", r.Config.CAHost, reenrollReq, reenrollReq.CSR))
   284  
   285  	reenrollResp, err := r.Identity.Reenroll(reenrollReq)
   286  	if err != nil {
   287  		return nil, errors.Wrap(err, "failed to re-enroll with CA")
   288  	}
   289  
   290  	newIdentity := reenrollResp.Identity
   291  
   292  	resp := &config.Response{}
   293  	resp.SignCert = newIdentity.GetECert().Cert()
   294  
   295  	// Only need to read key if a new key is being generated, which does not happen
   296  	// if the reenroll request has "ReuseKey" set to true
   297  	if !reuseKey {
   298  		key, err := r.ReadKey()
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  		resp.Keystore = key
   303  	}
   304  
   305  	// NOTE: Added this logic because the keystore file wasn't getting
   306  	// deleted, which impacts the next time the certificate is renewed, in that
   307  	// when trying to ReadKey(), there would be more than 1 file present.
   308  	err = r.DeleteKeystoreFile()
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  
   313  	// TODO: Currently not parsing reenroll response to get CACerts and
   314  	// Intermediate Certs again (like we do when inintially enrolling with CA)
   315  	// as those certs shouldn't need to be updated
   316  
   317  	return resp, nil
   318  }
   319  
   320  func (r *Reenroller) ReadKey() ([]byte, error) {
   321  	if r.BCCSP {
   322  		return nil, nil
   323  	}
   324  
   325  	keystoreDir := filepath.Join(r.HomeDir, "msp", "keystore")
   326  
   327  	files, err := ioutil.ReadDir(keystoreDir)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	if len(files) > 1 {
   333  		return nil, errors.Errorf("expecting only one key file to present in keystore '%s', but found multiple", keystoreDir)
   334  	}
   335  
   336  	for _, file := range files {
   337  		fileBytes, err := ioutil.ReadFile(filepath.Clean(filepath.Join(keystoreDir, file.Name())))
   338  		if err != nil {
   339  			return nil, err
   340  		}
   341  
   342  		block, _ := pem.Decode(fileBytes)
   343  		if block == nil {
   344  			continue
   345  		}
   346  
   347  		_, err = x509.ParsePKCS8PrivateKey(block.Bytes)
   348  		if err == nil {
   349  			return fileBytes, nil
   350  		}
   351  	}
   352  
   353  	return nil, errors.Errorf("failed to read private key from dir '%s'", keystoreDir)
   354  }
   355  
   356  func (r *Reenroller) DeleteKeystoreFile() error {
   357  	keystoreDir := filepath.Join(r.HomeDir, "msp", "keystore")
   358  
   359  	files, err := ioutil.ReadDir(keystoreDir)
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	for _, file := range files {
   365  		err = os.Remove(filepath.Join(keystoreDir, file.Name()))
   366  		if err != nil {
   367  			return errors.Wrapf(err, "failed to delete keystore directory '%s'", keystoreDir)
   368  		}
   369  	}
   370  
   371  	return nil
   372  }
   373  
   374  func EnrollmentConfigValidation(enrollConfig *current.Enrollment) error {
   375  	if enrollConfig.CAHost == "" {
   376  		return errors.New("unable to reenroll, CA host not specified")
   377  	}
   378  
   379  	if enrollConfig.CAPort == "" {
   380  		return errors.New("unable to reenroll, CA port not specified")
   381  	}
   382  
   383  	if enrollConfig.EnrollID == "" {
   384  		return errors.New("unable to reenroll, enrollment ID not specified")
   385  	}
   386  
   387  	if enrollConfig.CATLS.CACert == "" {
   388  		return errors.New("unable to reenroll, CA TLS certificate not specified")
   389  	}
   390  
   391  	return nil
   392  }