Abstract
This document defines the Sphinx cryptographic packet format for decryption mix networks, and provides a parameterization based around generic cryptographic primitives types. This document does not introduce any new crypto, but is meant to serve as an implementation guide.
- 1. Introduction
- 2. Cryptographic Primitives
- 3. Sphinx Packet Parameters
- 4. The Sphinx Cryptographic Packet Structure
- 5. Sphinx Packet Creation
- 6. Sphinx Packet Processing
- 7. Single Use Reply Block (SURB) Creation
- 8. Single Use Reply Block Replies
- 9. Anonymity Considerations
- 10. Security Considerations
- Appendix A. References
- Appendix B. Citing This Document
The Sphinx cryptographic packet format is a compact and provably secure design introduced by George Danezis and Ian Goldberg [SPHINX09]. It supports a full set of security features: indistinguishable replies, hiding the path length and relay position, detection of tagging attacks and replay attacks, as well as providing unlinkability for each leg of the packet’s journey over the network.
Message
- A variable-length sequence of octets sent anonymously through the network.Packet
- A fixed-length sequence of octets transmitted anonymously through the network, containing the encrypted message and metadata for routing.Header
- The packet header consisting of several components, which convey the information necessary to verify packet integrity and correctly process the packet.Payload
- The fixed-length portion of a packet containing an encrypted message or part of a message, to be delivered anonymously.Group
- A finite set of elements and a binary operation that satisfy the properties of closure, associativity, invertability, and the presence of an identity element.Group element
- An individual element of the group.Group generator
- A group element capable of generating any other element of the group, via repeated applications of the generator and the group operation.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119].
The "C" style Presentation Language as described in [RFC5246] Section 4 is used to represent data structures, except for cryptographic attributes, which are specified as opaque byte vectors.
x | y
denotes the concatenation of x and y.x ^ y
denotes the bitwise XOR of x and y.byte
an 8-bit octet.x[a:b]
denotes the sub-vector of x where a/b denote the start/end byte indexes (inclusive-exclusive); a/b may be omitted to signify the start/end of the vector x respectively.x[y]
denotes the y'th element of list x.x.len
denotes the length of list x.ZEROBYTES(N)
denotes N bytes of 0x00.RNG(N)
denotes N bytes of cryptographic random data.LEN(N)
denotes the length in bytes of N.CONSTANT_TIME_CMP(x, y)
denotes a constant time comparison between the byte vectors x and y, returning true iff x and y are equal.
This specification uses the following cryptographic primitives as the foundational building blocks for Sphinx:
H(M)
- A cryptographic hash function which takes an octet array M to produce a digest consisting of aHASH_LENGTH
byte octet array.H(M)
MUST be pre-image and collision resistant.MAC(K, M)
- A cryptographic message authentication code function which takes aM_KEY_LENGTH
byte octet array keyK
and arbitrary length octet array messageM
to produce an authentication tag consisting of aMAC_LENGTH
byte octet array.KDF(SALT, IKM)
- A key derivation function which takes an arbitrary length octet array saltSALT
and an arbitrary length octet array initial keyIKM
, to produce an octet array of arbitrary length.S(K, IV)
- A pseudo-random generator (stream cipher) which takes aS_KEY_LENGTH
byte octet array keyK
and aS_IV_LENGTH
byte octet array initialization vectorIV
to produce an octet array key stream of arbitrary length.SPRP_Encrypt(K, M)/SPRP_Decrypt(K, M)
- A strong pseudo-random permutation (SPRP) which takes aSPRP_KEY_LENGTH
byte octet array keyK
and arbitrary length messageM
, and produces the encrypted ciphertext or decrypted plaintext respectively.When used with the default payload authentication mechanism, the SPRP MUST be "fragile" in that any amount of modifications to
M
results in a large number of unpredictable changes across the whole message upon aSPRP_Encrypt()
orSPRP_Decrypt()
operation.EXP(X, Y)
- An exponentiation function which takes theGROUP_ELEMENT_LENGTH
byte octet array group elementsX
andY
, and returnsX ^^ Y
as aGROUP_ELEMENT_LENGTH
byte octet array.Let
G
denote the generator of the group, andEXP_KEYGEN()
return aGROUP_ELEMENT_LENGTH
byte octet array group element usable as private key.The group defined by
G
andEXP(X, Y)
MUST satisfy the Decision Diffie-Hellman problem.EXP_KEYGEN()
- Returns a new "suitable" private key forEXP()
.
Sphinx Packet creation and processing uses a common Key Derivation Function (KDF) to derive the required MAC and symmetric cryptographic keys from a per-hop shared secret.
The output of the KDF is partitioned according to the following structure:
struct { opaque header_mac[M_KEY_LENGTH]; opaque header_encryption[S_KEY_LENGTH]; opaque header_encryption_iv[S_IV_LENGTH]; opaque payload_encryption[SPRP_KEY_LENGTH] opaque blinding_factor[GROUP_ELEMENT_LENGTH]; } SphinxPacketKeys; Sphinx_KDF( info, shared_secret ) -> packet_keys
Inputs:
info
The optional context and application specific information.shared_secret
The per-hop shared secret derived from the Diffie-Hellman key exchange.
Outputs:
packet_keys
The SphinxPacketKeys required to handle packet creation or processing.The output packet_keys is calculated as follows:
kdf_out = KDF( info, shared_secret ) packet_keys = kdf_out[:LEN( SphinxPacketKeys )]
The Sphinx Packet Format is parameterized by the implementation based on the application and security requirements.
AD_LENGTH
- The constant amount of per-packet unencrypted additional data in bytes.PAYLOAD_TAG_LENGTH
- The length of the message payload authentication tag in bytes. This SHOULD be set to at least 16 bytes (128 bits).PER_HOP_RI_LENGTH
- The length of the per-hop Routing Information (:ref:`Section 4.1.1 <4.1.1>`) in bytes.NODE_ID_LENGTH
- The node identifier length in bytes.RECIPIENT_ID_LENGTH
- The recipient identifier length in bytes.SURB_ID_LENGTH
- The Single Use Reply Block (:ref:`Section 7 <7.0>`) identifier length in bytes.MAX_HOPS
- The maximum number of hops a packet can traverse.PAYLOAD_LENGTH
- The per-packet message payload length in bytes, including aPAYLOAD_TAG_LENGTH
byte authentication tag.KDF_INFO
- A constant opaque byte vector used as the info parameter to the KDF for the purpose of domain separation.
The Sphinx Packet Geometry is derived from the Sphinx Parameter Constants (:ref:`Section 3.1 <3.1>`). These are all derived parameters, and are primarily of interest to implementors.
ROUTING_INFO_LENGTH
- The total length of the "routing information" Sphinx Packet Header component in bytes:ROUTING_INFO_LENGTH = PER_HOP_RI_LENGTH * MAX_HOPS
HEADER_LENGTH
- The length of the Sphinx Packet Header in bytes:HEADER_LENGTH = AD_LENGTH + GROUP_ELEMENT_LENGTH + ROUTING_INFO_LENGTH + MAC_LENGTH
PACKET_LENGTH
- The length of the Sphinx Packet in bytes:PACKET_LENGTH = HEADER_LENGTH + PAYLOAD_LENGTH
Each Sphinx Packet consists of two parts: the Sphinx Packet Header and the Sphinx Packet Payload:
struct { opaque header[HEADER_LENGTH]; opaque payload[PAYLOAD_LENGTH]; } SphinxPacket;
header
- The packet header consists of several components, which convey the information necessary to verify packet integrity and correctly process the packet.payload
- The application message data.
The Sphinx Packet Header refers to the block of data immediately preceding the Sphinx Packet Payload in a Sphinx Packet.
The structure of the Sphinx Packet Header is defined as follows:
struct { opaque additional_data[AD_LENGTH]; /* Unencrypted. */ opaque group_element[GROUP_ELEMENT_LENGTH]; opaque routing_information[ROUTING_INFO_LENGTH]; opaque MAC[MAC_LENGTH]; } SphinxHeader;
additional_data
- Unencrypted per-packet Additional Data (AD) that is visible to every hop. The AD is authenticated on a per-hop basis.As the additional_data is sent in the clear and traverses the network unaltered, implementations MUST take care to ensure that the field cannot be used to track individual packets.
group_element
- An element of the cyclic group, used to derive the per-hop key material required to authenticate and process the rest of the SphinxHeader and decrypt a single layer of the Sphinx Packet Payload encryption.routing_information
- A vector of per-hop routing information, encrypted and authenticated in a nested manner. Each element of the vector consists of a series of routing commands, specifying all of the information required to process the packet.The precise encoding format is specified in :ref:`Section 4.1.1 <4.1.1>`.
MAC
- A message authentication code tag covering the additional_data, group_element, and routing_information.
The routing_information component of the Sphinx Packet Header contains a vector of per-hop routing information. When processing a packet, the per hop processing is set up such that the first element in the vector contains the routing commands for the current hop.
The structure of the routing information is as follows:
struct { RoutingCommand routing_commands<1..2^8-1>; /* PER_HOP_RI_LENGTH bytes */ opaque encrypted_routing_commands[ROUTING_INFO_LENGTH - PER_HOP_RI_LENGTH]; } RoutingInformation;
The structure of a single routing command is as follows:
struct { RoutingCommandType command; select (RoutingCommandType) { case null: NullCommand; case next_node_hop: NextNodeHopCommand; case recipient: RecipientCommand; case surb_reply: SURBReplyCommand; }; } RoutingCommand;
The following routing commands are currently defined:
enum { null(0), next_node_hop(1), recipient(2), surb_reply(3), /* Routing commands between 0 and 0x7f are reserved. */ (255) } RoutingCommandType;
The null routing command structure is as follows:
struct { opaque padding<0..PER_HOP_RI_LENGTH-1>; } NullCommand;
The next_node_hop command structure is as follows:
struct { opaque next_hop[NODE_ID_LENGTH]; opaque MAC[MAC_LENGTH]; } NextNodeHopCommand;
The recipient command structure is as follows:
struct { opaque recipient[RECIPEINT_ID_LENGTH]; } RecipientCommand;
The surb_reply command structure is as follows:
struct { opaque id[SURB_ID_LENGTH]; } SURBReplyCommand;
While the NullCommand
's padding field is specified as opaque,
implementations SHOULD zero fill the padding. The choice of '0x00'
as the terminal NullCommand is deliberate to ease implementation,
as ZEROBYTES(N)
produces a valid NullCommand RoutingCommand,
resulting in "appending zero filled padding" producing valid output.
Implementations MUST pad the routing_commands vector so that it is
exactly PER_HOP_RI_LENGTH
bytes, by appending a terminal NullCommand
if necessary.
Every non-terminal hop's routing_commands
MUST include a
NextNodeHopCommand
.
The Sphinx Packet Payload refers to the block of data immediately following the Sphinx Packet Header in a Sphinx Packet.
For most purposes the structure of the Sphinx Packet Payload can be treated as a single contiguous byte vector of opaque data.
Upon packet creation, the payload is repeatedly encrypted (unless it
is a SURB Reply, see :ref:`Section 7 <7.0>`) via keys derived from the
Diffie-Hellman key exchange between the packet's group_element
and the public key of each node in the path.
Authentication of packet integrity is done by prepending a tag set to a known value to the plaintext prior to the first encrypt operation. By virtue of the fragile nature of the SPRP function, any alteration to the encrypted payload as it traverses the network will result in an irrecoverably corrupted plaintext when the payload is decrypted by the recipient.
For the sake of brevity, the pseudocode for all of the operations will take a vector of the following PathHop structure as a parameter named path[] to specify the path a packet will traverse, along with the per-hop routing commands and per-hop public keys.
struct { /* There is no need for a node_id here, as routing_commands[0].next_hop specifies that information for all non-terminal hops. */ opaque public_key[GROUP_ELEMENT_LENGTH]; RoutingCommand routing_commands<1...2^8-1>; } PathHop;
It is assumed that each routing_commands vector except for the
terminal entry contains at least a RoutingCommand consisting of
a partially assembled NextNodeHopCommand with the next_hop
element
filled in with the identifier of the next hop.
Both the creation of a Sphinx Packet and the creation of a SURB requires the generation of a Sphinx Packet Header, so it is specified as a distinct operation.
Sphinx_Create_Header( additional_data, path[] ) -> sphinx_header, payload_keys
Inputs:
additional_data
The Additional Data that is visible to every node along the path in the header.path
The vector of PathHop structures in hop order, specifying the node id, public key, and routing commands for each hop.
Outputs:
* sphinx_header
The resulting Sphinx Packet Header.
payload_keys
The vector of SPRP keys used to encrypt the Sphinx Packet Payload, in hop order.
The Sphinx_Create_Header
operation consists of the following steps:
Derive the key material for each hop.
num_hops = route.len route_keys = [ ] route_group_elements = [ ] priv_key = EXP_KEYGEN() /* Calculate the key material for the 0th hop. */ group_element = EXP( G, priv_key ) route_group_elements += group_element shared_secret = EXP( path[0].public_key, priv_key ) route_keys += Sphinx_KDF( KDF_INFO, shared_secret ) blinding_factor = keys[0].blinding_factor /* Calculate the key material for rest of the hops. */ for i = 1; i < num_hops; ++i: shared_secret = EXP( path[i].public_key, priv_key ) for j = 0; j < i; ++j: shared_secret = EXP( shared_secret, keys[j].blinding_factor ) route_keys += Sphinx_KDF( KDF_INFO, shared_secret ) group_element = EXP( group_element, keys[i-1].blinding_factor ) route_group_elements += group_element
At the conclusion of the derivation process:
- route_keys - A vector of per-hop SphinxKeys.
- route_group_elements - A vector of per-hop group elements.
Derive the routing_information keystream and encrypted padding for each hop.
ri_keystream = [ ] ri_padding = [ ] for i = 0; i < num_hops; ++i: keystream = ZEROBYTES( ROUTING_INFO_LENGTH + PER_HOP_RI_LENGTH ) ^ S( route_keys[i].header_encryption, route_keys[i].header_encryption_iv ) ks_len = LEN( keystream ) - (i + 1) * PER_HOP_RI_LENGTH padding = keystream[ks_len:] if i > 0: prev_pad_len = LEN( ri_padding[i-1] ) padding = padding[:prev_pad_len] ^ ri_padding[i-1] | padding[prev_pad_len] ri_keystream += keystream[:ks_len] ri_padding += padding At the conclusion of the derivation process: ri_keystream - A vector of per-hop routing_information encryption keystreams. ri_padding - The per-hop encrypted routing_information padding.
Create the routing_information block.
/* Start with the terminal hop, and work backwards. */ i = num_hops - 1 /* Encode the terminal hop's routing commands. As the terminal hop can never have a NextNodeHopCommand, there are no per-hop alterations to be made. */ ri_fragment = path[i].routing_commands | ZEROBYTES( PER_HOP_RI_LENGTH - LEN( path[i].routing_commands ) ) /* Encrypt and MAC. */ ri_fragment ^= ri_keystream[i] mac = MAC( route_keys[i].header_mac, additional_data | route_group_elements[i] | ri_fragment | ri_padding[i-1] ) routing_info = ri_fragment if num_hops < MAX_HOPS: pad_len = (MAX_HOPS - num_hops) * PER_HOP_RI_LENGTH routing_info = routing_info | RNG( pad_len ) /* Calculate the routing info for the rest of the hops. */ for i = num_hops - 2; i >= 0; --i: cmds_to_encode = [ ] /* Find and finalize the NextNodeHopCommand. */ for j = 0; j < LEN( path[i].routing_commands; j++: cmd = path[i].routing_commands[j] if cmd.command == next_node_hop: /* Finalize the NextNodeHopCommand. */ cmd.MAC = mac cmds_to_encode = cmds_to_encode + cmd /* Append */ /* Append a terminal NullCommand. */ ri_fragment = cmds_to_encode | ZEROBYTES( PER_HOP_RI_LENGTH - LEN( cmds_to_encode ) ) /* Encrypt and MAC */ routing_info = ri_fragment | routing_info /* Prepend. */ routing_info ^= ri_keystream[i] if i > 0: mac = MAC( route_keys[i].header_mac, additional_data | route_group_elements[i] | routing_info | ri_padding[i-1] ) else: mac = MAC( route_keys[i].header_mac, additional_data | route_group_elements[i] | routing_info ) At the conclusion of the derivation process: routing_info - The completed routing_info block. mac - The MAC for the 0th hop.
Assemble the completed Sphinx Packet Header and Sphinx Packet Payload SPRP key vector.
/* Assemble the completed Sphinx Packet Header. */ SphinxHeader sphinx_header sphinx_header.additional_data = additional_data sphinx_header.group_element = route_group_elements[0] /* From step 1. */ sphinx_header.routing_info = routing_info /* From step 3. */ sphinx_header.mac = mac /* From step 3. */ /* Preserve the Sphinx Payload SPRP keys, to return to the caller. */ payload_keys = [ ] for i = 0; i < nr_hops; ++i: payload_keys += route_keys[i].payload_encryption At the conclusion of the assembly process: sphinx_header - The completed sphinx_header, to be returned. payload_keys - The vector of SPRP keys, to be returned.
Sphinx_Create_Packet( additional_data, path[], payload ) -> sphinx_packet
Inputs:
additional_data
The Additional Data that is visible to every node along the path in the header.path
The vector of PathHop structures in hop order, specifying the node id, public key, and routing commands for each hop.payload
The packet payload message plaintext.
Outputs:
sphinx_packet
The resulting Sphinx Packet.
The Sphinx_Create_Packet
operation consists of the following steps:
Create the Sphinx Packet Header and SPRP key vector.
sphinx_header, payload_keys = Sphinx_Create_Header( additional_data, path )
Prepend the authentication tag, and append padding to the payload.
payload = ZERO_BYTES( PAYLOAD_TAG_LENGTH ) | payload payload = payload | ZERO_BYTES( PAYLOAD_LENGTH - LEN( payload ) )
Encrypt the payload.
for i = nr_hops - 1; i >= 0; --i: payload = SPRP_Encrypt( payload_keys[i], payload )
Assemble the completed Sphinx Packet.
SphinxPacket sphinx_packet sphinx_packet.header = sphinx_header sphinx_packet.payload = payload
Mix nodes process incoming packets first by performing the
Sphinx_Unwrap
operation to authenticate and decrypt the packet, and
if applicable prepare the packet to be forwarded to the next node.
If Sphinx_Unwrap
returns an error for any given packet, the packet
MUST be discarded with no additional processing.
After a packet has been unwrapped successfully, a replay detection tag is checked to ensure that the packet has not been seen before. If the packet is a replay, the packet MUST be discarded with no additional processing.
The routing commands for the current hop are interpreted and executed, and finally the packet is forwarded to the next mix node over the network or presented to the application if the current node is the final recipient.
The Sphinx_Unwrap
operation is the majority of the per-hop packet
processing, handling authentication, decryption, and modifying the
packet prior to forwarding it to the next node.
Sphinx_Unwrap( routing_private_key, sphinx_packet ) -> sphinx_packet, routing_commands, replay_tag
Inputs:
private_routing_key
A group element GROUP_ELEMENT_LENGTH bytes in length, that serves as the unwrapping Mix's private key.sphinx_packet
A Sphinx packet to unwrap.
Outputs:
error
Indicating a unsuccessful unwrap operation if applicable.sphinx_packet
The resulting Sphinx packet.routing_commands
A vector of RoutingCommand, specifying the post unwrap actions to be taken on the packet.replay_tag
A tag used to detect whether this packet was processed before.
The Sphinx_Unwrap
operation consists of the following steps:
(Optional) Examine the Sphinx Packet Header's Additional Data.
If the header's
additional_data
element contains information required to complete the unwrap operation, such as specifying the packet format version or the cryptographic primitives used examine it now.Implementations MUST NOT treat the information in the
additional_data
element as trusted until after the completion of Step 3 ("Validate the Sphinx Packet Header").Calculate the hop's shared secret, and replay_tag.
hdr = sphinx_packet.header shared_secret = EXP( hdr.group_element, private_routing_key ) replay_tag = H( shared_secret )
Derive the various keys required for packet processing.
keys = Sphinx_KDF( KDF_INFO, shared_secret )
Validate the Sphinx Packet Header.
derived_mac = MAC( keys.header_mac, hdr.additional_data | hdr.group_element | hdr.routing_information ) if !CONSTANT_TIME_CMP( derived_mac, hdr.MAC): /* MUST abort processing if the header is invalid. */ return ErrorInvalidHeader
Extract the per-hop routing commands for the current hop.
/* Append padding to preserve length-invariance, as the routing commands for the current hop will be removed. */ padding = ZEROBYTES( PER_HOP_RI_LENGTH ) B = hdr.routing_information | padding /* Decrypt the entire routing_information block. */ B = B ^ S( keys.header_encryption, keys.header_encryption_iv )
Parse the per-hop routing commands.
cmd_buf = B[:PER_HOP_RI_LENGTH] new_routing_information = B[PER_HOP_RI_LENGTH:] next_mix_command_idx = -1 routing_commands = [ ] for idx = 0; idx < PER_HOP_RI_LENGTH { /* WARNING: Bounds checking omitted for brevity. */ cmd_type = b[idx] cmd = NULL switch cmd_type { case null: goto done /* No further commands. */ case next_node_hop: cmd = RoutingCommand( B[idx:idx+1+LEN( NextNodeHopCommand )] ) next_mix_command_idx = i /* Save for step 7. */ idx += 1 + LEN( NextNodeHopCommand ) break case recipient: cmd = RoutingCommand( B[idx:idx+1+LEN( FinalDestinationCommand )] ) idx += 1 + LEN( RecipientCommand ) break case surb_reply: cmd = RoutingCommand( B[idx:idx+1+LEN( SURBReplyCommand )] ) idx += 1 + LEN( SURBReplyCommand ) break default: /* MUST abort processing on unrecognized commands. */ return ErrorInvalidCommand } routing_commands += cmd /* Append cmd to the tail of the list. */ } done:
- At the conclusion of the parsing step:
routing_commands
- A vector of SphinxRoutingCommand, to beapplied at this hop.
new_routing_information
- The routing_information block to be sentto the next hop if any.
Decrypt the Sphinx Packet Payload.
payload = sphinx_packet.payload payload = SPRP_Decrypt( key.payload_encryption, payload ) sphinx_packet.payload = payload
Transform the packet for forwarding to the next mix, iff the routing commands vector included a NextNodeHopCommand.
if next_mix_command_idx != -1: cmd = routing_commands[next_mix_command_idx] hdr.group_element = EXP( hdr.group_element, keys.blinding_factor ) hdr.routing_information = new_routing_information hdr.mac = cmd.MAC sphinx_packet.hdr = hdr
Upon the completion of the Sphinx_Unwrap
operation, implementations
MUST take several additional steps. As the exact behavior is mostly
implementation specific, pseudocode will not be provided for most of
the post processing steps.
Apply replay detection to the packet.
The
replay_tag
value returned by Sphinx_Unwrap MUST be unique across all packets processed with a givenprivate_routing_key
.The exact specifics of how to detect replays is left up to the implementation, however any replays that are detected MUST be discarded immediately.
Act on the routing commands, if any.
The exact specifics of how implementations chose to apply routing commands is deliberately left unspecified, however in general:
Iff there is a
NextNodeHopCommand
, the packet should be forwarded to the next node based on thenext_hop
field upon completion of the post processing.The lack of a NextNodeHopCommand indicates that the packet is destined for the current node.
Iff there is a
SURBReplyCommand
, the packet should be treated as a SURBReply destined for the current node, and decrypted accordingly (See :ref:`Section 7.2 <7.2>`).If the implementation supports multiple recipients on a single node, the
RecipientCommand
command should be used to determine the correct recipient for the packet, and the payload delivered as appropriate.Note
It is possible for both a RecipientCommand and a NextNodeHopCommand to be present simultaneously in the routing commands for a given hop. The behavior when this situation occurs is implementation defined.
Authenticate the packet if required.
Iff the packet is destined for the current node, the integrity of the payload MUST be authenticated.
The authentication is done as follows:
derived_tag = sphinx_packet.payload[:PAYLOAD_TAG_LENGTH] expected_tag = ZEROBYTES( PAYLOAD_TAG_LENGTH ) if !CONSTANT_TIME_CMP( derived_tag, expected_tag ): /* Discard the packet with no further processing. */ return ErrorInvalidPayload /* Remove the authentication tag before presenting the payload to the application. */ sphinx_packet.payload = sphinx_packet.payload[PAYLOAD_TAG_LENGTH:]
A Single Use Reply Block (SURB) is a delivery token with a short lifetime, that can be used by the recipient to reply to the initial sender.
SURBs allow for anonymous replies, when the recipient does not know the sender of the message. Usage of SURBs guarantees anonymity properties but also makes the reply messages indistinguishable from forward messages both to external adversaries as well as the mix nodes.
When a SURB is created, a matching reply block Decryption Token is created, which is used to decrypt the reply message that is produced and delivered via the SURB.
The Sphinx SURB wire encoding is implementation defined, but for the purposes of illustrating creation and use, the following will be used:
struct { SphinxHeader sphinx_header; opaque first_hop[NODE_ID_LENGTH]; opaque payload_key[SPRP_KEY_LENGTH]; } SphinxSURB;
Structurally a SURB consists of three parts, a pre-generated Sphinx Packet Header, a node identifier for the first hop to use when using the SURB to reply, and cryptographic keying material by which to encrypt the reply's payload. All elements must be securely transmitted to the recipient, perhaps as part of a forward Sphinx Packet's Payload, but the exact specifics on how to accomplish this is left up to the implementation.
When creating a SURB, the terminal routing_commands vector SHOULD include a SURBReplyCommand, containing an identifier to ensure that the payload can be decrypted with the correct set of keys (Decryption Token). The routing command is left optional, as it is conceivable that implementations may chose to use trial decryption, and or limit the number of outstanding SURBs to solve this problem.
Sphinx_Create_SURB( additional_data, first_hop, path[] ) -> sphinx_surb, decryption_token
Inputs:
additional_data
The Additional Data that is visible to every node along the path in the header.first_hop
The node id of the first hop the recipient must use when replying via the SURB.path
The vector of PathHop structures in hop order, specifying the node id, public key, and routing commands for each hop.
Outputs:
sphinx_surb
The resulting Sphinx SURB.decryption_token
The Decryption Token associated with the SURB.
The Sphinx_Create_SURB operation consists of the following steps:
Create the Sphinx Packet Header and SPRP key vector.
sphinx_header, payload_keys = Sphinx_Create_Header( additional_data, path )
Create a key for the final layer of encryption.
final_key = RNG( SPRP_KEY_LENGTH )
Build the SURB and Decryption Token.
SphinxSURB sphinx_surb; sphinx_surb.sphinx_header = sphinx_header sphinx_surb.first_hop = first_hop sphinx_surb.payload_key = final_key decryption_token = final_key + payload_keys /* Prepend */
A Sphinx Reply packet that was generated using a SURB is externally indistinguishable from a forward Sphinx Packet as it traverses the network. However, the recipient of the reply has an additional decryption step, the packet starts off unencrypted, and accumulates layers of Sphinx Packet Payload decryption as it traverses the network.
Determining which decryption token to use when decrypting the SURB reply can be done via the SURBReplyCommand's id field, if one is included at the time of the SURB's creation.
Sphinx_Decrypt_SURB_Reply( decryption_token, payload ) -> message
Inputs:
decryption_token
The vector of keys allowing a client to decrypt the reply ciphertext payload. This decryption_token is generated when the SURB is created.payload
The Sphinx Packet ciphertext payload.
Outputs:
error
Indicating a unsuccessful unwrap operation if applicable.message
The plaintext message.
The Sphinx_Decrypt_SURB_Reply operation consists of the following steps:
Encrypt the message to reverse the decrypt operations the payload acquired as it traversed the network.
for i = LEN( decryption_token ) - 1; i > 0; --i: payload = SPRP_Encrypt( decryption_token[i], payload )
Decrypt and authenticate the message ciphertext.
message = SPRP_Decrypt( decryption_token[0], payload ) derived_tag = message[:PAYLOAD_TAG_LENGTH] expected_tag = ZEROBYTES( PAYLOAD_TAG_LENGTH ) if !CONSTANT_TIME_CMP( derived_tag, expected_tag ): return ErrorInvalidPayload message = message[PAYLOAD_TAG_LENGTH:]
The process for using a SURB to reply anonymously is slightly different from the standard packet creation process, as the Sphinx Packet Header is already generated (as part of the SURB), and there is an additional layer of Sphinx Packet Payload encryption that must be performed.
Sphinx_Create_SURB_Reply( sphinx_surb, payload ) -> sphinx_packet
Inputs:
sphinx_surb
The SphinxSURB structure, decoded from the implementation defined wire encoding.payload
The packet payload message plaintext.
The Sphinx_Create_SURB_Reply operation consists of the following steps:
Prepend the authentication tag, and append padding to the payload.
payload = ZERO_BYTES( PAYLOAD_TAG_LENGTH ) | payload payload = payload | ZERO_BYTES( PAYLOAD_LENGTH - LEN( payload ) )
Encrypt the payload.
payload = SPRP_Encrypt( sphinx_surb.payload_key, payload )
Assemble the completed Sphinx Packet.
SphinxPacket sphinx_packet sphinx_packet.header = sphinx_surb.sphinx_header sphinx_packet.payload = payload
The completed
sphinx_packet
MUST be sent to the node specified viasphinx_surb.node_id
, as the entire replysphinx_packet
's header is pre-generated.
Depending on the mix topology, there is no hard requirement that the per-hop routing info is padded to one fixed constant length.
For example, assuming a layered topology (referred to as stratified topology in the literature) [MIXTOPO10], where the layer of any given mix node is public information, as long as the following two invariants are maintained, there is no additional information available to an adversary:
- All packets entering any given mix node in a certain layer are uniform in length.
- All packets leaving any given mix node in a certain layer are uniform in length.
The only information available to an external or internal observer is the layer of any given mix node (via the packet length), which is information they are assumed to have by default in such a design.
The Sphinx Packet Construct is crafted such that any given packet
is bitwise unlinkable after a Sphinx_Unwrap operation, provided
that the optional Additional Data (AD) facility is not used. This
property ensures that external passive adversaries are unable to
track a packet based on content as it traverses the network. As
the on-the-wire AD field is static through the lifetime of a
packet (ie: left unaltered by the Sphinx_Unwrap
operation),
implementations and applications that wish to use this facility
MUST NOT transmit AD that can be used to distinctly identify
individual packets.
Each node acting as a mix MUST regenerate their asymmetric key pair relatively frequently. Upon key rotation the old private key MUST be securely destroyed. As each layer of a Sphinx Packet is encrypted via key material derived from the output of an ephemeral/static Diffie-Hellman key exchange, without the rotation, the construct does not provide Perfect Forward Secrecy. Implementations SHOULD implement defense-in-depth mitigations, for example by using strongly forward-secure link protocols to convey Sphinx Packets between nodes.
This frequent mix routing key rotation can limit SURB usage by directly reducing the lifetime of SURBs. In order to have a strong Forward Secrecy property while maintaining a higher SURB lifetime, designs such as forward secure mixes [SFMIX03] could be used.
Reply Blocks (SURBs), forward and reply Sphinx packets are all vulnerable to the compulsion threat, if they are captured by an adversary. The adversary can request iterative decryptions or keys from a series of honest mixes in order to perform a deanonymizing trace of the destination.
While a general solution to this class of attacks is beyond the scope of this document, applications that seek to mitigate or resist compulsion threats could implement the defenses proposed in [COMPULS05] via a series of routing command extensions.
Given a hypothetical scenario where Alice and Bob both wish to keep their location on the mix network hidden from the other, and Alice has somehow received a SURB from Bob, Alice MUST not utilize the SURB directly because in the volunteer operated mix network the first hop specified by the SURB could be operated by Bob for the purpose of deanonymizing Alice.
This problem could be solved via the incorporation of a "cross-over point" such as that described in [MIXMINION], for example by having Alice delegating the transmission of a SURB Reply to a randomly selected crossover point in the mix network, so that if the first hop in the SURB's return path is a malicious mix, the only information gained is the identity of the cross-over point.
The payload encryption's use of a fragile (non-malleable) SPRP is deliberate and implementations SHOULD NOT substitute it with a primitive that does not provide such a property (such as a stream cipher based PRF). In particular there is a class of correlation attacks (tagging attacks) targeting anonymity systems that involve modification to the ciphertext that are mitigated if alterations to the ciphertext result in unpredictable corruption of the plaintext (avalanche effect).
Additionally, as the PAYLOAD_TAG_LENGTH based tag-then-encrypt payload integrity authentication mechanism is predicated on the use of a non-malleable SPRP, implementations that substitute a different primitive MUST authenticate the payload using a different mechanism.
Alternatively, extending the MAC contained in the Sphinx Packet Header to cover the Sphinx Packet Payload will both defend against tagging attacks and authenticate payload integrity. However, such an extension does not work with the SURB construct presented in this specification, unless the SURB is only used to transmit payload that is known to the creator of the SURB.
[RFC2119] | Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, <http://www.rfc-editor.org/info/rfc2119>. |
[RFC5246] | Dierks, T. and E. Rescorla, "The Transport Layer Security (TLS) Protocol Version 1.2", RFC 5246, DOI 10.17487/RFC5246, August 2008, <http://www.rfc-editor.org/info/rfc5246>. |
[SPHINX09] | Danezis, G., Goldberg, I., "Sphinx: A Compact and Provably Secure Mix Format", DOI 10.1109/SP.2009.15, May 2009, <https://cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf>. |
[COMPULS05] | Danezis, G., Clulow, J., "Compulsion Resistant Anonymous Communications", Proceedings of Information Hiding Workshop, June 2005, <https://www.freehaven.net/anonbib/cache/ih05-danezisclulow.pdf>. |
[SFMIX03] | Danezis, G., "Forward Secure Mixes", Proceedings of 7th Nordic Workshop on Secure IT Systems, 2002, <https://www.freehaven.net/anonbib/cache/Dan:SFMix03.pdf>. |
[MIXTOPO10] | Diaz, C., Murdoch, S., Troncoso, C., "Impact of Network Topology on Anonymity and Overhead in Low-Latency Anonymity Networks", PETS, July 2010, <https://www.esat.kuleuven.be/cosic/publications/article-1230.pdf>. |
[MIXMINION] | Danezis, G., Dingledine, R., Mathewson, N., "Mixminion: Design of a Type III Anonymous Remailer Protocol", <https://www.mixminion.net/minion-design.pdf>. |
Note that the following bibtex entry is in the IEEEtran bibtex style as described in a document called "How to Use the IEEEtran BIBTEX Style".
@online{SphinxSpec, title = {Sphinx Mix Network Cryptographic Packet Format Specification}, author = {Yawning Angel and George Danezis and Claudia Diaz and Ania Piotrowska and David Stainton}, url = {https://github.com/katzenpost/docs/blob/master/specs/sphinx.rst}, year = {2017} }