Skip to content

Commit

Permalink
chore: add timestamp and ephemeral for opt-in dos validator (#1713)
Browse files Browse the repository at this point in the history
  • Loading branch information
alrevuelta authored May 5, 2023
1 parent eaa162e commit 3e0a693
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 17 deletions.
29 changes: 24 additions & 5 deletions apps/wakunode2/wakunode2_validator_signed.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ else:
{.push raises: [].}

import
std/math,
chronicles,
chronos,
metrics,
stew/byteutils,
stew/endians2,
libp2p/protocols/pubsub/gossipsub,
libp2p/protocols/pubsub/rpc/messages,
libp2p/protocols/pubsub/errors,
nimcrypto/sha2,
secp256k1

const MessageWindowInSec = 5*60 # +- 5 minutes

import
../../waku/v2/waku_relay/protocol,
../../waku/v2/waku_core
Expand All @@ -29,9 +33,22 @@ proc msgHash*(pubSubTopic: string, msg: WakuMessage): array[32, byte] =
ctx.update(pubsubTopic.toBytes())
ctx.update(msg.payload)
ctx.update(msg.contentTopic.toBytes())
ctx.update(msg.timestamp.uint64.toBytes(Endianness.littleEndian))
ctx.update(if msg.ephemeral: @[1.byte] else: @[0.byte])

return ctx.finish()

proc withinTimeWindow*(msg: WakuMessage): bool =
# Returns true if the message timestamp is:
# abs(now - msg.timestamp) < MessageWindowInSec
let ts = msg.timestamp
let now = getNowInNanosecondTime()
let window = getNanosecondTime(MessageWindowInSec)

if abs(now - ts) < window:
return true
return false

proc addSignedTopicValidator*(w: WakuRelay, topic: PubsubTopic, publicTopicKey: SkPublicKey) =
debug "adding validator to signed topic", topic=topic, publicTopicKey=publicTopicKey

Expand All @@ -40,11 +57,13 @@ proc addSignedTopicValidator*(w: WakuRelay, topic: PubsubTopic, publicTopicKey:
var outcome = errors.ValidationResult.Reject

if msg.isOk():
let msgHash = SkMessage(topic.msgHash(msg.get))
let recoveredSignature = SkSignature.fromRaw(msg.get.meta)
if recoveredSignature.isOk():
if recoveredSignature.get.verify(msgHash, publicTopicKey):
outcome = errors.ValidationResult.Accept
if msg.get.timestamp != 0:
if msg.get.withinTimeWindow():
let msgHash = SkMessage(topic.msgHash(msg.get))
let recoveredSignature = SkSignature.fromRaw(msg.get.meta)
if recoveredSignature.isOk():
if recoveredSignature.get.verify(msgHash, publicTopicKey):
outcome = errors.ValidationResult.Accept

waku_msg_validator_signed_outcome.inc(labelValues = [$outcome])
return outcome
Expand Down
49 changes: 38 additions & 11 deletions tests/wakunode2/test_validators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ suite "WakuNode2 - Validators":
# Stop all nodes
await allFutures(nodes.mapIt(it.stop()))

asyncTest "Spam protected topic rejects non-signed and wrongly-signed messages":
asyncTest "Spam protected topic rejects non-signed/wrongly-signed/no-timestamp messages":
# Create 5 nodes
let nodes = toSeq(0..<5).mapIt(newTestWakuNode(generateSecp256k1Key(), ValidIpAddress.init("0.0.0.0"), Port(0)))

Expand Down Expand Up @@ -156,23 +156,49 @@ suite "WakuNode2 - Validators":
version: 2, timestamp: now(), ephemeral: true)
await nodes[i].publish(spamProtectedTopic, unsignedMessage)

# Each node sends 10 messages that dont contain timestamp (total = 50)
for i in 0..<5:
for j in 0..<10:
let unsignedMessage = WakuMessage(
payload: urandom(1*(10^3)), contentTopic: spamProtectedTopic,
version: 2, timestamp: 0, ephemeral: true)
await nodes[i].publish(spamProtectedTopic, unsignedMessage)

# Each node sends 10 messages way BEFORE than the current timestmap (total = 50)
for i in 0..<5:
for j in 0..<10:
let beforeTimestamp = now() - getNanosecondTime(6*60)
let unsignedMessage = WakuMessage(
payload: urandom(1*(10^3)), contentTopic: spamProtectedTopic,
version: 2, timestamp: beforeTimestamp, ephemeral: true)
await nodes[i].publish(spamProtectedTopic, unsignedMessage)

# Each node sends 10 messages way LATER than the current timestmap (total = 50)
for i in 0..<5:
for j in 0..<10:
let afterTimestamp = now() - getNanosecondTime(6*60)
let unsignedMessage = WakuMessage(
payload: urandom(1*(10^3)), contentTopic: spamProtectedTopic,
version: 2, timestamp: afterTimestamp, ephemeral: true)
await nodes[i].publish(spamProtectedTopic, unsignedMessage)

# Wait for gossip
await sleepAsync(2.seconds)

# Since we have a full mesh with 5 nodes and each one publishes 50+50 msgs
# there are 500 messages being sent.
# 100 are received ok in the handler (first hop)
# 400 are are wrong so rejected (rejected not relayed)
# Since we have a full mesh with 5 nodes and each one publishes 50+50+50+50+50 msgs
# there are 1250 messages being sent.
# 250 are received ok in the handler (first hop)
# 1000 are are wrong so rejected (rejected not relayed)
check:
msgReceived == 100
msgReceived == 250

var msgRejected = 0
for i in 0..<5:
for k, v in nodes[i].wakuRelay.peerStats.mpairs:
msgRejected += v.topicInfos[spamProtectedTopic].invalidMessageDeliveries.int

check:
msgRejected == 400
msgRejected == 1000

await allFutures(nodes.mapIt(it.stop()))

Expand Down Expand Up @@ -273,10 +299,12 @@ suite "WakuNode2 - Validators":
let contentTopic = "content-topic"
let pubsubTopic = "pubsub-topic"
let payload = "1A12E077D0E89F9CAC11FBBB6A676C86120B5AD3E248B1F180E98F15EE43D2DFCF62F00C92737B2FF6F59B3ABA02773314B991C41DC19ADB0AD8C17C8E26757B"
let timestamp = 1683208172339052800
let ephemeral = true

# expected values
let expectedMsgAppHash = "0914369D6D0C13783A8E86409FE42C68D8E8296456B9A9468C845006BCE5B9B2"
let expectedSignature = "B139487797A242291E0DD3F689777E559FB749D565D55FF202C18E24F21312A555043437B4F808BB0D21D542D703873DC712D76A3BAF1C5C8FF754210D894AD4"
let expectedMsgAppHash = "662F8C20A335F170BD60ABC1F02AD66F0C6A6EE285DA2A53C95259E7937C0AE9"
let expectedSignature = "127FA211B2514F0E974A055392946DC1A14052182A6ABEFB8A6CD7C51DA1BF2E40595D28EF1A9488797C297EED3AAC45430005FB3A7F037BDD9FC4BD99F59E63"

let secretKey = SkSecretKey.fromHex(privateKey).expect("valid key")

Expand All @@ -286,12 +314,11 @@ suite "WakuNode2 - Validators":

var msg = WakuMessage(
payload: payload.fromHex(), contentTopic: contentTopic,
version: 2, timestamp: now(), ephemeral: true)
version: 2, timestamp: timestamp, ephemeral: ephemeral)

let msgAppHash = pubsubTopic.msgHash(msg)
let signature = secretKey.sign(SkMessage(msgAppHash)).toRaw()

check:
msgAppHash.toHex() == expectedMsgAppHash
signature.toHex() == expectedSignature

2 changes: 1 addition & 1 deletion waku/v2/waku_core/message/message.nim
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type WakuMessage* = object
# Number to discriminate different types of payload encryption.
# Compatibility with Whisper/WakuV1.
version*: uint32
# Sender generated timestamp. Deprecated. Superseded by `meta` attribute.
# Sender generated timestamp.
timestamp*: Timestamp
# The ephemeral attribute indicates signifies the transient nature of the
# message (if the message should be stored).
Expand Down

0 comments on commit 3e0a693

Please sign in to comment.