Skip to content

Instantly share code, notes, and snippets.

@ellenhp
Last active October 3, 2023 00:54
Show Gist options
  • Save ellenhp/82d651923f69eddfac6cc9732c1c95bb to your computer and use it in GitHub Desktop.
Save ellenhp/82d651923f69eddfac6cc9732c1c95bb to your computer and use it in GitHub Desktop.

☃️ Blizzard ☃️: Delightfully simple delay-tolerant networking

Blizzard is a protocol for setting up authenticated and encrypted channels in peer-to-peer networks where peers are not necessarily online simultaneously. In other words, it is a delay-tolerant protocol. Unlike most DTN protocols, Blizzard runs on severely resource-constrained devices like microcontrollers. This crate is no_std-compatible but does require an allocator.

The core Blizzard crate is built on top of a no_std-compatible fork of snow, a noise protocol implementation.

Features

To the best of my knowledge, Blizzard offers:

  • Simplicity. Reading the entire Blizzard codebase is a project for an afternoon, no more.
  • Initiator anonymity. Outside observers of a channel can't determine any long-term identifiers associated with the initiator of any new channel.
  • Limited protection of recipient identity. Identifying yourself as the recipient of a message doesn't reveal any long-term identifiers about yourself.
  • Forward secrecy. Each channel (not each message) uses a new ephemeral key. Support for re-keying a channel/ratcheting is planned but not currently supported.
  • Authenticated and encrypted channels. Outside observers of a channel can't forge or read messages in a channel.
  • Support for initial contact through a pre-shared one-time address.
  • Difficult, but not impossible, location tracking of peers. More details about this in the section on routing.
  • Flexible operation. It runs over any physical link with an MTU of at least 240 bytes.

Assumptions:

  • The Noise protocol framework's IK handshake pattern is secure.
  • An attacker can not obtain your private keys.

Non-goals

Blizzard is not tolerant of malicious denial-of-service behavior like Sybil attacks, flooding, and black-hole attacks. Network operation requires that peers are honest and will not attempt to disrupt the network. However, the network maintains security and anonymity guarantees in the face of malicious actors. Blizzard doesn't solve these issues because they are hard to solve and are primarily an academic concern. If someone is in a position to DoS a delay-tolerant network, they're usually in a position to do it in multiple ways, and stopping their attack is not feasible. It is sometimes possible to mitigate protocol-level denial-of-service attacks with clever design, but your attacker can move their attack to the physical layer without losing efficacy.

Blizzard does not handle any transport layer concerns. It is up to the user to send and receive messages over a transport. Blizzard messages can be sent over any transport, including unreliable transports like UDP. It is currently up to the user to handle message ordering and retransmission if their application requires those guarantees. Blizzard may implement these features at a protocol level in the future.

Future goals

While mitigating an intentional DoS attack is a non-goal, it would be nice to implement airtime fairness controls for honest peers to ensure QoS. Channels should also be able to coordinate re-keying according to the noise spec. But for now, symmetric session keys are long-lived.

Protocol Overview (highly technical)

Routing in Blizzard uses a slightly modified binary spray-and-wait scheme called stochastic binary spray-and-wait. A standard spray-and-wait system operates by proactively "spraying" a limited number of copies of a message across the network, then "waiting" for the bearer of one of those copies to come across its recipient and deliver it. The binary variant of spray-and-wait requires that peers give away half of their "copies" of the messages they know about to each new peer they encounter until they only have one left. When a peer only has one copy left, they enter the "wait" phase for that message, waiting for a recipient to trigger delivery.

Tracking attacks against spray-and-wait

Binary spray-and-wait performs very well, even compared to other DTN routing mechanisms with inferior anonymity properties. It is, however, possible for a malicious party to track a node through the network by creating a unique message and distributing only one copy. The recipient of this unique message will be identifiable anywhere on the network because they are the only peer with a copy of this message. This type of attack is referred to as an "active tracking attack" in this document.

Mitigation of active tracking attacks

Stochastic spray and wait differs in that the recipient of a message randomly adjusts the number of copies of each message they receive. This adjustment may include the recipient destroying their only copy of the message and never delivering it. The relay never learns how the count was adjusted. This adjustment helps mitigate active tracking attacks by preventing the attacker from knowing exactly how many copies of a message are on the network.

In Blizzard, the "waiting" phase involves an active component to help ensure recipient privacy. A node offers delivery of a message that it has "run out of copies" of by emitting an announcement packet. An announcement packet in Blizzard contains a binary fuse filter containing a set of addresses the announcer has messages for. The recipient of an announcement packet will perform a lookup into the binary fuse filter for each address in its active Blizzard channels and may attempt to trigger delivery if it finds matches.

Passive tracking attacks and mitigations

Similar to the active tracking attack described earlier, a malicious party can perform a passive tracking attack by attempting to determine the set of messages a target has copies. If an attacker is able to identify that set, they may monitor the network, looking for announcement packets containing exactly those messages. Like the active tracking attack, it's impossible to mitigate a passive tracking attack completely. However, choosing a probabilistic data structure for announcement packets helps here because enumerating the entire set of known messages is very difficult and results in about 2^48 possible entries, the vast majority of which are false positives.

Blizzard also has one more trick: before inserting an address into the filter, an announcer will repeatedly hash it a random number of times up to 64, then truncate the hash. Enumerating the values in the binary fuse filter only gives you these truncated address hashes. Since the hashes are truncated, you don't have enough information to continue the repeated hashing forwards to correlate to other announcement packets. Moreover, assuming the hash is a strong trapdoor function, you can't go backward to recover the original address or its previous hashes.

This random repeated hashing ensures the set of values in any one announcement's fuse filter will have minimal overlap with those in any other announcement's fuse filter beyond the expected number of birthday paradox collisions.

While enumerating the complete set of messages in an announcement packet is hard, it's relatively simple for a recipient to perform a forward lookup of a known message address. It involves repeatedly hashing each address 64 times and performing a lookup for each hash value to check for the address's presence.

Wire format

There are currently four kinds of packets in the Blizzard protocol:

  • Message packets
  • Announcement packets
  • Message request packets
  • Ping packets

Common features

All Blizzard packets begin with a one-byte header. The header contains:

  • Blizzard protocol version number (bits 4-7). The current version number is 0b0001.
  • Packet type (bits 0-3).

Message packets

Message packets correspond to the packet type 0b0000. They start with a one-byte copy count field representing the number of copies of the message the sender intends to convey. Following that, there is an 8-byte address field. The address field is described in detail later. Everything after the address is a noise protocol message, up to 230 bytes.

Announcement packets

Announcement packets correspond to packet type 0b0001 and exist to communicate the set of messages a peer has in its possession for delivery, stored in a serialized binary fuse filter with 16-bit fingerprints. The fuse filter may be larger than the Blizzard MTU of 240 bytes, so announcement packets begin with a one-byte sequence identifier representing the number of announcement packets that follow. All data following the sequence number is part of the serialized fuse filter, up to 238 bytes.

In half-duplex or shared channels like packet radio, peers should wait to transmit until 30 seconds have passed after the last announce packet is received or until an announce packet is received with sequence number zero, indicating that the announcing peer is finished transmitting.

Message request packets

Message request packets, corresponding to message type 0b0010, are emitted to request delivery of a specific message. They contain a flat list of 8-byte message addresses, up to 232 bytes.

Ping packets

A ping packet, corresponding to message type 0b0011, contains no data after the header.

Routing logic

When to ping

Peers should issue ping packets every 10 minutes over shared interfaces unless there is activity on that interface. A ping's purpose is to notify other peers on a shared interface, like packet radio, that it may be worth announcing.

When to announce

Announcements should be transmitted when an interface is active and the peer has reason to believe that a new node may have arrived. A peer may assume that there's a new node around because enough time has passed since its last announcement or because another announcement occurred, missing messages that the peer has copies of.

When to transmit messages

Messages should be transmitted when authored by a user if an interface is active or after an announcement is received missing that message's address, assuming the peer has copies to share proactively.

When to request messages

A peer may request messages at any time.

Typical message exchange

A typical encounter between Blizzard peers follows:

  1. Peer A pings
  2. Peer A waits and returns to step 1 if there is no activity.
  3. Peer B announces
  4. Peer A shares messages it has copies of that are not in Peer B's announcement
  5. Peer A announces
  6. Peer B shares messages that it has copies of that are not in Peer A's announcement
  7. [optional] Either peer requests delivery of messages addressed to them not previously shared. These unshared messages are in the "wait" phase of spray-and-wait.

Unpredictable addressing

A key feature of Blizzard is anonymity, provided by the lack of stable identifiers. Addresses rotate after each message, defined by the following algorithm. This algorithm is also used to populate an announcement's binary fuse filter unpredictably, as described in the passive tracking attack mitigation section.

Algorithm

function blizzard_rolling_code
input salt; // pseudorandom, len >= 32 bytes
input hash_count; // value > 0;
var rolling_code = [];
for _ in 0..hash_count {
  rolling_code = blake2s("BLIZZARD_ROLLING_CODE_SALT" + salt + rolling_code)
}
output rolling_code[0..8];

Assumptions:

  • Blake2s is a secure hash function configured to output a digest of length 32.
  • salt is pseudorandom, either generated by a CPRNG or derived from inputs from a CPRNG.

Properties:

  • Given hash_count and salt, anyone can produce blizzard_rolling_code(salt, hash_count) deterministically.
  • Given blizzard_rolling_code(salt, hash_count) and hash_count, it is cryptographically hard to determine salt.
  • Given blizzard_rolling_code(salt, hash_count) and hash_count for hash_count > 0, but not salt, it is cryptographically hard to produce blizzard_rolling_code(salt, hash_count + 1).

The last property requires the most thought. The justification is that to produce blizzard_rolling_code(salt, hash_count + 1), the attacker must determine the input to the blake2s function: "BLIZZARD_ROLLING_CODE_SALT" + salt + rolling_code.

Determining this value is hard because they only have the first 8 bytes of rolling_code, and the remaining 24 bytes are uniformly pseudorandom.

Choice of inputs

This function should be called once for every message in an announcement. salt should be the message's address, and hash_count should be a uniformly pseudorandom value between 1 and 64. Note that these inputs technically violate the requirements given earlier because the message address is only 8 bytes, leading to a 64-bit preimage space. The consequences of this are examined in the next section.

To determine a rolling address for an established noise session, salt should be the noise session handshake hash concatenated with the sending peer's noise public key, and hash_count should be one plus the number of packets that have been sent through the noise channel by the sending peer.

Consequences of an attacker cracking an address obfuscation hash

I claim that a 64-bit preimage space for the address obfuscation hash doesn't break the protocol for a few reasons:

  1. The attacker will have difficulty determining which of the 2^48 values in any given binary fuse filter to target for cracking.
  2. Successfully cracking this hash gets you the address of the message it refers to, which doesn't break any security guarantees (but may have privacy implications).

The obfuscation aims to force an attacker wishing to track users to use an active attack instead of a passive one, and a 64-bit preimage space doesn't impact that goal.

Licensing

Blizzard is dual-licensed under Apache 2.0 and MIT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment