diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/Cargo.lock b/Cargo.lock index f90d7bf..1e47858 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,24 +13,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" @@ -40,9 +37,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -59,9 +56,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -75,9 +72,9 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", @@ -97,9 +94,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "generic-array" @@ -113,9 +110,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -124,9 +121,9 @@ dependencies = [ [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -142,36 +139,36 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.147" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "platforms" -version = "3.1.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "ppv-lite86" @@ -181,18 +178,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -253,30 +250,41 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "spin" version = "0.5.2" @@ -291,9 +299,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.37" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -308,6 +316,7 @@ dependencies = [ "hkdf", "rand", "ring", + "sha2", "x25519-dalek", ] @@ -343,9 +352,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -353,9 +362,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -368,9 +377,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -378,9 +387,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -391,15 +400,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -429,9 +438,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "x25519-dalek" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", @@ -441,9 +450,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index d51a008..92d6626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ byteorder = "1.4.3" hkdf = "0.12.3" rand = "0.8.5" ring = "0.16.20" +sha2 = "0.10.8" x25519-dalek = "2.0.0" diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 78b58bd..0000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1692799911, - "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1693985761, - "narHash": "sha256-K5b+7j7Tt3+AqbWkcw+wMeqOAWyCD1MH26FPZyWXpdo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "0bffda19b8af722f8069d09d8b6a24594c80b352", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 1732e52..0000000 --- a/flake.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - in - { - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - llvmPackages_16.clang - llvmPackages_16.bintools - llvmPackages_16.libllvm - llvmPackages_16.lldb - rustup - ]; - # https://github.com/rust-lang/rust-bindgen#environment-variables - LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; - shellHook = '' - export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin - export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ - ''; - # Add glibc, clang, glib and other headers to bindgen search path - BINDGEN_EXTRA_CLANG_ARGS = - (builtins.map (a: ''-I"${a}/include"'') [ - pkgs.glibc.dev - ]) - # Includes with special directory paths - ++ [ - ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' - ''-I"${pkgs.glib.dev}/include/glib-2.0"'' - ''-I${pkgs.glib.out}/lib/glib-2.0/include/'' - ]; - packages = (with pkgs; [ - wireshark - ]); - }; - }); -} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..bcb719f --- /dev/null +++ b/shell.nix @@ -0,0 +1,9 @@ +{ pkgs ? import { } }: pkgs.mkShell { + buildInputs = with pkgs; [ + + ]; + RUST_BACKTRACE = 1; + packages = (with pkgs; [ + wireshark + ]); +} diff --git a/src/crypt.rs b/src/crypt.rs index 2637e63..b952827 100644 --- a/src/crypt.rs +++ b/src/crypt.rs @@ -1,11 +1,55 @@ //! Module for encrypting `TLSPlaintext` records. -use crate::proto; +use crate::proto::{ + self, + ser_de::{proto_enum, proto_struct, Value}, +}; -use ring::aead; +use ring::aead::{self, Nonce}; -struct TlsCiphertext { - encrypted_record: Vec, +pub struct TlsCiphertext { + /// The encrypted [`TlsInnerPlaintext`] record. + pub encrypted_record: Vec, +} +impl From> for TlsCiphertext { + fn from(value: Vec) -> Self { + TlsCiphertext { + encrypted_record: value, + } + } +} + +pub fn compute_keys(shared_secret: SharedSecret) { + let hkdf_expand_label = |secret: &[u8], label: &[u8], context: &[u8], length| { + proto_struct! { + #[derive(Debug)] + pub struct HkdfLabel { + pub length: u16, + pub label: proto::ser_de::List, + pub context: proto::ser_de::List, + } + } + let mut hkdf_label = Vec::new(); + HkdfLabel { + length, + label: { + let mut v = b"tls13 ".to_vec(); + v.extend_from_slice(label); + v.into() + }, + context: context.to_vec().into(), + } + .write(&mut hkdf_label) + .unwrap(); + + // TODO: use correct algo, the cipher suite hash algorithm! + let mut okm = [0u8; 42]; + hkdf::Hkdf::::new(None, secret).expand(&hkdf_label, &mut okm) + }; + + let derive_secret = |secret: &[u8], label: &[u8], messages: ()| { + hkdf_expand_label(secret, label, &[], 16) // todo: fix length + }; } pub struct AeadKey { @@ -16,12 +60,46 @@ impl AeadKey { fn new(algorithm: proto::CipherSuite, key_bytes: &[u8]) -> Self { Self { key: aead::LessSafeKey::new( - aead::UnboundKey::new(proto_algo_to_ring(algorithm), key_bytes).expect("invalid key"), + aead::UnboundKey::new(proto_algo_to_ring(algorithm), key_bytes) + .expect("invalid key"), ), } } } +mod seq { + use std::cell::Cell; + + use ring::aead::{self, Nonce}; + + /// The sequence ID generator. + /// There is a separate one maintained for reading and writing. + pub struct SeqIdGen { + next: Cell, + } + impl SeqIdGen { + pub fn new() -> SeqIdGen { + SeqIdGen { next: Cell::new(0) } + } + pub fn next(&self) -> SeqId { + let next = self.next.get(); + self.next.set(next.checked_add(1).unwrap()); + SeqId(next) + } + } + // Don't implement `Clone` to ensure every seq id is only used once. + pub struct SeqId(u64); + impl SeqId { + pub fn to_nonce(self) -> Nonce { + let mut nonce = [0; aead::NONCE_LEN]; + nonce[4..].copy_from_slice(&self.0.to_be_bytes()); + Nonce::assume_unique_for_key(nonce) + } + } +} +pub use seq::{SeqId, SeqIdGen}; +use x25519_dalek::SharedSecret; + fn proto_algo_to_ring(algo: proto::CipherSuite) -> &'static aead::Algorithm { match algo { proto::CipherSuite::TLS_AES_128_GCM_SHA256 => &aead::AES_128_GCM, @@ -33,15 +111,31 @@ fn proto_algo_to_ring(algo: proto::CipherSuite) -> &'static aead::Algorithm { } fn encrypt(key: AeadKey, message: &[u8], seq: u8) -> Vec { - let total_len = message.len() + key.key.algorithm().tag_len(); + let total_len = message.len() + key.key.algorithm().tag_len(); let mut ciphertext_payload = Vec::with_capacity(total_len); ciphertext_payload.extend_from_slice(message); - + // FIXME: dont use zero obviously let nonce = aead::Nonce::assume_unique_for_key([0; aead::NONCE_LEN]); // FIXME: fill out the AAD properly let aad = aead::Aad::from([0; 5]); - key.key.seal_in_place_append_tag(nonce, aad, &mut ciphertext_payload).unwrap(); + key.key + .seal_in_place_append_tag(nonce, aad, &mut ciphertext_payload) + .unwrap(); ciphertext_payload } + +pub struct TlsInnerPlaintext { + /// The `TLSPlaintext.fragment`` value + pub content: Vec, + /// The `TLSPlaintext.type` value + pub content_type: u8, + pub padding_len: u16, +} + +impl TlsCiphertext { + pub fn decrypt(&self, key: AeadKey, nonce: Nonce) -> TlsInnerPlaintext { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index 4e20971..66f072d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,10 +2,14 @@ mod crypt; pub mod proto; use std::{ + cell::RefCell, fmt::Debug, io::{self, Read, Write}, }; +use crypt::{SeqId, SeqIdGen}; +use proto::CipherSuite; + use crate::proto::TLSPlaintext; type Result = std::result::Result; @@ -16,172 +20,324 @@ pub struct ClientConnection { impl ClientConnection { pub fn establish(w: W, host: &str) -> Result { - let _setup = ClientSetupConnection::establish(w, host)?; + let mut setup = ClientSetupConnection { + stream: StreamState::new(w), + }; + let _setup = setup.establish(host)?; todo!() } } struct ClientSetupConnection { - _w: W, + stream: StreamState, } +mod stream_state { + use std::io::{Read, Write}; + + use crate::crypt::{SeqId, SeqIdGen}; + use crate::proto::{self, TLSPlaintext}; + use crate::Result; + + pub struct StreamState { + stream: W, + read_seq_id: SeqIdGen, + write_seq_id: SeqIdGen, + } + impl StreamState { + pub fn new(stream: W) -> Self { + Self { + stream, + read_seq_id: SeqIdGen::new(), + write_seq_id: SeqIdGen::new(), + } + } + pub fn write_flush_record(&mut self, plaintext: TLSPlaintext) -> Result<()> { + self.write_record(plaintext)?; + self.stream.flush()?; + Ok(()) + } + + pub fn write_record(&mut self, plaintext: TLSPlaintext) -> Result { + plaintext.write(&mut self.stream)?; + self.stream.flush()?; + Ok(self.write_seq_id.next()) + } + + pub fn read_record(&mut self) -> Result<(TLSPlaintext, SeqId)> { + let seq_id = self.read_seq_id.next(); + let frame = proto::TLSPlaintext::read(&mut self.stream)?; + Ok((frame, seq_id)) + } + } +} +use stream_state::StreamState; + macro_rules! unexpected_message { ($($tt:tt)*) => { Err(ErrorKind::UnexpectedMessage(format!($($tt)*)).into()) }; } +/** +https://datatracker.ietf.org/doc/html/rfc8446#appendix-A.1 + +```text + START <----+ + Send ClientHello | | Recv HelloRetryRequest + [K_send = early data] | | + v | + / WAIT_SH ----+ + | | Recv ServerHello + | | K_recv = handshake + Can | V + send | WAIT_EE + early | | Recv EncryptedExtensions + data | +--------+--------+ + | Using | | Using certificate + | PSK | v + | | WAIT_CERT_CR + | | Recv | | Recv CertificateRequest + | | Certificate | v + | | | WAIT_CERT + | | | | Recv Certificate + | | v v + | | WAIT_CV + | | | Recv CertificateVerify + | +> WAIT_FINISHED <+ + | | Recv Finished + \ | [Send EndOfEarlyData] + | K_send = handshake + | [Send Certificate [+ CertificateVerify]] + Can send | Send Finished + app data --> | K_send = K_recv = application + after here v + CONNECTED +``` +*/ +enum ConnectState { + Start, + WaitServerHello { + legacy_session_id: [u8; 32], + secret: RefCell>, + cipher_suites: Vec, + }, + WaitEncryptedExtensions, + WaitCertificateRequest, + WaitCertificate, + WaitCertificateVerify, + WaitFinished, + Connected, +} + impl ClientSetupConnection { - fn establish(mut stream: W, host: &str) -> Result { - let secret = x25519_dalek::EphemeralSecret::random_from_rng(rand::thread_rng()); - let public = x25519_dalek::PublicKey::from(&secret); + fn establish(&mut self, host: &str) -> Result { + let mut state = ConnectState::Start; - let legacy_session_id = rand::random::<[u8; 32]>(); - let cipher_suites = vec![proto::CipherSuite::TLS_AES_128_GCM_SHA256]; + loop { + let next_state = match &state { + ConnectState::Start => { + // https://datatracker.ietf.org/doc/html/rfc8446#section-2 + let secret = x25519_dalek::EphemeralSecret::random_from_rng(rand::thread_rng()); + let public = x25519_dalek::PublicKey::from(&secret); - let handshake = proto::Handshake::ClientHello { - legacy_version: proto::LEGACY_TLSV12, - random: rand::random(), - legacy_session_id: legacy_session_id.to_vec().into(), - cipher_suites: cipher_suites.clone().into(), - legacy_compressions_methods: vec![0].into(), - extensions: vec![ - proto::ExtensionCH::ServerName { - server_name: vec![proto::ServerName::HostName { - host_name: host.as_bytes().to_vec().into(), - }] - .into(), - }, - proto::ExtensionCH::ECPointFormat { - formats: vec![proto::ECPointFormat::Uncompressed].into(), - }, - proto::ExtensionCH::SupportedGroups { - groups: vec![proto::NamedGroup::X25519].into(), - }, - proto::ExtensionCH::KeyShare { - entries: vec![proto::KeyShareEntry::X25519 { - len: public.as_bytes().len().try_into().unwrap(), - key_exchange: *public.as_bytes(), - }] - .into(), - }, - proto::ExtensionCH::SignatureAlgorithms { - supported_signature_algorithms: vec![ - proto::SignatureScheme::ED25519, - proto::SignatureScheme::ED448, - proto::SignatureScheme::ECDSA_SECP256R1_SHA256, - proto::SignatureScheme::ECDSA_SECP384R1_SHA384, - proto::SignatureScheme::ECDSA_SECP521R1_SHA512, - proto::SignatureScheme::RSA_PSS_PSS_SHA256, - proto::SignatureScheme::RSA_PSS_PSS_SHA384, - proto::SignatureScheme::RSA_PSS_PSS_SHA512, - proto::SignatureScheme::RSA_PSS_RSAE_SHA256, - proto::SignatureScheme::RSA_PSS_RSAE_SHA384, - proto::SignatureScheme::RSA_PSS_RSAE_SHA512, - ] - .into(), - }, - proto::ExtensionCH::SupportedVersions { - versions: vec![proto::TLSV13].into(), - }, - ] - .into(), - }; - let plaintext = proto::TLSPlaintext::Handshake { handshake }; - plaintext.write(&mut stream)?; - stream.flush()?; + let legacy_session_id = rand::random::<[u8; 32]>(); + let cipher_suites = vec![proto::CipherSuite::TLS_AES_128_GCM_SHA256]; - let out = proto::TLSPlaintext::read(&mut stream)?; - dbg!(&out); - - let proto::TLSPlaintext::Handshake { - handshake: - proto::Handshake::ServerHello { - legacy_version, - random, - legacy_session_id_echo, - cipher_suite, - legacy_compression_method, - extensions, - }, - } = out - else { - return Err( - ErrorKind::UnexpectedMessage(format!("expected ServerHello, got {out:?}")).into(), - ); - }; - - if random.is_hello_retry_request() { - return Err(ErrorKind::HelloRetryRequest.into()); - } - - if legacy_version != proto::LEGACY_TLSV12 { - return unexpected_message!( - "unexpected TLS version in legacy_version field: {legacy_version:x?}" - ); - } - - if legacy_session_id_echo.as_ref() != legacy_session_id { - return unexpected_message!( - "server did not echo the legacy_session_id: {legacy_session_id_echo:?}" - ); - } - - if !cipher_suites.contains(&cipher_suite) { - return unexpected_message!( - "cipher suite from server not sent in client hello: {cipher_suite:?}" - ); - } - - if legacy_compression_method != 0 { - return unexpected_message!( - "legacy compression method MUST be zero: {legacy_compression_method}" - ); - } - - let mut supported_versions = false; - let mut server_key = None; - - for ext in extensions.as_ref() { - match ext { - proto::ExtensionSH::PreSharedKey => todo!(), - proto::ExtensionSH::SupportedVersions { selected_version } => { - if *selected_version != proto::TLSV13 { - return unexpected_message!("server returned non-TLS 1.3 version: {selected_version}"); + let handshake = proto::Handshake::ClientHello { + legacy_version: proto::LEGACY_TLSV12, + random: rand::random(), + legacy_session_id: legacy_session_id.to_vec().into(), + cipher_suites: cipher_suites.clone().into(), + legacy_compressions_methods: vec![0].into(), + extensions: vec![ + proto::ExtensionCH::ServerName { + server_name: vec![proto::ServerName::HostName { + host_name: host.as_bytes().to_vec().into(), + }] + .into(), + }, + proto::ExtensionCH::ECPointFormat { + formats: vec![proto::ECPointFormat::Uncompressed].into(), + }, + proto::ExtensionCH::SupportedGroups { + groups: vec![proto::NamedGroup::X25519].into(), + }, + proto::ExtensionCH::KeyShare { + entries: vec![proto::KeyShareEntry::X25519 { + len: public.as_bytes().len().try_into().unwrap(), + key_exchange: *public.as_bytes(), + }] + .into(), + }, + proto::ExtensionCH::SignatureAlgorithms { + supported_signature_algorithms: vec![ + proto::SignatureScheme::ED25519, + proto::SignatureScheme::ED448, + proto::SignatureScheme::ECDSA_SECP256R1_SHA256, + proto::SignatureScheme::ECDSA_SECP384R1_SHA384, + proto::SignatureScheme::ECDSA_SECP521R1_SHA512, + proto::SignatureScheme::RSA_PSS_PSS_SHA256, + proto::SignatureScheme::RSA_PSS_PSS_SHA384, + proto::SignatureScheme::RSA_PSS_PSS_SHA512, + proto::SignatureScheme::RSA_PSS_RSAE_SHA256, + proto::SignatureScheme::RSA_PSS_RSAE_SHA384, + proto::SignatureScheme::RSA_PSS_RSAE_SHA512, + ] + .into(), + }, + proto::ExtensionCH::SupportedVersions { + versions: vec![proto::TLSV13].into(), + }, + ] + .into(), + }; + let plaintext = proto::TLSPlaintext::Handshake { handshake }; + self.stream.write_flush_record(plaintext)?; + ConnectState::WaitServerHello { + legacy_session_id, + secret: RefCell::new(Some(secret)), + cipher_suites, } - supported_versions = true; - }, - proto::ExtensionSH::Cookie { .. } => todo!(), - proto::ExtensionSH::KeyShare { key_share } => { - let entry = key_share.unwrap_server_hello(); - match entry { - proto::KeyShareEntry::X25519 { len, key_exchange } => { - if *len != 32 { - return unexpected_message!("key length for X25519 key share must be 32: {len}"); + } + ConnectState::WaitServerHello { + legacy_session_id, + secret, + cipher_suites, + } => { + let (frame, seq_id) = self.stream.read_record()?; + if frame.should_drop() { + continue; + } + let proto::TLSPlaintext::Handshake { + handshake: + proto::Handshake::ServerHello { + legacy_version, + random, + legacy_session_id_echo, + cipher_suite, + legacy_compression_method, + extensions, + }, + } = frame + else { + return unexpected_message!("expected ServerHello, got {frame:?}"); + }; + + if random.is_hello_retry_request() { + return Err(ErrorKind::HelloRetryRequest.into()); + } + + if legacy_version != proto::LEGACY_TLSV12 { + return unexpected_message!( + "unexpected TLS version in legacy_version field: {legacy_version:x?}" + ); + } + + if legacy_session_id_echo.as_ref() != legacy_session_id { + return unexpected_message!( + "server did not echo the legacy_session_id: {legacy_session_id_echo:?}" + ); + } + + if !cipher_suites.contains(&cipher_suite) { + return unexpected_message!( + "cipher suite from server not sent in client hello: {cipher_suite:?}" + ); + } + + if legacy_compression_method != 0 { + return unexpected_message!( + "legacy compression method MUST be zero: {legacy_compression_method}" + ); + } + + let mut supported_versions = false; + let mut server_key = None; + + for ext in extensions.as_ref() { + match ext { + proto::ExtensionSH::PreSharedKey => todo!(), + proto::ExtensionSH::SupportedVersions { selected_version } => { + if *selected_version != proto::TLSV13 { + return unexpected_message!( + "server returned non-TLS 1.3 version: {selected_version}" + ); + } + supported_versions = true; } - server_key = Some(key_exchange); - }, + proto::ExtensionSH::Cookie { .. } => todo!(), + proto::ExtensionSH::KeyShare { key_share } => { + let entry = key_share.unwrap_server_hello(); + match entry { + proto::KeyShareEntry::X25519 { len, key_exchange } => { + if *len != 32 { + return unexpected_message!( + "key length for X25519 key share must be 32: {len}" + ); + } + server_key = Some(key_exchange); + } + } + } + } } - }, - } + + if !supported_versions { + return unexpected_message!( + "server did not send supported_versions extension" + ); + } + + let Some(server_key) = server_key else { + return unexpected_message!("server did not send its key"); + }; + let server_key = x25519_dalek::PublicKey::from(*server_key); + let dh_shared_secret = secret + .borrow_mut() + .take() + .unwrap() + .diffie_hellman(&server_key); + + println!( + "we have established a shared secret. dont leak it!! anyways here is it: {:x?}", + dh_shared_secret.as_bytes() + ); + + crypt::compute_keys(dh_shared_secret); + + ConnectState::WaitEncryptedExtensions + } + ConnectState::WaitEncryptedExtensions => { + let (frame, seq_id) = self.stream.read_record()?; + if frame.should_drop() { + continue; + } + let proto::TLSPlaintext::ApplicationData { data } = frame else { + return unexpected_message!("expected ApplicationData, got {frame:?}"); + }; + //crypt::TlsCiphertext::from(data).decrypt(key, seq_id.to_nonce()); + + todo!() + } + ConnectState::WaitCertificateRequest => todo!(), + ConnectState::WaitCertificate => todo!(), + ConnectState::WaitCertificateVerify => todo!(), + ConnectState::WaitFinished => todo!(), + ConnectState::Connected => todo!(), + }; + state = next_state; } + } +} - if !supported_versions { - return unexpected_message!("server did not send supported_versions extension"); +impl TLSPlaintext { + fn should_drop(&self) -> bool { + match self { + TLSPlaintext::ChangeCipherSpec { data: body } if body.as_ref() == &[0x01] => true, + _ => false, } - - let Some(server_key) = server_key else { - return unexpected_message!("server did not send its key"); - }; - let server_key = x25519_dalek::PublicKey::from(*server_key); - let dh_shared_secret = secret.diffie_hellman(&server_key); - - println!("we have established a shared secret. dont leak it!! anywhere here is it: {:x?}", dh_shared_secret.as_bytes()); - - dbg!(proto::TLSPlaintext::read(&mut stream))?; - - todo!() } } @@ -201,16 +357,16 @@ pub enum ErrorKind { impl From for Error { fn from(value: io::Error) -> Self { panic!("error: {value}"); - Self { - kind: ErrorKind::Io(value), - } + //Self { + // kind: ErrorKind::Io(value), + //} } } impl From for Error { fn from(value: ErrorKind) -> Self { panic!("error:{value:?}"); - Self { kind: value } + //Self { kind: value } } } diff --git a/src/main.rs b/src/main.rs index a2f1644..2da4619 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,6 @@ use std::net::TcpStream; // An example program that makes a shitty HTTP/1.1 request. fn main() { - let conn = tls::LoggingWriter(TcpStream::connect(("nilstrieb.dev", 443)).unwrap()); - tls::ClientConnection::establish(conn, "nilstrieb.dev").unwrap(); + let conn = tls::LoggingWriter(TcpStream::connect(("vps1.nilstrieb.dev", 443)).unwrap()); + tls::ClientConnection::establish(conn, "vps1.nilstrieb.dev").unwrap(); } diff --git a/src/proto.rs b/src/proto.rs index 3d4872c..f1a2c46 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -15,16 +15,26 @@ pub enum TLSPlaintext { legacy_version: ProtocolVersion, fragment: List, }, - /// This only exists for compatibility and must be sent immediately before the second flight. + /// This only exists for compatibility and must be sent immediately before the second flight. /// If this is received with the single byte value `0x01`, then it should just be dropped. - ChangeCipherSpec, + /// + /// An implementation may receive an unencrypted record of type + /// change_cipher_spec consisting of the single byte value 0x01 at any + /// time after the first ClientHello message has been sent or received + /// and before the peer's Finished message has been received and MUST + /// simply drop it without further processing. + ChangeCipherSpec { + data: List, + }, Alert { alert: Alert, }, Handshake { handshake: Handshake, }, - ApplicationData, + ApplicationData { + data: Vec, + }, } impl TLSPlaintext { @@ -36,9 +46,24 @@ impl TLSPlaintext { pub fn write(&self, w: &mut impl Write) -> io::Result<()> { match self { - TLSPlaintext::Invalid { .. } => todo!(), - TLSPlaintext::ChangeCipherSpec => todo!(), - TLSPlaintext::Alert { .. } => todo!(), + TLSPlaintext::Invalid { .. } => { + Self::INVALID.write(w)?; + LEGACY_TLSV12.write(w)?; + + todo!() + } + TLSPlaintext::ChangeCipherSpec { data: body } => { + Self::CHANGE_CIPHER_SPEC.write(w)?; + LEGACY_TLSV12.write(w)?; + body.write(w)?; + Ok(()) + } + TLSPlaintext::Alert { .. } => { + Self::ALERT.write(w)?; + LEGACY_TLSV12.write(w)?; + + todo!() + } TLSPlaintext::Handshake { handshake } => { Self::HANDSHAKE.write(w)?; // MUST be set to 0x0303 for all records @@ -55,7 +80,14 @@ impl TLSPlaintext { handshake.write(w)?; Ok(()) } - TLSPlaintext::ApplicationData => todo!(), + TLSPlaintext::ApplicationData { data } => { + Self::APPLICATION_DATA.write(w)?; + LEGACY_TLSV12.write(w)?; + let len: u16 = data.len().try_into().unwrap(); + len.write(w)?; + w.write_all(&data)?; + Ok(()) + } } } @@ -63,10 +95,19 @@ impl TLSPlaintext { let r = &mut FrameReader::new(r); let discr = u8::read(r)?; let _legacy_version = ProtocolVersion::read(r)?; - let _len = u16::read(r)?; + let len = u16::read(r)?; + if len > 2_u16.pow(14) { + return Err(crate::ErrorKind::InvalidFrame(Box::new(format!( + "TLSPLaintext opaque length exceeds 2^14: {len}" + ))) + .into()); + } match discr { Self::INVALID => todo!(), - Self::CHANGE_CIPHER_SPEC => todo!(), + Self::CHANGE_CIPHER_SPEC => { + let data = List::read_for_byte_length(len.into(), r)?; + Ok(Self::ChangeCipherSpec { data }) + } Self::ALERT => { let alert = Alert::read(r)?; Ok(Self::Alert { alert }) @@ -75,7 +116,12 @@ impl TLSPlaintext { let handshake = Handshake::read(r)?; Ok(TLSPlaintext::Handshake { handshake }) } - Self::APPLICATION_DATA => todo!(), + Self::APPLICATION_DATA => { + let data = List::::read_for_byte_length(len.into(), r)?; + Ok(Self::ApplicationData { + data: data.into_inner(), + }) + } _ => { return Err(crate::ErrorKind::InvalidFrame(Box::new(format!( "Invalid record discriminant: {discr}" @@ -86,6 +132,16 @@ impl TLSPlaintext { } } +// https://datatracker.ietf.org/doc/html/rfc8446#section-5.2 +proto_struct! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct TlsCiphertext { + pub opaque_type: u8, + pub legacy_record_version: u8, + pub encrypted_record: List, + } +} + pub type ProtocolVersion = u16; pub type Random = [u8; 32]; @@ -220,7 +276,6 @@ proto_enum! { } } - proto_enum! { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ServerName: u8 { @@ -385,7 +440,9 @@ pub enum ServerHelloKeyshare { impl ServerHelloKeyshare { pub fn unwrap_server_hello(&self) -> &KeyShareEntry { match self { - Self::HelloRetryRequest(_) => panic!("unexpected hello retry request, expected server hello"), + Self::HelloRetryRequest(_) => { + panic!("unexpected hello retry request, expected server hello") + } Self::ServerHello(entry) => entry, } } diff --git a/src/proto/ser_de.rs b/src/proto/ser_de.rs index b611274..2284c4a 100644 --- a/src/proto/ser_de.rs +++ b/src/proto/ser_de.rs @@ -53,14 +53,14 @@ macro_rules! proto_struct { impl crate::proto::ser_de::Value for $name { - fn write(&self, mut w: &mut W) -> io::Result<()> { + fn write(&self, mut w: &mut W) -> std::io::Result<()> { $( crate::proto::ser_de::Value::write(&self.$field_name, &mut w)?; )* Ok(()) } - fn read(r: &mut $crate::proto::ser_de::FrameReader) -> crate::Result { + fn read(r: &mut $crate::proto::ser_de::FrameReader) -> crate::Result { let ( $( $field_name ),* ) = ($( { crate::proto::ser_de::discard!($field_name); crate::proto::ser_de::Value::read(r)? } ),*); Ok(Self { @@ -248,17 +248,8 @@ impl Debug for List { impl + TryFrom + Default> Value for List { fn read(r: &mut FrameReader) -> crate::Result { - eprintln!("reading a list"); - let mut remaining_byte_size = Len::read(r)?.into(); - let mut v = Vec::new(); - eprintln!("list.. remaining bytes {remaining_byte_size}"); - while remaining_byte_size > 0 { - eprintln!("things {remaining_byte_size} {v:?}"); - let value = T::read(r)?; - remaining_byte_size -= value.byte_size(); - v.push(value); - } - Ok(Self(v, PhantomData)) + let remaining_byte_size = Len::read(r)?.into(); + Self::read_for_byte_length(remaining_byte_size, r) } fn write(&self, w: &mut W) -> io::Result<()> { let byte_size = self.0.iter().map(Value::byte_size).sum::(); @@ -278,6 +269,22 @@ impl + TryFrom + Default> Value for Li } } +impl + TryFrom + Default> List { + pub fn read_for_byte_length(mut remaining_byte_size: usize, r: &mut FrameReader) -> crate::Result { + let mut v = Vec::new(); + while remaining_byte_size > 0 { + let value = T::read(r)?; + remaining_byte_size -= value.byte_size(); + v.push(value); + } + Ok(Self(v, PhantomData)) + } + + pub fn into_inner(self) -> Vec { + self.0 + } +} + pub trait Value: Sized + std::fmt::Debug { fn write(&self, w: &mut W) -> io::Result<()>; fn read(r: &mut FrameReader) -> crate::Result;