diff --git a/eth/p2p/discoveryv5/encoding.nim b/eth/p2p/discoveryv5/encoding.nim index 5c14caf..2aa77d2 100644 --- a/eth/p2p/discoveryv5/encoding.nim +++ b/eth/p2p/discoveryv5/encoding.nim @@ -49,11 +49,12 @@ proc idNonceHash(nonce, ephkey: openarray[byte]): array[32, byte] = ctx.update(ephkey) ctx.finish().data -proc signIDNonce(c: Codec, idNonce, ephKey: openarray[byte]): SignatureNR = +proc signIDNonce*(c: Codec, idNonce, ephKey: openarray[byte]): SignatureNR = if signRawMessage(idNonceHash(idNonce, ephKey), c.privKey, result) != EthKeysStatus.Success: raise newException(EthKeysException, "Could not sign idNonce") -proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey, challenge: Whoareyou, result: var HandshakeSecrets) = +proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey, + idNonce: openarray[byte], result: var HandshakeSecrets) = var eph: SharedSecretFull if ecdhAgree(priv, pub, eph) != EthKeysStatus.Success: raise newException(EthKeysException, "ecdhAgree failed") @@ -68,9 +69,9 @@ proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey, challenge: Who static: assert(sizeof(result) == 16 * 3) var res = cast[ptr UncheckedArray[byte]](addr result) - hkdf(sha256, eph.data, challenge.idNonce, info, toOpenArray(res, 0, sizeof(result) - 1)) + hkdf(sha256, eph.data, idNonce, info, toOpenArray(res, 0, sizeof(result) - 1)) -proc encryptGCM(key, nonce, pt, authData: openarray[byte]): seq[byte] = +proc encryptGCM*(key, nonce, pt, authData: openarray[byte]): seq[byte] = var ectx: GCM[aes128] ectx.init(key, nonce, authData) result = newSeq[byte](pt.len + gcmTagSize) @@ -79,7 +80,8 @@ proc encryptGCM(key, nonce, pt, authData: openarray[byte]): seq[byte] = ectx.clear() proc makeAuthHeader(c: Codec, toNode: Node, nonce: array[gcmNonceSize, byte], - handhsakeSecrets: var HandshakeSecrets, challenge: Whoareyou): seq[byte] = + handshakeSecrets: var HandshakeSecrets, + challenge: Whoareyou): seq[byte] = var resp = AuthResponse(version: 5) let ln = c.localNode @@ -91,15 +93,16 @@ proc makeAuthHeader(c: Codec, toNode: Node, nonce: array[gcmNonceSize, byte], resp.signature = c.signIDNonce(challenge.idNonce, ephPubkey).getRaw - deriveKeys(ln.id, toNode.id, ephKey, toNode.node.pubKey, challenge, handhsakeSecrets) + deriveKeys(ln.id, toNode.id, ephKey, toNode.node.pubKey, challenge.idNonce, + handshakeSecrets) let respRlp = rlp.encode(resp) var zeroNonce: array[gcmNonceSize, byte] - let respEnc = encryptGCM(handhsakeSecrets.authRespKey, zeroNonce, respRLP, []) + let respEnc = encryptGCM(handshakeSecrets.authRespKey, zeroNonce, respRLP, []) - let header = AuthHeader(auth: nonce, idNonce: challenge.idNonce, scheme: authSchemeName, - ephemeralKey: ephPubkey, response: respEnc) + let header = AuthHeader(auth: nonce, idNonce: challenge.idNonce, + scheme: authSchemeName, ephemeralKey: ephPubkey, response: respEnc) rlp.encode(header) proc `xor`[N: static[int], T](a, b: array[N, T]): array[N, T] = @@ -181,7 +184,8 @@ proc decodePacketBody(typ: byte, body: openarray[byte], res: var Packet): bool = return true -proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader, challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node): bool = +proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader, + challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node): bool = if head.scheme != authSchemeName: warn "Unknown auth scheme" return false @@ -190,7 +194,8 @@ proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader, challenge: Whoar if recoverPublicKey(head.ephemeralKey, ephKey) != EthKeysStatus.Success: return false - deriveKeys(fromId, c.localNode.id, c.privKey, ephKey, challenge, secrets) + deriveKeys(fromId, c.localNode.id, c.privKey, ephKey, challenge.idNonce, + secrets) var zeroNonce: array[gcmNonceSize, byte] let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, []) diff --git a/tests/p2p/test_discv5_encoding.nim b/tests/p2p/test_discv5_encoding.nim index 5479946..b102c5c 100644 --- a/tests/p2p/test_discv5_encoding.nim +++ b/tests/p2p/test_discv5_encoding.nim @@ -1,6 +1,6 @@ -import unittest -import eth/p2p/discoveryv5/[types, encoding, enr] -import eth/rlp, stew/byteutils +import + unittest, stew/byteutils, stint, + eth/[rlp, keys] , eth/p2p/discoveryv5/[types, encoding, enr] # According to test vectors: # https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md @@ -121,3 +121,76 @@ suite "Discovery v5 Protocol Message Encodings": p.enrs = @[e1, e2] var reqId: RequestId = 1 check encodePacket(p, reqId).toHex == "04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235" + +suite "Discovery v5 Cryptographic Primitives": + test "ECDH": + const + # input + publicKey = "0x9961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231503061ac4aaee666073d7e5bc2c80c3f5c5b500c1cb5fd0a76abbb6b675ad157" + secretKey = "0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736" + # expected output + sharedSecret = "0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e" + + let + pub = initPublicKey(publicKey) + priv = initPrivateKey(secretKey) + var eph: SharedSecretFull + + check: + ecdhAgree(priv, pub, eph) == EthKeysStatus.Success + eph.data == hexToSeqByte(sharedSecret) + + test "Key Derivation": + const + # input + secretKey = "0x02a77e3aa0c144ae7c0a3af73692b7d6e5b7a2fdc0eda16e8d5e6cb0d08e88dd04" + nodeIdA = "0xa448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7" + nodeIdB = "0x885bba8dfeddd49855459df852ad5b63d13a3fae593f3f9fa7e317fd43651409" + idNonce = "0x0101010101010101010101010101010101010101010101010101010101010101" + # expected output + initiatorKey = "0x238d8b50e4363cf603a48c6cc3542967" + recipientKey = "0xbebc0183484f7e7ca2ac32e3d72c8891" + authRespKey = "0xe987ad9e414d5b4f9bfe4ff1e52f2fae" + + # Code doesn't allow to start from shared `secretKey`, but only from the + # public and private key. Would require pulling `ecdhAgree` out of + # `deriveKeys` + skip() + + test "Nonce Signing": + const + # input + idNonce = "0xa77e3aa0c144ae7c0a3af73692b7d6e5b7a2fdc0eda16e8d5e6cb0d08e88dd04" + ephemeralKey = "0x9961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231503061ac4aaee666073d7e5bc2c80c3f5c5b500c1cb5fd0a76abbb6b675ad157" + localSecretKey = "0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736" + # expected output + idNonceSig = "0xc5036e702a79902ad8aa147dabfe3958b523fd6fa36cc78e2889b912d682d8d35fdea142e141f690736d86f50b39746ba2d2fc510b46f82ee08f08fd55d133a4" + + let + c = Codec(privKey: initPrivateKey(localSecretKey)) + signature = signIDNonce(c, hexToByteArray[32](idNonce), + hexToByteArray[64](ephemeralKey)) + check signature.getRaw() == hexToByteArray[64](idNonceSig) + + test "Encryption/Decryption": + const + # input + encryptionKey = "0x9f2d77db7004bf8a1a85107ac686990b" + nonce = "0x27b5af763c446acd2749fe8e" + pt = "0x01c20101" + ad = "0x93a7400fa0d6a694ebc24d5cf570f65d04215b6ac00757875e3f3a5f42107903" + # expected output + messageCiphertext = "0xa5d12a2d94b8ccb3ba55558229867dc13bfa3648" + + let encrypted = encryptGCM(hexToByteArray[16](encryptionKey), + hexToByteArray[12](nonce), + hexToSeqByte(pt), + hexToByteArray[32](ad)) + check encrypted == hexToSeqByte(messageCiphertext) + + test "Authentication Header and Encrypted Message Generation": + # Can't work directly with the provided shared secret as keys are derived + # inside makeAuthHeader, and passed on one call up. + # The encryption of the auth-resp-pt uses one of these keys, as does the + # encryption of the message itself. So the whole test depends on this. + skip()