From 1d632b8c8dd2811b5ea7459d1469d95ddea7ff05 Mon Sep 17 00:00:00 2001 From: Anil Ambati Date: Wed, 2 May 2018 17:31:41 -0400 Subject: [PATCH] [FAB-9244] 2.Server changes to get Idemix credential This change set contains server side changes for getting an Idemix credential. Changes include: 1. New package lib/server/idemix that contains handler for /v1/api/idemix/credential endpoint, revocation component, issuer credential, and db accessor for credential and revocation component info 2. Mocks for all the new interfaces 3. Associated changes in the lib package Change-Id: Icd7f8d7a316a184010278e1f394d78b42cad5482 Signed-off-by: Anil Ambati --- docs/source/servercli.rst | 1 + lib/ca.go | 96 ++++-- lib/caconfig.go | 10 +- lib/certdbaccessor.go | 12 +- lib/client.go | 4 +- lib/client_test.go | 3 +- lib/dasqlite_test.go | 10 +- lib/dbaccessor.go | 13 +- lib/dbutil/dbutil.go | 134 ++++++-- lib/dbutil/mocks/FabricCADB.go | 114 +++++++ lib/dbutil/mocks/FabricCATx.go | 111 ++++++ lib/dbutil/mocks/Result.go | 67 ++++ lib/issuercredential.go | 119 ------- lib/server/idemix/creddbaccessor.go | 167 +++++++++ lib/server/idemix/creddbaccessor_test.go | 177 ++++++++++ lib/server/idemix/enrollhandler.go | 252 ++++++++++++++ lib/server/idemix/enrollhandler_test.go | 285 ++++++++++++++++ lib/server/idemix/idemixlib.go | 52 +++ lib/server/idemix/issuercredential.go | 174 ++++++++++ lib/server/idemix/issuercredential_test.go | 234 +++++++++++++ lib/server/idemix/mocks/CA.go | 122 +++++++ lib/server/idemix/mocks/CredDBAccessor.go | 86 +++++ lib/server/idemix/mocks/IssuerCredential.go | 106 ++++++ lib/server/idemix/mocks/Lib.go | 114 +++++++ .../idemix/mocks/RevocationComponent.go | 49 +++ lib/server/idemix/mocks/ServerRequestCtx.go | 129 +++++++ lib/server/idemix/mocks/User.go | 214 ++++++++++++ lib/server/idemix/revocationcomponent.go | 213 ++++++++++++ lib/server/idemix/revocationcomponent_test.go | 321 ++++++++++++++++++ lib/serverenroll.go | 2 +- lib/serveridemixenroll.go | 99 ++++++ lib/serverinfo.go | 6 +- testdata/IdemixPublicKey | Bin 0 -> 843 bytes testdata/IdemixSecretKey | 1 + 34 files changed, 3294 insertions(+), 203 deletions(-) create mode 100644 lib/dbutil/mocks/FabricCADB.go create mode 100644 lib/dbutil/mocks/FabricCATx.go create mode 100644 lib/dbutil/mocks/Result.go delete mode 100644 lib/issuercredential.go create mode 100644 lib/server/idemix/creddbaccessor.go create mode 100644 lib/server/idemix/creddbaccessor_test.go create mode 100644 lib/server/idemix/enrollhandler.go create mode 100644 lib/server/idemix/enrollhandler_test.go create mode 100644 lib/server/idemix/idemixlib.go create mode 100644 lib/server/idemix/issuercredential.go create mode 100644 lib/server/idemix/issuercredential_test.go create mode 100644 lib/server/idemix/mocks/CA.go create mode 100644 lib/server/idemix/mocks/CredDBAccessor.go create mode 100644 lib/server/idemix/mocks/IssuerCredential.go create mode 100644 lib/server/idemix/mocks/Lib.go create mode 100644 lib/server/idemix/mocks/RevocationComponent.go create mode 100644 lib/server/idemix/mocks/ServerRequestCtx.go create mode 100644 lib/server/idemix/mocks/User.go create mode 100644 lib/server/idemix/revocationcomponent.go create mode 100644 lib/server/idemix/revocationcomponent_test.go create mode 100644 lib/serveridemixenroll.go create mode 100644 testdata/IdemixPublicKey create mode 100644 testdata/IdemixSecretKey diff --git a/docs/source/servercli.rst b/docs/source/servercli.rst index 5b6d22988..aeaf30417 100644 --- a/docs/source/servercli.rst +++ b/docs/source/servercli.rst @@ -26,6 +26,7 @@ Fabric-CA Server's CLI --cacount int Number of non-default CA instances --cafiles stringSlice A list of comma-separated CA configuration files --cfg.affiliations.allowremove Enables removal of affiliations dynamically + --cfg.idemix.revocationhandlepoolsize int Specifies revocation handle pool size (default 100) --cfg.identities.allowremove Enables removal of identities dynamically --crl.expiry duration Expiration for the CRL generated by the gencrl request (default 24h0m0s) --crlsizelimit int Size limit of an acceptable CRL in bytes (default 512000) diff --git a/lib/ca.go b/lib/ca.go index b3923c77a..1eb9eb6a5 100644 --- a/lib/ca.go +++ b/lib/ca.go @@ -22,8 +22,6 @@ import ( "sync" "time" - "github.com/hyperledger/fabric/idemix" - "github.com/cloudflare/cfssl/config" cfcsr "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/initca" @@ -31,17 +29,18 @@ import ( "github.com/cloudflare/cfssl/signer" cflocalsigner "github.com/cloudflare/cfssl/signer/local" proto "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric-amcl/amcl" "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib/dbutil" "github.com/hyperledger/fabric-ca/lib/ldap" "github.com/hyperledger/fabric-ca/lib/metadata" + idemix "github.com/hyperledger/fabric-ca/lib/server/idemix" "github.com/hyperledger/fabric-ca/lib/spi" "github.com/hyperledger/fabric-ca/lib/tcert" "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/bccsp" "github.com/hyperledger/fabric/common/attrmgr" - "github.com/jmoiron/sqlx" "github.com/pkg/errors" ) @@ -78,17 +77,22 @@ type CA struct { // The database handle used to store certificates and optionally // the user registry information, unless LDAP it enabled for the // user registry function. - db *sqlx.DB + db *dbutil.DB // The crypto service provider (BCCSP) csp bccsp.BCCSP // The certificate DB accessor certDBAccessor *CertDBAccessor + // The Idemix credential DB accessor + credDBAccessor idemix.CredDBAccessor // The user registry registry spi.UserRegistry // The signer used for enrollment enrollSigner signer.Signer // idemix issuer credential for the CA - issuerCred IssuerCredential + issuerCred idemix.IssuerCredential + // A random number used in generation of Idemix nonces and credentials + idemixRand *amcl.RAND + rc idemix.RevocationComponent // The options to use in verifying a signature in token-based authentication verifyOptions *x509.VerifyOptions // The attribute manager @@ -278,15 +282,24 @@ func (ca *CA) initKeyMaterial(renew bool) error { func (ca *CA) initIdemixKeyMaterial(renew bool) error { log.Debug("Initialize Idemix key material") + rng, err := idemix.NewLib().GetRand() + if err != nil { + return errors.Wrapf(err, "Error generating random number") + } + ca.idemixRand = rng + idemixPubKey := ca.Config.CA.IdemixPublicKeyfile idemixSecretKey := ca.Config.CA.IdemixSecretKeyfile - issuerCred := newIssuerCredential(idemixPubKey, idemixSecretKey) + issuerCred := idemix.NewCAIdemixCredential(idemixPubKey, idemixSecretKey, idemix.NewLib()) if !renew { pubKeyFileExists := util.FileExists(idemixPubKey) privKeyFileExists := util.FileExists(idemixSecretKey) // If they both exist, the CA was already initialized, load the keys from the disk if pubKeyFileExists && privKeyFileExists { + log.Info("The Idemix issuer public and secret key files already exist") + log.Infof(" secret key file location: %s", idemixSecretKey) + log.Infof(" public key file location: %s", idemixPubKey) err := issuerCred.Load() if err != nil { return err @@ -295,11 +308,11 @@ func (ca *CA) initIdemixKeyMaterial(renew bool) error { return nil } } - ik, err := ca.getNewIssuerKey() + ik, err := issuerCred.NewIssuerKey() if err != nil { return err } - log.Infof("The idemix public and secret keys were generated for CA %s", ca.Config.CA.Name) + log.Infof("The Idemix public and secret keys were generated for CA %s", ca.Config.CA.Name) issuerCred.SetIssuerKey(ik) err = issuerCred.Store() if err != nil { @@ -309,27 +322,6 @@ func (ca *CA) initIdemixKeyMaterial(renew bool) error { return nil } -func (ca *CA) getNewIssuerKey() (*idemix.IssuerKey, error) { - rng, err := idemix.GetRand() - if err != nil { - log.Errorf("Error getting rng: \"%s\"", err) - return nil, errors.Wrapf(err, "Error generating issuer key") - } - // Currently, Idemix library supports these four attributes. The supported attribute names - // must also be known when creating issuer key. In the future, Idemix library will support - // arbitary attribute names, so removing the need to hardcode attribute names in the issuer - // key. - // OU - organization unit - // id - enrollment ID of the user - // isAdmin - if the user is admin - // revocationHandle - revocation handle of a credential - ik, err := idemix.NewIssuerKey([]string{"OU", "id", "isAdmin", "revocationHandle"}, rng) - if err != nil { - return nil, err - } - return ik, nil -} - // Get the CA certificate for this CA func (ca *CA) getCACert() (cert []byte, err error) { if ca.Config.Intermediate.ParentServer.URL != "" { @@ -547,6 +539,9 @@ func (ca *CA) initConfig() (err error) { log.Level = log.LevelDebug } ca.normalizeStringSlices() + if ca.Config.Cfg.Idemix.RevocationHandlePoolSize == 0 { + ca.Config.Cfg.Idemix.RevocationHandlePoolSize = idemix.DefaultRevocationHandlePoolSize + } return nil } @@ -688,6 +683,12 @@ func (ca *CA) initDB() error { // Set the certificate DB accessor ca.certDBAccessor = NewCertDBAccessor(ca.db, ca.levels.Certificate) + ca.credDBAccessor = idemix.NewCredentialAccessor(ca.db, ca.levels.Credential) + ca.rc, err = idemix.NewRevocationComponent(ca, &ca.Config.Cfg.Idemix, ca.levels.RCInfo) + if err != nil { + return err + } + // If DB initialization fails and we need to reinitialize DB, need to make sure to set the DB accessor for the signer if ca.enrollSigner != nil { ca.enrollSigner.SetDBAccessor(ca.certDBAccessor) @@ -919,11 +920,38 @@ func (ca *CA) addAffiliation(path, parentPath string) error { return ca.registry.InsertAffiliation(path, parentPath, ca.levels.Affiliation) } -// GetIssuerCredential returns IssuerCredential of this CA -func (ca *CA) GetIssuerCredential() IssuerCredential { +// GetName returns name of this CA +func (ca *CA) GetName() string { + return ca.Config.CA.Name +} + +// IdemixRand returns random number used by this CA in generation of nonces +// and Idemix credentials +func (ca *CA) IdemixRand() *amcl.RAND { + return ca.idemixRand +} + +// IssuerCredential returns IssuerCredential of this CA +func (ca *CA) IssuerCredential() idemix.IssuerCredential { return ca.issuerCred } +// RevocationComponent returns revocation component of this CA +func (ca *CA) RevocationComponent() idemix.RevocationComponent { + return ca.rc +} + +// DB returns the FabricCADB object (which represents database handle +// to the CA database) associated with this CA +func (ca *CA) DB() dbutil.FabricCADB { + return ca.db +} + +// CredDBAccessor returns the Idemix credential DB accessor for CA +func (ca *CA) CredDBAccessor() idemix.CredDBAccessor { + return ca.credDBAccessor +} + // CertDBAccessor returns the certificate DB accessor for CA func (ca *CA) CertDBAccessor() *CertDBAccessor { return ca.certDBAccessor @@ -935,7 +963,7 @@ func (ca *CA) DBAccessor() spi.UserRegistry { } // GetDB returns pointer to database -func (ca *CA) GetDB() *sqlx.DB { +func (ca *CA) GetDB() *dbutil.DB { return ca.db } @@ -1048,8 +1076,8 @@ func (ca *CA) getUserAffiliation(username string) (string, error) { return aff, nil } -// Fill the CA info structure appropriately -func (ca *CA) fillCAInfo(info *serverInfoResponseNet) error { +// fillCAInfo fills the CA info structure appropriately +func (ca *CA) fillCAInfo(info *ServerInfoResponseNet) error { caChain, err := ca.getCAChain() if err != nil { return err diff --git a/lib/caconfig.go b/lib/caconfig.go index a09fb392e..a769811b0 100644 --- a/lib/caconfig.go +++ b/lib/caconfig.go @@ -23,10 +23,10 @@ import ( "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib/dbutil" "github.com/hyperledger/fabric-ca/lib/ldap" + "github.com/hyperledger/fabric-ca/lib/server/idemix" "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/bccsp/factory" - "github.com/hyperledger/fabric/idemix" ) const ( @@ -90,10 +90,9 @@ csr: // "skip" - to skip the field. type CAConfig struct { Version string `skip:"true"` - Cfg cfgOptions + Cfg CfgOptions CA CAInfo Signing *config.Signing - IssuerKey *idemix.IssuerKey CSR api.CSRInfo Registry CAConfigRegistry Affiliations map[string]interface{} @@ -107,10 +106,11 @@ type CAConfig struct { CRL CRLConfig } -// cfgOptions is a CA configuration that allows for setting different options -type cfgOptions struct { +// CfgOptions is a CA configuration that allows for setting different options +type CfgOptions struct { Identities identitiesOptions Affiliations affiliationsOptions + Idemix idemix.CfgOptions } // identitiesOptions are options that are related to identities diff --git a/lib/certdbaccessor.go b/lib/certdbaccessor.go index a60ea488f..bae175099 100644 --- a/lib/certdbaccessor.go +++ b/lib/certdbaccessor.go @@ -22,16 +22,16 @@ import ( "strings" "time" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/cloudflare/cfssl/certdb" certsql "github.com/cloudflare/cfssl/certdb/sql" "github.com/cloudflare/cfssl/log" + "github.com/hyperledger/fabric-ca/lib/dbutil" "github.com/hyperledger/fabric-ca/lib/server" "github.com/hyperledger/fabric-ca/util" "github.com/kisielk/sqlstruct" - - "github.com/jmoiron/sqlx" ) const ( @@ -68,14 +68,14 @@ type CertRecord struct { type CertDBAccessor struct { level int accessor certdb.Accessor - db *sqlx.DB + db *dbutil.DB } // NewCertDBAccessor returns a new Accessor. -func NewCertDBAccessor(db *sqlx.DB, level int) *CertDBAccessor { +func NewCertDBAccessor(db *dbutil.DB, level int) *CertDBAccessor { cffslAcc := new(CertDBAccessor) cffslAcc.db = db - cffslAcc.accessor = certsql.NewAccessor(db) + cffslAcc.accessor = certsql.NewAccessor(db.DB) cffslAcc.level = level return cffslAcc } @@ -88,7 +88,7 @@ func (d *CertDBAccessor) checkDB() error { } // SetDB changes the underlying sql.DB object Accessor is manipulating. -func (d *CertDBAccessor) SetDB(db *sqlx.DB) { +func (d *CertDBAccessor) SetDB(db *dbutil.DB) { d.db = db } diff --git a/lib/client.go b/lib/client.go index c17874bee..3057a7c7e 100644 --- a/lib/client.go +++ b/lib/client.go @@ -158,7 +158,7 @@ func (c *Client) GetCAInfo(req *api.GetCAInfoRequest) (*GetCAInfoResponse, error if err != nil { return nil, err } - netSI := &serverInfoResponseNet{} + netSI := &ServerInfoResponseNet{} err = c.SendReq(cainforeq, netSI) if err != nil { return nil, err @@ -172,7 +172,7 @@ func (c *Client) GetCAInfo(req *api.GetCAInfoRequest) (*GetCAInfoResponse, error } // Convert from network to local server information -func (c *Client) net2LocalServerInfo(net *serverInfoResponseNet, local *GetCAInfoResponse) error { +func (c *Client) net2LocalServerInfo(net *ServerInfoResponseNet, local *GetCAInfoResponse) error { caChain, err := util.B64Decode(net.CAChain) if err != nil { return err diff --git a/lib/client_test.go b/lib/client_test.go index 949775df9..9f6cffbb3 100644 --- a/lib/client_test.go +++ b/lib/client_test.go @@ -1313,7 +1313,8 @@ func TestRevokedIdentity(t *testing.T) { // 'admin' revokes user 'TestUser' revReq := &api.RevocationRequest{ - Name: "TestUser", + Name: "TestUser", + GenCRL: true, } _, err = admin_id.Revoke(revReq) diff --git a/lib/dasqlite_test.go b/lib/dasqlite_test.go index 1649e578f..f2c26de28 100644 --- a/lib/dasqlite_test.go +++ b/lib/dasqlite_test.go @@ -44,7 +44,7 @@ DELETE FROM affiliations; type TestAccessor struct { Accessor *Accessor - DB *sqlx.DB + DB *dbutil.DB } func (ta *TestAccessor) Truncate() { @@ -81,7 +81,7 @@ func TestSQLite(t *testing.T) { } // Truncate truncates the DB -func Truncate(db *sqlx.DB) { +func Truncate(db *dbutil.DB) { var sql []string sql = []string{sqliteTruncateTables} @@ -120,10 +120,10 @@ func TestDBCreation(t *testing.T) { testWithExistingDb(t) } -func createSQLiteDB(path string, t *testing.T) (*sqlx.DB, *TestAccessor) { - db, err := sqlx.Open("sqlite3", path) +func createSQLiteDB(path string, t *testing.T) (*dbutil.DB, *TestAccessor) { + sqlxdb, err := sqlx.Open("sqlite3", path) assert.NoError(t, err, "Failed to open SQLite database") - + db := &dbutil.DB{DB: sqlxdb} accessor := NewDBAccessor(db) ta := &TestAccessor{ diff --git a/lib/dbaccessor.go b/lib/dbaccessor.go index 1e3024852..ee32e1128 100644 --- a/lib/dbaccessor.go +++ b/lib/dbaccessor.go @@ -22,16 +22,17 @@ import ( "github.com/hyperledger/fabric-ca/lib/attr" "github.com/hyperledger/fabric-ca/util" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib/dbutil" "github.com/hyperledger/fabric-ca/lib/spi" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/ocsp" - "github.com/jmoiron/sqlx" "github.com/kisielk/sqlstruct" ) @@ -97,11 +98,11 @@ type AffiliationRecord struct { // Accessor implements db.Accessor interface. type Accessor struct { - db *sqlx.DB + db *dbutil.DB } // NewDBAccessor is a constructor for the database API -func NewDBAccessor(db *sqlx.DB) *Accessor { +func NewDBAccessor(db *dbutil.DB) *Accessor { return &Accessor{ db: db, } @@ -115,7 +116,7 @@ func (d *Accessor) checkDB() error { } // SetDB changes the underlying sql.DB object Accessor is manipulating. -func (d *Accessor) SetDB(db *sqlx.DB) { +func (d *Accessor) SetDB(db *dbutil.DB) { d.db = db } @@ -902,7 +903,7 @@ func (d *Accessor) getResult(ids []UserRecord, affs []AffiliationRecord) *spi.Db } // Creates a DBUser object from the DB user record -func newDBUser(userRec *UserRecord, db *sqlx.DB) *DBUser { +func newDBUser(userRec *UserRecord, db *dbutil.DB) *DBUser { var user = new(DBUser) user.Name = userRec.Name user.pass = userRec.Pass @@ -934,7 +935,7 @@ type DBUser struct { spi.UserInfo pass []byte attrs map[string]api.Attribute - db *sqlx.DB + db *dbutil.DB } // GetName returns the enrollment ID of the user diff --git a/lib/dbutil/dbutil.go b/lib/dbutil/dbutil.go index a90f6e215..e7082075c 100644 --- a/lib/dbutil/dbutil.go +++ b/lib/dbutil/dbutil.go @@ -17,6 +17,7 @@ limitations under the License. package dbutil import ( + "database/sql" "fmt" "path/filepath" "regexp" @@ -35,15 +36,49 @@ var ( dbURLRegex = regexp.MustCompile("(Datasource:\\s*)?(\\S+):(\\S+)@|(Datasource:.*\\s)?(user=\\S+).*\\s(password=\\S+)|(Datasource:.*\\s)?(password=\\S+).*\\s(user=\\S+)") ) +// FabricCADB is the interface with functions implemented by sqlx.DB +// object that are used by Fabric CA server +type FabricCADB interface { + Select(dest interface{}, query string, args ...interface{}) error + NamedExec(query string, arg interface{}) (sql.Result, error) + Rebind(query string) string + MustBegin() *sqlx.Tx + // BeginTx has same behavior as MustBegin except it returns FabricCATx + // instead of *sqlx.Tx + BeginTx() FabricCATx +} + +// FabricCATx is the interface with functions implemented by sqlx.Tx +// object that are used by Fabric CA server +type FabricCATx interface { + Select(dest interface{}, query string, args ...interface{}) error + Rebind(query string) string + Exec(query string, args ...interface{}) (sql.Result, error) + Commit() error + Rollback() error +} + +// DB is an adapter for sqlx.DB and implements FabricCADB interface +type DB struct { + *sqlx.DB +} + // Levels contains the levels of identities, affiliations, and certificates type Levels struct { Identity int Affiliation int Certificate int + Credential int + RCInfo int +} + +// BeginTx implements BeginTx method of FabricCADB interface +func (db *DB) BeginTx() FabricCATx { + return db.MustBegin() } // NewUserRegistrySQLLite3 returns a pointer to a sqlite database -func NewUserRegistrySQLLite3(datasource string) (*sqlx.DB, error) { +func NewUserRegistrySQLLite3(datasource string) (*DB, error) { log.Debugf("Using sqlite database, connect to database in home (%s) directory", datasource) err := createSQLiteDBTables(datasource) @@ -69,15 +104,16 @@ func NewUserRegistrySQLLite3(datasource string) (*sqlx.DB, error) { db.SetMaxOpenConns(1) log.Debug("Successfully opened sqlite3 DB") - return db, nil + return &DB{db}, nil } func createSQLiteDBTables(datasource string) error { log.Debugf("Creating SQLite database (%s) if it does not exist...", datasource) - db, err := sqlx.Open("sqlite3", datasource) + sqldb, err := sqlx.Open("sqlite3", datasource) if err != nil { return errors.Wrap(err, "Failed to open SQLite database") } + db := &DB{sqldb} defer db.Close() err = doTransaction(db, createAllSQLiteTables) @@ -89,7 +125,7 @@ func createSQLiteDBTables(datasource string) error { if _, err := db.Exec("CREATE TABLE IF NOT EXISTS properties (property VARCHAR(255), value VARCHAR(256), PRIMARY KEY(property))"); err != nil { return errors.Wrap(err, "Error creating properties table") } - _, err = db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0')")) + _, err = db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0'), ('credential.level', '0'), ('rcinfo.level', '0')")) if err != nil { if !strings.Contains(err.Error(), "UNIQUE constraint failed") { return errors.Wrap(err, "Failed to initialize properties table") @@ -111,6 +147,14 @@ func createAllSQLiteTables(tx *sqlx.Tx, args ...interface{}) error { if err != nil { return err } + err = createSQLiteCredentialsTable(tx) + if err != nil { + return err + } + err = createSQLiteRevocationComponentTable(tx) + if err != nil { + return err + } return nil } @@ -138,8 +182,24 @@ func createSQLiteCertificateTable(tx *sqlx.Tx) error { return nil } +func createSQLiteCredentialsTable(tx *sqlx.Tx) error { + log.Debug("Creating credentials table if it does not exist") + if _, err := tx.Exec("CREATE TABLE IF NOT EXISTS credentials (id VARCHAR(255), revocation_handle blob NOT NULL, cred blob NOT NULL, ca_label blob, status blob NOT NULL, reason int, expiry timestamp, revoked_at timestamp, level INTEGER DEFAULT 0, PRIMARY KEY(revocation_handle))"); err != nil { + return errors.Wrap(err, "Error creating credentials table") + } + return nil +} + +func createSQLiteRevocationComponentTable(tx *sqlx.Tx) error { + log.Debug("Creating revocation_component_info table if it does not exist") + if _, err := tx.Exec("CREATE TABLE IF NOT EXISTS revocation_component_info (epoch INTEGER, next_handle INTEGER, lasthandle_in_pool INTEGER, level INTEGER DEFAULT 0, PRIMARY KEY(epoch))"); err != nil { + return errors.Wrap(err, "Error creating revocation_component_info table") + } + return nil +} + // NewUserRegistryPostgres opens a connection to a postgres database -func NewUserRegistryPostgres(datasource string, clientTLSConfig *tls.ClientTLSConfig) (*sqlx.DB, error) { +func NewUserRegistryPostgres(datasource string, clientTLSConfig *tls.ClientTLSConfig) (*DB, error) { log.Debugf("Using postgres database, connecting to database...") dbName := getDBName(datasource) @@ -200,7 +260,7 @@ func NewUserRegistryPostgres(datasource string, clientTLSConfig *tls.ClientTLSCo return nil, errors.Wrap(err, "Failed to create Postgres tables") } - return db, nil + return &DB{db}, nil } func createPostgresDatabase(dbName string, db *sqlx.DB) error { @@ -231,11 +291,19 @@ func createPostgresTables(dbName string, db *sqlx.DB) error { if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number bytea NOT NULL, authority_key_identifier bytea NOT NULL, ca_label bytea, status bytea NOT NULL, reason int, expiry timestamp, revoked_at timestamp, pem bytea NOT NULL, level INTEGER DEFAULT 0, PRIMARY KEY(serial_number, authority_key_identifier))"); err != nil { return errors.Wrap(err, "Error creating certificates table") } + log.Debug("Creating credentials table if it does not exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS credentials (id VARCHAR(255), revocation_handle bytea NOT NULL, cred bytea NOT NULL, ca_label bytea, status bytea NOT NULL, reason int, expiry timestamp, revoked_at timestamp, level INTEGER DEFAULT 0, PRIMARY KEY(revocation_handle))"); err != nil { + return errors.Wrap(err, "Error creating certificates table") + } + log.Debug("Creating revocation_component_info table if it does not exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS revocation_component_info (epoch INTEGER, next_handle INTEGER, lasthandle_in_pool INTEGER, level INTEGER DEFAULT 0, PRIMARY KEY(epoch))"); err != nil { + return errors.Wrap(err, "Error creating revocation_component_info table") + } log.Debug("Creating properties table if it does not exist") if _, err := db.Exec("CREATE TABLE IF NOT EXISTS properties (property VARCHAR(255), value VARCHAR(256), PRIMARY KEY(property))"); err != nil { return errors.Wrap(err, "Error creating properties table") } - _, err := db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0')")) + _, err := db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0'), ('credential.level', '0'), ('rcinfo.level', '0')")) if err != nil { if !strings.Contains(err.Error(), "duplicate key") { return err @@ -245,7 +313,7 @@ func createPostgresTables(dbName string, db *sqlx.DB) error { } // NewUserRegistryMySQL opens a connection to a postgres database -func NewUserRegistryMySQL(datasource string, clientTLSConfig *tls.ClientTLSConfig, csp bccsp.BCCSP) (*sqlx.DB, error) { +func NewUserRegistryMySQL(datasource string, clientTLSConfig *tls.ClientTLSConfig, csp bccsp.BCCSP) (*DB, error) { log.Debugf("Using MySQL database, connecting to database...") dbName := getDBName(datasource) @@ -290,7 +358,7 @@ func NewUserRegistryMySQL(datasource string, clientTLSConfig *tls.ClientTLSConfi return nil, errors.Wrap(err, "Failed to create MySQL tables") } - return db, nil + return &DB{db}, nil } func createMySQLDatabase(dbName string, db *sqlx.DB) error { @@ -323,11 +391,19 @@ func createMySQLTables(dbName string, db *sqlx.DB) error { if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number varbinary(128) NOT NULL, authority_key_identifier varbinary(128) NOT NULL, ca_label varbinary(128), status varbinary(128) NOT NULL, reason int, expiry timestamp DEFAULT 0, revoked_at timestamp DEFAULT 0, pem varbinary(4096) NOT NULL, level INTEGER DEFAULT 0, PRIMARY KEY(serial_number, authority_key_identifier)) DEFAULT CHARSET=utf8 COLLATE utf8_bin"); err != nil { return errors.Wrap(err, "Error creating certificates table") } + log.Debug("Creating credentials table if it doesn't exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS credentials (id VARCHAR(255), revocation_handle varbinary(128) NOT NULL, cred varbinary(4096) NOT NULL, ca_label varbinary(128), status varbinary(128) NOT NULL, reason int, expiry timestamp DEFAULT 0, revoked_at timestamp DEFAULT 0, level INTEGER DEFAULT 0, PRIMARY KEY(revocation_handle)) DEFAULT CHARSET=utf8 COLLATE utf8_bin"); err != nil { + return errors.Wrap(err, "Error creating certificates table") + } + log.Debug("Creating revocation_component_info table if it does not exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS revocation_component_info (epoch INTEGER, next_handle INTEGER, lasthandle_in_pool INTEGER, level INTEGER DEFAULT 0, PRIMARY KEY (epoch))"); err != nil { + return errors.Wrap(err, "Error creating revocation_component_info table") + } log.Debug("Creating properties table if it does not exist") if _, err := db.Exec("CREATE TABLE IF NOT EXISTS properties (property VARCHAR(255), value VARCHAR(256), PRIMARY KEY(property))"); err != nil { return errors.Wrap(err, "Error creating properties table") } - _, err := db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0')")) + _, err := db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0'), ('credential.level', '0'), ('rcinfo.level', '0')")) if err != nil { if !strings.Contains(err.Error(), "1062") { // MySQL error code for duplicate entry return err @@ -399,7 +475,7 @@ func MaskDBCred(str string) string { } // UpdateSchema updates the database tables to use the latest schema -func UpdateSchema(db *sqlx.DB, levels *Levels) error { +func UpdateSchema(db *DB, levels *Levels) error { log.Debug("Checking database schema...") switch db.DriverName() { @@ -415,7 +491,7 @@ func UpdateSchema(db *sqlx.DB, levels *Levels) error { } // UpdateDBLevel updates the levels for the tables in the database -func UpdateDBLevel(db *sqlx.DB, levels *Levels) error { +func UpdateDBLevel(db *DB, levels *Levels) error { log.Debugf("Updating database level to %+v", levels) _, err := db.Exec(db.Rebind("UPDATE properties SET value = ? WHERE (property = 'identity.level')"), levels.Identity) @@ -430,13 +506,20 @@ func UpdateDBLevel(db *sqlx.DB, levels *Levels) error { if err != nil { return err } - + _, err = db.Exec(db.Rebind("UPDATE properties SET value = ? WHERE (property = 'credential.level')"), levels.Credential) + if err != nil { + return err + } + _, err = db.Exec(db.Rebind("UPDATE properties SET value = ? WHERE (property = 'rcinfo.level')"), levels.RCInfo) + if err != nil { + return err + } return nil } -func currentDBLevels(db *sqlx.DB) (*Levels, error) { +func currentDBLevels(db *DB) (*Levels, error) { var err error - var identityLevel, affiliationLevel, certificateLevel int + var identityLevel, affiliationLevel, certificateLevel, credentialLevel, rcinfoLevel int err = db.Get(&identityLevel, "Select value FROM properties WHERE (property = 'identity.level')") if err != nil { @@ -450,15 +533,24 @@ func currentDBLevels(db *sqlx.DB) (*Levels, error) { if err != nil { return nil, err } - + err = db.Get(&credentialLevel, "Select value FROM properties WHERE (property = 'credential.level')") + if err != nil { + return nil, err + } + err = db.Get(&rcinfoLevel, "Select value FROM properties WHERE (property = 'rcinfo.level')") + if err != nil { + return nil, err + } return &Levels{ Identity: identityLevel, Affiliation: affiliationLevel, Certificate: certificateLevel, + Credential: credentialLevel, + RCInfo: rcinfoLevel, }, nil } -func updateSQLiteSchema(db *sqlx.DB, serverLevels *Levels) error { +func updateSQLiteSchema(db *DB, serverLevels *Levels) error { log.Debug("Update SQLite schema, if using outdated schema") var err error @@ -582,7 +674,7 @@ func updateCertificatesTable(tx *sqlx.Tx, args ...interface{}) error { return nil } -func updateMySQLSchema(db *sqlx.DB) error { +func updateMySQLSchema(db *DB) error { log.Debug("Update MySQL schema if using outdated schema") var err error @@ -642,7 +734,7 @@ func updateMySQLSchema(db *sqlx.DB) error { return nil } -func updatePostgresSchema(db *sqlx.DB) error { +func updatePostgresSchema(db *DB) error { log.Debug("Update Postgres schema if using outdated schema") var err error @@ -684,13 +776,13 @@ func updatePostgresSchema(db *sqlx.DB) error { return nil } -func doTransaction(db *sqlx.DB, doit func(tx *sqlx.Tx, args ...interface{}) error, args ...interface{}) error { +func doTransaction(db *DB, doit func(tx *sqlx.Tx, args ...interface{}) error, args ...interface{}) error { tx := db.MustBegin() err := doit(tx, args...) if err != nil { err2 := tx.Rollback() if err2 != nil { - log.Errorf("Error encounted while rolling back transaction: %s", err2) + log.Errorf("Error encountered while rolling back transaction: %s", err2) return err } return err diff --git a/lib/dbutil/mocks/FabricCADB.go b/lib/dbutil/mocks/FabricCADB.go new file mode 100644 index 000000000..50ebcb16b --- /dev/null +++ b/lib/dbutil/mocks/FabricCADB.go @@ -0,0 +1,114 @@ +/* +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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import dbutil "github.com/hyperledger/fabric-ca/lib/dbutil" +import mock "github.com/stretchr/testify/mock" +import sql "database/sql" +import sqlx "github.com/jmoiron/sqlx" + +// FabricCADB is an autogenerated mock type for the FabricCADB type +type FabricCADB struct { + mock.Mock +} + +// BeginTx provides a mock function with given fields: +func (_m *FabricCADB) BeginTx() dbutil.FabricCATx { + ret := _m.Called() + + var r0 dbutil.FabricCATx + if rf, ok := ret.Get(0).(func() dbutil.FabricCATx); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbutil.FabricCATx) + } + } + + return r0 +} + +// MustBegin provides a mock function with given fields: +func (_m *FabricCADB) MustBegin() *sqlx.Tx { + ret := _m.Called() + + var r0 *sqlx.Tx + if rf, ok := ret.Get(0).(func() *sqlx.Tx); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sqlx.Tx) + } + } + + return r0 +} + +// NamedExec provides a mock function with given fields: query, arg +func (_m *FabricCADB) NamedExec(query string, arg interface{}) (sql.Result, error) { + ret := _m.Called(query, arg) + + var r0 sql.Result + if rf, ok := ret.Get(0).(func(string, interface{}) sql.Result); ok { + r0 = rf(query, arg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, interface{}) error); ok { + r1 = rf(query, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Rebind provides a mock function with given fields: query +func (_m *FabricCADB) Rebind(query string) string { + ret := _m.Called(query) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(query) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Select provides a mock function with given fields: dest, query, args +func (_m *FabricCADB) Select(dest interface{}, query string, args ...interface{}) error { + var _ca []interface{} + _ca = append(_ca, dest, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, string, ...interface{}) error); ok { + r0 = rf(dest, query, args...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/lib/dbutil/mocks/FabricCATx.go b/lib/dbutil/mocks/FabricCATx.go new file mode 100644 index 000000000..a2a296b97 --- /dev/null +++ b/lib/dbutil/mocks/FabricCATx.go @@ -0,0 +1,111 @@ +/* +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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import mock "github.com/stretchr/testify/mock" +import sql "database/sql" + +// FabricCATx is an autogenerated mock type for the FabricCATx type +type FabricCATx struct { + mock.Mock +} + +// Commit provides a mock function with given fields: +func (_m *FabricCATx) Commit() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Exec provides a mock function with given fields: query, args +func (_m *FabricCATx) Exec(query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 sql.Result + if rf, ok := ret.Get(0).(func(string, ...interface{}) sql.Result); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok { + r1 = rf(query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Rebind provides a mock function with given fields: query +func (_m *FabricCATx) Rebind(query string) string { + ret := _m.Called(query) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(query) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Rollback provides a mock function with given fields: +func (_m *FabricCATx) Rollback() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Select provides a mock function with given fields: dest, query, args +func (_m *FabricCATx) Select(dest interface{}, query string, args ...interface{}) error { + var _ca []interface{} + _ca = append(_ca, dest, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, string, ...interface{}) error); ok { + r0 = rf(dest, query, args...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/lib/dbutil/mocks/Result.go b/lib/dbutil/mocks/Result.go new file mode 100644 index 000000000..dcdafc786 --- /dev/null +++ b/lib/dbutil/mocks/Result.go @@ -0,0 +1,67 @@ +/* +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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Result is an autogenerated mock type for the Result type +type Result struct { + mock.Mock +} + +// LastInsertId provides a mock function with given fields: +func (_m *Result) LastInsertId() (int64, error) { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RowsAffected provides a mock function with given fields: +func (_m *Result) RowsAffected() (int64, error) { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/lib/issuercredential.go b/lib/issuercredential.go deleted file mode 100644 index acf67ad06..000000000 --- a/lib/issuercredential.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright IBM Corp. 2018 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 ( - "io/ioutil" - - "github.com/cloudflare/cfssl/log" - proto "github.com/golang/protobuf/proto" - "github.com/hyperledger/fabric-ca/util" - "github.com/hyperledger/fabric/idemix" - "github.com/pkg/errors" -) - -// IssuerCredential represents CA's idemix credential -type IssuerCredential interface { - Load() error - Store() error - GetIssuerKey() (*idemix.IssuerKey, error) - SetIssuerKey(key *idemix.IssuerKey) -} - -type issuerCredential struct { - pubKeyFile string - secretKeyFile string - issuerKey *idemix.IssuerKey -} - -func newIssuerCredential(pubKeyFile, secretKeyFile string) IssuerCredential { - return &issuerCredential{ - pubKeyFile: pubKeyFile, - secretKeyFile: secretKeyFile, - } -} - -func (ic *issuerCredential) Load() error { - pubKeyFileExists := util.FileExists(ic.pubKeyFile) - secretKeyFileExists := util.FileExists(ic.secretKeyFile) - if pubKeyFileExists && secretKeyFileExists { - log.Info("The issuer public and secret key files already exist") - log.Infof(" secret key file location: %s", ic.secretKeyFile) - log.Infof(" public key file location: %s", ic.pubKeyFile) - pubKeyBytes, err := ioutil.ReadFile(ic.pubKeyFile) - if err != nil { - return errors.Wrapf(err, "Failed to read issuer public key") - } - pubKey := &idemix.IssuerPublicKey{} - err = proto.Unmarshal(pubKeyBytes, pubKey) - if err != nil { - return errors.Wrapf(err, "Failed to unmarshal issuer public key bytes") - } - err = pubKey.Check() - if err != nil { - return errors.Wrapf(err, "Issuer public key check failed") - } - privKey, err := ioutil.ReadFile(ic.secretKeyFile) - if err != nil { - return errors.Wrapf(err, "Failed to read issuer secret key") - } - ic.issuerKey = &idemix.IssuerKey{ - IPk: pubKey, - ISk: privKey, - } - } - return nil -} - -func (ic *issuerCredential) Store() error { - ik, err := ic.GetIssuerKey() - if err != nil { - return err - } - - ipkBytes, err := proto.Marshal(ik.IPk) - if err != nil { - return errors.New("Failed to marshal issuer public key") - } - - err = util.WriteFile(ic.pubKeyFile, ipkBytes, 0644) - if err != nil { - log.Errorf("Failed to store issuer public key: %s", err.Error()) - return errors.New("Failed to store issuer public key") - } - - err = util.WriteFile(ic.secretKeyFile, ik.ISk, 0644) - if err != nil { - log.Errorf("Failed to store issuer secret key: %s", err.Error()) - return errors.New("Failed to store issuer secret key") - } - - log.Infof("The issuer key was successfully stored. The public key is at: %s, secret key is at: %s", - ic.pubKeyFile, ic.secretKeyFile) - return nil -} - -func (ic *issuerCredential) GetIssuerKey() (*idemix.IssuerKey, error) { - if ic.issuerKey == nil { - return nil, errors.New("Issuer key is not set") - } - return ic.issuerKey, nil -} - -func (ic *issuerCredential) SetIssuerKey(key *idemix.IssuerKey) { - ic.issuerKey = key -} diff --git a/lib/server/idemix/creddbaccessor.go b/lib/server/idemix/creddbaccessor.go new file mode 100644 index 000000000..1ca38862c --- /dev/null +++ b/lib/server/idemix/creddbaccessor.go @@ -0,0 +1,167 @@ +/* +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 idemix + +import ( + "fmt" + "reflect" + "time" + + "github.com/cloudflare/cfssl/log" + "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/pkg/errors" + + "github.com/kisielk/sqlstruct" +) + +const ( + // InsertCredentialSQL is the SQL to add a credential to database + InsertCredentialSQL = ` +INSERT INTO credentials (id, revocation_handle, cred, ca_label, status, reason, expiry, revoked_at, level) + VALUES (:id, :revocation_handle, :cred, :ca_label, :status, :reason, :expiry, :revoked_at, :level);` + + // SelectCredentialByIDSQL is the SQL for getting credentials of a user + SelectCredentialByIDSQL = ` +SELECT %s FROM credentials +WHERE (id = ?);` + + // SelectCredentialSQL is the SQL for getting a credential given a revocation handle + SelectCredentialSQL = ` +SELECT %s FROM credentials +WHERE (revocation_handle = ?);` + + // UpdateRevokeCredentialSQL is the SQL for updating status of a credential to revoked + UpdateRevokeCredentialSQL = ` +UPDATE credentials +SET status='revoked', revoked_at=CURRENT_TIMESTAMP, reason=:reason +WHERE (id = :id AND status != 'revoked');` + + // DeleteCredentialbyID is the SQL for deleting credential of a user + DeleteCredentialbyID = ` +DELETE FROM credentials + WHERE (id = ?);` +) + +// CredRecord represents a credential database record +type CredRecord struct { + ID string `db:"id"` + RevocationHandle int `db:"revocation_handle"` + Cred string `db:"cred"` + CALabel string `db:"ca_label"` + Status string `db:"status"` + Reason int `db:"reason"` + Expiry time.Time `db:"expiry"` + RevokedAt time.Time `db:"revoked_at"` + Level int `db:"level"` +} + +// CredDBAccessor is the accessor for credentials database table +type CredDBAccessor interface { + // InsertCredential inserts specified Idemix credential record into database + InsertCredential(cr CredRecord) error + // GetCredential returns Idemix credential associated with the specified revocation + // handle + GetCredential(revocationHandle string) (*CredRecord, error) + // GetCredentialsByID returns Idemix credentials associated with the specified + // enrollment ID + GetCredentialsByID(id string) ([]CredRecord, error) +} + +// CredentialAccessor implements IdemixCredDBAccessor interface +type CredentialAccessor struct { + level int + db dbutil.FabricCADB +} + +// NewCredentialAccessor returns a new CredentialAccessor. +func NewCredentialAccessor(db dbutil.FabricCADB, level int) *CredentialAccessor { + ac := new(CredentialAccessor) + ac.db = db + ac.level = level + return ac +} + +// SetDB changes the underlying sql.DB object Accessor is manipulating. +func (ac *CredentialAccessor) SetDB(db dbutil.FabricCADB) { + ac.db = db +} + +// InsertCredential puts a CredentialRecord into db. +func (ac *CredentialAccessor) InsertCredential(cr CredRecord) error { + log.Debug("DB: Insert Credential") + err := ac.checkDB() + if err != nil { + return err + } + cr.Level = ac.level + res, err := ac.db.NamedExec(InsertCredentialSQL, cr) + if err != nil { + return errors.Wrap(err, "Failed to insert credential into database") + } + + numRowsAffected, err := res.RowsAffected() + + if numRowsAffected == 0 { + return errors.New("Failed to insert the credential record; no rows affected") + } + + if numRowsAffected != 1 { + return errors.Errorf("Expected to affect 1 entry in credentials table but affected %d", + numRowsAffected) + } + + return err +} + +// GetCredentialsByID gets a CredentialRecord indexed by id. +func (ac *CredentialAccessor) GetCredentialsByID(id string) ([]CredRecord, error) { + log.Debugf("DB: Get credentials by ID '%s'", id) + err := ac.checkDB() + if err != nil { + return nil, err + } + crs := []CredRecord{} + err = ac.db.Select(&crs, fmt.Sprintf(ac.db.Rebind(SelectCredentialByIDSQL), sqlstruct.Columns(CredRecord{})), id) + if err != nil { + return nil, errors.Wrapf(err, "Failed to get credentials for identity '%s' from database", id) + } + + return crs, nil +} + +// GetCredential gets a CredentialRecord indexed by revocationHandle. +func (ac *CredentialAccessor) GetCredential(revocationHandle string) (*CredRecord, error) { + log.Debugf("DB: Get credential by revocation handle '%s'", revocationHandle) + err := ac.checkDB() + if err != nil { + return nil, err + } + cr := &CredRecord{} + err = ac.db.Select(cr, fmt.Sprintf(ac.db.Rebind(SelectCredentialSQL), sqlstruct.Columns(CredRecord{})), revocationHandle) + if err != nil { + return nil, errors.Wrapf(err, "Failed to get credential associated with revocation handle '%s' from database", revocationHandle) + } + + return cr, nil +} + +func (ac *CredentialAccessor) checkDB() error { + if ac.db == nil || reflect.ValueOf(ac.db).IsNil() { + return errors.New("Database is not set") + } + return nil +} diff --git a/lib/server/idemix/creddbaccessor_test.go b/lib/server/idemix/creddbaccessor_test.go new file mode 100644 index 000000000..e92c619b2 --- /dev/null +++ b/lib/server/idemix/creddbaccessor_test.go @@ -0,0 +1,177 @@ +/* +Copyright IBM Corp. 2018 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 idemix_test + +import ( + "fmt" + "testing" + "time" + + dmocks "github.com/hyperledger/fabric-ca/lib/dbutil/mocks" + . "github.com/hyperledger/fabric-ca/lib/server/idemix" + "github.com/kisielk/sqlstruct" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestInsertCredentialNilDB(t *testing.T) { + credRecord := getCredRecord() + + var db *dmocks.FabricCADB + accessor := NewCredentialAccessor(db, 1) + err := accessor.InsertCredential(credRecord) + assert.Error(t, err) + assert.Equal(t, "Database is not set", err.Error()) +} + +func TestInsertCredential(t *testing.T) { + credRecord := getCredRecord() + result := new(dmocks.Result) + result.On("RowsAffected").Return(int64(1), nil) + db := new(dmocks.FabricCADB) + db.On("NamedExec", InsertCredentialSQL, credRecord).Return(result, nil) + db.On("Rebind", InsertCredentialSQL).Return(InsertCredentialSQL) + accessor := NewCredentialAccessor(nil, 1) + accessor.SetDB(db) + err := accessor.InsertCredential(credRecord) + assert.NoError(t, err) +} + +func TestInsertCredentialNoRowsAffected(t *testing.T) { + credRecord := getCredRecord() + result := new(dmocks.Result) + result.On("RowsAffected").Return(int64(0), nil) + db := new(dmocks.FabricCADB) + db.On("NamedExec", InsertCredentialSQL, credRecord).Return(result, nil) + db.On("Rebind", InsertCredentialSQL).Return(InsertCredentialSQL) + accessor := NewCredentialAccessor(db, 1) + err := accessor.InsertCredential(credRecord) + assert.Error(t, err) + assert.Equal(t, "Failed to insert the credential record; no rows affected", err.Error()) +} + +func TestInsertCredentialTwoRowsAffected(t *testing.T) { + credRecord := getCredRecord() + result := new(dmocks.Result) + result.On("RowsAffected").Return(int64(2), nil) + db := new(dmocks.FabricCADB) + db.On("NamedExec", InsertCredentialSQL, credRecord).Return(result, nil) + db.On("Rebind", InsertCredentialSQL).Return(InsertCredentialSQL) + accessor := NewCredentialAccessor(db, 1) + err := accessor.InsertCredential(credRecord) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Expected to affect 1 entry in credentials table but affected") +} + +func TestInsertCredentialExecError(t *testing.T) { + credRecord := getCredRecord() + db := new(dmocks.FabricCADB) + db.On("NamedExec", InsertCredentialSQL, credRecord).Return(nil, errors.New("Exec error")) + db.On("Rebind", InsertCredentialSQL).Return(InsertCredentialSQL) + accessor := NewCredentialAccessor(db, 1) + err := accessor.InsertCredential(credRecord) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Failed to insert credential into databas") +} + +func TestGetCredentialsByIDNilDB(t *testing.T) { + var db *dmocks.FabricCADB + accessor := NewCredentialAccessor(db, 1) + _, err := accessor.GetCredentialsByID("1") + assert.Error(t, err) + assert.Equal(t, "Database is not set", err.Error()) +} + +func TestGetCredentialsByIDSelectError(t *testing.T) { + db := new(dmocks.FabricCADB) + db.On("Rebind", SelectCredentialByIDSQL).Return(SelectCredentialByIDSQL) + crs := []CredRecord{} + q := fmt.Sprintf(SelectCredentialByIDSQL, sqlstruct.Columns(CredRecord{})) + f := getCredSelectFunc(t, true) + db.On("Select", &crs, q, "foo").Return(f) + accessor := NewCredentialAccessor(db, 1) + _, err := accessor.GetCredentialsByID("foo") + assert.Error(t, err) +} + +func TestGetCredentialsByID(t *testing.T) { + db := new(dmocks.FabricCADB) + db.On("Rebind", SelectCredentialByIDSQL).Return(SelectCredentialByIDSQL) + crs := []CredRecord{} + q := fmt.Sprintf(SelectCredentialByIDSQL, sqlstruct.Columns(CredRecord{})) + f := getCredSelectFunc(t, false) + db.On("Select", &crs, q, "foo").Return(f) + accessor := NewCredentialAccessor(db, 1) + rcrs, err := accessor.GetCredentialsByID("foo") + assert.NoError(t, err) + assert.Equal(t, 1, len(rcrs)) +} + +func TestGetCredentialNilDB(t *testing.T) { + var db *dmocks.FabricCADB + accessor := NewCredentialAccessor(db, 1) + _, err := accessor.GetCredential("1") + assert.Error(t, err) + assert.Equal(t, "Database is not set", err.Error()) +} + +func TestGetCredentialSelectError(t *testing.T) { + db := new(dmocks.FabricCADB) + db.On("Rebind", SelectCredentialSQL).Return(SelectCredentialSQL) + cr := CredRecord{} + q := fmt.Sprintf(SelectCredentialSQL, sqlstruct.Columns(CredRecord{})) + db.On("Select", &cr, q, "1").Return(errors.New("Select error")) + accessor := NewCredentialAccessor(db, 1) + _, err := accessor.GetCredential("1") + assert.Error(t, err) +} + +func TestGetCredential(t *testing.T) { + db := new(dmocks.FabricCADB) + db.On("Rebind", SelectCredentialSQL).Return(SelectCredentialSQL) + cr := CredRecord{} + q := fmt.Sprintf(SelectCredentialSQL, sqlstruct.Columns(CredRecord{})) + db.On("Select", &cr, q, "1").Return(nil) + accessor := NewCredentialAccessor(db, 1) + _, err := accessor.GetCredential("1") + assert.NoError(t, err) +} + +func getCredSelectFunc(t *testing.T, isError bool) func(interface{}, string, ...interface{}) error { + return func(dest interface{}, query string, args ...interface{}) error { + crs := dest.(*[]CredRecord) + cr := getCredRecord() + *crs = append(*crs, cr) + if isError { + return errors.New("Failed to get credentials from DB") + } + return nil + } +} + +func getCredRecord() CredRecord { + return CredRecord{ + ID: "foo", + CALabel: "", + Expiry: time.Now(), + Level: 1, + Reason: 0, + Status: "good", + RevocationHandle: 1, + Cred: "blah", + } +} diff --git a/lib/server/idemix/enrollhandler.go b/lib/server/idemix/enrollhandler.go new file mode 100644 index 000000000..ad2c176ea --- /dev/null +++ b/lib/server/idemix/enrollhandler.go @@ -0,0 +1,252 @@ +/* +Copyright IBM Corp. 2018 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 idemix + +import ( + "fmt" + "strconv" + "strings" + + "github.com/cloudflare/cfssl/log" + proto "github.com/golang/protobuf/proto" + amcl "github.com/hyperledger/fabric-amcl/amcl" + fp256bn "github.com/hyperledger/fabric-amcl/amcl/FP256BN" + "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/hyperledger/fabric-ca/lib/spi" + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/idemix" + "github.com/pkg/errors" +) + +// ServerRequestCtx is the server request context that Idemix enroll expects +type ServerRequestCtx interface { + BasicAuthentication() (string, error) + TokenAuthentication() (string, error) + GetCA() (CA, error) + GetCaller() (spi.User, error) + ReadBody(body interface{}) error +} + +// CA is the CA that Idemix enroll expects +type CA interface { + GetName() string + DB() dbutil.FabricCADB + IdemixRand() *amcl.RAND + IssuerCredential() IssuerCredential + RevocationComponent() RevocationComponent + CredDBAccessor() CredDBAccessor +} + +// EnrollmentResponse is the idemix enrollment response from the server +type EnrollmentResponse struct { + // Base64 encoding of idemix Credential + Credential string + // Attribute name-value pairs + Attrs map[string]string + // Base64 encoding of Credential Revocation list + //CRL string + // Base64 encoding of the issuer nonce + Nonce string +} + +// EnrollRequestHandler is the handler for Idemix enroll request +type EnrollRequestHandler struct { + IsBasicAuth bool + Ctx ServerRequestCtx + EnrollmentID string + CA CA + IdmxLib Lib +} + +// HandleIdemixEnroll handles processing for Idemix enroll +func (h *EnrollRequestHandler) HandleIdemixEnroll() (*EnrollmentResponse, error) { + err := h.Authenticate() + if err != nil { + return nil, err + } + + var req api.IdemixEnrollmentRequestNet + err = h.Ctx.ReadBody(&req) + if err != nil { + return nil, err + } + + // Get the targeted CA + h.CA, err = h.Ctx.GetCA() + if err != nil { + return nil, err + } + + if req.CredRequest == nil { + nonce := h.GenerateNonce() + + // TODO: store the nonce so it can be validated later + + resp := &EnrollmentResponse{ + Nonce: util.B64Encode(idemix.BigToBytes(nonce)), + } + return resp, nil + } + + ik, err := h.CA.IssuerCredential().GetIssuerKey() + if err != nil { + log.Errorf("Failed to get Idemix issuer key for the CA %s: %s", h.CA.GetName(), err.Error()) + return nil, errors.WithMessage(err, fmt.Sprintf("Failed to get Idemix issuer key for the CA: %s", + h.CA.GetName())) + } + + caller, err := h.Ctx.GetCaller() + if err != nil { + log.Errorf("Failed to get caller of the request: %s", err.Error()) + return nil, err + } + + // TODO: validate issuer nonce + + // Check the if credential request is valid + err = req.CredRequest.Check(ik.GetIPk()) + if err != nil { + log.Errorf("Invalid Idemix credential request: %s", err.Error()) + return nil, errors.WithMessage(err, "Invalid Idemix credential request") + } + + // Get revocation handle for the credential + rh, err := h.CA.RevocationComponent().GetNewRevocationHandle() + if err != nil { + return nil, err + } + + // Get attributes for the identity + attrMap, attrs, err := h.GetAttributeValues(caller, ik.GetIPk(), rh) + if err != nil { + return nil, err + } + + cred, err := h.IdmxLib.NewCredential(ik, req.CredRequest, attrs, h.CA.IdemixRand()) + if err != nil { + log.Errorf("CA '%s' failed to create new Idemix credential for identity '%s': %s", + h.CA.GetName(), h.EnrollmentID, err.Error()) + return nil, errors.New("Failed to create new Idemix credential") + } + credBytes, err := proto.Marshal(cred) + if err != nil { + return nil, errors.New("Failed to marshal Idemix credential to bytes") + } + b64CredBytes := util.B64Encode(credBytes) + + // Store the credential in the database + err = h.CA.CredDBAccessor().InsertCredential(CredRecord{ + CALabel: h.CA.GetName(), + ID: caller.GetName(), + Status: "good", + Cred: b64CredBytes, + RevocationHandle: int(*rh), + }) + if err != nil { + log.Errorf("Failed to store the Idemix credential for identity '%s' in the database: %s", caller.GetName(), err.Error()) + return nil, errors.New("Failed to store the Idemix credential") + } + + // TODO: Get CRL from revocation authority of the CA + + resp := &EnrollmentResponse{ + Credential: b64CredBytes, + Attrs: attrMap, + } + + if h.IsBasicAuth { + err = caller.LoginComplete() + if err != nil { + return nil, err + } + } + + // Success + return resp, nil +} + +// Authenticate authenticates the Idemix enroll request +func (h *EnrollRequestHandler) Authenticate() error { + var err error + if h.IsBasicAuth { + h.EnrollmentID, err = h.Ctx.BasicAuthentication() + if err != nil { + return err + } + } else { + h.EnrollmentID, err = h.Ctx.TokenAuthentication() + if err != nil { + return err + } + } + return nil +} + +// GenerateNonce generates a nonce for an Idemix enroll request +func (h *EnrollRequestHandler) GenerateNonce() *fp256bn.BIG { + return h.IdmxLib.RandModOrder(h.CA.IdemixRand()) +} + +// GetAttributeValues returns attribute values of the caller of Idemix enroll request +func (h *EnrollRequestHandler) GetAttributeValues(caller spi.User, ipk *idemix.IssuerPublicKey, + rh *RevocationHandle) (map[string]string, []*fp256bn.BIG, error) { + rc := []*fp256bn.BIG{} + attrMap := make(map[string]string) + for _, attrName := range ipk.AttributeNames { + if attrName == AttrEnrollmentID { + idBytes := []byte(caller.GetName()) + rc = append(rc, idemix.HashModOrder(idBytes)) + attrMap[attrName] = caller.GetName() + } else if attrName == AttrOU { + ou := []string{} + for _, aff := range caller.GetAffiliationPath() { + ou = append(ou, aff) + } + ouVal := strings.Join(ou, ".") + ouBytes := []byte(ouVal) + rc = append(rc, idemix.HashModOrder(ouBytes)) + attrMap[attrName] = ouVal + } else if attrName == AttrRevocationHandle { + rhi := int(*rh) + rc = append(rc, fp256bn.NewBIGint(rhi)) + attrMap[attrName] = strconv.Itoa(rhi) + } else if attrName == AttrRole { + isAdmin := false + attrObj, err := caller.GetAttribute("isAdmin") + if err == nil { + isAdmin, err = strconv.ParseBool(attrObj.GetValue()) + } + role := 0 + if isAdmin { + role = 1 + } + rc = append(rc, fp256bn.NewBIGint(int(role))) + attrMap[attrName] = strconv.FormatBool(isAdmin) + } else { + attrObj, err := caller.GetAttribute(attrName) + if err != nil { + log.Errorf("Failed to get attribute %s for user %s: %s", attrName, caller.GetName(), err.Error()) + } else { + attrBytes := []byte(attrObj.GetValue()) + rc = append(rc, idemix.HashModOrder(attrBytes)) + attrMap[attrName] = attrObj.GetValue() + } + } + } + return attrMap, rc, nil +} diff --git a/lib/server/idemix/enrollhandler_test.go b/lib/server/idemix/enrollhandler_test.go new file mode 100644 index 000000000..5652fecdf --- /dev/null +++ b/lib/server/idemix/enrollhandler_test.go @@ -0,0 +1,285 @@ +/* +Copyright IBM Corp. 2018 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 idemix_test + +import ( + "testing" + + proto "github.com/golang/protobuf/proto" + amcl "github.com/hyperledger/fabric-amcl/amcl/FP256BN" + "github.com/hyperledger/fabric-ca/api" + . "github.com/hyperledger/fabric-ca/lib/server/idemix" + "github.com/hyperledger/fabric-ca/lib/server/idemix/mocks" + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/idemix" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestIdemixEnrollInvalidBasicAuth(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("BasicAuthentication").Return("", errors.New("bad credentials")) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true} + _, err := handler.HandleIdemixEnroll() + assert.Error(t, err, "Idemix enroll should fail if basic auth credentials are invalid") +} +func TestIdemixEnrollInvalidTokenAuth(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("TokenAuthentication").Return("", errors.New("bad credentials")) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: false} + _, err := handler.HandleIdemixEnroll() + assert.Error(t, err, "Idemix enroll should fail if token auth credentials are invalid") +} +func TestIdemixEnrollBadReqBody(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("BasicAuthentication").Return("foo", nil) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true} + req := api.IdemixEnrollmentRequestNet{} + req.CredRequest = nil + ctx.On("ReadBody", &req).Return(errors.New("Invalid request body")) + _, err := handler.HandleIdemixEnroll() + assert.Error(t, err, "Idemix enroll should return error if reading body fails") +} +func TestIdemixEnrollGetCAError(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("BasicAuthentication").Return("foo", nil) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true} + req := api.IdemixEnrollmentRequestNet{} + req.CredRequest = nil + ctx.On("ReadBody", &req).Return(nil) + ctx.On("GetCA").Return(nil, errors.New("Failure getting CA from context")) + _, err := handler.HandleIdemixEnroll() + assert.Error(t, err, "Idemix enroll should return error if getting CA from context fails") +} +func TestHandleIdemixEnrollForNonce(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("BasicAuthentication").Return("foo", nil) + idemixlib := new(mocks.Lib) + rnd, err := idemix.GetRand() + if err != nil { + t.Fatalf("Error generating a random number") + } + rmo := idemix.RandModOrder(rnd) + idemixlib.On("GetRand").Return(rnd, nil) + idemixlib.On("RandModOrder", rnd).Return(rmo) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true, IdmxLib: idemixlib} + req := api.IdemixEnrollmentRequestNet{} + req.CredRequest = nil + ctx.On("ReadBody", &req).Return(nil) + ca := new(mocks.CA) + ca.On("IdemixRand").Return(rnd) + ctx.On("GetCA").Return(ca, nil) + _, err = handler.HandleIdemixEnroll() + assert.NoError(t, err, "Idemix enroll should return a valid nonce") +} +func TestHandleIdemixEnrollForNonceTokenAuth(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("TokenAuthentication").Return("foo", nil) + + idemixlib := new(mocks.Lib) + rnd, err := idemix.GetRand() + if err != nil { + t.Fatalf("Error generating a random number") + } + rmo := idemix.RandModOrder(rnd) + idemixlib.On("GetRand").Return(rnd, nil) + idemixlib.On("RandModOrder", rnd).Return(rmo) + + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: false, IdmxLib: idemixlib} + req := api.IdemixEnrollmentRequestNet{} + req.CredRequest = nil + ctx.On("ReadBody", &req).Return(nil) + ca := new(mocks.CA) + ca.On("IdemixRand").Return(rnd) + ctx.On("GetCA").Return(ca, nil) + _, err = handler.HandleIdemixEnroll() + assert.NoError(t, err, "Idemix enroll should return a valid nonce") +} +func TestHandleIdemixEnrollForCredentialError(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + ctx.On("BasicAuthentication").Return("foo", nil) + + idemixlib := new(mocks.Lib) + rnd, err := idemix.GetRand() + if err != nil { + t.Fatalf("Error generating a random number") + } + rmo := idemix.RandModOrder(rnd) + idemixlib.On("GetRand").Return(rnd, nil) + idemixlib.On("RandModOrder", rnd).Return(rmo) + + issuerCred := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixlib) + ca := new(mocks.CA) + ca.On("GetName").Return("") + ca.On("IssuerCredential").Return(issuerCred) + ca.On("IdemixRand").Return(rnd) + + ctx.On("GetCA").Return(ca, nil) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true, IdmxLib: idemixlib, CA: ca} + nonce := handler.GenerateNonce() + + credReq, _, _, err := newIdemixCredentialRequest(t, nonce) + f := getReadBodyFunc(t, credReq) + req := api.IdemixEnrollmentRequestNet{} + ctx.On("ReadBody", &req).Return(f) + + _, err = handler.HandleIdemixEnroll() + assert.Error(t, err, "Idemix enroll should return error if IssuerCredential has not been loaded from disk") + if err != nil { + assert.Contains(t, err.Error(), "Failed to get Idemix issuer key for the CA") + } + + err = issuerCred.Load() + if err != nil { + t.Fatalf("Failed to load issuer credential") + } + ctx.On("GetCaller").Return(nil, errors.New("Error when getting caller of the request")) + _, err = handler.HandleIdemixEnroll() + assert.Error(t, err, "Idemix enroll should return error if ctx.GetCaller returns error") + if err != nil { + assert.Contains(t, err.Error(), "Error when getting caller of the request") + } +} +func TestHandleIdemixEnrollForCredentialSuccess(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + idemixlib := new(mocks.Lib) + rnd, err := idemix.GetRand() + if err != nil { + t.Fatalf("Error generating a random number") + } + rmo := idemix.RandModOrder(rnd) + idemixlib.On("GetRand").Return(rnd, nil) + idemixlib.On("RandModOrder", rnd).Return(rmo) + + issuerCred := NewCAIdemixCredential(testPublicKeyFile, + testSecretKeyFile, idemixlib) + err = issuerCred.Load() + if err != nil { + t.Fatalf("Failed to load issuer credential") + } + ik, _ := issuerCred.GetIssuerKey() + + rh := RevocationHandle(1) + rc := new(mocks.RevocationComponent) + rc.On("GetNewRevocationHandle").Return(&rh, nil) + + ca := new(mocks.CA) + ca.On("GetName").Return("") + ca.On("IssuerCredential").Return(issuerCred) + ca.On("IdemixRand").Return(rnd) + ca.On("RevocationComponent").Return(rc) + + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true, IdmxLib: idemixlib, CA: ca} + nonce := handler.GenerateNonce() + + caller := new(mocks.User) + caller.On("GetName").Return("foo") + caller.On("GetAffiliationPath").Return([]string{"a", "b", "c"}) + caller.On("GetAttribute", "isAdmin").Return(&api.Attribute{Name: "isAdmin", Value: "true"}, nil) + caller.On("LoginComplete").Return(nil) + + credReq, _, _, err := newIdemixCredentialRequest(t, nonce) + if err != nil { + t.Fatalf("Failed to create test credential request") + } + _, attrs, err := handler.GetAttributeValues(caller, ik.IPk, &rh) + if err != nil { + t.Fatalf("Failed to get attributes") + } + cred, err := idemix.NewCredential(ik, credReq, attrs, rnd) + if err != nil { + t.Fatalf("Failed to create credential") + } + idemixlib.On("NewCredential", ik, credReq, attrs, rnd).Return(cred, nil) + + b64CredBytes, err := getB64EncodedCred(cred) + if err != nil { + t.Fatalf("Failed to base64 encode credential") + } + credAccessor := new(mocks.CredDBAccessor) + credAccessor.On("InsertCredential", CredRecord{RevocationHandle: 1, + CALabel: "", ID: "foo", Status: "good", Cred: b64CredBytes}).Return(nil) + + ca.On("CredDBAccessor").Return(credAccessor, nil) + + ctx.On("BasicAuthentication").Return("foo", nil) + f := getReadBodyFunc(t, credReq) + ctx.On("ReadBody", &api.IdemixEnrollmentRequestNet{}).Return(f) + ctx.On("GetCA").Return(ca, nil) + ctx.On("GetCaller").Return(caller, nil) + + // Now setup of all mocks is over, test the method + _, err = handler.HandleIdemixEnroll() + assert.NoError(t, err, "Idemix enroll should return error because ctx.GetCaller returned error") +} + +func TestGetAttributeValues(t *testing.T) { + ctx := new(mocks.ServerRequestCtx) + idemixlib := new(mocks.Lib) + handler := EnrollRequestHandler{Ctx: ctx, IsBasicAuth: true, IdmxLib: idemixlib} + + caller := new(mocks.User) + caller.On("GetName").Return("foo") + caller.On("GetAffiliationPath").Return([]string{"a", "b", "c"}) + caller.On("GetAttribute", "isAdmin").Return(&api.Attribute{Name: "isAdmin", Value: "true"}, nil) + caller.On("GetAttribute", "type").Return(&api.Attribute{Name: "type", Value: "client"}, nil) + caller.On("LoginComplete").Return(nil) + + rh := RevocationHandle(1) + + attrNames := GetAttributeNames() + attrNames = append(attrNames, "type") + ipk := idemix.IssuerPublicKey{AttributeNames: attrNames} + _, _, err := handler.GetAttributeValues(caller, &ipk, &rh) + assert.NoError(t, err) +} + +func getB64EncodedCred(cred *idemix.Credential) (string, error) { + credBytes, err := proto.Marshal(cred) + if err != nil { + return "", errors.New("Failed to marshal credential to bytes") + } + b64CredBytes := util.B64Encode(credBytes) + return b64CredBytes, nil +} + +func getReadBodyFunc(t *testing.T, credReq *idemix.CredRequest) func(body interface{}) error { + return func(body interface{}) error { + enrollReq, _ := body.(*api.IdemixEnrollmentRequestNet) + if credReq == nil { + return errors.New("Error reading the body") + } + enrollReq.CredRequest = credReq + return nil + } +} + +func newIdemixCredentialRequest(t *testing.T, nonce *amcl.BIG) (*idemix.CredRequest, *amcl.BIG, *amcl.BIG, error) { + idmxlib := new(mocks.Lib) + issuerCred := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idmxlib) + err := issuerCred.Load() + if err != nil { + t.Fatalf("Failed to load issuer credential") + } + ik, err := issuerCred.GetIssuerKey() + if err != nil { + t.Fatalf("Issuer credential returned error while getting issuer key") + } + rng, err := idemix.GetRand() + if err != nil { + return nil, nil, nil, err + } + sk := idemix.RandModOrder(rng) + randCred := idemix.RandModOrder(rng) + return idemix.NewCredRequest(sk, randCred, nonce, ik.IPk, rng), sk, randCred, nil +} diff --git a/lib/server/idemix/idemixlib.go b/lib/server/idemix/idemixlib.go new file mode 100644 index 000000000..e410ddb8e --- /dev/null +++ b/lib/server/idemix/idemixlib.go @@ -0,0 +1,52 @@ +/* +Copyright IBM Corp. 2018 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 idemix + +import ( + "github.com/hyperledger/fabric-amcl/amcl" + fp256bn "github.com/hyperledger/fabric-amcl/amcl/FP256BN" + "github.com/hyperledger/fabric/idemix" +) + +// Lib represents idemix library +type Lib interface { + NewIssuerKey(AttributeNames []string, rng *amcl.RAND) (*idemix.IssuerKey, error) + NewCredential(key *idemix.IssuerKey, m *idemix.CredRequest, attrs []*fp256bn.BIG, rng *amcl.RAND) (*idemix.Credential, error) + GetRand() (*amcl.RAND, error) + RandModOrder(rng *amcl.RAND) *fp256bn.BIG +} + +// libImpl is adapter for idemix library. It implements Lib interface +type libImpl struct{} + +// NewLib returns an instance of an object that implements Lib interface +func NewLib() Lib { + return &libImpl{} +} + +func (i *libImpl) GetRand() (*amcl.RAND, error) { + return idemix.GetRand() +} +func (i *libImpl) NewCredential(key *idemix.IssuerKey, m *idemix.CredRequest, attrs []*fp256bn.BIG, rng *amcl.RAND) (*idemix.Credential, error) { + return idemix.NewCredential(key, m, attrs, rng) +} +func (i *libImpl) RandModOrder(rng *amcl.RAND) *fp256bn.BIG { + return idemix.RandModOrder(rng) +} +func (i *libImpl) NewIssuerKey(AttributeNames []string, rng *amcl.RAND) (*idemix.IssuerKey, error) { + return idemix.NewIssuerKey(AttributeNames, rng) +} diff --git a/lib/server/idemix/issuercredential.go b/lib/server/idemix/issuercredential.go new file mode 100644 index 000000000..a0f466b64 --- /dev/null +++ b/lib/server/idemix/issuercredential.go @@ -0,0 +1,174 @@ +/* +Copyright IBM Corp. 2018 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 idemix + +import ( + "io/ioutil" + + "github.com/cloudflare/cfssl/log" + proto "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric-ca/util" + "github.com/hyperledger/fabric/idemix" + "github.com/pkg/errors" +) + +const ( + // AttrEnrollmentID is the attribute name for enrollment ID + AttrEnrollmentID = "EnrollmentID" + // AttrRole is the attribute name for role + AttrRole = "Role" + // AttrOU is the attribute name for OU + AttrOU = "OU" + // AttrRevocationHandle is the attribute name for revocation handle + AttrRevocationHandle = "RevocationHandle" +) + +// IssuerCredential represents CA's Idemix credential +type IssuerCredential interface { + // Load loads the CA's Idemix credential from the disk + Load() error + // Store stores the CA's Idemix credential to the disk + Store() error + // GetIssuerKey returns *idemix.IssuerKey that represents + // CA's Idemix public and secret key + GetIssuerKey() (*idemix.IssuerKey, error) + // SetIssuerKey sets issuer key + SetIssuerKey(key *idemix.IssuerKey) + // Returns new instance of idemix.IssuerKey + NewIssuerKey() (*idemix.IssuerKey, error) +} + +// caIdemixCredential implements IssuerCredential interface +type caIdemixCredential struct { + pubKeyFile string + secretKeyFile string + issuerKey *idemix.IssuerKey + idemixLib Lib +} + +// NewCAIdemixCredential returns an instance of an object that implements IssuerCredential interface +func NewCAIdemixCredential(pubKeyFile, secretKeyFile string, lib Lib) IssuerCredential { + return &caIdemixCredential{ + pubKeyFile: pubKeyFile, + secretKeyFile: secretKeyFile, + idemixLib: lib, + } +} + +// Load loads the CA's Idemix public and private key from the location specified +// by pubKeyFile and secretKeyFile attributes, respectively +func (ic *caIdemixCredential) Load() error { + pubKeyBytes, err := ioutil.ReadFile(ic.pubKeyFile) + if err != nil { + return errors.Wrapf(err, "Failed to read CA's Idemix public key") + } + if len(pubKeyBytes) == 0 { + return errors.New("CA's Idemix public key file is empty") + } + pubKey := &idemix.IssuerPublicKey{} + err = proto.Unmarshal(pubKeyBytes, pubKey) + if err != nil { + return errors.Wrapf(err, "Failed to unmarshal CA's Idemix public key bytes") + } + err = pubKey.Check() + if err != nil { + return errors.Wrapf(err, "CA Idemix public key check failed") + } + privKey, err := ioutil.ReadFile(ic.secretKeyFile) + if err != nil { + return errors.Wrapf(err, "Failed to read CA's Idemix secret key") + } + if len(privKey) == 0 { + return errors.New("CA's Idemix secret key file is empty") + } + ic.issuerKey = &idemix.IssuerKey{ + IPk: pubKey, + ISk: privKey, + } + //TODO: check if issuer key is valid by checking public and secret key pair + return nil +} + +// Store stores the CA's Idemix public and private key to the location +// specified by pubKeyFile and secretKeyFile attributes, respectively +func (ic *caIdemixCredential) Store() error { + ik, err := ic.GetIssuerKey() + if err != nil { + return err + } + + ipkBytes, err := proto.Marshal(ik.IPk) + if err != nil { + return errors.New("Failed to marshal CA's Idemix public key") + } + + err = util.WriteFile(ic.pubKeyFile, ipkBytes, 0644) + if err != nil { + log.Errorf("Failed to store CA's Idemix public key: %s", err.Error()) + return errors.New("Failed to store CA's Idemix public key") + } + + err = util.WriteFile(ic.secretKeyFile, ik.ISk, 0644) + if err != nil { + log.Errorf("Failed to store CA's Idemix secret key: %s", err.Error()) + return errors.New("Failed to store CA's Idemix secret key") + } + + log.Infof("The CA's issuer key was successfully stored. The public key is at: %s, secret key is at: %s", + ic.pubKeyFile, ic.secretKeyFile) + return nil +} + +// GetIssuerKey returns idemix.IssuerKey object that is associated with +// this CAIdemixCredential +func (ic *caIdemixCredential) GetIssuerKey() (*idemix.IssuerKey, error) { + if ic.issuerKey == nil { + return nil, errors.New("CA's Idemix credential is not set") + } + return ic.issuerKey, nil +} + +// SetIssuerKey sets idemix.IssuerKey object +func (ic *caIdemixCredential) SetIssuerKey(key *idemix.IssuerKey) { + ic.issuerKey = key +} + +// NewIssuerKey creates new Issuer key +func (ic *caIdemixCredential) NewIssuerKey() (*idemix.IssuerKey, error) { + rng, err := ic.idemixLib.GetRand() + if err != nil { + return nil, errors.Wrapf(err, "Error creating new issuer key") + } + // Currently, Idemix library supports these four attributes. The supported attribute names + // must also be known when creating issuer key. In the future, Idemix library will support + // arbitary attribute names, so removing the need to hardcode attribute names in the issuer + // key. + // OU - organization unit + // Role - if the user is admin or member + // EnrollmentID - enrollment ID of the user + // RevocationHandle - revocation handle of a credential + ik, err := ic.idemixLib.NewIssuerKey(GetAttributeNames(), rng) + if err != nil { + return nil, err + } + return ik, nil +} + +// GetAttributeNames returns attribute names supported by the Fabric CA for Idemix credentials +func GetAttributeNames() []string { + return []string{AttrOU, AttrRole, AttrEnrollmentID, AttrRevocationHandle} +} diff --git a/lib/server/idemix/issuercredential_test.go b/lib/server/idemix/issuercredential_test.go new file mode 100644 index 000000000..a6191f413 --- /dev/null +++ b/lib/server/idemix/issuercredential_test.go @@ -0,0 +1,234 @@ +/* +Copyright IBM Corp. 2018 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 idemix_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + proto "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric-ca/lib/server/idemix/mocks" + "github.com/hyperledger/fabric/idemix" + "github.com/pkg/errors" + + . "github.com/hyperledger/fabric-ca/lib/server/idemix" + "github.com/stretchr/testify/assert" +) + +const ( + testPublicKeyFile = "../../../testdata/IdemixPublicKey" + testSecretKeyFile = "../../../testdata/IdemixSecretKey" +) + +func TestLoadEmptyIdemixPublicKey(t *testing.T) { + testdir, err := ioutil.TempDir(".", "issuerkeyloadTest") + pubkeyfile, err := ioutil.TempFile(testdir, "IdemixPublicKey") + defer os.RemoveAll(testdir) + idemixLib := new(mocks.Lib) + ic := NewCAIdemixCredential(pubkeyfile.Name(), testSecretKeyFile, idemixLib) + err = ic.Load() + assert.Error(t, err, "Should have failed to load non existing issuer public key") + if err != nil { + assert.Contains(t, err.Error(), "CA's Idemix public key file is empty") + } +} + +func TestLoadFakeIdemixPublicKey(t *testing.T) { + testdir, err := ioutil.TempDir(".", "issuerkeyloadTest") + pubkeyfile, err := ioutil.TempFile(testdir, "IdemixPublicKey") + privkeyfile, err := ioutil.TempFile(testdir, "IdemixSecretKey") + defer os.RemoveAll(testdir) + _, err = pubkeyfile.WriteString("foo") + if err != nil { + t.Fatalf("Failed to write to the file %s", pubkeyfile.Name()) + } + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(pubkeyfile.Name(), privkeyfile.Name(), idemixLib) + err = ik.Load() + assert.Error(t, err, "Should have failed to load non existing issuer public key") + if err != nil { + assert.Contains(t, err.Error(), "Failed to unmarshal CA's Idemix public key bytes") + } +} + +func TestLoadNonExistentIdemixSecretKey(t *testing.T) { + testdir, err := ioutil.TempDir(".", "issuerkeyloadTest") + privkeyfile, err := ioutil.TempFile(testdir, "IdemixSecretKey") + defer os.RemoveAll(testdir) + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, privkeyfile.Name(), idemixLib) + err = ik.Load() + assert.Error(t, err, "Should have failed to load non existing issuer secret key") + if err != nil { + assert.Contains(t, err.Error(), "CA's Idemix secret key file is empty") + } +} + +func TestLoadEmptyIdemixSecretKey(t *testing.T) { + testdir, err := ioutil.TempDir(".", "issuerkeyloadTest") + defer os.RemoveAll(testdir) + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, filepath.Join(testdir, "IdemixSecretKey"), idemixLib) + err = ik.Load() + assert.Error(t, err, "Should have failed to load non existing issuer secret key") + if err != nil { + assert.Contains(t, err.Error(), "Failed to read CA's Idemix secret key") + } +} + +func TestLoad(t *testing.T) { + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + err := ik.Load() + assert.NoError(t, err, "Failed to load CA's issuer idemix credential") + + err = ik.Store() + assert.NoError(t, err, "Failed to store CA's issuer idemix credential") +} + +func TestStoreNilIssuerKey(t *testing.T) { + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + err := ik.Store() + assert.Error(t, err, "Should fail if store is called without setting the issuer key or loading the issuer key from disk") + if err != nil { + assert.Equal(t, err.Error(), "CA's Idemix credential is not set") + } +} + +func TestStoreNilIdemixPublicKey(t *testing.T) { + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + ik.SetIssuerKey(&idemix.IssuerKey{}) + err := ik.Store() + assert.Error(t, err, "Should fail if store is called with empty issuer public key byte array") + if err != nil { + assert.Equal(t, err.Error(), "Failed to marshal CA's Idemix public key") + } +} + +func TestStoreInvalidPublicKeyFilePath(t *testing.T) { + pubkeyfile := "./testdata1/IdemixPublicKey" + + // Valid issuer public key + validPubKeyFile := testPublicKeyFile + pubKeyBytes, err := ioutil.ReadFile(validPubKeyFile) + if err != nil { + t.Fatalf("Failed to read idemix public key file %s", validPubKeyFile) + } + + pubKey := &idemix.IssuerPublicKey{} + err = proto.Unmarshal(pubKeyBytes, pubKey) + if err != nil { + t.Fatalf("Failed to unmarshal idemix public key bytes from %s", validPubKeyFile) + } + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(pubkeyfile, testSecretKeyFile, idemixLib) + ik.SetIssuerKey(&idemix.IssuerKey{IPk: pubKey}) + err = ik.Store() + assert.Error(t, err, "Should fail if issuer public key is being stored to non-existent directory") + if err != nil { + assert.Equal(t, err.Error(), "Failed to store CA's Idemix public key") + } +} + +func TestStoreInvalidSecretKeyFilePath(t *testing.T) { + testdir, err := ioutil.TempDir(".", "issuerkeystoreTest") + defer os.RemoveAll(testdir) + + // foo directory is non-existent + privkeyfile := filepath.Join(testdir, "foo/IdemixSecretKey") + + // Valid issuer public key + pubKeyBytes, err := ioutil.ReadFile(testPublicKeyFile) + if err != nil { + t.Fatalf("Failed to read idemix public key file %s", testPublicKeyFile) + } + + pubKey := &idemix.IssuerPublicKey{} + err = proto.Unmarshal(pubKeyBytes, pubKey) + if err != nil { + t.Fatalf("Failed to unmarshal idemix public key bytes from %s", testPublicKeyFile) + } + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, privkeyfile, idemixLib) + ik.SetIssuerKey(&idemix.IssuerKey{IPk: pubKey}) + err = ik.Store() + assert.Error(t, err, "Should fail if issuer secret key is being stored to non-existent directory") + if err != nil { + assert.Equal(t, "Failed to store CA's Idemix secret key", err.Error()) + } +} + +func TestGetIssuerKey(t *testing.T) { + idemixLib := new(mocks.Lib) + ik := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + _, err := ik.GetIssuerKey() + assert.Error(t, err, "GetIssuerKey should return an error if it is called without setting the issuer key or loading the issuer key from disk") + if err != nil { + assert.Equal(t, err.Error(), "CA's Idemix credential is not set") + } + err = ik.Load() + if err != nil { + t.Fatalf("Load of valid issuer public and secret key should not fail: %s", err) + } + _, err = ik.GetIssuerKey() + assert.NoError(t, err, "GetIssuerKey should not return an error if the issuer key is set") +} + +func TestNewIssuerKeyGetRandError(t *testing.T) { + idemixLib := new(mocks.Lib) + idemixLib.On("GetRand").Return(nil, errors.New("Failed to generate random number")) + ic := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + _, err := ic.NewIssuerKey() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Error creating new issuer key") +} + +func TestNewIssuerKeyError(t *testing.T) { + idemixLib := new(mocks.Lib) + rnd, err := NewLib().GetRand() + if err != nil { + t.Fatalf("Failed to generate a random number: %s", err.Error()) + } + idemixLib.On("GetRand").Return(rnd, nil) + idemixLib.On("NewIssuerKey", GetAttributeNames(), rnd).Return(nil, errors.New("Failed to create new issuer key")) + ic := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + _, err = ic.NewIssuerKey() + assert.Error(t, err) +} + +func TestNewIssuerKey(t *testing.T) { + idemixLib := new(mocks.Lib) + idemix := NewLib() + rnd, err := idemix.GetRand() + if err != nil { + t.Fatalf("Failed to generate a random number: %s", err.Error()) + } + attrNames := GetAttributeNames() + ik, err := idemix.NewIssuerKey(attrNames, rnd) + if err != nil { + t.Fatalf("Failed to create new issuer key: %s", err.Error()) + } + idemixLib.On("GetRand").Return(rnd, nil) + idemixLib.On("NewIssuerKey", attrNames, rnd).Return(ik, nil) + ic := NewCAIdemixCredential(testPublicKeyFile, testSecretKeyFile, idemixLib) + _, err = ic.NewIssuerKey() + assert.NoError(t, err) +} diff --git a/lib/server/idemix/mocks/CA.go b/lib/server/idemix/mocks/CA.go new file mode 100644 index 000000000..c87058ff7 --- /dev/null +++ b/lib/server/idemix/mocks/CA.go @@ -0,0 +1,122 @@ +/* +Copyright IBM Corp. 2018 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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import amcl "github.com/hyperledger/fabric-amcl/amcl" +import dbutil "github.com/hyperledger/fabric-ca/lib/dbutil" +import idemix "github.com/hyperledger/fabric-ca/lib/server/idemix" +import mock "github.com/stretchr/testify/mock" + +// CA is an autogenerated mock type for the CA type +type CA struct { + mock.Mock +} + +// CredDBAccessor provides a mock function with given fields: +func (_m *CA) CredDBAccessor() idemix.CredDBAccessor { + ret := _m.Called() + + var r0 idemix.CredDBAccessor + if rf, ok := ret.Get(0).(func() idemix.CredDBAccessor); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(idemix.CredDBAccessor) + } + } + + return r0 +} + +// DB provides a mock function with given fields: +func (_m *CA) DB() dbutil.FabricCADB { + ret := _m.Called() + + var r0 dbutil.FabricCADB + if rf, ok := ret.Get(0).(func() dbutil.FabricCADB); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbutil.FabricCADB) + } + } + + return r0 +} + +// GetName provides a mock function with given fields: +func (_m *CA) GetName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// IdemixRand provides a mock function with given fields: +func (_m *CA) IdemixRand() *amcl.RAND { + ret := _m.Called() + + var r0 *amcl.RAND + if rf, ok := ret.Get(0).(func() *amcl.RAND); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*amcl.RAND) + } + } + + return r0 +} + +// IssuerCredential provides a mock function with given fields: +func (_m *CA) IssuerCredential() idemix.IssuerCredential { + ret := _m.Called() + + var r0 idemix.IssuerCredential + if rf, ok := ret.Get(0).(func() idemix.IssuerCredential); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(idemix.IssuerCredential) + } + } + + return r0 +} + +// RevocationComponent provides a mock function with given fields: +func (_m *CA) RevocationComponent() idemix.RevocationComponent { + ret := _m.Called() + + var r0 idemix.RevocationComponent + if rf, ok := ret.Get(0).(func() idemix.RevocationComponent); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(idemix.RevocationComponent) + } + } + + return r0 +} diff --git a/lib/server/idemix/mocks/CredDBAccessor.go b/lib/server/idemix/mocks/CredDBAccessor.go new file mode 100644 index 000000000..6afbdc52c --- /dev/null +++ b/lib/server/idemix/mocks/CredDBAccessor.go @@ -0,0 +1,86 @@ +/* +Copyright IBM Corp. 2018 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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import idemix "github.com/hyperledger/fabric-ca/lib/server/idemix" +import mock "github.com/stretchr/testify/mock" + +// CredDBAccessor is an autogenerated mock type for the CredDBAccessor type +type CredDBAccessor struct { + mock.Mock +} + +// GetCredential provides a mock function with given fields: revocationHandle +func (_m *CredDBAccessor) GetCredential(revocationHandle string) (*idemix.CredRecord, error) { + ret := _m.Called(revocationHandle) + + var r0 *idemix.CredRecord + if rf, ok := ret.Get(0).(func(string) *idemix.CredRecord); ok { + r0 = rf(revocationHandle) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*idemix.CredRecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(revocationHandle) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCredentialsByID provides a mock function with given fields: id +func (_m *CredDBAccessor) GetCredentialsByID(id string) ([]idemix.CredRecord, error) { + ret := _m.Called(id) + + var r0 []idemix.CredRecord + if rf, ok := ret.Get(0).(func(string) []idemix.CredRecord); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]idemix.CredRecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InsertCredential provides a mock function with given fields: cr +func (_m *CredDBAccessor) InsertCredential(cr idemix.CredRecord) error { + ret := _m.Called(cr) + + var r0 error + if rf, ok := ret.Get(0).(func(idemix.CredRecord) error); ok { + r0 = rf(cr) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/lib/server/idemix/mocks/IssuerCredential.go b/lib/server/idemix/mocks/IssuerCredential.go new file mode 100644 index 000000000..70e52993e --- /dev/null +++ b/lib/server/idemix/mocks/IssuerCredential.go @@ -0,0 +1,106 @@ +/* +Copyright IBM Corp. 2018 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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import idemix "github.com/hyperledger/fabric/idemix" + +import mock "github.com/stretchr/testify/mock" + +// IssuerCredential is an autogenerated mock type for the IssuerCredential type +type IssuerCredential struct { + mock.Mock +} + +// GetIssuerKey provides a mock function with given fields: +func (_m *IssuerCredential) GetIssuerKey() (*idemix.IssuerKey, error) { + ret := _m.Called() + + var r0 *idemix.IssuerKey + if rf, ok := ret.Get(0).(func() *idemix.IssuerKey); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*idemix.IssuerKey) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Load provides a mock function with given fields: +func (_m *IssuerCredential) Load() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewIssuerKey provides a mock function with given fields: +func (_m *IssuerCredential) NewIssuerKey() (*idemix.IssuerKey, error) { + ret := _m.Called() + + var r0 *idemix.IssuerKey + if rf, ok := ret.Get(0).(func() *idemix.IssuerKey); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*idemix.IssuerKey) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetIssuerKey provides a mock function with given fields: key +func (_m *IssuerCredential) SetIssuerKey(key *idemix.IssuerKey) { + _m.Called(key) +} + +// Store provides a mock function with given fields: +func (_m *IssuerCredential) Store() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/lib/server/idemix/mocks/Lib.go b/lib/server/idemix/mocks/Lib.go new file mode 100644 index 000000000..42fc7a301 --- /dev/null +++ b/lib/server/idemix/mocks/Lib.go @@ -0,0 +1,114 @@ +/* +Copyright IBM Corp. 2018 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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import FP256BN "github.com/hyperledger/fabric-amcl/amcl/FP256BN" +import amcl "github.com/hyperledger/fabric-amcl/amcl" +import fabricidemix "github.com/hyperledger/fabric/idemix" + +import mock "github.com/stretchr/testify/mock" + +// Lib is an autogenerated mock type for the Lib type +type Lib struct { + mock.Mock +} + +// GetRand provides a mock function with given fields: +func (_m *Lib) GetRand() (*amcl.RAND, error) { + ret := _m.Called() + + var r0 *amcl.RAND + if rf, ok := ret.Get(0).(func() *amcl.RAND); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*amcl.RAND) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewCredential provides a mock function with given fields: key, m, attrs, rng +func (_m *Lib) NewCredential(key *fabricidemix.IssuerKey, m *fabricidemix.CredRequest, attrs []*FP256BN.BIG, rng *amcl.RAND) (*fabricidemix.Credential, error) { + ret := _m.Called(key, m, attrs, rng) + + var r0 *fabricidemix.Credential + if rf, ok := ret.Get(0).(func(*fabricidemix.IssuerKey, *fabricidemix.CredRequest, []*FP256BN.BIG, *amcl.RAND) *fabricidemix.Credential); ok { + r0 = rf(key, m, attrs, rng) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fabricidemix.Credential) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*fabricidemix.IssuerKey, *fabricidemix.CredRequest, []*FP256BN.BIG, *amcl.RAND) error); ok { + r1 = rf(key, m, attrs, rng) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewIssuerKey provides a mock function with given fields: AttributeNames, rng +func (_m *Lib) NewIssuerKey(AttributeNames []string, rng *amcl.RAND) (*fabricidemix.IssuerKey, error) { + ret := _m.Called(AttributeNames, rng) + + var r0 *fabricidemix.IssuerKey + if rf, ok := ret.Get(0).(func([]string, *amcl.RAND) *fabricidemix.IssuerKey); ok { + r0 = rf(AttributeNames, rng) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fabricidemix.IssuerKey) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string, *amcl.RAND) error); ok { + r1 = rf(AttributeNames, rng) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RandModOrder provides a mock function with given fields: rng +func (_m *Lib) RandModOrder(rng *amcl.RAND) *FP256BN.BIG { + ret := _m.Called(rng) + + var r0 *FP256BN.BIG + if rf, ok := ret.Get(0).(func(*amcl.RAND) *FP256BN.BIG); ok { + r0 = rf(rng) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*FP256BN.BIG) + } + } + + return r0 +} diff --git a/lib/server/idemix/mocks/RevocationComponent.go b/lib/server/idemix/mocks/RevocationComponent.go new file mode 100644 index 000000000..5339c4e61 --- /dev/null +++ b/lib/server/idemix/mocks/RevocationComponent.go @@ -0,0 +1,49 @@ +/* +Copyright IBM Corp. 2018 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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import idemix "github.com/hyperledger/fabric-ca/lib/server/idemix" +import mock "github.com/stretchr/testify/mock" + +// RevocationComponent is an autogenerated mock type for the RevocationComponent type +type RevocationComponent struct { + mock.Mock +} + +// GetNewRevocationHandle provides a mock function with given fields: +func (_m *RevocationComponent) GetNewRevocationHandle() (*idemix.RevocationHandle, error) { + ret := _m.Called() + + var r0 *idemix.RevocationHandle + if rf, ok := ret.Get(0).(func() *idemix.RevocationHandle); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*idemix.RevocationHandle) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/lib/server/idemix/mocks/ServerRequestCtx.go b/lib/server/idemix/mocks/ServerRequestCtx.go new file mode 100644 index 000000000..82f4d3d83 --- /dev/null +++ b/lib/server/idemix/mocks/ServerRequestCtx.go @@ -0,0 +1,129 @@ +/* +Copyright IBM Corp. 2018 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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import idemix "github.com/hyperledger/fabric-ca/lib/server/idemix" +import mock "github.com/stretchr/testify/mock" +import spi "github.com/hyperledger/fabric-ca/lib/spi" + +// ServerRequestCtx is an autogenerated mock type for the ServerRequestCtx type +type ServerRequestCtx struct { + mock.Mock +} + +// BasicAuthentication provides a mock function with given fields: +func (_m *ServerRequestCtx) BasicAuthentication() (string, error) { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCA provides a mock function with given fields: +func (_m *ServerRequestCtx) GetCA() (idemix.CA, error) { + ret := _m.Called() + + var r0 idemix.CA + if rf, ok := ret.Get(0).(func() idemix.CA); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(idemix.CA) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCaller provides a mock function with given fields: +func (_m *ServerRequestCtx) GetCaller() (spi.User, error) { + ret := _m.Called() + + var r0 spi.User + if rf, ok := ret.Get(0).(func() spi.User); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(spi.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReadBody provides a mock function with given fields: body +func (_m *ServerRequestCtx) ReadBody(body interface{}) error { + ret := _m.Called(body) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(body) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TokenAuthentication provides a mock function with given fields: +func (_m *ServerRequestCtx) TokenAuthentication() (string, error) { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/lib/server/idemix/mocks/User.go b/lib/server/idemix/mocks/User.go new file mode 100644 index 000000000..248b8dfb3 --- /dev/null +++ b/lib/server/idemix/mocks/User.go @@ -0,0 +1,214 @@ +/* +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. +*/ +// Code generated by mockery v1.0.0 + +package mocks + +import api "github.com/hyperledger/fabric-ca/api" +import mock "github.com/stretchr/testify/mock" + +// User is an autogenerated mock type for the User type +type User struct { + mock.Mock +} + +// GetAffiliationPath provides a mock function with given fields: +func (_m *User) GetAffiliationPath() []string { + ret := _m.Called() + + var r0 []string + if rf, ok := ret.Get(0).(func() []string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + return r0 +} + +// GetAttribute provides a mock function with given fields: name +func (_m *User) GetAttribute(name string) (*api.Attribute, error) { + ret := _m.Called(name) + + var r0 *api.Attribute + if rf, ok := ret.Get(0).(func(string) *api.Attribute); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Attribute) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAttributes provides a mock function with given fields: attrNames +func (_m *User) GetAttributes(attrNames []string) ([]api.Attribute, error) { + ret := _m.Called(attrNames) + + var r0 []api.Attribute + if rf, ok := ret.Get(0).(func([]string) []api.Attribute); ok { + r0 = rf(attrNames) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api.Attribute) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(attrNames) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLevel provides a mock function with given fields: +func (_m *User) GetLevel() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// GetMaxEnrollments provides a mock function with given fields: +func (_m *User) GetMaxEnrollments() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// GetName provides a mock function with given fields: +func (_m *User) GetName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// GetType provides a mock function with given fields: +func (_m *User) GetType() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Login provides a mock function with given fields: password, caMaxEnrollment +func (_m *User) Login(password string, caMaxEnrollment int) error { + ret := _m.Called(password, caMaxEnrollment) + + var r0 error + if rf, ok := ret.Get(0).(func(string, int) error); ok { + r0 = rf(password, caMaxEnrollment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// LoginComplete provides a mock function with given fields: +func (_m *User) LoginComplete() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ModifyAttributes provides a mock function with given fields: attrs +func (_m *User) ModifyAttributes(attrs []api.Attribute) error { + ret := _m.Called(attrs) + + var r0 error + if rf, ok := ret.Get(0).(func([]api.Attribute) error); ok { + r0 = rf(attrs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Revoke provides a mock function with given fields: +func (_m *User) Revoke() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetLevel provides a mock function with given fields: level +func (_m *User) SetLevel(level int) error { + ret := _m.Called(level) + + var r0 error + if rf, ok := ret.Get(0).(func(int) error); ok { + r0 = rf(level) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/lib/server/idemix/revocationcomponent.go b/lib/server/idemix/revocationcomponent.go new file mode 100644 index 000000000..6c4a01ae2 --- /dev/null +++ b/lib/server/idemix/revocationcomponent.go @@ -0,0 +1,213 @@ +/* +Copyright IBM Corp. 2018 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 idemix + +import ( + "fmt" + + "github.com/cloudflare/cfssl/log" + "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +const ( + // InsertRCInfo is the SQL for inserting revocation component info + InsertRCInfo = "INSERT into revocation_component_info(epoch, next_handle, lasthandle_in_pool, level) VALUES (:epoch, :next_handle, :lasthandle_in_pool, :level)" + // SelectRCInfo is the query string for getting revocation component info + SelectRCInfo = "SELECT * FROM revocation_component_info" + // UpdateNextAndLastHandle is the SQL for updating next and last revocation handle + UpdateNextAndLastHandle = "UPDATE revocation_component_info SET next_handle = ? AND lasthandle_in_pool = ? WHERE (epoch = ?)" + // UpdateNextHandle s the SQL for updating next revocation handle + UpdateNextHandle = "UPDATE revocation_component_info SET next_handle = ? WHERE (epoch = ?)" + // DefaultRevocationHandlePoolSize is the default revocation handle pool size + DefaultRevocationHandlePoolSize = 100 +) + +// RevocationHandle is the identifier of the credential using which a user can +// prove to the verifier that his/her credential is not revoked with a zero knowledge +// proof +type RevocationHandle int + +// RevocationComponent is responsible for generating revocation handles and +// credential revocation info (CRI) +type RevocationComponent interface { + GetNewRevocationHandle() (*RevocationHandle, error) +} + +// RevocationComponentInfo is the revocation component information record that is +// stored in the database +type RevocationComponentInfo struct { + Epoch int `db:"epoch"` + NextRevocationHandle int `db:"next_handle"` + LastHandleInPool int `db:"lasthandle_in_pool"` + Level int `db:"level"` +} + +// revocationComponent implements RevocationComponent interface +type revocationComponent struct { + ca CA + db dbutil.FabricCADB + info *RevocationComponentInfo + opts *CfgOptions +} + +// CfgOptions encapsulates Idemix related the configuration options +type CfgOptions struct { + RevocationHandlePoolSize int `def:"100" help:"Specifies revocation handle pool size"` +} + +// NewRevocationComponent constructor for revocation component +func NewRevocationComponent(ca CA, opts *CfgOptions, level int) (RevocationComponent, error) { + rc := &revocationComponent{ + ca, ca.DB(), nil, opts, + } + var err error + rc.info, err = rc.getRCInfoFromDB() + if err == nil { + // If epoch is 0, it means this is the first time revocation component is being + // initialized. Initilize revocation component info and store it in the database + if rc.info.Epoch == 0 { + rcInfo := RevocationComponentInfo{ + Epoch: 1, + NextRevocationHandle: 1, + LastHandleInPool: opts.RevocationHandlePoolSize, + Level: level, + } + err = rc.addRCInfoToDB(&rcInfo) + } + } + if err != nil { + return nil, errors.WithMessage(err, + fmt.Sprintf("Failed to initialize revocation component for CA %s", ca.GetName())) + } + return rc, nil +} + +// GetNewRevocationHandle returns a new revocation handle +func (rc *revocationComponent) GetNewRevocationHandle() (*RevocationHandle, error) { + h, err := rc.getNextRevocationHandle() + if err != nil { + return nil, err + } + rh := RevocationHandle(h) + return &rh, err +} + +func (rc *revocationComponent) getRCInfoFromDB() (*RevocationComponentInfo, error) { + rcinfos := []RevocationComponentInfo{} + err := rc.db.Select(&rcinfos, SelectRCInfo) + if err != nil { + return nil, err + } + if len(rcinfos) == 0 { + return &RevocationComponentInfo{ + 0, 0, 0, 0, + }, nil + } + return &rcinfos[0], nil +} + +func (rc *revocationComponent) addRCInfoToDB(rcInfo *RevocationComponentInfo) error { + res, err := rc.db.NamedExec(InsertRCInfo, rcInfo) + if err != nil { + return errors.New("Failed to insert revocation component info into database") + } + + numRowsAffected, err := res.RowsAffected() + if numRowsAffected == 0 { + return errors.New("Failed to insert the revocation component info record; no rows affected") + } + + if numRowsAffected != 1 { + return errors.Errorf("Expected to affect 1 entry in revocation component info table but affected %d", + numRowsAffected) + } + return err +} + +// getNextRevocationHandle returns next revocation handle +func (rc *revocationComponent) getNextRevocationHandle() (int, error) { + result, err := rc.doTransaction(rc.db, rc.getNextRevocationHandleTx, nil) + if err != nil { + return 0, err + } + + nextHandle := result.(int) + return nextHandle, nil +} + +func (rc *revocationComponent) getNextRevocationHandleTx(tx dbutil.FabricCATx, args ...interface{}) (interface{}, error) { + var err error + + // Get the latest revocation component info from the database + rcInfos := []RevocationComponentInfo{} + query := SelectRCInfo + err = tx.Select(&rcInfos, tx.Rebind(query)) + if err != nil { + return nil, errors.New("Failed to get revocation component info from database") + } + if len(rcInfos) == 0 { + return nil, errors.New("No revocation component info found in database") + } + rcInfo := rcInfos[0] + + nextHandle := rcInfo.NextRevocationHandle + newNextHandle := rcInfo.NextRevocationHandle + 1 + var inQuery string + if nextHandle == rcInfo.LastHandleInPool { + newLastHandleInPool := rcInfo.LastHandleInPool + 100 + query = UpdateNextAndLastHandle + inQuery, args, err = sqlx.In(query, newNextHandle, newLastHandleInPool, rcInfo.Epoch) + } else { + query = UpdateNextHandle + inQuery, args, err = sqlx.In(query, newNextHandle, rcInfo.Epoch) + } + if err != nil { + return nil, errors.Wrapf(err, "Failed to construct query '%s'", query) + } + _, err = tx.Exec(tx.Rebind(inQuery), args...) + if err != nil { + return nil, errors.Wrapf(err, "Failed to update revocation component info") + } + + return nextHandle, nil +} + +func (rc *revocationComponent) doTransaction(db dbutil.FabricCADB, doit func(tx dbutil.FabricCATx, args ...interface{}) (interface{}, error), args ...interface{}) (interface{}, error) { + if db == nil { + return nil, errors.New("Failed to correctly setup database connection") + } + tx := db.BeginTx() + result, err := doit(tx, args...) + if err != nil { + err2 := tx.Rollback() + if err2 != nil { + errMsg := fmt.Sprintf("Error encountered while rolling back transaction: %s, original error: %s", err2.Error(), err.Error()) + log.Errorf(errMsg) + return nil, errors.New(errMsg) + } + return nil, err + } + + err = tx.Commit() + if err != nil { + return nil, errors.Wrap(err, "Error encountered while committing transaction") + } + + return result, nil +} diff --git a/lib/server/idemix/revocationcomponent_test.go b/lib/server/idemix/revocationcomponent_test.go new file mode 100644 index 000000000..029bb67bb --- /dev/null +++ b/lib/server/idemix/revocationcomponent_test.go @@ -0,0 +1,321 @@ +/* +Copyright IBM Corp. 2018 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 idemix_test + +import ( + "testing" + + dmocks "github.com/hyperledger/fabric-ca/lib/dbutil/mocks" + . "github.com/hyperledger/fabric-ca/lib/server/idemix" + "github.com/hyperledger/fabric-ca/lib/server/idemix/mocks" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestGetRCInfoFromDBError(t *testing.T) { + ca := new(mocks.CA) + ca.On("GetName").Return("") + rcinfos := []RevocationComponentInfo{} + db := new(dmocks.FabricCADB) + db.On("Select", &rcinfos, "SELECT * FROM revocation_component_info"). + Return(errors.New("Failed to execute select query")) + ca.On("DB").Return(db) + _, err := NewRevocationComponent(ca, &CfgOptions{RevocationHandlePoolSize: 100}, 1) + assert.Error(t, err) +} + +func TestGetRCInfoFromNewDBSelectError(t *testing.T) { + ca := new(mocks.CA) + ca.On("GetName").Return("") + + db := new(dmocks.FabricCADB) + rcInfos := []RevocationComponentInfo{} + f := getSelectFunc(t, true, true) + db.On("Select", &rcInfos, SelectRCInfo).Return(f) + ca.On("DB").Return(db) + _, err := NewRevocationComponent(ca, &CfgOptions{RevocationHandlePoolSize: 100}, 1) + assert.Error(t, err) +} + +func TestGetRCInfoFromNewDBInsertFailure(t *testing.T) { + ca, db := setupForInsertTests(t) + rcinfo := RevocationComponentInfo{ + Epoch: 1, + NextRevocationHandle: 1, + LastHandleInPool: 100, + Level: 1, + } + result := new(dmocks.Result) + result.On("RowsAffected").Return(int64(0), nil) + db.On("NamedExec", InsertRCInfo, &rcinfo).Return(result, nil) + ca.On("DB").Return(db) + _, err := NewRevocationComponent(ca, &CfgOptions{RevocationHandlePoolSize: 100}, 1) + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), "Failed to insert the revocation component info record; no rows affected") + } +} + +func TestGetRCInfoFromNewDBInsertFailure1(t *testing.T) { + ca, db := setupForInsertTests(t) + rcinfo := RevocationComponentInfo{ + Epoch: 1, + NextRevocationHandle: 1, + LastHandleInPool: 100, + Level: 1, + } + result := new(dmocks.Result) + result.On("RowsAffected").Return(int64(2), nil) + db.On("NamedExec", InsertRCInfo, &rcinfo).Return(result, nil) + ca.On("DB").Return(db) + _, err := NewRevocationComponent(ca, &CfgOptions{RevocationHandlePoolSize: 100}, 1) + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), "Expected to affect 1 entry in revocation component info table but affected") + } +} + +func TestGetRCInfoFromNewDBInsertError(t *testing.T) { + ca, db := setupForInsertTests(t) + rcinfo := RevocationComponentInfo{ + Epoch: 1, + NextRevocationHandle: 1, + LastHandleInPool: 100, + Level: 1, + } + db.On("NamedExec", InsertRCInfo, &rcinfo).Return(nil, + errors.New("Inserting revocation component info into DB failed")) + ca.On("DB").Return(db) + _, err := NewRevocationComponent(ca, &CfgOptions{RevocationHandlePoolSize: 100}, 1) + assert.Error(t, err) +} + +func TestGetNewRevocationHandleSelectError(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + tx.On("Commit").Return(nil) + tx.On("Rollback").Return(nil) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextHandle).Return(UpdateNextHandle) + tx.On("Exec", UpdateNextHandle, 2, 1).Return(nil, nil) + rcInfos := []RevocationComponentInfo{} + fnc := getTxSelectFunc(t, &rcInfos, 1, true, true) + tx.On("Select", &rcInfos, SelectRCInfo).Return(fnc) + + db.On("BeginTx").Return(tx) + _, err := rc.GetNewRevocationHandle() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Failed to get revocation component info from database") +} + +func TestGetNewRevocationHandleNoData(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + tx.On("Commit").Return(nil) + tx.On("Rollback").Return(nil) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextHandle).Return(UpdateNextHandle) + tx.On("Exec", UpdateNextHandle, 2, 1).Return(nil, nil) + rcInfos := []RevocationComponentInfo{} + fnc := getTxSelectFunc(t, &rcInfos, 1, false, false) + tx.On("Select", &rcInfos, SelectRCInfo).Return(fnc) + + db.On("BeginTx").Return(tx) + _, err := rc.GetNewRevocationHandle() + assert.Error(t, err) + assert.Contains(t, err.Error(), "No revocation component info found in database") +} + +func TestGetNewRevocationHandleExecError(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + rcInfos := []RevocationComponentInfo{} + fnc := getTxSelectFunc(t, &rcInfos, 1, false, true) + tx.On("Select", &rcInfos, SelectRCInfo).Return(fnc) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextHandle).Return(UpdateNextHandle) + tx.On("Exec", UpdateNextHandle, 2, 1).Return(nil, errors.New("Exec error")) + tx.On("Commit").Return(nil) + tx.On("Rollback").Return(nil) + + db.On("BeginTx").Return(tx) + _, err := rc.GetNewRevocationHandle() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Failed to update revocation component info") +} + +func TestGetNewRevocationHandleRollbackError(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + rcInfos := []RevocationComponentInfo{} + fnc := getTxSelectFunc(t, &rcInfos, 1, false, true) + tx.On("Select", &rcInfos, SelectRCInfo).Return(fnc) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextHandle).Return(UpdateNextHandle) + tx.On("Exec", UpdateNextHandle, 2, 1).Return(nil, errors.New("Exec error")) + tx.On("Commit").Return(nil) + tx.On("Rollback").Return(errors.New("Rollback error")) + + db.On("BeginTx").Return(tx) + _, err := rc.GetNewRevocationHandle() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Error encountered while rolling back transaction") +} + +func TestGetNewRevocationHandleCommitError(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + tx.On("Commit").Return(errors.New("Error commiting")) + tx.On("Rollback").Return(nil) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextHandle).Return(UpdateNextHandle) + tx.On("Exec", UpdateNextHandle, 2, 1).Return(nil, nil) + rcInfos := []RevocationComponentInfo{} + f1 := getTxSelectFunc(t, &rcInfos, 1, false, true) + tx.On("Select", &rcInfos, SelectRCInfo).Return(f1) + + db.On("BeginTx").Return(tx) + _, err := rc.GetNewRevocationHandle() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Error encountered while committing transaction") +} + +func TestGetNewRevocationHandle(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + tx.On("Commit").Return(nil) + tx.On("Rollback").Return(nil) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextHandle).Return(UpdateNextHandle) + tx.On("Exec", UpdateNextHandle, 2, 1).Return(nil, nil) + rcInfos := []RevocationComponentInfo{} + f1 := getTxSelectFunc(t, &rcInfos, 1, false, true) + tx.On("Select", &rcInfos, SelectRCInfo).Return(f1) + + db.On("BeginTx").Return(tx) + rh, err := rc.GetNewRevocationHandle() + assert.NoError(t, err) + assert.Equal(t, 1, int(*rh)) +} + +func TestGetNewRevocationHandleLastHandle(t *testing.T) { + db := new(dmocks.FabricCADB) + rc := getRevocationComponent(t, db) + + tx := new(dmocks.FabricCATx) + tx.On("Commit").Return(nil) + tx.On("Rollback").Return(nil) + tx.On("Rebind", SelectRCInfo).Return(SelectRCInfo) + tx.On("Rebind", UpdateNextAndLastHandle).Return(UpdateNextAndLastHandle) + tx.On("Exec", UpdateNextAndLastHandle, 101, 200, 1).Return(nil, nil) + rcInfos := []RevocationComponentInfo{} + f1 := getTxSelectFunc(t, &rcInfos, 100, false, true) + tx.On("Select", &rcInfos, SelectRCInfo).Return(f1) + + db.On("BeginTx").Return(tx) + rh, err := rc.GetNewRevocationHandle() + assert.NoError(t, err) + assert.Equal(t, 100, int(*rh)) +} + +func setupForInsertTests(t *testing.T) (*mocks.CA, *dmocks.FabricCADB) { + ca := new(mocks.CA) + ca.On("GetName").Return("") + + db := new(dmocks.FabricCADB) + rcInfos := []RevocationComponentInfo{} + f := getSelectFunc(t, false, false) + db.On("Select", &rcInfos, SelectRCInfo).Return(f) + return ca, db +} + +func getRevocationComponent(t *testing.T, db *dmocks.FabricCADB) RevocationComponent { + ca := new(mocks.CA) + ca.On("GetName").Return("") + + f := getSelectFunc(t, true, false) + + rcInfosForSelect := []RevocationComponentInfo{} + db.On("Select", &rcInfosForSelect, SelectRCInfo).Return(f) + rcinfo := RevocationComponentInfo{ + Epoch: 1, + NextRevocationHandle: 1, + LastHandleInPool: 100, + Level: 1, + } + result := new(dmocks.Result) + result.On("RowsAffected").Return(int64(1), nil) + db.On("NamedExec", InsertRCInfo, &rcinfo).Return(result, nil) + ca.On("DB").Return(db) + rc, err := NewRevocationComponent(ca, &CfgOptions{RevocationHandlePoolSize: 100}, 1) + if err != nil { + t.Fatalf("Failed to get revocation component instance: %s", err.Error()) + } + return rc +} + +func getSelectFunc(t *testing.T, newDB bool, isError bool) func(interface{}, string, ...interface{}) error { + return func(dest interface{}, query string, args ...interface{}) error { + rcInfos, _ := dest.(*[]RevocationComponentInfo) + if !newDB { + rcInfo := RevocationComponentInfo{ + Epoch: 0, + NextRevocationHandle: 1, + LastHandleInPool: 100, + Level: 1, + } + *rcInfos = append(*rcInfos, rcInfo) + } + if isError { + return errors.New("Failed to get RevocationComponentInfo from DB") + } + return nil + } +} + +func getTxSelectFunc(t *testing.T, rcs *[]RevocationComponentInfo, nextRH int, isError bool, isAppend bool) func(interface{}, string, ...interface{}) error { + return func(dest interface{}, query string, args ...interface{}) error { + rcInfos := dest.(*[]RevocationComponentInfo) + rcInfo := RevocationComponentInfo{ + Epoch: 1, + NextRevocationHandle: nextRH, + LastHandleInPool: 100, + Level: 1, + } + if isAppend { + *rcInfos = append(*rcInfos, rcInfo) + *rcs = append(*rcs, rcInfo) + } + + if isError { + return errors.New("Failed to get RevocationComponentInfo from DB") + } + return nil + } +} diff --git a/lib/serverenroll.go b/lib/serverenroll.go index 868dacda2..284ae12c7 100644 --- a/lib/serverenroll.go +++ b/lib/serverenroll.go @@ -61,7 +61,7 @@ type enrollmentResponseNet struct { // Base64 encoded PEM-encoded ECert Cert string // The server information - ServerInfo serverInfoResponseNet + ServerInfo ServerInfoResponseNet } func newEnrollEndpoint(s *Server) *serverEndpoint { diff --git a/lib/serveridemixenroll.go b/lib/serveridemixenroll.go new file mode 100644 index 000000000..48ae0c529 --- /dev/null +++ b/lib/serveridemixenroll.go @@ -0,0 +1,99 @@ +/* +Copyright IBM Corp. 2018 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 ( + "github.com/cloudflare/cfssl/log" + "github.com/hyperledger/fabric-ca/lib/server/idemix" + "github.com/hyperledger/fabric-ca/lib/spi" +) + +// IdemixEnrollmentResponseNet is the idemix enrollment response from the server +type IdemixEnrollmentResponseNet struct { + // Base64 encoding of idemix Credential + Credential string + // Attribute name-value pairs + Attrs map[string]string + // Base64 encoding of Credential Revocation list + //CRL string + // Base64 encoding of the issuer nonce + Nonce string + // The server information + ServerInfo ServerInfoResponseNet +} + +func newIdemixEnrollEndpoint(s *Server) *serverEndpoint { + return &serverEndpoint{ + Methods: []string{"POST"}, + Handler: handleIdemixEnrollReq, + Server: s, + successRC: 201, + } +} + +// handleIdemixEnrollReq handles an Idemix enroll request +func handleIdemixEnrollReq(ctx *serverRequestContextImpl) (interface{}, error) { + _, _, isBasicAuth := ctx.req.BasicAuth() + handler := idemix.EnrollRequestHandler{ + Ctx: &idemixServerCtx{ctx}, + IsBasicAuth: isBasicAuth, + IdmxLib: idemix.NewLib(), + } + + idemixEnrollResp, err := handler.HandleIdemixEnroll() + if err != nil { + log.Errorf("Error processing the /idemix/credential request: %s", err.Error()) + return nil, err + } + resp := newIdemixEnrollmentResponseNet(idemixEnrollResp) + err = ctx.ca.fillCAInfo(&resp.ServerInfo) + if err != nil { + return nil, err + } + return resp, nil +} + +// newIdemixEnrollmentResponseNet returns an instance of IdemixEnrollmentResponseNet that is +// constructed using the specified idemix.EnrollmentResponse object +func newIdemixEnrollmentResponseNet(resp *idemix.EnrollmentResponse) IdemixEnrollmentResponseNet { + return IdemixEnrollmentResponseNet{ + Nonce: resp.Nonce, + Attrs: resp.Attrs, + Credential: resp.Credential, + ServerInfo: ServerInfoResponseNet{}} +} + +// idemixServerCtx implements idemix.ServerRequestContext +type idemixServerCtx struct { + srvCtx *serverRequestContextImpl +} + +func (c *idemixServerCtx) BasicAuthentication() (string, error) { + return c.srvCtx.BasicAuthentication() +} +func (c *idemixServerCtx) TokenAuthentication() (string, error) { + return c.srvCtx.TokenAuthentication() +} +func (c *idemixServerCtx) GetCA() (idemix.CA, error) { + return c.srvCtx.GetCA() +} +func (c *idemixServerCtx) GetCaller() (spi.User, error) { + return c.srvCtx.GetCaller() +} +func (c *idemixServerCtx) ReadBody(body interface{}) error { + return c.srvCtx.ReadBody(body) +} diff --git a/lib/serverinfo.go b/lib/serverinfo.go index 024a5bee2..89167cd02 100644 --- a/lib/serverinfo.go +++ b/lib/serverinfo.go @@ -20,8 +20,8 @@ import ( "github.com/hyperledger/fabric-ca/lib/metadata" ) -// The response to the GET /info request -type serverInfoResponseNet struct { +// ServerInfoResponseNet is the response to the GET /cainfo request +type ServerInfoResponseNet struct { // CAName is a unique name associated with fabric-ca-server's CA CAName string // Base64 encoding of PEM-encoded certificate chain @@ -46,7 +46,7 @@ func cainfoHandler(ctx *serverRequestContextImpl) (interface{}, error) { if err != nil { return nil, err } - resp := &serverInfoResponseNet{} + resp := &ServerInfoResponseNet{} err = ca.fillCAInfo(resp) if err != nil { return nil, err diff --git a/testdata/IdemixPublicKey b/testdata/IdemixPublicKey new file mode 100644 index 0000000000000000000000000000000000000000..1bad442c914aec29af7c4c61fb6fafd1a3789876 GIT binary patch literal 843 zcmV-R1GM}K0#8*61X6EoWeN;MZgOvIY;9$3bV)=C5K?7!Z)0I}X>V>wVQyq>WfDXR zAoN<%VSm#MGCxAunc;dYTOfa?H`(|#4j1;@&91gd;1VD<_eC(yvMhglrwFnwpgSj? zMK=LPCj*W3lFwD~NvHH0L<%78-Ds~Z$#TRC_(tQXu_Hy^5Tsf<(iKeRZh8~Sa726( zAW%3LE&KOAIF{%sk0|wnb35i$F^S}_%4ay}y> zL<%4~rF42q%f&NBQ?e;j0n1T2I5DkXTIQ`dv{@o~8Bcx^AO+{htNmVUUR{Ft;^qkx z*WXyvHFtrnH0!=5$yrefDVm#=1#?SN_Psa_N&2!t5vq z77`$2dH|Tc_5iE5WfI6t+@;!33#i@rKH^7J0xLa22`|AWd5v44+k^2%L_p71oN=e_M?-+zh zKSAm2RDi$+3?X!%oGaBq&(fH~^xeCHU zOeGq9R}zFR1@eVG;*HE8{$U625+MCdgSyr&Y$gYGRVMnvTt*@Eiow1oGKn?$0sZ5N z?Akg+3LvSjR>2qv>oK1;zT6T(TJaUgHd39P4V%&(?3lB!tjrQ1{%TOb+kNSUwz)|Z z4kGsrK|SV7JozY3iR1_VXbnSlLLi3Q-aXC%L3L%i8Gtn$XbbOnyg81_LM_P^9`boCT!+x}W0tt_dieP`B-QXr|Mnk!d4h|*c