diff --git a/cmd/fabric-ca-client/command/main_test.go b/cmd/fabric-ca-client/command/main_test.go index 3a2b5acaf..99bcc60b9 100644 --- a/cmd/fabric-ca-client/command/main_test.go +++ b/cmd/fabric-ca-client/command/main_test.go @@ -1,17 +1,7 @@ /* -Copyright IBM Corp. 2017 All Rights Reserved. +Copyright IBM Corp. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +SPDX-License-Identifier: Apache-2.0 */ package command @@ -2464,11 +2454,11 @@ func registerAndRevokeUsers(t *testing.T, admin *lib.Identity, num int) []*big.I t.Fatalf("Failed to enroll the identity '%s': %s", userName, err) } - cert, err := enrollResp.Identity.GetECert().GetX509Cert() - if err != nil { - t.Fatalf("Failed to get enrollment certificate for the user %s: %s", userName, err) + x509Cred := enrollResp.Identity.GetECert() + if x509Cred == nil || x509Cred.GetX509Cert() == nil { + t.Fatalf("Failed to get enrollment certificate for the user %s", userName) } - + cert := x509Cred.GetX509Cert() revokeReq := &api.RevocationRequest{} if i%2 == 0 { revokeReq.Name = userName diff --git a/lib/client.go b/lib/client.go index b62cba5a3..12f9cc4b7 100644 --- a/lib/client.go +++ b/lib/client.go @@ -16,20 +16,27 @@ import ( "net/url" "os" "path" + "path/filepath" "strconv" "strings" + proto "github.com/golang/protobuf/proto" + fp256bn "github.com/hyperledger/fabric-amcl/amcl/FP256BN" "github.com/pkg/errors" cfsslapi "github.com/cloudflare/cfssl/api" "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib/client/credential" + idemixcred "github.com/hyperledger/fabric-ca/lib/client/credential/idemix" + x509cred "github.com/hyperledger/fabric-ca/lib/client/credential/x509" "github.com/hyperledger/fabric-ca/lib/common" "github.com/hyperledger/fabric-ca/lib/streamer" "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/bccsp" + "github.com/hyperledger/fabric/idemix" "github.com/mitchellh/mapstructure" ) @@ -42,13 +49,32 @@ type Client struct { // Denotes if the client object is already initialized initialized bool // File and directory paths - keyFile, certFile, caCertsDir string + keyFile, certFile, idemixCredFile, idemixCredsDir, ipkFile, caCertsDir string // The crypto service provider (BCCSP) csp bccsp.BCCSP // HTTP client associated with this Fabric CA client httpClient *http.Client } +// GetCAInfoResponse is the response from the GetCAInfo call +type GetCAInfoResponse struct { + // CAName is the name of the CA + CAName string + // CAChain is the PEM-encoded bytes of the fabric-ca-server's CA chain. + // The 1st element of the chain is the root CA cert + CAChain []byte + // Idemix issuer public key of the CA + IssuerPublicKey []byte + // Version of the server + Version string +} + +// EnrollmentResponse is the response from Client.Enroll and Identity.Reenroll +type EnrollmentResponse struct { + Identity *Identity + CAInfo GetCAInfoResponse +} + // Init initializes the client func (c *Client) Init() error { if !c.initialized { @@ -85,6 +111,17 @@ func (c *Client) Init() error { return errors.Wrap(err, "Failed to create cacerts directory") } + // CA's Idemix public key + c.ipkFile = filepath.Join(mspDir, "IssuerPublicKey") + + // Idemix credentials directory + c.idemixCredsDir = path.Join(mspDir, "user") + err = os.MkdirAll(c.idemixCredsDir, 0755) + if err != nil { + return errors.Wrap(err, "Failed to create Idemix credentials directory 'user'") + } + c.idemixCredFile = path.Join(c.idemixCredsDir, "SignerConfig") + // Initialize BCCSP (the crypto layer) c.csp, err = util.InitBCCSP(&cfg.CSP, mspDir, c.HomeDir) if err != nil { @@ -122,19 +159,6 @@ func (c *Client) initHTTPClient() error { return nil } -// GetCAInfoResponse is the response from the GetServerInfo call -type GetCAInfoResponse struct { - // CAName is the name of the CA - CAName string - // CAChain is the PEM-encoded bytes of the fabric-ca-server's CA chain. - // The 1st element of the chain is the root CA cert - CAChain []byte - // Idemix issuer public key of the CA - IssuerPublicKey []byte - // Version of the server - Version string -} - // GetCAInfo returns generic CA information func (c *Client) GetCAInfo(req *api.GetCAInfoRequest) (*GetCAInfoResponse, error) { err := c.Init() @@ -162,6 +186,53 @@ func (c *Client) GetCAInfo(req *api.GetCAInfoRequest) (*GetCAInfoResponse, error return localSI, nil } +// GenCSR generates a CSR (Certificate Signing Request) +func (c *Client) GenCSR(req *api.CSRInfo, id string) ([]byte, bccsp.Key, error) { + log.Debugf("GenCSR %+v", req) + + err := c.Init() + if err != nil { + return nil, nil, err + } + + cr := c.newCertificateRequest(req) + cr.CN = id + + if cr.KeyRequest == nil { + cr.KeyRequest = newCfsslBasicKeyRequest(api.NewBasicKeyRequest()) + } + + key, cspSigner, err := util.BCCSPKeyRequestGenerate(cr, c.csp) + if err != nil { + log.Debugf("failed generating BCCSP key: %s", err) + return nil, nil, err + } + + csrPEM, err := csr.Generate(cspSigner, cr) + if err != nil { + log.Debugf("failed generating CSR: %s", err) + return nil, nil, err + } + + return csrPEM, key, nil +} + +// Enroll enrolls a new identity +// @param req The enrollment request +func (c *Client) Enroll(req *api.EnrollmentRequest) (*EnrollmentResponse, error) { + log.Debugf("Enrolling %+v", req) + + err := c.Init() + if err != nil { + return nil, err + } + + if strings.ToLower(req.Type) == "idemix" { + return c.handleIdemixEnroll(req) + } + return c.handleX509Enroll(req) +} + // Convert from network to local server information func (c *Client) net2LocalServerInfo(net *common.CAInfoResponseNet, local *GetCAInfoResponse) error { caChain, err := util.B64Decode(net.CAChain) @@ -181,22 +252,7 @@ func (c *Client) net2LocalServerInfo(net *common.CAInfoResponseNet, local *GetCA return nil } -// EnrollmentResponse is the response from Client.Enroll and Identity.Reenroll -type EnrollmentResponse struct { - Identity *Identity - CAInfo GetCAInfoResponse -} - -// Enroll enrolls a new identity -// @param req The enrollment request -func (c *Client) Enroll(req *api.EnrollmentRequest) (*EnrollmentResponse, error) { - log.Debugf("Enrolling %+v", req) - - err := c.Init() - if err != nil { - return nil, err - } - +func (c *Client) handleX509Enroll(req *api.EnrollmentRequest) (*EnrollmentResponse, error) { // Generate the CSR csrPEM, key, err := c.GenCSR(req.CSR, req.Name) if err != nil { @@ -236,6 +292,113 @@ func (c *Client) Enroll(req *api.EnrollmentRequest) (*EnrollmentResponse, error) return c.newEnrollmentResponse(&result, req.Name, key) } +// Handles enrollment request for an Idemix credential +// 1. Sends a request with empty body to the /api/v1/idemix/credentail REST endpoint +// of the server to get a Nonce from the CA +// 2. Constructs a credential request using the nonce, CA's idemix public key +// 3. Sends a request with the CredentialRequest object in the body to the +// /api/v1/idemix/credentail REST endpoint to get a credential +func (c *Client) handleIdemixEnroll(req *api.EnrollmentRequest) (*EnrollmentResponse, error) { + log.Debugf("Getting nonce from CA %s", req.CAName) + reqNet := &api.IdemixEnrollmentRequestNet{ + CAName: req.CAName, + } + var identity *Identity + + // Get nonce from the CA + body, err := util.Marshal(reqNet, "NonceRequest") + if err != nil { + return nil, errors.WithMessage(err, "Failed to marshal nonce request") + } + post, err := c.newPost("idemix/credential", body) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create HTTP request for getting a nonce") + } + err = c.addAuthHeaderForIdemixEnroll(req, identity, body, post) + if err != nil { + return nil, errors.WithMessage(err, + "Either username/password or X509 enrollment certificate is required to request an Idemix credential") + } + + // Send the request and process the response + var result common.IdemixEnrollmentResponseNet + err = c.SendReq(post, &result) + if err != nil { + return nil, err + } + nonceBytes, err := util.B64Decode(result.Nonce) + + if err != nil { + return nil, errors.WithMessage(err, + fmt.Sprintf("Failed to decode nonce that was returned by CA %s", req.CAName)) + } + nonce := fp256bn.FromBytes(nonceBytes) + log.Infof("Successfully got nonce from CA %s", req.CAName) + + ipkBytes, err := util.B64Decode(result.CAInfo.IssuerPublicKey) + if err != nil { + return nil, errors.WithMessage(err, fmt.Sprintf("Failed to decode issuer public key that was returned by CA %s", req.CAName)) + } + // Create credential request + credReq, sk, rand, err := c.newIdemixCredentialRequest(nonce, ipkBytes) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create an Idemix credential request") + } + reqNet.CredRequest = credReq + log.Info("Successfully created an Idemix credential request") + + body, err = util.Marshal(reqNet, "CredentialRequest") + if err != nil { + return nil, errors.WithMessage(err, "Failed to marshal Idemix credential request") + } + + // Send the cred request to the CA + post, err = c.newPost("idemix/credential", body) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create HTTP request for getting Idemix credential") + } + err = c.addAuthHeaderForIdemixEnroll(req, identity, body, post) + if err != nil { + return nil, errors.WithMessage(err, + "Either username/password or X509 enrollment certificate is required to request idemix credential") + } + err = c.SendReq(post, &result) + if err != nil { + return nil, err + } + log.Infof("Successfully received Idemix credential from CA %s", req.CAName) + return c.newIdemixEnrollmentResponse(identity, &result, sk, rand, req.Name) +} + +// addAuthHeaderForIdemixEnroll adds authenticate header to the specified HTTP request +// It adds basic authentication header if userName and password are specified in the +// specified EnrollmentRequest object. Else, checks if a X509 credential in the client's +// MSP directory, if so, loads the identity, creates an oauth token based on the loaded +// identity's X509 credential, and adds the token to the HTTP request. The loaded +// identity is passed back to the caller. +func (c *Client) addAuthHeaderForIdemixEnroll(req *api.EnrollmentRequest, id *Identity, + body []byte, post *http.Request) error { + if req.Name != "" && req.Secret != "" { + post.SetBasicAuth(req.Name, req.Secret) + return nil + } + if id == nil { + err := c.checkX509Enrollment() + if err != nil { + return err + } + id, err = c.LoadMyIdentity() + if err != nil { + return err + } + } + err := id.addTokenAuthHdr(post, body) + if err != nil { + return err + } + return nil +} + // newEnrollmentResponse creates a client enrollment response from a network response // @param result The result from server // @param id Name of identity being enrolled or reenrolled @@ -246,8 +409,17 @@ func (c *Client) newEnrollmentResponse(result *common.EnrollmentResponseNet, id if err != nil { return nil, errors.WithMessage(err, "Invalid response format from server") } + signer, err := x509cred.NewSigner(key, certByte) + if err != nil { + return nil, err + } + x509Cred := x509cred.NewCredential(c.certFile, c.keyFile, c) + err = x509Cred.SetVal(signer) + if err != nil { + return nil, err + } resp := &EnrollmentResponse{ - Identity: newIdentity(c, id, key, certByte), + Identity: NewIdentity(c, id, []credential.Credential{x509Cred}), } err = c.net2LocalServerInfo(&result.ServerInfo, &resp.CAInfo) if err != nil { @@ -256,35 +428,59 @@ func (c *Client) newEnrollmentResponse(result *common.EnrollmentResponseNet, id return resp, nil } -// GenCSR generates a CSR (Certificate Signing Request) -func (c *Client) GenCSR(req *api.CSRInfo, id string) ([]byte, bccsp.Key, error) { - log.Debugf("GenCSR %+v", req) +// newIdemixEnrollmentResponse creates a client idemix enrollment response from a network response +func (c *Client) newIdemixEnrollmentResponse(identity *Identity, result *common.IdemixEnrollmentResponseNet, + sk, rand *fp256bn.BIG, id string) (*EnrollmentResponse, error) { + log.Debugf("newIdemixEnrollmentResponse %s", id) + credBytes, err := util.B64Decode(result.Credential) + if err != nil { + return nil, errors.WithMessage(err, "Invalid response format from server") + } - err := c.Init() + icred := &idemix.Credential{} + err = proto.Unmarshal(credBytes, icred) if err != nil { - return nil, nil, err + return nil, errors.WithMessage(err, "Failed to unmarshal Idemix credential bytes") } - cr := c.newCertificateRequest(req) - cr.CN = id + icred.Complete(rand) + ccredBytes, err := proto.Marshal(icred) + if err != nil { + return nil, errors.WithMessage(err, "Failed to marshal completed Idemix credential") + } - if cr.KeyRequest == nil { - cr.KeyRequest = newCfsslBasicKeyRequest(api.NewBasicKeyRequest()) + // Create SignerConfig object with credential bytes from the response + // and secret key + isAdmin, _ := strconv.ParseBool(result.Attrs["Role"]) + signerConfig := &idemixcred.SignerConfig{ + Cred: ccredBytes, + Sk: idemix.BigToBytes(sk), + IsAdmin: isAdmin, + OrganizationalUnitIdentifier: result.Attrs["OU"], + EnrollmentID: result.Attrs["EnrollmentID"], } - key, cspSigner, err := util.BCCSPKeyRequestGenerate(cr, c.csp) + // Create IdemixCredential object + cred := idemixcred.NewCredential(c.idemixCredFile, c) + err = cred.SetVal(signerConfig) if err != nil { - log.Debugf("failed generating BCCSP key: %s", err) - return nil, nil, err + return nil, err + } + if identity == nil { + identity = NewIdentity(c, id, []credential.Credential{cred}) + } else { + identity.creds = append(identity.creds, cred) } - csrPEM, err := csr.Generate(cspSigner, cr) + resp := &EnrollmentResponse{ + Identity: identity, + } + err = c.net2LocalServerInfo(&result.CAInfo, &resp.CAInfo) if err != nil { - log.Debugf("failed generating CSR: %s", err) - return nil, nil, err + return nil, err } - - return csrPEM, key, nil + log.Infof("Successfully processed response from the CA") + return resp, nil } // newCertificateRequest creates a certificate request which is used to generate @@ -314,60 +510,102 @@ func (c *Client) newCertificateRequest(req *api.CSRInfo) *csr.CertificateRequest return &cr } -// LoadMyIdentity loads the client's identity from disk -func (c *Client) LoadMyIdentity() (*Identity, error) { - err := c.Init() +// newIdemixCredentialRequest returns CredentialRequest object, a secret key, and a random number used in +// the creation of credential request. +func (c *Client) newIdemixCredentialRequest(nonce *fp256bn.BIG, ipkBytes []byte) (*idemix.CredRequest, *fp256bn.BIG, *fp256bn.BIG, error) { + rng, err := idemix.GetRand() if err != nil { - return nil, err + return nil, nil, nil, err } - return c.LoadIdentity(c.keyFile, c.certFile) + sk := idemix.RandModOrder(rng) + randCred := idemix.RandModOrder(rng) + + issuerPubKey, err := c.getIssuerPubKey(ipkBytes) + if err != nil { + return nil, nil, nil, err + } + return idemix.NewCredRequest(sk, randCred, nonce, issuerPubKey, rng), sk, randCred, nil } -// StoreMyIdentity stores my identity to disk -func (c *Client) StoreMyIdentity(cert []byte) error { - err := c.Init() +func (c *Client) getIssuerPubKey(ipkBytes []byte) (*idemix.IssuerPublicKey, error) { + var err error + if ipkBytes == nil || len(ipkBytes) == 0 { + ipkBytes, err = ioutil.ReadFile(c.ipkFile) + if err != nil { + return nil, errors.Wrapf(err, "Error reading CA's Idemix public key at '%s'", c.ipkFile) + } + } + pubKey := &idemix.IssuerPublicKey{} + err = proto.Unmarshal(ipkBytes, pubKey) if err != nil { - return err + return nil, err } - err = util.WriteFile(c.certFile, cert, 0644) + return pubKey, nil +} + +// LoadMyIdentity loads the client's identity from disk +func (c *Client) LoadMyIdentity() (*Identity, error) { + err := c.Init() if err != nil { - return errors.WithMessage(err, "Failed to store my certificate") + return nil, err } - log.Infof("Stored client certificate at %s", c.certFile) - return nil + return c.LoadIdentity(c.keyFile, c.certFile, c.idemixCredFile) } // LoadIdentity loads an identity from disk -func (c *Client) LoadIdentity(keyFile, certFile string) (*Identity, error) { +func (c *Client) LoadIdentity(keyFile, certFile, idemixCredFile string) (*Identity, error) { log.Debugf("Loading identity: keyFile=%s, certFile=%s", keyFile, certFile) err := c.Init() if err != nil { return nil, err } - cert, err := util.ReadFile(certFile) + x509Cred := x509cred.NewCredential(certFile, keyFile, c) + err = x509Cred.Load() if err != nil { - log.Debugf("No cert found at %s", certFile) - return nil, err + return nil, errors.WithMessage(err, "Failed to load X509 credential") } - key, _, _, err := util.GetSignerFromCertFile(certFile, c.csp) - if err != nil { - // Fallback: attempt to read out of keyFile and import - log.Debugf("No key found in BCCSP keystore, attempting fallback") - key, err = util.ImportBCCSPKeyFromPEM(keyFile, c.csp, true) + creds := []credential.Credential{x509Cred} + _, err = os.Stat(idemixCredFile) + if err == nil { + idemixCred := idemixcred.NewCredential(idemixCredFile, c) + err = idemixCred.Load() if err != nil { - return nil, errors.WithMessage(err, fmt.Sprintf("Could not find the private key in BCCSP keystore nor in keyfile %s", keyFile)) + log.Debugf("No idemix credential found at %s", idemixCredFile) } + creds = append(creds, idemixCred) } - return c.NewIdentity(key, cert) + return c.NewIdentity(creds) } // NewIdentity creates a new identity -func (c *Client) NewIdentity(key bccsp.Key, cert []byte) (*Identity, error) { - name, err := util.GetEnrollmentIDFromPEM(cert) +func (c *Client) NewIdentity(creds []credential.Credential) (*Identity, error) { + if len(creds) == 0 { + return nil, errors.New("No credentials spcified. Atleast one credential must be specified") + } + name, err := creds[0].EnrollmentID() if err != nil { return nil, err } - return newIdentity(c, name, key, cert), nil + if len(creds) == 1 { + return NewIdentity(c, name, creds), nil + } + + //TODO: Get the enrollment ID from the creds...they all should return same value + // for i := 1; i < len(creds); i++ { + // localid, err := creds[i].EnrollmentID() + // if err != nil { + // return nil, err + // } + // if localid != name { + // return nil, errors.New("Specified credentials belong to different identities, they should be long to same identity") + // } + // } + return NewIdentity(c, name, creds), nil +} + +// NewX509Identity creates a new identity +func (c *Client) NewX509Identity(name string, creds []credential.Credential) x509cred.Identity { + return NewIdentity(c, name, creds) } // LoadCSRInfo reads CSR (Certificate Signing Request) from a file @@ -390,6 +628,16 @@ func (c *Client) GetCertFilePath() string { return c.certFile } +// GetCSP returns BCCSP instance associated with this client +func (c *Client) GetCSP() bccsp.BCCSP { + return c.csp +} + +// GetIssuerPubKey returns issuer public key associated with this client +func (c *Client) GetIssuerPubKey() (*idemix.IssuerPublicKey, error) { + return c.getIssuerPubKey(nil) +} + // newGet create a new GET request func (c *Client) newGet(endpoint string) (*http.Request, error) { curl, err := c.getURL(endpoint) @@ -551,6 +799,23 @@ func (c *Client) CheckEnrollment() error { if err != nil { return err } + var x509Enrollment, idemixEnrollment bool + err = c.checkX509Enrollment() + if err == nil { + x509Enrollment = true + } + err = c.checkIdemixEnrollment() + if err == nil { + idemixEnrollment = true + } + if x509Enrollment || idemixEnrollment { + return nil + } + log.Errorf("Enrollment check failed: %s", err.Error()) + return errors.New("Enrollment information does not exist. Please execute enroll command first. Example: fabric-ca-client enroll -u http://user:userpw@serverAddr:serverPort") +} + +func (c *Client) checkX509Enrollment() error { keyFileExists := util.FileExists(c.keyFile) certFileExists := util.FileExists(c.certFile) if keyFileExists && certFileExists { @@ -565,7 +830,53 @@ func (c *Client) CheckEnrollment() error { return nil } } - return errors.New("Enrollment information does not exist. Please execute enroll command first. Example: fabric-ca-client enroll -u http://user:userpw@serverAddr:serverPort") + return errors.New("X509 enrollment information does not exist") +} + +// checkIdemixEnrollment returns an error if CA's Idemix public key and user's +// Idemix credential does not exist and if they exist and credential verification +// fails. Returns nil if the credential verification suucceeds +func (c *Client) checkIdemixEnrollment() error { + idemixIssuerPubKeyExists := util.FileExists(c.ipkFile) + idemixCredExists := util.FileExists(c.idemixCredFile) + if idemixIssuerPubKeyExists && idemixCredExists { + err := c.verifyIdemixCredential() + if err != nil { + return errors.WithMessage(err, "Idemix enrollment check failed") + } + return nil + } + return errors.New("Idemix enrollment information does not exist") +} + +func (c *Client) verifyIdemixCredential() error { + ipk, err := c.getIssuerPubKey(nil) + if err != nil { + return err + } + credfileBytes, err := util.ReadFile(c.idemixCredFile) + if err != nil { + return errors.Wrapf(err, "Failed to read %s", c.idemixCredFile) + } + signerConfig := &idemixcred.SignerConfig{} + err = json.Unmarshal(credfileBytes, signerConfig) + if err != nil { + return errors.Wrapf(err, "Failed to unmarshal signer config from %s", c.idemixCredFile) + } + + cred := new(idemix.Credential) + err = proto.Unmarshal(signerConfig.GetCred(), cred) + if err != nil { + return errors.Wrap(err, "Failed to unmarshal Idemix credential from signer config") + } + sk := fp256bn.FromBytes(signerConfig.GetSk()) + + // Verify that the credential is cryptographically valid + err = cred.Ver(sk, ipk) + if err != nil { + return errors.Wrap(err, "Idemix credential is not cryptographically valid") + } + return nil } func newCfsslBasicKeyRequest(bkr *api.BasicKeyRequest) *csr.BasicKeyRequest { diff --git a/lib/client/credential/credential.go b/lib/client/credential/credential.go new file mode 100644 index 000000000..fa1f507f8 --- /dev/null +++ b/lib/client/credential/credential.go @@ -0,0 +1,38 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package credential + +import ( + "net/http" + + "github.com/hyperledger/fabric-ca/api" +) + +// Credential represents an credential of an identity +type Credential interface { + // Type returns type of this credential + Type() string + // EnrollmentID returns enrollment ID associated with this credential + // Returns an error if the credential value is not set (SetVal is not called) + // or not loaded from the disk (Load is not called) + EnrollmentID() (string, error) + // Val returns credential value. + // Returns an error if the credential value is not set (SetVal is not called) + // or not loaded from the disk (Load is not called) + Val() (interface{}, error) + // Sets the credential value + SetVal(val interface{}) error + // Stores the credential value to disk + Store() error + // Loads the credential value from disk and sets the value of this credential + Load() error + // CreateOAuthToken returns oauth autentication token for that request with + // specified body + CreateOAuthToken(req *http.Request, reqBody []byte) (string, error) + // Submits revoke request to the Fabric CA server to revoke this credential + RevokeSelf() (*api.RevocationResponse, error) +} diff --git a/lib/client/credential/idemix/credential.go b/lib/client/credential/idemix/credential.go new file mode 100644 index 000000000..0c665bf9e --- /dev/null +++ b/lib/client/credential/idemix/credential.go @@ -0,0 +1,168 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package idemix + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/cloudflare/cfssl/log" + "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/bccsp" + idemix "github.com/hyperledger/fabric/idemix" + "github.com/pkg/errors" +) + +const ( + // CredType is the string that represents Idemix credential type + CredType = "Idemix" +) + +// Client represents a client that will load/store an Idemix credential +type Client interface { + GetIssuerPubKey() (*idemix.IssuerPublicKey, error) + GetCSP() bccsp.BCCSP +} + +// Credential represents an Idemix credential. Implements Credential interface +type Credential struct { + client Client + signerConfigFile string + val *SignerConfig +} + +// NewCredential is constructor for idemix.Credential +func NewCredential(signerConfigFile string, c Client) *Credential { + return &Credential{ + c, signerConfigFile, nil, + } +} + +// Type returns Idemix +func (cred *Credential) Type() string { + return CredType +} + +// Val returns *SignerConfig associated with this Idemix credential +func (cred *Credential) Val() (interface{}, error) { + if cred.val == nil { + return nil, errors.New("Credential value is not set") + } + return cred.val, nil +} + +// EnrollmentID returns enrollment ID associated with this Idemix credential +func (cred *Credential) EnrollmentID() (string, error) { + if cred.val == nil { + return "", errors.New("Credential value is not set") + } + return "", errors.New("Not implemented") // TODO +} + +// SetVal sets *SignerConfig for this Idemix credential +func (cred *Credential) SetVal(val interface{}) error { + s, ok := val.(*SignerConfig) + if !ok { + return errors.New("The credential should be of type *SignerConfig for idemix Credential") + } + cred.val = s + return nil +} + +// Store stores this Idemix credential to the location specified by the +// signerConfigFile attribute +func (cred *Credential) Store() error { + val, err := cred.Val() + if err != nil { + return err + } + signerConfigBytes, err := json.Marshal(val) + if err != nil { + return errors.Wrapf(err, "Failed to marshal SignerConfig") + } + err = util.WriteFile(cred.signerConfigFile, signerConfigBytes, 0644) + if err != nil { + return errors.WithMessage(err, "Failed to store the Idemix credential") + } + log.Infof("Stored the Idemix credential at %s", cred.signerConfigFile) + return nil +} + +// Load loads the Idemix credential from the location specified by the +// signerConfigFile attribute +func (cred *Credential) Load() error { + signerConfigBytes, err := util.ReadFile(cred.signerConfigFile) + if err != nil { + log.Debugf("No credential found at %s: %s", cred.signerConfigFile, err.Error()) + return err + } + val := SignerConfig{} + err = json.Unmarshal(signerConfigBytes, &val) + if err != nil { + return errors.Wrapf(err, fmt.Sprintf("Failed to unmarshal SignerConfig bytes from %s", cred.signerConfigFile)) + } + cred.val = &val + return nil +} + +// CreateOAuthToken creates oauth token based on this Idemix credential +func (cred *Credential) CreateOAuthToken(req *http.Request, reqBody []byte) (string, error) { + return "", errors.New("Not implemented") // TODO + // enrollmentID, err := cred.EnrollmentID() + // if err != nil { + // return "", err + // } + // rng, err := idemix.GetRand() + // if err != nil { + // return "", errors.WithMessage(err, "Failed to get a random number while creating oauth token") + // } + // // Get user's secret key + // sk := amcl.FromBytes(cred.val.GetSk()) + + // // Get issuer public key + // ipk, err := cred.client.GetIssuerPubKey() + // if err != nil { + // return "", errors.WithMessage(err, "Failed to get CA's Idemix public key while creating oauth token") + // } + + // // Generate a fresh Pseudonym (and a corresponding randomness) + // nym, randNym := idemix.MakeNym(sk, ipk, rng) + + // nymBytes := []byte{} + // nym.ToBytes(nymBytes) + // nym64Encoding := util.B64Encode(nymBytes) + // body64Encoding := util.B64Encode(reqBody) + // msg := nym64Encoding + "." + body64Encoding + + // digest, digestError := cred.client.GetCSP().Hash([]byte(msg), &bccsp.SHAOpts{}) + // if digestError != nil { + // return "", errors.WithMessage(digestError, fmt.Sprintf("Failed to create authentication token '%s'", msg)) + // } + + // // a disclosure vector is formed (indicating that all attributes from the credential are revealed) + // disclosure := []byte{1, 1, 1, 1} + + // var credential *idemix.Credential + // err = json.Unmarshal(cred.val.GetCred(), credential) + // if err != nil { + // errors.Wrapf(err, "Failed to unmarshal Idemix credential while creating oauth token") + // } + // sig, err := idemix.NewSignature(credential, sk, nym, randNym, ipk, disclosure, digest, rng) + // if err != nil { + // errors.Wrapf(err, "Failed to create signature while creating oauth token") + // } + // sigBytes, err := json.Marshal(sig) + // token := enrollmentID + "." + util.B64Encode(sigBytes) + // return token, nil +} + +// RevokeSelf revokes this Idemix credential +func (cred *Credential) RevokeSelf() (*api.RevocationResponse, error) { + return nil, errors.New("Not implemented") //TODO +} diff --git a/lib/client/credential/idemix/credential_test.go b/lib/client/credential/idemix/credential_test.go new file mode 100644 index 000000000..701ab7193 --- /dev/null +++ b/lib/client/credential/idemix/credential_test.go @@ -0,0 +1,128 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package idemix_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + lib "github.com/hyperledger/fabric-ca/lib" + . "github.com/hyperledger/fabric-ca/lib/client/credential/idemix" + "github.com/stretchr/testify/assert" +) + +const ( + testDataDir = "../../../../testdata" + testSignerConfigFile = testDataDir + "/IdemixSignerConfig" +) + +func TestIdemixCredential(t *testing.T) { + clientHome, err := ioutil.TempDir(testDataDir, "idemixcredtest") + if err != nil { + t.Fatalf("Failed to create temp directory: %s", err.Error()) + } + defer os.RemoveAll(clientHome) + + signerConfig := filepath.Join(clientHome, "SignerConfig") + client := &lib.Client{ + Config: &lib.ClientConfig{ + URL: fmt.Sprintf("http://localhost:7054"), + }, + HomeDir: clientHome, + } + + idemixCred := NewCredential(signerConfig, client) + + assert.Equal(t, idemixCred.Type(), CredType, "Type for a IdemixCredential instance must be Idemix") + _, err = idemixCred.Val() + assert.Error(t, err, "Val should return error if credential has not been loaded from disk or set") + if err != nil { + assert.Equal(t, err.Error(), "Credential value is not set") + } + _, err = idemixCred.EnrollmentID() + assert.Error(t, err, "EnrollmentID should return an error if credential has not been loaded from disk or set") + if err != nil { + assert.Equal(t, err.Error(), "Credential value is not set") + } + + err = idemixCred.SetVal("hello") + assert.Error(t, err, "SetVal should fail as it expects an object of type *SignerConfig") + + err = idemixCred.Store() + assert.Error(t, err, "Store should return an error if credential has not been set") + + err = idemixCred.Load() + assert.Error(t, err, "Load should fail as %s is not found", signerConfig) + + err = ioutil.WriteFile(signerConfig, []byte("hello"), 0744) + if err != nil { + t.Fatalf("Failed to write to file %s: %s", signerConfig, err.Error()) + } + err = idemixCred.Load() + assert.Error(t, err, "Load should fail as %s contains invalid data", signerConfig) + + err = lib.CopyFile(testSignerConfigFile, signerConfig) + if err != nil { + t.Fatalf("Failed to copy %s to %s: %s", testSignerConfigFile, signerConfig, err.Error()) + } + + err = idemixCred.Load() + assert.NoError(t, err, "Load should not return error as %s exists and is valid", signerConfig) + + val, err := idemixCred.Val() + assert.NoError(t, err, "Val should not return error as credential is loaded") + + signercfg, _ := val.(*SignerConfig) + cred := signercfg.GetCred() + assert.NotNil(t, cred) + assert.True(t, len(cred) > 0, "Credential bytes length should be more than zero") + enrollID := signercfg.GetEnrollmentID() + assert.Equal(t, "admin", enrollID, "Enrollment ID of the Idemix credential in testdata/IdemixSignerConfig should be admin") + sk := signercfg.GetSk() + assert.NotNil(t, sk, "secret key should not be nil") + assert.True(t, len(sk) > 0, "Secret key bytes length should be more than zero") + signercfg.GetOrganizationalUnitIdentifier() + isAdmin := signercfg.GetIsAdmin() + assert.False(t, isAdmin) + + err = idemixCred.SetVal(val) + assert.NoError(t, err, "Setting the value that we got from the credential should not return an error") + + if err = os.Chmod(signerConfig, 0000); err != nil { + t.Fatalf("Failed to chmod SignerConfig file %s: %v", signerConfig, err) + } + err = idemixCred.Store() + assert.Error(t, err, "Store should fail as %s is not writable", signerConfig) + + if err = os.Chmod(signerConfig, 0644); err != nil { + t.Fatalf("Failed to chmod SignerConfig file %s: %v", signerConfig, err) + } + err = idemixCred.Store() + assert.NoError(t, err, "Store should not fail as %s is writable and Idemix credential value is set", signerConfig) + + _, err = idemixCred.Val() + assert.NoError(t, err, "Val should not return error as Idemix credential has been loaded") + + _, err = idemixCred.EnrollmentID() + assert.Error(t, err, "EnrollmentID is not implemented for Idemix credential") + + body := []byte("hello") + req, err := http.NewRequest("GET", "localhost:7054/enroll", bytes.NewReader(body)) + if err != nil { + t.Fatalf("Failed to create HTTP request: %s", err.Error()) + } + _, err = idemixCred.CreateOAuthToken(req, body) + assert.Error(t, err, "CreateOAuthToken is not implemented for Idemix credential") + + _, err = idemixCred.RevokeSelf() + assert.Error(t, err, "RevokeSelf should fail as it is not implmented for Idemix credential") +} diff --git a/lib/client/credential/idemix/signerconfig.go b/lib/client/credential/idemix/signerconfig.go new file mode 100644 index 000000000..576678065 --- /dev/null +++ b/lib/client/credential/idemix/signerconfig.go @@ -0,0 +1,48 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package idemix + +// SignerConfig contains the crypto material to set up an idemix signing identity +type SignerConfig struct { + // Cred represents the serialized idemix credential of the default signer + Cred []byte `protobuf:"bytes,1,opt,name=Cred,proto3" json:"Cred,omitempty"` + // Sk is the secret key of the default signer, corresponding to credential Cred + Sk []byte `protobuf:"bytes,2,opt,name=Sk,proto3" json:"Sk,omitempty"` + // OrganizationalUnitIdentifier defines the organizational unit the default signer is in + OrganizationalUnitIdentifier string `protobuf:"bytes,3,opt,name=organizational_unit_identifier,json=organizationalUnitIdentifier" json:"organizational_unit_identifier,omitempty"` + // IsAdmin defines whether the default signer is admin or not + IsAdmin bool `protobuf:"varint,4,opt,name=is_admin,json=isAdmin" json:"is_admin,omitempty"` + // EnrollmentID contains the enrollment id of this signer + EnrollmentID string `protobuf:"bytes,5,opt,name=enrollment_id,json=enrollmentId" json:"enrollment_id,omitempty"` +} + +// GetCred returns credential associated with this signer config +func (s *SignerConfig) GetCred() []byte { + return s.Cred + +} + +// GetSk returns secret key associated with this signer config +func (s *SignerConfig) GetSk() []byte { + return s.Sk +} + +// GetOrganizationalUnitIdentifier returns OU of the user associated with this signer config +func (s *SignerConfig) GetOrganizationalUnitIdentifier() string { + return s.OrganizationalUnitIdentifier +} + +// GetIsAdmin returns true if the user associated with this signer config is an admin, else +// returns false +func (s *SignerConfig) GetIsAdmin() bool { + return s.IsAdmin +} + +// GetEnrollmentID returns enrollment ID of the user associated with this signer config +func (s *SignerConfig) GetEnrollmentID() string { + return s.EnrollmentID +} diff --git a/lib/client/credential/x509/credential.go b/lib/client/credential/x509/credential.go new file mode 100644 index 000000000..7f3b84554 --- /dev/null +++ b/lib/client/credential/x509/credential.go @@ -0,0 +1,155 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package x509 + +import ( + "encoding/hex" + "fmt" + "net/http" + + "github.com/cloudflare/cfssl/log" + "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib/client/credential" + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/bccsp" + "github.com/pkg/errors" +) + +const ( + // CredType is the string that represents X509 credential type + CredType = "X509" +) + +// Client represents a client that will load/store an Idemix credential +type Client interface { + NewX509Identity(name string, creds []credential.Credential) Identity + GetCSP() bccsp.BCCSP +} + +// Identity represents an identity +type Identity interface { + Revoke(req *api.RevocationRequest) (*api.RevocationResponse, error) +} + +// Credential represents a X509 credential. Implements Credential interface +type Credential struct { + client Client + certFile string + keyFile string + val *Signer +} + +// NewCredential is constructor for X509 Credential +func NewCredential(certFile, keyFile string, c Client) *Credential { + return &Credential{ + c, certFile, keyFile, nil, + } +} + +// Type returns X509 +func (cred *Credential) Type() string { + return CredType +} + +// Val returns *Signer associated with this X509 credential +func (cred *Credential) Val() (interface{}, error) { + if cred.val == nil { + return nil, errors.New("Credential value is not set") + } + return cred.val, nil +} + +// EnrollmentID returns enrollment ID of this X509 credential +func (cred *Credential) EnrollmentID() (string, error) { + if cred.val == nil { + return "", errors.New("Credential value is not set") + } + return cred.val.GetName(), nil +} + +// SetVal sets *Signer for this X509 credential +func (cred *Credential) SetVal(val interface{}) error { + s, ok := val.(*Signer) + if !ok { + return errors.New("The credential value should be of type *Signer for X509 credential") + } + cred.val = s + return nil +} + +// Load loads the certificate and key from the location specified by +// certFile attribute using the BCCSP of the client. The private key is +// loaded from the location specified by the keyFile attribute, if the +// private key is not found in the keystore managed by BCCSP +func (cred *Credential) Load() error { + cert, err := util.ReadFile(cred.certFile) + if err != nil { + log.Debugf("No certificate found at %s", cred.certFile) + return err + } + csp := cred.getCSP() + key, _, _, err := util.GetSignerFromCertFile(cred.certFile, csp) + if err != nil { + // Fallback: attempt to read out of keyFile and import + log.Debugf("No key found in the BCCSP keystore, attempting fallback") + key, err = util.ImportBCCSPKeyFromPEM(cred.keyFile, csp, true) + if err != nil { + return errors.WithMessage(err, fmt.Sprintf("Could not find the private key in the BCCSP keystore nor in the keyfile %s", cred.keyFile)) + } + } + cred.val, err = NewSigner(key, cert) + if err != nil { + return err + } + return nil +} + +// Store stores the certificate associated with this X509 credential to the location +// specified by certFile attribute +func (cred *Credential) Store() error { + if cred.val == nil { + return errors.New("Certificate is not set") + } + err := util.WriteFile(cred.certFile, cred.val.Cert(), 0644) + if err != nil { + return errors.WithMessage(err, "Failed to store the certificate") + } + log.Infof("Stored client certificate at %s", cred.certFile) + return nil +} + +// CreateOAuthToken creates oauth token based on this X509 credential +func (cred *Credential) CreateOAuthToken(req *http.Request, reqBody []byte) (string, error) { + return util.CreateToken(cred.getCSP(), cred.val.certBytes, cred.val.key, req.Method, req.URL.RequestURI(), reqBody) +} + +// RevokeSelf revokes this X509 credential +func (cred *Credential) RevokeSelf() (*api.RevocationResponse, error) { + val := cred.val + if val == nil { + return nil, errors.New("Credential value is not set") + } + serial := util.GetSerialAsHex(val.cert.SerialNumber) + aki := hex.EncodeToString(val.cert.AuthorityKeyId) + req := &api.RevocationRequest{ + Serial: serial, + AKI: aki, + } + name, err := cred.EnrollmentID() + if err != nil { + return nil, err + } + id := cred.client.NewX509Identity(name, []credential.Credential{cred}) + return id.Revoke(req) +} + +func (cred *Credential) getCSP() bccsp.BCCSP { + if cred.client != nil && cred.client.GetCSP() != nil { + return cred.client.GetCSP() + } + return util.GetDefaultBCCSP() +} diff --git a/lib/client/credential/x509/credential_test.go b/lib/client/credential/x509/credential_test.go new file mode 100644 index 000000000..2dee86c91 --- /dev/null +++ b/lib/client/credential/x509/credential_test.go @@ -0,0 +1,234 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package x509_test + +import ( + "bytes" + "crypto/x509" + "encoding/hex" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib" + "github.com/hyperledger/fabric-ca/lib/client/credential" + . "github.com/hyperledger/fabric-ca/lib/client/credential/x509" + "github.com/hyperledger/fabric-ca/lib/client/credential/x509/mocks" + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/bccsp/factory" + "github.com/stretchr/testify/assert" +) + +const ( + testDataDir = "../../../../testdata" +) + +func TestX509Credential(t *testing.T) { + clientHome, err := ioutil.TempDir(testDataDir, "x509credtest") + if err != nil { + t.Fatalf("Failed to create temp directory: %s", err.Error()) + } + defer os.RemoveAll(clientHome) + + err = lib.CopyFile(filepath.Join(testDataDir, "ec256-1-cert.pem"), filepath.Join(clientHome, "ec256-1-cert.pem")) + if err != nil { + t.Fatalf("Failed to copy ec256-1-cert.pem to %s: %s", clientHome, err.Error()) + } + err = os.MkdirAll(filepath.Join(clientHome, "msp/keystore"), 0777) + if err != nil { + t.Fatalf("Failed to create msp/keystore directory: %s", err.Error()) + } + + client := &lib.Client{ + Config: &lib.ClientConfig{ + URL: fmt.Sprintf("http://localhost:7054"), + CSP: &factory.FactoryOpts{ + SwOpts: &factory.SwOpts{ + HashFamily: "SHA2", + SecLevel: 256, + FileKeystore: &factory.FileKeystoreOpts{ + KeyStorePath: "msp/keystore", + }, + }, + }, + }, + HomeDir: clientHome, + } + certFile := filepath.Join(client.HomeDir, "fake-cert.pem") + keyFile := filepath.Join(client.HomeDir, "fake-key.pem") + x509Cred := NewCredential(certFile, keyFile, client) + + assert.Equal(t, x509Cred.Type(), CredType, "Type for a X509Credential instance must be X509") + _, err = x509Cred.Val() + assert.Error(t, err, "Val should return error as credential has not been loaded from disk or set") + if err != nil { + assert.Equal(t, err.Error(), "Credential value is not set") + } + _, err = x509Cred.EnrollmentID() + assert.Error(t, err, "EnrollmentID should retrun an error as credential has not been loaded from disk or set") + if err != nil { + assert.Equal(t, err.Error(), "Credential value is not set") + } + + err = x509Cred.Store() + assert.Error(t, err, "Store should must retrun an error as credential has not been loaded from disk or set") + + err = x509Cred.SetVal("hello") + assert.Error(t, err, "SetVal should fail as it expects an object of type *Signer") + + _, err = x509Cred.RevokeSelf() + assert.Error(t, err, "RevokeSelf should return an error as credential has not been loaded from disk or set") + + err = x509Cred.Load() + assert.Error(t, err, "Load should have failed to load non-existent certificate file") + + certFile = filepath.Join(client.HomeDir, "ec256-1-cert.pem") + keyFile = filepath.Join(client.HomeDir, "ec256-1-key.pem") + + err = client.Init() + if err != nil { + t.Fatalf("Failed to initialize client: %s", err.Error()) + } + x509Cred = NewCredential(certFile, keyFile, client) + err = x509Cred.Load() + assert.Error(t, err, "Load should have failed to load key file") + assert.Contains(t, err.Error(), "Could not find the private key in the BCCSP keystore nor in the keyfile") + + err = lib.CopyFile(filepath.Join(testDataDir, "ec256-1-key.pem"), filepath.Join(client.HomeDir, "ec256-1-key.pem")) + if err != nil { + t.Fatalf("Failed to copy ec256-1-key.pem to %s: %s", clientHome, err.Error()) + } + err = x509Cred.Load() + assert.NoError(t, err, "Load should not fail to load as both cert and key files exist and are valid") + + err = os.Remove(keyFile) + if err != nil { + t.Fatalf("Failed to remove file %s: %s", keyFile, err.Error()) + } + keystore := filepath.Join(clientHome, "msp/keystore/ec256-1-key.pem") + err = lib.CopyFile(filepath.Join(testDataDir, "ec256-1-key.pem"), keystore) + if err != nil { + t.Fatalf("Failed to copy ec256-1-key.pem to %s: %s", keystore, err.Error()) + } + err = x509Cred.Load() + assert.NoError(t, err, "Should not fail to load x509 credential as cert exists and key is in bccsp keystore") + + _, err = x509Cred.Val() + assert.NoError(t, err, "Val should not return error as x509 credential has been loaded") + + _, err = x509Cred.EnrollmentID() + assert.NoError(t, err, "EnrollmentID should not return error as credential has been loaded") + + if err = os.Chmod(certFile, 0000); err != nil { + t.Fatalf("Failed to chmod certificate file %s: %v", certFile, err) + } + err = x509Cred.Store() + assert.Error(t, err, "Store should fail as %s is not writable", certFile) + + if err = os.Chmod(certFile, 0644); err != nil { + t.Fatalf("Failed to chmod certificate file %s: %v", certFile, err) + } + + err = x509Cred.Store() + assert.NoError(t, err, "Store should not fail as x509 credential is set and cert file path is valid") + + body := []byte("hello") + req, err := http.NewRequest("GET", "localhost:7054/enroll", bytes.NewReader(body)) + if err != nil { + t.Fatalf("Failed to create HTTP request: %s", err.Error()) + } + _, err = x509Cred.CreateOAuthToken(req, body) + assert.NoError(t, err, "CreateOAuthToken should not return error") +} + +func TestRevokeSelf(t *testing.T) { + clientHome, err := ioutil.TempDir(testDataDir, "revokeselftest") + if err != nil { + t.Fatalf("Failed to create temp directory: %s", err.Error()) + } + defer os.RemoveAll(clientHome) + + err = lib.CopyFile(filepath.Join(testDataDir, "ec256-1-cert.pem"), filepath.Join(clientHome, "ec256-1-cert.pem")) + if err != nil { + t.Fatalf("Failed to copy ec256-1-cert.pem to %s: %s", clientHome, err.Error()) + } + err = os.MkdirAll(filepath.Join(clientHome, "msp/keystore"), 0777) + if err != nil { + t.Fatalf("Failed to create msp/keystore directory: %s", err.Error()) + } + keystore := filepath.Join(clientHome, "msp/keystore/ec256-1-key.pem") + err = lib.CopyFile(filepath.Join(testDataDir, "ec256-1-key.pem"), keystore) + if err != nil { + t.Fatalf("Failed to copy ec256-1-key.pem to %s: %s", keystore, err.Error()) + } + + id := new(mocks.Identity) + client := new(mocks.Client) + opts := &factory.FactoryOpts{ + SwOpts: &factory.SwOpts{ + HashFamily: "SHA2", + SecLevel: 256, + FileKeystore: &factory.FileKeystoreOpts{ + KeyStorePath: "msp/keystore", + }, + }, + } + bccsp, err := util.InitBCCSP(&opts, filepath.Join(clientHome, "msp/keystore"), clientHome) + if err != nil { + t.Fatalf("Failed initialize BCCSP: %s", err.Error()) + } + client.On("GetCSP").Return(bccsp) + client.On("GetCSP").Return(nil) + certFile := filepath.Join(clientHome, "ec256-1-cert.pem") + cert, err := readCert(certFile) + if err != nil { + t.Fatalf("Failed to read the cert: %s", err.Error()) + } + x509Cred := NewCredential(certFile, keystore, client) + err = x509Cred.Load() + if err != nil { + t.Fatalf("Should not fail to load x509 credential as cert exists and key is in bccsp keystore: %s", err.Error()) + } + name, err := x509Cred.EnrollmentID() + assert.NoError(t, err, "EnrollmentID() should not return an error") + client.On("NewX509Identity", name, []credential.Credential{x509Cred}).Return(id) + + serial := util.GetSerialAsHex(cert.SerialNumber) + aki := hex.EncodeToString(cert.AuthorityKeyId) + req := &api.RevocationRequest{ + Serial: serial, + AKI: aki, + } + id.On("Revoke", req).Return(&api.RevocationResponse{}, nil) + + _, err = x509Cred.RevokeSelf() + assert.NoError(t, err) + + body := []byte{} + httpReq, err := http.NewRequest("GET", "localhost:7054/enroll", bytes.NewReader(body)) + if err != nil { + t.Fatalf("Failed to create HTTP request: %s", err.Error()) + } + _, err = x509Cred.CreateOAuthToken(httpReq, body) + assert.NoError(t, err) +} + +func readCert(certFile string) (*x509.Certificate, error) { + certBytes, err := util.ReadFile(certFile) + if err != nil { + return nil, err + } + cert, err := util.GetX509CertificateFromPEM(certBytes) + if err != nil { + return nil, err + } + return cert, nil +} diff --git a/lib/client/credential/x509/mocks/Client.go b/lib/client/credential/x509/mocks/Client.go new file mode 100644 index 000000000..adbedd85f --- /dev/null +++ b/lib/client/credential/x509/mocks/Client.go @@ -0,0 +1,50 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import bccsp "github.com/hyperledger/fabric/bccsp" +import credential "github.com/hyperledger/fabric-ca/lib/client/credential" +import mock "github.com/stretchr/testify/mock" +import x509 "github.com/hyperledger/fabric-ca/lib/client/credential/x509" + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// GetCSP provides a mock function with given fields: +func (_m *Client) GetCSP() bccsp.BCCSP { + ret := _m.Called() + + var r0 bccsp.BCCSP + if rf, ok := ret.Get(0).(func() bccsp.BCCSP); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(bccsp.BCCSP) + } + } + + return r0 +} + +// NewX509Identity provides a mock function with given fields: name, creds +func (_m *Client) NewX509Identity(name string, creds []credential.Credential) x509.Identity { + ret := _m.Called(name, creds) + + var r0 x509.Identity + if rf, ok := ret.Get(0).(func(string, []credential.Credential) x509.Identity); ok { + r0 = rf(name, creds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(x509.Identity) + } + } + + return r0 +} diff --git a/lib/client/credential/x509/mocks/Identity.go b/lib/client/credential/x509/mocks/Identity.go new file mode 100644 index 000000000..382c704d6 --- /dev/null +++ b/lib/client/credential/x509/mocks/Identity.go @@ -0,0 +1,39 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import api "github.com/hyperledger/fabric-ca/api" +import mock "github.com/stretchr/testify/mock" + +// Identity is an autogenerated mock type for the Identity type +type Identity struct { + mock.Mock +} + +// Revoke provides a mock function with given fields: req +func (_m *Identity) Revoke(req *api.RevocationRequest) (*api.RevocationResponse, error) { + ret := _m.Called(req) + + var r0 *api.RevocationResponse + if rf, ok := ret.Get(0).(func(*api.RevocationRequest) *api.RevocationResponse); ok { + r0 = rf(req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.RevocationResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*api.RevocationRequest) error); ok { + r1 = rf(req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/lib/client/credential/x509/signer.go b/lib/client/credential/x509/signer.go new file mode 100644 index 000000000..3e09dd390 --- /dev/null +++ b/lib/client/credential/x509/signer.go @@ -0,0 +1,76 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package x509 + +import ( + "crypto/x509" + "fmt" + + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/bccsp" + "github.com/hyperledger/fabric/common/attrmgr" + "github.com/pkg/errors" +) + +// NewSigner is constructor for Signer +func NewSigner(key bccsp.Key, cert []byte) (*Signer, error) { + s := &Signer{ + key: key, + certBytes: cert, + } + var err error + s.cert, err = util.GetX509CertificateFromPEM(s.certBytes) + if err != nil { + return nil, errors.WithMessage(err, "Failed to unmarshal X509 certificate bytes") + } + s.name = util.GetEnrollmentIDFromX509Certificate(s.cert) + return s, nil +} + +// Signer represents a signer +// Each identity may have multiple signers, currently one ecert and multiple tcerts +type Signer struct { + // Private key + key bccsp.Key + // Certificate bytes + certBytes []byte + // X509 certificate that is constructed from the cert bytes associated with this signer + cert *x509.Certificate + // Common name from the certificate associated with this signer + name string +} + +// Key returns the key bytes of this signer +func (s *Signer) Key() bccsp.Key { + return s.key +} + +// Cert returns the cert bytes of this signer +func (s *Signer) Cert() []byte { + return s.certBytes +} + +// GetX509Cert returns the X509 certificate for this signer +func (s *Signer) GetX509Cert() *x509.Certificate { + return s.cert +} + +// GetName returns common name that is retrieved from the Subject of the certificate +// associated with this signer +func (s *Signer) GetName() string { + return s.name +} + +// Attributes returns the attributes that are in the certificate +func (s *Signer) Attributes() (*attrmgr.Attributes, error) { + cert := s.GetX509Cert() + attrs, err := attrmgr.New().GetAttributesFromCert(cert) + if err != nil { + return nil, fmt.Errorf("Failed getting attributes for '%s': %s", s.name, err) + } + return attrs, nil +} diff --git a/lib/client/credential/x509/signer_test.go b/lib/client/credential/x509/signer_test.go new file mode 100644 index 000000000..d4d8cdb44 --- /dev/null +++ b/lib/client/credential/x509/signer_test.go @@ -0,0 +1,36 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package x509_test + +import ( + "path/filepath" + "testing" + + . "github.com/hyperledger/fabric-ca/lib/client/credential/x509" + "github.com/hyperledger/fabric-ca/util" + "github.com/stretchr/testify/assert" +) + +func TestNewSignerError(t *testing.T) { + _, err := NewSigner(nil, []byte{}) + assert.Error(t, err, "NewSigner should return an error if cert byte array is empty") +} + +func TestNewSigner(t *testing.T) { + certBytes, err := util.ReadFile(filepath.Join(testDataDir, "ec256-1-cert.pem")) + if err != nil { + t.Fatalf("Failed to read the cert: %s", err.Error()) + } + signer, err := NewSigner(nil, certBytes) + assert.NoError(t, err, "NewSigner should not return an error if cert bytes are valid") + + assert.NotNil(t, signer.GetX509Cert()) + assert.Nil(t, signer.Key()) + assert.NotEmpty(t, signer.GetName()) + _, err = signer.Attributes() + assert.NoError(t, err) +} diff --git a/lib/client_test.go b/lib/client_test.go index 9f6cffbb3..ad62511ad 100644 --- a/lib/client_test.go +++ b/lib/client_test.go @@ -1,17 +1,7 @@ /* -Copyright IBM Corp. 2016 All Rights Reserved. +Copyright IBM Corp. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +SPDX-License-Identifier: Apache-2.0 */ package lib_test @@ -55,7 +45,7 @@ const ( DefaultCA = "" ) -func TestCLIClientConfigStat(t *testing.T) { +func TestClientConfigStat(t *testing.T) { wd, err := os.Getwd() if err != nil { t.Fatalf("failed to get cwd: %s", err) @@ -104,7 +94,7 @@ func TestCLIClientConfigStat(t *testing.T) { } } -func TestCLIClientInit(t *testing.T) { +func TestClientInit(t *testing.T) { client := new(Client) client.Config = new(ClientConfig) client.Config.MSPDir = string(make([]byte, 1)) @@ -155,7 +145,101 @@ func TestCLIClientInit(t *testing.T) { } } -func TestCLIClient(t *testing.T) { +func TestIdemixEnroll(t *testing.T) { + srvHome, err := ioutil.TempDir(testdataDir, "idemixenrollsrv") + if err != nil { + t.Fatal("Failed to create server home directory") + } + clientHome, err := ioutil.TempDir(testdataDir, "idemixenrollclient") + if err != nil { + t.Fatal("Failed to create server home directory") + } + + server := TestGetServer(ctport1, srvHome, "", 5, t) + if server == nil { + t.Fatal("Failed to create test server") + } + err = server.Start() + if err != nil { + t.Fatalf("Failed to start server: %s", err) + } + stopserver := true + defer func() { + if stopserver { + err = server.Stop() + if err != nil { + t.Errorf("Failed to stop server: %s", err) + } + } + os.RemoveAll(srvHome) + os.RemoveAll(clientHome) + }() + + client := &Client{ + Config: &ClientConfig{URL: fmt.Sprintf("http://localhost:%d", ctport1)}, + HomeDir: clientHome, + } + + cainfo, err := client.GetCAInfo(&api.GetCAInfoRequest{}) + if err != nil { + t.Fatalf("Failed to get CA info: %s", err) + } + err = util.WriteFile(filepath.Join(clientHome, "msp/IssuerPublicKey"), cainfo.IssuerPublicKey, 0644) + if err != nil { + t.Fatalf("Failed to store CA's idemix public key: %s", err) + } + + req := &api.EnrollmentRequest{ + Type: "idemix", + } + + _, err = client.Enroll(req) + assert.Error(t, err, "Idemix enroll should have failed as no user id and secret are not specified in the enrollment request") + + req.Name = "admin" + req.Secret = "adminpw1" + assert.Error(t, err, "Idemix enroll should have failed as secret is incorrect in the enrollment request") + + req.Secret = "adminpw" + idemixEnrollRes, err := client.Enroll(req) + assert.NoError(t, err, "Idemix enroll should not have failed with valid userid/password") + idemixCred := idemixEnrollRes.Identity.GetIdemixCredential() + err = idemixCred.Store() + if err != nil { + t.Fatalf("Failed to store idemix cred") + } + + req.Type = "x509" + enrollRes, err := client.Enroll(req) + assert.NoError(t, err, "X509 enroll should not fail") + + err = enrollRes.Identity.Store() + if err != nil { + t.Fatalf("Failed to store X509 credential: %s", err) + } + + req.Type = "idemix" + req.Name = "" + req.Secret = "" + enrollRes, err = client.Enroll(req) + assert.NoError(t, err, "Idemix enroll should not have failed with valid x509 enrollment certificate") + err = enrollRes.Identity.Store() + if err != nil { + t.Fatalf("Failed to store idenditity: %s", err.Error()) + } + _, err = client.LoadIdentity("", filepath.Join(clientHome, "msp/signcerts/cert.pem"), filepath.Join(clientHome, "msp/user/SignerConfig")) + assert.NoError(t, err, "Failed to load identity that has both X509 and Idemix credentials") + + err = client.CheckEnrollment() + assert.NoError(t, err, "CheckEnrollment should not return an error") + + CopyFile("../testdata/ec256-1-cert.pem", filepath.Join(clientHome, "msp/signcerts/cert.pem")) + CopyFile("../testdata/ec256-1-key.pem", filepath.Join(clientHome, "msp/keystore/key.pem")) + _, err = client.Enroll(req) + assert.Error(t, err, "Idemix enroll should fail as the certificate is of unregistered user") +} + +func TestClient(t *testing.T) { server := TestGetServer(ctport1, path.Join(serversDir, "c1"), "", 1, t) if server == nil { return @@ -553,7 +637,8 @@ func testRevocation(c *Client, t *testing.T, user string, withPriv, ecertOnly bo id := eresp.Identity var revResp *api.RevocationResponse if ecertOnly { - revResp, err = id.GetECert().RevokeSelf() + x509Cred := id.GetX509Credential() + revResp, err = x509Cred.RevokeSelf() } else { revResp, err = id.RevokeSelf() } @@ -565,8 +650,8 @@ func testRevocation(c *Client, t *testing.T, user string, withPriv, ecertOnly bo } // Assert that the cert serial in the revocation response is same as that of user certificate - cert, err := id.GetECert().GetX509Cert() - if err != nil { + cert := id.GetECert().GetX509Cert() + if cert == nil { t.Fatalf("Failed to get certificate for the enrolled user %s: %s", user, err) } assert.Equal(t, 1, len(revResp.RevokedCerts), "Expected 1 certificate to be revoked") @@ -746,21 +831,21 @@ func testLoadBadCSRInfo(c *Client, t *testing.T) { } func testLoadIdentity(c *Client, t *testing.T) { - _, err := c.LoadIdentity("foo", "bar") + _, err := c.LoadIdentity("foo", "bar", "rab") if err == nil { - t.Error("testLoadIdentity foo/bar passed but should have failed") + t.Error("testLoadIdentity foo/bar/rab passed but should have failed") } - _, err = c.LoadIdentity("foo", "../testdata/ec.pem") + _, err = c.LoadIdentity("foo", "../testdata/ec.pem", "bar") if err == nil { t.Error("testLoadIdentity foo passed but should have failed") } - _, err = c.LoadIdentity("../testdata/ec-key.pem", "../testdata/ec.pem") + _, err = c.LoadIdentity("../testdata/ec-key.pem", "../testdata/ec.pem", "bar") if err != nil { t.Errorf("testLoadIdentity failed: %s", err) } } -func TestCLICustomizableMaxEnroll(t *testing.T) { +func TestCustomizableMaxEnroll(t *testing.T) { srv := TestGetServer(ctport2, path.Join(serversDir, "c2"), "", 3, t) if srv == nil { return @@ -1095,7 +1180,7 @@ func setupGenCRLTest(t *testing.T, serverHome, clientHome string) (*Server, *Ide return server, adminID } -func TestCLINormalizeUrl(t *testing.T) { +func TestNormalizeUrl(t *testing.T) { u, err := NormalizeURL("") if err != nil { t.Errorf("normalizeURL empty: %s", err) @@ -1137,7 +1222,7 @@ func TestCLINormalizeUrl(t *testing.T) { } } -func TestCLISendBadPost(t *testing.T) { +func TestSendBadPost(t *testing.T) { c := new(Client) c.Config = new(ClientConfig) @@ -1152,7 +1237,7 @@ func TestCLISendBadPost(t *testing.T) { } // Test to make sure that CSR is generated by GenCSR function -func TestCLIGenCSR(t *testing.T) { +func TestGenCSR(t *testing.T) { config := new(ClientConfig) homeDir := filepath.Join(testdataDir, "identity") @@ -1222,10 +1307,14 @@ func TestCLIGenCSR(t *testing.T) { // Test to make sure that once an identity is revoked, all subsequent commands // invoked by revoked user should be rejected by server for all its issued certificates func TestRevokedIdentity(t *testing.T) { + testHome, err := ioutil.TempDir(".", "revoketesthome") + if err != nil { + t.Fatalf("Failed to create temp directory: %s", err.Error()) + } serverdir := filepath.Join(testdataDir, "server") srv := TestGetServer(ctport1, serverdir, "", -1, t) - err := srv.Start() + err = srv.Start() if err != nil { t.Fatalf("Failed to start server: %s", err) } @@ -1238,7 +1327,7 @@ func TestRevokedIdentity(t *testing.T) { if err != nil { t.Errorf("RemoveAll failed: %s", err) } - err = os.RemoveAll("client") + err = os.RemoveAll(testHome) if err != nil { t.Errorf("RemoveAll failed: %s", err) } @@ -1247,7 +1336,7 @@ func TestRevokedIdentity(t *testing.T) { // Enroll admin c := &Client{ Config: &ClientConfig{URL: fmt.Sprintf("http://localhost:%d", ctport1)}, - HomeDir: "client/admin", + HomeDir: filepath.Join(testHome, "admin"), } enrollReq := &api.EnrollmentRequest{ @@ -1278,7 +1367,7 @@ func TestRevokedIdentity(t *testing.T) { // Enroll 'TestUser' TestUserClient := &Client{ Config: &ClientConfig{URL: fmt.Sprintf("http://localhost:%d", ctport1)}, - HomeDir: "client/TestUserClient", + HomeDir: filepath.Join(testHome, "TestUserClient"), } enrollReq = &api.EnrollmentRequest{ @@ -1296,7 +1385,7 @@ func TestRevokedIdentity(t *testing.T) { // Enroll 'TestUser' again with a different home/msp directory TestUserClient2 := &Client{ Config: &ClientConfig{URL: fmt.Sprintf("http://localhost:%d", ctport1)}, - HomeDir: "client/TestUserClient2", + HomeDir: filepath.Join(testHome, "TestUserClient2"), } enrollReq = &api.EnrollmentRequest{ diff --git a/lib/client_whitebox_test.go b/lib/client_whitebox_test.go index 3803b240a..edf4ac2bf 100644 --- a/lib/client_whitebox_test.go +++ b/lib/client_whitebox_test.go @@ -8,7 +8,6 @@ package lib import ( "crypto/rand" "crypto/x509" - "encoding/pem" "fmt" "io/ioutil" "net/http" @@ -20,6 +19,7 @@ import ( "github.com/cloudflare/cfssl/log" "github.com/cloudflare/cfssl/signer" "github.com/hyperledger/fabric-ca/api" + cax509 "github.com/hyperledger/fabric-ca/lib/client/credential/x509" "github.com/hyperledger/fabric-ca/lib/common" "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/bccsp" @@ -302,12 +302,9 @@ func testImpersonation(id *Identity, t *testing.T) { if err != nil { t.Fatalf("Failed to create self-signed fake cert: %s", err) } - fakeCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: fakeCertBytes}) - fakeID := newIdentity(id.GetClient(), "admin", privateKey, fakeCert) - _, err = fakeID.RevokeSelf() - t.Logf("fakeID.RevokeSelf: %v", err) + _, err = cax509.NewSigner(privateKey, fakeCertBytes) if err == nil { - t.Fatalf("Fake ID should have failed revocation") + t.Fatalf("Should have failed to create signer with fake certificate") } } diff --git a/lib/identity.go b/lib/identity.go index 2dd8f4248..6dcd25df6 100644 --- a/lib/identity.go +++ b/lib/identity.go @@ -16,30 +16,27 @@ import ( "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib/client/credential" + "github.com/hyperledger/fabric-ca/lib/client/credential/idemix" + "github.com/hyperledger/fabric-ca/lib/client/credential/x509" "github.com/hyperledger/fabric-ca/lib/common" "github.com/hyperledger/fabric-ca/util" - "github.com/hyperledger/fabric/bccsp" ) -func newIdentity(client *Client, name string, key bccsp.Key, cert []byte) *Identity { - id := new(Identity) - id.name = name - id.ecert = newSigner(key, cert, id) - id.client = client - if client != nil { - id.CSP = client.csp - } else { - id.CSP = util.GetDefaultBCCSP() - } - return id -} - // Identity is fabric-ca's implementation of an identity type Identity struct { name string - ecert *Signer client *Client - CSP bccsp.BCCSP + creds []credential.Credential +} + +// NewIdentity is the constructor for identity +func NewIdentity(client *Client, name string, creds []credential.Credential) *Identity { + id := new(Identity) + id.name = name + id.client = client + id.creds = creds + return id } // GetName returns the identity name @@ -52,13 +49,43 @@ func (i *Identity) GetClient() *Client { return i.client } +// GetIdemixCredential returns Idemix credential of this identity +func (i *Identity) GetIdemixCredential() credential.Credential { + for _, cred := range i.creds { + if cred.Type() == idemix.CredType { + return cred + } + } + return nil +} + +// GetX509Credential returns X509 credential of this identity +func (i *Identity) GetX509Credential() credential.Credential { + for _, cred := range i.creds { + if cred.Type() == x509.CredType { + return cred + } + } + return nil +} + // GetECert returns the enrollment certificate signer for this identity -func (i *Identity) GetECert() *Signer { - return i.ecert +// Returns nil if the identity does not have a X509 credential +func (i *Identity) GetECert() *x509.Signer { + for _, cred := range i.creds { + if cred.Type() == x509.CredType { + v, _ := cred.Val() + if v != nil { + s, _ := v.(*x509.Signer) + return s + } + } + } + return nil } // GetTCertBatch returns a batch of TCerts for this identity -func (i *Identity) GetTCertBatch(req *api.GetTCertBatchRequest) ([]*Signer, error) { +func (i *Identity) GetTCertBatch(req *api.GetTCertBatchRequest) ([]*x509.Signer, error) { reqBody, err := util.Marshal(req, "GetTCertBatchRequest") if err != nil { return nil, err @@ -425,7 +452,13 @@ func (i *Identity) Store() error { if i.client == nil { return errors.New("An identity with no client may not be stored") } - return i.client.StoreMyIdentity(i.ecert.cert) + for _, cred := range i.creds { + err := cred.Store() + if err != nil { + return err + } + } + return nil } // Get sends a get request to an endpoint @@ -523,11 +556,15 @@ func (i *Identity) Post(endpoint string, reqBody []byte, result interface{}, que func (i *Identity) addTokenAuthHdr(req *http.Request, body []byte) error { log.Debug("Adding token-based authorization header") - cert := i.ecert.cert - key := i.ecert.key - token, err := util.CreateToken(i.CSP, cert, key, req.Method, req.URL.RequestURI(), body) - if err != nil { - return errors.WithMessage(err, "Failed to add token authorization header") + var token string + var err error + for _, cred := range i.creds { + if cred.Type() == x509.CredType { + token, err = cred.CreateOAuthToken(req, body) + if err != nil { + return errors.WithMessage(err, "Failed to add token authorization header") + } + } } req.Header.Set("authorization", token) return nil diff --git a/lib/identity_test.go b/lib/identity_test.go index 83f22cedb..2efe6ec6b 100644 --- a/lib/identity_test.go +++ b/lib/identity_test.go @@ -1,40 +1,32 @@ /* -Copyright IBM Corp. 2016 All Rights Reserved. +Copyright IBM Corp. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +SPDX-License-Identifier: Apache-2.0 */ package lib import ( - "io/ioutil" "testing" "github.com/hyperledger/fabric-ca/api" - "github.com/hyperledger/fabric-ca/util" - "github.com/hyperledger/fabric/bccsp/factory" + clientcred "github.com/hyperledger/fabric-ca/lib/client/credential" + "github.com/hyperledger/fabric-ca/lib/client/credential/x509" "github.com/stretchr/testify/assert" ) -func getIdentity() *Identity { - key, _ := util.ImportBCCSPKeyFromPEM("../tesdata/ec-key.pem", factory.GetDefault(), true) - cert, _ := ioutil.ReadFile("../tesdata/ec.pem") - id := newIdentity(nil, "test", key, cert) +func getIdentity(t *testing.T) *Identity { + cred := x509.NewCredential("../testdata/ec.pem", "../testdata/ec-key.pem", nil) + err := cred.Load() + if err != nil { + t.Fatalf("Failed to load credential from non existant file ../tesdata/ec.pem: %s", err.Error()) + } + id := NewIdentity(nil, "test", []clientcred.Credential{cred}) return id } func TestIdentity(t *testing.T) { - id := getIdentity() + id := getIdentity(t) testGetName(id, t) testGetECert(id, t) } @@ -71,7 +63,7 @@ func testGetECert(id *Identity, t *testing.T) { } func TestGetCertificatesErr(t *testing.T) { - id := getIdentity() + id := getIdentity(t) id.client = &Client{ Config: &ClientConfig{}, } diff --git a/lib/server.go b/lib/server.go index 52c66b4ea..9eb746c75 100644 --- a/lib/server.go +++ b/lib/server.go @@ -1,17 +1,7 @@ /* -Copyright IBM Corp. 2017 All Rights Reserved. +Copyright IBM Corp. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +SPDX-License-Identifier: Apache-2.0 */ package lib @@ -464,6 +454,7 @@ func (s *Server) registerHandlers() { s.registerHandler("cainfo", newCAInfoEndpoint(s)) s.registerHandler("register", newRegisterEndpoint(s)) s.registerHandler("enroll", newEnrollEndpoint(s)) + s.registerHandler("idemix/credential", newIdemixEnrollEndpoint(s)) s.registerHandler("reenroll", newReenrollEndpoint(s)) s.registerHandler("revoke", newRevokeEndpoint(s)) s.registerHandler("tcert", newTCertEndpoint(s)) diff --git a/lib/server_test.go b/lib/server_test.go index 617a44530..25a8e5947 100644 --- a/lib/server_test.go +++ b/lib/server_test.go @@ -1,17 +1,7 @@ /* -Copyright IBM Corp. 2017 All Rights Reserved. +Copyright IBM Corp. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +SPDX-License-Identifier: Apache-2.0 */ package lib_test @@ -223,13 +213,12 @@ func TestSRVRootServer(t *testing.T) { } user1 = eresp.Identity // Make sure the OUs are correct based on the identity type and affiliation - cert, err := user1.GetECert().GetX509Cert() - if err != nil { - assert.NoErrorf(t, err, "Failed to get user1's enrollment certificate") - } else { - ouPath := strings.Join(cert.Subject.OrganizationalUnit, ".") - assert.Equal(t, "user.hyperledger.fabric.security", ouPath, "Invalid OU path in certificate") - } + cert := user1.GetECert().GetX509Cert() + assert.NotNil(t, cert, "Failed to get user1's enrollment certificate") + + ouPath := strings.Join(cert.Subject.OrganizationalUnit, ".") + assert.Equal(t, "user.hyperledger.fabric.security", ouPath, "Invalid OU path in certificate") + // The admin ID should have 1 cert in the DB now dba := server.CA.CertDBAccessor() recs, err = dba.GetCertificatesByID("admin") diff --git a/lib/signer.go b/lib/signer.go deleted file mode 100644 index f8dc9d14a..000000000 --- a/lib/signer.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright IBM Corp. 2016 All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package lib - -import ( - "crypto/x509" - "fmt" - - "github.com/cloudflare/cfssl/log" - "github.com/hyperledger/fabric-ca/api" - "github.com/hyperledger/fabric-ca/util" - "github.com/hyperledger/fabric/bccsp" - "github.com/hyperledger/fabric/common/attrmgr" -) - -func newSigner(key bccsp.Key, cert []byte, id *Identity) *Signer { - return &Signer{ - key: key, - cert: cert, - id: id, - client: id.client, - } -} - -// Signer represents a signer -// Each identity may have multiple signers, currently one ecert and multiple tcerts -type Signer struct { - key bccsp.Key - cert []byte - id *Identity - client *Client -} - -// Key returns the key bytes of this signer -func (s *Signer) Key() bccsp.Key { - return s.key -} - -// Cert returns the cert bytes of this signer -func (s *Signer) Cert() []byte { - return s.cert -} - -// GetX509Cert returns the X509 certificate for this signer -func (s *Signer) GetX509Cert() (*x509.Certificate, error) { - cert, err := util.GetX509CertificateFromPEM(s.cert) - if err != nil { - return nil, fmt.Errorf("Failed getting X509 certificate for '%s': %s", s.id.name, err) - } - return cert, nil -} - -// RevokeSelf revokes only the certificate associated with this signer -func (s *Signer) RevokeSelf() (*api.RevocationResponse, error) { - log.Debugf("RevokeSelf %s", s.id.name) - serial, aki, err := GetCertID(s.cert) - if err != nil { - return nil, err - } - req := &api.RevocationRequest{ - Serial: serial, - AKI: aki, - } - return s.id.Revoke(req) -} - -// Attributes returns the attributes that are in the certificate -func (s *Signer) Attributes() (*attrmgr.Attributes, error) { - cert, err := s.GetX509Cert() - if err != nil { - return nil, fmt.Errorf("Failed getting attributes for '%s': %s", s.id.name, err) - } - attrs, err := attrmgr.New().GetAttributesFromCert(cert) - if err != nil { - return nil, fmt.Errorf("Failed getting attributes for '%s': %s", s.id.name, err) - } - return attrs, nil -} diff --git a/scripts/run_unit_tests b/scripts/run_unit_tests index 71c4b8299..68255ff41 100755 --- a/scripts/run_unit_tests +++ b/scripts/run_unit_tests @@ -13,9 +13,10 @@ export PATH=$PATH:$GOPATH/bin go get github.com/axw/gocov/... go get github.com/AlekSi/gocov-xml -# Skipping /lib/common package as there is only file that contains request/response structs used by both client and server. It needs to be removed -# when code is added to this paackage -PKGS=`go list github.com/hyperledger/fabric-ca/... | grep -Ev '/vendor/|/api|/dbutil|/ldap|/mocks|/test/fabric-ca-load-tester|/integration|/fabric-ca-client$|/lib/common$'` +# Skipping /lib/common package as there is only one file that contains request/response structs used by both client and server. It needs to be removed from exclude package list +# when code is added to this package +# Skipping credential package as there is only one file that contains Credential interface definition. It needs to be removed from exclude package list when code is added to this package +PKGS=`go list github.com/hyperledger/fabric-ca/... | grep -Ev '/vendor/|/api|/dbutil|/ldap|/mocks|/test/fabric-ca-load-tester|/integration|/fabric-ca-client$|/credential$|/lib/common$'` gocov test -timeout 15m $PKGS | gocov-xml > coverage.xml diff --git a/testdata/IdemixSignerConfig b/testdata/IdemixSignerConfig new file mode 100644 index 000000000..bf12a21cf --- /dev/null +++ b/testdata/IdemixSignerConfig @@ -0,0 +1 @@ +{"Cred":"CkQKIOH4gt5kerso36Fgh4leTfu/W/+e4K1hpoa4/4xaN9AlEiC4lGrimBXw7mHTEoqcDv8m/4OhzGnREaL9/WJz/UNUIxJECiCYKKapWMiFeeP9SLYGPzjJRWRnZuYt/e+uULXU3OATgBIgw23Am9ERqoWZzNUQxh9HPRmZBTU/WWa2WVnDAtLF8JIaIAiHHF3HXshKrPCUPzqbDKDN5jnggLK14FSQPQuZ2rgGIiDmsif/DFb9BldlmCCCwGZSoz65O8GVBQH1XYROK3Q7Jyog47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUqIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKiCMaXbltUEEFb3pCL1N7hXfsWepyHP8S7ioH28qtEipGCogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=","Sk":"XcWzoDdRO/ShAOKHr3NjynxwStqY60t7hsx7WVY/vws=","enrollment_id":"admin"} \ No newline at end of file