관리 메뉴

HAMA 블로그

[이더리움] RLPX - Encryption handshake 본문

블록체인

[이더리움] RLPX - Encryption handshake

[하마] 이승현 (wowlsh93@gmail.com) 2019. 1. 21. 11:26


이더리움에서 TCP기반의 스트리밍을 위해 peer끼리 커넥션을 맺을때 아래와 같은 순서를 갖는데 

1. 암호화키를 교환하여 앞으로는 이 키 기반으로 통신하게 함
2. 프로토콜 정보(버전등)를 교환하여 서로 동업자인지 확인하고 
3. rlp 로 엔코딩하고 서로 약속된 frame에 맞춰서 메세지를 주고 받는다.

이 글에서는 1번에 대해서 코드와 함께 알아 볼 것이다.

접속을 하는 peer를 initiator 라고 하고 접속을 받는 peer를 receiver 라고 한다.
한번 접속했던 경우 known peer라고 하고, 처음 접속하는 peer를 new peer라고 한다. 
한번 접속했던 경우에는 session token을 보관하는데, 그렇더라도 항상 접속할때마다 그 기반으로 새로운 키를 만들어 사용한다.

(참고: 스트리밍과  RPC 와 차이점? 어차피 TCP소켓통신은 똑같지만 RPC는 한단계 더 추상화 한 것. 보통 함수호출 후 간단한 리턴을 받을때 RPC 사용. 스트리밍은 느낌 그대로 큰 데이터를 주고 받는 느낌으로 사용.이더리움에서 peer간 블록전송을 위해서는 스트리밍, 클라이언트가 트랜잭션 일으킬때는 HTTP 기반 JSON RPC API를 사용한다. 하이퍼레저 패브릭 패브릭에서는 성능이 좋은 gRPC를 적극적으로 사용하는데 반해 이더리움에서는 사용하지 않는 듯하다.  The Ethereum ABI to gRPC protobuf transpiler 이런게 있긴 하다.)

앞으로 글을 읽어 내려 갈때 만날 단어들 정리 (자세한 차이는 구글링)
ECDSA : Elliptic Curve Digital Signature Algorithm (기본 서명,암호화)
ECIES :  Elliptic Curve Integrated Encryption scheme (공유 대칭키 생성) 
ECDH : Elliptic Curve Diffie–Hellman key exchange (공유 대칭키 생성)
HMAC:  hash message authentication code  (해싱에 의한 메세지 검증 및 인증)


Encrpytion handshake

엘리스와 댄이 암호화 통신을 하기 위해서는 보통 댄의 공개키를 앨리스가 받아서, 그것으로 대칭키를 암호화해서 댄에게 주면  댄이 자신의 개인키로 복호화하여 안전하게 통신할수 있는것을 상상 할 수 있다. 근데 이때 문제는 공개키가 엘리스로 갈때 탈취 될 수 있다는 것이며, 댄의 공개키인지 확실히 알 수 없다. 보통 CA를 이용해서 전자서명을 통해 해결하곤 하는데, 여기서는 CA도 필요없고  대칭키를 이동하는 과정 없이 해결하는 방식을 사용한다. 

해당 과정에 대해서 코드를 통해 살펴보자. (가독성을 위해 예외처리는 모두 삭제)

// secrets represents the connection secrets
// which are negotiated during the encryption handshake.
type secrets struct {
RemoteID discover.NodeID
AES, MAC []byte
EgressMAC, IngressMAC hash.Hash
Token []byte
}

이후 과정은 결국 위의 자신과 리모트가 공유할 대칭키(secrets)를 갖기 위함이란 걸 알아두자.이 대칭키는 추후에 있을 실제 블록공유등의 read/write 메세지를 암호화하기 위해 이용될 것이다.

// This field must be set to a valid secp256k1 private key.
PrivateKey *ecdsa.PrivateKey `toml:"-"`

1. 각 peer는 자신만의 PrivateKey를 secp256k1 타입으로 가지고 있어야 한다. 

  --nodekey value       P2P node key file
  --nodekeyhex value    P2P node key as hex (for testing)

이 키는 위의 커맨드 라인  옵션에 따라  crypto.LoadECDSA(file) 나 crypto.HexToECDSA(hex) 함수로 임시적으로 만들어 진다.

if c.id, err = c.doEncHandshake(srv.PrivateKey, dialDest); err != nil {
return err
}

2. Encryption handshake 진입 (내 노드의 임시 개인키와 노드디스커버리를 통해 찾은 리모트 피어 정보를 매개변수로) 

if dial == nil {
sec, err = receiverEncHandshake(t.fd, prv, nil)
} else {
sec, err = initiatorEncHandshake(t.fd, prv, dial.ID, nil)
}

3. initiator 면 initiatorEncHandshake 를 receiver 면 receiverEncHandshake 호출. 

h := &encHandshake{initiator: true, remoteID: remoteID}
authMsg, err := h.makeAuthMsg(prv, token)
if err != nil {
return s, err
}
authPacket, err := sealEIP8(authMsg, h)
if err != nil {
return s, err
}
if _, err = conn.Write(authPacket); err != nil {
return s, err
}

authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
if err != nil {
return s, err
}
if err := h.handleAuthResp(authRespMsg); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)

4. initiatorEncHandshake 내에서 makeAuthMsg로 진입한다. 

// 메개변수 prv 는 자기노드의 개인키이고, token은 널이다. func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey, token []byte) (*authMsgV4, error) {
//리모트노드의ID는 이더리움에서 Public Key로 사용된다.ecdsa.PublicKey 로 리턴한다. rpub, err := h.remoteID.Pubkey()
// ECDSA public key 를 ECIES public key로 사용되게 변환한다.
h.remotePub = ecies.ImportECDSAPublic(rpub)
// random initiator nonce 생성
h.initNonce = make([]byte, shaLen)
if _, err := rand.Read(h.initNonce); err != nil {
return nil, err
}
// ECDH를 위한 랜덤 키페어 생성
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
// 자신개인키(prv)로 ECIES 키페어 만들고,remotePub을 섞어서 shared-secret 생성

token, err = h.staticSharedSecret(prv) // shared-secret 와 nonce 를 ^ 해서 sigend 생성
signed := xor(token, h.initNonce) // signed 를 리모트의 개인키를 통해 사인해 줘서 signature 생성
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
// 최종적으로 authMsgV4 생성 // RLPx v4 handshake auth (defined in EIP-8).
msg := new(authMsgV4)
copy(msg.Signature[:], signature)
copy(msg.InitiatorPubkey[:], crypto.FromECDSAPub(&prv.PublicKey)[1:])
copy(msg.Nonce[:], h.initNonce)
msg.Version = 4
return msg, nil
}

5. authMsg 를 리턴한다. 


var padSpace = make([]byte, 300)

func sealEIP8(msg interface{}, h *encHandshake) ([]byte, error) {
buf := new(bytes.Buffer)
if err := rlp.Encode(buf, msg); err != nil {
return nil, err
}
// pad with random amount of data. the amount needs to be at least 100 bytes to make
// the message distinguishable from pre-EIP-8 handshakes.
pad := padSpace[:mrand.Intn(len(padSpace)-100)+100]
buf.Write(pad)
prefix := make([]byte, 2)
binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead))

enc, err := ecies.Encrypt(rand.Reader, h.remotePub, buf.Bytes(), nil, prefix)
return append(prefix, enc...), err
}

6. 5번에서 받은 auth 메세지에 패딩을 추가해서 리모트Pub키로 ecies.Encrypt 한후에 prefix를 추가해서 리턴한다.

if _, err = conn.Write(authPacket); err != nil {
return s, err
}

7. 6번에서 리턴한 패킷(authPacket)을 리모트로 쏴 준다.

// receiverEncHandshake negotiates a session token on conn.
// it should be called on the listening side of the connection.
//
// prv is the local client's private key.
// token is the token from a previous session with this node.
func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) {
authMsg := new(authMsgV4)
authPacket, err := readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn)
if err != nil {
return s, err
}
h := new(encHandshake)
if err := h.handleAuthMsg(authMsg, prv); err != nil {
return s, err
}

authRespMsg, err := h.makeAuthResp()
if err != nil {
return s, err
}
var authRespPacket []byte
if authMsg.gotPlain {
authRespPacket, err = authRespMsg.sealPlain(h)
} else {
authRespPacket, err = sealEIP8(authRespMsg, h)
}
if err != nil {
return s, err
}
if _, err = conn.Write(authRespPacket); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)
}

8. 리모트 peer는 해당 authPacket을 받아서 처리 (secrets 를 생성) 한후에 다시 initiator로 ack 해준다.


func readHandshakeMsg(msg plainDecoder, plainSize int, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) {
buf := make([]byte, plainSize)
if _, err := io.ReadFull(r, buf); err != nil {
return buf, err
}
// Attempt decoding pre-EIP-8 "plain" format.
key := ecies.ImportECDSA(prv)
if dec, err := key.Decrypt(buf, nil, nil); err == nil {
msg.decodePlain(dec)
return buf, nil
}
// Could be EIP-8 format, try that.
prefix := buf[:2]
size := binary.BigEndian.Uint16(prefix)
if size < uint16(plainSize) {
return buf, fmt.Errorf("size underflow, need at least %d bytes", plainSize)
}
buf = append(buf, make([]byte, size-uint16(plainSize)+2)...)
if _, err := io.ReadFull(r, buf[plainSize:]); err != nil {
return buf, err
}
dec, err := key.Decrypt(buf[2:], nil, prefix)
if err != nil {
return buf, err
}
// Can't use rlp.DecodeBytes here because it rejects
// trailing data (forward-compatibility).
s := rlp.NewStream(bytes.NewReader(dec), 0)
return buf, s.Decode(msg)
}
authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
if err != nil {
return s, err
}
if err := h.handleAuthResp(authRespMsg); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)
func (h *encHandshake) handleAuthResp(msg *authRespV4) (err error) {
h.respNonce = msg.Nonce[:]
h.remoteRandomPub, err = importPublicKey(msg.RandomPubkey[:])
return err
}

9. initiator 는 리모트 peer로부터 받은 auth메세지로 randomPub 키를 생성한다. 이 말은 상호간에 랜덤하게 생성된 pub/pri 키를 활용한다는 뜻이다.

// secrets is called after the handshake is completed.
// It extracts the connection secrets from the handshake values.
func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
if err != nil {
return secrets{}, err
}

// derive base secrets from ephemeral key agreement
sharedSecret := crypto.Keccak256(ecdheSecret, crypto.Keccak256(h.respNonce, h.initNonce))
aesSecret := crypto.Keccak256(ecdheSecret, sharedSecret)
s := secrets{
RemoteID: h.remoteID,
AES: aesSecret,
MAC: crypto.Keccak256(ecdheSecret, aesSecret),
}

// setup sha3 instances for the MACs
mac1 := sha3.NewKeccak256()
mac1.Write(xor(s.MAC, h.respNonce))
mac1.Write(auth)
mac2 := sha3.NewKeccak256()
mac2.Write(xor(s.MAC, h.initNonce))
mac2.Write(authResp)
if h.initiator {
s.EgressMAC, s.IngressMAC = mac1, mac2
} else {
s.EgressMAC, s.IngressMAC = mac2, mac1
}

return s, nil
}

9. random 하게 생셩된 pub/priv를 사용하여 secrets를 만들기 시작한다. 

굉장히 복잡해 보이는게 사실이나 중요 포인트는 2가지이다.
- 고정적인 NodeID(public key) 와 노드 고유의 Private 키를 가지고 random pri-key / pub-key를 상호 생성/교환 
- 랜덤으로 생성한 pub/pri 키를 통해 secrets 생성이다.

이렇게 생성된 secrets.AES 로는 암호화/복호화를 하고, MAC으로는 메세지 authentication을 하게 된다. 
아래처럼 정리 될 수있다.

auth -> E(remote-pubk, S(ephemeral-privk, static-shared-secret ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x0)
auth-ack -> E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0)

static-shared-secret = ecdh.agree(privkey, remote-pubk)

E: 첫번째 인자로 암호화
S: 첫번째 인자로 사인 
H: 해싱
|| : 더하기 
^ : XOR
auth: initiator가 만들어서 receiver에 전송하는 메세지
auth-ack : receiver가 돌려주는 메세지
서로간의 ephemeral-pubk (임시공개키)를 교환하는것이 핵심이다.
이렇게 교환된 임시공개키와 자신이 가지고있는 임시개인키를 이용하여 각자 secrets를 아래처럼 만든다.

ephemeral-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = keccak256(ephemeral-shared-secret || keccak256(nonce || initiator-nonce))
aes-secret = keccak256(ephemeral-shared-secret || shared-secret)
# destroy shared-secret
mac-secret = keccak256(ephemeral-shared-secret || aes-secret)
# destroy ephemeral-shared-secret

Initiator:
egress-mac = keccak256.update(mac-secret ^ recipient-nonce || auth-sent-init)
# destroy nonce
ingress-mac = keccak256.update(mac-secret ^ initiator-nonce || auth-recvd-ack)
# destroy remote-nonce

Recipient:
egress-mac = keccak256.update(mac-secret ^ initiator-nonce || auth-sent-ack)
# destroy nonce
ingress-mac = keccak256.update(mac-secret ^ recipient-nonce || auth-recvd-init)
# destroy remote-nonce

 임시 대칭키 (shared_secrete) = 자신의 임시 개인키 와 상대방의 임시공개키 만드는 것을 볼 수 있다.
 이 임시 대칭키는 상대방의 임시개인키와 자신의 임시공개키로도 똑같이 만들 수 있고 ECDH라고 한다.
마지막으로 임시대칭키를 이용해서 인증과 데이타가 변경되지 않았다는 것을 보증하는 MAC도 생성하며 처음 연결시에 만들어진 Ingress/egressMAC은 데이터를 주고 받으면서 계속 업데이트 되어 보안이 더 강화된다.


Comments