mirror of https://github.com/oxen-io/lokinet
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
684 lines
15 KiB
Plaintext
684 lines
15 KiB
Plaintext
LLARP v0
|
|
|
|
LLARP (Low Latency Anon Routing Protocol) is a protocol for anonymizing senders and
|
|
recipiants of encrypted messages sent over the internet without a centralied
|
|
trusted party.
|
|
|
|
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 RFC 2119 [RFC2119].
|
|
|
|
basic structures:
|
|
|
|
all structures are key, value dictionaries encoded with bittorrent encoding
|
|
notation:
|
|
|
|
a + b is a concatanated with b
|
|
|
|
a ^ b is a bitwise XOR b
|
|
|
|
x[a:b] is a memory slice of x from index a to b
|
|
|
|
BE(x) is bittorrent encode x
|
|
|
|
BD(x) is bittorrent decode x
|
|
|
|
{ a: b, y: z } is a dictionary with two keys a and y
|
|
who's values are b and z respectively
|
|
|
|
[ a, b, c ... ] is a list containing a b c and more items in that order
|
|
|
|
"<description>" is a bytestring who's contents and length is described by the
|
|
quoted value <description>
|
|
|
|
"<value>" * N is a bytestring containing the <value> concatenated N times.
|
|
|
|
cryptography:
|
|
|
|
H(x) is 512 bit blake2b digest of x
|
|
HS(x) is 256 bit blake2b digest of x
|
|
MD(x, k) is 512 bit blake2b hmac of x with secret value k
|
|
MDS(x, k) is 256 bit blake2b hmac of x with secret value k
|
|
NE(k, x) is sntrup4591761 encrypt data x to public key k
|
|
ND(k, x) is sntrup4591761 decrypt data x with private key k
|
|
SE(k, n, x) is chacha20 encrypt data x using symettric key k and nounce n
|
|
SD(k, n, x) is chacha20 dectypt data x using symettric key k and nounce n
|
|
S(k, x) is sign x with ed25519 using seed k
|
|
ECKG() is generate ec keypair (p, s) public key p, seed s, both 32 bytes
|
|
V(k, x, sig) is verify x data using signature sig using public key k
|
|
DH(x, y) is a ecdh key exchange using ed25519 scalarmult between public keys x
|
|
and y
|
|
KE(x, y) is a ecdh key exchange using H(x + y + DH(x, y))
|
|
PKE(x, y, n) is a path key exchange using MDS(n, KE(x, y))
|
|
TKE(x, y, n) is a transport key exchange using MD(n, KE(x, y))
|
|
RAND(n) is n random bytes
|
|
|
|
---
|
|
|
|
|
|
wire protocol:
|
|
|
|
as of version 0 plaintext sctp is used, future versions will use an encrypted
|
|
udp transport (IWP).
|
|
|
|
|
|
wire decryption:
|
|
|
|
the first 32 bytes are message authentication bytes, h
|
|
the next 32 bytes are nounce for shared secret, n
|
|
the remaining bytes are interpreted as ciphertext, x
|
|
|
|
a shared secret s is generated via TKE(us, them, n)
|
|
next the integrity of the ciphertext is done by checking MDS(n + x, s) == h
|
|
if the ciphertext is valid then the frame is decrypted via SD(s, n, x)
|
|
|
|
wire encryption:
|
|
|
|
given variadic sized payload p, 32 byte nounce n and public encryption keys A
|
|
and B
|
|
|
|
s = TKE(A, B, n)
|
|
x = SE(s, n, p)
|
|
h = MDS(n + x, s)
|
|
|
|
the resulting data is:
|
|
|
|
h + n + x
|
|
|
|
|
|
handshake:
|
|
|
|
0) intro
|
|
|
|
32 bytes hmac, h
|
|
32 bytes nounce, n
|
|
64 bytes elligator sqaured encoded alice's transport public encryption key, a.k
|
|
variadic bytes padding, w0
|
|
|
|
Alice transmits ( h + n + a.k + w0 ) to Bob from the transport address matching
|
|
his public transport encryption key.
|
|
|
|
Bob recieves ( h + n + a.k + w0 )
|
|
|
|
1) intro ack
|
|
|
|
sent in reply to an intro, bob sends an intro ack encrypted to Alice using
|
|
|
|
32 bytes hmac, h
|
|
32 bytes nounce, n
|
|
32 bytes ciphertext, x
|
|
variadic bytes padding, w1
|
|
|
|
token = RAND(32)
|
|
k = TKE(a.k, b.k, n)
|
|
x = SE(k, token, n[0:24])
|
|
h = MDS(n + x, k)
|
|
|
|
Bob transmits ( h + n + x + w1 )
|
|
Alice recieves ( h + n + x + w1 ) and verifies that h == MDS(n + x, k) silently
|
|
dropping if it does not match.
|
|
|
|
2) token offer
|
|
|
|
Alice sends the token from the intro ack back to Bob
|
|
|
|
32 bytes hmac, h
|
|
32 bytes nounce, n
|
|
32 bytes ciphertext, x
|
|
variadic byttes padding, w2
|
|
|
|
k = TKE(a.k, b.k, n)
|
|
x = SE(k, token, n[0:24])
|
|
h = MDS(n + x, k)
|
|
|
|
Alice transmits ( h + n + x + w2 )
|
|
Bob recieves ( h + n + x + w2) and verifies that h == MDS(n + x, k) silently
|
|
drops if not matching
|
|
|
|
4) token ack
|
|
|
|
Bob acks the token that he got from Alice
|
|
|
|
32 bytes hmac, h
|
|
32 bytes nounce, n
|
|
32 bytes ciphertext, x
|
|
variadic byttes padding, w3
|
|
|
|
S = TKE(a.k, b.k, token)
|
|
x = SE(S, token, n[0:24])
|
|
h = MDS(n + x, S)
|
|
|
|
Alice transmits ( h + n + x + w3 ) to Bob and the session is now established
|
|
using shared secret S
|
|
|
|
Bob receves ( h + n + x + w2 ) and verifies that h == MDS(n + x, S)
|
|
|
|
IWP payload format:
|
|
|
|
ciphertext:
|
|
32 bytes hmac, h
|
|
32 bytes nounce, n
|
|
N bytes of ciphertext, x
|
|
|
|
plaintext header, H
|
|
8 bits protocol version, v (currently 0)
|
|
8 bits message type, t
|
|
12 bits payload size, s
|
|
4 bits flags, f
|
|
|
|
plaintext payload: P
|
|
s bytes of data
|
|
N bytes remaining data is discarded
|
|
|
|
D = H + P
|
|
x = SE(D, S, n)
|
|
h = MDS(n + x, S)
|
|
|
|
Alice transmits h + n + x
|
|
|
|
Bob recieves recieve h + n + x
|
|
|
|
Bob checks hmac by verifying h == MDS(n + x, S)
|
|
|
|
if the hmac fails the data is silently dropped
|
|
|
|
message types:
|
|
|
|
XMIT = 0x01
|
|
|
|
begin link layer message transmission
|
|
|
|
ACKS = 0x02
|
|
|
|
acknolege link layer message fragment
|
|
|
|
FRAG = 0x03
|
|
|
|
transmit link layer message fragment
|
|
|
|
flags:
|
|
|
|
SESSION_INVALIDATED = 1 << 0
|
|
|
|
this session is now invalidated and a new session is required
|
|
|
|
HIGH_PACKET_DROP = 1 << 1
|
|
|
|
high packet drop detected
|
|
|
|
HIGH_MTU_DETECTED = 1 << 2
|
|
|
|
the network uses an mtu greater than 1488 bytes
|
|
|
|
PROTOCOL_UPGRADE = 1 << 3
|
|
|
|
indicates we want to do protocol upgrade (future use)
|
|
|
|
XMIT payload:
|
|
|
|
start transmiting a link layer message
|
|
|
|
msg_bytes = BE(msg)
|
|
|
|
32 bytes hash of message computed by HS(msg_bytes)
|
|
64 bits unsigned int message id
|
|
12 bits unsigned int fragment size bytes, s
|
|
4 bits unsigned int nonzero number of fragments, n
|
|
8 bits size of last fragment in bytes, l
|
|
|
|
msg_bytes is s * (n - 1) + l bytes long
|
|
|
|
FRAG payload:
|
|
|
|
transmit a link layer message fragment
|
|
|
|
64 bits message id
|
|
4 bits ignored
|
|
4 bits unsigned int fragment number
|
|
remaining bytes of payload are fragment data
|
|
|
|
ACKS payload:
|
|
|
|
indicates we which chunks we have recieved
|
|
|
|
64 bits message id
|
|
16 bits bitmask of chunks we have received
|
|
remaining bytes discarded
|
|
|
|
|
|
control flow:
|
|
|
|
To transmit link message over an established session the transmitter sends an
|
|
XMIT frame.
|
|
In reply to an XMIT frame the recipiant MUST send an ACKS frame with an emtpy
|
|
bitmask.
|
|
After the transmitter recieves the first ACKS frame it is allowed to start
|
|
sending FRAG messages.
|
|
When all fragmenets are obtained by the recipiant, the recipiant sends an ACKS
|
|
frame with a full bitfield (0xFFFF), to indicate the link message was recieved.
|
|
In the event of packet drop the sender decides when to retransmit FRAG frames
|
|
with expontential backoff.
|
|
|
|
In the event of packet loss greater than 50% over 10 second the session is
|
|
invalidated and must be renegotiated with a new handshake.
|
|
|
|
---
|
|
|
|
datastructures:
|
|
|
|
all datastructures are assumed version 0 if they lack a v value
|
|
otherwise version is provided by the v value
|
|
|
|
address info (AI)
|
|
|
|
An address info (AI) defines a publically reachable ipv6 endpoint
|
|
|
|
{
|
|
c: transport_rank_uint16,
|
|
e: "<32 bytes public encryption key>",
|
|
d: "<transport dialect name>",
|
|
i: "<16 bytes big endian public ipv6 address>",
|
|
p: port_uint16,
|
|
v: 0
|
|
}
|
|
|
|
Exit Info (XI)
|
|
|
|
{
|
|
a: "<16 bytes big endian ipv6 address>",
|
|
b: "<16 bytes big endian ipv6 netmask>",
|
|
v: 0
|
|
}
|
|
|
|
router contact (RC)
|
|
|
|
{
|
|
a: [ one, or, many, AI, here ... ],
|
|
k: "<32 bytes public signing/encryption identity key>",
|
|
x: [ Exit, Infos ],
|
|
v: 0,
|
|
z: "<64 bytes signature using identity key>"
|
|
}
|
|
|
|
service info (SI)
|
|
|
|
{
|
|
n: "<optional claimed name>",
|
|
s: "<32 bytes public signing key>",
|
|
v: 0,
|
|
x: "<optional nounce for vanity>"
|
|
}
|
|
|
|
service address (SA)
|
|
|
|
H(BE(SI))
|
|
|
|
introducer (I)
|
|
|
|
{
|
|
i: "<32 bytes public key of router>",
|
|
p: path_id_uint64,
|
|
v: 0,
|
|
x: time_expires_seconds_since_epoch_uint64
|
|
}
|
|
|
|
introducer set (IS)
|
|
|
|
{
|
|
a: "<64 bytes SA>",
|
|
e: "<1218 bytes ntru public encryption key>",
|
|
i: [ I, I, I, ... ],
|
|
v: 0,
|
|
z: "<64 bytes signature using service info signing key>"
|
|
}
|
|
|
|
|
|
---
|
|
|
|
Encrypted frames:
|
|
|
|
Encrypted frames are encrypted containers for link message records like LRCR.
|
|
|
|
32 bytes hmac, h
|
|
32 bytes nounce, n
|
|
32 bytes ephmeral sender's public encryption key, k
|
|
remaining bytes ciphertext, x
|
|
|
|
decryption:
|
|
|
|
0) verify hmac
|
|
|
|
S = PKE(n, k, our_RC.K)
|
|
verify h == MDS(n + k + x, S)
|
|
|
|
If the hmac verification fails the entire parent message is discarded
|
|
|
|
1) decrypt and decode
|
|
|
|
new_x = SD(S, n[0:24], x)
|
|
msg = BD(new_x)
|
|
|
|
If the decoding fails the entire parent message is discarded
|
|
|
|
encryption:
|
|
|
|
to encrypt a frame to a router with public key B.k
|
|
|
|
0) prepare nounce n, ephemeral keypair (A.k, s) and derive shared secret S
|
|
|
|
A.k, s = ECKG()
|
|
n = RAND(32)
|
|
S = PKE(p, A.k, B.k)
|
|
|
|
1) encode and encrypt
|
|
|
|
x = BE(msg)
|
|
new_x = SE(S, n[0:24], x)
|
|
|
|
2) generate hmac
|
|
|
|
h = MDS(n + A.k + x, S)
|
|
|
|
resulting frame is h + n + A.k + new_x
|
|
|
|
|
|
---
|
|
|
|
link layer messages:
|
|
|
|
the link layer is responsible for anonymising the source and destination of
|
|
routing layer messages.
|
|
|
|
any link layer message without a key v is assumed to be version 0 otherwise
|
|
indicates the protocol version in use.
|
|
|
|
link relay commit message (LRCM)
|
|
|
|
|
|
{
|
|
a: "c",
|
|
b: [ list, of, encrypted, frames ],
|
|
v: 0,
|
|
}
|
|
|
|
|
|
link relay commit record (LRCR)
|
|
|
|
record requesting path with id p relay messages for x seconds to router
|
|
on network who's i is equal to RC.k and decrypt data any messages using
|
|
PKE(n, rc.K, c) as symettric key for encryption and decryption.
|
|
|
|
{
|
|
c: "<32 byte public signing/encryption key used for further communication>",
|
|
i: "<32 byte RC.k of next hop>",
|
|
n: "<32 bytes nounce for key exchange>",
|
|
p: path_id_uint64,
|
|
v: 0,
|
|
x: seconds_lifetime_uint64
|
|
}
|
|
|
|
if i is equal to RC.k then any LRDM.z values are decrypted and interpreted as
|
|
routing layer messages.
|
|
|
|
if i is not equal to RC.k then forward the LRCM with first element removed
|
|
and the last element holding our hop's reply. this ensures that the first entry
|
|
in the forwarded LRCM is for the next hop in the requested path.
|
|
|
|
if i is equal to RC.k unconditionally send a LRDM with encrypted payload
|
|
holding a LRSM with our record at the end and the previous ones in the front.
|
|
|
|
link relay reject record (LRRR)
|
|
|
|
sent in reply to a LRCM indicating we have rejected the request to relay data
|
|
for path with id p, the recipiant of this message MUST backoff sending LRCM for
|
|
b milliseconds or recipiant MAY get banned by recipiant router for an undefined
|
|
amount of time. r contains a bytestring of 7 bit clean ascii metadata indicating
|
|
why the commit was rejected. if included r MUST be logged or collected for later
|
|
review by node operator. inclusion of r is OPTIONAL. review of collected events
|
|
is RECOMMENDED.
|
|
|
|
{
|
|
b: miliseconds_backoff_uint64,
|
|
c: "r",
|
|
p: path_id_uint64,
|
|
r: "<optional reason metadata here>",
|
|
v: 0,
|
|
x: "<N bytes arbirary padding>"
|
|
}
|
|
|
|
link relay accept record (LRAR)
|
|
|
|
sent in reply to a LRCM indicating we have accepted the request to relay data
|
|
for path with id p.
|
|
|
|
{
|
|
c: "a",
|
|
p: path_id_uint64,
|
|
v: 0,
|
|
x: "<N bytes arbitrary padding>"
|
|
}
|
|
|
|
|
|
link relay status message (LRSM)
|
|
|
|
sent inside a LRDM after build has reached the end of the path to finish the
|
|
path build and send the result of the build.
|
|
|
|
{
|
|
a: "s",
|
|
p: [list, of, encrypted, replies],
|
|
v: 0,
|
|
}
|
|
|
|
|
|
link relay upstream message (LRUM)
|
|
|
|
sent to relay data via upstream direction of a previously created path.
|
|
decrypt z using previously derived key and nounce y. Relay with new_y and new_z
|
|
in upstream direction as a LRUM.
|
|
|
|
new_z = SD(k, y, z)
|
|
new_y = y ^ new_z[0:24]
|
|
|
|
{
|
|
a: "u",
|
|
p: path_id_uint64,
|
|
v: 0,
|
|
y: "<insert 24 bytes nounce here>",
|
|
z: "<insert N bytes payload here>"
|
|
}
|
|
|
|
link relay downstream message (LRDM)
|
|
|
|
sent to relay data via downstream direction of a previously created path.
|
|
encrypt z using previously derived key and nonce new_y and relay in downstream
|
|
direction as a LRDM.
|
|
|
|
new_y = y ^ z[0:24]
|
|
new_z = SE(k, new_y, z)
|
|
|
|
{
|
|
a: "d",
|
|
p: path_id_uint64,
|
|
v: 0,
|
|
y: "<insert 24 bytes nounce here>",
|
|
z: "<insert N bytes payload here>"
|
|
}
|
|
|
|
link relay exit message (LRXM)
|
|
|
|
sent to exit a previously commited path before it expires.
|
|
verify signature using cancel key c in relay commit message.
|
|
|
|
{
|
|
a: "x",
|
|
b: [ list, of, exit, records, as, encrpyted, frames ],
|
|
v: 0,
|
|
}
|
|
|
|
link relay exit record (LRXR)
|
|
|
|
{
|
|
c: "x",
|
|
p: path_id_uint64,
|
|
v: 0,
|
|
x: "<N bytes padding>",
|
|
z: "<64 bytes signature>"
|
|
}
|
|
|
|
---
|
|
|
|
direct paths:
|
|
|
|
a direct path is a "0 hop" path built by Alice to communicate directly to Bob
|
|
for point to point transmission of routing layer messages.
|
|
|
|
these are built by sending a LRCM where B has 1 entry
|
|
|
|
---
|
|
|
|
routing layer:
|
|
|
|
the routing layer provides inter network communication between the LLARP link
|
|
layer and ip (internet protocol) for exit traffic or ap (anonymous protocol) for
|
|
hidden services. replies to messages are sent back via the path they
|
|
originated from inside a LRDM.
|
|
|
|
for direct communication between routers a direct path MUST be used, these
|
|
messages MUST NOT be sent on the link leyer.
|
|
|
|
obtain exit address message (OXAM)
|
|
|
|
sent to an exit router to obtain a NAT ip address for ip exit traffic.
|
|
replies are sent down the path that messages originate from.
|
|
|
|
{
|
|
A: "X",
|
|
I: "<32 bytes signing public key for future communication>",
|
|
V: 0,
|
|
X: lifetime_of_address_mapping_in_seconds_uint64,
|
|
}
|
|
|
|
grant exit address messsage (GXAM)
|
|
|
|
sent in response to a OXAM to grant an ip for exit traffic from an external
|
|
ip address used for exit traffic.
|
|
|
|
{
|
|
A: "G",
|
|
E: "<16 byte big endian externally reachable ipv6 address>",
|
|
I: "<32 bytes signing public key of requester>",
|
|
V: 0,
|
|
Z: "<64 bytes signature using exit's signing key>"
|
|
}
|
|
|
|
reject exit address message (RXAM)
|
|
|
|
{
|
|
A: "R",
|
|
B: backoff_milliseconds_uint64,
|
|
I: "<32 bytes signing public key of requester>",
|
|
R: "<optional reject metadata>",
|
|
V: 0,
|
|
Z: "<64 bytes signature signed by exit>"
|
|
}
|
|
|
|
|
|
|
|
transfer data fragment message (TDFM)
|
|
|
|
variant 1 (with path id):
|
|
|
|
transfer data to another path with id P on the local router place Y and X values
|
|
into y and z values in LRDM message respectively.
|
|
|
|
{
|
|
A: "T",
|
|
P: path_id_uint64,
|
|
V: 0,
|
|
X: "<N bytes payload>",
|
|
Y: "<24 bytes nounce>",
|
|
Z: "<64 bytes signature of entire message where Z is set to NUL>",
|
|
}
|
|
|
|
variant 2 (no path id):
|
|
|
|
transfer ip traffic for exit
|
|
|
|
{
|
|
A: "T",
|
|
V: 0,
|
|
X: "<N bytes ipv6 packet>",
|
|
Z: "<64 bytes signature of previously provided signing key>"
|
|
}
|
|
|
|
find introduction message (FIM)
|
|
|
|
{
|
|
A: "F",
|
|
S: "<64 bytes dht key>",
|
|
V: 0,
|
|
T: transaction_id_uint64
|
|
}
|
|
|
|
got introduction message (GIM)
|
|
|
|
{
|
|
A: "G",
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
X: [ IS, IS, IS, ... ]
|
|
}
|
|
|
|
publish introduction message (PIM)
|
|
|
|
publish one or many IM into the dht at once.
|
|
each IS will be placed in the dht
|
|
|
|
version 0 uses the SA of each IS as the keyspace location.
|
|
|
|
in the future the location will be determined by the dht kdf
|
|
which uses a shared random source to obfuscate keyspace location.
|
|
|
|
|
|
{
|
|
A: "P",
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
X: [ IS, IS, IS, ... ],
|
|
}
|
|
|
|
acknoleged introduction message (AIM)
|
|
|
|
acknolege the publishing of a previous PIM, X contains the backoff values in ms
|
|
for the previously provided IS, if backoff is 0 the operation was successfull
|
|
|
|
{
|
|
A: "A",
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
X: [ 0, 0, backoff, ...],
|
|
}
|
|
|
|
find router contact message (FRCM)
|
|
|
|
find a router by public key
|
|
|
|
{
|
|
A: "F",
|
|
K: "<32 byte public key of router>",
|
|
T: transaction_id_uint64
|
|
V: 0
|
|
}
|
|
|
|
got router contact message (GRCM)
|
|
|
|
R is a list containing a single RC if found or is an empty list if not found
|
|
sent in reply to FRCM only
|
|
{
|
|
A: "G",
|
|
R: [RC],
|
|
T: transaction_id_uint64,
|
|
V: 0
|
|
}
|