1 Star 1 Fork 0

Hyperledger Fabric 国密 / fabric

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
idemixmsp.go 20.48 KB
一键复制 编辑 原始数据 按行查看 历史
Jtyoui 提交于 2021-07-22 15:59 . 国密
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package msp
import (
"bytes"
"encoding/hex"
"fmt"
"time"
"github.com/golang/protobuf/proto"
"gitee.com/hyperledger-fabric-gm/fabric/bccsp"
idemixbccsp "gitee.com/hyperledger-fabric-gm/fabric/bccsp/idemix"
"gitee.com/hyperledger-fabric-gm/fabric/bccsp/sw"
m "gitee.com/hyperledger-fabric-gm/fabric/protos/msp"
"github.com/pkg/errors"
"go.uber.org/zap/zapcore"
)
const (
// AttributeIndexOU contains the index of the OU attribute in the idemix credential attributes
AttributeIndexOU = iota
// AttributeIndexRole contains the index of the Role attribute in the idemix credential attributes
AttributeIndexRole
// AttributeIndexEnrollmentId contains the index of the Enrollment ID attribute in the idemix credential attributes
AttributeIndexEnrollmentId
// AttributeIndexRevocationHandle contains the index of the Revocation Handle attribute in the idemix credential attributes
AttributeIndexRevocationHandle
)
const (
// AttributeNameOU is the attribute name of the Organization Unit attribute
AttributeNameOU = "OU"
// AttributeNameRole is the attribute name of the Role attribute
AttributeNameRole = "Role"
// AttributeNameEnrollmentId is the attribute name of the Enrollment ID attribute
AttributeNameEnrollmentId = "EnrollmentID"
// AttributeNameRevocationHandle is the attribute name of the revocation handle attribute
AttributeNameRevocationHandle = "RevocationHandle"
)
// index of the revocation handle attribute in the credential
const rhIndex = 3
// discloseFlags will be passed to the idemix signing and verification routines.
// It informs idemix to disclose both attributes (OU and Role) when signing,
// while hiding attributes EnrollmentID and RevocationHandle.
var discloseFlags = []byte{1, 1, 0, 0}
type idemixmsp struct {
csp bccsp.BCCSP
version MSPVersion
ipk bccsp.Key
signer *idemixSigningIdentity
name string
revocationPK bccsp.Key
epoch int
}
// newIdemixMsp creates a new instance of idemixmsp
func newIdemixMsp(version MSPVersion) (MSP, error) {
mspLogger.Debugf("Creating Idemix-based MSP instance")
csp, err := idemixbccsp.New(sw.NewDummyKeyStore())
if err != nil {
panic(fmt.Sprintf("unexpected condition, error received [%s]", err))
}
msp := idemixmsp{csp: csp}
msp.version = version
return &msp, nil
}
func (msp *idemixmsp) Setup(conf1 *m.MSPConfig) error {
mspLogger.Debugf("Setting up Idemix-based MSP instance")
if conf1 == nil {
return errors.Errorf("setup error: nil conf reference")
}
if conf1.Type != int32(IDEMIX) {
return errors.Errorf("setup error: config is not of type IDEMIX")
}
var conf m.IdemixMSPConfig
err := proto.Unmarshal(conf1.Config, &conf)
if err != nil {
return errors.Wrap(err, "failed unmarshalling idemix msp config")
}
msp.name = conf.Name
mspLogger.Debugf("Setting up Idemix MSP instance %s", msp.name)
// Import Issuer Public Key
IssuerPublicKey, err := msp.csp.KeyImport(
conf.Ipk,
&bccsp.IdemixIssuerPublicKeyImportOpts{
Temporary: true,
AttributeNames: []string{
AttributeNameOU,
AttributeNameRole,
AttributeNameEnrollmentId,
AttributeNameRevocationHandle,
},
})
if err != nil {
importErr, ok := errors.Cause(err).(*bccsp.IdemixIssuerPublicKeyImporterError)
if !ok {
panic("unexpected condition, BCCSP did not return the expected *bccsp.IdemixIssuerPublicKeyImporterError")
}
switch importErr.Type {
case bccsp.IdemixIssuerPublicKeyImporterUnmarshallingError:
return errors.WithMessage(err, "failed to unmarshal ipk from idemix msp config")
case bccsp.IdemixIssuerPublicKeyImporterHashError:
return errors.WithMessage(err, "setting the hash of the issuer public key failed")
case bccsp.IdemixIssuerPublicKeyImporterValidationError:
return errors.WithMessage(err, "cannot setup idemix msp with invalid public key")
case bccsp.IdemixIssuerPublicKeyImporterNumAttributesError:
fallthrough
case bccsp.IdemixIssuerPublicKeyImporterAttributeNameError:
return errors.Errorf("issuer public key must have have attributes OU, Role, EnrollmentId, and RevocationHandle")
default:
panic(fmt.Sprintf("unexpected condtion, issuer public key import error not valid, got [%d]", importErr.Type))
}
}
msp.ipk = IssuerPublicKey
// Import revocation public key
RevocationPublicKey, err := msp.csp.KeyImport(
conf.RevocationPk,
&bccsp.IdemixRevocationPublicKeyImportOpts{Temporary: true},
)
if err != nil {
return errors.WithMessage(err, "failed to import revocation public key")
}
msp.revocationPK = RevocationPublicKey
if conf.Signer == nil {
// No credential in config, so we don't setup a default signer
mspLogger.Debug("idemix msp setup as verification only msp (no key material found)")
return nil
}
// A credential is present in the config, so we setup a default signer
// Import User secret key
UserKey, err := msp.csp.KeyImport(conf.Signer.Sk, &bccsp.IdemixUserSecretKeyImportOpts{Temporary: true})
if err != nil {
return errors.WithMessage(err, "failed importing signer secret key")
}
// Derive NymPublicKey
NymKey, err := msp.csp.KeyDeriv(UserKey, &bccsp.IdemixNymKeyDerivationOpts{Temporary: true, IssuerPK: IssuerPublicKey})
if err != nil {
return errors.WithMessage(err, "failed deriving nym")
}
NymPublicKey, err := NymKey.PublicKey()
if err != nil {
return errors.Wrapf(err, "failed getting public nym key")
}
role := &m.MSPRole{
MspIdentifier: msp.name,
Role: m.MSPRole_MEMBER,
}
if checkRole(int(conf.Signer.Role), ADMIN) {
role.Role = m.MSPRole_ADMIN
}
ou := &m.OrganizationUnit{
MspIdentifier: msp.name,
OrganizationalUnitIdentifier: conf.Signer.OrganizationalUnitIdentifier,
CertifiersIdentifier: IssuerPublicKey.SKI(),
}
enrollmentId := conf.Signer.EnrollmentId
// Verify credential
valid, err := msp.csp.Verify(
UserKey,
conf.Signer.Cred,
nil,
&bccsp.IdemixCredentialSignerOpts{
IssuerPK: IssuerPublicKey,
Attributes: []bccsp.IdemixAttribute{
{Type: bccsp.IdemixBytesAttribute, Value: []byte(conf.Signer.OrganizationalUnitIdentifier)},
{Type: bccsp.IdemixIntAttribute, Value: getIdemixRoleFromMSPRole(role)},
{Type: bccsp.IdemixBytesAttribute, Value: []byte(enrollmentId)},
{Type: bccsp.IdemixHiddenAttribute},
},
},
)
if err != nil || !valid {
return errors.WithMessage(err, "Credential is not cryptographically valid")
}
// Create the cryptographic evidence that this identity is valid
proof, err := msp.csp.Sign(
UserKey,
nil,
&bccsp.IdemixSignerOpts{
Credential: conf.Signer.Cred,
Nym: NymKey,
IssuerPK: IssuerPublicKey,
Attributes: []bccsp.IdemixAttribute{
{Type: bccsp.IdemixBytesAttribute},
{Type: bccsp.IdemixIntAttribute},
{Type: bccsp.IdemixHiddenAttribute},
{Type: bccsp.IdemixHiddenAttribute},
},
RhIndex: rhIndex,
CRI: conf.Signer.CredentialRevocationInformation,
},
)
if err != nil {
return errors.WithMessage(err, "Failed to setup cryptographic proof of identity")
}
// Set up default signer
msp.signer = &idemixSigningIdentity{
idemixidentity: newIdemixIdentity(msp, NymPublicKey, role, ou, proof),
Cred: conf.Signer.Cred,
UserKey: UserKey,
NymKey: NymKey,
enrollmentId: enrollmentId}
return nil
}
// GetVersion returns the version of this MSP
func (msp *idemixmsp) GetVersion() MSPVersion {
return msp.version
}
func (msp *idemixmsp) GetType() ProviderType {
return IDEMIX
}
func (msp *idemixmsp) GetIdentifier() (string, error) {
return msp.name, nil
}
func (msp *idemixmsp) GetSigningIdentity(identifier *IdentityIdentifier) (SigningIdentity, error) {
return nil, errors.Errorf("GetSigningIdentity not implemented")
}
func (msp *idemixmsp) GetDefaultSigningIdentity() (SigningIdentity, error) {
mspLogger.Debugf("Obtaining default idemix signing identity")
if msp.signer == nil {
return nil, errors.Errorf("no default signer setup")
}
return msp.signer, nil
}
func (msp *idemixmsp) DeserializeIdentity(serializedID []byte) (Identity, error) {
sID := &m.SerializedIdentity{}
err := proto.Unmarshal(serializedID, sID)
if err != nil {
return nil, errors.Wrap(err, "could not deserialize a SerializedIdentity")
}
if sID.Mspid != msp.name {
return nil, errors.Errorf("expected MSP ID %s, received %s", msp.name, sID.Mspid)
}
return msp.deserializeIdentityInternal(sID.GetIdBytes())
}
func (msp *idemixmsp) deserializeIdentityInternal(serializedID []byte) (Identity, error) {
mspLogger.Debug("idemixmsp: deserializing identity")
serialized := new(m.SerializedIdemixIdentity)
err := proto.Unmarshal(serializedID, serialized)
if err != nil {
return nil, errors.Wrap(err, "could not deserialize a SerializedIdemixIdentity")
}
if serialized.NymX == nil || serialized.NymY == nil {
return nil, errors.Errorf("unable to deserialize idemix identity: pseudonym is invalid")
}
// Import NymPublicKey
var rawNymPublicKey []byte
rawNymPublicKey = append(rawNymPublicKey, serialized.NymX...)
rawNymPublicKey = append(rawNymPublicKey, serialized.NymY...)
NymPublicKey, err := msp.csp.KeyImport(
rawNymPublicKey,
&bccsp.IdemixNymPublicKeyImportOpts{Temporary: true},
)
if err != nil {
return nil, errors.WithMessage(err, "failed to import nym public key")
}
// OU
ou := &m.OrganizationUnit{}
err = proto.Unmarshal(serialized.Ou, ou)
if err != nil {
return nil, errors.Wrap(err, "cannot deserialize the OU of the identity")
}
// Role
role := &m.MSPRole{}
err = proto.Unmarshal(serialized.Role, role)
if err != nil {
return nil, errors.Wrap(err, "cannot deserialize the role of the identity")
}
return newIdemixIdentity(msp, NymPublicKey, role, ou, serialized.Proof), nil
}
func (msp *idemixmsp) Validate(id Identity) error {
var identity *idemixidentity
switch t := id.(type) {
case *idemixidentity:
identity = id.(*idemixidentity)
case *idemixSigningIdentity:
identity = id.(*idemixSigningIdentity).idemixidentity
default:
return errors.Errorf("identity type %T is not recognized", t)
}
mspLogger.Debugf("Validating identity %+v", identity)
if identity.GetMSPIdentifier() != msp.name {
return errors.Errorf("the supplied identity does not belong to this msp")
}
return identity.verifyProof()
}
func (id *idemixidentity) verifyProof() error {
// Verify signature
valid, err := id.msp.csp.Verify(
id.msp.ipk,
id.associationProof,
nil,
&bccsp.IdemixSignerOpts{
RevocationPublicKey: id.msp.revocationPK,
Attributes: []bccsp.IdemixAttribute{
{Type: bccsp.IdemixBytesAttribute, Value: []byte(id.OU.OrganizationalUnitIdentifier)},
{Type: bccsp.IdemixIntAttribute, Value: getIdemixRoleFromMSPRole(id.Role)},
{Type: bccsp.IdemixHiddenAttribute},
{Type: bccsp.IdemixHiddenAttribute},
},
RhIndex: rhIndex,
Epoch: id.msp.epoch,
},
)
if err == nil && !valid {
panic("unexpected condition, an error should be returned for an invalid signature")
}
return err
}
func (msp *idemixmsp) SatisfiesPrincipal(id Identity, principal *m.MSPPrincipal) error {
err := msp.Validate(id)
if err != nil {
return errors.Wrap(err, "identity is not valid with respect to this MSP")
}
return msp.satisfiesPrincipalValidated(id, principal)
}
// satisfiesPrincipalValidated performs all the tasks of satisfiesPrincipal except the identity validation,
// such that combined principals will not cause multiple expensive identity validations.
func (msp *idemixmsp) satisfiesPrincipalValidated(id Identity, principal *m.MSPPrincipal) error {
switch principal.PrincipalClassification {
// in this case, we have to check whether the
// identity has a role in the msp - member or admin
case m.MSPPrincipal_ROLE:
// Principal contains the msp role
mspRole := &m.MSPRole{}
err := proto.Unmarshal(principal.Principal, mspRole)
if err != nil {
return errors.Wrap(err, "could not unmarshal MSPRole from principal")
}
// at first, we check whether the MSP
// identifier is the same as that of the identity
if mspRole.MspIdentifier != msp.name {
return errors.Errorf("the identity is a member of a different MSP (expected %s, got %s)", mspRole.MspIdentifier, id.GetMSPIdentifier())
}
// now we validate the different msp roles
switch mspRole.Role {
case m.MSPRole_MEMBER:
// in the case of member, we simply check
// whether this identity is valid for the MSP
mspLogger.Debugf("Checking if identity satisfies MEMBER role for %s", msp.name)
return nil
case m.MSPRole_ADMIN:
mspLogger.Debugf("Checking if identity satisfies ADMIN role for %s", msp.name)
if id.(*idemixidentity).Role.Role != m.MSPRole_ADMIN {
return errors.Errorf("user is not an admin")
}
return nil
case m.MSPRole_PEER:
if msp.version >= MSPv1_3 {
return errors.Errorf("idemixmsp only supports client use, so it cannot satisfy an MSPRole PEER principal")
}
fallthrough
case m.MSPRole_CLIENT:
if msp.version >= MSPv1_3 {
return nil // any valid idemixmsp member must be a client
}
fallthrough
default:
return errors.Errorf("invalid MSP role type %d", int32(mspRole.Role))
}
// in this case we have to serialize this instance
// and compare it byte-by-byte with Principal
case m.MSPPrincipal_IDENTITY:
mspLogger.Debugf("Checking if identity satisfies IDENTITY principal")
idBytes, err := id.Serialize()
if err != nil {
return errors.Wrap(err, "could not serialize this identity instance")
}
rv := bytes.Compare(idBytes, principal.Principal)
if rv == 0 {
return nil
}
return errors.Errorf("the identities do not match")
case m.MSPPrincipal_ORGANIZATION_UNIT:
ou := &m.OrganizationUnit{}
err := proto.Unmarshal(principal.Principal, ou)
if err != nil {
return errors.Wrap(err, "could not unmarshal OU from principal")
}
mspLogger.Debugf("Checking if identity is part of OU \"%s\" of mspid \"%s\"", ou.OrganizationalUnitIdentifier, ou.MspIdentifier)
// at first, we check whether the MSP
// identifier is the same as that of the identity
if ou.MspIdentifier != msp.name {
return errors.Errorf("the identity is a member of a different MSP (expected %s, got %s)", ou.MspIdentifier, id.GetMSPIdentifier())
}
if ou.OrganizationalUnitIdentifier != id.(*idemixidentity).OU.OrganizationalUnitIdentifier {
return errors.Errorf("user is not part of the desired organizational unit")
}
return nil
case m.MSPPrincipal_COMBINED:
if msp.version <= MSPv1_1 {
return errors.Errorf("Combined MSP Principals are unsupported in MSPv1_1")
}
// Principal is a combination of multiple principals.
principals := &m.CombinedPrincipal{}
err := proto.Unmarshal(principal.Principal, principals)
if err != nil {
return errors.Wrap(err, "could not unmarshal CombinedPrincipal from principal")
}
// Return an error if there are no principals in the combined principal.
if len(principals.Principals) == 0 {
return errors.New("no principals in CombinedPrincipal")
}
// Recursively call msp.SatisfiesPrincipal for all combined principals.
// There is no limit for the levels of nesting for the combined principals.
for _, cp := range principals.Principals {
err = msp.satisfiesPrincipalValidated(id, cp)
if err != nil {
return err
}
}
// The identity satisfies all the principals
return nil
case m.MSPPrincipal_ANONYMITY:
if msp.version <= MSPv1_1 {
return errors.Errorf("Anonymity MSP Principals are unsupported in MSPv1_1")
}
anon := &m.MSPIdentityAnonymity{}
err := proto.Unmarshal(principal.Principal, anon)
if err != nil {
return errors.Wrap(err, "could not unmarshal MSPIdentityAnonymity from principal")
}
switch anon.AnonymityType {
case m.MSPIdentityAnonymity_ANONYMOUS:
return nil
case m.MSPIdentityAnonymity_NOMINAL:
return errors.New("principal is nominal, but idemix MSP is anonymous")
default:
return errors.Errorf("unknown principal anonymity type: %d", anon.AnonymityType)
}
default:
return errors.Errorf("invalid principal type %d", int32(principal.PrincipalClassification))
}
}
// IsWellFormed checks if the given identity can be deserialized into its provider-specific .
// In this MSP implementation, an identity is considered well formed if it contains a
// marshaled SerializedIdemixIdentity protobuf message.
func (id *idemixmsp) IsWellFormed(identity *m.SerializedIdentity) error {
sId := new(m.SerializedIdemixIdentity)
err := proto.Unmarshal(identity.IdBytes, sId)
if err != nil {
return errors.Wrap(err, "not an idemix identity")
}
return nil
}
func (msp *idemixmsp) GetTLSRootCerts() [][]byte {
// TODO
return nil
}
func (msp *idemixmsp) GetTLSIntermediateCerts() [][]byte {
// TODO
return nil
}
type idemixidentity struct {
NymPublicKey bccsp.Key
msp *idemixmsp
id *IdentityIdentifier
Role *m.MSPRole
OU *m.OrganizationUnit
// associationProof contains cryptographic proof that this identity
// belongs to the MSP id.msp, i.e., it proves that the pseudonym
// is constructed from a secret key on which the CA issued a credential.
associationProof []byte
}
func (id *idemixidentity) Anonymous() bool {
return true
}
func newIdemixIdentity(msp *idemixmsp, NymPublicKey bccsp.Key, role *m.MSPRole, ou *m.OrganizationUnit, proof []byte) *idemixidentity {
id := &idemixidentity{}
id.NymPublicKey = NymPublicKey
id.msp = msp
id.Role = role
id.OU = ou
id.associationProof = proof
raw, err := NymPublicKey.Bytes()
if err != nil {
panic(fmt.Sprintf("unexpected condition, failed marshalling nym public key [%s]", err))
}
id.id = &IdentityIdentifier{
Mspid: msp.name,
Id: bytes.NewBuffer(raw).String(),
}
return id
}
func (id *idemixidentity) ExpiresAt() time.Time {
// Idemix MSP currently does not use expiration dates or revocation,
// so we return the zero time to indicate this.
return time.Time{}
}
func (id *idemixidentity) GetIdentifier() *IdentityIdentifier {
return id.id
}
func (id *idemixidentity) GetMSPIdentifier() string {
mspid, _ := id.msp.GetIdentifier()
return mspid
}
func (id *idemixidentity) GetOrganizationalUnits() []*OUIdentifier {
// we use the (serialized) public key of this MSP as the CertifiersIdentifier
certifiersIdentifier, err := id.msp.ipk.Bytes()
if err != nil {
mspIdentityLogger.Errorf("Failed to marshal ipk in GetOrganizationalUnits: %s", err)
return nil
}
return []*OUIdentifier{{certifiersIdentifier, id.OU.OrganizationalUnitIdentifier}}
}
func (id *idemixidentity) Validate() error {
return id.msp.Validate(id)
}
func (id *idemixidentity) Verify(msg []byte, sig []byte) error {
if mspIdentityLogger.IsEnabledFor(zapcore.DebugLevel) {
mspIdentityLogger.Debugf("Verify Idemix sig: msg = %s", hex.Dump(msg))
mspIdentityLogger.Debugf("Verify Idemix sig: sig = %s", hex.Dump(sig))
}
_, err := id.msp.csp.Verify(
id.NymPublicKey,
sig,
msg,
&bccsp.IdemixNymSignerOpts{
IssuerPK: id.msp.ipk,
},
)
return err
}
func (id *idemixidentity) SatisfiesPrincipal(principal *m.MSPPrincipal) error {
return id.msp.SatisfiesPrincipal(id, principal)
}
func (id *idemixidentity) Serialize() ([]byte, error) {
serialized := &m.SerializedIdemixIdentity{}
raw, err := id.NymPublicKey.Bytes()
if err != nil {
return nil, errors.Wrapf(err, "could not serialize nym of identity %s", id.id)
}
// This is an assumption on how the underlying idemix implementation work.
// TODO: change this in future version
serialized.NymX = raw[:len(raw)/2]
serialized.NymY = raw[len(raw)/2:]
ouBytes, err := proto.Marshal(id.OU)
if err != nil {
return nil, errors.Wrapf(err, "could not marshal OU of identity %s", id.id)
}
roleBytes, err := proto.Marshal(id.Role)
if err != nil {
return nil, errors.Wrapf(err, "could not marshal role of identity %s", id.id)
}
serialized.Ou = ouBytes
serialized.Role = roleBytes
serialized.Proof = id.associationProof
idemixIDBytes, err := proto.Marshal(serialized)
if err != nil {
return nil, err
}
sID := &m.SerializedIdentity{Mspid: id.GetMSPIdentifier(), IdBytes: idemixIDBytes}
idBytes, err := proto.Marshal(sID)
if err != nil {
return nil, errors.Wrapf(err, "could not marshal a SerializedIdentity structure for identity %s", id.id)
}
return idBytes, nil
}
type idemixSigningIdentity struct {
*idemixidentity
Cred []byte
UserKey bccsp.Key
NymKey bccsp.Key
enrollmentId string
}
func (id *idemixSigningIdentity) Sign(msg []byte) ([]byte, error) {
mspLogger.Debugf("Idemix identity %s is signing", id.GetIdentifier())
sig, err := id.msp.csp.Sign(
id.UserKey,
msg,
&bccsp.IdemixNymSignerOpts{
Nym: id.NymKey,
IssuerPK: id.msp.ipk,
},
)
if err != nil {
return nil, err
}
return sig, nil
}
func (id *idemixSigningIdentity) GetPublicVersion() Identity {
return id.idemixidentity
}
Go
1
https://gitee.com/hyperledger-fabric-gm/fabric.git
git@gitee.com:hyperledger-fabric-gm/fabric.git
hyperledger-fabric-gm
fabric
fabric
v1.4.9

搜索帮助