mirror of
https://github.com/oxen-io/lokinet.git
synced 2024-11-13 01:10:24 +00:00
6652cc4bde
i don't know why this wasn't here. i could've sworn it was.
757 lines
17 KiB
Plaintext
757 lines
17 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:
|
|
|
|
see crypto_v0.txt
|
|
|
|
---
|
|
|
|
wire protocol
|
|
|
|
see iwp-v0.txt
|
|
|
|
---
|
|
|
|
datastructures:
|
|
|
|
all datastructures are assumed version 0 if they lack a v value
|
|
otherwise version is provided by the v value
|
|
|
|
all ip addresses can be ipv4 via hybrid dual stack ipv4 mapped ipv6 addresses,
|
|
i.e ::ffff.8.8.8.8. The underlying implementation MAY implement ipv4 as native
|
|
ipv4 instead of using a hybrid dual stack.
|
|
|
|
net address:
|
|
|
|
net addresses are a variable length byte string, if between 7 and 15 bytes it's
|
|
treated as a dot notation ipv4 address (xxx.xxx.xxx.xxx)
|
|
if it's exactly 16 bytes it's treated as a big endian encoding ipv6 address.
|
|
|
|
address info (AI)
|
|
|
|
An address info (AI) defines a publically reachable endpoint
|
|
|
|
{
|
|
c: transport_rank_uint16,
|
|
d: "<transport dialect name>",
|
|
e: "<32 bytes public encryption key>",
|
|
i: "<net address>",
|
|
p: port_uint16,
|
|
v: 0
|
|
}
|
|
|
|
example iwp address info:
|
|
|
|
{
|
|
c: 1,
|
|
d: "iwp",
|
|
e: "<32 bytes of 0x61>",
|
|
i: "123.123.123.123",
|
|
p: 1234,
|
|
v: 0
|
|
}
|
|
|
|
bencoded form:
|
|
|
|
d1:ci1e1:d3:iwp1:e32:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1:d3:iwp1:i15:123.123.123.1231:pi1234e1:vi0ee
|
|
|
|
Exit Info (XI)
|
|
|
|
An exit info (XI) defines a exit address that can relay exit traffic to the
|
|
internet.
|
|
|
|
{
|
|
a: "<net address exit address>",
|
|
b: "<net address exit netmask>",
|
|
k: "<32 bytes public encryption/signing key>",
|
|
v: 0
|
|
}
|
|
|
|
|
|
Exit Route (XR)
|
|
|
|
An exit route (XR) define an allocated exit address and any additional
|
|
information required to access the internet via that exit address.
|
|
|
|
{
|
|
a: "<16 bytes big endian ipv6 gateway address>",
|
|
b: "<16 bytes big endian ipv6 netmask>",
|
|
c: "<16 bytes big endian ipv6 source address>",
|
|
l: lifetime_in_seconds_uint64,
|
|
v: 0
|
|
}
|
|
|
|
router contact (RC)
|
|
|
|
router's full identity
|
|
|
|
{
|
|
a: [ one, or, many, AI, here ... ],
|
|
k: "<32 bytes public long term identity signing key>",
|
|
n: "<optional max 32 bytes router nickname>",
|
|
p: "<32 bytes public path encryption key>",
|
|
u: last_updated_seconds_since_epoch_uint64,
|
|
v: 0,
|
|
x: [ Exit, Infos ],
|
|
z: "<64 bytes signature using identity key>"
|
|
}
|
|
|
|
service info (SI)
|
|
|
|
public information blob for a hidden service
|
|
|
|
e is the long term public encryption key
|
|
s is the long term public signing key
|
|
v is the protocol version
|
|
x is a nounce value for generating vanity addresses that can be omitted
|
|
|
|
if x is included it MUST be equal to 16 bytes
|
|
|
|
{
|
|
e: "<32 bytes public encryption key>",
|
|
s: "<32 bytes public signing key>",
|
|
v: 0,
|
|
x: "<optional 16 bytes nonce for vanity>"
|
|
}
|
|
|
|
service address (SA)
|
|
|
|
the "network address" of a hidden service, which is computed as the blake2b
|
|
256 bit hash of the public infomration blob.
|
|
|
|
HS(BE(SI))
|
|
|
|
when in string form it's encoded with z-base32 and uses the .loki tld
|
|
|
|
introduction (I)
|
|
|
|
a descriptor annoucing a path to a hidden service
|
|
|
|
k is the rc.k value of the router to contact
|
|
p is the path id on the router that is owned by the service
|
|
v is the protocol version
|
|
x is the timestamp seconds since epoch that this introduction expires at
|
|
|
|
{
|
|
k: "<32 bytes public identity key of router>",
|
|
l: advertised_path_latency_ms_uint64, (optional)
|
|
p: "<16 bytes path id>",
|
|
v: 0,
|
|
x: time_expires_seconds_since_epoch_uint64
|
|
}
|
|
|
|
introduction set (IS)
|
|
|
|
a signed set of introductions for a hidden service
|
|
a is the service info of the publisher
|
|
i is the list of introductions that this service is advertising with
|
|
k is the public key to use when doing encryption to this hidden service
|
|
n is a 16 byte null padded utf-8 encoded string tagging the hidden service in
|
|
a topic searchable via a lookup (optional)
|
|
v is the protocol version
|
|
w is an optinal proof of work for DoS protection (slot for future)
|
|
z is the signature of the entire IS where z is set to zero signed by the hidden
|
|
service's signing key.
|
|
|
|
{
|
|
a: SI,
|
|
i: [ I, I, I, ... ],
|
|
k: "<1218 bytes sntrup4591761 public key block>",
|
|
n: "<16 bytes service topic (optional)>",
|
|
t: timestamp_uint64_milliseconds_since_epoch_published_at,
|
|
v: 0,
|
|
w: optional proof of work,
|
|
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, n)
|
|
|
|
1) encode and encrypt
|
|
|
|
x = BE(msg)
|
|
new_x = SE(S, n[0:24], x)
|
|
|
|
2) generate message authentication
|
|
|
|
h = MDS(n + A.k + new_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 introduce message (LIM)
|
|
|
|
This message MUST be the first link message sent before any others. This message
|
|
identifies the sender as having the RC contained in r. The recipiant MUST
|
|
validate the RC's signature and ensure that the public key in use is listed in
|
|
the RC.a matching the ipv6 address it originated from.
|
|
|
|
{
|
|
a: "i",
|
|
n: "<32 bytes nonce for key exhcange>",
|
|
p: uint64_milliseconds_session_period,
|
|
r: RC,
|
|
v: 0,
|
|
z: "<64 bytes signature of entire message by r.k>"
|
|
}
|
|
|
|
the link session will be kept open for p milliseconds after which
|
|
the session MUST be renegotiated.
|
|
|
|
link relay commit message (LRCM)
|
|
|
|
request a commit to relay traffic to another node.
|
|
|
|
{
|
|
a: "c",
|
|
c: [ list, of, encrypted, frames ],
|
|
v: 0
|
|
}
|
|
|
|
c and r MUST contain dummy records if the hop length is less than the maximum
|
|
hop length.
|
|
|
|
link relay commit record (LRCR)
|
|
|
|
record requesting relaying messages for 600 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.
|
|
|
|
if l is provided and is less than 600 and greater than 10 then that lifespan
|
|
is used (in seconds) instead of 600 seconds.
|
|
|
|
if w is provided and fits the required proof of work then the lifetime of
|
|
the path is extended by w.y seconds
|
|
|
|
{
|
|
c: "<32 byte public encryption key used for upstream>",
|
|
i: "<32 byte RC.k of next hop>",
|
|
l: uint_optional_lifespan,
|
|
n: "<32 bytes nounce for key exchange>",
|
|
r: "<16 bytes rx path id>",
|
|
t: "<16 bytes tx path id>",
|
|
v: 0,
|
|
w: proof of work
|
|
}
|
|
|
|
w if provided is a dict with the following struct
|
|
|
|
{
|
|
t: time_created_seconds_since_epoch,
|
|
v: 0,
|
|
y: uint32_seconds_extended_lifetime,
|
|
z: "<32 bytes nonce>"
|
|
}
|
|
|
|
the validity of the proof of work is that given
|
|
|
|
h = HS(BE(w))
|
|
|
|
h has log_e(y) prefixed bytes being 0x00
|
|
|
|
this proof of work requirement is subject to change
|
|
|
|
if i is equal to RC.k then any LRDM.x values are decrypted and interpreted as
|
|
routing layer messages. This indicates that we are the farthest hop in the path.
|
|
|
|
link relay upstream message (LRUM)
|
|
|
|
sent to relay data via upstream direction of a previously created path.
|
|
|
|
{
|
|
a: "u",
|
|
p: "<16 bytes path id>",
|
|
v: 0,
|
|
x: "<N bytes encrypted x1 value>",
|
|
y: "<32 bytes nonce>"
|
|
}
|
|
|
|
x1 = SD(k, y, x)
|
|
|
|
if we are the farthest hop, process x1 as a routing message
|
|
otherwise transmit a LRUM to the next hop
|
|
|
|
{
|
|
a: "u",
|
|
p: p,
|
|
v: 0,
|
|
x: x1,
|
|
y: y ^ HS(k)
|
|
}
|
|
|
|
link relay downstream message (LRDM)
|
|
|
|
sent to relay data via downstream direction of a previously created path.
|
|
|
|
{
|
|
a: "d",
|
|
p: "<16 bytes path id>",
|
|
v: 0,
|
|
x: "<N bytes encrypted x1 value>",
|
|
y: "<32 bytes nonce>"
|
|
}
|
|
|
|
if we are the creator of the path decrypt x for each hop key k
|
|
|
|
x = SD(k, y, x)
|
|
|
|
otherwise transmit LRDM to next hop
|
|
|
|
x1 = SE(k, y, x)
|
|
|
|
{
|
|
a: "d",
|
|
p: p,
|
|
v: 0,
|
|
x: x1,
|
|
y: y ^ HS(k)
|
|
}
|
|
|
|
link immediate dht message (LIDM):
|
|
|
|
transfer one or more dht messages directly without a previously made path.
|
|
|
|
{
|
|
a: "m",
|
|
m: [many, dht, messages],
|
|
v: 0
|
|
}
|
|
|
|
|
|
link stateless relay message (LSRM)
|
|
|
|
statelessly relay a link message.
|
|
|
|
{
|
|
a: "r",
|
|
c: r_counter_uint8,
|
|
d: "<32 bytes rc.K of destination>",
|
|
s: "<32 bytes rc.K of source>",
|
|
v: 0,
|
|
x: "<N bytes encrypted link message>",
|
|
y: "<24 bytes nounce>",
|
|
z: "<64 bytes signature>"
|
|
}
|
|
|
|
ONLY exchanged over ethernet, if recieved from an IP link it MUST be discarded.
|
|
|
|
relay an encrypted link message from source s to destination d.
|
|
check signature z using public key s and discard if invalid signature.
|
|
|
|
if d is equal to ourRC.k then decrypt x via SD(KE(d, s), y, x) and process it as
|
|
a link message. if the inner decrypted link message is a LRCM forward all
|
|
following LRUM, LRDM and LRSM to s via a LSRM. LIDM and LSRM are discarded.
|
|
|
|
if d is not equal to ourRC.k then forward it to an ethernet peer that is cloeser
|
|
to d than you are. if you are closer to d than all of your other ethernet peers
|
|
then increment c and send to the ethernet peer with the lowest detected latency
|
|
that isn't the peer that this message was recieved from but ONLY if c is less
|
|
than 128. if c is equal to or greater than 128 then the message is discarded.
|
|
|
|
---
|
|
|
|
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. all routing messages have an S value which
|
|
provides the sequence number of the message so the messages can be ordered.
|
|
|
|
ipv4 addresses are allowed via ipv4 mapped ipv6 addresses, i.e. ::ffff.10.0.0.1
|
|
|
|
path confirm message (PCM)
|
|
|
|
sent as the first message down a path after it's built to confirm it was built
|
|
|
|
always the first message sent
|
|
|
|
{
|
|
A: "P",
|
|
L: uint64_milliseconds_path_lifetime,
|
|
S: 0,
|
|
T: uint64_milliseconds_sent_timestamp,
|
|
V: 0
|
|
}
|
|
|
|
path latency message (PLM)
|
|
|
|
a latency measurement message, reply with a PLM response if we are the far end
|
|
of a path.
|
|
|
|
variant 1, request, generated by the path creator:
|
|
|
|
{
|
|
A: "L",
|
|
S: uint64_sequence_number,
|
|
V: 0
|
|
}
|
|
|
|
variant 2, response, generated by the endpoint that recieved the request.
|
|
|
|
{
|
|
A: "L",
|
|
S: uint64_sequence_number,
|
|
T: uint64_timestamp_recieved,
|
|
V: 0
|
|
}
|
|
|
|
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>",
|
|
S: uint64_sequence_number,
|
|
T: uint64_transaction_id,
|
|
V: 0,
|
|
X: lifetime_of_address_mapping_in_seconds_uint64,
|
|
Z: "<64 bytes signature using I>"
|
|
}
|
|
|
|
grant exit address messsage (GXAM)
|
|
|
|
sent in response to an OXAM to grant an ip for exit traffic from an external
|
|
ip address used for exit traffic.
|
|
|
|
{
|
|
A: "G",
|
|
E: XR,
|
|
I: "<32 bytes signing public key of requester>",
|
|
S: uint64_sequence_number,
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
Z: "<64 bytes signature using exit info's signing key>"
|
|
}
|
|
|
|
E contains an exit route that was granted to the requester that can be used with
|
|
IP exit traffic.
|
|
|
|
The requester will now have any ip traffic going to address S forwarded to them
|
|
via the path that originally sent the OXAM and any TDFM will is recieved on the
|
|
same path will be forwarded out to the internet, given that they have
|
|
valid signatures and addresses.
|
|
|
|
|
|
reject exit address message (RXAM)
|
|
|
|
sent in response to an OXAM to indicate that exit traffic is not allowed or
|
|
was denied.
|
|
|
|
{
|
|
A: "J",
|
|
B: backoff_milliseconds_uint64,
|
|
I: "<32 bytes signing public key of requester>",
|
|
R: "<optional reject metadata>",
|
|
S: uint64_sequence_number,
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
Z: "<64 bytes signature signed by exit info's signing key>"
|
|
}
|
|
|
|
B is set to a backoff value.
|
|
R contains additional metadata text describing why the exit was rejected.
|
|
|
|
|
|
discarded data fragment message (DDFM)
|
|
|
|
sent in reply to TDFM when we don't have a path locally or are doing network
|
|
congestion control. indcates a TDFM was discarded.
|
|
|
|
{
|
|
A: "D",
|
|
P: "<16 bytes path id>",
|
|
S: uint64_sequence_number_of_fragment_dropped,
|
|
V: 0
|
|
}
|
|
|
|
transfer data fragment message (TDFM)
|
|
|
|
transfer data between paths.
|
|
|
|
{
|
|
A: "T",
|
|
P: "<16 bytes path id>",
|
|
S: uint64_sequence_number,
|
|
T: hidden_service_frame,
|
|
V: 0
|
|
}
|
|
|
|
transfer data to another path with id P on the local router place a random 32
|
|
byte and T values into y and z values into a LRDM message (respectively) and
|
|
send it in the downstream direction. if this path does not exist on the router
|
|
it is replied to with a DDFM.
|
|
|
|
|
|
|
|
hidden service data (HSD)
|
|
|
|
data sent anonymously over the network to a recipiant from a sender.
|
|
sent inside a HSFM encrypted with a shared secret.
|
|
|
|
{
|
|
a: protocol_number_uint,
|
|
d: "<N bytes payload>",
|
|
i: Introduction for reply,
|
|
n: uint_message_sequence_number,
|
|
o: N seconds until this converstation plans terminate,
|
|
s: SI of sender,
|
|
t: "<16 bytes converstation tag present only when n is 0>",
|
|
v: 0
|
|
}
|
|
|
|
|
|
hidden service frame (HSF)
|
|
|
|
TODO: document this better
|
|
|
|
intro message (variant 1)
|
|
|
|
start a new session
|
|
|
|
{
|
|
A: "H",
|
|
C: "<1048 bytes ciphertext block>",
|
|
D: "<N bytes encrypted HSD>",
|
|
N: "<32 bytes nonce for key exchange>",
|
|
V: 0,
|
|
Z: "<64 bytes signature of entire message using sender's signing key>"
|
|
}
|
|
|
|
alice (A) wants to talk to bob (B) over the network, both have hidden services
|
|
set up and are online on the network.
|
|
|
|
A and B are both SI.
|
|
A_sk is alice's private signing key.
|
|
|
|
for alice (A) to send the string "beep" to bob (B), alice picks an introduction
|
|
to use on one of her paths (I_A) such that I_A is aligning with one of bobs's
|
|
paths (I_B)
|
|
|
|
alice generates:
|
|
|
|
T = RAND(16)
|
|
|
|
m = {
|
|
a: 0,
|
|
d: "beep",
|
|
i: I_A,
|
|
n: 0,
|
|
s: A,
|
|
t: T,
|
|
v: 0
|
|
}
|
|
|
|
X = BE(m)
|
|
|
|
C, K = PQKE_A(I_B.k)
|
|
N = RAND(32)
|
|
D = SE(X, K, N)
|
|
|
|
M = {
|
|
A: "T",
|
|
P: I_B.P,
|
|
S: uint64_sequence_number,
|
|
T: {
|
|
A: "H",
|
|
C: C,
|
|
D: D,
|
|
N: N,
|
|
V: 0,
|
|
Z: "\x00" * 64
|
|
},
|
|
V: 0
|
|
}
|
|
|
|
Z = S(A_sk, BE(M))
|
|
|
|
alice transmits a TDFM to router with public key I_B.K via her path that ends
|
|
with router with public key I_B.k
|
|
|
|
{
|
|
A: "T",
|
|
P: I_B.P,
|
|
S: uint64_sequence_number,
|
|
T: {
|
|
A: "H",
|
|
C: C,
|
|
D: D,
|
|
N: N,
|
|
V: 0,
|
|
Z: Z
|
|
},
|
|
V: 0
|
|
}
|
|
|
|
the shared secret (S) for further message encryption is:
|
|
|
|
S = HS(K + PKE(A, B, sk, N))
|
|
|
|
given sk is the local secret encryption key used by the current hidden service
|
|
|
|
please note:
|
|
signature verification can only be done after decryption
|
|
|
|
TODO: explain bob's side too (it's invsere of alice's process)
|
|
|
|
data from a previously made session (variant 2)
|
|
|
|
transfer data on a session previously made
|
|
|
|
{
|
|
A: "H",
|
|
D: "<N bytes encrypted HSD>",
|
|
N: "<32 bytes nonce for symettric cipher>",
|
|
T: "<16 bytes converstation tag>",
|
|
V: 0,
|
|
Z: "<64 bytes signature using sender's signing key>"
|
|
}
|
|
|
|
|
|
transfer ip traffic message (TITM)
|
|
|
|
transfer ip traffic for exit
|
|
|
|
{
|
|
A: "E",
|
|
S: uint64_sequence_number,
|
|
V: 0,
|
|
X: "<N bytes ip packet>",
|
|
Y: "<16 bytes nounce>",
|
|
Z: "<64 bytes signature using previously provided signing key>"
|
|
}
|
|
|
|
X is parsed as an IP packet and the source addresss is extracted.
|
|
Next we find the corrisponding signing key for a previously granted exit address
|
|
and use it to validate the siganture of the entire message. If the signing key
|
|
cannot be found or the signature is invalid this message is dropped, otherwise
|
|
the X value is sent on the appropriate exit network interface.
|
|
|
|
When we recieve an ip packet from the internet to an exit address, we put it
|
|
into a TITM, signed with the exit info's signing key and send it downstream the
|
|
corrisponding path in an LRDM.
|
|
|
|
update exit path message (UXPM)
|
|
|
|
sent from a new path by client to indicate that a previously established exit
|
|
should use the new path that this message came from.
|
|
|
|
{
|
|
A: "U",
|
|
S: uint64_sequence_number,
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
Y: "<16 bytes nounce>",
|
|
Z: "<64 bytes signature using previously provided signing key>"
|
|
}
|
|
|
|
T is the transaction ID from the GXAM
|
|
|
|
close exit path message (CXPM)
|
|
|
|
client sends a CXPM when the exit is no longer needed.
|
|
The address used in exit MAY be reused later.
|
|
|
|
{
|
|
A: "C",
|
|
S: uint64_sequence_number,
|
|
T: transaction_id_uint64,
|
|
V: 0,
|
|
Y: "<16 bytes nounce>",
|
|
Z: "<64 bytes signagure using previously provided signing key>"
|
|
}
|
|
|
|
DHT message holder message:
|
|
|
|
wrapper message for sending many dht messages down a path.
|
|
|
|
{
|
|
A: "M",
|
|
M: [many, dht, messages, here],
|
|
S: uint64_sequence_number,
|
|
V: 0
|
|
}
|