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 }