fabric源码解析26——验证体系

fabric源码解析26——验证体系

概述

fabric的验证体系是一个比较大也比较宽泛的话题,本文中尽量叙述,但肯定会有不到之处或模糊错误之处,因为笔者现在落笔的感觉仍是不够透彻。验证的最最基本的过程就是:外界提供数据,fabric(经过计算)进行核对,有效的数据(对象)通过,无效的屏蔽。而验证体系中所要分析清楚的地方也可以浓缩为一句话:在什么时候为了什么而使用什么工具验证了什么。《fabric源码12-13》两篇分别讲述了fabric在验证过程中使用到的两个主要的验证工具MSP和BCCSP,在此建议读者先温故一下。

另外,若是和笔者一样,是数字签名体系技术的小白,在此建议先读一下Alice和Bob的故事:https://blog.csdn.net/u014419512/article/details/26290821 ,另外拓展的推荐《深入浅出密码学——常用加密技术原理与应用》这本书。如此,密码学和签名技术方面是可以单独成书的主题,所以笔者只会班门弄斧般很粗线头的描述,具体还请自行学习。

下面均以SoftWare(SW)形式的BCCSP、escda加密算法、SHA256哈希算法为例。验证体系大体分为交易验证,网络验证,策略验证三部分,下文将分而述之。

peer结点本地BCCSP和MSP的初始化

fabric的验证体系依赖于BCCSP和MSP对象,BCCSP提供各种哈希和加密算法,MSP持有(并使用)BCCSP且提供身份管理。在fabric中,能实质代表一个身份的是数字证书,这些数字证书位于容器的/var/hyperledger/production/msp目录下。持有数字证书的是MSP对象,而每个peer结点的MSP对象在生成peer结点时已经被编译入peer程序实体中:

  1. 在peer/main.go中,main -> common.InitCrypto(mspMgrConfigDir, mspID),其中mspMgrConfigDir是MSP配置所在的路径,来自于core.yaml中的mspConfigPath配置项,mspID为MSP对象的ID,来自于core.yaml中的localMspId配置项 -> peer/common/common.go的InitCrypto中,viperutil.EnhancedExactUnmarshalKey("peer.BCCSP", &bccspConfig)整体读取了core.yaml中BCSSP的全部配置,mspmgmt.LoadLocalMsp(mspMgrConfigDir, bccspConfig, localMSPID)使用MSP配置路径,MSPID和BCSSP配置,创建自己的MSP。
  2. 在msp/mgmt/mgmt.go的LoadLocalMsp中,conf, err := msp.GetLocalMspConfig(dir, bccspConfig, mspID) -> msp/configbuilder.go的GetLocalMspConfig中,首先确定了用于验证的公匙目录signcertDir用于签名的私匙目录keystoreDir,然后据此factory.InitFactories(bccspConfig)初始化了MSP所使用的BCCSP对象(BCCSP使用的是私匙),signcert, err := getPemMaterialFromDir(signcertDir)读取了所有用于签名的公匙,getMspConfig()则彻底将各种证书(参看sampleconfig/msp下的各个目录,如admincerts下的管理员证书,cacerts下的peer结点的根证书,tlscacerts下的TLS证书)读取并放入MSP的配置对象MSPConfig中返回 -> 重回msp/mgmt/mgmt.go的LoadLocalMsp中,获取MSP的配置项conf后,GetLocalMSP().Setup(conf)获取了同文件中的全局变量localMsp,即peer结点使用的MSP对象,然后使用conf初始化localMsp(主要是调用getCertFromPem将配置中的证书数据都转化为了x509格式证书。

在使用cryptogen工具生成一个组织可用的msp时,生成的各种证书的拓扑结构如下图所示(以组织org1和组织中的一个节点peer0为例):

cryptogen-certs-topology.png

基本拓扑结构即是一个组织会生成一个CA证书,一个TLS CA证书。一个管理员身份证书,若干个普通用户身份证书。一个peer节点会生成一个代表自己的身份证书,一个自己作为服务端的tls server证书。组织的节点、管理员、普通用户的身份证书均有CA签发,tls server证书均有TLS CA签发。

交易(身份)验证

身份验证和交易验证几乎等价,一个peer点发送的交易的前提是自身得加入到channel中,而每笔交易中也会验证发送交易的peer的签名。这里选取名字为媛媛的peer点加入channel的交易为例子进行说明。注意,这里的交易验证只指那些不跨组织(即不跨MSP)的交易,如同组织内的背书交易,同组织内的gossip散播block的交易。至于需要多个组织的主体签名的交易的验证,如升级配置,则涉及策略验证,会专门在策略验证章节讲述。

  1. 执行peer channel join -b mychannel.block命令,在peer/channel/join.go的join -> executeJoin -> signedProp, err = putils.GetSignedProposal(prop, cf.Signer),在准备向背书结点发送的交易数据包时,得到的是已经签名过的消息signedProp,这里传入的cf.Signer其实就是媛媛的签名身份对象。
  2. cf.Signer由peer/common/common.go中的GetDefaultSigner()得到,可以看到获取的就是媛媛的本地MSP的中包含的签名对象msp.signer。msp.signer是媛媛初始化自己的MSP时就形成的,具体的还是回到msp/configbuilder.go的GetLocalMspConfig中,sigid := &msp.SigningIdentityInfo{PublicSigner: signcert[0], PrivateSigner: nil}msp/signcerts中一个公匙证书赋值给sigid,随后sigid作为参数传入getMspConfig(dir, ID, sigid) -> 几经辗转,sigid被传入msp/mspimpl.go的getSigningIdentityFromConf中,在这个函数中 -> idPub, pubKey, err := msp.getIdentityFromConf(sidInfo.PublicSigner)获取了媛媛的身份ID证书公匙
  3. getIdentityFromConf中,cert, err := msp.getCertFromPem(idBytes)将二进制的证书数据转化为了x509.Certificate格式 -> certPubK, err := msp.bccsp.KeyImport(...)抽取了证书中所包含的公匙(且不存储至KeyStore中,公匙certPubK为ecdsa.PublicKey格式) -> digest, err := msp.bccsp.Hash(cert.Raw, hashOpt)根据证书和媛媛的BCCSP关于身份ID的哈希选项hashOpt(SHA256),获取证书的摘要,这个摘要是用于hex.EncodeToString(digest)以作为媛媛的标识ID使用的 -> mspId, err := newIdentity(id, cert, certPubK, msp)根据标识ID、证书、证书公匙、所接受的管理的MSP对象,共同组成一个身份ID(identity),即媛媛的在交易中的身份mspId -> return mspId, certPubK, nil将媛媛的身份ID、证书公匙一起返回。
  4. 重回第2步中,获取媛媛的身份ID和证书公匙后,privKey, err := msp.bccsp.GetKey(pubKey.SKI()),利用公匙的SKI,通过BCCSP的私匙库keystore寻找到对应的私匙privKey(包裹了ecdsa.PrivateKey的ecdsaPrivateKey)。私匙库keystore是在BCCSP初始化的时候生成的,sw类型的BCCSP,具体可定位到bccsp/factory/swfactory.go的Get中,fks, err := sw.NewFileBasedKeyStore(...)生成了一个私匙库keystore对象,该对象记录了媛媛的私匙所在的目录(在msp/keystore中) -> peerSigner, err := signer.New(msp.bccsp, privKey)使用媛媛的BCCSP、私匙、私匙携带的公匙生成一支“签名笔”,注意这里只接受非对称加密算法的私匙。
  5. 媛媛有了“签名笔”后,digest, err := msp.bccsp.Hash(idPub.(*identity).cert.Raw, hashOpt)使用媛媛的证书和同样的哈希选项hashOpt计算出媛媛证书的摘要 -> id := &IdentityIdentifier{...} -> newSigningIdentity(...)使用媛媛的“签名笔”和其他数据(均是媛媛身份ID中的数据),生成媛媛的签名身份(signingidentity)。这个签名身份其实就是媛媛的身份ID+“签名笔”。这个签名身份一路返回,最终赋值给了媛媛的MSP的signer成员,该成员即是第1步用于签名媛媛加入channel的申请消息的cf.Signer。
  6. 重回第1步,定位到protos/utils/txutils.go的GetSignedProposal中,propBytes, err := GetBytesProposal(prop)将申请消息简单的Marshal后 -> signature, err := signer.Sign(propBytes)媛媛使用了自己的签名身份对申请消息进行签名 -> return &peer.SignedProposal{...}将签名和原始申请一同封装到签名申请中返回。
  7. 第6步的签名过程可以定位到msp/identities.go的signingidentity的Sign(msg)接口:digest, err := id.msp.bccsp.Hash(msg, hashOpt)使用预定的哈希算法将消息哈希得到摘要 -> id.signer.Sign(rand.Reader, digest, nil)将随机数、摘要、nil(签名选项)传入“签名笔”进行签名 -> 定位到bccsp/signer/signer.go的bccspCryptoSigner的Sign接口中,s.csp.Sign(s.key, digest, opts)将媛媛的私匙、申请摘要、nil传入 -> 一路辗转,最终定位到bccsp/sw/ecdsa.go的ecdsaSigner的Sign接口,在signECDSA(...)中,使用媛媛的私匙、申请摘要,完成了最终的签名。在此先预先延伸一下,《神雕侠侣》中,在绝情谷中黄蓉推测断肠草就是情花的解药,所给的理由是万物相生相克,毒物百步之内必有解毒之物。放到代码里面,可以说,加密签名的函数在ecdsa.go中,将来验证这个签名的函数也必在“百步之内”——其实就是同文件里的verifyECDSA函数。
  8. 重回第6步,获取到最终的签过名的申请signedProp后,cf.EndorserClient.ProcessProposal(...)将签名申请发送给了背书结点。
  9. 背书结点收到媛媛的签名申请,(由于是peer channel join,因此实际上是媛媛给自己当背书节点,自己给自己发,为做区分,可以称为发送者媛媛、接收者媛媛),定位到core/endorser/endorser.go的Endorser的ProcessProposal接口中,在开始处即validation.ValidateProposalMessage(signedProp)对签名申请进行了验证 -> core/common/validation/msgvalidation.go的ValidateProposalMessage中 -> 先从签名申请中抽取一些字段,顺便验证数据结构,然后checkSignatureFromCreator(shdr.Creator, e.Signature, ...)对签名申请的发送者媛媛的身份、签名进行了验证。这里传入的比较重要的参数是shdr.Creatore.Signature(1) shdr.Creator最初由第1步在executeJoin中的creator, err := cf.Signer.Serialize()生成,而cf.Signer即为第5步中生成的签名身份,而cf.Signer.Serialize()所做的(在msp/identities.go中)是将媛媛签名身份中的本地MSP的mspid,二进制的证书装进一个SerializedIdentity对象后再整体将SerializedIdentity转换为[]byte数据,这就是shdr.Creator的内容。 (2) e.Signature是对申请消息的签名。
  10. checkSignatureFromCreator 中:(1) mspObj := mspmgmt.GetIdentityDeserializer(ChainID)根据链ID(亦是Channel ID)获取了一个本地的MSP管理者,这个管理者有两种类型:MSPConfigHandlermspManagerImpl。此时(如果按照上面的交易peer channel join …),因为peer本地的链的一系列资源仍未建立,因此msp/mgmt/mgmt.go中的mspMap中此时并没有ChainID的MSPManager对象(mspMap由该文件中的XXXSetMSPManager赋值,而该函数只有在peer channel join执行至最后在core/peer/peer.go中的CreateChainFromBlock -> createChain中被调用),因此在mspmgmt.GetIdentityDeserializer(ChainID) -> GetManagerForChain中会进入if !ok分支而创建一个新的mspManagerImpl
    。(同时也说明mspMap中的mspManagerImpl类型的MSPManager在peer channel完成后被MSPConfigHandler类型的MSPManager覆盖)。(2) creator, err := mspObj.DeserializeIdentity(creatorBytes),根据creatorBytes中的mspid,获取(接收者媛媛的)对应MSP对象,然后根据(发送者媛媛的)creatorBytes中的证书,再生成一个发送者媛媛的identity对象,(但此时发送者媛媛的identity中的msp对象是接收者媛媛的本地msp对象)。(3) creator.Validate()(msp/mspimpl.go),使用接收者媛媛的msp对象,对发送者媛媛的身份(identity对象)进行验证,主要是对identity对象中所持有的证书的有效性进行验证。
  11. 在第10步(3)的验证过程,creator.Validate()(msp/identities.go) -> id.msp.Validate(id),在msp/mspimpl.go中,均由bccspmsp的接口实现:(1)msp.validateIdentity(id) -> msp.getCertificationChainForBCCSPIdentity(id) -> msp.getValidationChain(id.cert, false) -> msp.getUniqueValidationChain(cert, msp.getValidityOptsForCert(cert)) -> validationChains, err := cert.Verify(opts),获取一个有效的证书链validationChains(最终是使用发送者媛媛identity对象中包含的证书(转为x509.Certificate)的Verify(opts)接口实现的,请参看x509库对应的例子)。这里一个证书只允许有一个证书链,也即一个证书不允许由多个ca签发。此外,validationChains这个证书数组,应该是下标0处是发送者媛媛证书自身,1处是证书的父级证书,依次后推,又因为现实情况是,发送者媛媛的证书(在msp/signcerts中)直接是由CA证书(在msp/ca中)签发的,因此1处即CA证书。(2) 继续,仍在msp.validateIdentity(id)中,msp.validateIdentityAgainstChain(id, validationChain) -> msp.validateCertAgainstChain(id.cert, validationChain),存在一个证书链,已经说明发送者媛媛的证书是有效的,不过这里需要进一步的查看签发给发送者媛媛证书的CA证书是否有效,即不在CRL列表中。若此CA证书不在CRL列表中,则正式宣布发送者媛媛的身份(证书)有效。
  12. 重回第10步的checkSignatureFromCreator 中,在验证完毕发送者媛媛的证书之后,最后一步,err = creator.Verify(msg, sig),对发送者媛媛发送数据的签名进行验证,在msp/identities.go中:(1)hashOpt, err := id.getHashOpt(...) -> digest, err := id.msp.bccsp.Hash(msg, hashOpt),根据哈希选项,计算发送者媛媛发送的数据的指纹。(2)valid, err := id.msp.bccsp.Verify(id.pk, sig, digest, nil),根据发送者媛媛证书公匙id.pk、签名sig、数据指纹,使用接收者媛媛的msp对象中的bccsp,对签名进行验证。
  13. 在第12步(2)的验证过程中,设定使用的是sw方式的bccsp,则在bccsp/sw/impl.go的Verify(...)实现中:(1)verifier, found := csp.verifiers[reflect.TypeOf(k)],根据公匙类型获取相应的验证器verifier,在这里获取的是ecdsa类型的验证器对象ecdsaPublicKeyKeyVerifier,在bccsp/sw/ecdsa.go中实现。(2)valid, err = verifier.Verify(k, signature, digest, opts) -> verifyECDSA(...),使用ecdsa验证器验证签名。一切如第7步预言的那样,最后执行验证的verifyECDSA(...)函数确实同执行签名的函数signECDSA(...)同在bccsp/sw/ecdsa.go中。至此,接收者媛媛对发送者媛媛的身份(证书)签名的验证结束。
  14. 重回第9步,接收者媛媛验证发送者媛媛的交易申请的函数ValidateProposalMessage(...)中。在进行完对发送者媛媛的身份(证书)签名的验证后,还会根据交易申请的类型,做进一步的简单验证,这里默认只有背书交易HeaderType_ENDORSER_TRANSACTION一种类型,进行validateChaincodeProposalMessage(prop, hdr)验证。这里仅作最简单的检查,检查交易申请的SignedProposal.Proposal.Header.ChannelHeader.ChaincodeHeaderExtension.PayloadVisibility字段,该字段在交易申请的类型是HeaderType_CHAINCODE_PACKAGE(即链码包)时才有用,此时发送者媛媛的交易申请是HeaderType_CONFIG类型。至此,接收者媛媛对发送者媛媛发来的交易申请验证完毕
  15. 在此总结一下:以上的验证过程虽说是媛媛自己验证自己,但沿用的依然是CA验证其所签名的证书的方法,即msp中,用ca目录下的CA证书去验证signcerts下的证书。拓展到同一组织的不同节点,比如A和B,由于A和B各自的msp目录下的signcerts证书都是由同一个CA证书签发的,该CA证书也都存在于节点各自的msp/ca目录下,因此对彼此的验证也不会存在问题。按照开篇所述,即:B节点在收到A发送过来的交易申请时,使用”查看A发送的交易申请的数据头部中所携带的身份证书在B的MSP对象中是否存在有效证书链(即A的证书是否是B所持有的ca证书所签发)的方法“去验证A的身份、交易数据是否有效。同时,也说明,若是跨组织的交易传送,则不会使用CA进行验证(验证也不会通过),而是靠TLS连接(即grpc的安全连接)进行身份的确定,如peer节点向orderer节点。

网络验证

fabric的网络验证基于gprc的TLS连接,而cryptogen工具默认生成的用于节点作为服务端时使用的服务端证书均不包含在MSP对象中(与msp目录同级别的tls目录),节点MSP对象中只包含用于签发服务端证书的TLS CA证书(msp/tlscacerts目录)。客户端与服务端建立TLS连接的握手过程可以有10步有余。具体如下图:

tls_shake.png

该图来自此篇博客:https://segmentfault.com/a/1190000009002353 。对tls握手过程的详解可参看此文。这里只说在与fabric相关的比较重要的环节:

  1. 第3步Certificate,即客户端和服务器端相互hello之后,服务器端将自身持有的服务器证书发送给客户端,供客户端验证,让客户端知道自己连的服务端没问题。因此,客户端需要持有签发服务器端证书的ca证书。在fabric中,如节点peerA连接ordererB,执行peer channel create命令创建一个新channel时,客户端是peerA,服务器端是ordererB。ordererB节点在自己的配置文件orderer.yaml(或对应的环境变量)中,Orderer.General.TLS.Certificate指定了服务端证书(tls/server.crt),Orderer.General.TLS.PrivateKey指定了服务端证书的私匙(tls/server.key),Orderer.General.TLS.RootCAs指定了签署服务端证书的TLS CA根证书(tls/ca.crt)。在执行peer channel create时,ordererB会将Orderer.General.TLS.Certificate指定的证书发给peerA,而peerA虽然本身并不持有ordererB的TLS CA证书(其msp/tlsca中的TLS CA证书是自己组织中的TLS CA证书而非orderer组织的TLS CA证书),但是在执行peer channel create会指定一个--cafile参数去指定所连接的ordererB的TLS CA证书(与Orderer.General.TLS.RootCAs指定的是同一个证书,只是文件名可能不同)。
  2. 第7步Certificate (optional)。这一步是可选的,服务器端若执行了第5步CertificateRequest (optional),即服务端也向客户端索要客户端证书,则客户端在第7步会向服务端传送自己的证书。服务端要想验证客户端证书,就必须持有签发客户端证书的TLS CA证书。还是上述peerA和ordererB的例子,ordererB在orderer.yaml中,Orderer.General.TLS.ClientAuthEnabled指定了是否需要验证客户端证书(即对应CertificateRequest),Orderer.General.TLS.ClientRootCAs指定了签发了客户端证书的TLS CA证书。peerA节点的配置文件core.yaml(或对应的环境变量)中,core.peer.tls.cert.file指定了自己的tls证书(tls/server.crt),core.peer.tls.key.file指定了tls证书对应的私匙(tls/server.key),core.peer.tls.rootcert.file指定了签发tls证书的TLS CA证书(tls/ca.crt)。若Orderer.General.TLS.ClientAuthEnabled为ture(默认为false,即服务端不验证客户端证书),则peerA需将自己core.peer.tls.cert.file指定的证书发给ordererB,而ordererB的Orderer.General.TLS.ClientRootCAs必须指向core.peer.tls.rootcert.file的值。
  3. 上述peerA与ordererB的TLS连接验证,属于跨组织节点之间的验证。若同一组织中,如org1中的peer0和peer1,由于其所持有的tls证书均有同一个TLS CA证书签发,且该TLS CA证书会存在与org1组织的MSP对象(也就是peer0和peer1所持有的MSP对象)中,因此验证不会存在问题。其他交易,如peer chaincode xxx,只要开启tls连接且涉及到了不同节点间的grpc连接,均会发生上述证书的交换验证。
  4. 依旧以peer作为客户端连接orderer服务端为例,具体到客户端的源码:peer/common/ordererclient.go的GetBroadcastClient(orderingEndpoint string, tlsEnabled bool, caFile string)(此处为典型,而不是唯一一处客户端源码),用于创建一个连接orderer的客户端对象:tlsEnabled取值于peer节点的core.peer.tls.enable配置项,若tlsEnabled为true,即开启TLS连接,则进入该if tlsEnabled中 -> caFile值来自于peer命令的--cafile参数,指定签发服务端orderer证书的TLS CA。若指定caFile,则进入if caFile != ""中 -> creds, err := credentials.NewClientTLSFromFile(caFile, ""),根据caFile给客户端创建一个TLS凭证(即验证的依据) -> opts = append(opts, grpc.WithTransportCredentials(creds)),将TLS凭证作为一个客户端拨号的选项,放入opts -> conn, err := grpc.Dial(orderingEndpoint, opts...),客户端拨号与服务器端建立连接,在内部使用TLS凭证选项。
  5. 具体到服务器的源码(依然是典型而非唯一):orderer/main.go的main中,grpcServer := initializeGrpcServer(conf) -> secureConfig := initializeSecureServerConfig(conf)从orderer.yaml的配置conf中抽取出tls相关的配置值,若未配置某项,则使用默认值替代。对于orderer.yaml中tls部分的配置释义,上文已有详述。 -> grpcServer, err := comm.NewGRPCServerFromListener(lis, secureConfig)根据tls配置项和服务端监听地址,创建grpc的服务端连接对象。在这个函数中(core/comm/server.go) -> 与创建客户端类似,若Orderer.General.TLS.Enable为true,则进入if secureConfig.UseTLS -> 若指定Orderer.General.TLS.Certificate和Orderer.General.TLS.PrivateKey,则进入if secureConfig.ServerKey != nil && secureConfig.ServerCertificate != nil -> grpcServer.tlsEnabled = true,将服务端连接tls开启,cert, err := tls.X509KeyPair(secureConfig.ServerCertificate, secureConfig.ServerKey)grpcServer.serverCertificate = cert,将服务端的证书和私匙转为X509格式的证书对象,赋值给服务端连接 -> grpcServer.tlsConfig.ClientAuth = tls.RequestClientCert,根据Orderer.General.TLS.ClientAuthEnabled值,决定服务端连接是否向客户端索要证书,若为true,则进入if secureConfig.RequireClientCert,并在if len(secureConfig.ClientRootCAs) > 0中,读取Orderer.General.TLS.ClientRootCAs指定的客户端证书的TLS CA证书,存储在服务端连接的grpcServer.clientRootCAs中 -> creds := NewServerTransportCredentials(grpcServer.tlsConfig),读取了各种关于服务端tls的配置和证书后,形成tlsConfig配置项,并据此形成服务端凭证creds,作为创建服务端的选项serverOpts,最终用于创建服务端对象,如此,服务端在与客户端建立tls连接时,可顺利进行相互验证。
  6. 总结一下:网络验证体系中,当客户端(如peer)与服务端(如orderer)为了顺利建立安全连接时,使用标准的TLS握手流程,各自提供所持有的tls凭证,进行身份的验证

policy验证:

fabric的策略类型有两种:SignaturePolicy和ImplicitMetaPolicy。前者用于交易策略,如背书策略,链码实例化策略等。后者用于Channel的管理,控制着Channel的权限控制(主要是Channel的配置的修改权限的控制)。

SignaturePolicy

从名字上可知,这种策略是基于节点证书签名的策略,即检查一个交易所持有的签名是否满足某种角色(member或admin)或某个数量或具体某个人或某个部门的要求。具体使用何种类型的标准判断,则由SignaturePolicy(唯一的)成员Type决定——Type有两个类型实现:

//1.SignedBy类型。即指定由某个节点身份进行签名
//SignedBy为数组下标,意思是必须由某个身份集合数组中的第SignedBy个身份签名,交易才算有效。
type SignaturePolicy_SignedBy struct {
    SignedBy int32
}

//2.N out of类型。
//是一个递归类型,嵌套了一个SignaturePolicy数组集合(子策略),即必须满足该集合中所存在的任意N个子策略有效,该策略才算有效。(而子策略因为是SignaturePolicy,所以子策略依然可能是SignedBy类型,也可能是N out of类型,若是N out of类型,则子策略拥有子子策略。比如策略A的子策略之一Ab的类型是N out of类型,则需要Ab的子子策略集合中任一N个是有效的,Ab才算有效。如此递归,依次类推,但可以想象,递归到最底层,所有的叶节点策略一定是SignedBy类型)。
type SignaturePolicy_NOutOf_ struct {
    NOutOf *SignaturePolicy_NOutOf
}
type SignaturePolicy_NOutOf struct {
    N     int32
    Rules []*SignaturePolicy  //子策略
}

这里以背书策略为例,进行详述。当管理员用户执行peer chaincode instantiate ....部署链码时,可以指定-P参数指定了对运行该链码所产生的交易结果集进行怎样的签名,交易结果才算有效,如-P "OR ('Org1MSP.member','Org2MSP.member')",规定着交易结果集必须由Org1或Org2中的一个member签名,才算有效。具体的,交易背书策略所形成的对象如下:

type SignaturePolicyEnvelope struct {
    Version    int32
    Rule       *SignaturePolicy
    Identities []*common1.MSPPrincipal
}

该结构体中,Version是策略的版本号,一般随链码的版本号。Rule是签名策略:(1)若Rule的类型是SignedBy类型,则签名必须由Identities[Rule.SignedBy]处的身份的签名,交易才算有效。(2)若Rule是N out of类型,则签名Identities中所持有的身份是否有至少有N个身份(身份的角色/身份/所属部门)满足数据中所对应的签名。

背书策略比较难理解,一是递归的存在,二是类型较多,三是涉及到一部分DSL的知识(笔者也只是了解),因此直接分析较困难,比较好的做法是参看对应的test文件。定位到common/cauthdsl/policyparser_test.go的TestAndTestOr中,以TestAnd为例:最终对比的是assert.True(t, reflect.DeepEqual(p1, p2))且要求p1和p2完全相等,因此p1, err := FromString("AND('A.member', 'B.member')")与p2是一致的,也因此通过FromString("AND('A.member', 'B.member')")所生成的SignaturePolicyEnvelope就是p2组装的结构,而p2的结构在函数中就很明了:p2.Identities组合了A和B的角色身份,p2.Rule联合了两个子策略SignedBy(0)SignedBy(1),表明交易若要满足此策略,交易所含有的签名身份必须满足p2.Identities[0]p2.Identities[1]两处的身份。

通过common/cauthdsl/cauthdsl.go的complie(...)可根据SignaturePolicyEnvelope中的规则Rule,参考身份集Identities编译成一个函数(可称此函数为策略计算器),该函数输入一个交易的签名数据集[]SignedData,经计算(这个过程笔者看的似懂非懂,就不予详述了)直接将是否满足策略的最终结果(true或false)返回。在common/cauthdsl/policy.go中,将策略计算器当作一个工具封装在policy中,就可以表示一个策略对象,供在具体的交易过程中使用,具体的,就是使用评估函数Evaluate(里面也是主要使用了策略计算器这个工具)评估一个签名数据集[]SignedData

背书策略的验证过程:

  1. 管理员用户执行peer chaincode instantiate ... -P "OR('Org1MSP.member','Org2MSP.member')",这里要说明一下,命令行的话只支持AND/OR和MSPID.ROLE格式的组合。执行后,开始运行部署chaincode的源码。
  2. 在peer/chaincode/instantiate.go中,instantiate -> spec, err := getChaincodeSpec(cmd)(同目录common.go中) -> checkChaincodeCmdParams(cmd) -> 因为指定了-P,因此进入if policy != common.UndefinedParamValue {分支,将"OR('Org1MSP.member','Org2MSP.member')"通过FromString(policy)编码成SignaturePolicyEnvelope后转为[]byte格式后赋值给全局变量policyMarhsalled -> 重回instantiate.go的instantiate中,policyMarhsalled通过utils.CreateDeployProposalFromCDS(...)被打包进ChaincodeSpec,存储在ChaincodeSpec.Input的3的位置(参看protos/utils/proputils.go中的createProposalFromCDS)。这就是背书策略的原始数据来源。
  3. 部署链码的交易将包含了背书策略的部署数据包,最终交到了core/scc/lscc.go中的Invoke(stub)函数中并进入case DEPLOY:分支,将第2步放入ChaincodeSpec.Input中的参数一一分解出,由于arg[3]的地方就是第2步所放进去的背书策略SignaturePolicyEnvelope,因此进入if len(args) > 3 && len(args[3]) > 0中取出背书策略,同时也可以这里的else中看到,如果未指定链码的背书策略,则会使用cauthdsl.SignedByAnyMember(peer.GetMSPIDs(chainname))获取一个默认背书策略,该策略是通道中所有组织中的任一member成员签名即为有效。 -> 背书策略同其他数据一起进入lscc.executeDeploy(...policy...),在此函数中,背书策略数据被赋值给ChaincodeData.Policy成员 -> lscc.getInstantiationPolicy(chainname, ccpack)lscc.checkInstantiationPolicy(...)分别获取和进行评估了链码的实例化策略,这里因为在讲背书策略,因此不展开讲,只提一下。 -> 包含链码背书策略的ChaincodeData传入lscc.createChaincode(stub, cd),一路追随,最终在putChaincodeData()中的stub.PutState(cd.Name, cdbytes),将链码的名字作为key,链码的ChaincodeData作为value,组成一个键值对被放入交易模拟器的写集中(关于如何形成写集比较复杂,这里不详述,可参看《fabric源码解析20》),形成一个Envelope返回第1步执行命令的管理员节点,即回到peer/chaincode/instantiate.go的chaincodeDeploy中,env, err := instantiate(cmd, cf),env就是返回的写集中带有部署链码键值对(name - ChaincodeData)的Envelope,自然也在ChaincodeData中包含了链码的背书策略。 -> cf.BroadcastClient.Send(env),将env发送给orderer,然后就是数据上链的过程,然后再是同过gossip将env散播给通道内的其他节点。至此,链码的背书策略作为链码数据的一部分得以上链保存。
  4. 当用户发起一笔调用已部署的链码的交易时,执行peer chaincode invoke ...(此处一般是SDK客户端发起的交易,客户端决定发给哪个peer节点,只要条件符合——客户端与peer节点建立的有tls连接——则该peer节点就是背书节点),peer节点模拟交易产生交易结果集并对交易结果集进行签名。这一过程从peer/chaincode/invoke.go的chaincodeInvoke -> peer/chaincode/common.go的chaincodeInvokeOrQuery -> ChaincodeInvokeOrQuery中,proposalResp, err = endorserClient.ProcessProposal(context.Background(), signedProp)将交易提交给背书节点进行模拟交易,获取交易结果proposalResp -> env, err := putils.CreateSignedTx(prop, signer, proposalResp)对交易结果进行签名背书 -> err = bc.Send(env)将已背书的结果发给orderer。这里需要说明的是,这里将一个背书节点的背书结果直接发送给了orderer,如果不符合背书策略,则会当作无效交易。但是SDK客户端(一般就是开发者以此开发的实际应用)有责任收集背书节点的背书交易并对收集结果是否满足背书策略进行判断。如果未进行判断,交易直接提交给orderer节点,经orderer排序记入orderer账本后,通过deliver服务传播到主记账节点,主记账节点再通过gossip服务散播给与之连接的普通记账节点。在记账节点记入之前,会进行背书策略的检查。
  5. 在创建peer本地链对象时(core/peer/peer.go的createChain()),c := committer.NewLedgerCommitterReactive(...)创建了一个committer对象(该对象包含peer本地账本对象PeerLedger、验证对象Validator),并通过参数由service.GetGossipService().InitializeChannel(...)传入了gossip模块。在主记账节点(peer节点)的gossip模块接到orderer端的block时,会在gossip/state/state.go的commitBlock(block)中使用committer.Commit(block)将block对象提交到committer所持有的账本对象PeerLedger中,也就是主记账节点的本地账本,即core/committer/committer_impl.go的Commit(block)所实现的。在Commit(block)中,committer又会使用所持有的验证对象Validator通过lc.validator.Validate(block)对block中的每笔交易进行验证,其中就包含了背书策略的验证。若通过了,才会执行lc.ledger.Commit(block)将block最终写入账本。
  6. Validator对象验证block中交易的过程发生在core/committer/txvalidator/validator.go的Validate(block) -> v.vscc.VSCCValidateTx(payload, d, env),当交易是HeaderType_ENDORSER_TRANSACTION类型时,Validator会使用持有的vscc验证器进行交易的背书检查。 -> 当读写集中的名字空间(其实就是链码ID,读写集每一个键值对都有对应的名字,按照链码ID分,即每个链码的读写集是分开的)是应用链码时,会进入if !v.sccprovider.IsSysCC(ccID)分支。 -> 在for _, ns := range wrNamespace循环中,按照Channel ID、链码ID逐一执行txcc, vscc, policy, err := v.GetInfoForValidate(chdr.TxId, chdr.ChannelId, ns),继而执行cd, err := v.getCDataForCC(ccID),从而根据链码ID获取第3步部署链码时上链的包含背书策略的ChaincodeData(通过qe.GetState("lscc", ccid)获取,链码部署后,链码的ChaincodeData数据是lscc名字空间下,这个很自然,因为是lscc对链码进行的部署),进而从中获取链码版本-txcc,验证链码ID-vscc,链码背书策略-policy。 -> if ns == ccID && txcc.ChaincodeVersion != ccVer,检查当前chaincode版本是否变化,即交易在最终上链之前chaincode又被重新部署,原版本所产生的交易(自然而然)在此被认为无效。 -> v.VSCCValidateTxForCC(envBytes,...,policy)该函数真正对交易的背书是否符合背书策略进行了检查。
  7. VSCCValidateTxForCC(envBytes,...,policy)中,(1) args := [][]byte{[]byte(""), envBytes, policy},组装调用系统链码vscc的3个参数([0]为空,[1]为交易本身,[2]为背书策略SignaturePolicyEnvelope)。(2) res, _, err := v.ccprovider.ExecuteChaincode(ctxt, cccid, args),根据验证链码cccid(即包含vscc的信息)和参数args,调用vscc进行验证。这里直接定位至core/scc/vscc/validator_onevalidsignature.go的Invoke(...)函数。
  8. Invoke(...)函数中,policy, _, err := pProvider.NewPolicy(args[2])将参数2(就是链码背书策略SignaturePolicyEnvelope)计算转化为一个可以使用的策略对象policy。 -> 在for _, act := range tx.Actions循环中,signatureSet, err := vscc.deduplicateIdentity(cap)抽取每一个交易的签名数据集,然后err = policy.Evaluate(signatureSet)使用策略对象policy的评估函数评估当笔交易的签名数据集以此完成最终的策略验证。若通过,交易数据会继续提交,否则对交易的验证旅程就此返回,交易被标记为无效交易。

ImplicitMetaPolicy

ImplicitMetaPolicy可以称为隐含策略,之所以称为隐含策略,用于定义通道的策略。通道的配置策略是递归结构,类似于一个文件目录,根目录就是通道,而每个配置组(ConfigGroup)都类似一个文件夹,每一级“目录”都有自己的策略(分为读、写、管理共3种策略),即要在某一层级读取数据、写入数据、更改策略,需要分别满足路径上每一层级的读、写、管理策略。一般来说通道的策略是默认的,所拥有的配置组也是默认的,均定义在common/policies/policy.go开始处的const常量,具体如下:

  • Channel - 根策略。
  • Channel/Application - 通道应用策略,控制是否有权限在通道上创建应用通道、部署组织、链码,读取数据等。如下文的A,B,C处。
  • Channel/Orderer - 通道的Orderer端的权限控制,控制是否有权限在Orderer端读写数据。
  • Channel//OrgID - 表示Application或Orderer,通道上具体组织的控制策略。对应com
//1.配置组数据格式如下。...为省略值,特别标注A-D处为通道策略相关,供下文叙述使用。
//3.A-C处ConfigPolicy.Policy为SignaturePolicy或ImplicitMetaPolicy。
ChannelGroup{
  Version:...
  Values:...
  Groups{
    "Application":ApplicationGroup{
      Version:...
      Values:...
      Groups{
        "Org1":ApplicationOrgGroup{...}
        "Org2":ApplicationOrgGroup{...}
        ...
      }
      Policies{
        "/Channel/Application/Reader":ConfigPolicy{} ------ A,读策略
        "/Channel/Application/Writer":ConfigPolicy{} ------ B,写策略
        "/Channel/Application/Admin":ConfigPolicy{}  ------ C,管理策略
      }
      ModePolicy:"Admin"                             ------ D,修改策略
    }
    "Orderer":OrdererGroup{...}
  }
  Policies{
    "/Channel/Reader":ConfigPolicy{}
    "/Channel/Writer":ConfigPolicy{}
    "/Channel/Admin":ConfigPolicy{}
  }
  ModePolicy:"Admin"
}

印象中官方文档大概有这么一句话,就是默认的这组策略工作的就很好,因此在通道策略方面,保持默认的即可。这些通道的策略会根据配置文件configtx.yaml,使用configtxgen工具作为配置数据将上述ChannelGroup结构存储在生成的genesis.block中,生成过程对应common/configtx/tool/main.go的doOutputBlock中的doOutputBlock -> provisional.go的New(config)中(相当复杂,不详述)。在创建应用通道(Application Channel)时,也会继承这些策略,在core/peer/peer.go的CreateChainFromBlock -> createChain(cid, l, cb) -> configtxManager, err := configtx.NewManagerImpl(envelopeConfig...),从genesis.block中抽取配置信息,并为每一层级配备管理对象,创建配置管理对象configtxManager -> ac, ok := configtxInitializer.ApplicationConfig()并将ac(即为创建的应用通道的配置信息,其中自然也包含上述A-C三处策略数据)赋值给应用通道的ChainSupport中的Application成员。这些是下文描述验证通道策略过程的基础。

  1. 当发起一笔交易给背书节点时,对应Channel上的策略检查器会对交易所携带的身份信息进行检查,检查其是否符合通道的策略。定位到core/endorser/endorser.go的ProcessProposal中,若调用的是应用链码,则在该函数中就会进行e.checkACL(chdr.ChannelId, policies.ChannelApplicationWriters, signedProp)进行通道权限的检查,policies.ChannelApplicationWriters表明对应检查的策略是Channel/Application/Writes权限,相当于上文中的B。
  2. checkACL中,e.policyChecker.CheckPolicy(...)使用了策略检查器进行检查,策略检查器在peer node start创建背书对象Endorser时创建的,原型是core/policy/policy.go中的policyChecker,包含了策略管理获取器channelPolicyManagerGetter。channelPolicyManagerGetter在/core/peer/peer.go中定义,可以通过GetPolicyManager()获取(从上文应用通道的ChainSupport中的Application成员中获取)指定路径的策略管理者。
  3. 在策略检查器所执行的CheckPolicy()中 -> p.CheckPolicyBySignedData(channelID, policyName, sd)policyManager, _ := p.channelPolicyManagerGetter.Manager(channelID)通过策略管理获取器获取了Channel/Application/层级的策略管理者policyManager -> policy, _ := policyManager.GetPolicy(policyName),策略管理者根据策略名policyName获取指定的策略policy -> err := policy.Evaluate(sd)最终验证交易的签名数据sd(依然是SignedData格式)是否符合策略。
  4. 这些通道策略的检查会在很多地方发生,如调用了系统链码,也会进行检查,如调用qscc进行查询时,定位到/core/scc/qscc/query.go的Invoke中,e.policyChecker.CheckPolicy(cid, policies.ChannelApplicationReaders, sp)即检查了查询交易中所携带的身份信息是否拥有policies.ChannelApplicationReaders,即上文A处的权限。即拥有策略检查器policyChecker的地方,一般都伴随着使用该工具进行策略检查。

同样的,因为策略检查器牵扯的结构对象比较复杂,上述过程也是很粗线条的描述,因此参看一下test文件。在core/policy/policy_test.go的TestPolicyChecker中:(1) A、B、C相当于三个我们创建的应用通道的ChannelID,[]byte(Alice)、[]byte(Bob)相当于一个身份对象(即本文交易验证章节第9步中所提到的媛媛的签名身份signingidentity进行Serialize()操作所得到的二进制SerializedIdentity),[]byte(msg1)、[]byte(msg2)、[]byte(msg3)相当于对交易的签名。(2) sProp, _ := utils.MockSignedEndorserProposalOrPanic(...)就相当于创建了一个通道A由Alice签名的交易sProp,然后使用类似pc.CheckPolicy("A", "readers", sProp)的函数去验证sProp是否符合某通道的某策略。

这一部分讲的很不透彻,原因在于笔者也是压根没透彻理解。这部分牵扯到配置的策略的代码真是耐着性子也看不下去。

总结

此处的总结不是本文的总结,而是对整个源码解析系列的总结。该系列至此结束。该系列写作的过程真正印证了自己长久萦绕在心里的淡淡想法:想要获取,可能你需要先给予一些东西。你的进步,或者被认可或知道,在没有一些先前条件的情况下,是你需要去创造出来一些东西。这里,创造的就是一系列文章。将来,依照此经验,去创造一些更多的东西,创造。

源码解析从来就是一个吃力耗时且不讨好的工作,毕竟你去面试,谁也不可能照着源码去问你问题。而且fabric源码变化如此快,分析的底本是1.0的,现在已经出到1.2了,又加入了很多新的东西,自己都没精力去跟踪学习。而且很自然的,你分析的1.0,最新的已经1.2了,看客自然就不想看你1.0的源码解析了,过时感很容易就来了。就比如你若不是为了单纯学习C的编码技能,你不会去看linux2.4的内核源码解析,因为现在的内核版本都至少是4了。

UC体:震惊!现代心理学发展百年,不及此人半小时的研究!

彩蛋收尾。

心理学是从哲学体系中分化而来。古希腊罗马时期,就有柏拉图、亚里士多德等著名的哲学家作为心理学的先知贡献了一些力量。虽然之后,西方心理学经历了长期的教皇统治时期,但从14世纪到16世纪的文艺复兴,又使科学和人文主义得以兴起,又出现了笛卡尔、洛克等心理学思想奠基人,自此各个流派心理学在西方开始渐渐发展,直到十九世纪末,冯特于德国莱比锡大学创立首间心理学实验室,及后首位美国心理学家威廉·詹姆士编写美国首本心理学教科书《心理学原理》,标志着现代心理学作为一门科学,正式确立。二十世纪至今,心理学飞速发展,众多各个流派仍一边争论不休,仍然没有统一的理论。

中国的心理学发展史,由于长期封建王朝的存在,一直缺乏生长土壤。直到鸦片战争以后,西方心理学传入中国,而真正在国内作为一门科学进行研究要到辛亥革命时期。建国以来,国内心理学研究虽然有一定发展,但仍然无法满足日新月异的社会变化。

人的心理如此复杂,人是如此复杂,以至于对人的研究始终是一道跨越时间和人类层面的难题。然而,震惊的是,中国的心理学研究可能要就此赶超西方了!!!面对几千年来研究人和人的心理的心理学的发展的缓慢,面对国内落后的心理学研究与现实社会的矛盾的日益突出,国内A IN的人事工作者,实际上已经在不为人知且不求闻达的情况下,对人的心理和人的自身的研究达到了登峰造极的地步。在短短的半小时的交流中,其就能清清楚楚的看清一个人的心理,并无比透彻的定性一个人。如此神乎其神的理论深度和实践能力,足以超越荣格,阿德勒。以此推断,A IN的该人事工作者,肯定受邀参加了今年8月2日在德国法兰克福举办的IAAP/IAJS Conference,在会上发表了重要演讲也未可知。然而如此种种神迹皆不见于报,如此低调,亦不得不让人深感敬服。

猜你喜欢

转载自blog.csdn.net/idsuf698987/article/details/81677133
今日推荐