forked from oxy-secure/oxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
protocol.txt
108 lines (82 loc) · 9.84 KB
/
protocol.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
There are three main phases to the lifecycle of an Oxy connection: the knock, the kex, and the main body.
THE KNOCK
=========
The knock phase consists of a client sending a single UDP packet to a server on a specific port between 1025 and 65535. This packet is built using a piece of data known as the "knock key". The knock key is a collection of random bytes that are pre-shared out-of-band between the server and all clients of the server. The port number for the knock is derived from this data, along with the message contents. Message contents are a fixed-width (100 byte) value derived from hashing the knock key with the current number of seconds since the unix epoch modulo 60.
Upon receiving a UDP packet on the designated port, the server calculates (and caches) knock packet values for the current time-window, the previous time-window (i.e. minus 60 seconds), and the next time-window (i.e. plus sixty seconds). The server performs a byte-for-byte comparison of the packet value with the server-calculated knock values. In the case of an exact match, the server records the source address and monotonic timestamp of the knock packet. Each knock value is retained for fifty seconds after being received.
When the server has valid knock values recorded, it establishes a TCP bind and begins accepting TCP connection. Upon receiving a TCP connection the server checks the source address to identify if the source address corresponds to a valid outstanding knock. If it does not, the server terminates the connection immediately without transmitting any data. If the connection does correspond to a valid source address, the server forks: the child process re-executes the oxy executable to become a connection process, whereas the server process returns to processing incoming connections.
Fifty seconds after the most recent valid knock was received, the server closes the TCP bind and will no longer accept incoming TCP connections.
THE KEX
=======
The kex (or "Key EXchange") consists of the client sending three size-specified messages to the server, then the server sending three size-specified replied. Messages consist of two bytes (big endian, unsigned), specifying the size of the following message.
The first message consists of one byte indicating the kex version number (currently must be zero), followed by a Ed25519 public key value. This is the long-term client key.
The second message consists of eight bytes containing a big-endian representation of the number of whole seconds since the unix epoch, followed by a different X25519 public key. This is the ephemeral client key.
The third message contains an Ed25519 signature of the value of the second message, signed using the long-term client key from the first message.
The server performs authentication intially by directly comparing the shared long-term client key value to its database of known client keys. No deserialization or cryptographic processing is done at this time. Only when the server is pre-existingly in possession of a byte-for-byte identical public key value does the server proceed with signature verification. Upon successful verification of the signature contained in the third message (and verification that the eight-byte timestamp is current), the server proceeds to send three symmetrical messages: a long term server public key, an ephemeral server public key, and a signature message authenticating the ephemeral key.
At this point, both parties derive the connection key by performing Elliptic Curve Diffie-Hellman using the ephemeral keys, and combining the result with the pre-shared static key using the PBKFD2 algorithm. Note: the pre-shared static key used at this step is different than the pre-shared static key used as the "knock key". The static key used at this step may be different for each user of a particular Oxy server, and is selected based on the long-term client key sent in the first kex message. The use of a static key at this point ensures the protocol is robust against adversaries who are able to quickly undermine the ECDSA or ECDH algorithms (i.e. adversaries with effective quantum computing).
Two distinct static keys are derived from the ECDH Result + PSK: an "Alice" key and a "Bob" key. Both keys are identically derived by both parties, but the use of separate keys prevents initialization vector re-use as both parties transmit data independently. The side of the connection imagined to correspond to a user with a keyboard transmits using the "Alice" key, whereas the non-user side transmits using the "Bob" key.
THE BODY
========
The body of an Oxy connection consists of a series of fixed-size (272 byte) frames of AES-256-GCM data. Each frame consists of 256 bytes of payload data and 16 bytes of Authenticated Encryption tag. Initialization vectors start at zero for each side of the connection, and each side increments its initialization vector counter by one when it transmits a frame.
Inside the decrypted 256 bytes of payload data is one byte that indicates the number of bytes in each frame that is "relevant data". A "saturated" frame contains 255 bytes of relevant data. An "unsaturated" frame consists of less than 255 bytes of relevant data. A protocol message is delivered across any number of saturated frames, terminated by one unsaturated frame. The use of fixed-size frames limits the ability of attackers to derive information from message length, and in some cases prevents attackers from being able to identify underlying message boundaries.
A protocol message is a CBOR (RFC 7049) document corresponding to an enum variant of the OxyMessage enum. Enum variants are described using their variant number - as such, re-ordering variants, or inserting a new variant at any location other than the end of the enumeration constitutes a breaking protocol change. As of this writing, there are 45 established variants.
A full listing of current message variants (with payloads) is included below. This information is copied from the source code file "message.rs":
pub enum OxyMessage {
ProtocolVersionQuery { },
ProtocolVersionAnnounce { version: u64 },
Ping { },
Pong { },
Exit { },
DummyMessage { data: Vec<u8> },
BasicCommand { command: String },
BasicCommandOutput { stdout: Vec<u8>, stderr: Vec<u8> },
PipeCommand { command: String },
PipeCommandOutput { reference: u64, stdout: Vec<u8>, stderr: Vec<u8> },
PipeCommandInput { reference: u64, input: Vec<u8> },
PipeCommandExited { reference: u64 },
Reject { reference: u64, note: String },
Success { reference: u64 },
PtyRequest { command: Option<String> },
PtySizeAdvertisement { w: u16, h: u16 },
PtyInput { data: Vec<u8> },
PtyOutput { data: Vec<u8> },
PtyExited { status: i32 },
DownloadRequest { path: String, offset_start: Option<u64>, offset_end: Option<u64> },
UploadRequest { path: String, filepart: String, offset_start: Option<u64> },
FileData { reference: u64, data: Vec<u8> },
RemoteOpen { addr: String },
RemoteBind { addr: String },
CloseRemoteBind { reference: u64 },
RemoteStreamData { reference: u64, data: Vec<u8> },
LocalStreamData { reference: u64, data: Vec<u8> },
RemoteStreamClosed { reference: u64 },
LocalStreamClosed { reference: u64 },
BindConnectionAccepted { reference: u64 },
TunnelRequest { tap: bool, name: String },
TunnelData { reference: u64, data: Vec<u8> },
StatRequest { path: String },
StatResult { reference: u64, len: u64, is_dir: bool, is_file: bool, owner: String, group: String, octal_permissions: u16, atime: Option<SystemTime>, mtime: Option<SystemTime>, ctime: Option<SystemTime> },
ReadDir { path: String },
ReadDirResult { reference: u64, complete: bool, answers: Vec<String> },
FileHashRequest { path: String, offset_start: Option<u64>, offset_end: Option<u64>, hash_algorithm: u64 },
FileHashData { reference: u64, digest: Vec<u8> },
FileTruncateRequest { path: String, len: u64 },
KnockForward { destination: String, knock: Vec<u8> },
AdvertiseXAuth { cookie: String },
UsernameAdvertisement { username: String },
CompressionRequest { compression_type: u64 },
CompressionStart { compression_type: u64 },
EnvironmentAdvertisement { key: String, value: String},
}
The oxy protocol relies upon at least half in-order message delivery for message semantics. Full in-order delivery is ensured by the TCP protocol, but this is more than is required as each side maintains a separate "Alice" ticker and "Bob" ticker identifying the protocol message number of each message as it is processed. These tickers are similar to the IV tickers used for encryption frames, but spread apart from the IV tickers anytime a protocol message is spread across more than one frame.
In many cases, one protocol message will reference a previous protocol message by number (fields name "reference") in the above listing. For example, suppose Alice has previously sent 50 messages, and Bob has previously sent 100 messages. Then, Alice sends a message such as:
DownloadRequest {
path: "/etc/shadow",
offset_start: None,
offset_end: None,
}
That message will be recorded by Bob as "inbound message 51". Bob may then attempt to open /etc/shadow but be met with a permission denied eror. Bob would then respond:
Reject {
reference: 51,
note: "Permission denied"
}
Oxy is designed to gracefully reject messages with variant numbers larger than all known variants. This permits future extension of the message list without causing a compatibility break. However, it should be noted that this project exists as a break-away from the SSH standard founded on the principal that backwards compatibilty is not a sufficient justification for failings of functionality. As such, this protocol may undergo multiple breaking changes in the future. In particular, key exchange is likely to change from a Ed25519 based mechanism to a supersingular isogeny based method as supersingular isogeny cryptography achieves greater maturity.