成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

剝開(kāi)比原看代碼04:如何連上一個(gè)比原節(jié)點(diǎn)

BigNerdCoding / 2039人閱讀

摘要:總的來(lái)說(shuō),在比原中有一個(gè)類(lèi),它用于集中處理節(jié)點(diǎn)與外界交互的邏輯,而它的創(chuàng)建和啟動(dòng),又都是在中進(jìn)行的。我考慮的是這樣一種情況,比如某用戶(hù)在筆記本上運(yùn)行比原節(jié)點(diǎn),然后在公開(kāi)場(chǎng)合上網(wǎng),使用了黑客提供的。

作者:freewind

比原項(xiàng)目倉(cāng)庫(kù):

Github地址:https://github.com/Bytom/bytom

Gitee地址:https://gitee.com/BytomBlockc...

在上一篇我們已經(jīng)知道了比原是如何監(jiān)聽(tīng)節(jié)點(diǎn)的p2p端口,本篇就要繼續(xù)在上篇中提到的問(wèn)題:我們?nèi)绾纬晒Φ倪B接上比原的節(jié)點(diǎn),并且通過(guò)身份驗(yàn)證,以便后續(xù)繼續(xù)交換數(shù)據(jù)?

在上一篇中,我們的比原節(jié)點(diǎn)是以solonet這個(gè)chain_id啟動(dòng)的,它監(jiān)聽(tīng)的是46658端口。我們可以使用telnet連上它:

$ telnet localhost 46658
Trying 127.0.0.1...
Connected to localhost.
Escape character is "^]".
??S??%?z???_?端??????U[e

可以看到,它發(fā)過(guò)來(lái)了一些亂碼。這些亂碼是什么意思?我們應(yīng)該怎么應(yīng)答它?這是本篇將要回答的問(wèn)題。

定位發(fā)送代碼

首先我們得定位到比原向剛連接上來(lái)的節(jié)點(diǎn)發(fā)送數(shù)據(jù)的地方。說(shuō)實(shí)話(huà),這里實(shí)在是太繞了,山路十八彎,每次我想找到這段代碼,都需要花好一陣功夫。所以下面這段流程,我覺(jué)得你以后可能經(jīng)常會(huì)過(guò)來(lái)看看。

總的來(lái)說(shuō),在比原中有一個(gè)Switch類(lèi),它用于集中處理節(jié)點(diǎn)與外界交互的邏輯,而它的創(chuàng)建和啟動(dòng),又都是在SyncManager中進(jìn)行的。另外,監(jiān)聽(tīng)p2p端口并拿到相應(yīng)的連接對(duì)象的操作,與跟連接的對(duì)象進(jìn)行數(shù)據(jù)交互的操作,又是分開(kāi)的,前者是在創(chuàng)建SyncManager的時(shí)候進(jìn)行的,后者是在SyncManager的啟動(dòng)(Start)方法里交由Switch進(jìn)行的。所以總體來(lái)說(shuō),這一塊邏輯有點(diǎn)復(fù)雜(亂),繞來(lái)繞去的。

這里不先評(píng)價(jià)代碼的好壞,我們還是先把比原的處理邏輯搞清楚吧。

下面還是從啟動(dòng)開(kāi)始,但是由于我們?cè)谇懊嬉呀?jīng)出現(xiàn)過(guò)多次,所以我會(huì)盡量把不需要的代碼省略掉,帶著大家快速到達(dá)目的地,然后再詳細(xì)分析。

首先是bytomd node的入口函數(shù):

cmd/bytomd/main.go#L54

func main() {
    cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))
    cmd.Execute()
}

轉(zhuǎn)交給處理參數(shù)node的函數(shù):

cmd/bytomd/commands/run_node.go#L41

func runNode(cmd *cobra.Command, args []string) error {
    // Create & start node
    n := node.NewNode(config)
    if _, err := n.Start(); err != nil {
    // ...
}

如前一篇所述,“監(jiān)聽(tīng)端口”的操作是在node.NewNode(config)中完成的,這次發(fā)送數(shù)據(jù)的任務(wù)是在n.Start()中進(jìn)行的。

但是我們還是需要看一下node.NewNode,因?yàn)樗镌趧?chuàng)建SyncManager對(duì)象的時(shí)候,生成了一個(gè)供當(dāng)前連接使用的私鑰,它會(huì)在后面用到,用于產(chǎn)生公鑰。

node/node.go#L59-L142

func NewNode(config *cfg.Config) *Node {
    // ...
    syncManager, _ := netsync.NewSyncManager(config, chain, txPool, newBlockCh)
    // ...
}

netsync/handle.go#L42-L82

func NewSyncManager(config *cfg.Config, chain *core.Chain, txPool *core.TxPool, newBlockCh chan *bc.Hash) (*SyncManager, error) {
    manager := &SyncManager{
        txPool:     txPool,
        chain:      chain,
        privKey:    crypto.GenPrivKeyEd25519(),
        // ...
}

就是這個(gè)privKey,它是通過(guò)ed25519生成的,后面會(huì)用到。這個(gè)私鑰僅在本次連接中使用,每個(gè)連接都會(huì)生成一個(gè)新的。

讓我們?cè)倩氐街骶€runNode,其中n.Start又將被轉(zhuǎn)交到NodeOnStart方法:

node/node.go#L169

func (n *Node) OnStart() error {
    // ...
    n.syncManager.Start()
    // ...
}

轉(zhuǎn)交到SyncManagerStart方法:

netsync/handle.go#L141

func (sm *SyncManager) Start() {
    go sm.netStart()
    // ...
}

然后在另一個(gè)例程(goroutine)中調(diào)用了netStart()方法:

netsync/handle.go#L121

func (sm *SyncManager) netStart() error {
    // Start the switch
    _, err := sm.sw.Start()
    // ...
}

在這里終于調(diào)用了SwitchStart方法(sm.sw中的sw就是一個(gè)Switch對(duì)象):

p2p/switch.go#L186

func (sw *Switch) OnStart() error {
    // ...
    // Start listeners
    for _, listener := range sw.listeners {
        go sw.listenerRoutine(listener)
    }
    // ...
}

這里的sw.listeners,就包含了監(jiān)聽(tīng)p2p端口的listener。然后調(diào)用listenerRoutine()方法,感覺(jué)快到了。

p2p/switch.go#L496

func (sw *Switch) listenerRoutine(l Listener) {
    // ...
    err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig)
    // ...    
}

在這里拿到了連接到p2p端口的連接對(duì)象inConn們,傳入一堆參數(shù),準(zhǔn)備大刑伺候:

p2p/switch.go#L643

func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error {
    // ...
    peer, err := newInboundPeerWithConfig(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config)
    // ...
}

把需要的參數(shù)細(xì)化出來(lái),再次傳入:

p2p/peer.go#L87

func newInboundPeerWithConfig(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {
    return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config)
}

再繼續(xù),馬上就到了。

p2p/peer.go#L91

func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {
    // ...
    // Encrypt connection
    if config.AuthEnc {
        // ...
        conn, err = MakeSecretConnection(conn, ourNodePrivKey)
        // ...
    }
    // ...
}

終于到了關(guān)鍵的函數(shù)MakeSecretConnection()了。由于config.AuthEnc的默認(rèn)值是true,所以如果沒(méi)有特別設(shè)置的話(huà),它就會(huì)進(jìn)入MakeSecretConnection,在這里完成身份驗(yàn)證等各種操作,它也是我們本篇講解的重點(diǎn)。

好,下面我們開(kāi)始。

詳解MakeSecretConnection

這個(gè)函數(shù)的邏輯看起來(lái)是相當(dāng)復(fù)雜的,引入了很多密鑰和各種加解密,還多次跟相應(yīng)的peer進(jìn)行數(shù)據(jù)發(fā)送和接收,如果不明白它為什么要這么做,是很難理解清楚的。好在一旦理解以后,明白了它的意圖,整個(gè)就簡(jiǎn)單了。

總的來(lái)說(shuō),比原的節(jié)點(diǎn)之間的數(shù)據(jù)交互,是需要很高的安全性的,尤其是數(shù)據(jù)不能明文傳送,否則一旦遇到了壞的“中間人”(可以理解為數(shù)據(jù)從一個(gè)節(jié)點(diǎn)到另一個(gè)節(jié)點(diǎn)中途需要經(jīng)過(guò)的各種網(wǎng)關(guān)、路由器、代理等等),數(shù)據(jù)就有可能被竊取甚至修改??紤]一下這個(gè)場(chǎng)景:用戶(hù)A想把100萬(wàn)個(gè)比原從自己的帳號(hào)轉(zhuǎn)到用戶(hù)B的帳戶(hù),結(jié)果信息被中間人修改,最后轉(zhuǎn)到了中間人指定的帳戶(hù)C,那么這損失就大了,甚至無(wú)法追回。(有同學(xué)問(wèn),“區(qū)塊鏈上的每個(gè)交易不是會(huì)有多個(gè)節(jié)點(diǎn)驗(yàn)證嗎?如果只有單一節(jié)點(diǎn)使壞,應(yīng)該不會(huì)生效吧”。我考慮的是這樣一種情況,比如某用戶(hù)在筆記本上運(yùn)行比原節(jié)點(diǎn),然后在公開(kāi)場(chǎng)合上網(wǎng),使用了黑客提供的wifi。那么該節(jié)點(diǎn)與其它結(jié)點(diǎn)的所有連接都可以被中間人攻擊,廣播出去的交易可以同時(shí)被修改,這樣其它節(jié)點(diǎn)拿到的都是修改后的交易。至于這種方法是否可以生效,還需要我讀完更多的代碼才能確定,這里暫時(shí)算是一個(gè)猜想吧,等我以后再來(lái)確認(rèn))

所以比原節(jié)點(diǎn)之間傳輸信息的時(shí)候是加密的,使用了某些非對(duì)稱(chēng)加密的方法。這些方法需要在最開(kāi)始的時(shí)候,節(jié)點(diǎn)雙方都把自己的公鑰轉(zhuǎn)給對(duì)方,之后再發(fā)信息時(shí)就可以使用對(duì)方的公鑰加密,再由對(duì)方使用私鑰解密。加密后的數(shù)據(jù),雖然還會(huì)經(jīng)過(guò)各種中間人的轉(zhuǎn)發(fā)才能到達(dá)對(duì)方,但是只要中間人沒(méi)有在最開(kāi)始拿到雙方的明文公鑰并替換成自己的假冒公鑰,它就沒(méi)有辦法知道真實(shí)的數(shù)據(jù)是什么,也就沒(méi)有辦法竊取或修改。

所以這個(gè)函數(shù)的最終目的,就是:把自己的公鑰安全的發(fā)送給對(duì)方,同時(shí)安全得拿到對(duì)方的公鑰。

如果僅僅是發(fā)送公鑰,那本質(zhì)上就是發(fā)送一些字節(jié)數(shù)據(jù)過(guò)去,應(yīng)該很簡(jiǎn)單。但是比原為了達(dá)到安全的目的,還進(jìn)行了如下的思考:

只發(fā)送公鑰還不夠,還需要先用我的私鑰把一段數(shù)據(jù)簽個(gè)名,一起發(fā)過(guò)去,讓對(duì)方驗(yàn)證一下,以保證我發(fā)過(guò)去的公鑰是正確的

明文發(fā)送公鑰不安全,所以得把它加密一下再發(fā)送

為了加密發(fā)送,我和對(duì)方都需要生成另一對(duì)一次性的公鑰和私鑰,專(zhuān)門(mén)用于這次加密,用完后就丟掉

為了讓我們雙方都能正確的加解密,所以需要找到一種方式,在兩邊生成同樣的用于簽名的數(shù)據(jù)(challenge)和加解密時(shí)需要的參數(shù)(sharedSecret, sendNonce/recvNonce

另外還有一些過(guò)度的考慮:

在發(fā)送加密數(shù)據(jù)的時(shí)候,擔(dān)心每次要發(fā)送的數(shù)據(jù)過(guò)多,影響性能,所以把數(shù)據(jù)分成多個(gè)塊發(fā)送

為了配合多次發(fā)送和接收,還需要考慮如何讓兩邊的sendNoncerecvNonce保持同步改變

在發(fā)送公鑰及簽名數(shù)據(jù)時(shí),把它們包裝成了一個(gè)對(duì)象,再進(jìn)行額外的序列化和反序列化操作

我之所以認(rèn)為這些是“過(guò)度”的考慮,是因?yàn)樵谶@個(gè)交互過(guò)程中,數(shù)據(jù)的長(zhǎng)度是固定的,并且很短(只有100多個(gè)字節(jié)),根本不需要考慮分塊。另外公鑰和簽名數(shù)據(jù)就是兩個(gè)簡(jiǎn)單的、長(zhǎng)度固定的字節(jié)數(shù)組,并且只在這里用一次,我覺(jué)得可以直接發(fā)送兩個(gè)數(shù)組即可,包裝成對(duì)象及序列化后,我們還需要考慮序列化之后的數(shù)組長(zhǎng)度是如何變化的。

在查閱了相關(guān)的代碼以后,我發(fā)現(xiàn)這一處邏輯只在這里使用了一次,沒(méi)有必要提前考慮到通用但更復(fù)雜的情況,提前編碼。畢竟那些情況有可能永遠(yuǎn)不會(huì)發(fā)生,而提前寫(xiě)好的代碼所增加的復(fù)雜度以及可能多出來(lái)的bug卻是永遠(yuǎn)存在了。

《敏捷軟件開(kāi)發(fā) 原則、模式和實(shí)踐》這本書(shū)告訴我們:不要預(yù)先設(shè)計(jì),盡量用簡(jiǎn)單的辦法實(shí)現(xiàn),等到變化真的到來(lái)了,再考慮如何重構(gòu)讓它適應(yīng)這種變化。

下面講解“MakeSecretConnection”,由于該方法有點(diǎn)長(zhǎng),所以會(huì)分成幾塊:

p2p/listener.go#L52

func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKeyEd25519) (*SecretConnection, error) {

    locPubKey := locPrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519)

首先注意的是參數(shù)locPrivKey,它就是在前面最開(kāi)始的時(shí)候,在SyncManager中生成的用于本次連接通信的私鑰。然后根據(jù)該私鑰,生成對(duì)應(yīng)的公鑰,對(duì)于同一個(gè)私鑰,生成的公鑰總是相同的。

這個(gè)私鑰的長(zhǎng)度是64字節(jié),公鑰是32字節(jié),可見(jiàn)兩者不是一樣長(zhǎng)的。公鑰短一些,更適合加密(速度快一點(diǎn))。

呆會(huì)兒在最后會(huì)使用該私鑰對(duì)一段數(shù)據(jù)進(jìn)行簽名,然后跟這個(gè)公鑰一起,經(jīng)過(guò)加密后發(fā)送給peer,讓他驗(yàn)證。成功之后,對(duì)方會(huì)一直持有這個(gè)公鑰,向我們發(fā)送數(shù)據(jù)前會(huì)用它對(duì)數(shù)據(jù)進(jìn)行加密。

接著,

    // Generate ephemeral keys for perfect forward secrecy.
    locEphPub, locEphPriv := genEphKeys()

這里生成了一對(duì)一次性的公私鑰,用于本次連接中對(duì)開(kāi)始那個(gè)公鑰(和簽名數(shù)據(jù))進(jìn)行加密。

待會(huì)兒會(huì)發(fā)把這里生成的locEphPub以明文的方式傳給對(duì)方(為什么是明文?因?yàn)楸仨毜糜幸淮蚊魑陌l(fā)送,不然對(duì)方一開(kāi)始就拿到加密的數(shù)據(jù)沒(méi)法解開(kāi)),它就我們?cè)诒疚拈_(kāi)始通過(guò)telnet localhost 46658時(shí)收到的那一堆亂碼。

genEphKeys(),對(duì)應(yīng)于:

p2p/secret_connection.go#L189

func genEphKeys() (ephPub, ephPriv *[32]byte) {
    var err error
    ephPub, ephPriv, err = box.GenerateKey(crand.Reader)
    if err != nil {
        cmn.PanicCrisis("Could not generate ephemeral keypairs")
    }
    return
}

它調(diào)用了golang.org/x/crypto/nacl/boxGenerateKey函數(shù),在內(nèi)部使用了curve25519算法,生成的兩個(gè)key的長(zhǎng)度都是32字節(jié)。

可以看到,它跟前面的公私鑰的長(zhǎng)度不是完全一樣的,可見(jiàn)兩者使用了不同的加密算法。前面的是ed25519,而這里是curve25519

接著回到MakeSecretConnection,繼續(xù):

    // Write local ephemeral pubkey and receive one too.
    // NOTE: every 32-byte string is accepted as a Curve25519 public key
    // (see DJB"s Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf)
    remEphPub, err := shareEphPubKey(conn, locEphPub)
    if err != nil {
        return nil, err
    }

這個(gè)shareEphPubKey就是把剛生成的一次性的locEphPub發(fā)給對(duì)方,同時(shí)也從對(duì)方那里讀取對(duì)方生成的一次性公鑰(長(zhǎng)度為32字節(jié)):

p2p/secret_connection.go#L198

func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[32]byte, err error) {
    var err1, err2 error

    cmn.Parallel(
        func() {
            _, err1 = conn.Write(locEphPub[:])
        },
        func() {
            remEphPub = new([32]byte)
            _, err2 = io.ReadFull(conn, remEphPub[:])
        },
    )

    if err1 != nil {
        return nil, err1
    }
    if err2 != nil {
        return nil, err2
    }

    return remEphPub, nil
}

由于MakeSecretConnection這個(gè)函數(shù),是兩個(gè)比原節(jié)點(diǎn)在建立起p2p連接時(shí)都會(huì)執(zhí)行的,所以?xún)烧咭龅氖虑槎际且粯拥?。如果我發(fā)了數(shù)據(jù),則對(duì)方也會(huì)發(fā)相應(yīng)的數(shù)據(jù),然后兩邊都需要讀取。所以我發(fā)了什么樣的數(shù)據(jù),我也要同時(shí)拿到什么樣的數(shù)據(jù)。

再回想本文開(kāi)始提到的telnet localhost 46658,當(dāng)我們接收到那一段亂碼時(shí),也需要給對(duì)方發(fā)過(guò)去32個(gè)字節(jié),雙方才能進(jìn)行下一步。

再回到MakeSecretConnection,接著:

    // Compute common shared secret.
    shrSecret := computeSharedSecret(remEphPub, locEphPriv)

雙方拿到對(duì)方的一次性公鑰后,都會(huì)和自己生成的一次性私鑰(注意,是私鑰)做一個(gè)運(yùn)算,生成一個(gè)叫shrSecret的密鑰在后面使用。怎么用呢?就是用它來(lái)對(duì)要發(fā)送的公鑰及簽名數(shù)據(jù)進(jìn)行加密,以及對(duì)對(duì)方發(fā)過(guò)來(lái)的公鑰和簽名數(shù)據(jù)進(jìn)行解密。

computeSharedSecret函數(shù)對(duì)應(yīng)的代碼是這樣:

p2p/secret_connection.go#L221

func computeSharedSecret(remPubKey, locPrivKey *[32]byte) (shrSecret *[32]byte) {
    shrSecret = new([32]byte)
    box.Precompute(shrSecret, remPubKey, locPrivKey)
    return
}

它是通過(guò)對(duì)方的公鑰和自己的私鑰算出來(lái)的。

這里有一個(gè)神奇的地方,就是雙方算出來(lái)的shrSecret是一樣的!也就是說(shuō),假設(shè)這里使用該算法(curve25519)生成了兩對(duì)公私鑰:

privateKey1, publicKey1
privateKey2, publicKey2

并且

publicKey2 + privateKey1 ===> sharedSecret1
publicKey1 + privateKey2 ===> sharedSecret2

那么sharedSecret1sharedSecret2是一樣的,所以雙方才可以拿各自算出來(lái)的shrSecret去解密對(duì)方的加密數(shù)據(jù)。

再接著,會(huì)根據(jù)雙方的一次性公鑰做一些計(jì)算,以供后面使用。

    // Sort by lexical order.
    loEphPub, hiEphPub := sort32(locEphPub, remEphPub)

首先是拿對(duì)方和自己的一次性公鑰進(jìn)行排序,這樣兩邊得到的loEphPubhiEphPub就是一樣的,后面在計(jì)算數(shù)值時(shí)就能得到相同的值。

然后是計(jì)算nonces,

    // Generate nonces to use for secretbox.
    recvNonce, sendNonce := genNonces(loEphPub, hiEphPub, locEphPub == loEphPub)

nonces和前面的shrSecret都是在給公鑰和簽名數(shù)據(jù)加解密時(shí)使用的。其中shrSecret是固定的,而nonce在不同的信息之間是應(yīng)該不同的,用于區(qū)別信息。

這里計(jì)算出來(lái)的recvNoncesendNonce,一個(gè)是用于接收數(shù)據(jù)后解密,一個(gè)是用于發(fā)送數(shù)據(jù)時(shí)加密。連接雙方的這兩個(gè)數(shù)據(jù)都是相反的,也就是說(shuō),一方的recvNonce與另一方的sendNonce相等,這樣當(dāng)一方使用sendNonce加密后,另一方才可以使用相同數(shù)值的recvNonce進(jìn)行解密。

在后面我們還可以看到,當(dāng)一方發(fā)送完數(shù)據(jù)后,其持有的sendNonce會(huì)增2,另一方接收并解密后,其recvNonce也會(huì)增2,雙方始終保持一致。(為什么是增2而不是增1,后面有解答)

genNonces的代碼如下:

p2p/secret_connection.go#L238

func genNonces(loPubKey, hiPubKey *[32]byte, locIsLo bool) (recvNonce, sendNonce *[24]byte) {
    nonce1 := hash24(append(loPubKey[:], hiPubKey[:]...))
    nonce2 := new([24]byte)
    copy(nonce2[:], nonce1[:])
    nonce2[len(nonce2)-1] ^= 0x01
    if locIsLo {
        recvNonce = nonce1
        sendNonce = nonce2
    } else {
        recvNonce = nonce2
        sendNonce = nonce1
    }
    return
}

可以看到,其中的一個(gè)nonce就是把前面排序后的loPubKeyhiPubKey組合起來(lái),而另一個(gè)nonce就是把最后一個(gè)bit的值由0變成1(或者由1變成0),這樣兩者就會(huì)是一個(gè)奇數(shù)一個(gè)偶數(shù)。而后來(lái)在對(duì)nonce進(jìn)行自增操作的時(shí)候,每次都是增2,這樣就保證了recvNoncesendNonce不會(huì)出現(xiàn)相等的情況,是一個(gè)很巧妙的設(shè)計(jì)。

后面又通過(guò)判斷local is loPubKey,保證了兩邊得到的recvNoncesendNonce正好相反,且一邊的recvNonce與另一邊的sendNonce正好相等。

再回到MakeSecretConnection,繼續(xù):

    // Generate common challenge to sign.
    challenge := genChallenge(loEphPub, hiEphPub)

這里根據(jù)loEphPubhiEphPub計(jì)算出來(lái)challenge,在后面將會(huì)使用自己的私鑰對(duì)它進(jìn)行簽名,再跟公鑰一起發(fā)給對(duì)方,讓對(duì)方驗(yàn)證。由于雙方的loEphPubhiEphPub是相等的,所以算出來(lái)的challenge也是相等的。

p2p/secret_connection.go#L253

func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) {
    return hash32(append(loPubKey[:], hiPubKey[:]...))
}

可以看到genChallenge就是把兩個(gè)一次性公鑰放在一起,并做了一個(gè)hash操作,得到了一個(gè)32字節(jié)的數(shù)組。

其中的hash32采用了SHA256的算法,它生成摘要的長(zhǎng)度就是32個(gè)字節(jié)。

p2p/secret_connection.go#L303

func hash32(input []byte) (res *[32]byte) {
    hasher := sha256.New()
    hasher.Write(input) // does not error
    resSlice := hasher.Sum(nil)
    res = new([32]byte)
    copy(res[:], resSlice)
    return
}

再回到MakeSecretConnection,繼續(xù):

    // Construct SecretConnection.
    sc := &SecretConnection{
        conn:       conn,
        recvBuffer: nil,
        recvNonce:  recvNonce,
        sendNonce:  sendNonce,
        shrSecret:  shrSecret,
    }

這里是生成了一個(gè)SecretConnection的對(duì)象,把相關(guān)的nonces和shrSecret傳過(guò)去,因?yàn)榇魰?huì)兒對(duì)公鑰及簽名數(shù)據(jù)的加解密操作,都放在了那邊,而這幾個(gè)參數(shù)都是需要用上的。

前面經(jīng)過(guò)了這么多的準(zhǔn)備工作,終于差不多了。下面將會(huì)使用自己的私鑰對(duì)challenge數(shù)據(jù)進(jìn)行簽名,然后跟自己的公鑰一起發(fā)送給對(duì)方:

    // Sign the challenge bytes for authentication.
    locSignature := signChallenge(challenge, locPrivKey)

    // Share (in secret) each other"s pubkey & challenge signature
    authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature)
    if err != nil {
        return nil, err
    }

其中的signChallenge就是簡(jiǎn)單的使用自己的私鑰對(duì)challenge數(shù)據(jù)進(jìn)行簽名,得到的是一個(gè)32字節(jié)的摘要:

func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKeyEd25519) (signature crypto.SignatureEd25519) {
    signature = locPrivKey.Sign(challenge[:]).Unwrap().(crypto.SignatureEd25519)
    return
}

而在shareAuthSignature中,則是把自己的公鑰與簽名后的數(shù)據(jù)locSignature一起,經(jīng)過(guò)SecretConnection的加密后傳給對(duì)方,也同時(shí)從對(duì)方那里讀取他的公鑰和簽名數(shù)據(jù),再解密。由于這一塊代碼涉及的東西比較多(有分塊,加解密,序列化與反序列化),所以放在后面再講。

再然后,

    remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig
    if !remPubKey.VerifyBytes(challenge[:], remSignature) {
        return nil, errors.New("Challenge verification failed")
    }

從對(duì)方傳過(guò)來(lái)的數(shù)據(jù)中拿出對(duì)方的公鑰和對(duì)方簽過(guò)名的數(shù)據(jù),對(duì)它們進(jìn)行驗(yàn)證。由于對(duì)方在簽名時(shí),使用的challenge數(shù)據(jù)和我們這邊產(chǎn)生的challenge一樣,所以可以直接拿出本地的challenge使用。

最后,如果驗(yàn)證通過(guò)的話(huà),則把對(duì)方的公鑰也加到SecretConnection對(duì)象中,供以后使用。

    // We"ve authorized.
    sc.remPubKey = remPubKey.Unwrap().(crypto.PubKeyEd25519)
    return sc, nil
}

到這里,我們就可以回答最開(kāi)始的問(wèn)題了:我們應(yīng)該怎樣連接一個(gè)比原節(jié)點(diǎn)呢?

答案就是:

先連上對(duì)方的p2p端口

讀取32個(gè)字節(jié),這是對(duì)方的一次性公鑰

把自己生成的一次性公鑰發(fā)給對(duì)方

讀取對(duì)方經(jīng)過(guò)加密后的公鑰+簽名數(shù)據(jù),并驗(yàn)證

把自己的公鑰和簽名數(shù)據(jù)經(jīng)過(guò)加密后,發(fā)送給對(duì)方,等待對(duì)方驗(yàn)證

如果兩邊都沒(méi)有斷開(kāi),則說(shuō)明驗(yàn)證通過(guò),后面就可以進(jìn)行更多的數(shù)據(jù)交互啦

關(guān)于shareAuthSignature的細(xì)節(jié)

前面說(shuō)到,當(dāng)使用自己的私鑰把challenge簽名得到locSignature后,將通過(guò)shareAuthSignature把它和自己的公鑰一起發(fā)給對(duì)方。它里做了很多事,我們?cè)谶@一節(jié)詳細(xì)講解一下。

shareAuthSignature的代碼如下:

p2p/secret_connection.go#L267

func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKeyEd25519, signature crypto.SignatureEd25519) (*authSigMessage, error) {
    var recvMsg authSigMessage
    var err1, err2 error

    cmn.Parallel(
        func() {
            msgBytes := wire.BinaryBytes(authSigMessage{pubKey.Wrap(), signature.Wrap()})
            _, err1 = sc.Write(msgBytes)
        },
        func() {
            readBuffer := make([]byte, authSigMsgSize)
            _, err2 = io.ReadFull(sc, readBuffer)
            if err2 != nil {
                return
            }
            n := int(0) // not used.
            recvMsg = wire.ReadBinary(authSigMessage{}, bytes.NewBuffer(readBuffer), authSigMsgSize, &n, &err2).(authSigMessage)
        })

    if err1 != nil {
        return nil, err1
    }
    if err2 != nil {
        return nil, err2
    }

    return &recvMsg, nil
}

可以看到,它做了這樣幾件事:

首先是把公鑰和簽名數(shù)據(jù)組合成了一個(gè)authSigMessage對(duì)象:authSigMessage{pubKey.Wrap(), signature.Wrap()}

然后通過(guò)一個(gè)叫go-wire的第三方庫(kù),把它序列化成了一個(gè)字節(jié)數(shù)組

然后調(diào)用SecretConnection.Write()方法,把這個(gè)數(shù)組發(fā)給對(duì)方。需要注意的是,在這個(gè)方法內(nèi)部,將對(duì)數(shù)據(jù)進(jìn)行分塊,并使用Go語(yǔ)言的secretBox.Seal對(duì)數(shù)據(jù)進(jìn)行加密。

同時(shí)從對(duì)方讀取指定長(zhǎng)度的數(shù)據(jù)(其中的authSigMsgSize為常量,值為const authSigMsgSize = (32 + 1) + (64 + 1)

然后通過(guò)SecretConnection對(duì)象中的方法讀取它,同時(shí)進(jìn)行解密

然后再通過(guò)go-wire把它變成一個(gè)authSigMessage對(duì)象

如果一切正常,把authSigMessage返回給調(diào)用者MakeSecretConnection

這里我覺(jué)得沒(méi)有必要使用go-wire對(duì)數(shù)據(jù)進(jìn)行序列化和反序列化,因?yàn)橐l(fā)送的兩個(gè)數(shù)組長(zhǎng)度是確定的(一個(gè)32,一個(gè)64),不論是發(fā)送還是讀取,都很容易確定長(zhǎng)度和拆分規(guī)則。而引入了go-wire以后,就需要知道它的工作細(xì)節(jié)(比如它產(chǎn)生的字節(jié)個(gè)數(shù)是(32 + 1) + (64 + 1)),而這個(gè)復(fù)雜性是沒(méi)有必要引入的。

SecretConnectionReadWrite

在上一段,對(duì)于發(fā)送數(shù)據(jù)時(shí)的分塊和加解密相關(guān)的操作,都放在了SecretConnection的方法中。比如sc.Write(msgBytes)io.ReadFull(sc, readBuffer)(其中的sc都是指SecretConnection對(duì)象),用到的就是SecretConnectionWriteRead。

p2p/secret_connection.go#L110

func (sc *SecretConnection) Write(data []byte) (n int, err error) {
    for 0 < len(data) {
        var frame []byte = make([]byte, totalFrameSize)
        var chunk []byte
        if dataMaxSize < len(data) {
            chunk = data[:dataMaxSize]
            data = data[dataMaxSize:]
        } else {
            chunk = data
            data = nil
        }
        chunkLength := len(chunk)
        binary.BigEndian.PutUint16(frame, uint16(chunkLength))
        copy(frame[dataLenSize:], chunk)

        // encrypt the frame
        var sealedFrame = make([]byte, sealedFrameSize)
        secretbox.Seal(sealedFrame[:0], frame, sc.sendNonce, sc.shrSecret)
        // fmt.Printf("secretbox.Seal(sealed:%X,sendNonce:%X,shrSecret:%X
", sealedFrame, sc.sendNonce, sc.shrSecret)
        incr2Nonce(sc.sendNonce)
        // end encryption

        _, err := sc.conn.Write(sealedFrame)
        if err != nil {
            return n, err
        } else {
            n += len(chunk)
        }
    }
    return
}

Write里面,除了向連接對(duì)象寫(xiě)入數(shù)據(jù)(sc.conn.Write(sealedFrame))外,它主要做了三件事:

首先是如果數(shù)據(jù)過(guò)長(zhǎng)(長(zhǎng)度超過(guò)dataMaxSize,即1024),則要把它分成多個(gè)塊。由于最后一個(gè)塊的數(shù)據(jù)可能填不滿(mǎn),所以每個(gè)塊的最開(kāi)始要用2個(gè)字節(jié)寫(xiě)入本塊中實(shí)際數(shù)據(jù)的長(zhǎng)度。

然后是調(diào)用Go的secretbox.Seal方法,對(duì)塊數(shù)據(jù)進(jìn)行加密,用到了sendNonceshrSecret這兩個(gè)參數(shù)

最后是對(duì)sendNonce進(jìn)行自增操作,這樣可保證每次發(fā)送時(shí)使用的nonce都不一樣;另外每次增2,這樣可保證它不會(huì)跟recvNonce重復(fù)

SecretConnectionRead操作,跟前面正好相反:

p2p/secret_connection.go#L143

func (sc *SecretConnection) Read(data []byte) (n int, err error) {
    if 0 < len(sc.recvBuffer) {
        n_ := copy(data, sc.recvBuffer)
        sc.recvBuffer = sc.recvBuffer[n_:]
        return
    }

    sealedFrame := make([]byte, sealedFrameSize)
    _, err = io.ReadFull(sc.conn, sealedFrame)
    if err != nil {
        return
    }

    // decrypt the frame
    var frame = make([]byte, totalFrameSize)
    // fmt.Printf("secretbox.Open(sealed:%X,recvNonce:%X,shrSecret:%X
", sealedFrame, sc.recvNonce, sc.shrSecret)
    _, ok := secretbox.Open(frame[:0], sealedFrame, sc.recvNonce, sc.shrSecret)
    if !ok {
        return n, errors.New("Failed to decrypt SecretConnection")
    }
    incr2Nonce(sc.recvNonce)
    // end decryption

    var chunkLength = binary.BigEndian.Uint16(frame) // read the first two bytes
    if chunkLength > dataMaxSize {
        return 0, errors.New("chunkLength is greater than dataMaxSize")
    }
    var chunk = frame[dataLenSize : dataLenSize+chunkLength]

    n = copy(data, chunk)
    sc.recvBuffer = chunk[n:]
    return
}

它除了正常的讀取字節(jié)外,也是做了三件事:

按塊讀取,每次讀滿(mǎn)sealedFrameSize個(gè)字節(jié),并按前兩個(gè)字節(jié)指定的長(zhǎng)度來(lái)確認(rèn)有效數(shù)據(jù)

對(duì)數(shù)據(jù)進(jìn)行解密,使用secretbox.Open以及recvNonceshrSecret這兩個(gè)參數(shù)

對(duì)recvNonce進(jìn)行自增2的操作,以便與對(duì)方的sendNonce保持一致,供下次解密使用

需要注意的是,這個(gè)函數(shù)返回的n(已讀取數(shù)據(jù)),是指的解密之后的,所以要比真實(shí)讀取的數(shù)據(jù)小一點(diǎn)。另外,在前面的shareAuthSignature中,使用的是io.ReadFull(sc),并且要讀滿(mǎn)authSigMsgSize個(gè)字節(jié),所以假如數(shù)據(jù)過(guò)長(zhǎng)的話(huà),這個(gè)Read方法可能要被調(diào)用多次。

在這一塊,由于作者假設(shè)了發(fā)送的數(shù)據(jù)的長(zhǎng)度可能過(guò)長(zhǎng),所以才需要這么復(fù)雜的分塊操作,而實(shí)際上是不需要的。如果我們簡(jiǎn)單點(diǎn)處理,是可以做到以下兩個(gè)簡(jiǎn)化的:

不需要分塊,發(fā)送一次就夠了

也因此不需要計(jì)算和維護(hù)recvNoncesendNonce,直接給個(gè)常量即可,反正只用一次,不會(huì)存在沖突

邏輯可以簡(jiǎn)單很多。而且我查了一下,這塊代碼在整個(gè)項(xiàng)目中,目前只使用了一次。如果未來(lái)真的需要,到時(shí)候再加也不遲。

目前的做法是否足夠安全

從上面的分析我們可以看到,比原為了保證節(jié)點(diǎn)間通信的安全性,是做了大量的工作的。那么,當(dāng)前的做法,是否可以完全杜絕中間人攻擊呢?

按我的理解,還是不行的,因?yàn)槿绻腥送耆宄吮仍尿?yàn)證流程,還是可以寫(xiě)出相應(yīng)的工具。比如,中間人可以按照下面的方式:

中間人首先自己生成一對(duì)一次性公鑰和一對(duì)最后用于簽名和驗(yàn)證的公私鑰(后面稱(chēng)為長(zhǎng)期公鑰),用于假冒節(jié)點(diǎn)密鑰

當(dāng)雙方節(jié)點(diǎn)建立起連接時(shí),中間人可以拿到雙方的一次性公鑰,因?yàn)樗鼈兪敲魑牡?/p>

中間人把自己生成的一次性公鑰發(fā)給雙方,假冒是來(lái)自對(duì)方節(jié)點(diǎn)的

雙方節(jié)點(diǎn)使用自己和中間人的一次性公鑰,對(duì)數(shù)據(jù)進(jìn)行加密傳給對(duì)方,此時(shí)中間人拿到數(shù)據(jù)后,可以利用自己生成的假冒一次性公鑰以及雙方之前發(fā)過(guò)來(lái)的一次性公鑰對(duì)其解密,從而拿到雙方的長(zhǎng)期公鑰

中間人將自己生成的長(zhǎng)期公鑰以及利用自己的長(zhǎng)期私鑰簽名的數(shù)據(jù)發(fā)給雙方節(jié)點(diǎn)

雙方節(jié)點(diǎn)拿到了中間人的長(zhǎng)期公鑰和簽名數(shù)據(jù),并驗(yàn)證通過(guò)

最后雙方節(jié)點(diǎn)都信任對(duì)方(實(shí)際上是信任了騙子中間人)

之后雙方節(jié)點(diǎn)向?qū)Ψ桨l(fā)送的信息(使用騙子提供的長(zhǎng)期公鑰加密),會(huì)被中間人使用相應(yīng)的長(zhǎng)期私鑰解密,從而被竊取,甚至修改后再經(jīng)過(guò)加密后轉(zhuǎn)發(fā)給另一方,而另一方完全信任,會(huì)執(zhí)行,從而導(dǎo)致?lián)p失

這個(gè)過(guò)程可以使用下圖來(lái)輔助理解:

那么這是否說(shuō)明比原的做法白做了呢?不,我認(rèn)為比原的做法已經(jīng)夠用了。

按我目前的了解,對(duì)于防范中間人,并沒(méi)有完全完美的辦法(因?yàn)槿绾伪WC安全的把公鑰通過(guò)網(wǎng)絡(luò)發(fā)送給另一方本身就是一個(gè)充滿(mǎn)挑戰(zhàn)的問(wèn)題),目前多數(shù)是證書(shū)等做法。對(duì)于比原來(lái)說(shuō),如果采用這種做法,會(huì)讓節(jié)點(diǎn)的部署和維護(hù)麻煩很多。而目前的做法,雖然不能完全杜絕,但是其實(shí)已經(jīng)解決了大部分的問(wèn)題:

沒(méi)有明文發(fā)送真正的公鑰,使得一些通用型的中間人工具無(wú)法使用

在發(fā)送公鑰時(shí),以及對(duì)簽名進(jìn)行認(rèn)證時(shí),使用了兩種不同類(lèi)型的加密方案,并且它們?cè)贕o以外的語(yǔ)言的實(shí)現(xiàn)中,可能不太兼容,這就使得騙子必須也會(huì)使用Go來(lái)編程

中間人必須讀懂比原的代碼并對(duì)此處每一個(gè)細(xì)節(jié)都清楚才可能寫(xiě)出正確的工具

我覺(jué)得這基本上就杜絕了一大撥技術(shù)能力不過(guò)關(guān)的騙子。只要我們?cè)谑褂玫臅r(shí)候,再注意防范(比如不使用不安全的網(wǎng)絡(luò)或者代理),我覺(jué)得基本上就沒(méi)什么問(wèn)題了。

代碼流程圖

最后,把我閱讀這段代碼過(guò)程中畫(huà)的流程圖分享出來(lái),也許對(duì)你自己閱讀的時(shí)候有幫助:

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/24158.html

相關(guān)文章

  • 剝開(kāi)原看代碼03:比原如何監(jiān)聽(tīng)p2p端口的

    摘要:?jiǎn)?dòng)直到進(jìn)入所以我們首先需要知道,比原在源代碼中是如何啟動(dòng),并且一步步走進(jìn)了的世界。后面省略了一些代碼,主要是用來(lái)獲取當(dāng)前監(jiān)聽(tīng)的實(shí)際以及外網(wǎng),并記錄在日志中。 比原是如何監(jiān)聽(tīng)p2p端口的 我們知道,在使用bytomd init --chain_id mainnet/testnet/solonet初始化比原的時(shí)候,它會(huì)根據(jù)給定的chain_id的不同,使用不同的端口(參看config/t...

    layman 評(píng)論0 收藏0
  • 剝開(kāi)原看代碼05:如何比原節(jié)點(diǎn)拿到區(qū)塊數(shù)據(jù)?

    摘要:作者比原項(xiàng)目倉(cāng)庫(kù)地址地址在前一篇中,我們已經(jīng)知道如何連上一個(gè)比原節(jié)點(diǎn)的端口,并與對(duì)方完成身份驗(yàn)證。代碼如下可以看到,首先是從眾多的中,找到最合適的那個(gè)。到這里,我們其實(shí)已經(jīng)知道比原是如何向其它節(jié)點(diǎn)請(qǐng)求區(qū)塊數(shù)據(jù),以及何時(shí)把信息發(fā)送出去。 作者:freewind 比原項(xiàng)目倉(cāng)庫(kù): Github地址:https://github.com/Bytom/bytom Gitee地址:https://...

    233jl 評(píng)論0 收藏0
  • 剝開(kāi)原看代碼10:比原如何通過(guò)/create-key接口創(chuàng)建密鑰的

    摘要:如果傳的是,就會(huì)在內(nèi)部使用默認(rèn)的隨機(jī)數(shù)生成器生成隨機(jī)數(shù)并生成密鑰。使用的是,生成的是一個(gè)形如這樣的全球唯一的隨機(jī)數(shù)把密鑰以文件形式保存在硬盤(pán)上。 作者:freewind 比原項(xiàng)目倉(cāng)庫(kù): Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇,我們探討了從瀏覽器的dashb...

    ccj659 評(píng)論0 收藏0
  • 剝開(kāi)原看代碼06:比原如何把請(qǐng)求區(qū)塊數(shù)據(jù)的信息發(fā)出去的

    摘要:作者比原項(xiàng)目倉(cāng)庫(kù)地址地址在前一篇中,我們說(shuō)到,當(dāng)比原向其它節(jié)點(diǎn)請(qǐng)求區(qū)塊數(shù)據(jù)時(shí),會(huì)發(fā)送一個(gè)把需要的區(qū)塊告訴對(duì)方,并把該信息對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)放入對(duì)應(yīng)的通道中,等待發(fā)送。這個(gè)就是真正與連接對(duì)象綁定的一個(gè)緩存區(qū),寫(xiě)入到它里面的數(shù)據(jù),會(huì)被發(fā)送出去。 作者:freewind 比原項(xiàng)目倉(cāng)庫(kù): Github地址:https://github.com/Bytom/bytom Gitee地址:https:...

    CloudwiseAPM 評(píng)論0 收藏0
  • 剝開(kāi)原看代碼01:初始化時(shí)生成的配置文件在哪兒

    摘要:所以這個(gè)文章系列叫作剝開(kāi)比原看代碼。所以我的問(wèn)題是比原初始化時(shí),產(chǎn)生了什么樣的配置文件,放在了哪個(gè)目錄下下面我將結(jié)合源代碼,來(lái)回答這個(gè)問(wèn)題。將用來(lái)確認(rèn)數(shù)據(jù)目錄是有效的,并且將根據(jù)傳入的不同,來(lái)生成不同的內(nèi)容寫(xiě)入到配置文件中。 作者:freewind 比原項(xiàng)目倉(cāng)庫(kù): Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee...

    felix0913 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<