-
Notifications
You must be signed in to change notification settings - Fork 668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement P2P auth handshake #96
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My brain hurts a little trying to ingest all of this stuff.
evm/p2p/auth.py
Outdated
initiator = HandshakeInitiator(remote, privkey) | ||
reader, writer = yield from initiator.connect() | ||
|
||
initiator_nonce = keccak(int_to_big_endian(random.randint(0, 2 ** 256 - 1))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it safe for us to be using random.random
? My understanding has always been no.
With pycryptodome
we can use Crypto.Random.get_random_bytes(n)
which I understand to be appropriate for key generation. thoughts?
evm/p2p/auth.py
Outdated
|
||
ephemeral_pubkey, responder_nonce, auth_ack = initiator.decode_auth_ack_message(ack_data) | ||
aes_secret, mac_secret, egress_mac, ingress_mac = initiator.derive_secrets( | ||
initiator_nonce, responder_nonce, ephemeral_pubkey, auth_init, auth_ack) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting nitpick
I find this style hard to read. Can you drop these all onto their own lines and maybe even call it with keyword arguments to ensure that argument ordering doesn't ever get subtly borked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but as I said below I'm moving this to another branch
evm/p2p/auth.py
Outdated
aes_secret, mac_secret, egress_mac, ingress_mac = initiator.derive_secrets( | ||
initiator_nonce, responder_nonce, ephemeral_pubkey, auth_init, auth_ack) | ||
|
||
peer = Peer(remote, privkey, reader, writer, aes_secret, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've always been a bit frightened of function calls and object instantaition that uses more than 3-4 positional arguments given the ease of transposing them or just plain getting the order wrong. Thoughts on converting this to use keyword arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm also a bit weary of them so I'm happy to do that. But I'll move this whole function and the creation of the Peer class to another branch I have, as this really belongs there and will mean less conflicts when I rebase the other branch after we're done with the code review
evm/p2p/auth.py
Outdated
except AuthenticationError: | ||
eph_pubkey, nonce, version = decode_ack_eip8(ciphertext, self.privkey) | ||
self.got_eip8_auth = True | ||
auth_ack = ciphertext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this assignment struck me as strange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch; it makes no sense for this method to do this and then return it afterwards, as this is the very argument passed into the method. Fixed so it no longer returns auth_ack and dropped the assignment
evm/p2p/auth.py
Outdated
# S || H(ephemeral-pubk) || pubk || nonce || 0x0 | ||
flag = 0x0 | ||
auth_message = S + keccak(self.ephemeral_pubkey) + self.pubkey + \ | ||
nonce + ascii_chr(flag) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ascii_chr(flag
seems like strange indirection for the literal b'\x00'
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, fixed
evm/p2p/auth.py
Outdated
if self.got_eip8_auth: | ||
# The EIP-8 version has an authenticated length prefix. | ||
prefix = struct.pack('>H', len(ack_message) + ecies.encrypt_overhead_length) | ||
auth_ack = ecies.encrypt(ack_message, self.remote.pubkey, shared_mac_data=prefix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename to suffix
to avoid the overwriting of this value on the next line.
evm/p2p/ecies.py
Outdated
mode = modes.CTR | ||
curve = ec.SECP256K1() | ||
# ECIES using AES256 and HMAC-SHA-256-32 | ||
key_len = 32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on renaming this and the others above it to KEY_LEN
?
I find that casing helps distinguish top level module variables from vars in the local context.
evm/p2p/constants.py
Outdated
encrypted_auth_msg_len = auth_msg_len + encrypt_overhead_length | ||
|
||
# Length of encrypted pre-EIP-8 handshake reply | ||
encrypted_auth_ack_len = auth_ack_len + encrypt_overhead_length |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can all of these be changed to CAP_CASE
so that it's a bit easier to tell they are constants when they are used in other modules.
evm/p2p/kademlia.py
Outdated
@@ -81,7 +81,7 @@ def __init__(self, pubkey, address): | |||
@classmethod | |||
def from_uri(cls, uri): | |||
ip, port, pubkey = host_port_pubkey_from_uri(uri) | |||
return cls(pubkey, Address(ip.decode(), int(port))) | |||
return cls(pubkey, Address(ip.decode(), int(port), int(port))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one line but this seems like it could get extracted and merged separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was left by accident, I'll remove it
evm/p2p/utils.py
Outdated
def sxor(s1, s2): | ||
if len(s1) != len(s2): | ||
raise ValueError("Cannot sxor strings of different length") | ||
return b''.join(ascii_chr(safe_ord(a) ^ safe_ord(b)) for a, b in zip(s1, s2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if this is much better in terms of readability. but...
bytes((itertools.starmap(operator.xor, zip(a, b))))
# or just
bytes(x ^ y for x, y in zip(a, b))
The ascii_chr
and safe_ord
are python2.7 compat right? Those can be dropped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, leftover from stuff I copied from pydevp2p
This should be rebased now that https://github.com/pipermerriam/py-evm/pull/95 is merged. |
Rebased and I think I've addressed all your comments, so should be ready for another pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to merge as is, but I did leave a few more comments.
evm/p2p/auth.py
Outdated
S = self.ephemeral_privkey.sign_msg_hash(secret_xor_nonce).to_bytes() | ||
|
||
# S || H(ephemeral-pubk) || pubk || nonce || 0x0 | ||
auth_message = S + keccak(self.ephemeral_pubkey.to_bytes()) + self.pubkey.to_bytes() + \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've found this format a bit more readable than using \
to split across multiple lines.
auth_message = (
S +
keccak(self.ephemeral_pubkey.to_bytes()) +
self.pubkey.to_bytes() +
nonce +
b'\x00'
)
Or:
auth_message = b''.join((
S,
keccak(self.ephemeral_pubkey.to_bytes()),
self.pubkey.to_bytes(),
nonce,
b'\x00',
))
privkey = ecies.generate_privkey() | ||
ciphertext = ecies.encrypt(msg, privkey.public_key) | ||
decrypted = ecies.decrypt(ciphertext, privkey) | ||
assert decrypted == msg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the type of test that I've historically used hypothesis for (things that have a round trip operation like encrypt/decrypt, encode/decode)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see!
evm/p2p/test_ecies.py
Outdated
assert hmac == tag_expected | ||
|
||
|
||
# FIXME: Document those values; this test was lifted from pydevp2p |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of remaining FIXME
left in. Fine to fix them later but it might be good to at least provide a link here for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Included the links in the comments
@@ -0,0 +1,4 @@ | |||
def sxor(s1, s2): | |||
if len(s1) != len(s2): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is used in cryptographic operations, should this use a constant time compare?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that the case already, since we're comparing their lengths?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should not review code late at night. 👍
evm/utils/keccak.py
Outdated
@@ -10,4 +10,9 @@ def keccak(value): | |||
return keccak_256(value).digest() | |||
|
|||
|
|||
# FIXME: this is a terrible name. | |||
def keccak_updateable(value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking that it might be better to just use keccak_256
directly from the sha3
library instead of this proxy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I'll do that
I tried to use coincurve for the ecdh exchange, but apparently it is not compatible with geth's implementation, so had to resort to pyca/cryptography.