nim-eth-p2p/ethp2p/auth.nim

469 lines
16 KiB
Nim

#
# Ethereum P2P
# (c) Copyright 2018
# Status Research & Development GmbH
#
# See the file "LICENSE", included in this
# distribution, for details about the copyright.
#
## This module implements Ethereum authentication
import endians
import ecc, ecies, rlp
import nimcrypto/sysrand, nimcrypto/hash, nimcrypto/utils, nimcrypto/hmac
import nimcrypto/rijndael, nimcrypto/keccak, nimcrypto/sha2
import hexdump
const
SupportedRlpxVersion* = 4
# REVIEW: If these messages have fixed lenghts, they will be
# better described by an object type (see my similar comments
# in the ecies module.
PlainAuthMessageLength* = 194
PlainAuthAckMessageLength* = 97
AuthMessageLength* = 307
AuthAckMessageLength* = 210
type
PlainAuthMessage* = object {.packed.}
signature: RawSignature
keyhash: array[keccak256.sizeDigest, byte]
pubkey: PublicKey
nonce: array[keccak256.sizeDigest, byte]
flag: byte
PlainAuthAckMessage* = object {.packed.}
pubkey: PublicKey
nonce: array[keccak256.sizeDigest, byte]
flag: byte
HandshakeFlag* = enum
Initiator, ## `Handshake` owner is connection initiator
Responder, ## `Handshake` owner is connection responder
Eip8 ## Flag indicates that EIP-8 handshake is used
AuthStatus* = enum
Success, ## Operation was successful
RandomError, ## Could not obtain random data
EcdhError, ## ECDH shared secret could not be calculated
SignatureError, ## Signature could not be obtained
EciesError, ## ECIES encryption/decryption error
InvalidPubKey, ## Invalid public key
InvalidAuth, ## Invalid Authentication message
InvalidAck, ## Invalid Authentication ACK message
RlpError, ## Error while decoding RLP stream
IncompleteError ## Data incomplete error
Handshake* = object
version: uint8
flags: set[HandshakeFlag]
host*: KeyPair
ephemeral*: KeyPair
remoteHPubkey*: PublicKey
remoteEPubkey*: PublicKey
initiatorNonce*: Nonce
responderNonce*: Nonce
ConnectionSecret* = object
# REVIEW: it would be nice if Nimcrypto defines distinct or
# alias types such as `aes256.key` instead of having to spell
# out the full array type everywhere.
aesKey*: array[aes256.sizeKey, byte]
macKey*: array[KeyLength, byte]
egressMac*: array[keccak256.sizeDigest, byte]
ingressMac*: array[keccak256.sizeDigest, byte]
# PlainAuthMessage* = array[PlainAuthMessageLength, byte]
# PlainAuthAckMessage* = array[PlainAuthAckMessageLength, byte]
AuthMessage* = array[AuthMessageLength, byte]
AuthAckMessage* = array[AuthAckMessageLength, byte]
AuthException* = object of Exception
proc sxor[T](a: var openarray[T], b: openarray[T]) =
assert(len(a) == len(b))
for i in 0 ..< len(a):
a[i] = a[i] xor b[i]
proc empty[T](v: openarray[T]): bool =
var r: T
for item in v:
r = r + item
result = (r == T(0))
proc move[T](dst: var openarray[T], src: openarray[T],
dstx: int = 0, dsty: int = -1, srcx: int = 0, srcy: int = -1) =
let sx = if srcx < 0: (len(src) + srcx) else: srcx
let sy = if srcy < 0: (len(src) + srcy) else: srcy
let dx = if dstx < 0: (len(dst) + dstx) else: dstx
let dy = if dsty < 0: (len(dst) + dsty) else: dsty
assert(sy - sx == dy - dx)
moveMem(addr dst[dstx], unsafeAddr src[srcx], (dy - dx + 1) * sizeof(T))
proc newHandshake*(flags: set[HandshakeFlag] = {Initiator}): Handshake =
var p: ptr byte
result.flags = flags
result.ephemeral = newKeyPair()
if Initiator in flags:
if randomBytes(result.initiatorNonce) != len(result.initiatorNonce):
raise newException(AuthException, "Could not obtain random data!")
else:
if randomBytes(result.responderNonce) != len(result.responderNonce):
raise newException(AuthException, "Could not obtain random data!")
proc authMessagePreEIP8*(h: var Handshake,
pubkey: PublicKey,
output: var PlainAuthMessage,
flag: int = 0): AuthStatus =
## Create plain preEIP8 authentication message.
var secret: SharedSecret
var signature: Signature
var flagb = byte(flag)
if ecdhAgree(h.host.seckey, pubkey, secret) != EccStatus.Success:
return(EcdhError)
var xornonce = h.initiatorNonce
xornonce.sxor(secret)
if signMessage(h.ephemeral.seckey, xornonce, signature) != EccStatus.Success:
return(SignatureError)
h.remoteHPubkey = pubkey
output.signature = signature.getRaw()
output.keyhash = keccak256.digest(h.ephemeral.pubkey.getRaw().data).data
output.pubkey = cast[PublicKey](h.host.pubkey.getRaw().data)
output.nonce = h.initiatorNonce
output.flag = flagb
proc authAckMessagePreEIP8*(h: var Handshake,
output: var PlainAuthAckMessage,
flag: int = 0): AuthStatus =
output.pubkey = cast[PublicKey](h.ephemeral.pubkey.getRaw().data)
output.nonce = h.responderNonce
output.flag = byte(flag)
proc encryptAuthMessage*(input: ptr byte, inputlen: int,
output: ptr byte, outputlen: int,
pubkey: PublicKey, shmac: ptr byte = nil,
shlen: int = 0): AuthStatus =
result = Success
if eciesEncrypt(input, output, inputlen, outputlen,
pubkey, shmac, shlen) != EciesStatus.Success:
result = EciesError
proc encryptAuthMessage*(input: PlainAuthMessage,
output: var AuthMessage,
pubkey: PublicKey): AuthStatus =
result = Success
result = encryptAuthMessage(unsafeAddr input[0], PlainAuthMessageLength,
addr output[0], AuthMessageLength, pubkey)
proc decryptAuthMessage*(input: ptr byte, inputlen: int,
output: ptr byte, outputlen: int,
seckey: PrivateKey, shmac: ptr byte = nil,
shlen: int = 0): AuthStatus =
result = Success
if eciesDecrypt(input, output, inputlen, outputlen,
seckey, shmac, shlen) != EciesStatus.Success:
result = EciesError
proc decryptAuthMessage*(input: AuthMessage, output: var PlainAuthMessage,
seckey: PrivateKey): AuthStatus =
result = decryptAuthMessage(unsafeAddr input[0], AuthMessageLength,
addr output[0], PlainAuthMessageLength,
seckey)
proc encryptAuthAckMessage*(input: ptr byte, inputlen: int,
output: ptr byte, outputlen: int,
pubkey: PublicKey, shmac: ptr byte = nil,
shlen: int = 0): AuthStatus =
result = Success
if eciesEncrypt(input, output, inputlen, outputlen,
pubkey, shmac, shlen) != EciesStatus.Success:
result = EciesError
proc encryptAuthAckMessage*(input: PlainAuthAckMessage,
output: var AuthAckMessage,
pubkey: PublicKey): AuthStatus =
result = encryptAuthAckMessage(unsafeAddr input[0], PlainAuthAckMessageLength,
addr output[0], AuthAckMessageLength,
pubkey)
proc decryptAuthAckMessage*(input: ptr byte, inputlen: int,
output: ptr byte, outputlen: int,
seckey: PrivateKey, shmac: ptr byte = nil,
shlen: int = 0): AuthStatus =
result = Success
if eciesDecrypt(input, output, inputlen, outputlen,
seckey, shmac, shlen) != EciesStatus.Success:
result = EciesError
proc decryptAuthAckMessage*(input: AuthAckMessage,
output: var PlainAuthAckMessage,
seckey: PrivateKey): AuthStatus =
result = decryptAuthAckMessage(unsafeAddr input[0], AuthAckMessageLength,
addr output[0], PlainAuthAckMessageLength,
seckey)
proc decodePlainAuthMessage(h: var Handshake, m: PlainAuthMessage): AuthStatus =
var secret: SharedSecret
var nonce: array[32, byte]
var pubkey: PublicKey
copyMem(addr nonce[0], unsafeAddr m[161], KeyLength)
if recoverPublicKey(unsafeAddr m[97], sizeof(PublicKey),
pubkey) != EccStatus.Success:
return(InvalidPubKey)
if ecdhAgree(h.host.seckey, pubkey, secret) != EccStatus.Success:
return(EcdhError)
var xornonce = nonce
xornonce.sxor(secret)
if recoverSignatureKey(unsafeAddr m[0], SignatureLength, addr xornonce[0],
h.remoteEPubkey) != EccStatus.Success:
return(SignatureError)
h.initiatorNonce = nonce
h.remoteHPubkey = pubkey
result = Success
proc decodePlainAuthAckMessage*(h: var Handshake,
m: PlainAuthAckMessage): AuthStatus =
if recoverPublicKey(m, h.remoteEPubkey, 0, 63) != EccStatus.Success:
return(InvalidPubKey)
h.responderNonce[0..31] = m[64..95]
result = Success
proc getSecrets*(h: var Handshake,
msg: ptr byte, msglen: int,
ack: ptr byte, acklen: int,
secret: var ConnectionSecret): AuthStatus =
var
shsec: SharedSecret
ctx0: keccak256
ctx1: keccak256
digest: array[keccak256.sizeDigest, byte]
mac1: array[keccak256.sizeDigest, byte]
mac2: array[keccak256.sizeDigest, byte]
xornonce: Nonce
# ecdhe-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
if ecdhAgree(h.ephemeral.seckey, h.remoteEPubkey, shsec) != EccStatus.Success:
return(EcdhError)
# shared-secret = keccak(ecdhe-secret || keccak(nonce || initiator-nonce))
ctx0.init()
ctx1.init()
ctx1.update(addr h.responderNonce[0], uint(len(h.responderNonce)))
ctx1.update(addr h.initiatorNonce[0], uint(len(h.initiatorNonce)))
digest = ctx1.finish().data
ctx1.init() # clean keccak256 context
ctx0.update(addr shsec[0], uint(sizeof(SharedSecret)))
ctx0.update(addr digest[0], uint(keccak256.sizeDigest))
digest = ctx0.finish().data
# aes-secret = keccak(ecdhe-secret || shared-secret)
ctx0.init()
ctx0.update(addr shsec[0], uint(sizeof(SharedSecret)))
ctx0.update(addr digest[0], uint(keccak256.sizeDigest))
secret.aesKey = ctx0.finish().data
# mac-secret = keccak(ecdhe-secret || aes-secret)
ctx0.init()
ctx0.update(addr shsec[0], uint(sizeof(SharedSecret)))
ctx0.update(addr secret.aesKey[0], uint(keccak256.sizeDigest))
secret.macKey = ctx0.finish().data
zeroMem(addr shsec[0], sizeof(SharedSecret)) # clean ecdhe-secret
# egress-mac = keccak256(mac-secret ^ recipient-nonce || auth-sent-init)
xornonce = secret.macKey
xornonce.sxor(h.responderNonce)
ctx0.init()
ctx0.update(addr xornonce[0], uint(sizeof(Nonce)))
ctx0.update(msg, uint(msglen))
mac1 = ctx0.finish().data
# ingress-mac = keccak256(mac-secret ^ initiator-nonce || auth-recvd-ack)
xornonce = secret.macKey
xornonce.sxor(h.initiatorNonce)
ctx0.init()
ctx0.update(addr xornonce[0], uint(sizeof(Nonce)))
ctx0.update(ack, uint(acklen))
mac2 = ctx0.finish().data
ctx0.init() # clean keccak256 context
zeroMem(addr xornonce[0], sizeof(Nonce)) # clean xornonce
if Initiator in h.flags:
secret.egressMac = mac1
secret.ingressMac = mac2
else:
secret.ingressMac = mac1
secret.egressMac = mac2
zeroMem(addr mac1[0], keccak256.sizeDigest) # clean temporary mac1
zeroMem(addr mac2[0], keccak256.sizeDigest) # clean temporary mac2
result = Success
proc getSecrets*(h: var Handshake, msg: AuthMessage, ack: AuthAckMessage,
secret: var ConnectionSecret): AuthStatus =
result = getSecrets(h, unsafeAddr msg[0], AuthMessageLength,
unsafeAddr ack[0], AuthAckMessageLength,
secret)
proc decodeAuthEip8Message*(h: var Handshake, msg: ptr byte,
msglen: int): AuthStatus =
var
pubkey: PublicKey
nonce: Nonce
size: uint16
secret: SharedSecret
if msglen < 2:
return(InvalidAuth)
bigEndian16(addr size, msg)
if (2 + int(size)) > msglen:
return(InvalidAuth)
# Maximum `size` value is 65535 bytes
var outlen = eciesDecryptedLength(int(size))
var output = newSeq[byte](outlen)
var input = cast[ptr UncheckedArray[byte]](msg)
if decryptAuthMessage(addr input[2], int(size), addr output[0],
outlen, h.host.seckey,
addr input[0], 2) != Success:
return(EciesError)
try:
var reader = rlpFromBytes(output.toRange())
if not reader.isList() or reader.listLen() < 4:
return(InvalidAuth)
if reader.listElem(0).blobLen != SignatureLength:
return(InvalidAuth)
if reader.listElem(1).blobLen != PublicKeyLength:
return(InvalidAuth)
if reader.listElem(2).blobLen != KeyLength:
return(InvalidAuth)
if reader.listElem(3).blobLen != 1:
return(InvalidAuth)
var signatureBr = reader.listElem(0).toBytes()
var pubkeyBr = reader.listElem(1).toBytes()
var nonceBr = reader.listElem(2).toBytes()
var versionBr = reader.listElem(3).toBytes()
if recoverPublicKey(pubkeyBr.baseAddr, PublicKeyLength,
pubkey) != EccStatus.Success:
return(InvalidPubKey)
copyMem(addr nonce[0], nonceBr.baseAddr, KeyLength)
if ecdhAgree(h.host.seckey, pubkey, secret) != EccStatus.Success:
return(EcdhError)
var xornonce = nonce
xornonce.sxor(secret)
if recoverSignatureKey(signatureBr.baseAddr, SignatureLength,
addr xornonce[0],
h.remoteEPubkey) != EccStatus.Success:
return(SignatureError)
h.initiatorNonce = nonce
h.remoteHPubkey = pubkey
h.version = cast[ptr byte](versionBr.baseAddr)[]
result = Success
except:
return(RlpError)
proc decodeAuthAckEip8Message(h: var Handshake, msg: ptr byte,
msglen: int): AuthStatus =
var size: uint16
if msglen < 2:
return(IncompleteError)
bigEndian16(addr size, msg)
if (2 + int(size)) > msglen:
return(IncompleteError)
# Maximum `size` value is 65535 bytes
var outlen = eciesDecryptedLength(int(size))
var output = newSeq[byte](outlen)
var input = cast[ptr UncheckedArray[byte]](msg)
if decryptAuthMessage(addr input[2], int(size), addr output[0],
outlen, h.host.seckey,
addr input[0], 2) != Success:
return(EciesError)
try:
var reader = rlpFromBytes(output.toRange())
if not reader.isList() or reader.listLen() < 3:
return(InvalidAck)
if reader.listElem(0).blobLen != PublicKeyLength:
return(InvalidAck)
if reader.listElem(1).blobLen != KeyLength:
return(InvalidAck)
if reader.listElem(2).blobLen != 1:
return(InvalidAck)
let pubkeyBr = reader.listElem(0).toBytes()
let nonceBr = reader.listElem(1).toBytes()
let versionBr = reader.listElem(2).toBytes()
if recoverPublicKey(pubkeyBr.baseAddr, PublicKeyLength,
h.remoteEPubkey) != EccStatus.Success:
return(InvalidPubKey)
copyMem(addr h.responderNonce[0], nonceBr.baseAddr, KeyLength)
h.version = cast[ptr byte](versionBr.baseAddr)[]
result = Success
except:
return(RlpError)
proc decodeAuthMessage*(h: var Handshake, msg: ptr byte,
msglen: int): AuthStatus =
if msglen < AuthMessageLength:
return(IncompleteError)
elif msglen == AuthMessageLength:
# Decoding plain authentication message
var plain: PlainAuthMessage
result = decryptAuthMessage(msg, msglen, addr plain[0],
sizeof(PlainAuthMessage), h.host.seckey)
if result == Success:
result = decodePlainAuthMessage(h, plain)
else:
# Decoding EIP-8 authentication message
result = decodeAuthEip8Message(h, msg, msglen)
if result == Success:
h.flags.incl(EIP8)
proc decodeAckMessage*(h: var Handshake, msg: ptr byte,
msglen: int): AuthStatus =
if msglen < AuthAckMessageLength:
return(IncompleteError)
elif msglen == AuthAckMessageLength:
# Decoding plain authentication ACK message
var plain: PlainAuthAckMessage
result = decryptAuthAckMessage(msg, msglen, addr plain[0],
PlainAuthAckMessageLength,
h.host.seckey)
if result == Success:
result = decodePlainAuthAckMessage(h, plain)
else:
# Decoding EIP-8 ACK authentication message
result = decodeAuthAckEip8Message(h, msg, msglen)
proc decodeAuthMessage*(h: var Handshake, msg: openarray[byte]): AuthStatus =
result = decodeAuthMessage(h, unsafeAddr msg[0], len(msg))
proc decodeAckMessage*(h: var Handshake, msg: openarray[byte]): AuthStatus =
result = decodeAckMessage(h, unsafeAddr msg[0], len(msg))