摘要:總的來說,在比原中有一個類,它用于集中處理節點與外界交互的邏輯,而它的創建和啟動,又都是在中進行的。我考慮的是這樣一種情況,比如某用戶在筆記本上運行比原節點,然后在公開場合上網,使用了黑客提供的。
作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockc...
在上一篇我們已經知道了比原是如何監聽節點的p2p端口,本篇就要繼續在上篇中提到的問題:我們如何成功的連接上比原的節點,并且通過身份驗證,以便后續繼續交換數據?
在上一篇中,我們的比原節點是以solonet這個chain_id啟動的,它監聽的是46658端口。我們可以使用telnet連上它:
$ telnet localhost 46658 Trying 127.0.0.1... Connected to localhost. Escape character is "^]". ??S??%?z???_?端??????U[e
可以看到,它發過來了一些亂碼。這些亂碼是什么意思?我們應該怎么應答它?這是本篇將要回答的問題。
定位發送代碼首先我們得定位到比原向剛連接上來的節點發送數據的地方。說實話,這里實在是太繞了,山路十八彎,每次我想找到這段代碼,都需要花好一陣功夫。所以下面這段流程,我覺得你以后可能經常會過來看看。
總的來說,在比原中有一個Switch類,它用于集中處理節點與外界交互的邏輯,而它的創建和啟動,又都是在SyncManager中進行的。另外,監聽p2p端口并拿到相應的連接對象的操作,與跟連接的對象進行數據交互的操作,又是分開的,前者是在創建SyncManager的時候進行的,后者是在SyncManager的啟動(Start)方法里交由Switch進行的。所以總體來說,這一塊邏輯有點復雜(亂),繞來繞去的。
這里不先評價代碼的好壞,我們還是先把比原的處理邏輯搞清楚吧。
下面還是從啟動開始,但是由于我們在前面已經出現過多次,所以我會盡量把不需要的代碼省略掉,帶著大家快速到達目的地,然后再詳細分析。
首先是bytomd node的入口函數:
cmd/bytomd/main.go#L54
func main() { cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) cmd.Execute() }
轉交給處理參數node的函數:
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 { // ... }
如前一篇所述,“監聽端口”的操作是在node.NewNode(config)中完成的,這次發送數據的任務是在n.Start()中進行的。
但是我們還是需要看一下node.NewNode,因為它里在創建SyncManager對象的時候,生成了一個供當前連接使用的私鑰,它會在后面用到,用于產生公鑰。
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(), // ... }
就是這個privKey,它是通過ed25519生成的,后面會用到。這個私鑰僅在本次連接中使用,每個連接都會生成一個新的。
讓我們再回到主線runNode,其中n.Start又將被轉交到Node的OnStart方法:
node/node.go#L169
func (n *Node) OnStart() error { // ... n.syncManager.Start() // ... }
轉交到SyncManager的Start方法:
netsync/handle.go#L141
func (sm *SyncManager) Start() { go sm.netStart() // ... }
然后在另一個例程(goroutine)中調用了netStart()方法:
netsync/handle.go#L121
func (sm *SyncManager) netStart() error { // Start the switch _, err := sm.sw.Start() // ... }
在這里終于調用了Switch的Start方法(sm.sw中的sw就是一個Switch對象):
p2p/switch.go#L186
func (sw *Switch) OnStart() error { // ... // Start listeners for _, listener := range sw.listeners { go sw.listenerRoutine(listener) } // ... }
這里的sw.listeners,就包含了監聽p2p端口的listener。然后調用listenerRoutine()方法,感覺快到了。
p2p/switch.go#L496
func (sw *Switch) listenerRoutine(l Listener) { // ... err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig) // ... }
在這里拿到了連接到p2p端口的連接對象inConn們,傳入一堆參數,準備大刑伺候:
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) // ... }
把需要的參數細化出來,再次傳入:
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) }
再繼續,馬上就到了。
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) // ... } // ... }
終于到了關鍵的函數MakeSecretConnection()了。由于config.AuthEnc的默認值是true,所以如果沒有特別設置的話,它就會進入MakeSecretConnection,在這里完成身份驗證等各種操作,它也是我們本篇講解的重點。
好,下面我們開始。
詳解MakeSecretConnection這個函數的邏輯看起來是相當復雜的,引入了很多密鑰和各種加解密,還多次跟相應的peer進行數據發送和接收,如果不明白它為什么要這么做,是很難理解清楚的。好在一旦理解以后,明白了它的意圖,整個就簡單了。
總的來說,比原的節點之間的數據交互,是需要很高的安全性的,尤其是數據不能明文傳送,否則一旦遇到了壞的“中間人”(可以理解為數據從一個節點到另一個節點中途需要經過的各種網關、路由器、代理等等),數據就有可能被竊取甚至修改。考慮一下這個場景:用戶A想把100萬個比原從自己的帳號轉到用戶B的帳戶,結果信息被中間人修改,最后轉到了中間人指定的帳戶C,那么這損失就大了,甚至無法追回。(有同學問,“區塊鏈上的每個交易不是會有多個節點驗證嗎?如果只有單一節點使壞,應該不會生效吧”。我考慮的是這樣一種情況,比如某用戶在筆記本上運行比原節點,然后在公開場合上網,使用了黑客提供的wifi。那么該節點與其它結點的所有連接都可以被中間人攻擊,廣播出去的交易可以同時被修改,這樣其它節點拿到的都是修改后的交易。至于這種方法是否可以生效,還需要我讀完更多的代碼才能確定,這里暫時算是一個猜想吧,等我以后再來確認)
所以比原節點之間傳輸信息的時候是加密的,使用了某些非對稱加密的方法。這些方法需要在最開始的時候,節點雙方都把自己的公鑰轉給對方,之后再發信息時就可以使用對方的公鑰加密,再由對方使用私鑰解密。加密后的數據,雖然還會經過各種中間人的轉發才能到達對方,但是只要中間人沒有在最開始拿到雙方的明文公鑰并替換成自己的假冒公鑰,它就沒有辦法知道真實的數據是什么,也就沒有辦法竊取或修改。
所以這個函數的最終目的,就是:把自己的公鑰安全的發送給對方,同時安全得拿到對方的公鑰。
如果僅僅是發送公鑰,那本質上就是發送一些字節數據過去,應該很簡單。但是比原為了達到安全的目的,還進行了如下的思考:
只發送公鑰還不夠,還需要先用我的私鑰把一段數據簽個名,一起發過去,讓對方驗證一下,以保證我發過去的公鑰是正確的
明文發送公鑰不安全,所以得把它加密一下再發送
為了加密發送,我和對方都需要生成另一對一次性的公鑰和私鑰,專門用于這次加密,用完后就丟掉
為了讓我們雙方都能正確的加解密,所以需要找到一種方式,在兩邊生成同樣的用于簽名的數據(challenge)和加解密時需要的參數(sharedSecret, sendNonce/recvNonce)
另外還有一些過度的考慮:
在發送加密數據的時候,擔心每次要發送的數據過多,影響性能,所以把數據分成多個塊發送
為了配合多次發送和接收,還需要考慮如何讓兩邊的sendNonce和recvNonce保持同步改變
在發送公鑰及簽名數據時,把它們包裝成了一個對象,再進行額外的序列化和反序列化操作
我之所以認為這些是“過度”的考慮,是因為在這個交互過程中,數據的長度是固定的,并且很短(只有100多個字節),根本不需要考慮分塊。另外公鑰和簽名數據就是兩個簡單的、長度固定的字節數組,并且只在這里用一次,我覺得可以直接發送兩個數組即可,包裝成對象及序列化后,我們還需要考慮序列化之后的數組長度是如何變化的。
在查閱了相關的代碼以后,我發現這一處邏輯只在這里使用了一次,沒有必要提前考慮到通用但更復雜的情況,提前編碼。畢竟那些情況有可能永遠不會發生,而提前寫好的代碼所增加的復雜度以及可能多出來的bug卻是永遠存在了。
《敏捷軟件開發 原則、模式和實踐》這本書告訴我們:不要預先設計,盡量用簡單的辦法實現,等到變化真的到來了,再考慮如何重構讓它適應這種變化。
下面講解“MakeSecretConnection”,由于該方法有點長,所以會分成幾塊:
p2p/listener.go#L52
func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKeyEd25519) (*SecretConnection, error) { locPubKey := locPrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519)
首先注意的是參數locPrivKey,它就是在前面最開始的時候,在SyncManager中生成的用于本次連接通信的私鑰。然后根據該私鑰,生成對應的公鑰,對于同一個私鑰,生成的公鑰總是相同的。
這個私鑰的長度是64字節,公鑰是32字節,可見兩者不是一樣長的。公鑰短一些,更適合加密(速度快一點)。
呆會兒在最后會使用該私鑰對一段數據進行簽名,然后跟這個公鑰一起,經過加密后發送給peer,讓他驗證。成功之后,對方會一直持有這個公鑰,向我們發送數據前會用它對數據進行加密。
接著,
// Generate ephemeral keys for perfect forward secrecy. locEphPub, locEphPriv := genEphKeys()
這里生成了一對一次性的公私鑰,用于本次連接中對開始那個公鑰(和簽名數據)進行加密。
待會兒會發把這里生成的locEphPub以明文的方式傳給對方(為什么是明文?因為必須得有一次明文發送,不然對方一開始就拿到加密的數據沒法解開),它就我們在本文開始通過telnet localhost 46658時收到的那一堆亂碼。
genEphKeys(),對應于:
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 }
它調用了golang.org/x/crypto/nacl/box的GenerateKey函數,在內部使用了curve25519算法,生成的兩個key的長度都是32字節。
可以看到,它跟前面的公私鑰的長度不是完全一樣的,可見兩者使用了不同的加密算法。前面的是ed25519,而這里是curve25519。
接著回到MakeSecretConnection,繼續:
// 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 }
這個shareEphPubKey就是把剛生成的一次性的locEphPub發給對方,同時也從對方那里讀取對方生成的一次性公鑰(長度為32字節):
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這個函數,是兩個比原節點在建立起p2p連接時都會執行的,所以兩者要做的事情都是一樣的。如果我發了數據,則對方也會發相應的數據,然后兩邊都需要讀取。所以我發了什么樣的數據,我也要同時拿到什么樣的數據。
再回想本文開始提到的telnet localhost 46658,當我們接收到那一段亂碼時,也需要給對方發過去32個字節,雙方才能進行下一步。
再回到MakeSecretConnection,接著:
// Compute common shared secret. shrSecret := computeSharedSecret(remEphPub, locEphPriv)
雙方拿到對方的一次性公鑰后,都會和自己生成的一次性私鑰(注意,是私鑰)做一個運算,生成一個叫shrSecret的密鑰在后面使用。怎么用呢?就是用它來對要發送的公鑰及簽名數據進行加密,以及對對方發過來的公鑰和簽名數據進行解密。
computeSharedSecret函數對應的代碼是這樣:
p2p/secret_connection.go#L221
func computeSharedSecret(remPubKey, locPrivKey *[32]byte) (shrSecret *[32]byte) { shrSecret = new([32]byte) box.Precompute(shrSecret, remPubKey, locPrivKey) return }
它是通過對方的公鑰和自己的私鑰算出來的。
這里有一個神奇的地方,就是雙方算出來的shrSecret是一樣的!也就是說,假設這里使用該算法(curve25519)生成了兩對公私鑰:
privateKey1, publicKey1 privateKey2, publicKey2
并且
publicKey2 + privateKey1 ===> sharedSecret1 publicKey1 + privateKey2 ===> sharedSecret2
那么sharedSecret1和sharedSecret2是一樣的,所以雙方才可以拿各自算出來的shrSecret去解密對方的加密數據。
再接著,會根據雙方的一次性公鑰做一些計算,以供后面使用。
// Sort by lexical order. loEphPub, hiEphPub := sort32(locEphPub, remEphPub)
首先是拿對方和自己的一次性公鑰進行排序,這樣兩邊得到的loEphPub和hiEphPub就是一樣的,后面在計算數值時就能得到相同的值。
然后是計算nonces,
// Generate nonces to use for secretbox. recvNonce, sendNonce := genNonces(loEphPub, hiEphPub, locEphPub == loEphPub)
nonces和前面的shrSecret都是在給公鑰和簽名數據加解密時使用的。其中shrSecret是固定的,而nonce在不同的信息之間是應該不同的,用于區別信息。
這里計算出來的recvNonce與sendNonce,一個是用于接收數據后解密,一個是用于發送數據時加密。連接雙方的這兩個數據都是相反的,也就是說,一方的recvNonce與另一方的sendNonce相等,這樣當一方使用sendNonce加密后,另一方才可以使用相同數值的recvNonce進行解密。
在后面我們還可以看到,當一方發送完數據后,其持有的sendNonce會增2,另一方接收并解密后,其recvNonce也會增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 }
可以看到,其中的一個nonce就是把前面排序后的loPubKey和hiPubKey組合起來,而另一個nonce就是把最后一個bit的值由0變成1(或者由1變成0),這樣兩者就會是一個奇數一個偶數。而后來在對nonce進行自增操作的時候,每次都是增2,這樣就保證了recvNonce與sendNonce不會出現相等的情況,是一個很巧妙的設計。
后面又通過判斷local is loPubKey,保證了兩邊得到的recvNonce與sendNonce正好相反,且一邊的recvNonce與另一邊的sendNonce正好相等。
再回到MakeSecretConnection,繼續:
// Generate common challenge to sign. challenge := genChallenge(loEphPub, hiEphPub)
這里根據loEphPub和hiEphPub計算出來challenge,在后面將會使用自己的私鑰對它進行簽名,再跟公鑰一起發給對方,讓對方驗證。由于雙方的loEphPub和hiEphPub是相等的,所以算出來的challenge也是相等的。
p2p/secret_connection.go#L253
func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) { return hash32(append(loPubKey[:], hiPubKey[:]...)) }
可以看到genChallenge就是把兩個一次性公鑰放在一起,并做了一個hash操作,得到了一個32字節的數組。
其中的hash32采用了SHA256的算法,它生成摘要的長度就是32個字節。
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,繼續:
// Construct SecretConnection. sc := &SecretConnection{ conn: conn, recvBuffer: nil, recvNonce: recvNonce, sendNonce: sendNonce, shrSecret: shrSecret, }
這里是生成了一個SecretConnection的對象,把相關的nonces和shrSecret傳過去,因為呆會兒對公鑰及簽名數據的加解密操作,都放在了那邊,而這幾個參數都是需要用上的。
前面經過了這么多的準備工作,終于差不多了。下面將會使用自己的私鑰對challenge數據進行簽名,然后跟自己的公鑰一起發送給對方:
// 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就是簡單的使用自己的私鑰對challenge數據進行簽名,得到的是一個32字節的摘要:
func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKeyEd25519) (signature crypto.SignatureEd25519) { signature = locPrivKey.Sign(challenge[:]).Unwrap().(crypto.SignatureEd25519) return }
而在shareAuthSignature中,則是把自己的公鑰與簽名后的數據locSignature一起,經過SecretConnection的加密后傳給對方,也同時從對方那里讀取他的公鑰和簽名數據,再解密。由于這一塊代碼涉及的東西比較多(有分塊,加解密,序列化與反序列化),所以放在后面再講。
再然后,
remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig if !remPubKey.VerifyBytes(challenge[:], remSignature) { return nil, errors.New("Challenge verification failed") }
從對方傳過來的數據中拿出對方的公鑰和對方簽過名的數據,對它們進行驗證。由于對方在簽名時,使用的challenge數據和我們這邊產生的challenge一樣,所以可以直接拿出本地的challenge使用。
最后,如果驗證通過的話,則把對方的公鑰也加到SecretConnection對象中,供以后使用。
// We"ve authorized. sc.remPubKey = remPubKey.Unwrap().(crypto.PubKeyEd25519) return sc, nil }
到這里,我們就可以回答最開始的問題了:我們應該怎樣連接一個比原節點呢?
答案就是:
先連上對方的p2p端口
讀取32個字節,這是對方的一次性公鑰
把自己生成的一次性公鑰發給對方
讀取對方經過加密后的公鑰+簽名數據,并驗證
把自己的公鑰和簽名數據經過加密后,發送給對方,等待對方驗證
如果兩邊都沒有斷開,則說明驗證通過,后面就可以進行更多的數據交互啦
關于shareAuthSignature的細節前面說到,當使用自己的私鑰把challenge簽名得到locSignature后,將通過shareAuthSignature把它和自己的公鑰一起發給對方。它里做了很多事,我們在這一節詳細講解一下。
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 }
可以看到,它做了這樣幾件事:
首先是把公鑰和簽名數據組合成了一個authSigMessage對象:authSigMessage{pubKey.Wrap(), signature.Wrap()}
然后通過一個叫go-wire的第三方庫,把它序列化成了一個字節數組
然后調用SecretConnection.Write()方法,把這個數組發給對方。需要注意的是,在這個方法內部,將對數據進行分塊,并使用Go語言的secretBox.Seal對數據進行加密。
同時從對方讀取指定長度的數據(其中的authSigMsgSize為常量,值為const authSigMsgSize = (32 + 1) + (64 + 1))
然后通過SecretConnection對象中的方法讀取它,同時進行解密
然后再通過go-wire把它變成一個authSigMessage對象
如果一切正常,把authSigMessage返回給調用者MakeSecretConnection
這里我覺得沒有必要使用go-wire對數據進行序列化和反序列化,因為要發送的兩個數組長度是確定的(一個32,一個64),不論是發送還是讀取,都很容易確定長度和拆分規則。而引入了go-wire以后,就需要知道它的工作細節(比如它產生的字節個數是(32 + 1) + (64 + 1)),而這個復雜性是沒有必要引入的。
SecretConnection的Read和Write在上一段,對于發送數據時的分塊和加解密相關的操作,都放在了SecretConnection的方法中。比如sc.Write(msgBytes)和io.ReadFull(sc, readBuffer)(其中的sc都是指SecretConnection對象),用到的就是SecretConnection的Write和Read。
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里面,除了向連接對象寫入數據(sc.conn.Write(sealedFrame))外,它主要做了三件事:
首先是如果數據過長(長度超過dataMaxSize,即1024),則要把它分成多個塊。由于最后一個塊的數據可能填不滿,所以每個塊的最開始要用2個字節寫入本塊中實際數據的長度。
然后是調用Go的secretbox.Seal方法,對塊數據進行加密,用到了sendNonce和shrSecret這兩個參數
最后是對sendNonce進行自增操作,這樣可保證每次發送時使用的nonce都不一樣;另外每次增2,這樣可保證它不會跟recvNonce重復
而SecretConnection的Read操作,跟前面正好相反:
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 }
它除了正常的讀取字節外,也是做了三件事:
按塊讀取,每次讀滿sealedFrameSize個字節,并按前兩個字節指定的長度來確認有效數據
對數據進行解密,使用secretbox.Open以及recvNonce和shrSecret這兩個參數
對recvNonce進行自增2的操作,以便與對方的sendNonce保持一致,供下次解密使用
需要注意的是,這個函數返回的n(已讀取數據),是指的解密之后的,所以要比真實讀取的數據小一點。另外,在前面的shareAuthSignature中,使用的是io.ReadFull(sc),并且要讀滿authSigMsgSize個字節,所以假如數據過長的話,這個Read方法可能要被調用多次。
在這一塊,由于作者假設了發送的數據的長度可能過長,所以才需要這么復雜的分塊操作,而實際上是不需要的。如果我們簡單點處理,是可以做到以下兩個簡化的:
不需要分塊,發送一次就夠了
也因此不需要計算和維護recvNonce和sendNonce,直接給個常量即可,反正只用一次,不會存在沖突
邏輯可以簡單很多。而且我查了一下,這塊代碼在整個項目中,目前只使用了一次。如果未來真的需要,到時候再加也不遲。
目前的做法是否足夠安全從上面的分析我們可以看到,比原為了保證節點間通信的安全性,是做了大量的工作的。那么,當前的做法,是否可以完全杜絕中間人攻擊呢?
按我的理解,還是不行的,因為如果有人完全清楚了比原的驗證流程,還是可以寫出相應的工具。比如,中間人可以按照下面的方式:
中間人首先自己生成一對一次性公鑰和一對最后用于簽名和驗證的公私鑰(后面稱為長期公鑰),用于假冒節點密鑰
當雙方節點建立起連接時,中間人可以拿到雙方的一次性公鑰,因為它們是明文的
中間人把自己生成的一次性公鑰發給雙方,假冒是來自對方節點的
雙方節點使用自己和中間人的一次性公鑰,對數據進行加密傳給對方,此時中間人拿到數據后,可以利用自己生成的假冒一次性公鑰以及雙方之前發過來的一次性公鑰對其解密,從而拿到雙方的長期公鑰
中間人將自己生成的長期公鑰以及利用自己的長期私鑰簽名的數據發給雙方節點
雙方節點拿到了中間人的長期公鑰和簽名數據,并驗證通過
最后雙方節點都信任對方(實際上是信任了騙子中間人)
之后雙方節點向對方發送的信息(使用騙子提供的長期公鑰加密),會被中間人使用相應的長期私鑰解密,從而被竊取,甚至修改后再經過加密后轉發給另一方,而另一方完全信任,會執行,從而導致損失
這個過程可以使用下圖來輔助理解:
那么這是否說明比原的做法白做了呢?不,我認為比原的做法已經夠用了。
按我目前的了解,對于防范中間人,并沒有完全完美的辦法(因為如何保證安全的把公鑰通過網絡發送給另一方本身就是一個充滿挑戰的問題),目前多數是證書等做法。對于比原來說,如果采用這種做法,會讓節點的部署和維護麻煩很多。而目前的做法,雖然不能完全杜絕,但是其實已經解決了大部分的問題:
沒有明文發送真正的公鑰,使得一些通用型的中間人工具無法使用
在發送公鑰時,以及對簽名進行認證時,使用了兩種不同類型的加密方案,并且它們在Go以外的語言的實現中,可能不太兼容,這就使得騙子必須也會使用Go來編程
中間人必須讀懂比原的代碼并對此處每一個細節都清楚才可能寫出正確的工具
我覺得這基本上就杜絕了一大撥技術能力不過關的騙子。只要我們在使用的時候,再注意防范(比如不使用不安全的網絡或者代理),我覺得基本上就沒什么問題了。
代碼流程圖最后,把我閱讀這段代碼過程中畫的流程圖分享出來,也許對你自己閱讀的時候有幫助:
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24158.html
摘要:啟動直到進入所以我們首先需要知道,比原在源代碼中是如何啟動,并且一步步走進了的世界。后面省略了一些代碼,主要是用來獲取當前監聽的實際以及外網,并記錄在日志中。 比原是如何監聽p2p端口的 我們知道,在使用bytomd init --chain_id mainnet/testnet/solonet初始化比原的時候,它會根據給定的chain_id的不同,使用不同的端口(參看config/t...
摘要:作者比原項目倉庫地址地址在前一篇中,我們已經知道如何連上一個比原節點的端口,并與對方完成身份驗證。代碼如下可以看到,首先是從眾多的中,找到最合適的那個。到這里,我們其實已經知道比原是如何向其它節點請求區塊數據,以及何時把信息發送出去。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://...
摘要:如果傳的是,就會在內部使用默認的隨機數生成器生成隨機數并生成密鑰。使用的是,生成的是一個形如這樣的全球唯一的隨機數把密鑰以文件形式保存在硬盤上。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇,我們探討了從瀏覽器的dashb...
摘要:作者比原項目倉庫地址地址在前一篇中,我們說到,當比原向其它節點請求區塊數據時,會發送一個把需要的區塊告訴對方,并把該信息對應的二進制數據放入對應的通道中,等待發送。這個就是真正與連接對象綁定的一個緩存區,寫入到它里面的數據,會被發送出去。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https:...
摘要:所以這個文章系列叫作剝開比原看代碼。所以我的問題是比原初始化時,產生了什么樣的配置文件,放在了哪個目錄下下面我將結合源代碼,來回答這個問題。將用來確認數據目錄是有效的,并且將根據傳入的不同,來生成不同的內容寫入到配置文件中。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee...
閱讀 928·2021-11-23 09:51
閱讀 993·2021-11-18 10:02
閱讀 1908·2021-09-10 11:27
閱讀 3138·2021-09-10 10:51
閱讀 778·2019-08-29 15:13
閱讀 2064·2019-08-29 11:32
閱讀 2501·2019-08-29 11:25
閱讀 3044·2019-08-26 11:46