From af63ad4870b1d2660c9b7896a3e7af307b469775 Mon Sep 17 00:00:00 2001 From: Kris Kwiatkowski Date: Sat, 21 Feb 2026 08:05:44 +0000 Subject: [PATCH] Init --- .gitignore | 1 + Cargo.lock | 1755 +++++++++++++++++ Cargo.toml | 60 + src/alert.rs | 121 ++ src/application_data.rs | 30 + src/asynch.rs | 541 +++++ src/blocking.rs | 525 +++++ src/buffer.rs | 304 +++ src/cert_verify.rs | 296 +++ src/certificate.rs | 160 ++ src/change_cipher_spec.rs | 37 + src/cipher.rs | 15 + src/cipher_suites.rs | 26 + src/common/decrypted_buffer_info.rs | 24 + src/common/decrypted_read_handler.rs | 64 + src/common/mod.rs | 2 + src/config.rs | 421 ++++ src/connection.rs | 636 ++++++ src/content_types.rs | 22 + src/extensions/extension_data/alpn.rs | 56 + src/extensions/extension_data/key_share.rs | 135 ++ .../extension_data/max_fragment_length.rs | 44 + src/extensions/extension_data/mod.rs | 11 + .../extension_data/pre_shared_key.rs | 63 + .../extension_data/psk_key_exchange_modes.rs | 53 + src/extensions/extension_data/server_name.rs | 133 ++ .../extension_data/signature_algorithms.rs | 164 ++ .../signature_algorithms_cert.rs | 34 + .../extension_data/supported_groups.rs | 104 + .../extension_data/supported_versions.rs | 65 + .../extension_data/unimplemented.rs | 24 + src/extensions/extension_group_macro.rs | 102 + src/extensions/messages.rs | 106 + src/extensions/mod.rs | 80 + src/fmt.rs | 223 +++ src/handshake/binder.rs | 39 + src/handshake/certificate.rs | 174 ++ src/handshake/certificate_request.rs | 50 + src/handshake/certificate_verify.rs | 56 + src/handshake/client_hello.rs | 188 ++ src/handshake/encrypted_extensions.rs | 19 + src/handshake/finished.rs | 52 + src/handshake/mod.rs | 241 +++ src/handshake/new_session_ticket.rs | 33 + src/handshake/server_hello.rs | 83 + src/key_schedule.rs | 499 +++++ src/lib.rs | 169 ++ src/native_pki.rs | 468 +++++ src/parse_buffer.rs | 173 ++ src/read_buffer.rs | 180 ++ src/record.rs | 224 +++ src/record_reader.rs | 479 +++++ src/send_policy.rs | 37 + src/write_buffer.rs | 287 +++ tests/common/mod.rs | 369 ++++ tests/fixtures/chain.pem | 21 + tests/fixtures/intermediate-ca-key.pem | 5 + tests/fixtures/intermediate-ca.pem | 11 + tests/fixtures/intermediate-server-key.pem | 5 + tests/fixtures/intermediate-server.pem | 10 + tests/fixtures/leaf-client-key.pem | 5 + tests/fixtures/leaf-client.pem | 13 + tests/fixtures/leaf-server-key.pem | 5 + tests/fixtures/leaf-server.pem | 10 + tests/fixtures/root-ca-key.pem | 5 + tests/fixtures/root-ca.pem | 13 + tests/fixtures/root-ca.srl | 1 + tests/fixtures/rsa-leaf-client-key.pem | 28 + tests/fixtures/rsa-leaf-client.pem | 19 + tests/fixtures/rsa-leaf-server-key.pem | 28 + tests/fixtures/rsa-leaf-server.pem | 18 + tests/fixtures/rsa-root-ca-key.pem | 28 + tests/fixtures/rsa-root-ca.pem | 21 + tests/fixtures/setup_fixtures.sh | 66 + tests/integration.rs | 1305 ++++++++++++ 75 files changed, 11874 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/alert.rs create mode 100644 src/application_data.rs create mode 100644 src/asynch.rs create mode 100644 src/blocking.rs create mode 100644 src/buffer.rs create mode 100644 src/cert_verify.rs create mode 100644 src/certificate.rs create mode 100644 src/change_cipher_spec.rs create mode 100644 src/cipher.rs create mode 100644 src/cipher_suites.rs create mode 100644 src/common/decrypted_buffer_info.rs create mode 100644 src/common/decrypted_read_handler.rs create mode 100644 src/common/mod.rs create mode 100644 src/config.rs create mode 100644 src/connection.rs create mode 100644 src/content_types.rs create mode 100644 src/extensions/extension_data/alpn.rs create mode 100644 src/extensions/extension_data/key_share.rs create mode 100644 src/extensions/extension_data/max_fragment_length.rs create mode 100644 src/extensions/extension_data/mod.rs create mode 100644 src/extensions/extension_data/pre_shared_key.rs create mode 100644 src/extensions/extension_data/psk_key_exchange_modes.rs create mode 100644 src/extensions/extension_data/server_name.rs create mode 100644 src/extensions/extension_data/signature_algorithms.rs create mode 100644 src/extensions/extension_data/signature_algorithms_cert.rs create mode 100644 src/extensions/extension_data/supported_groups.rs create mode 100644 src/extensions/extension_data/supported_versions.rs create mode 100644 src/extensions/extension_data/unimplemented.rs create mode 100644 src/extensions/extension_group_macro.rs create mode 100644 src/extensions/messages.rs create mode 100644 src/extensions/mod.rs create mode 100644 src/fmt.rs create mode 100644 src/handshake/binder.rs create mode 100644 src/handshake/certificate.rs create mode 100644 src/handshake/certificate_request.rs create mode 100644 src/handshake/certificate_verify.rs create mode 100644 src/handshake/client_hello.rs create mode 100644 src/handshake/encrypted_extensions.rs create mode 100644 src/handshake/finished.rs create mode 100644 src/handshake/mod.rs create mode 100644 src/handshake/new_session_ticket.rs create mode 100644 src/handshake/server_hello.rs create mode 100644 src/key_schedule.rs create mode 100644 src/lib.rs create mode 100644 src/native_pki.rs create mode 100644 src/parse_buffer.rs create mode 100644 src/read_buffer.rs create mode 100644 src/record.rs create mode 100644 src/record_reader.rs create mode 100644 src/send_policy.rs create mode 100644 src/write_buffer.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/fixtures/chain.pem create mode 100644 tests/fixtures/intermediate-ca-key.pem create mode 100644 tests/fixtures/intermediate-ca.pem create mode 100644 tests/fixtures/intermediate-server-key.pem create mode 100644 tests/fixtures/intermediate-server.pem create mode 100644 tests/fixtures/leaf-client-key.pem create mode 100644 tests/fixtures/leaf-client.pem create mode 100644 tests/fixtures/leaf-server-key.pem create mode 100644 tests/fixtures/leaf-server.pem create mode 100644 tests/fixtures/root-ca-key.pem create mode 100644 tests/fixtures/root-ca.pem create mode 100644 tests/fixtures/root-ca.srl create mode 100644 tests/fixtures/rsa-leaf-client-key.pem create mode 100644 tests/fixtures/rsa-leaf-client.pem create mode 100644 tests/fixtures/rsa-leaf-server-key.pem create mode 100644 tests/fixtures/rsa-leaf-server.pem create mode 100644 tests/fixtures/rsa-root-ca-key.pem create mode 100644 tests/fixtures/rsa-root-ca.pem create mode 100755 tests/fixtures/setup_fixtures.sh create mode 100644 tests/integration.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b3c7f9d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1755 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.7", + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "der_derive", + "heapless 0.9.2", + "time", +] + +[[package]] +name = "der_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2163a0e204a148662b6b6816d4b5d5668a5f2f8df498ccbd5cd0e864e78fecba" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid 0.9.6", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.7", + "group", + "hkdf", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" +dependencies = [ + "defmt", +] + +[[package]] +name = "embedded-io-adapters" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900a1c087f1f7d17cdc84a1290df91521cd90933efa76d68e568385d889f2f4" +dependencies = [ + "embedded-io", + "embedded-io-async", + "tokio", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +dependencies = [ + "as-slice", + "generic-array 0.14.7", + "hash32 0.1.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "defmt", + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mote-tls" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "const-oid 0.10.2", + "defmt", + "der 0.8.0", + "digest", + "ecdsa", + "ed25519-dalek", + "embedded-io", + "embedded-io-adapters", + "embedded-io-async", + "env_logger", + "generic-array 0.14.7", + "heapless 0.6.1", + "heapless 0.9.2", + "hkdf", + "hmac", + "log", + "mio 0.8.11", + "openssl", + "p256", + "p384", + "pem-parser", + "portable-atomic", + "rand", + "rand_core", + "rsa", + "rustls", + "rustls-pemfile", + "rustls-webpki", + "serde", + "sha2", + "signature", + "tokio", + "typenum", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443598a432c1c2dc0ad1b98e160b51caa94380894f09f932de45845527bd7ad0" +dependencies = [ + "regex", + "rustc-serialize", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array 0.14.7", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio 1.1.1", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..42900f4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "mote-tls" +version = "0.1.0" +edition = "2024" +description = "TLS 1.3 client with no_std and no allocator support" +license = "Apache-2.0" +keywords = ["async", "tls", "no_std", "bare-metal", "network"] + +[dependencies] +portable-atomic = { version = "1.6.0", default-features = false } +p256 = { version = "0.13", default-features = false, features = [ "ecdh", "ecdsa", "sha256" ] } +p384 = { version = "0.13", default-features = false, features = [ "ecdsa", "sha384" ], optional = true } +ed25519-dalek = { version = "2.2", default-features = false, optional = true } +rsa = { version = "0.9.9", default-features = false, features = ["sha2"], optional = true } +rand_core = { version = "0.6.3", default-features = false } +hkdf = "0.12.3" +hmac = "0.12.1" +sha2 = { version = "0.10.2", default-features = false } +aes-gcm = { version = "0.10.1", default-features = false, features = ["aes"] } +digest = { version = "0.10.3", default-features = false, features = [ "core-api" ] } +typenum = { version = "1.15.0", default-features = false } +heapless = { version = "0.9", default-features = false } +heapless_typenum = { package = "heapless", version = "0.6", default-features = false } +embedded-io = "0.7" +embedded-io-async = "0.7" +embedded-io-adapters = { version = "0.7", optional = true } +generic-array = { version = "0.14", default-features = false } +webpki = { package = "rustls-webpki", version = "0.101.7", default-features = false, optional = true } +const-oid = { version = "0.10.1", optional = true } +der = { version = "0.8.0-rc.2", features = ["derive", "oid", "time", "heapless"], optional = true } +signature = { version = "2.2", default-features = false } +ecdsa = { version = "0.16.9", default-features = false } + +# Logging alternatives +log = { version = "0.4", optional = true } +defmt = { version = "1.0.1", optional = true } + +[dev-dependencies] +env_logger = "0.11" +tokio = { version = "1", features = ["full"] } +mio = { version = "0.8.3", features = ["os-poll", "net"] } +rustls = "0.21.6" +rustls-pemfile = "1.0" +serde = { version = "1.0", features = ["derive"] } +rand = "0.8" +log = "0.4" +pem-parser = "0.1.1" +openssl = "0.10.44" + +[features] +default = ["std", "log", "tokio"] +defmt = ["dep:defmt", "embedded-io/defmt", "heapless/defmt"] +std = ["embedded-io/std", "embedded-io-async/std"] +tokio = ["embedded-io-adapters/tokio-1"] +alloc = [] +webpki = ["dep:webpki"] +native-pki = ["dep:der","dep:const-oid"] +rsa = ["dep:rsa", "native-pki", "alloc"] +ed25519 = ["dep:ed25519-dalek", "native-pki"] +p384 = ["dep:p384", "native-pki"] diff --git a/src/alert.rs b/src/alert.rs new file mode 100644 index 0000000..241a58c --- /dev/null +++ b/src/alert.rs @@ -0,0 +1,121 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use crate::parse_buffer::ParseBuffer; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AlertLevel { + Warning = 1, + Fatal = 2, +} + +impl AlertLevel { + #[must_use] + pub fn of(num: u8) -> Option { + match num { + 1 => Some(AlertLevel::Warning), + 2 => Some(AlertLevel::Fatal), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AlertDescription { + CloseNotify = 0, + UnexpectedMessage = 10, + BadRecordMac = 20, + RecordOverflow = 22, + HandshakeFailure = 40, + BadCertificate = 42, + UnsupportedCertificate = 43, + CertificateRevoked = 44, + CertificateExpired = 45, + CertificateUnknown = 46, + IllegalParameter = 47, + UnknownCa = 48, + AccessDenied = 49, + DecodeError = 50, + DecryptError = 51, + ProtocolVersion = 70, + InsufficientSecurity = 71, + InternalError = 80, + InappropriateFallback = 86, + UserCanceled = 90, + MissingExtension = 109, + UnsupportedExtension = 110, + UnrecognizedName = 112, + BadCertificateStatusResponse = 113, + UnknownPskIdentity = 115, + CertificateRequired = 116, + NoApplicationProtocol = 120, +} + +impl AlertDescription { + #[must_use] + pub fn of(num: u8) -> Option { + match num { + 0 => Some(AlertDescription::CloseNotify), + 10 => Some(AlertDescription::UnexpectedMessage), + 20 => Some(AlertDescription::BadRecordMac), + 22 => Some(AlertDescription::RecordOverflow), + 40 => Some(AlertDescription::HandshakeFailure), + 42 => Some(AlertDescription::BadCertificate), + 43 => Some(AlertDescription::UnsupportedCertificate), + 44 => Some(AlertDescription::CertificateRevoked), + 45 => Some(AlertDescription::CertificateExpired), + 46 => Some(AlertDescription::CertificateUnknown), + 47 => Some(AlertDescription::IllegalParameter), + 48 => Some(AlertDescription::UnknownCa), + 49 => Some(AlertDescription::AccessDenied), + 50 => Some(AlertDescription::DecodeError), + 51 => Some(AlertDescription::DecryptError), + 70 => Some(AlertDescription::ProtocolVersion), + 71 => Some(AlertDescription::InsufficientSecurity), + 80 => Some(AlertDescription::InternalError), + 86 => Some(AlertDescription::InappropriateFallback), + 90 => Some(AlertDescription::UserCanceled), + 109 => Some(AlertDescription::MissingExtension), + 110 => Some(AlertDescription::UnsupportedExtension), + 112 => Some(AlertDescription::UnrecognizedName), + 113 => Some(AlertDescription::BadCertificateStatusResponse), + 115 => Some(AlertDescription::UnknownPskIdentity), + 116 => Some(AlertDescription::CertificateRequired), + 120 => Some(AlertDescription::NoApplicationProtocol), + _ => None, + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Alert { + pub(crate) level: AlertLevel, + pub(crate) description: AlertDescription, +} + +impl Alert { + #[must_use] + pub fn new(level: AlertLevel, description: AlertDescription) -> Self { + Self { level, description } + } + + pub fn parse(buf: &mut ParseBuffer<'_>) -> Result { + let level = buf.read_u8()?; + let desc = buf.read_u8()?; + + Ok(Self { + level: AlertLevel::of(level).ok_or(ProtocolError::DecodeError)?, + description: AlertDescription::of(desc).ok_or(ProtocolError::DecodeError)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + buf.push(self.level as u8) + .map_err(|_| ProtocolError::EncodeError)?; + buf.push(self.description as u8) + .map_err(|_| ProtocolError::EncodeError)?; + Ok(()) + } +} diff --git a/src/application_data.rs b/src/application_data.rs new file mode 100644 index 0000000..9ef177c --- /dev/null +++ b/src/application_data.rs @@ -0,0 +1,30 @@ +use crate::buffer::CryptoBuffer; +use crate::record::RecordHeader; +use core::fmt::{Debug, Formatter}; + +pub struct ApplicationData<'a> { + pub(crate) header: RecordHeader, + pub(crate) data: CryptoBuffer<'a>, +} + +impl Debug for ApplicationData<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "ApplicationData {:x?}", self.data.len()) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for ApplicationData<'a> { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "ApplicationData {}", self.data.len()); + } +} + +impl<'a> ApplicationData<'a> { + pub fn new(rx_buf: CryptoBuffer<'a>, header: RecordHeader) -> ApplicationData<'a> { + Self { + header, + data: rx_buf, + } + } +} diff --git a/src/asynch.rs b/src/asynch.rs new file mode 100644 index 0000000..73eed2a --- /dev/null +++ b/src/asynch.rs @@ -0,0 +1,541 @@ +use core::sync::atomic::{AtomicBool, Ordering}; + +use crate::ProtocolError; +use crate::common::decrypted_buffer_info::DecryptedBufferInfo; +use crate::common::decrypted_read_handler::DecryptedReadHandler; +use crate::connection::{Handshake, State, decrypt_record}; +use crate::send_policy::FlushPolicy; +use crate::key_schedule::KeySchedule; +use crate::key_schedule::{ReadKeySchedule, WriteKeySchedule}; +use crate::read_buffer::ReadBuffer; +use crate::record::{ClientRecord, ClientRecordHeader}; +use crate::record_reader::{RecordReader, RecordReaderBorrowMut}; +use crate::write_buffer::{WriteBuffer, WriteBufferBorrowMut}; +use embedded_io::Error as _; +use embedded_io::ErrorType; +use embedded_io_async::{BufRead, Read as AsyncRead, Write as AsyncWrite}; + +pub use crate::config::*; + +/// Type representing an async TLS connection. An instance of this type can +/// be used to establish a TLS connection, write and read encrypted data over this connection, +/// and closing to free up the underlying resources. +pub struct SecureStream<'a, Socket, CipherSuite> +where + Socket: AsyncRead + AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + delegate: Socket, + opened: AtomicBool, + key_schedule: KeySchedule, + record_reader: RecordReader<'a>, + record_write_buf: WriteBuffer<'a>, + decrypted: DecryptedBufferInfo, + flush_policy: FlushPolicy, +} + +impl<'a, Socket, CipherSuite> SecureStream<'a, Socket, CipherSuite> +where + Socket: AsyncRead + AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + pub fn is_opened(&mut self) -> bool { + *self.opened.get_mut() + } + /// Create a new TLS connection with the provided context and a async I/O implementation + /// + /// NOTE: The record read buffer should be sized to fit an encrypted TLS record. The size of this record + /// depends on the server configuration, but the maximum allowed value for a TLS record is 16640 bytes, + /// which should be a safe value to use. + /// + /// The write record buffer can be smaller than the read buffer. During writes [`TLS_RECORD_OVERHEAD`] bytes of + /// overhead is added per record, so the buffer must at least be this large. Large writes are split into multiple + /// records if depending on the size of the write buffer. + /// The largest of the two buffers will be used to encode the TLS handshake record, hence either of the + /// buffers must at least be large enough to encode a handshake. + pub fn new( + delegate: Socket, + record_read_buf: &'a mut [u8], + record_write_buf: &'a mut [u8], + ) -> Self { + Self { + delegate, + opened: AtomicBool::new(false), + key_schedule: KeySchedule::new(), + record_reader: RecordReader::new(record_read_buf), + record_write_buf: WriteBuffer::new(record_write_buf), + decrypted: DecryptedBufferInfo::default(), + flush_policy: FlushPolicy::default(), + } + } + + /// Returns a reference to the current flush policy. + /// + /// The flush policy controls whether the underlying transport is flushed + /// (via its `flush()` method) after writing a TLS record. + #[inline] + pub fn flush_policy(&self) -> FlushPolicy { + self.flush_policy + } + + /// Replace the current flush policy with the provided one. + /// + /// This sets how and when the connection will call `flush()` on the + /// underlying transport after writing records. + #[inline] + pub fn set_flush_policy(&mut self, policy: FlushPolicy) { + self.flush_policy = policy; + } + + /// Open a TLS connection, performing the handshake with the configuration provided when + /// creating the connection instance. + /// + /// Returns an error if the handshake does not proceed. If an error occurs, the connection + /// instance must be recreated. + pub async fn open( + &mut self, + mut context: ConnectContext<'_, CP>, + ) -> Result<(), ProtocolError> + where + CP: CryptoBackend, + { + let mut handshake: Handshake = Handshake::new(); + if let (Ok(verifier), Some(server_name)) = ( + context.crypto_provider.verifier(), + context.config.server_name, + ) { + verifier.set_hostname_verification(server_name)?; + } + let mut state = State::ClientHello; + + while state != State::ApplicationData { + let next_state = state + .process( + &mut self.delegate, + &mut handshake, + &mut self.record_reader, + &mut self.record_write_buf, + &mut self.key_schedule, + context.config, + &mut context.crypto_provider, + ) + .await?; + trace!("State {:?} -> {:?}", state, next_state); + state = next_state; + } + *self.opened.get_mut() = true; + + Ok(()) + } + + /// Encrypt and send the provided slice over the connection. The connection + /// must be opened before writing. + /// + /// The slice may be buffered internally and not written to the connection immediately. + /// In this case [`Self::flush()`] should be called to force the currently buffered writes + /// to be written to the connection. + /// + /// Returns the number of bytes buffered/written. + pub async fn write(&mut self, buf: &[u8]) -> Result { + if self.is_opened() { + if !self + .record_write_buf + .contains(ClientRecordHeader::ApplicationData) + { + self.flush().await?; + self.record_write_buf + .start_record(ClientRecordHeader::ApplicationData)?; + } + + let buffered = self.record_write_buf.append(buf); + + if self.record_write_buf.is_full() { + self.flush().await?; + } + + Ok(buffered) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + /// Force all previously written, buffered bytes to be encoded into a tls record and written + /// to the connection. + pub async fn flush(&mut self) -> Result<(), ProtocolError> { + if !self.record_write_buf.is_empty() { + let key_schedule = self.key_schedule.write_state(); + let slice = self.record_write_buf.close_record(key_schedule)?; + + self.delegate + .write_all(slice) + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + + key_schedule.increment_counter(); + + if self.flush_policy.flush_transport() { + self.flush_transport().await?; + } + } + + Ok(()) + } + + #[inline] + async fn flush_transport(&mut self) -> Result<(), ProtocolError> { + self.delegate + .flush() + .await + .map_err(|e| ProtocolError::Io(e.kind())) + } + + fn create_read_buffer(&mut self) -> ReadBuffer<'_> { + self.decrypted.create_read_buffer(self.record_reader.buf) + } + + /// Read and decrypt data filling the provided slice. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let mut buffer = self.read_buffered().await?; + + let len = buffer.pop_into(buf); + trace!("Copied {} bytes", len); + + Ok(len) + } + + /// Reads buffered data. If nothing is in memory, it'll wait for a TLS record and process it. + pub async fn read_buffered(&mut self) -> Result, ProtocolError> { + if self.is_opened() { + while self.decrypted.is_empty() { + self.read_application_data().await?; + } + + Ok(self.create_read_buffer()) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + async fn read_application_data(&mut self) -> Result<(), ProtocolError> { + let buf_ptr_range = self.record_reader.buf.as_ptr_range(); + let record = self + .record_reader + .read(&mut self.delegate, self.key_schedule.read_state()) + .await?; + + let mut handler = DecryptedReadHandler { + source_buffer: buf_ptr_range, + buffer_info: &mut self.decrypted, + is_open: self.opened.get_mut(), + }; + decrypt_record( + self.key_schedule.read_state(), + record, + |_key_schedule, record| handler.handle(record), + )?; + + Ok(()) + } + + /// Close a connection instance, returning the ownership of the config, random generator and the async I/O provider. + async fn close_internal(&mut self) -> Result<(), ProtocolError> { + self.flush().await?; + + let is_opened = self.is_opened(); + let (write_key_schedule, read_key_schedule) = self.key_schedule.as_split(); + let slice = self.record_write_buf.write_record( + &ClientRecord::close_notify(is_opened), + write_key_schedule, + Some(read_key_schedule), + )?; + + self.delegate + .write_all(slice) + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + + self.key_schedule.write_state().increment_counter(); + + self.flush_transport().await + } + + /// Close a connection instance, returning the ownership of the async I/O provider. + pub async fn close(mut self) -> Result { + match self.close_internal().await { + Ok(()) => Ok(self.delegate), + Err(e) => Err((self.delegate, e)), + } + } + + pub fn split( + &mut self, + ) -> ( + TlsReader<'_, Socket, CipherSuite>, + TlsWriter<'_, Socket, CipherSuite>, + ) + where + Socket: Clone, + { + let (wks, rks) = self.key_schedule.as_split(); + + let reader = TlsReader { + opened: &self.opened, + delegate: self.delegate.clone(), + key_schedule: rks, + record_reader: self.record_reader.reborrow_mut(), + decrypted: &mut self.decrypted, + }; + let writer = TlsWriter { + opened: &self.opened, + delegate: self.delegate.clone(), + key_schedule: wks, + record_write_buf: self.record_write_buf.reborrow_mut(), + flush_policy: self.flush_policy, + }; + + (reader, writer) + } +} + +impl<'a, Socket, CipherSuite> ErrorType for SecureStream<'a, Socket, CipherSuite> +where + Socket: AsyncRead + AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + type Error = ProtocolError; +} + +impl<'a, Socket, CipherSuite> AsyncRead for SecureStream<'a, Socket, CipherSuite> +where + Socket: AsyncRead + AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + async fn read(&mut self, buf: &mut [u8]) -> Result { + SecureStream::read(self, buf).await + } +} + +impl<'a, Socket, CipherSuite> BufRead for SecureStream<'a, Socket, CipherSuite> +where + Socket: AsyncRead + AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { + self.read_buffered().await.map(|mut buf| buf.peek_all()) + } + + fn consume(&mut self, amt: usize) { + self.create_read_buffer().pop(amt); + } +} + +impl<'a, Socket, CipherSuite> AsyncWrite for SecureStream<'a, Socket, CipherSuite> +where + Socket: AsyncRead + AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + async fn write(&mut self, buf: &[u8]) -> Result { + SecureStream::write(self, buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + SecureStream::flush(self).await + } +} + +pub struct TlsReader<'a, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + opened: &'a AtomicBool, + delegate: Socket, + key_schedule: &'a mut ReadKeySchedule, + record_reader: RecordReaderBorrowMut<'a>, + decrypted: &'a mut DecryptedBufferInfo, +} + +impl AsRef for TlsReader<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + fn as_ref(&self) -> &Socket { + &self.delegate + } +} + +impl<'a, Socket, CipherSuite> TlsReader<'a, Socket, CipherSuite> +where + Socket: AsyncRead + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn create_read_buffer(&mut self) -> ReadBuffer<'_> { + self.decrypted.create_read_buffer(self.record_reader.buf) + } + + /// Reads buffered data. If nothing is in memory, it'll wait for a TLS record and process it. + pub async fn read_buffered(&mut self) -> Result, ProtocolError> { + if self.opened.load(Ordering::Acquire) { + while self.decrypted.is_empty() { + self.read_application_data().await?; + } + + Ok(self.create_read_buffer()) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + async fn read_application_data(&mut self) -> Result<(), ProtocolError> { + let buf_ptr_range = self.record_reader.buf.as_ptr_range(); + let record = self + .record_reader + .read(&mut self.delegate, self.key_schedule) + .await?; + + let mut opened = self.opened.load(Ordering::Acquire); + let mut handler = DecryptedReadHandler { + source_buffer: buf_ptr_range, + buffer_info: self.decrypted, + is_open: &mut opened, + }; + let result = decrypt_record(self.key_schedule, record, |_key_schedule, record| { + handler.handle(record) + }); + + if !opened { + self.opened.store(false, Ordering::Release); + } + result + } +} + +pub struct TlsWriter<'a, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + opened: &'a AtomicBool, + delegate: Socket, + key_schedule: &'a mut WriteKeySchedule, + record_write_buf: WriteBufferBorrowMut<'a>, + flush_policy: FlushPolicy, +} + +impl<'a, Socket, CipherSuite> TlsWriter<'a, Socket, CipherSuite> +where + Socket: AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + #[inline] + async fn flush_transport(&mut self) -> Result<(), ProtocolError> { + self.delegate + .flush() + .await + .map_err(|e| ProtocolError::Io(e.kind())) + } +} + +impl AsRef for TlsWriter<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + fn as_ref(&self) -> &Socket { + &self.delegate + } +} + +impl ErrorType for TlsWriter<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + type Error = ProtocolError; +} + +impl ErrorType for TlsReader<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + type Error = ProtocolError; +} + +impl<'a, Socket, CipherSuite> AsyncRead for TlsReader<'a, Socket, CipherSuite> +where + Socket: AsyncRead + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + async fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let mut buffer = self.read_buffered().await?; + + let len = buffer.pop_into(buf); + trace!("Copied {} bytes", len); + + Ok(len) + } +} + +impl<'a, Socket, CipherSuite> BufRead for TlsReader<'a, Socket, CipherSuite> +where + Socket: AsyncRead + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { + self.read_buffered().await.map(|mut buf| buf.peek_all()) + } + + fn consume(&mut self, amt: usize) { + self.create_read_buffer().pop(amt); + } +} + +impl<'a, Socket, CipherSuite> AsyncWrite for TlsWriter<'a, Socket, CipherSuite> +where + Socket: AsyncWrite + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + async fn write(&mut self, buf: &[u8]) -> Result { + if self.opened.load(Ordering::Acquire) { + if !self + .record_write_buf + .contains(ClientRecordHeader::ApplicationData) + { + self.flush().await?; + self.record_write_buf + .start_record(ClientRecordHeader::ApplicationData)?; + } + + let buffered = self.record_write_buf.append(buf); + + if self.record_write_buf.is_full() { + self.flush().await?; + } + + Ok(buffered) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + if !self.record_write_buf.is_empty() { + let slice = self.record_write_buf.close_record(self.key_schedule)?; + + self.delegate + .write_all(slice) + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + + self.key_schedule.increment_counter(); + + if self.flush_policy.flush_transport() { + self.flush_transport().await?; + } + } + + Ok(()) + } +} diff --git a/src/blocking.rs b/src/blocking.rs new file mode 100644 index 0000000..5f93a2f --- /dev/null +++ b/src/blocking.rs @@ -0,0 +1,525 @@ +use core::sync::atomic::Ordering; + +use crate::common::decrypted_buffer_info::DecryptedBufferInfo; +use crate::common::decrypted_read_handler::DecryptedReadHandler; +use crate::connection::{Handshake, State, decrypt_record}; +use crate::send_policy::FlushPolicy; +use crate::key_schedule::KeySchedule; +use crate::key_schedule::{ReadKeySchedule, WriteKeySchedule}; +use crate::read_buffer::ReadBuffer; +use crate::record::{ClientRecord, ClientRecordHeader}; +use crate::record_reader::{RecordReader, RecordReaderBorrowMut}; +use crate::write_buffer::{WriteBuffer, WriteBufferBorrowMut}; +use embedded_io::Error as _; +use embedded_io::{BufRead, ErrorType, Read, Write}; +use portable_atomic::AtomicBool; + +pub use crate::ProtocolError; +pub use crate::config::*; + +/// Type representing a TLS connection. An instance of this type can +/// be used to establish a TLS connection, write and read encrypted data over this connection, +/// and closing to free up the underlying resources. +pub struct SecureStream<'a, Socket, CipherSuite> +where + Socket: Read + Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + delegate: Socket, + opened: AtomicBool, + key_schedule: KeySchedule, + record_reader: RecordReader<'a>, + record_write_buf: WriteBuffer<'a>, + decrypted: DecryptedBufferInfo, + flush_policy: FlushPolicy, +} + +impl<'a, Socket, CipherSuite> SecureStream<'a, Socket, CipherSuite> +where + Socket: Read + Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn is_opened(&mut self) -> bool { + *self.opened.get_mut() + } + + /// Create a new TLS connection with the provided context and a blocking I/O implementation + /// + /// NOTE: The record read buffer should be sized to fit an encrypted TLS record. The size of this record + /// depends on the server configuration, but the maximum allowed value for a TLS record is 16640 bytes, + /// which should be a safe value to use. + /// + /// The write record buffer can be smaller than the read buffer. During writes [`TLS_RECORD_OVERHEAD`] bytes of + /// overhead is added per record, so the buffer must at least be this large. Large writes are split into multiple + /// records if depending on the size of the write buffer. + /// The largest of the two buffers will be used to encode the TLS handshake record, hence either of the + /// buffers must at least be large enough to encode a handshake. + pub fn new( + delegate: Socket, + record_read_buf: &'a mut [u8], + record_write_buf: &'a mut [u8], + ) -> Self { + Self { + delegate, + opened: AtomicBool::new(false), + key_schedule: KeySchedule::new(), + record_reader: RecordReader::new(record_read_buf), + record_write_buf: WriteBuffer::new(record_write_buf), + decrypted: DecryptedBufferInfo::default(), + flush_policy: FlushPolicy::default(), + } + } + + /// Returns a reference to the current flush policy. + /// + /// The flush policy controls whether the underlying transport is flushed + /// (via its `flush()` method) after writing a TLS record. + #[inline] + pub fn flush_policy(&self) -> FlushPolicy { + self.flush_policy + } + + /// Replace the current flush policy with the provided one. + /// + /// This sets how and when the connection will call `flush()` on the + /// underlying transport after writing records. + #[inline] + pub fn set_flush_policy(&mut self, policy: FlushPolicy) { + self.flush_policy = policy; + } + + /// Open a TLS connection, performing the handshake with the configuration provided when + /// creating the connection instance. + /// + /// Returns an error if the handshake does not proceed. If an error occurs, the connection + /// instance must be recreated. + pub fn open(&mut self, mut context: ConnectContext) -> Result<(), ProtocolError> + where + CP: CryptoBackend, + { + let mut handshake: Handshake = Handshake::new(); + if let (Ok(verifier), Some(server_name)) = ( + context.crypto_provider.verifier(), + context.config.server_name, + ) { + verifier.set_hostname_verification(server_name)?; + } + let mut state = State::ClientHello; + + while state != State::ApplicationData { + let next_state = state.process_blocking( + &mut self.delegate, + &mut handshake, + &mut self.record_reader, + &mut self.record_write_buf, + &mut self.key_schedule, + context.config, + &mut context.crypto_provider, + )?; + trace!("State {:?} -> {:?}", state, next_state); + state = next_state; + } + *self.opened.get_mut() = true; + + Ok(()) + } + + /// Encrypt and send the provided slice over the connection. The connection + /// must be opened before writing. + /// + /// The slice may be buffered internally and not written to the connection immediately. + /// In this case [`Self::flush()`] should be called to force the currently buffered writes + /// to be written to the connection. + /// + /// Returns the number of bytes buffered/written. + pub fn write(&mut self, buf: &[u8]) -> Result { + if self.is_opened() { + if !self + .record_write_buf + .contains(ClientRecordHeader::ApplicationData) + { + self.flush()?; + self.record_write_buf + .start_record(ClientRecordHeader::ApplicationData)?; + } + + let buffered = self.record_write_buf.append(buf); + + if self.record_write_buf.is_full() { + self.flush()?; + } + + Ok(buffered) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + /// Force all previously written, buffered bytes to be encoded into a tls record and written + /// to the connection. + pub fn flush(&mut self) -> Result<(), ProtocolError> { + if !self.record_write_buf.is_empty() { + let key_schedule = self.key_schedule.write_state(); + let slice = self.record_write_buf.close_record(key_schedule)?; + + self.delegate + .write_all(slice) + .map_err(|e| ProtocolError::Io(e.kind()))?; + + key_schedule.increment_counter(); + + if self.flush_policy.flush_transport() { + self.flush_transport()?; + } + } + + Ok(()) + } + + #[inline] + fn flush_transport(&mut self) -> Result<(), ProtocolError> { + self.delegate.flush().map_err(|e| ProtocolError::Io(e.kind())) + } + + fn create_read_buffer(&mut self) -> ReadBuffer<'_> { + self.decrypted.create_read_buffer(self.record_reader.buf) + } + + /// Read and decrypt data filling the provided slice. + pub fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let mut buffer = self.read_buffered()?; + + let len = buffer.pop_into(buf); + trace!("Copied {} bytes", len); + + Ok(len) + } + + /// Reads buffered data. If nothing is in memory, it'll wait for a TLS record and process it. + pub fn read_buffered(&mut self) -> Result, ProtocolError> { + if self.is_opened() { + while self.decrypted.is_empty() { + self.read_application_data()?; + } + + Ok(self.create_read_buffer()) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + fn read_application_data(&mut self) -> Result<(), ProtocolError> { + let buf_ptr_range = self.record_reader.buf.as_ptr_range(); + let key_schedule = self.key_schedule.read_state(); + let record = self + .record_reader + .read_blocking(&mut self.delegate, key_schedule)?; + + let mut handler = DecryptedReadHandler { + source_buffer: buf_ptr_range, + buffer_info: &mut self.decrypted, + is_open: self.opened.get_mut(), + }; + decrypt_record(key_schedule, record, |_key_schedule, record| { + handler.handle(record) + })?; + + Ok(()) + } + + fn close_internal(&mut self) -> Result<(), ProtocolError> { + self.flush()?; + + let is_opened = self.is_opened(); + let (write_key_schedule, read_key_schedule) = self.key_schedule.as_split(); + let slice = self.record_write_buf.write_record( + &ClientRecord::close_notify(is_opened), + write_key_schedule, + Some(read_key_schedule), + )?; + + self.delegate + .write_all(slice) + .map_err(|e| ProtocolError::Io(e.kind()))?; + + self.key_schedule.write_state().increment_counter(); + + self.flush_transport()?; + + Ok(()) + } + + /// Close a connection instance, returning the ownership of the I/O provider. + pub fn close(mut self) -> Result { + match self.close_internal() { + Ok(()) => Ok(self.delegate), + Err(e) => Err((self.delegate, e)), + } + } + + pub fn split( + &mut self, + ) -> ( + TlsReader<'_, Socket, CipherSuite>, + TlsWriter<'_, Socket, CipherSuite>, + ) + where + Socket: Clone, + { + let (wks, rks) = self.key_schedule.as_split(); + + let reader = TlsReader { + opened: &self.opened, + delegate: self.delegate.clone(), + key_schedule: rks, + record_reader: self.record_reader.reborrow_mut(), + decrypted: &mut self.decrypted, + }; + let writer = TlsWriter { + opened: &self.opened, + delegate: self.delegate.clone(), + key_schedule: wks, + record_write_buf: self.record_write_buf.reborrow_mut(), + flush_policy: self.flush_policy, + }; + + (reader, writer) + } +} + +impl<'a, Socket, CipherSuite> ErrorType for SecureStream<'a, Socket, CipherSuite> +where + Socket: Read + Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + type Error = ProtocolError; +} + +impl<'a, Socket, CipherSuite> Read for SecureStream<'a, Socket, CipherSuite> +where + Socket: Read + Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + SecureStream::read(self, buf) + } +} + +impl<'a, Socket, CipherSuite> BufRead for SecureStream<'a, Socket, CipherSuite> +where + Socket: Read + Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { + self.read_buffered().map(|mut buf| buf.peek_all()) + } + + fn consume(&mut self, amt: usize) { + self.create_read_buffer().pop(amt); + } +} + +impl<'a, Socket, CipherSuite> Write for SecureStream<'a, Socket, CipherSuite> +where + Socket: Read + Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn write(&mut self, buf: &[u8]) -> Result { + SecureStream::write(self, buf) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + SecureStream::flush(self) + } +} + +pub struct TlsReader<'a, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + opened: &'a AtomicBool, + delegate: Socket, + key_schedule: &'a mut ReadKeySchedule, + record_reader: RecordReaderBorrowMut<'a>, + decrypted: &'a mut DecryptedBufferInfo, +} + +impl AsRef for TlsReader<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + fn as_ref(&self) -> &Socket { + &self.delegate + } +} + +impl<'a, Socket, CipherSuite> TlsReader<'a, Socket, CipherSuite> +where + Socket: Read + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn create_read_buffer(&mut self) -> ReadBuffer<'_> { + self.decrypted.create_read_buffer(self.record_reader.buf) + } + + /// Reads buffered data. If nothing is in memory, it'll wait for a TLS record and process it. + pub fn read_buffered(&mut self) -> Result, ProtocolError> { + if self.opened.load(Ordering::Acquire) { + while self.decrypted.is_empty() { + self.read_application_data()?; + } + + Ok(self.create_read_buffer()) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + fn read_application_data(&mut self) -> Result<(), ProtocolError> { + let buf_ptr_range = self.record_reader.buf.as_ptr_range(); + let record = self + .record_reader + .read_blocking(&mut self.delegate, self.key_schedule)?; + + let mut opened = self.opened.load(Ordering::Acquire); + let mut handler = DecryptedReadHandler { + source_buffer: buf_ptr_range, + buffer_info: self.decrypted, + is_open: &mut opened, + }; + let result = decrypt_record(self.key_schedule, record, |_key_schedule, record| { + handler.handle(record) + }); + + if !opened { + self.opened.store(false, Ordering::Release); + } + result + } +} + +pub struct TlsWriter<'a, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + opened: &'a AtomicBool, + delegate: Socket, + key_schedule: &'a mut WriteKeySchedule, + record_write_buf: WriteBufferBorrowMut<'a>, + flush_policy: FlushPolicy, +} + +impl<'a, Socket, CipherSuite> TlsWriter<'a, Socket, CipherSuite> +where + Socket: Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn flush_transport(&mut self) -> Result<(), ProtocolError> { + self.delegate.flush().map_err(|e| ProtocolError::Io(e.kind())) + } +} + +impl AsRef for TlsWriter<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + fn as_ref(&self) -> &Socket { + &self.delegate + } +} + +impl ErrorType for TlsWriter<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + type Error = ProtocolError; +} + +impl ErrorType for TlsReader<'_, Socket, CipherSuite> +where + CipherSuite: TlsCipherSuite + 'static, +{ + type Error = ProtocolError; +} + +impl<'a, Socket, CipherSuite> Read for TlsReader<'a, Socket, CipherSuite> +where + Socket: Read + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let mut buffer = self.read_buffered()?; + + let len = buffer.pop_into(buf); + trace!("Copied {} bytes", len); + + Ok(len) + } +} + +impl<'a, Socket, CipherSuite> BufRead for TlsReader<'a, Socket, CipherSuite> +where + Socket: Read + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { + self.read_buffered().map(|mut buf| buf.peek_all()) + } + + fn consume(&mut self, amt: usize) { + self.create_read_buffer().pop(amt); + } +} + +impl<'a, Socket, CipherSuite> Write for TlsWriter<'a, Socket, CipherSuite> +where + Socket: Write + 'a, + CipherSuite: TlsCipherSuite + 'static, +{ + fn write(&mut self, buf: &[u8]) -> Result { + if self.opened.load(Ordering::Acquire) { + if !self + .record_write_buf + .contains(ClientRecordHeader::ApplicationData) + { + self.flush()?; + self.record_write_buf + .start_record(ClientRecordHeader::ApplicationData)?; + } + + let buffered = self.record_write_buf.append(buf); + + if self.record_write_buf.is_full() { + self.flush()?; + } + + Ok(buffered) + } else { + Err(ProtocolError::MissingHandshake) + } + } + + fn flush(&mut self) -> Result<(), Self::Error> { + if !self.record_write_buf.is_empty() { + let slice = self.record_write_buf.close_record(self.key_schedule)?; + + self.delegate + .write_all(slice) + .map_err(|e| ProtocolError::Io(e.kind()))?; + + self.key_schedule.increment_counter(); + + if self.flush_policy.flush_transport() { + self.flush_transport()?; + } + } + + Ok(()) + } +} diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..ed34bab --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,304 @@ +use crate::ProtocolError; +use aes_gcm::Error; +use aes_gcm::aead::Buffer; + +pub struct CryptoBuffer<'b> { + buf: &'b mut [u8], + offset: usize, + len: usize, +} + +impl<'b> CryptoBuffer<'b> { + #[allow(dead_code)] + pub(crate) fn empty() -> Self { + Self { + buf: &mut [], + offset: 0, + len: 0, + } + } + + pub(crate) fn wrap(buf: &'b mut [u8]) -> Self { + Self { + buf, + offset: 0, + len: 0, + } + } + + pub(crate) fn wrap_with_pos(buf: &'b mut [u8], pos: usize) -> Self { + Self { + buf, + offset: 0, + len: pos, + } + } + + pub fn push(&mut self, b: u8) -> Result<(), ProtocolError> { + if self.space() > 0 { + self.buf[self.offset + self.len] = b; + self.len += 1; + Ok(()) + } else { + error!("Failed to push byte"); + Err(ProtocolError::InsufficientSpace) + } + } + + pub fn push_u16(&mut self, num: u16) -> Result<(), ProtocolError> { + let data = num.to_be_bytes(); + self.extend_from_slice(&data) + } + + pub fn push_u24(&mut self, num: u32) -> Result<(), ProtocolError> { + let data = num.to_be_bytes(); + self.extend_from_slice(&[data[1], data[2], data[3]]) + } + + pub fn push_u32(&mut self, num: u32) -> Result<(), ProtocolError> { + let data = num.to_be_bytes(); + self.extend_from_slice(&data) + } + + fn set(&mut self, idx: usize, val: u8) -> Result<(), ProtocolError> { + if idx < self.len { + self.buf[self.offset + idx] = val; + Ok(()) + } else { + error!( + "Failed to set byte: index {} is out of range for {} elements", + idx, self.len + ); + Err(ProtocolError::InsufficientSpace) + } + } + + fn set_u16(&mut self, idx: usize, val: u16) -> Result<(), ProtocolError> { + let [upper, lower] = val.to_be_bytes(); + self.set(idx, upper)?; + self.set(idx + 1, lower)?; + Ok(()) + } + + fn set_u24(&mut self, idx: usize, val: u32) -> Result<(), ProtocolError> { + let [_, upper, mid, lower] = val.to_be_bytes(); + self.set(idx, upper)?; + self.set(idx + 1, mid)?; + self.set(idx + 2, lower)?; + Ok(()) + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + &mut self.buf[self.offset..self.offset + self.len] + } + + pub fn as_slice(&self) -> &[u8] { + &self.buf[self.offset..self.offset + self.len] + } + + fn extend_internal(&mut self, other: &[u8]) -> Result<(), ProtocolError> { + if self.space() < other.len() { + error!( + "Failed to extend buffer. Space: {} required: {}", + self.space(), + other.len() + ); + Err(ProtocolError::InsufficientSpace) + } else { + let start = self.offset + self.len; + self.buf[start..start + other.len()].clone_from_slice(other); + self.len += other.len(); + Ok(()) + } + } + + fn space(&self) -> usize { + self.capacity() - (self.offset + self.len) + } + + pub fn extend_from_slice(&mut self, other: &[u8]) -> Result<(), ProtocolError> { + self.extend_internal(other) + } + + fn truncate_internal(&mut self, len: usize) { + if len <= self.capacity() - self.offset { + self.len = len; + } + } + + pub fn truncate(&mut self, len: usize) { + self.truncate_internal(len); + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn release(self) -> (&'b mut [u8], usize, usize) { + (self.buf, self.offset, self.len) + } + + pub fn capacity(&self) -> usize { + self.buf.len() + } + + pub fn forward(self) -> CryptoBuffer<'b> { + let len = self.len; + self.offset(len) + } + + pub fn rewind(self) -> CryptoBuffer<'b> { + self.offset(0) + } + + pub(crate) fn offset(self, offset: usize) -> CryptoBuffer<'b> { + let new_len = self.len + self.offset - offset; + /*info!( + "offset({}) len({}) -> offset({}), len({})", + self.offset, self.len, offset, new_len + );*/ + CryptoBuffer { + buf: self.buf, + len: new_len, + offset, + } + } + + pub fn with_u8_length( + &mut self, + op: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let len_pos = self.len; + self.push(0)?; + let start = self.len; + + let r = op(self)?; + + let len = (self.len() - start) as u8; + self.set(len_pos, len)?; + + Ok(r) + } + + pub fn with_u16_length( + &mut self, + op: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let len_pos = self.len; + self.push_u16(0)?; + let start = self.len; + + let r = op(self)?; + + let len = (self.len() - start) as u16; + self.set_u16(len_pos, len)?; + + Ok(r) + } + + pub fn with_u24_length( + &mut self, + op: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let len_pos = self.len; + self.push_u24(0)?; + let start = self.len; + + let r = op(self)?; + + let len = (self.len() - start) as u32; + self.set_u24(len_pos, len)?; + + Ok(r) + } +} + +impl AsRef<[u8]> for CryptoBuffer<'_> { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +impl AsMut<[u8]> for CryptoBuffer<'_> { + fn as_mut(&mut self) -> &mut [u8] { + self.as_mut_slice() + } +} + +impl Buffer for CryptoBuffer<'_> { + fn extend_from_slice(&mut self, other: &[u8]) -> Result<(), Error> { + self.extend_internal(other).map_err(|_| Error) + } + + fn truncate(&mut self, len: usize) { + self.truncate_internal(len); + } +} + +#[cfg(test)] +mod test { + use super::CryptoBuffer; + + #[test] + fn encode() { + let mut buf1 = [0; 4]; + let mut c = CryptoBuffer::wrap(&mut buf1); + c.push_u24(1027).unwrap(); + + let mut buf2 = [0; 4]; + let mut c = CryptoBuffer::wrap(&mut buf2); + c.push_u24(0).unwrap(); + c.set_u24(0, 1027).unwrap(); + + assert_eq!(buf1, buf2); + + let decoded = u32::from_be_bytes([0, buf1[0], buf1[1], buf1[2]]); + assert_eq!(1027, decoded); + } + + #[test] + fn offset_calc() { + let mut buf = [0; 8]; + let mut c = CryptoBuffer::wrap(&mut buf); + c.push(1).unwrap(); + c.push(2).unwrap(); + c.push(3).unwrap(); + + assert_eq!(&[1, 2, 3], c.as_slice()); + + let l = c.len(); + let mut c = c.offset(l); + + c.push(4).unwrap(); + c.push(5).unwrap(); + c.push(6).unwrap(); + + assert_eq!(&[4, 5, 6], c.as_slice()); + + let mut c = c.offset(0); + + c.push(7).unwrap(); + c.push(8).unwrap(); + + assert_eq!(&[1, 2, 3, 4, 5, 6, 7, 8], c.as_slice()); + + let mut c = c.offset(6); + c.set(0, 14).unwrap(); + c.set(1, 15).unwrap(); + + let c = c.offset(0); + assert_eq!(&[1, 2, 3, 4, 5, 6, 14, 15], c.as_slice()); + + let mut c = c.offset(4); + c.truncate(0); + c.extend_from_slice(&[10, 11, 12, 13]).unwrap(); + assert_eq!(&[10, 11, 12, 13], c.as_slice()); + + let c = c.offset(0); + assert_eq!(&[1, 2, 3, 4, 10, 11, 12, 13], c.as_slice()); + } +} diff --git a/src/cert_verify.rs b/src/cert_verify.rs new file mode 100644 index 0000000..2094cc3 --- /dev/null +++ b/src/cert_verify.rs @@ -0,0 +1,296 @@ +use crate::ProtocolError; +use crate::config::{Certificate, TlsCipherSuite, TlsClock, Verifier}; +use crate::extensions::extension_data::signature_algorithms::SignatureScheme; +use crate::handshake::{ + certificate::{ + Certificate as OwnedCertificate, CertificateEntryRef, CertificateRef as ServerCertificate, + }, + certificate_verify::HandshakeVerifyRef, +}; +use core::marker::PhantomData; +use digest::Digest; +use heapless::Vec; +#[cfg(all(not(feature = "alloc"), feature = "webpki"))] +impl TryInto<&'static webpki::SignatureAlgorithm> for SignatureScheme { + type Error = ProtocolError; + fn try_into(self) -> Result<&'static webpki::SignatureAlgorithm, Self::Error> { + // TODO: support other schemes via 'alloc' feature + #[allow(clippy::match_same_arms)] // Style + match self { + SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512 => Err(ProtocolError::InvalidSignatureScheme), + + /* ECDSA algorithms */ + SignatureScheme::EcdsaSecp256r1Sha256 => Ok(&webpki::ECDSA_P256_SHA256), + SignatureScheme::EcdsaSecp384r1Sha384 => Ok(&webpki::ECDSA_P384_SHA384), + SignatureScheme::EcdsaSecp521r1Sha512 => Err(ProtocolError::InvalidSignatureScheme), + + /* RSASSA-PSS algorithms with public key OID rsaEncryption */ + SignatureScheme::RsaPssRsaeSha256 + | SignatureScheme::RsaPssRsaeSha384 + | SignatureScheme::RsaPssRsaeSha512 => Err(ProtocolError::InvalidSignatureScheme), + + /* EdDSA algorithms */ + SignatureScheme::Ed25519 => Ok(&webpki::ED25519), + SignatureScheme::Ed448 + | SignatureScheme::Sha224Ecdsa + | SignatureScheme::Sha224Rsa + | SignatureScheme::Sha224Dsa => Err(ProtocolError::InvalidSignatureScheme), + + /* RSASSA-PSS algorithms with public key OID RSASSA-PSS */ + SignatureScheme::RsaPssPssSha256 + | SignatureScheme::RsaPssPssSha384 + | SignatureScheme::RsaPssPssSha512 => Err(ProtocolError::InvalidSignatureScheme), + + /* Legacy algorithms */ + SignatureScheme::RsaPkcs1Sha1 | SignatureScheme::EcdsaSha1 => { + Err(ProtocolError::InvalidSignatureScheme) + } + + /* Ml-DSA */ + SignatureScheme::MlDsa44 | SignatureScheme::MlDsa65 | SignatureScheme::MlDsa87 => { + Err(ProtocolError::InvalidSignatureScheme) + } + + /* Brainpool */ + SignatureScheme::Sha256BrainpoolP256r1 + | SignatureScheme::Sha384BrainpoolP384r1 + | SignatureScheme::Sha512BrainpoolP512r1 => Err(ProtocolError::InvalidSignatureScheme), + } + } +} + +#[cfg(all(feature = "alloc", feature = "webpki"))] +impl TryInto<&'static webpki::SignatureAlgorithm> for SignatureScheme { + type Error = ProtocolError; + fn try_into(self) -> Result<&'static webpki::SignatureAlgorithm, Self::Error> { + match self { + SignatureScheme::RsaPkcs1Sha256 => Ok(&webpki::RSA_PKCS1_2048_8192_SHA256), + SignatureScheme::RsaPkcs1Sha384 => Ok(&webpki::RSA_PKCS1_2048_8192_SHA384), + SignatureScheme::RsaPkcs1Sha512 => Ok(&webpki::RSA_PKCS1_2048_8192_SHA512), + + /* ECDSA algorithms */ + SignatureScheme::EcdsaSecp256r1Sha256 => Ok(&webpki::ECDSA_P256_SHA256), + SignatureScheme::EcdsaSecp384r1Sha384 => Ok(&webpki::ECDSA_P384_SHA384), + SignatureScheme::EcdsaSecp521r1Sha512 => Err(ProtocolError::InvalidSignatureScheme), + + /* RSASSA-PSS algorithms with public key OID rsaEncryption */ + SignatureScheme::RsaPssRsaeSha256 => Ok(&webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY), + SignatureScheme::RsaPssRsaeSha384 => Ok(&webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY), + SignatureScheme::RsaPssRsaeSha512 => Ok(&webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY), + + /* EdDSA algorithms */ + SignatureScheme::Ed25519 => Ok(&webpki::ED25519), + SignatureScheme::Ed448 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::Sha224Ecdsa => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::Sha224Rsa => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::Sha224Dsa => Err(ProtocolError::InvalidSignatureScheme), + + /* RSASSA-PSS algorithms with public key OID RSASSA-PSS */ + SignatureScheme::RsaPssPssSha256 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::RsaPssPssSha384 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::RsaPssPssSha512 => Err(ProtocolError::InvalidSignatureScheme), + + /* Legacy algorithms */ + SignatureScheme::RsaPkcs1Sha1 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::EcdsaSha1 => Err(ProtocolError::InvalidSignatureScheme), + + /* MlDsa */ + SignatureScheme::MlDsa44 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::MlDsa65 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::MlDsa87 => Err(ProtocolError::InvalidSignatureScheme), + + /* Brainpool */ + SignatureScheme::Sha256BrainpoolP256r1 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::Sha384BrainpoolP384r1 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::Sha512BrainpoolP512r1 => Err(ProtocolError::InvalidSignatureScheme), + } + } +} + +static ALL_SIGALGS: &[&webpki::SignatureAlgorithm] = &[ + &webpki::ECDSA_P256_SHA256, + &webpki::ECDSA_P256_SHA384, + &webpki::ECDSA_P384_SHA256, + &webpki::ECDSA_P384_SHA384, + &webpki::ED25519, +]; + +pub struct CertVerifier<'a, CipherSuite, Clock, const CERT_SIZE: usize> +where + Clock: TlsClock, + CipherSuite: TlsCipherSuite, +{ + ca: Certificate<&'a [u8]>, + host: Option>, + certificate_transcript: Option, + certificate: Option>, + _clock: PhantomData, +} + +impl<'a, CipherSuite, Clock, const CERT_SIZE: usize> CertVerifier<'a, CipherSuite, Clock, CERT_SIZE> +where + Clock: TlsClock, + CipherSuite: TlsCipherSuite, +{ + #[must_use] + pub fn new(ca: Certificate<&'a [u8]>) -> Self { + Self { + ca, + host: None, + certificate_transcript: None, + certificate: None, + _clock: PhantomData, + } + } +} + +impl Verifier + for CertVerifier<'_, CipherSuite, Clock, CERT_SIZE> +where + CipherSuite: TlsCipherSuite, + Clock: TlsClock, +{ + fn set_hostname_verification(&mut self, hostname: &str) -> Result<(), ProtocolError> { + self.host.replace( + heapless::String::try_from(hostname).map_err(|_| ProtocolError::InsufficientSpace)?, + ); + Ok(()) + } + + fn verify_certificate( + &mut self, + transcript: &CipherSuite::Hash, + cert: ServerCertificate, + ) -> Result<(), ProtocolError> { + verify_certificate(self.host.as_deref(), &self.ca, &cert, Clock::now())?; + self.certificate.replace(cert.try_into()?); + self.certificate_transcript.replace(transcript.clone()); + Ok(()) + } + + fn verify_signature(&mut self, verify: HandshakeVerifyRef) -> Result<(), ProtocolError> { + let handshake_hash = unwrap!(self.certificate_transcript.take()); + let ctx_str = b"TLS 1.3, server CertificateVerify\x00"; + let mut msg: Vec = Vec::new(); + msg.resize(64, 0x20).map_err(|_| ProtocolError::EncodeError)?; + msg.extend_from_slice(ctx_str) + .map_err(|_| ProtocolError::EncodeError)?; + msg.extend_from_slice(&handshake_hash.finalize()) + .map_err(|_| ProtocolError::EncodeError)?; + + let certificate = unwrap!(self.certificate.as_ref()).try_into()?; + verify_signature(&msg[..], &certificate, &verify)?; + Ok(()) + } +} + +fn verify_signature( + message: &[u8], + certificate: &ServerCertificate, + verify: &HandshakeVerifyRef, +) -> Result<(), ProtocolError> { + let mut verified = false; + if !certificate.entries.is_empty() { + // TODO: Support intermediates... + if let CertificateEntryRef::X509(certificate) = certificate.entries[0] { + let cert = webpki::EndEntityCert::try_from(certificate).map_err(|e| { + warn!("ProtocolError loading cert: {:?}", e); + ProtocolError::DecodeError + })?; + + trace!( + "Verifying with signature scheme {:?}", + verify.signature_scheme + ); + info!("Signature: {:x?}", verify.signature); + let pkisig = verify.signature_scheme.try_into()?; + match cert.verify_signature(pkisig, message, verify.signature) { + Ok(()) => { + verified = true; + } + Err(e) => { + info!("ProtocolError verifying signature: {:?}", e); + } + } + } + } + if !verified { + return Err(ProtocolError::InvalidSignature); + } + Ok(()) +} + +fn verify_certificate( + verify_host: Option<&str>, + ca: &Certificate<&[u8]>, + certificate: &ServerCertificate, + now: Option, +) -> Result<(), ProtocolError> { + let mut verified = false; + let mut host_verified = false; + if let Certificate::X509(ca) = ca { + let trust = webpki::TrustAnchor::try_from_cert_der(ca).map_err(|e| { + warn!("ProtocolError loading CA: {:?}", e); + ProtocolError::DecodeError + })?; + + trace!("We got {} certificate entries", certificate.entries.len()); + + if !certificate.entries.is_empty() { + // TODO: Support intermediates... + if let CertificateEntryRef::X509(certificate) = certificate.entries[0] { + let cert = webpki::EndEntityCert::try_from(certificate).map_err(|e| { + warn!("ProtocolError loading cert: {:?}", e); + ProtocolError::DecodeError + })?; + + let time = if let Some(now) = now { + webpki::Time::from_seconds_since_unix_epoch(now) + } else { + // If no clock is provided, the validity check will fail + webpki::Time::from_seconds_since_unix_epoch(0) + }; + info!("Certificate is loaded!"); + match cert.verify_for_usage( + ALL_SIGALGS, + &[trust], + &[], + time, + webpki::KeyUsage::server_auth(), + &[], + ) { + Ok(()) => verified = true, + Err(e) => { + warn!("ProtocolError verifying certificate: {:?}", e); + } + } + + if let Some(server_name) = verify_host { + match webpki::SubjectNameRef::try_from_ascii(server_name.as_bytes()) { + Ok(subject) => match cert.verify_is_valid_for_subject_name(subject) { + Ok(()) => host_verified = true, + Err(e) => { + warn!("ProtocolError verifying host: {:?}", e); + } + }, + Err(e) => { + warn!("ProtocolError verifying host: {:?}", e); + } + } + } + } + } + } + + if !verified { + return Err(ProtocolError::InvalidCertificate); + } + + if !host_verified && verify_host.is_some() { + return Err(ProtocolError::InvalidCertificate); + } + Ok(()) +} diff --git a/src/certificate.rs b/src/certificate.rs new file mode 100644 index 0000000..9563408 --- /dev/null +++ b/src/certificate.rs @@ -0,0 +1,160 @@ +use core::cmp::Ordering; +use der::asn1::{ + BitStringRef, GeneralizedTime, IntRef, ObjectIdentifier, SequenceOf, SetOf, UtcTime, +}; +use der::{AnyRef, Choice, Enumerated, Sequence, ValueOrd}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Sequence, ValueOrd)] +pub struct AlgorithmIdentifier<'a> { + pub oid: ObjectIdentifier, + pub parameters: Option>, +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for AlgorithmIdentifier<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "AlgorithmIdentifier:{}", &self.oid.as_bytes()) + } +} + +pub const ECDSA_SHA256: AlgorithmIdentifier = AlgorithmIdentifier { + oid: ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2"), + parameters: None, +}; +#[cfg(feature = "p384")] +pub const ECDSA_SHA384: AlgorithmIdentifier = AlgorithmIdentifier { + oid: ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3"), + parameters: None, +}; +#[cfg(feature = "ed25519")] +pub const ED25519: AlgorithmIdentifier = AlgorithmIdentifier { + oid: ObjectIdentifier::new_unwrap("1.3.101.112"), + parameters: None, +}; +#[cfg(feature = "rsa")] +pub const RSA_PKCS1_SHA256: AlgorithmIdentifier = AlgorithmIdentifier { + oid: ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11"), + parameters: Some(AnyRef::NULL), +}; +#[cfg(feature = "rsa")] +pub const RSA_PKCS1_SHA384: AlgorithmIdentifier = AlgorithmIdentifier { + oid: ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12"), + parameters: Some(AnyRef::NULL), +}; +#[cfg(feature = "rsa")] +pub const RSA_PKCS1_SHA512: AlgorithmIdentifier = AlgorithmIdentifier { + oid: ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13"), + parameters: Some(AnyRef::NULL), +}; + +#[derive(Debug, Clone, PartialEq, Eq, Copy, Enumerated)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[asn1(type = "INTEGER")] +#[repr(u8)] +pub enum Version { + /// Version 1 (default) + V1 = 0, + /// Version 2 + V2 = 1, + /// Version 3 + V3 = 2, +} + +impl ValueOrd for Version { + fn value_cmp(&self, other: &Self) -> der::Result { + (*self as u8).value_cmp(&(*other as u8)) + } +} + +impl Default for Version { + fn default() -> Self { + Self::V1 + } +} + +#[derive(Sequence, ValueOrd)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DecodedCertificate<'a> { + pub tbs_certificate: TbsCertificate<'a>, + pub signature_algorithm: AlgorithmIdentifier<'a>, + pub signature: BitStringRef<'a>, +} + +#[derive(Debug, Sequence, ValueOrd)] +pub struct AttributeTypeAndValue<'a> { + pub oid: ObjectIdentifier, + pub value: AnyRef<'a>, +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for AttributeTypeAndValue<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Attribute:{} Value:{}", + &self.oid.as_bytes(), + &self.value.value() + ) + } +} + +#[derive(Debug, Choice, ValueOrd)] +pub enum Time { + #[asn1(type = "UTCTime")] + UtcTime(UtcTime), + + #[asn1(type = "GeneralizedTime")] + GeneralTime(GeneralizedTime), +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Time { + fn format(&self, fmt: defmt::Formatter) { + match self { + Time::UtcTime(utc_time) => { + defmt::write!(fmt, "UtcTime:{}", utc_time.to_unix_duration()) + } + Time::GeneralTime(generalized_time) => { + defmt::write!(fmt, "GeneralTime:{}", generalized_time.to_unix_duration()) + } + } + } +} + +#[derive(Debug, Sequence, ValueOrd)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Validity { + pub not_before: Time, + pub not_after: Time, +} + +#[derive(Debug, Sequence, ValueOrd)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SubjectPublicKeyInfoRef<'a> { + pub algorithm: AlgorithmIdentifier<'a>, + pub public_key: BitStringRef<'a>, +} + +#[derive(Debug, Sequence, ValueOrd)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TbsCertificate<'a> { + #[asn1(context_specific = "0", default = "Default::default")] + pub version: Version, + + pub serial_number: IntRef<'a>, + pub signature: AlgorithmIdentifier<'a>, + pub issuer: SequenceOf, 1>, 7>, + + pub validity: Validity, + pub subject: SequenceOf, 1>, 7>, + pub subject_public_key_info: SubjectPublicKeyInfoRef<'a>, + + #[asn1(context_specific = "1", tag_mode = "IMPLICIT", optional = "true")] + pub issuer_unique_id: Option>, + + #[asn1(context_specific = "2", tag_mode = "IMPLICIT", optional = "true")] + pub subject_unique_id: Option>, + + #[asn1(context_specific = "3", tag_mode = "EXPLICIT", optional = "true")] + pub extensions: Option>, +} diff --git a/src/change_cipher_spec.rs b/src/change_cipher_spec.rs new file mode 100644 index 0000000..069ff3a --- /dev/null +++ b/src/change_cipher_spec.rs @@ -0,0 +1,37 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use crate::parse_buffer::ParseBuffer; + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ChangeCipherSpec {} + +#[allow(clippy::unnecessary_wraps)] // TODO +impl ChangeCipherSpec { + pub fn new() -> Self { + Self {} + } + + pub fn read(_rx_buf: &mut [u8]) -> Result { + // info!("change cipher spec of len={}", rx_buf.len()); + // TODO: Decode data + Ok(Self {}) + } + + #[allow(dead_code)] + pub fn parse(_: &mut ParseBuffer) -> Result { + Ok(Self {}) + } + + #[allow(dead_code, clippy::unused_self)] + pub(crate) fn encode(self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + buf.push(1).map_err(|_| ProtocolError::EncodeError)?; + Ok(()) + } +} + +impl Default for ChangeCipherSpec { + fn default() -> Self { + ChangeCipherSpec::new() + } +} diff --git a/src/cipher.rs b/src/cipher.rs new file mode 100644 index 0000000..0dd9d39 --- /dev/null +++ b/src/cipher.rs @@ -0,0 +1,15 @@ +use crate::application_data::ApplicationData; +use crate::extensions::extension_data::supported_groups::NamedGroup; +use p256::ecdh::SharedSecret; + +pub struct CryptoEngine {} + +#[allow(clippy::unused_self, clippy::needless_pass_by_value)] // TODO +impl CryptoEngine { + pub fn new(_group: NamedGroup, _shared: SharedSecret) -> Self { + Self {} + } + + #[allow(dead_code)] + pub fn decrypt(&self, _: &ApplicationData) {} +} diff --git a/src/cipher_suites.rs b/src/cipher_suites.rs new file mode 100644 index 0000000..2463e5a --- /dev/null +++ b/src/cipher_suites.rs @@ -0,0 +1,26 @@ +use crate::parse_buffer::{ParseBuffer, ParseError}; + +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CipherSuite { + TlsAes128GcmSha256 = 0x1301, + TlsAes256GcmSha384 = 0x1302, + TlsChacha20Poly1305Sha256 = 0x1303, + TlsAes128CcmSha256 = 0x1304, + TlsAes128Ccm8Sha256 = 0x1305, + TlsPskAes128GcmSha256 = 0x00A8, +} + +impl CipherSuite { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u16()? { + v if v == Self::TlsAes128GcmSha256 as u16 => Ok(Self::TlsAes128GcmSha256), + v if v == Self::TlsAes256GcmSha384 as u16 => Ok(Self::TlsAes256GcmSha384), + v if v == Self::TlsChacha20Poly1305Sha256 as u16 => Ok(Self::TlsChacha20Poly1305Sha256), + v if v == Self::TlsAes128CcmSha256 as u16 => Ok(Self::TlsAes128CcmSha256), + v if v == Self::TlsAes128Ccm8Sha256 as u16 => Ok(Self::TlsAes128Ccm8Sha256), + v if v == Self::TlsPskAes128GcmSha256 as u16 => Ok(Self::TlsPskAes128GcmSha256), + _ => Err(ParseError::InvalidData), + } + } +} diff --git a/src/common/decrypted_buffer_info.rs b/src/common/decrypted_buffer_info.rs new file mode 100644 index 0000000..c29616a --- /dev/null +++ b/src/common/decrypted_buffer_info.rs @@ -0,0 +1,24 @@ +use crate::read_buffer::ReadBuffer; + +#[derive(Default)] +pub struct DecryptedBufferInfo { + pub offset: usize, + pub len: usize, + pub consumed: usize, +} + +impl DecryptedBufferInfo { + pub fn create_read_buffer<'b>(&'b mut self, buffer: &'b [u8]) -> ReadBuffer<'b> { + let offset = self.offset + self.consumed; + let end = self.offset + self.len; + ReadBuffer::new(&buffer[offset..end], &mut self.consumed) + } + + pub fn len(&self) -> usize { + self.len - self.consumed + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} diff --git a/src/common/decrypted_read_handler.rs b/src/common/decrypted_read_handler.rs new file mode 100644 index 0000000..a440c45 --- /dev/null +++ b/src/common/decrypted_read_handler.rs @@ -0,0 +1,64 @@ +use core::ops::Range; + +use crate::{ + ProtocolError, alert::AlertDescription, common::decrypted_buffer_info::DecryptedBufferInfo, + config::TlsCipherSuite, handshake::ServerHandshake, record::ServerRecord, +}; + +pub struct DecryptedReadHandler<'a> { + pub source_buffer: Range<*const u8>, + pub buffer_info: &'a mut DecryptedBufferInfo, + pub is_open: &'a mut bool, +} + +impl DecryptedReadHandler<'_> { + pub fn handle( + &mut self, + record: ServerRecord<'_, CipherSuite>, + ) -> Result<(), ProtocolError> { + match record { + ServerRecord::ApplicationData(data) => { + let slice = data.data.as_slice(); + let slice_ptrs = slice.as_ptr_range(); + + debug_assert!( + self.source_buffer.contains(&slice_ptrs.start) + && self.source_buffer.contains(&slice_ptrs.end) + ); + + let offset = unsafe { + // SAFETY: The assertion above ensures `slice` is a subslice of the read buffer. + // This, in turn, ensures we don't violate safety constraints of `offset_from`. + + // TODO: We are only assuming here that the pointers are derived from the read + // buffer. While this is reasonable, and we don't do any pointer magic, + // it's not an invariant. + slice_ptrs.start.offset_from(self.source_buffer.start) as usize + }; + + self.buffer_info.offset = offset; + self.buffer_info.len = slice.len(); + self.buffer_info.consumed = 0; + Ok(()) + } + ServerRecord::Alert(alert) => { + if let AlertDescription::CloseNotify = alert.description { + *self.is_open = false; + Err(ProtocolError::ConnectionClosed) + } else { + Err(ProtocolError::InternalError) + } + } + ServerRecord::ChangeCipherSpec(_) => Err(ProtocolError::InternalError), + ServerRecord::Handshake(ServerHandshake::NewSessionTicket(_)) => { + // TODO: we should validate extensions and abort. We can do this automatically + // as long as the connection is unsplit, however, split connections must be aborted + // by the user. + Ok(()) + } + ServerRecord::Handshake(_) => { + unimplemented!() + } + } + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..a5d9c68 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,2 @@ +pub mod decrypted_buffer_info; +pub mod decrypted_read_handler; diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..98505b0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,421 @@ +use core::marker::PhantomData; + +use crate::ProtocolError; +use crate::cipher_suites::CipherSuite; +use crate::extensions::extension_data::signature_algorithms::SignatureScheme; +use crate::extensions::extension_data::supported_groups::NamedGroup; +pub use crate::handshake::certificate::{CertificateEntryRef, CertificateRef}; +pub use crate::handshake::certificate_verify::HandshakeVerifyRef; +use aes_gcm::{AeadInPlace, Aes128Gcm, Aes256Gcm, KeyInit}; +use digest::core_api::BlockSizeUser; +use digest::{Digest, FixedOutput, OutputSizeUser, Reset}; +use ecdsa::elliptic_curve::SecretKey; +use generic_array::ArrayLength; +use heapless::Vec; +use p256::ecdsa::SigningKey; +use rand_core::CryptoRngCore; +pub use sha2::{Sha256, Sha384}; +use typenum::{Sum, U10, U12, U16, U32}; + +pub use crate::extensions::extension_data::max_fragment_length::MaxFragmentLength; + +pub const TLS_RECORD_OVERHEAD: usize = 128; + +// longest label is 12b -> buf <= 2 + 1 + 6 + longest + 1 + hash_out = hash_out + 22 +type LongestLabel = U12; +type LabelOverhead = U10; +type LabelBuffer = Sum< + <::Hash as OutputSizeUser>::OutputSize, + Sum, +>; + +/// Represents a TLS 1.3 cipher suite +pub trait TlsCipherSuite { + const CODE_POINT: u16; + type Cipher: KeyInit + AeadInPlace; + type KeyLen: ArrayLength; + type IvLen: ArrayLength; + + type Hash: Digest + Reset + Clone + OutputSizeUser + BlockSizeUser + FixedOutput; + type LabelBufferSize: ArrayLength; +} + +pub struct Aes128GcmSha256; +impl TlsCipherSuite for Aes128GcmSha256 { + const CODE_POINT: u16 = CipherSuite::TlsAes128GcmSha256 as u16; + type Cipher = Aes128Gcm; + type KeyLen = U16; + type IvLen = U12; + + type Hash = Sha256; + type LabelBufferSize = LabelBuffer; +} + +pub struct Aes256GcmSha384; +impl TlsCipherSuite for Aes256GcmSha384 { + const CODE_POINT: u16 = CipherSuite::TlsAes256GcmSha384 as u16; + type Cipher = Aes256Gcm; + type KeyLen = U32; + type IvLen = U12; + + type Hash = Sha384; + type LabelBufferSize = LabelBuffer; +} + +/// A TLS 1.3 verifier. +/// +/// The verifier is responsible for verifying certificates and signatures. Since certificate verification is +/// an expensive process, this trait allows clients to choose how much verification should take place, +/// and also to skip the verification if the server is verified through other means (I.e. a pre-shared key). +pub trait Verifier +where + CipherSuite: TlsCipherSuite, +{ + /// Host verification is enabled by passing a server hostname. + fn set_hostname_verification(&mut self, hostname: &str) -> Result<(), crate::ProtocolError>; + + /// Verify a certificate. + /// + /// The handshake transcript up to this point and the server certificate is provided + /// for the implementation to use. The verifier is responsible for resolving the CA + /// certificate internally. + fn verify_certificate( + &mut self, + transcript: &CipherSuite::Hash, + cert: CertificateRef, + ) -> Result<(), ProtocolError>; + + /// Verify the certificate signature. + /// + /// The signature verification uses the transcript and certificate provided earlier to decode the provided signature. + fn verify_signature(&mut self, verify: HandshakeVerifyRef) -> Result<(), crate::ProtocolError>; +} + +pub struct NoVerify; + +impl Verifier for NoVerify +where + CipherSuite: TlsCipherSuite, +{ + fn set_hostname_verification(&mut self, _hostname: &str) -> Result<(), crate::ProtocolError> { + Ok(()) + } + + fn verify_certificate( + &mut self, + _transcript: &CipherSuite::Hash, + _cert: CertificateRef, + ) -> Result<(), ProtocolError> { + Ok(()) + } + + fn verify_signature(&mut self, _verify: HandshakeVerifyRef) -> Result<(), crate::ProtocolError> { + Ok(()) + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[must_use = "ConnectConfig does nothing unless consumed"] +pub struct ConnectConfig<'a> { + pub(crate) server_name: Option<&'a str>, + pub(crate) alpn_protocols: Option<&'a [&'a [u8]]>, + pub(crate) psk: Option<(&'a [u8], Vec<&'a [u8], 4>)>, + pub(crate) signature_schemes: Vec, + pub(crate) named_groups: Vec, + pub(crate) max_fragment_length: Option, +} + +pub trait TlsClock { + fn now() -> Option; +} + +pub struct NoClock; + +impl TlsClock for NoClock { + fn now() -> Option { + None + } +} + +pub trait CryptoBackend { + type CipherSuite: TlsCipherSuite; + type Signature: AsRef<[u8]>; + + fn rng(&mut self) -> impl CryptoRngCore; + + fn verifier(&mut self) -> Result<&mut impl Verifier, crate::ProtocolError> { + Err::<&mut NoVerify, _>(crate::ProtocolError::Unimplemented) + } + + /// Provide a signing key for client certificate authentication. + /// + /// The provider resolves the private key internally (e.g. from memory, flash, or a hardware + /// crypto module such as an HSM/TPM/secure element). + fn signer( + &mut self, + ) -> Result<(impl signature::SignerMut, SignatureScheme), crate::ProtocolError> + { + Err::<(NoSign, _), crate::ProtocolError>(crate::ProtocolError::Unimplemented) + } + + /// Resolve the client certificate for mutual TLS authentication. + /// + /// Return `None` if no client certificate is available (an empty certificate message will + /// be sent to the server). The data type `D` can be borrowed (`&[u8]`) or owned + /// (e.g. `heapless::Vec`) — the certificate is only needed long enough to encode + /// into the TLS message. + fn client_cert(&mut self) -> Option>> { + None::> + } +} + +impl CryptoBackend for &mut T { + type CipherSuite = T::CipherSuite; + + type Signature = T::Signature; + + fn rng(&mut self) -> impl CryptoRngCore { + T::rng(self) + } + + fn verifier(&mut self) -> Result<&mut impl Verifier, crate::ProtocolError> { + T::verifier(self) + } + + fn signer( + &mut self, + ) -> Result<(impl signature::SignerMut, SignatureScheme), crate::ProtocolError> + { + T::signer(self) + } + + fn client_cert(&mut self) -> Option>> { + T::client_cert(self) + } +} + +pub struct NoSign; + +impl signature::Signer for NoSign { + fn try_sign(&self, _msg: &[u8]) -> Result { + unimplemented!() + } +} + +pub struct SkipVerifyProvider<'a, CipherSuite, RNG> { + rng: RNG, + priv_key: Option<&'a [u8]>, + client_cert: Option>, + _marker: PhantomData, +} + +impl SkipVerifyProvider<'_, (), RNG> { + pub fn new( + rng: RNG, + ) -> SkipVerifyProvider<'static, CipherSuite, RNG> { + SkipVerifyProvider { + rng, + priv_key: None, + client_cert: None, + _marker: PhantomData, + } + } +} + +impl<'a, CipherSuite: TlsCipherSuite, RNG: CryptoRngCore> SkipVerifyProvider<'a, CipherSuite, RNG> { + pub fn with_priv_key(mut self, priv_key: &'a [u8]) -> Self { + self.priv_key = Some(priv_key); + self + } + + pub fn with_cert(mut self, cert: Certificate<&'a [u8]>) -> Self { + self.client_cert = Some(cert); + self + } +} + +impl CryptoBackend + for SkipVerifyProvider<'_, CipherSuite, RNG> +{ + type CipherSuite = CipherSuite; + type Signature = p256::ecdsa::DerSignature; + + fn rng(&mut self) -> impl CryptoRngCore { + &mut self.rng + } + + fn signer( + &mut self, + ) -> Result<(impl signature::SignerMut, SignatureScheme), crate::ProtocolError> + { + let key_der = self.priv_key.ok_or(ProtocolError::InvalidPrivateKey)?; + let secret_key = + SecretKey::from_sec1_der(key_der).map_err(|_| ProtocolError::InvalidPrivateKey)?; + + Ok(( + SigningKey::from(&secret_key), + SignatureScheme::EcdsaSecp256r1Sha256, + )) + } + + fn client_cert(&mut self) -> Option>> { + self.client_cert.clone() + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ConnectContext<'a, CP> +where + CP: CryptoBackend, +{ + pub(crate) config: &'a ConnectConfig<'a>, + pub(crate) crypto_provider: CP, +} + +impl<'a, CP> ConnectContext<'a, CP> +where + CP: CryptoBackend, +{ + /// Create a new context with a given config and a crypto provider. + pub fn new(config: &'a ConnectConfig<'a>, crypto_provider: CP) -> Self { + Self { + config, + crypto_provider, + } + } +} + +impl<'a> ConnectConfig<'a> { + pub fn new() -> Self { + let mut config = Self { + signature_schemes: Vec::new(), + named_groups: Vec::new(), + max_fragment_length: None, + psk: None, + server_name: None, + alpn_protocols: None, + }; + + if cfg!(feature = "alloc") { + config = config.enable_rsa_signatures(); + } + + unwrap!( + config + .signature_schemes + .push(SignatureScheme::EcdsaSecp256r1Sha256) + .ok() + ); + unwrap!( + config + .signature_schemes + .push(SignatureScheme::EcdsaSecp384r1Sha384) + .ok() + ); + unwrap!(config.signature_schemes.push(SignatureScheme::Ed25519).ok()); + + unwrap!(config.named_groups.push(NamedGroup::Secp256r1)); + + config + } + + /// Enable RSA ciphers even if they might not be supported. + pub fn enable_rsa_signatures(mut self) -> Self { + unwrap!( + self.signature_schemes + .push(SignatureScheme::RsaPkcs1Sha256) + .ok() + ); + unwrap!( + self.signature_schemes + .push(SignatureScheme::RsaPkcs1Sha384) + .ok() + ); + unwrap!( + self.signature_schemes + .push(SignatureScheme::RsaPkcs1Sha512) + .ok() + ); + unwrap!( + self.signature_schemes + .push(SignatureScheme::RsaPssRsaeSha256) + .ok() + ); + unwrap!( + self.signature_schemes + .push(SignatureScheme::RsaPssRsaeSha384) + .ok() + ); + unwrap!( + self.signature_schemes + .push(SignatureScheme::RsaPssRsaeSha512) + .ok() + ); + self + } + + pub fn with_server_name(mut self, server_name: &'a str) -> Self { + self.server_name = Some(server_name); + self + } + + /// Configure ALPN protocol names to send in the ClientHello. + /// + /// The server will select one of the offered protocols and echo it back + /// in EncryptedExtensions. This is required for endpoints that multiplex + /// protocols on a single port (e.g. AWS IoT Core MQTT over port 443). + pub fn with_alpn(mut self, protocols: &'a [&'a [u8]]) -> Self { + self.alpn_protocols = Some(protocols); + self + } + + /// Configures the maximum plaintext fragment size. + /// + /// This option may help reduce memory size, as smaller fragment lengths require smaller + /// read/write buffers. Note that mote-tls does not currently use this option to fragment + /// writes. Note that the buffers need to include some overhead over the configured fragment + /// length. + /// + /// From [RFC 6066, Section 4. Maximum Fragment Length Negotiation](https://www.rfc-editor.org/rfc/rfc6066#page-8): + /// + /// > Without this extension, TLS specifies a fixed maximum plaintext + /// > fragment length of 2^14 bytes. It may be desirable for constrained + /// > clients to negotiate a smaller maximum fragment length due to memory + /// > limitations or bandwidth limitations. + /// + /// > For example, if the negotiated length is 2^9=512, then, when using currently defined + /// > cipher suites ([...]) and null compression, the record-layer output can be at most + /// > 805 bytes: 5 bytes of headers, 512 bytes of application data, 256 bytes of padding, + /// > and 32 bytes of MAC. + pub fn with_max_fragment_length(mut self, max_fragment_length: MaxFragmentLength) -> Self { + self.max_fragment_length = Some(max_fragment_length); + self + } + + /// Resets the max fragment length to 14 bits (16384). + pub fn reset_max_fragment_length(mut self) -> Self { + self.max_fragment_length = None; + self + } + + pub fn with_psk(mut self, psk: &'a [u8], identities: &[&'a [u8]]) -> Self { + // TODO: Remove potential panic + self.psk = Some((psk, unwrap!(Vec::from_slice(identities).ok()))); + self + } +} + +impl Default for ConnectConfig<'_> { + fn default() -> Self { + ConnectConfig::new() + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Certificate { + X509(D), + RawPublicKey(D), +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..9b956ec --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,636 @@ +use crate::config::{TlsCipherSuite, ConnectConfig}; +use crate::handshake::{ClientHandshake, ServerHandshake}; +use crate::key_schedule::{KeySchedule, ReadKeySchedule, WriteKeySchedule}; +use crate::record::{ClientRecord, ServerRecord}; +use crate::record_reader::RecordReader; +use crate::write_buffer::WriteBuffer; +use crate::{HandshakeVerify, CryptoBackend, ProtocolError, Verifier}; +use crate::{ + alert::{Alert, AlertDescription, AlertLevel}, + handshake::{certificate::CertificateRef, certificate_request::CertificateRequest}, +}; +use core::fmt::Debug; +use digest::Digest; +use embedded_io::Error as _; +use embedded_io::{Read as BlockingRead, Write as BlockingWrite}; +use embedded_io_async::{Read as AsyncRead, Write as AsyncWrite}; + +use crate::application_data::ApplicationData; +use crate::buffer::CryptoBuffer; +use digest::generic_array::typenum::Unsigned; +use p256::ecdh::EphemeralSecret; +use signature::SignerMut; + +use crate::content_types::ContentType; +use crate::parse_buffer::ParseBuffer; +use aes_gcm::aead::{AeadCore, AeadInPlace, KeyInit}; + +pub(crate) fn decrypt_record( + key_schedule: &mut ReadKeySchedule, + record: ServerRecord<'_, CipherSuite>, + mut cb: impl FnMut( + &mut ReadKeySchedule, + ServerRecord<'_, CipherSuite>, + ) -> Result<(), ProtocolError>, +) -> Result<(), ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + if let ServerRecord::ApplicationData(ApplicationData { + header, + data: mut app_data, + }) = record + { + let server_key = key_schedule.get_key()?; + let nonce = key_schedule.get_nonce()?; + + let crypto = ::new(server_key); + crypto + .decrypt_in_place(&nonce, header.data(), &mut app_data) + .map_err(|_| ProtocolError::CryptoError)?; + + let padding = app_data + .as_slice() + .iter() + .enumerate() + .rfind(|(_, b)| **b != 0); + if let Some((index, _)) = padding { + app_data.truncate(index + 1); + }; + + let content_type = + ContentType::of(*app_data.as_slice().last().unwrap()).ok_or(ProtocolError::InvalidRecord)?; + + trace!("Decrypting: content type = {:?}", content_type); + + // Remove the content type + app_data.truncate(app_data.len() - 1); + + let mut buf = ParseBuffer::new(app_data.as_slice()); + match content_type { + ContentType::Handshake => { + // Decode potentially coalesced handshake messages + while buf.remaining() > 0 { + let inner = ServerHandshake::read(&mut buf, key_schedule.transcript_hash())?; + cb(key_schedule, ServerRecord::Handshake(inner))?; + } + } + ContentType::ApplicationData => { + let inner = ApplicationData::new(app_data, header); + cb(key_schedule, ServerRecord::ApplicationData(inner))?; + } + ContentType::Alert => { + let alert = Alert::parse(&mut buf)?; + cb(key_schedule, ServerRecord::Alert(alert))?; + } + _ => return Err(ProtocolError::Unimplemented), + } + key_schedule.increment_counter(); + } else { + trace!("Not decrypting: content_type = {:?}", record.content_type()); + cb(key_schedule, record)?; + } + Ok(()) +} + +pub(crate) fn encrypt( + key_schedule: &WriteKeySchedule, + buf: &mut CryptoBuffer<'_>, +) -> Result<(), ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + let client_key = key_schedule.get_key()?; + let nonce = key_schedule.get_nonce()?; + // trace!("encrypt key {:02x?}", client_key); + // trace!("encrypt nonce {:02x?}", nonce); + // trace!("plaintext {} {:02x?}", buf.len(), buf.as_slice(),); + //let crypto = Aes128Gcm::new_varkey(&self.key_schedule.get_client_key()).unwrap(); + let crypto = ::new(client_key); + let len = buf.len() + ::TagSize::to_usize(); + + if len > buf.capacity() { + return Err(ProtocolError::InsufficientSpace); + } + + trace!("output size {}", len); + let len_bytes = (len as u16).to_be_bytes(); + let additional_data = [ + ContentType::ApplicationData as u8, + 0x03, + 0x03, + len_bytes[0], + len_bytes[1], + ]; + + crypto + .encrypt_in_place(&nonce, &additional_data, buf) + .map_err(|_| ProtocolError::InvalidApplicationData) +} + +pub struct Handshake +where + CipherSuite: TlsCipherSuite, +{ + traffic_hash: Option, + secret: Option, + certificate_request: Option, +} + +impl Handshake +where + CipherSuite: TlsCipherSuite, +{ + pub fn new() -> Handshake { + Handshake { + traffic_hash: None, + secret: None, + certificate_request: None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + ClientHello, + ServerHello, + ServerVerify, + ClientCert, + ClientCertVerify, + ClientFinished, + ApplicationData, +} + +impl<'a> State { + #[allow(clippy::too_many_arguments)] + pub async fn process<'v, Transport, CP>( + self, + transport: &mut Transport, + handshake: &mut Handshake, + record_reader: &mut RecordReader<'_>, + tx_buf: &mut WriteBuffer<'_>, + key_schedule: &mut KeySchedule, + config: &ConnectConfig<'a>, + crypto_provider: &mut CP, + ) -> Result + where + Transport: AsyncRead + AsyncWrite + 'a, + CP: CryptoBackend, + { + match self { + State::ClientHello => { + let (state, tx) = + client_hello(key_schedule, config, crypto_provider, tx_buf, handshake)?; + + respond(tx, transport, key_schedule).await?; + + Ok(state) + } + State::ServerHello => { + let record = record_reader + .read(transport, key_schedule.read_state()) + .await?; + + let result = process_server_hello(handshake, key_schedule, record); + + handle_processing_error(result, transport, key_schedule, tx_buf).await + } + State::ServerVerify => { + let record = record_reader + .read(transport, key_schedule.read_state()) + .await?; + + let result = + process_server_verify(handshake, key_schedule, crypto_provider, record); + + handle_processing_error(result, transport, key_schedule, tx_buf).await + } + State::ClientCert => { + let (state, tx) = client_cert(handshake, key_schedule, crypto_provider, tx_buf)?; + + respond(tx, transport, key_schedule).await?; + + Ok(state) + } + State::ClientCertVerify => { + let (result, tx) = client_cert_verify(key_schedule, crypto_provider, tx_buf)?; + + respond(tx, transport, key_schedule).await?; + + result + } + State::ClientFinished => { + let tx = client_finished(key_schedule, tx_buf)?; + + respond(tx, transport, key_schedule).await?; + + client_finished_finalize(key_schedule, handshake) + } + State::ApplicationData => Ok(State::ApplicationData), + } + } + + #[allow(clippy::too_many_arguments)] + pub fn process_blocking<'v, Transport, CP>( + self, + transport: &mut Transport, + handshake: &mut Handshake, + record_reader: &mut RecordReader<'_>, + tx_buf: &mut WriteBuffer, + key_schedule: &mut KeySchedule, + config: &ConnectConfig<'a>, + crypto_provider: &mut CP, + ) -> Result + where + Transport: BlockingRead + BlockingWrite + 'a, + CP: CryptoBackend, + { + match self { + State::ClientHello => { + let (state, tx) = + client_hello(key_schedule, config, crypto_provider, tx_buf, handshake)?; + + respond_blocking(tx, transport, key_schedule)?; + + Ok(state) + } + State::ServerHello => { + let record = record_reader.read_blocking(transport, key_schedule.read_state())?; + + let result = process_server_hello(handshake, key_schedule, record); + + handle_processing_error_blocking(result, transport, key_schedule, tx_buf) + } + State::ServerVerify => { + let record = record_reader.read_blocking(transport, key_schedule.read_state())?; + + let result = + process_server_verify(handshake, key_schedule, crypto_provider, record); + + handle_processing_error_blocking(result, transport, key_schedule, tx_buf) + } + State::ClientCert => { + let (state, tx) = client_cert(handshake, key_schedule, crypto_provider, tx_buf)?; + + respond_blocking(tx, transport, key_schedule)?; + + Ok(state) + } + State::ClientCertVerify => { + let (result, tx) = client_cert_verify(key_schedule, crypto_provider, tx_buf)?; + + respond_blocking(tx, transport, key_schedule)?; + + result + } + State::ClientFinished => { + let tx = client_finished(key_schedule, tx_buf)?; + + respond_blocking(tx, transport, key_schedule)?; + + client_finished_finalize(key_schedule, handshake) + } + State::ApplicationData => Ok(State::ApplicationData), + } + } +} + +fn handle_processing_error_blocking( + result: Result, + transport: &mut impl BlockingWrite, + key_schedule: &mut KeySchedule, + tx_buf: &mut WriteBuffer, +) -> Result +where + CipherSuite: TlsCipherSuite, +{ + if let Err(ProtocolError::AbortHandshake(level, description)) = result { + let (write_key_schedule, read_key_schedule) = key_schedule.as_split(); + let tx = tx_buf.write_record( + &ClientRecord::Alert(Alert { level, description }, false), + write_key_schedule, + Some(read_key_schedule), + )?; + + respond_blocking(tx, transport, key_schedule)?; + } + + result +} + +fn respond_blocking( + tx: &[u8], + transport: &mut impl BlockingWrite, + key_schedule: &mut KeySchedule, +) -> Result<(), ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + transport + .write_all(tx) + .map_err(|e| ProtocolError::Io(e.kind()))?; + + key_schedule.write_state().increment_counter(); + + transport.flush().map_err(|e| ProtocolError::Io(e.kind()))?; + + Ok(()) +} + +async fn handle_processing_error( + result: Result, + transport: &mut impl AsyncWrite, + key_schedule: &mut KeySchedule, + tx_buf: &mut WriteBuffer<'_>, +) -> Result +where + CipherSuite: TlsCipherSuite, +{ + if let Err(ProtocolError::AbortHandshake(level, description)) = result { + let (write_key_schedule, read_key_schedule) = key_schedule.as_split(); + let tx = tx_buf.write_record( + &ClientRecord::Alert(Alert { level, description }, false), + write_key_schedule, + Some(read_key_schedule), + )?; + + respond(tx, transport, key_schedule).await?; + } + + result +} + +async fn respond( + tx: &[u8], + transport: &mut impl AsyncWrite, + key_schedule: &mut KeySchedule, +) -> Result<(), ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + transport + .write_all(tx) + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + + key_schedule.write_state().increment_counter(); + + transport + .flush() + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + + Ok(()) +} + +fn client_hello<'r, CP>( + key_schedule: &mut KeySchedule, + config: &ConnectConfig, + crypto_provider: &mut CP, + tx_buf: &'r mut WriteBuffer, + handshake: &mut Handshake, +) -> Result<(State, &'r [u8]), ProtocolError> +where + CP: CryptoBackend, +{ + key_schedule.initialize_early_secret(config.psk.as_ref().map(|p| p.0))?; + let (write_key_schedule, read_key_schedule) = key_schedule.as_split(); + let client_hello = ClientRecord::client_hello(config, crypto_provider); + let slice = tx_buf.write_record(&client_hello, write_key_schedule, Some(read_key_schedule))?; + + if let ClientRecord::Handshake(ClientHandshake::ClientHello(client_hello), _) = client_hello { + handshake.secret.replace(client_hello.secret); + Ok((State::ServerHello, slice)) + } else { + Err(ProtocolError::EncodeError) + } +} + +fn process_server_hello( + handshake: &mut Handshake, + key_schedule: &mut KeySchedule, + record: ServerRecord<'_, CipherSuite>, +) -> Result +where + CipherSuite: TlsCipherSuite, +{ + match record { + ServerRecord::Handshake(server_handshake) => match server_handshake { + ServerHandshake::ServerHello(server_hello) => { + trace!("********* ServerHello"); + let secret = handshake.secret.take().ok_or(ProtocolError::InvalidHandshake)?; + let shared = server_hello + .calculate_shared_secret(&secret) + .ok_or(ProtocolError::InvalidKeyShare)?; + key_schedule.initialize_handshake_secret(shared.raw_secret_bytes())?; + Ok(State::ServerVerify) + } + _ => Err(ProtocolError::InvalidHandshake), + }, + ServerRecord::Alert(alert) => { + Err(ProtocolError::HandshakeAborted(alert.level, alert.description)) + } + _ => Err(ProtocolError::InvalidRecord), + } +} + +fn process_server_verify( + handshake: &mut Handshake, + key_schedule: &mut KeySchedule, + crypto_provider: &mut CP, + record: ServerRecord<'_, CP::CipherSuite>, +) -> Result +where + CP: CryptoBackend, +{ + let mut state = State::ServerVerify; + decrypt_record(key_schedule.read_state(), record, |key_schedule, record| { + match record { + ServerRecord::Handshake(server_handshake) => { + match server_handshake { + ServerHandshake::EncryptedExtensions(_) => {} + ServerHandshake::Certificate(certificate) => { + let transcript = key_schedule.transcript_hash(); + if let Ok(verifier) = crypto_provider.verifier() { + verifier.verify_certificate(transcript, certificate)?; + debug!("Certificate verified!"); + } else { + debug!("Certificate verification skipped due to no verifier!"); + } + } + ServerHandshake::HandshakeVerify(verify) => { + if let Ok(verifier) = crypto_provider.verifier() { + verifier.verify_signature(verify)?; + debug!("Signature verified!"); + } else { + debug!("Signature verification skipped due to no verifier!"); + } + } + ServerHandshake::CertificateRequest(request) => { + handshake.certificate_request.replace(request.try_into()?); + } + ServerHandshake::Finished(finished) => { + if !key_schedule.verify_server_finished(&finished)? { + warn!("Server signature verification failed"); + return Err(ProtocolError::InvalidSignature); + } + + // trace!("server verified {}", verified); + state = if handshake.certificate_request.is_some() { + State::ClientCert + } else { + handshake + .traffic_hash + .replace(key_schedule.transcript_hash().clone()); + State::ClientFinished + }; + } + _ => return Err(ProtocolError::InvalidHandshake), + } + } + ServerRecord::ChangeCipherSpec(_) => {} + _ => return Err(ProtocolError::InvalidRecord), + } + + Ok(()) + })?; + Ok(state) +} + +fn client_cert<'r, CP>( + handshake: &mut Handshake, + key_schedule: &mut KeySchedule, + crypto_provider: &mut CP, + buffer: &'r mut WriteBuffer, +) -> Result<(State, &'r [u8]), ProtocolError> +where + CP: CryptoBackend, +{ + handshake + .traffic_hash + .replace(key_schedule.transcript_hash().clone()); + + let request_context = &handshake + .certificate_request + .as_ref() + .ok_or(ProtocolError::InvalidHandshake)? + .request_context; + + // Declare cert before certificate so owned data outlives the CertificateRef that borrows it + let cert = crypto_provider.client_cert(); + let mut certificate = CertificateRef::with_context(request_context); + let next_state = if let Some(ref cert) = cert { + certificate.add(cert.into())?; + State::ClientCertVerify + } else { + State::ClientFinished + }; + let (write_key_schedule, read_key_schedule) = key_schedule.as_split(); + + buffer + .write_record( + &ClientRecord::Handshake(ClientHandshake::ClientCert(certificate), true), + write_key_schedule, + Some(read_key_schedule), + ) + .map(|slice| (next_state, slice)) +} + +fn client_cert_verify<'r, CP>( + key_schedule: &mut KeySchedule, + crypto_provider: &mut CP, + buffer: &'r mut WriteBuffer, +) -> Result<(Result, &'r [u8]), ProtocolError> +where + CP: CryptoBackend, +{ + let (result, record) = match crypto_provider.signer() { + Ok((mut signing_key, signature_scheme)) => { + let ctx_str = b"TLS 1.3, client CertificateVerify\x00"; + + // 64 (pad) + 34 (ctx) + 48 (SHA-384) = 146 bytes required + let mut msg: heapless::Vec = heapless::Vec::new(); + msg.resize(64, 0x20).map_err(|_| ProtocolError::EncodeError)?; + msg.extend_from_slice(ctx_str) + .map_err(|_| ProtocolError::EncodeError)?; + msg.extend_from_slice(&key_schedule.transcript_hash().clone().finalize()) + .map_err(|_| ProtocolError::EncodeError)?; + + let signature = signing_key.sign(&msg); + + trace!( + "Signature: {:?} ({})", + signature.as_ref(), + signature.as_ref().len() + ); + + let certificate_verify = HandshakeVerify { + signature_scheme, + signature: heapless::Vec::from_slice(signature.as_ref()).unwrap(), + }; + + ( + Ok(State::ClientFinished), + ClientRecord::Handshake( + ClientHandshake::ClientCertVerify(certificate_verify), + true, + ), + ) + } + Err(e) => { + error!("Failed to obtain signing key: {:?}", e); + ( + Err(e), + ClientRecord::Alert( + Alert::new(AlertLevel::Warning, AlertDescription::CloseNotify), + true, + ), + ) + } + }; + + let (write_key_schedule, read_key_schedule) = key_schedule.as_split(); + + buffer + .write_record(&record, write_key_schedule, Some(read_key_schedule)) + .map(|slice| (result, slice)) +} + +fn client_finished<'r, CipherSuite>( + key_schedule: &mut KeySchedule, + buffer: &'r mut WriteBuffer, +) -> Result<&'r [u8], ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + let client_finished = key_schedule + .create_client_finished() + .map_err(|_| ProtocolError::InvalidHandshake)?; + + let (write_key_schedule, read_key_schedule) = key_schedule.as_split(); + + buffer.write_record( + &ClientRecord::Handshake(ClientHandshake::Finished(client_finished), true), + write_key_schedule, + Some(read_key_schedule), + ) +} + +fn client_finished_finalize( + key_schedule: &mut KeySchedule, + handshake: &mut Handshake, +) -> Result +where + CipherSuite: TlsCipherSuite, +{ + key_schedule.replace_transcript_hash( + handshake + .traffic_hash + .take() + .ok_or(ProtocolError::InvalidHandshake)?, + ); + key_schedule.initialize_master_secret()?; + + Ok(State::ApplicationData) +} diff --git a/src/content_types.rs b/src/content_types.rs new file mode 100644 index 0000000..b81d925 --- /dev/null +++ b/src/content_types.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ContentType { + Invalid = 0, + ChangeCipherSpec = 20, + Alert = 21, + Handshake = 22, + ApplicationData = 23, +} + +impl ContentType { + pub fn of(num: u8) -> Option { + match num { + 0 => Some(Self::Invalid), + 20 => Some(Self::ChangeCipherSpec), + 21 => Some(Self::Alert), + 22 => Some(Self::Handshake), + 23 => Some(Self::ApplicationData), + _ => None, + } + } +} diff --git a/src/extensions/extension_data/alpn.rs b/src/extensions/extension_data/alpn.rs new file mode 100644 index 0000000..48444aa --- /dev/null +++ b/src/extensions/extension_data/alpn.rs @@ -0,0 +1,56 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +/// ALPN protocol name list per RFC 7301, Section 3.1. +/// +/// Wire format: +/// ```text +/// opaque ProtocolName<1..2^8-1>; +/// +/// struct { +/// ProtocolName protocol_name_list<2..2^16-1> +/// } ProtocolNameList; +/// ``` +#[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AlpnProtocolNameList<'a> { + pub protocols: &'a [&'a [u8]], +} + +impl<'a> AlpnProtocolNameList<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + // We parse but don't store the individual protocol names in a heapless + // container — just validate the wire format. The slice reference is kept + // for the lifetime of the parse buffer, but since we can't reconstruct + // `&[&[u8]]` from a flat buffer without allocation, we store an empty + // slice. Callers that need the parsed protocols (server-side) would need + // a different approach; for our client-side use we only need encode(). + let list_len = buf.read_u16()? as usize; + let mut list_buf = buf.slice(list_len)?; + + while !list_buf.is_empty() { + let name_len = list_buf.read_u8()? as usize; + if name_len == 0 { + return Err(ParseError::InvalidData); + } + let _name = list_buf.slice(name_len)?; + } + + Ok(Self { protocols: &[] }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + // Outer u16 length prefix for the ProtocolNameList + buf.with_u16_length(|buf| { + for protocol in self.protocols { + buf.push(protocol.len() as u8) + .map_err(|_| ProtocolError::EncodeError)?; + buf.extend_from_slice(protocol)?; + } + Ok(()) + }) + } +} diff --git a/src/extensions/extension_data/key_share.rs b/src/extensions/extension_data/key_share.rs new file mode 100644 index 0000000..31638c7 --- /dev/null +++ b/src/extensions/extension_data/key_share.rs @@ -0,0 +1,135 @@ +use heapless::Vec; + +use crate::buffer::CryptoBuffer; +use crate::extensions::extension_data::supported_groups::NamedGroup; + +use crate::ProtocolError; +use crate::parse_buffer::{ParseBuffer, ParseError}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct KeyShareServerHello<'a>(pub KeyShareEntry<'a>); + +impl<'a> KeyShareServerHello<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + Ok(KeyShareServerHello(KeyShareEntry::parse(buf)?)) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + self.0.encode(buf) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct KeyShareClientHello<'a, const N: usize> { + pub client_shares: Vec, N>, +} + +impl<'a, const N: usize> KeyShareClientHello<'a, N> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + let len = buf.read_u16()? as usize; + Ok(KeyShareClientHello { + client_shares: buf.read_list(len, KeyShareEntry::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u16_length(|buf| { + for client_share in &self.client_shares { + client_share.encode(buf)?; + } + Ok(()) + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct KeyShareHelloRetryRequest { + pub selected_group: NamedGroup, +} + +#[allow(dead_code)] +impl KeyShareHelloRetryRequest { + pub fn parse(buf: &mut ParseBuffer) -> Result { + Ok(Self { + selected_group: NamedGroup::parse(buf)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + self.selected_group.encode(buf) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct KeyShareEntry<'a> { + pub(crate) group: NamedGroup, + pub(crate) opaque: &'a [u8], +} + +impl<'a> KeyShareEntry<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + let group = NamedGroup::parse(buf)?; + + let opaque_len = buf.read_u16()?; + let opaque = buf.slice(opaque_len as usize)?; + + Ok(Self { + group, + opaque: opaque.as_slice(), + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + self.group.encode(buf)?; + + buf.with_u16_length(|buf| buf.extend_from_slice(self.opaque)) + .map_err(|_| ProtocolError::EncodeError) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::sync::Once; + + static INIT: Once = Once::new(); + + fn setup() { + INIT.call_once(|| { + env_logger::init(); + }); + } + + #[test] + fn test_parse_empty() { + setup(); + let buffer = [ + 0x00, 0x17, // Secp256r1 + 0x00, 0x00, // key_exchange length = 0 bytes + ]; + let result = KeyShareEntry::parse(&mut ParseBuffer::new(&buffer)).unwrap(); + + assert_eq!(NamedGroup::Secp256r1, result.group); + assert_eq!(0, result.opaque.len()); + } + + #[test] + fn test_parse() { + setup(); + let buffer = [ + 0x00, 0x17, // Secp256r1 + 0x00, 0x02, // key_exchange length = 2 bytes + 0xAA, 0xBB, + ]; + let result = KeyShareEntry::parse(&mut ParseBuffer::new(&buffer)).unwrap(); + + assert_eq!(NamedGroup::Secp256r1, result.group); + assert_eq!(2, result.opaque.len()); + assert_eq!([0xAA, 0xBB], result.opaque); + } +} diff --git a/src/extensions/extension_data/max_fragment_length.rs b/src/extensions/extension_data/max_fragment_length.rs new file mode 100644 index 0000000..881b27a --- /dev/null +++ b/src/extensions/extension_data/max_fragment_length.rs @@ -0,0 +1,44 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +/// Maximum plaintext fragment length +/// +/// RFC 6066, Section 4. Maximum Fragment Length Negotiation +/// Without this extension, TLS specifies a fixed maximum plaintext +/// fragment length of 2^14 bytes. It may be desirable for constrained +/// clients to negotiate a smaller maximum fragment length due to memory +/// limitations or bandwidth limitations. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaxFragmentLength { + /// 512 bytes + Bits9 = 1, + /// 1024 bytes + Bits10 = 2, + /// 2048 bytes + Bits11 = 3, + /// 4096 bytes + Bits12 = 4, +} + +impl MaxFragmentLength { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u8()? { + 1 => Ok(Self::Bits9), + 2 => Ok(Self::Bits10), + 3 => Ok(Self::Bits11), + 4 => Ok(Self::Bits12), + other => { + warn!("Read unknown MaxFragmentLength: {}", other); + Err(ParseError::InvalidData) + } + } + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push(*self as u8).map_err(|_| ProtocolError::EncodeError) + } +} diff --git a/src/extensions/extension_data/mod.rs b/src/extensions/extension_data/mod.rs new file mode 100644 index 0000000..6ea5323 --- /dev/null +++ b/src/extensions/extension_data/mod.rs @@ -0,0 +1,11 @@ +pub mod alpn; +pub mod key_share; +pub mod max_fragment_length; +pub mod pre_shared_key; +pub mod psk_key_exchange_modes; +pub mod server_name; +pub mod signature_algorithms; +pub mod signature_algorithms_cert; +pub mod supported_groups; +pub mod supported_versions; +pub mod unimplemented; diff --git a/src/extensions/extension_data/pre_shared_key.rs b/src/extensions/extension_data/pre_shared_key.rs new file mode 100644 index 0000000..49f4875 --- /dev/null +++ b/src/extensions/extension_data/pre_shared_key.rs @@ -0,0 +1,63 @@ +use crate::buffer::CryptoBuffer; + +use crate::ProtocolError; +use crate::parse_buffer::{ParseBuffer, ParseError}; + +use heapless::Vec; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PreSharedKeyClientHello<'a, const N: usize> { + pub identities: Vec<&'a [u8], N>, + pub hash_size: usize, +} + +impl PreSharedKeyClientHello<'_, N> { + pub fn parse(_buf: &mut ParseBuffer) -> Result { + unimplemented!() + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u16_length(|buf| { + for identity in &self.identities { + buf.with_u16_length(|buf| buf.extend_from_slice(identity)) + .map_err(|_| ProtocolError::EncodeError)?; + + // NOTE: No support for ticket age, set to 0 as recommended by RFC + buf.push_u32(0).map_err(|_| ProtocolError::EncodeError)?; + } + Ok(()) + }) + .map_err(|_| ProtocolError::EncodeError)?; + + // NOTE: We encode binders later after computing the transcript. + let binders_len = (1 + self.hash_size) * self.identities.len(); + buf.push_u16(binders_len as u16) + .map_err(|_| ProtocolError::EncodeError)?; + + for _ in 0..binders_len { + buf.push(0).map_err(|_| ProtocolError::EncodeError)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PreSharedKeyServerHello { + pub selected_identity: u16, +} + +impl PreSharedKeyServerHello { + pub fn parse(buf: &mut ParseBuffer) -> Result { + Ok(Self { + selected_identity: buf.read_u16()?, + }) + } + + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push_u16(self.selected_identity) + .map_err(|_| ProtocolError::EncodeError) + } +} diff --git a/src/extensions/extension_data/psk_key_exchange_modes.rs b/src/extensions/extension_data/psk_key_exchange_modes.rs new file mode 100644 index 0000000..830b2ee --- /dev/null +++ b/src/extensions/extension_data/psk_key_exchange_modes.rs @@ -0,0 +1,53 @@ +use crate::buffer::CryptoBuffer; + +use crate::ProtocolError; +use crate::parse_buffer::{ParseBuffer, ParseError}; + +use heapless::Vec; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PskKeyExchangeMode { + PskKe = 0, + PskDheKe = 1, +} +impl PskKeyExchangeMode { + fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u8()? { + 0 => Ok(Self::PskKe), + 1 => Ok(Self::PskDheKe), + other => { + warn!("Read unknown PskKeyExchangeMode: {}", other); + Err(ParseError::InvalidData) + } + } + } + + fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push(self as u8).map_err(|_| ProtocolError::EncodeError) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PskKeyExchangeModes { + pub modes: Vec, +} +impl PskKeyExchangeModes { + pub fn parse(buf: &mut ParseBuffer) -> Result { + let data_length = buf.read_u8()? as usize; + + Ok(Self { + modes: buf.read_list::<_, N>(data_length, PskKeyExchangeMode::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u8_length(|buf| { + for mode in &self.modes { + mode.encode(buf)?; + } + Ok(()) + }) + } +} diff --git a/src/extensions/extension_data/server_name.rs b/src/extensions/extension_data/server_name.rs new file mode 100644 index 0000000..14cc25c --- /dev/null +++ b/src/extensions/extension_data/server_name.rs @@ -0,0 +1,133 @@ +use heapless::Vec; + +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + extensions::ExtensionType, + parse_buffer::{ParseBuffer, ParseError}, +}; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum NameType { + HostName = 0, +} + +impl NameType { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u8()? { + 0 => Ok(Self::HostName), + other => { + warn!("Read unknown NameType: {}", other); + Err(ParseError::InvalidData) + } + } + } + + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push(self as u8).map_err(|_| ProtocolError::EncodeError) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ServerName<'a> { + pub name_type: NameType, + pub name: &'a str, +} + +impl<'a> ServerName<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ParseError> { + let name_type = NameType::parse(buf)?; + let name_len = buf.read_u16()?; + let name = buf.slice(name_len as usize)?.as_slice(); + + // RFC 6066, Section 3. Server Name Indication + // The hostname is represented as a byte + // string using ASCII encoding without a trailing dot. + if name.is_ascii() { + Ok(ServerName { + name_type, + name: core::str::from_utf8(name).map_err(|_| ParseError::InvalidData)?, + }) + } else { + Err(ParseError::InvalidData) + } + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + self.name_type.encode(buf)?; + + buf.with_u16_length(|buf| buf.extend_from_slice(self.name.as_bytes())) + .map_err(|_| ProtocolError::EncodeError) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ServerNameList<'a, const N: usize> { + pub names: Vec, N>, +} + +impl<'a> ServerNameList<'a, 1> { + pub fn single(server_name: &'a str) -> Self { + let mut names = Vec::<_, 1>::new(); + + names + .push(ServerName { + name_type: NameType::HostName, + name: server_name, + }) + .unwrap(); + + ServerNameList { names } + } +} + +impl<'a, const N: usize> ServerNameList<'a, N> { + #[allow(dead_code)] + pub const EXTENSION_TYPE: ExtensionType = ExtensionType::ServerName; + + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ParseError> { + let data_length = buf.read_u16()? as usize; + + Ok(Self { + names: buf.read_list::<_, N>(data_length, ServerName::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u16_length(|buf| { + for name in &self.names { + name.encode(buf)?; + } + + Ok(()) + }) + } +} + +// RFC 6066, Section 3. Server Name Indication +// A server that receives a client hello containing the "server_name" +// extension [..]. In this event, the server +// SHALL include an extension of type "server_name" in the (extended) +// server hello. The "extension_data" field of this extension SHALL be +// empty. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ServerNameResponse; + +impl ServerNameResponse { + pub fn parse(buf: &mut ParseBuffer) -> Result { + if buf.is_empty() { + Ok(Self) + } else { + Err(ParseError::InvalidData) + } + } + + #[allow(clippy::unused_self, clippy::unnecessary_wraps)] + pub fn encode(&self, _buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + Ok(()) + } +} diff --git a/src/extensions/extension_data/signature_algorithms.rs b/src/extensions/extension_data/signature_algorithms.rs new file mode 100644 index 0000000..5a3a2c5 --- /dev/null +++ b/src/extensions/extension_data/signature_algorithms.rs @@ -0,0 +1,164 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +use heapless::Vec; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SignatureScheme { + /* RSASSA-PKCS1-v1_5 algorithms */ + RsaPkcs1Sha256, + RsaPkcs1Sha384, + RsaPkcs1Sha512, + + /* ECDSA algorithms */ + EcdsaSecp256r1Sha256, + EcdsaSecp384r1Sha384, + EcdsaSecp521r1Sha512, + + /* RSASSA-PSS algorithms with public key OID rsaEncryption */ + RsaPssRsaeSha256, + RsaPssRsaeSha384, + RsaPssRsaeSha512, + + /* EdDSA algorithms */ + Ed25519, + Ed448, + + /* RSASSA-PSS algorithms with public key OID RSASSA-PSS */ + RsaPssPssSha256, + RsaPssPssSha384, + RsaPssPssSha512, + + Sha224Ecdsa, + Sha224Rsa, + Sha224Dsa, + + /* Legacy algorithms */ + RsaPkcs1Sha1, + EcdsaSha1, + + /* Brainpool */ + Sha256BrainpoolP256r1, + Sha384BrainpoolP384r1, + Sha512BrainpoolP512r1, + + /* ML-DSA */ + MlDsa44, + MlDsa65, + MlDsa87, + /* Reserved Code Points */ + //private_use(0xFE00..0xFFFF), + //(0xFFFF) +} + +impl SignatureScheme { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u16()? { + 0x0401 => Ok(Self::RsaPkcs1Sha256), + 0x0501 => Ok(Self::RsaPkcs1Sha384), + 0x0601 => Ok(Self::RsaPkcs1Sha512), + + 0x0403 => Ok(Self::EcdsaSecp256r1Sha256), + 0x0503 => Ok(Self::EcdsaSecp384r1Sha384), + 0x0603 => Ok(Self::EcdsaSecp521r1Sha512), + + 0x0804 => Ok(Self::RsaPssRsaeSha256), + 0x0805 => Ok(Self::RsaPssRsaeSha384), + 0x0806 => Ok(Self::RsaPssRsaeSha512), + + 0x0807 => Ok(Self::Ed25519), + 0x0808 => Ok(Self::Ed448), + + 0x0809 => Ok(Self::RsaPssPssSha256), + 0x080a => Ok(Self::RsaPssPssSha384), + 0x080b => Ok(Self::RsaPssPssSha512), + + 0x0303 => Ok(Self::Sha224Ecdsa), + 0x0301 => Ok(Self::Sha224Rsa), + 0x0302 => Ok(Self::Sha224Dsa), + + 0x0201 => Ok(Self::RsaPkcs1Sha1), + 0x0203 => Ok(Self::EcdsaSha1), + + 0x081A => Ok(Self::Sha256BrainpoolP256r1), + 0x081B => Ok(Self::Sha384BrainpoolP384r1), + 0x081C => Ok(Self::Sha512BrainpoolP512r1), + + 0x0904 => Ok(Self::MlDsa44), + 0x0905 => Ok(Self::MlDsa65), + 0x0906 => Ok(Self::MlDsa87), + + _ => Err(ParseError::InvalidData), + } + } + + #[must_use] + pub fn as_u16(self) -> u16 { + match self { + Self::RsaPkcs1Sha256 => 0x0401, + Self::RsaPkcs1Sha384 => 0x0501, + Self::RsaPkcs1Sha512 => 0x0601, + + Self::EcdsaSecp256r1Sha256 => 0x0403, + Self::EcdsaSecp384r1Sha384 => 0x0503, + Self::EcdsaSecp521r1Sha512 => 0x0603, + + Self::RsaPssRsaeSha256 => 0x0804, + Self::RsaPssRsaeSha384 => 0x0805, + Self::RsaPssRsaeSha512 => 0x0806, + + Self::Ed25519 => 0x0807, + Self::Ed448 => 0x0808, + + Self::RsaPssPssSha256 => 0x0809, + Self::RsaPssPssSha384 => 0x080a, + Self::RsaPssPssSha512 => 0x080b, + + Self::Sha224Ecdsa => 0x0303, + Self::Sha224Rsa => 0x0301, + Self::Sha224Dsa => 0x0302, + + Self::RsaPkcs1Sha1 => 0x0201, + Self::EcdsaSha1 => 0x0203, + + Self::Sha256BrainpoolP256r1 => 0x081A, + Self::Sha384BrainpoolP384r1 => 0x081B, + Self::Sha512BrainpoolP512r1 => 0x081C, + + Self::MlDsa44 => 0x0904, + Self::MlDsa65 => 0x0905, + Self::MlDsa87 => 0x0906, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SignatureAlgorithms { + pub supported_signature_algorithms: Vec, +} + +impl SignatureAlgorithms { + pub fn parse(buf: &mut ParseBuffer) -> Result { + let data_length = buf.read_u16()? as usize; + + Ok(Self { + supported_signature_algorithms: buf + .read_list::<_, N>(data_length, SignatureScheme::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u16_length(|buf| { + for &a in &self.supported_signature_algorithms { + buf.push_u16(a.as_u16()) + .map_err(|_| ProtocolError::EncodeError)?; + } + Ok(()) + }) + } +} diff --git a/src/extensions/extension_data/signature_algorithms_cert.rs b/src/extensions/extension_data/signature_algorithms_cert.rs new file mode 100644 index 0000000..ec75ea8 --- /dev/null +++ b/src/extensions/extension_data/signature_algorithms_cert.rs @@ -0,0 +1,34 @@ +use crate::buffer::CryptoBuffer; +use crate::extensions::extension_data::signature_algorithms::SignatureScheme; + +use crate::ProtocolError; +use crate::parse_buffer::{ParseBuffer, ParseError}; + +use heapless::Vec; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SignatureAlgorithmsCert { + pub supported_signature_algorithms: Vec, +} + +impl SignatureAlgorithmsCert { + pub fn parse(buf: &mut ParseBuffer) -> Result { + let data_length = buf.read_u16()? as usize; + + Ok(Self { + supported_signature_algorithms: buf + .read_list::<_, N>(data_length, SignatureScheme::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u16_length(|buf| { + for &a in &self.supported_signature_algorithms { + buf.push_u16(a.as_u16()) + .map_err(|_| ProtocolError::EncodeError)?; + } + Ok(()) + }) + } +} diff --git a/src/extensions/extension_data/supported_groups.rs b/src/extensions/extension_data/supported_groups.rs new file mode 100644 index 0000000..3e3c69c --- /dev/null +++ b/src/extensions/extension_data/supported_groups.rs @@ -0,0 +1,104 @@ +use heapless::Vec; + +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum NamedGroup { + /* Elliptic Curve Groups (ECDHE) */ + Secp256r1, + Secp384r1, + Secp521r1, + X25519, + X448, + + /* Finite Field Groups (DHE) */ + Ffdhe2048, + Ffdhe3072, + Ffdhe4096, + Ffdhe6144, + Ffdhe8192, + + /* Post-quantum hybrid groups */ + X25519MLKEM768, + SecP256r1MLKEM768, + SecP384r1MLKEM1024, +} + +impl NamedGroup { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u16()? { + 0x0017 => Ok(Self::Secp256r1), + 0x0018 => Ok(Self::Secp384r1), + 0x0019 => Ok(Self::Secp521r1), + 0x001D => Ok(Self::X25519), + 0x001E => Ok(Self::X448), + + 0x0100 => Ok(Self::Ffdhe2048), + 0x0101 => Ok(Self::Ffdhe3072), + 0x0102 => Ok(Self::Ffdhe4096), + 0x0103 => Ok(Self::Ffdhe6144), + 0x0104 => Ok(Self::Ffdhe8192), + + 0x11EB => Ok(Self::SecP256r1MLKEM768), + 0x11EC => Ok(Self::X25519MLKEM768), + 0x11ED => Ok(Self::SecP384r1MLKEM1024), + + _ => Err(ParseError::InvalidData), + } + } + + pub fn as_u16(self) -> u16 { + match self { + Self::Secp256r1 => 0x0017, + Self::Secp384r1 => 0x0018, + Self::Secp521r1 => 0x0019, + Self::X25519 => 0x001D, + Self::X448 => 0x001E, + + Self::Ffdhe2048 => 0x0100, + Self::Ffdhe3072 => 0x0101, + Self::Ffdhe4096 => 0x0102, + Self::Ffdhe6144 => 0x0103, + Self::Ffdhe8192 => 0x0104, + + Self::SecP256r1MLKEM768 => 0x11EB, + Self::X25519MLKEM768 => 0x11EC, + Self::SecP384r1MLKEM1024 => 0x11ED, + } + } + + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push_u16(self.as_u16()) + .map_err(|_| ProtocolError::EncodeError) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SupportedGroups { + pub supported_groups: Vec, +} + +impl SupportedGroups { + pub fn parse(buf: &mut ParseBuffer) -> Result { + let data_length = buf.read_u16()? as usize; + + Ok(Self { + supported_groups: buf.read_list::<_, N>(data_length, NamedGroup::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u16_length(|buf| { + for g in &self.supported_groups { + g.encode(buf)?; + } + Ok(()) + }) + } +} diff --git a/src/extensions/extension_data/supported_versions.rs b/src/extensions/extension_data/supported_versions.rs new file mode 100644 index 0000000..3d3526e --- /dev/null +++ b/src/extensions/extension_data/supported_versions.rs @@ -0,0 +1,65 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; +use heapless::Vec; + +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProtocolVersion(u16); + +impl ProtocolVersion { + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push_u16(self.0).map_err(|_| ProtocolError::EncodeError) + } + + pub fn parse(buf: &mut ParseBuffer) -> Result { + buf.read_u16().map(Self) + } +} + +pub const TLS13: ProtocolVersion = ProtocolVersion(0x0304); + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SupportedVersionsClientHello { + pub versions: Vec, +} + +impl SupportedVersionsClientHello { + pub fn parse(buf: &mut ParseBuffer) -> Result { + let data_length = buf.read_u8()? as usize; + + Ok(Self { + versions: buf.read_list::<_, N>(data_length, ProtocolVersion::parse)?, + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.with_u8_length(|buf| { + for v in &self.versions { + v.encode(buf)?; + } + Ok(()) + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SupportedVersionsServerHello { + pub selected_version: ProtocolVersion, +} + +impl SupportedVersionsServerHello { + pub fn parse(buf: &mut ParseBuffer) -> Result { + Ok(Self { + selected_version: ProtocolVersion::parse(buf)?, + }) + } + + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + self.selected_version.encode(buf) + } +} diff --git a/src/extensions/extension_data/unimplemented.rs b/src/extensions/extension_data/unimplemented.rs new file mode 100644 index 0000000..82e605f --- /dev/null +++ b/src/extensions/extension_data/unimplemented.rs @@ -0,0 +1,24 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Unimplemented<'a> { + pub data: &'a [u8], +} + +impl<'a> Unimplemented<'a> { + #[allow(clippy::unnecessary_wraps)] + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + Ok(Self { + data: buf.as_slice(), + }) + } + + pub fn encode(&self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.extend_from_slice(self.data) + } +} diff --git a/src/extensions/extension_group_macro.rs b/src/extensions/extension_group_macro.rs new file mode 100644 index 0000000..3d3e362 --- /dev/null +++ b/src/extensions/extension_group_macro.rs @@ -0,0 +1,102 @@ +macro_rules! extension_group { + (pub enum $name:ident$(<$lt:lifetime>)? { + $($extension:ident($extension_data:ty)),+ + }) => { + #[derive(Debug, Clone)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[allow(dead_code)] // extension_data may not be used + pub enum $name$(<$lt>)? { + $($extension($extension_data)),+ + } + + #[allow(dead_code)] // not all methods are used + impl$(<$lt>)? $name$(<$lt>)? { + pub fn extension_type(&self) -> crate::extensions::ExtensionType { + match self { + $(Self::$extension(_) => crate::extensions::ExtensionType::$extension),+ + } + } + + pub fn encode(&self, buf: &mut crate::buffer::CryptoBuffer) -> Result<(), crate::ProtocolError> { + self.extension_type().encode(buf)?; + + buf.with_u16_length(|buf| match self { + $(Self::$extension(ext_data) => ext_data.encode(buf)),+ + }) + } + + pub fn parse(buf: &mut crate::parse_buffer::ParseBuffer$(<$lt>)?) -> Result { + // Consume extension data even if we don't recognize the extension + let extension_type = crate::extensions::ExtensionType::parse(buf); + let data_len = buf.read_u16().map_err(|_| crate::ProtocolError::DecodeError)? as usize; + let mut ext_data = buf.slice(data_len).map_err(|_| crate::ProtocolError::DecodeError)?; + + let ext_type = extension_type.map_err(|err| { + warn!("Failed to read extension type: {:?}", err); + match err { + crate::parse_buffer::ParseError::InvalidData => crate::ProtocolError::UnknownExtensionType, + _ => crate::ProtocolError::DecodeError, + } + })?; + + debug!("Read extension type {:?}", ext_type); + trace!("Extension data length: {}", data_len); + + match ext_type { + $(crate::extensions::ExtensionType::$extension => Ok(Self::$extension(<$extension_data>::parse(&mut ext_data).map_err(|err| { + warn!("Failed to parse extension data: {:?}", err); + crate::ProtocolError::DecodeError + })?)),)+ + + #[allow(unreachable_patterns)] + other => { + warn!("Read unexpected ExtensionType: {:?}", other); + // Section 4.2. Extensions + // If an implementation receives an extension + // which it recognizes and which is not specified for the message in + // which it appears, it MUST abort the handshake with an + // "illegal_parameter" alert. + Err(crate::ProtocolError::AbortHandshake( + crate::alert::AlertLevel::Fatal, + crate::alert::AlertDescription::IllegalParameter, + )) + } + } + } + + pub fn parse_vector( + buf: &mut crate::parse_buffer::ParseBuffer$(<$lt>)?, + ) -> Result, crate::ProtocolError> { + let extensions_len = buf + .read_u16() + .map_err(|_| crate::ProtocolError::InvalidExtensionsLength)?; + + let mut ext_buf = buf.slice(extensions_len as usize)?; + + let mut extensions = heapless::Vec::new(); + + while !ext_buf.is_empty() { + trace!("Extension buffer: {}", ext_buf.remaining()); + match Self::parse(&mut ext_buf) { + Ok(extension) => { + extensions + .push(extension) + .map_err(|_| crate::ProtocolError::DecodeError)?; + } + Err(crate::ProtocolError::UnknownExtensionType) => { + // ignore unrecognized extension type + } + Err(err) => return Err(err), + } + } + + trace!("Read {} extensions", extensions.len()); + Ok(extensions) + } + } + }; +} + +// This re-export makes it possible to omit #[macro_export] +// https://stackoverflow.com/a/67140319 +pub(crate) use extension_group; diff --git a/src/extensions/messages.rs b/src/extensions/messages.rs new file mode 100644 index 0000000..a133550 --- /dev/null +++ b/src/extensions/messages.rs @@ -0,0 +1,106 @@ +use crate::extensions::{ + extension_data::{ + alpn::AlpnProtocolNameList, + key_share::{KeyShareClientHello, KeyShareServerHello}, + max_fragment_length::MaxFragmentLength, + pre_shared_key::{PreSharedKeyClientHello, PreSharedKeyServerHello}, + psk_key_exchange_modes::PskKeyExchangeModes, + server_name::{ServerNameList, ServerNameResponse}, + signature_algorithms::SignatureAlgorithms, + signature_algorithms_cert::SignatureAlgorithmsCert, + supported_groups::SupportedGroups, + supported_versions::{SupportedVersionsClientHello, SupportedVersionsServerHello}, + unimplemented::Unimplemented, + }, + extension_group_macro::extension_group, +}; + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with CH +extension_group! { + pub enum ClientHelloExtension<'a> { + ServerName(ServerNameList<'a, 1>), + SupportedVersions(SupportedVersionsClientHello<1>), + SignatureAlgorithms(SignatureAlgorithms<25>), + SupportedGroups(SupportedGroups<13>), + KeyShare(KeyShareClientHello<'a, 1>), + PreSharedKey(PreSharedKeyClientHello<'a, 4>), + PskKeyExchangeModes(PskKeyExchangeModes<4>), + SignatureAlgorithmsCert(SignatureAlgorithmsCert<25>), + MaxFragmentLength(MaxFragmentLength), + StatusRequest(Unimplemented<'a>), + UseSrtp(Unimplemented<'a>), + Heartbeat(Unimplemented<'a>), + ApplicationLayerProtocolNegotiation(AlpnProtocolNameList<'a>), + SignedCertificateTimestamp(Unimplemented<'a>), + ClientCertificateType(Unimplemented<'a>), + ServerCertificateType(Unimplemented<'a>), + Padding(Unimplemented<'a>), + EarlyData(Unimplemented<'a>), + Cookie(Unimplemented<'a>), + CertificateAuthorities(Unimplemented<'a>), + OidFilters(Unimplemented<'a>), + PostHandshakeAuth(Unimplemented<'a>) + } +} + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with SH +extension_group! { + pub enum ServerHelloExtension<'a> { + KeyShare(KeyShareServerHello<'a>), + PreSharedKey(PreSharedKeyServerHello), + Cookie(Unimplemented<'a>), // temporary so we don't trip up on HelloRetryRequests + SupportedVersions(SupportedVersionsServerHello) + } +} + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with EE +extension_group! { + pub enum EncryptedExtensionsExtension<'a> { + ServerName(ServerNameResponse), + MaxFragmentLength(MaxFragmentLength), + SupportedGroups(SupportedGroups<13>), + UseSrtp(Unimplemented<'a>), + Heartbeat(Unimplemented<'a>), + ApplicationLayerProtocolNegotiation(AlpnProtocolNameList<'a>), + ClientCertificateType(Unimplemented<'a>), + ServerCertificateType(Unimplemented<'a>), + EarlyData(Unimplemented<'a>) + } +} + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with CR +extension_group! { + pub enum CertificateRequestExtension<'a> { + StatusRequest(Unimplemented<'a>), + SignatureAlgorithms(SignatureAlgorithms<25>), + SignedCertificateTimestamp(Unimplemented<'a>), + CertificateAuthorities(Unimplemented<'a>), + OidFilters(Unimplemented<'a>), + SignatureAlgorithmsCert(Unimplemented<'a>), + CompressCertificate(Unimplemented<'a>) + } +} + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with CT +extension_group! { + pub enum CertificateExtension<'a> { + StatusRequest(Unimplemented<'a>), + SignedCertificateTimestamp(Unimplemented<'a>) + } +} + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with NST +extension_group! { + pub enum NewSessionTicketExtension<'a> { + EarlyData(Unimplemented<'a>) + } +} + +// Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.2 table, rows marked with HRR +extension_group! { + pub enum HelloRetryRequestExtension<'a> { + KeyShare(Unimplemented<'a>), + Cookie(Unimplemented<'a>), + SupportedVersions(Unimplemented<'a>) + } +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs new file mode 100644 index 0000000..a97c1c6 --- /dev/null +++ b/src/extensions/mod.rs @@ -0,0 +1,80 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +mod extension_group_macro; + +pub mod extension_data; +pub mod messages; + +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ExtensionType { + ServerName = 0, + MaxFragmentLength = 1, + StatusRequest = 5, + SupportedGroups = 10, + SignatureAlgorithms = 13, + UseSrtp = 14, + Heartbeat = 15, + ApplicationLayerProtocolNegotiation = 16, + SignedCertificateTimestamp = 18, + ClientCertificateType = 19, + ServerCertificateType = 20, + Padding = 21, + CompressCertificate = 27, + PreSharedKey = 41, + EarlyData = 42, + SupportedVersions = 43, + Cookie = 44, + PskKeyExchangeModes = 45, + CertificateAuthorities = 47, + OidFilters = 48, + PostHandshakeAuth = 49, + SignatureAlgorithmsCert = 50, + KeyShare = 51, +} + +impl ExtensionType { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u16()? { + v if v == Self::ServerName as u16 => Ok(Self::ServerName), + v if v == Self::MaxFragmentLength as u16 => Ok(Self::MaxFragmentLength), + v if v == Self::StatusRequest as u16 => Ok(Self::StatusRequest), + v if v == Self::SupportedGroups as u16 => Ok(Self::SupportedGroups), + v if v == Self::SignatureAlgorithms as u16 => Ok(Self::SignatureAlgorithms), + v if v == Self::UseSrtp as u16 => Ok(Self::UseSrtp), + v if v == Self::Heartbeat as u16 => Ok(Self::Heartbeat), + v if v == Self::ApplicationLayerProtocolNegotiation as u16 => { + Ok(Self::ApplicationLayerProtocolNegotiation) + } + v if v == Self::SignedCertificateTimestamp as u16 => { + Ok(Self::SignedCertificateTimestamp) + } + v if v == Self::ClientCertificateType as u16 => Ok(Self::ClientCertificateType), + v if v == Self::ServerCertificateType as u16 => Ok(Self::ServerCertificateType), + v if v == Self::Padding as u16 => Ok(Self::Padding), + v if v == Self::CompressCertificate as u16 => Ok(Self::CompressCertificate), + v if v == Self::PreSharedKey as u16 => Ok(Self::PreSharedKey), + v if v == Self::EarlyData as u16 => Ok(Self::EarlyData), + v if v == Self::SupportedVersions as u16 => Ok(Self::SupportedVersions), + v if v == Self::Cookie as u16 => Ok(Self::Cookie), + v if v == Self::PskKeyExchangeModes as u16 => Ok(Self::PskKeyExchangeModes), + v if v == Self::CertificateAuthorities as u16 => Ok(Self::CertificateAuthorities), + v if v == Self::OidFilters as u16 => Ok(Self::OidFilters), + v if v == Self::PostHandshakeAuth as u16 => Ok(Self::PostHandshakeAuth), + v if v == Self::SignatureAlgorithmsCert as u16 => Ok(Self::SignatureAlgorithmsCert), + v if v == Self::KeyShare as u16 => Ok(Self::KeyShare), + other => { + warn!("Read unknown ExtensionType: {}", other); + Err(ParseError::InvalidData) + } + } + } + + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push_u16(self as u16).map_err(|_| ProtocolError::EncodeError) + } +} diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..0ef8f2e --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,223 @@ +#![macro_use] +#![allow(unused_macros)] + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/src/handshake/binder.rs b/src/handshake/binder.rs new file mode 100644 index 0000000..645bc85 --- /dev/null +++ b/src/handshake/binder.rs @@ -0,0 +1,39 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use core::fmt::{Debug, Formatter}; +//use digest::generic_array::{ArrayLength, GenericArray}; +use generic_array::{ArrayLength, GenericArray}; +// use heapless::Vec; + +pub struct PskBinder> { + pub verify: GenericArray, +} + +#[cfg(feature = "defmt")] +impl> defmt::Format for PskBinder { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "verify length:{}", &self.verify.len()); + } +} + +impl> Debug for PskBinder { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PskBinder").finish() + } +} + +impl> PskBinder { + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + let len = self.verify.len() as u8; + //buf.extend_from_slice(&[len[1], len[2], len[3]]); + buf.push(len).map_err(|_| ProtocolError::EncodeError)?; + buf.extend_from_slice(&self.verify[..self.verify.len()]) + .map_err(|_| ProtocolError::EncodeError)?; + Ok(()) + } + + #[allow(dead_code)] + pub fn len() -> usize { + N::to_usize() + } +} diff --git a/src/handshake/certificate.rs b/src/handshake/certificate.rs new file mode 100644 index 0000000..c3ef5b7 --- /dev/null +++ b/src/handshake/certificate.rs @@ -0,0 +1,174 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use crate::extensions::messages::CertificateExtension; +use crate::parse_buffer::ParseBuffer; +use heapless::Vec; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CertificateRef<'a> { + raw_entries: &'a [u8], + request_context: &'a [u8], + + pub entries: Vec, 16>, +} + +impl<'a> CertificateRef<'a> { + pub fn with_context(request_context: &'a [u8]) -> Self { + Self { + raw_entries: &[], + request_context, + entries: Vec::new(), + } + } + + pub fn add(&mut self, entry: CertificateEntryRef<'a>) -> Result<(), ProtocolError> { + self.entries.push(entry).map_err(|_| { + error!("CertificateRef: InsufficientSpace"); + ProtocolError::InsufficientSpace + }) + } + + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + let request_context_len = buf.read_u8().map_err(|_| ProtocolError::InvalidCertificate)?; + let request_context = buf + .slice(request_context_len as usize) + .map_err(|_| ProtocolError::InvalidCertificate)?; + let entries_len = buf.read_u24().map_err(|_| ProtocolError::InvalidCertificate)?; + let mut raw_entries = buf + .slice(entries_len as usize) + .map_err(|_| ProtocolError::InvalidCertificate)?; + + let entries = CertificateEntryRef::parse_vector(&mut raw_entries)?; + + Ok(Self { + raw_entries: raw_entries.as_slice(), + request_context: request_context.as_slice(), + entries, + }) + } + + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + buf.with_u8_length(|buf| buf.extend_from_slice(self.request_context))?; + buf.with_u24_length(|buf| { + for entry in &self.entries { + entry.encode(buf)?; + } + Ok(()) + })?; + + Ok(()) + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CertificateEntryRef<'a> { + X509(&'a [u8]), + RawPublicKey(&'a [u8]), +} + +impl<'a> CertificateEntryRef<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result { + let entry_len = buf + .read_u24() + .map_err(|_| ProtocolError::InvalidCertificateEntry)?; + let cert = buf + .slice(entry_len as usize) + .map_err(|_| ProtocolError::InvalidCertificateEntry)?; + + let entry = CertificateEntryRef::X509(cert.as_slice()); + + // Validate extensions + CertificateExtension::parse_vector::<2>(buf)?; + + Ok(entry) + } + + pub fn parse_vector( + buf: &mut ParseBuffer<'a>, + ) -> Result, ProtocolError> { + let mut result = Vec::new(); + + while !buf.is_empty() { + result + .push(Self::parse(buf)?) + .map_err(|_| ProtocolError::DecodeError)?; + } + + Ok(result) + } + + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + match *self { + CertificateEntryRef::RawPublicKey(_key) => { + todo!("ASN1_subjectPublicKeyInfo encoding?"); + // buf.with_u24_length(|buf| buf.extend_from_slice(key))?; + } + CertificateEntryRef::X509(cert) => { + buf.with_u24_length(|buf| buf.extend_from_slice(cert))?; + } + } + + // Zero extensions for now + buf.push_u16(0)?; + Ok(()) + } +} + +impl<'a, D: AsRef<[u8]>> From<&'a crate::config::Certificate> for CertificateEntryRef<'a> { + fn from(cert: &'a crate::config::Certificate) -> Self { + match cert { + crate::config::Certificate::X509(data) => CertificateEntryRef::X509(data.as_ref()), + crate::config::Certificate::RawPublicKey(data) => { + CertificateEntryRef::RawPublicKey(data.as_ref()) + } + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Certificate { + request_context: Vec, + entries_data: Vec, +} + +impl Certificate { + pub fn request_context(&self) -> &[u8] { + &self.request_context[..] + } +} + +impl<'a, const N: usize> TryFrom> for Certificate { + type Error = ProtocolError; + fn try_from(cert: CertificateRef<'a>) -> Result { + let mut request_context = Vec::new(); + request_context + .extend_from_slice(cert.request_context) + .map_err(|_| ProtocolError::OutOfMemory)?; + let mut entries_data = Vec::new(); + entries_data + .extend_from_slice(cert.raw_entries) + .map_err(|_| ProtocolError::OutOfMemory)?; + + Ok(Self { + request_context, + entries_data, + }) + } +} + +impl<'a, const N: usize> TryFrom<&'a Certificate> for CertificateRef<'a> { + type Error = ProtocolError; + fn try_from(cert: &'a Certificate) -> Result { + let request_context = cert.request_context(); + let entries = + CertificateEntryRef::parse_vector(&mut ParseBuffer::from(&cert.entries_data[..]))?; + Ok(Self { + raw_entries: &cert.entries_data[..], + request_context, + entries, + }) + } +} diff --git a/src/handshake/certificate_request.rs b/src/handshake/certificate_request.rs new file mode 100644 index 0000000..c17d83d --- /dev/null +++ b/src/handshake/certificate_request.rs @@ -0,0 +1,50 @@ +use crate::extensions::messages::CertificateRequestExtension; +use crate::parse_buffer::ParseBuffer; +use crate::{ProtocolError, unused}; +use heapless::Vec; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CertificateRequestRef<'a> { + pub(crate) request_context: &'a [u8], +} + +impl<'a> CertificateRequestRef<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ProtocolError> { + let request_context_len = buf + .read_u8() + .map_err(|_| ProtocolError::InvalidCertificateRequest)?; + let request_context = buf + .slice(request_context_len as usize) + .map_err(|_| ProtocolError::InvalidCertificateRequest)?; + + // Validate extensions + let extensions = CertificateRequestExtension::parse_vector::<6>(buf)?; + + unused(extensions); + Ok(Self { + request_context: request_context.as_slice(), + }) + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CertificateRequest { + pub(crate) request_context: Vec, +} + +impl<'a> TryFrom> for CertificateRequest { + type Error = ProtocolError; + fn try_from(cert: CertificateRequestRef<'a>) -> Result { + let mut request_context = Vec::new(); + request_context + .extend_from_slice(cert.request_context) + .map_err(|_| { + error!("CertificateRequest: InsufficientSpace"); + ProtocolError::InsufficientSpace + })?; + + Ok(Self { request_context }) + } +} diff --git a/src/handshake/certificate_verify.rs b/src/handshake/certificate_verify.rs new file mode 100644 index 0000000..167a028 --- /dev/null +++ b/src/handshake/certificate_verify.rs @@ -0,0 +1,56 @@ +use crate::ProtocolError; +use crate::extensions::extension_data::signature_algorithms::SignatureScheme; +use crate::parse_buffer::ParseBuffer; + +use super::CryptoBuffer; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct HandshakeVerifyRef<'a> { + pub signature_scheme: SignatureScheme, + pub signature: &'a [u8], +} + +impl<'a> HandshakeVerifyRef<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ProtocolError> { + let signature_scheme = + SignatureScheme::parse(buf).map_err(|_| ProtocolError::InvalidSignatureScheme)?; + + let len = buf.read_u16().map_err(|_| ProtocolError::InvalidSignature)?; + let signature = buf + .slice(len as usize) + .map_err(|_| ProtocolError::InvalidSignature)?; + + Ok(Self { + signature_scheme, + signature: signature.as_slice(), + }) + } +} + +// Calculations for max. signature sizes: +// ecdsaSHA256 -> 6 bytes (ASN.1 structure) + 32-33 bytes (r) + 32-33 bytes (s) = 70..72 bytes +// ecdsaSHA384 -> 6 bytes (ASN.1 structure) + 48-49 bytes (r) + 48-49 bytes (s) = 102..104 bytes +// Ed25519 -> 6 bytes (ASN.1 structure) + 32-33 bytes (r) + 32-33 bytes (s) = 70..72 bytes +// RSA2048 -> 256 bytes +// RSA3072 -> 384 bytee +// RSA4096 -> 512 bytes +#[cfg(feature = "rsa")] +const SIGNATURE_SIZE: usize = 512; +#[cfg(not(feature = "rsa"))] +const SIGNATURE_SIZE: usize = 104; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct HandshakeVerify { + pub(crate) signature_scheme: SignatureScheme, + pub(crate) signature: heapless::Vec, +} + +impl HandshakeVerify { + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + buf.push_u16(self.signature_scheme.as_u16())?; + buf.with_u16_length(|buf| buf.extend_from_slice(self.signature.as_slice()))?; + Ok(()) + } +} diff --git a/src/handshake/client_hello.rs b/src/handshake/client_hello.rs new file mode 100644 index 0000000..6ddc26d --- /dev/null +++ b/src/handshake/client_hello.rs @@ -0,0 +1,188 @@ +use core::marker::PhantomData; + +use digest::{Digest, OutputSizeUser}; +use heapless::Vec; +use p256::EncodedPoint; +use p256::ecdh::EphemeralSecret; +use p256::elliptic_curve::rand_core::RngCore; +use typenum::Unsigned; + +use crate::ProtocolError; +use crate::config::{TlsCipherSuite, ConnectConfig}; +use crate::extensions::extension_data::alpn::AlpnProtocolNameList; +use crate::extensions::extension_data::key_share::{KeyShareClientHello, KeyShareEntry}; +use crate::extensions::extension_data::pre_shared_key::PreSharedKeyClientHello; +use crate::extensions::extension_data::psk_key_exchange_modes::{ + PskKeyExchangeMode, PskKeyExchangeModes, +}; +use crate::extensions::extension_data::server_name::ServerNameList; +use crate::extensions::extension_data::signature_algorithms::SignatureAlgorithms; +use crate::extensions::extension_data::supported_groups::{NamedGroup, SupportedGroups}; +use crate::extensions::extension_data::supported_versions::{SupportedVersionsClientHello, TLS13}; +use crate::extensions::messages::ClientHelloExtension; +use crate::handshake::{LEGACY_VERSION, Random}; +use crate::key_schedule::{HashOutputSize, WriteKeySchedule}; +use crate::{CryptoBackend, buffer::CryptoBuffer}; + +pub struct ClientHello<'config, CipherSuite> +where + CipherSuite: TlsCipherSuite, +{ + pub(crate) config: &'config ConnectConfig<'config>, + random: Random, + cipher_suite: PhantomData, + pub(crate) secret: EphemeralSecret, +} + +impl<'config, CipherSuite> ClientHello<'config, CipherSuite> +where + CipherSuite: TlsCipherSuite, +{ + pub fn new(config: &'config ConnectConfig<'config>, mut provider: CP) -> Self + where + CP: CryptoBackend, + { + let mut random = [0; 32]; + provider.rng().fill_bytes(&mut random); + + Self { + config, + random, + cipher_suite: PhantomData, + secret: EphemeralSecret::random(&mut provider.rng()), + } + } + + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + let public_key = EncodedPoint::from(&self.secret.public_key()); + let public_key = public_key.as_ref(); + + buf.push_u16(LEGACY_VERSION) + .map_err(|_| ProtocolError::EncodeError)?; + buf.extend_from_slice(&self.random) + .map_err(|_| ProtocolError::EncodeError)?; + + // session id (empty) + buf.push(0).map_err(|_| ProtocolError::EncodeError)?; + + // cipher suites (2+) + //buf.extend_from_slice(&((self.config.cipher_suites.len() * 2) as u16).to_be_bytes()); + //for c in self.config.cipher_suites.iter() { + //buf.extend_from_slice(&(*c as u16).to_be_bytes()); + //} + buf.push_u16(2).map_err(|_| ProtocolError::EncodeError)?; + buf.push_u16(CipherSuite::CODE_POINT) + .map_err(|_| ProtocolError::EncodeError)?; + + // compression methods, 1 byte of 0 + buf.push(1).map_err(|_| ProtocolError::EncodeError)?; + buf.push(0).map_err(|_| ProtocolError::EncodeError)?; + + // extensions (1+) + buf.with_u16_length(|buf| { + // Section 4.2.1. Supported Versions + // Implementations of this specification MUST send this extension in the + // ClientHello containing all versions of TLS which they are prepared to + // negotiate + ClientHelloExtension::SupportedVersions(SupportedVersionsClientHello { + versions: Vec::from_slice(&[TLS13]).unwrap(), + }) + .encode(buf)?; + + ClientHelloExtension::SignatureAlgorithms(SignatureAlgorithms { + supported_signature_algorithms: self.config.signature_schemes.clone(), + }) + .encode(buf)?; + + if let Some(max_fragment_length) = self.config.max_fragment_length { + ClientHelloExtension::MaxFragmentLength(max_fragment_length).encode(buf)?; + } + + ClientHelloExtension::SupportedGroups(SupportedGroups { + supported_groups: self.config.named_groups.clone(), + }) + .encode(buf)?; + + ClientHelloExtension::PskKeyExchangeModes(PskKeyExchangeModes { + modes: Vec::from_slice(&[PskKeyExchangeMode::PskDheKe]).unwrap(), + }) + .encode(buf)?; + + ClientHelloExtension::KeyShare(KeyShareClientHello { + client_shares: Vec::from_slice(&[KeyShareEntry { + group: NamedGroup::Secp256r1, + opaque: public_key, + }]) + .unwrap(), + }) + .encode(buf)?; + + if let Some(server_name) = self.config.server_name { + ClientHelloExtension::ServerName(ServerNameList::single(server_name)) + .encode(buf)?; + } + + if let Some(alpn_protocols) = self.config.alpn_protocols { + ClientHelloExtension::ApplicationLayerProtocolNegotiation(AlpnProtocolNameList { + protocols: alpn_protocols, + }) + .encode(buf)?; + } + + // Section 4.2 + // When multiple extensions of different types are present, the + // extensions MAY appear in any order, with the exception of + // "pre_shared_key" which MUST be the last extension in + // the ClientHello. + if let Some((_, identities)) = &self.config.psk { + ClientHelloExtension::PreSharedKey(PreSharedKeyClientHello { + identities: identities.clone(), + hash_size: ::output_size(), + }) + .encode(buf)?; + } + + Ok(()) + })?; + + Ok(()) + } + + pub fn finalize( + &self, + enc_buf: &mut [u8], + transcript: &mut CipherSuite::Hash, + write_key_schedule: &mut WriteKeySchedule, + ) -> Result<(), ProtocolError> { + // Special case for PSK which needs to: + // + // 1. Add the client hello without the binders to the transcript + // 2. Create the binders for each identity using the transcript + // 3. Add the rest of the client hello. + // + // This causes a few issues since lengths must be correctly inside the payload, + // but won't actually be added to the record buffer until the end. + if let Some((_, identities)) = &self.config.psk { + let binders_len = identities.len() * (1 + HashOutputSize::::to_usize()); + + let binders_pos = enc_buf.len() - binders_len; + + // NOTE: Exclude the binders_len itself from the digest + transcript.update(&enc_buf[0..binders_pos - 2]); + + // Append after the client hello data. Sizes have already been set. + let mut buf = CryptoBuffer::wrap(&mut enc_buf[binders_pos..]); + // Create a binder and encode for each identity + for _id in identities { + let binder = write_key_schedule.create_psk_binder(transcript)?; + binder.encode(&mut buf)?; + } + + transcript.update(&enc_buf[binders_pos - 2..]); + } else { + transcript.update(enc_buf); + } + + Ok(()) + } +} diff --git a/src/handshake/encrypted_extensions.rs b/src/handshake/encrypted_extensions.rs new file mode 100644 index 0000000..b1abb98 --- /dev/null +++ b/src/handshake/encrypted_extensions.rs @@ -0,0 +1,19 @@ +use core::marker::PhantomData; + +use crate::extensions::messages::EncryptedExtensionsExtension; + +use crate::ProtocolError; +use crate::parse_buffer::ParseBuffer; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct EncryptedExtensions<'a> { + _todo: PhantomData<&'a ()>, +} + +impl<'a> EncryptedExtensions<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ProtocolError> { + EncryptedExtensionsExtension::parse_vector::<16>(buf)?; + Ok(EncryptedExtensions { _todo: PhantomData }) + } +} diff --git a/src/handshake/finished.rs b/src/handshake/finished.rs new file mode 100644 index 0000000..d4246de --- /dev/null +++ b/src/handshake/finished.rs @@ -0,0 +1,52 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use crate::parse_buffer::ParseBuffer; +use core::fmt::{Debug, Formatter}; +//use digest::generic_array::{ArrayLength, GenericArray}; +use generic_array::{ArrayLength, GenericArray}; +// use heapless::Vec; + +pub struct Finished> { + pub verify: GenericArray, + pub hash: Option>, +} + +#[cfg(feature = "defmt")] +impl> defmt::Format for Finished { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "verify length:{}", &self.verify.len()); + } +} + +impl> Debug for Finished { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Finished") + .field("verify", &self.hash) + .finish() + } +} + +impl> Finished { + pub fn parse(buf: &mut ParseBuffer, _len: u32) -> Result { + // info!("finished len: {}", len); + let mut verify = GenericArray::default(); + buf.fill(&mut verify)?; + //let hash = GenericArray::from_slice() + //let hash: Result, ()> = buf + //.slice(len as usize) + //.map_err(|_| ProtocolError::InvalidHandshake)? + //.into(); + // info!("hash {:?}", verify); + //let hash = hash.map_err(|_| ProtocolError::InvalidHandshake)?; + // info!("hash ng {:?}", verify); + Ok(Self { verify, hash: None }) + } + + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + //let len = self.verify.len().to_be_bytes(); + //buf.extend_from_slice(&[len[1], len[2], len[3]]); + buf.extend_from_slice(&self.verify[..self.verify.len()]) + .map_err(|_| ProtocolError::EncodeError)?; + Ok(()) + } +} diff --git a/src/handshake/mod.rs b/src/handshake/mod.rs new file mode 100644 index 0000000..902bf93 --- /dev/null +++ b/src/handshake/mod.rs @@ -0,0 +1,241 @@ +//use p256::elliptic_curve::AffinePoint; +use crate::ProtocolError; +use crate::config::TlsCipherSuite; +use crate::handshake::certificate::CertificateRef; +use crate::handshake::certificate_request::CertificateRequestRef; +use crate::handshake::certificate_verify::{HandshakeVerify, HandshakeVerifyRef}; +use crate::handshake::client_hello::ClientHello; +use crate::handshake::encrypted_extensions::EncryptedExtensions; +use crate::handshake::finished::Finished; +use crate::handshake::new_session_ticket::NewSessionTicket; +use crate::handshake::server_hello::ServerHello; +use crate::key_schedule::HashOutputSize; +use crate::parse_buffer::{ParseBuffer, ParseError}; +use crate::{buffer::CryptoBuffer, key_schedule::WriteKeySchedule}; +use core::fmt::{Debug, Formatter}; +use sha2::Digest; + +pub mod binder; +pub mod certificate; +pub mod certificate_request; +pub mod certificate_verify; +pub mod client_hello; +pub mod encrypted_extensions; +pub mod finished; +pub mod new_session_ticket; +pub mod server_hello; + +const LEGACY_VERSION: u16 = 0x0303; + +type Random = [u8; 32]; + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum HandshakeType { + ClientHello = 1, + ServerHello = 2, + NewSessionTicket = 4, + EndOfEarlyData = 5, + EncryptedExtensions = 8, + Certificate = 11, + CertificateRequest = 13, + HandshakeVerify = 15, + Finished = 20, + KeyUpdate = 24, + MessageHash = 254, +} + +impl HandshakeType { + pub fn parse(buf: &mut ParseBuffer) -> Result { + match buf.read_u8()? { + 1 => Ok(HandshakeType::ClientHello), + 2 => Ok(HandshakeType::ServerHello), + 4 => Ok(HandshakeType::NewSessionTicket), + 5 => Ok(HandshakeType::EndOfEarlyData), + 8 => Ok(HandshakeType::EncryptedExtensions), + 11 => Ok(HandshakeType::Certificate), + 13 => Ok(HandshakeType::CertificateRequest), + 15 => Ok(HandshakeType::HandshakeVerify), + 20 => Ok(HandshakeType::Finished), + 24 => Ok(HandshakeType::KeyUpdate), + 254 => Ok(HandshakeType::MessageHash), + _ => Err(ParseError::InvalidData), + } + } +} + +#[allow(clippy::large_enum_variant)] +pub enum ClientHandshake<'config, 'a, CipherSuite> +where + CipherSuite: TlsCipherSuite, +{ + ClientCert(CertificateRef<'a>), + ClientCertVerify(HandshakeVerify), + ClientHello(ClientHello<'config, CipherSuite>), + Finished(Finished>), +} + +impl ClientHandshake<'_, '_, CipherSuite> +where + CipherSuite: TlsCipherSuite, +{ + fn handshake_type(&self) -> HandshakeType { + match self { + ClientHandshake::ClientHello(_) => HandshakeType::ClientHello, + ClientHandshake::Finished(_) => HandshakeType::Finished, + ClientHandshake::ClientCert(_) => HandshakeType::Certificate, + ClientHandshake::ClientCertVerify(_) => HandshakeType::HandshakeVerify, + } + } + + fn encode_inner(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + match self { + ClientHandshake::ClientHello(inner) => inner.encode(buf), + ClientHandshake::Finished(inner) => inner.encode(buf), + ClientHandshake::ClientCert(inner) => inner.encode(buf), + ClientHandshake::ClientCertVerify(inner) => inner.encode(buf), + } + } + + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + buf.push(self.handshake_type() as u8) + .map_err(|_| ProtocolError::EncodeError)?; + + buf.with_u24_length(|buf| self.encode_inner(buf)) + } + + pub fn finalize( + &self, + buf: &mut CryptoBuffer, + transcript: &mut CipherSuite::Hash, + write_key_schedule: &mut WriteKeySchedule, + ) -> Result<(), ProtocolError> { + let enc_buf = buf.as_mut_slice(); + if let ClientHandshake::ClientHello(hello) = self { + hello.finalize(enc_buf, transcript, write_key_schedule) + } else { + transcript.update(enc_buf); + Ok(()) + } + } + + pub fn finalize_encrypted(buf: &mut CryptoBuffer, transcript: &mut CipherSuite::Hash) { + let enc_buf = buf.as_slice(); + let end = enc_buf.len(); + transcript.update(&enc_buf[0..end]); + } +} + +#[allow(clippy::large_enum_variant)] +pub enum ServerHandshake<'a, CipherSuite: TlsCipherSuite> { + ServerHello(ServerHello<'a>), + EncryptedExtensions(EncryptedExtensions<'a>), + NewSessionTicket(NewSessionTicket<'a>), + Certificate(CertificateRef<'a>), + CertificateRequest(CertificateRequestRef<'a>), + HandshakeVerify(HandshakeVerifyRef<'a>), + Finished(Finished>), +} + +impl ServerHandshake<'_, CipherSuite> { + #[allow(dead_code)] + pub fn handshake_type(&self) -> HandshakeType { + match self { + ServerHandshake::ServerHello(_) => HandshakeType::ServerHello, + ServerHandshake::EncryptedExtensions(_) => HandshakeType::EncryptedExtensions, + ServerHandshake::NewSessionTicket(_) => HandshakeType::NewSessionTicket, + ServerHandshake::Certificate(_) => HandshakeType::Certificate, + ServerHandshake::CertificateRequest(_) => HandshakeType::CertificateRequest, + ServerHandshake::HandshakeVerify(_) => HandshakeType::HandshakeVerify, + ServerHandshake::Finished(_) => HandshakeType::Finished, + } + } +} + +impl Debug for ServerHandshake<'_, CipherSuite> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + ServerHandshake::ServerHello(inner) => Debug::fmt(inner, f), + ServerHandshake::EncryptedExtensions(inner) => Debug::fmt(inner, f), + ServerHandshake::Certificate(inner) => Debug::fmt(inner, f), + ServerHandshake::CertificateRequest(inner) => Debug::fmt(inner, f), + ServerHandshake::HandshakeVerify(inner) => Debug::fmt(inner, f), + ServerHandshake::Finished(inner) => Debug::fmt(inner, f), + ServerHandshake::NewSessionTicket(inner) => Debug::fmt(inner, f), + } + } +} + +#[cfg(feature = "defmt")] +impl<'a, CipherSuite: TlsCipherSuite> defmt::Format for ServerHandshake<'a, CipherSuite> { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + ServerHandshake::ServerHello(inner) => defmt::write!(f, "{}", inner), + ServerHandshake::EncryptedExtensions(inner) => defmt::write!(f, "{}", inner), + ServerHandshake::Certificate(inner) => defmt::write!(f, "{}", inner), + ServerHandshake::CertificateRequest(inner) => defmt::write!(f, "{}", inner), + ServerHandshake::HandshakeVerify(inner) => defmt::write!(f, "{}", inner), + ServerHandshake::Finished(inner) => defmt::write!(f, "{}", inner), + ServerHandshake::NewSessionTicket(inner) => defmt::write!(f, "{}", inner), + } + } +} + +impl<'a, CipherSuite: TlsCipherSuite> ServerHandshake<'a, CipherSuite> { + pub fn read( + buf: &mut ParseBuffer<'a>, + digest: &mut CipherSuite::Hash, + ) -> Result { + let handshake_start = buf.offset(); + let mut handshake = Self::parse(buf)?; + let handshake_end = buf.offset(); + + if let ServerHandshake::Finished(finished) = &mut handshake { + finished.hash.replace(digest.clone().finalize()); + } + + digest.update(&buf.as_slice()[handshake_start..handshake_end]); + + Ok(handshake) + } + + fn parse(buf: &mut ParseBuffer<'a>) -> Result { + let handshake_type = HandshakeType::parse(buf).map_err(|_| ProtocolError::InvalidHandshake)?; + + trace!("handshake = {:?}", handshake_type); + + let content_len = buf.read_u24().map_err(|_| ProtocolError::InvalidHandshake)?; + + let handshake = match handshake_type { + //HandshakeType::ClientHello => {} + HandshakeType::ServerHello => ServerHandshake::ServerHello(ServerHello::parse(buf)?), + HandshakeType::NewSessionTicket => { + ServerHandshake::NewSessionTicket(NewSessionTicket::parse(buf)?) + } + //HandshakeType::EndOfEarlyData => {} + HandshakeType::EncryptedExtensions => { + ServerHandshake::EncryptedExtensions(EncryptedExtensions::parse(buf)?) + } + HandshakeType::Certificate => ServerHandshake::Certificate(CertificateRef::parse(buf)?), + + HandshakeType::CertificateRequest => { + ServerHandshake::CertificateRequest(CertificateRequestRef::parse(buf)?) + } + + HandshakeType::HandshakeVerify => { + ServerHandshake::HandshakeVerify(HandshakeVerifyRef::parse(buf)?) + } + HandshakeType::Finished => { + ServerHandshake::Finished(Finished::parse(buf, content_len)?) + } + //HandshakeType::KeyUpdate => {} + //HandshakeType::MessageHash => {} + t => { + warn!("Unimplemented handshake type: {:?}", t); + return Err(ProtocolError::Unimplemented); + } + }; + + Ok(handshake) + } +} diff --git a/src/handshake/new_session_ticket.rs b/src/handshake/new_session_ticket.rs new file mode 100644 index 0000000..7b488e1 --- /dev/null +++ b/src/handshake/new_session_ticket.rs @@ -0,0 +1,33 @@ +use core::marker::PhantomData; + +use crate::extensions::messages::NewSessionTicketExtension; +use crate::parse_buffer::ParseBuffer; +use crate::{ProtocolError, unused}; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct NewSessionTicket<'a> { + _todo: PhantomData<&'a ()>, +} + +impl<'a> NewSessionTicket<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ProtocolError> { + let lifetime = buf.read_u32()?; + let age_add = buf.read_u32()?; + + let nonce_length = buf.read_u8()?; + let nonce = buf + .slice(nonce_length as usize) + .map_err(|_| ProtocolError::InvalidNonceLength)?; + + let ticket_length = buf.read_u16()?; + let ticket = buf + .slice(ticket_length as usize) + .map_err(|_| ProtocolError::InvalidTicketLength)?; + + let extensions = NewSessionTicketExtension::parse_vector::<1>(buf)?; + + unused((lifetime, age_add, nonce, ticket, extensions)); + Ok(Self { _todo: PhantomData }) + } +} diff --git a/src/handshake/server_hello.rs b/src/handshake/server_hello.rs new file mode 100644 index 0000000..77ecc82 --- /dev/null +++ b/src/handshake/server_hello.rs @@ -0,0 +1,83 @@ +use heapless::Vec; + +use crate::cipher_suites::CipherSuite; +use crate::cipher::CryptoEngine; +use crate::extensions::extension_data::key_share::KeyShareEntry; +use crate::extensions::messages::ServerHelloExtension; +use crate::parse_buffer::ParseBuffer; +use crate::{ProtocolError, unused}; +use p256::PublicKey; +use p256::ecdh::{EphemeralSecret, SharedSecret}; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ServerHello<'a> { + extensions: Vec, 4>, +} + +impl<'a> ServerHello<'a> { + pub fn parse(buf: &mut ParseBuffer<'a>) -> Result, ProtocolError> { + //let mut buf = ParseBuffer::new(&buf[0..content_length]); + //let mut buf = ParseBuffer::new(&buf); + + let _version = buf.read_u16().map_err(|_| ProtocolError::InvalidHandshake)?; + + let mut random = [0; 32]; + buf.fill(&mut random)?; + + let session_id_length = buf + .read_u8() + .map_err(|_| ProtocolError::InvalidSessionIdLength)?; + + //info!("sh 1"); + + let session_id = buf + .slice(session_id_length as usize) + .map_err(|_| ProtocolError::InvalidSessionIdLength)?; + //info!("sh 2"); + + let cipher_suite = CipherSuite::parse(buf).map_err(|_| ProtocolError::InvalidCipherSuite)?; + + ////info!("sh 3"); + // skip compression method, it's 0. + buf.read_u8()?; + + let extensions = ServerHelloExtension::parse_vector(buf)?; + + // debug!("server random {:x}", random); + // debug!("server session-id {:x}", session_id.as_slice()); + debug!("server cipher_suite {:?}", cipher_suite); + debug!("server extensions {:?}", extensions); + + unused(session_id); + Ok(Self { extensions }) + } + + pub fn key_share(&self) -> Option<&KeyShareEntry<'_>> { + self.extensions.iter().find_map(|e| { + if let ServerHelloExtension::KeyShare(entry) = e { + Some(&entry.0) + } else { + None + } + }) + } + + pub fn calculate_shared_secret(&self, secret: &EphemeralSecret) -> Option { + let server_key_share = self.key_share()?; + let server_public_key = PublicKey::from_sec1_bytes(server_key_share.opaque).ok()?; + Some(secret.diffie_hellman(&server_public_key)) + } + + #[allow(dead_code)] + pub fn initialize_crypto_engine(&self, secret: &EphemeralSecret) -> Option { + let server_key_share = self.key_share()?; + + let group = server_key_share.group; + + let server_public_key = PublicKey::from_sec1_bytes(server_key_share.opaque).ok()?; + let shared = secret.diffie_hellman(&server_public_key); + + Some(CryptoEngine::new(group, shared)) + } +} diff --git a/src/key_schedule.rs b/src/key_schedule.rs new file mode 100644 index 0000000..5cae611 --- /dev/null +++ b/src/key_schedule.rs @@ -0,0 +1,499 @@ +use crate::handshake::binder::PskBinder; +use crate::handshake::finished::Finished; +use crate::{ProtocolError, config::TlsCipherSuite}; +use digest::OutputSizeUser; +use digest::generic_array::ArrayLength; +use hmac::{Mac, SimpleHmac}; +use sha2::Digest; +use sha2::digest::generic_array::{GenericArray, typenum::Unsigned}; + +pub type HashOutputSize = + <::Hash as OutputSizeUser>::OutputSize; +pub type LabelBufferSize = ::LabelBufferSize; + +pub type IvArray = GenericArray::IvLen>; +pub type KeyArray = GenericArray::KeyLen>; +pub type HashArray = GenericArray>; + +type Hkdf = hkdf::Hkdf< + ::Hash, + SimpleHmac<::Hash>, +>; + +enum Secret +where + CipherSuite: TlsCipherSuite, +{ + Uninitialized, + Initialized(Hkdf), +} + +impl Secret +where + CipherSuite: TlsCipherSuite, +{ + fn replace(&mut self, secret: Hkdf) { + *self = Self::Initialized(secret); + } + + fn as_ref(&self) -> Result<&Hkdf, ProtocolError> { + match self { + Secret::Initialized(secret) => Ok(secret), + Secret::Uninitialized => Err(ProtocolError::InternalError), + } + } + + fn make_expanded_hkdf_label>( + &self, + label: &[u8], + context_type: ContextType, + ) -> Result, ProtocolError> { + //info!("make label {:?} {}", label, len); + let mut hkdf_label = heapless_typenum::Vec::>::new(); + hkdf_label + .extend_from_slice(&N::to_u16().to_be_bytes()) + .map_err(|()| ProtocolError::InternalError)?; + + let label_len = 6 + label.len() as u8; + hkdf_label + .extend_from_slice(&label_len.to_be_bytes()) + .map_err(|()| ProtocolError::InternalError)?; + hkdf_label + .extend_from_slice(b"tls13 ") + .map_err(|()| ProtocolError::InternalError)?; + hkdf_label + .extend_from_slice(label) + .map_err(|()| ProtocolError::InternalError)?; + + match context_type { + ContextType::None => { + hkdf_label.push(0).map_err(|_| ProtocolError::InternalError)?; + } + ContextType::Hash(context) => { + hkdf_label + .extend_from_slice(&(context.len() as u8).to_be_bytes()) + .map_err(|()| ProtocolError::InternalError)?; + hkdf_label + .extend_from_slice(&context) + .map_err(|()| ProtocolError::InternalError)?; + } + } + + let mut okm = GenericArray::default(); + //info!("label {:x?}", label); + self.as_ref()? + .expand(&hkdf_label, &mut okm) + .map_err(|_| ProtocolError::CryptoError)?; + //info!("expand {:x?}", okm); + Ok(okm) + } +} + +pub struct SharedState +where + CipherSuite: TlsCipherSuite, +{ + secret: HashArray, + hkdf: Secret, +} + +impl SharedState +where + CipherSuite: TlsCipherSuite, +{ + fn new() -> Self { + Self { + secret: GenericArray::default(), + hkdf: Secret::Uninitialized, + } + } + + fn initialize(&mut self, ikm: &[u8]) { + let (secret, hkdf) = Hkdf::::extract(Some(self.secret.as_ref()), ikm); + self.hkdf.replace(hkdf); + self.secret = secret; + } + + fn derive_secret( + &mut self, + label: &[u8], + context_type: ContextType, + ) -> Result, ProtocolError> { + self.hkdf + .make_expanded_hkdf_label::>(label, context_type) + } + + fn derived(&mut self) -> Result<(), ProtocolError> { + self.secret = self.derive_secret(b"derived", ContextType::empty_hash())?; + Ok(()) + } +} + +pub(crate) struct KeyScheduleState +where + CipherSuite: TlsCipherSuite, +{ + traffic_secret: Secret, + counter: u64, + key: KeyArray, + iv: IvArray, +} + +impl KeyScheduleState +where + CipherSuite: TlsCipherSuite, +{ + fn new() -> Self { + Self { + traffic_secret: Secret::Uninitialized, + counter: 0, + key: KeyArray::::default(), + iv: IvArray::::default(), + } + } + + #[inline] + pub fn get_key(&self) -> Result<&KeyArray, ProtocolError> { + Ok(&self.key) + } + + #[inline] + pub fn get_iv(&self) -> Result<&IvArray, ProtocolError> { + Ok(&self.iv) + } + + pub fn get_nonce(&self) -> Result, ProtocolError> { + let iv = self.get_iv()?; + Ok(KeySchedule::::get_nonce(self.counter, iv)) + } + + fn calculate_traffic_secret( + &mut self, + label: &[u8], + shared: &mut SharedState, + transcript_hash: &CipherSuite::Hash, + ) -> Result<(), ProtocolError> { + let secret = shared.derive_secret(label, ContextType::transcript_hash(transcript_hash))?; + let traffic_secret = + Hkdf::::from_prk(&secret).map_err(|_| ProtocolError::InternalError)?; + + self.traffic_secret.replace(traffic_secret); + self.key = self + .traffic_secret + .make_expanded_hkdf_label(b"key", ContextType::None)?; + self.iv = self + .traffic_secret + .make_expanded_hkdf_label(b"iv", ContextType::None)?; + self.counter = 0; + Ok(()) + } + + pub fn increment_counter(&mut self) { + self.counter = unwrap!(self.counter.checked_add(1)); + } +} + +enum ContextType +where + CipherSuite: TlsCipherSuite, +{ + None, + Hash(HashArray), +} + +impl ContextType +where + CipherSuite: TlsCipherSuite, +{ + fn transcript_hash(hash: &CipherSuite::Hash) -> Self { + Self::Hash(hash.clone().finalize()) + } + + fn empty_hash() -> Self { + Self::Hash( + ::new() + .chain_update([]) + .finalize(), + ) + } +} + +pub struct KeySchedule +where + CipherSuite: TlsCipherSuite, +{ + shared: SharedState, + client_state: WriteKeySchedule, + server_state: ReadKeySchedule, +} + +impl KeySchedule +where + CipherSuite: TlsCipherSuite, +{ + pub fn new() -> Self { + Self { + shared: SharedState::new(), + client_state: WriteKeySchedule { + state: KeyScheduleState::new(), + binder_key: Secret::Uninitialized, + }, + server_state: ReadKeySchedule { + state: KeyScheduleState::new(), + transcript_hash: ::new(), + }, + } + } + + pub(crate) fn transcript_hash(&mut self) -> &mut CipherSuite::Hash { + &mut self.server_state.transcript_hash + } + + pub(crate) fn replace_transcript_hash(&mut self, hash: CipherSuite::Hash) { + self.server_state.transcript_hash = hash; + } + + pub fn as_split( + &mut self, + ) -> ( + &mut WriteKeySchedule, + &mut ReadKeySchedule, + ) { + (&mut self.client_state, &mut self.server_state) + } + + pub(crate) fn write_state(&mut self) -> &mut WriteKeySchedule { + &mut self.client_state + } + + pub(crate) fn read_state(&mut self) -> &mut ReadKeySchedule { + &mut self.server_state + } + + pub fn create_client_finished( + &self, + ) -> Result>, ProtocolError> { + let key = self + .client_state + .state + .traffic_secret + .make_expanded_hkdf_label::>( + b"finished", + ContextType::None, + )?; + + let mut hmac = SimpleHmac::::new_from_slice(&key) + .map_err(|_| ProtocolError::CryptoError)?; + Mac::update( + &mut hmac, + &self.server_state.transcript_hash.clone().finalize(), + ); + let verify = hmac.finalize().into_bytes(); + + Ok(Finished { verify, hash: None }) + } + + fn get_nonce(counter: u64, iv: &IvArray) -> IvArray { + //info!("counter = {} {:x?}", counter, &counter.to_be_bytes(),); + let counter = Self::pad::(&counter.to_be_bytes()); + + //info!("counter = {:x?}", counter); + // info!("iv = {:x?}", iv); + + let mut nonce = GenericArray::default(); + + for (index, (l, r)) in iv[0..CipherSuite::IvLen::to_usize()] + .iter() + .zip(counter.iter()) + .enumerate() + { + nonce[index] = l ^ r; + } + + //debug!("nonce {:x?}", nonce); + + nonce + } + + fn pad>(input: &[u8]) -> GenericArray { + // info!("padding input = {:x?}", input); + let mut padded = GenericArray::default(); + for (index, byte) in input.iter().rev().enumerate() { + /*info!( + "{} pad {}={:x?}", + index, + ((N::to_usize() - index) - 1), + *byte + );*/ + padded[(N::to_usize() - index) - 1] = *byte; + } + padded + } + + fn zero() -> HashArray { + GenericArray::default() + } + + // Initializes the early secrets with a callback for any PSK binders + pub fn initialize_early_secret(&mut self, psk: Option<&[u8]>) -> Result<(), ProtocolError> { + self.shared.initialize( + #[allow(clippy::or_fun_call)] + psk.unwrap_or(Self::zero().as_slice()), + ); + + let binder_key = self + .shared + .derive_secret(b"ext binder", ContextType::empty_hash())?; + self.client_state.binder_key.replace( + Hkdf::::from_prk(&binder_key).map_err(|_| ProtocolError::InternalError)?, + ); + self.shared.derived() + } + + pub fn initialize_handshake_secret(&mut self, ikm: &[u8]) -> Result<(), ProtocolError> { + self.shared.initialize(ikm); + + self.calculate_traffic_secrets(b"c hs traffic", b"s hs traffic")?; + self.shared.derived() + } + + pub fn initialize_master_secret(&mut self) -> Result<(), ProtocolError> { + self.shared.initialize(Self::zero().as_slice()); + + //let context = self.transcript_hash.as_ref().unwrap().clone().finalize(); + //info!("Derive keys, hash: {:x?}", context); + + self.calculate_traffic_secrets(b"c ap traffic", b"s ap traffic")?; + self.shared.derived() + } + + fn calculate_traffic_secrets( + &mut self, + client_label: &[u8], + server_label: &[u8], + ) -> Result<(), ProtocolError> { + self.client_state.state.calculate_traffic_secret( + client_label, + &mut self.shared, + &self.server_state.transcript_hash, + )?; + + self.server_state.state.calculate_traffic_secret( + server_label, + &mut self.shared, + &self.server_state.transcript_hash, + )?; + + Ok(()) + } +} + +impl Default for KeySchedule +where + CipherSuite: TlsCipherSuite, +{ + fn default() -> Self { + KeySchedule::new() + } +} + +pub struct WriteKeySchedule +where + CipherSuite: TlsCipherSuite, +{ + state: KeyScheduleState, + binder_key: Secret, +} +impl WriteKeySchedule +where + CipherSuite: TlsCipherSuite, +{ + pub(crate) fn increment_counter(&mut self) { + self.state.increment_counter(); + } + + pub(crate) fn get_key(&self) -> Result<&KeyArray, ProtocolError> { + self.state.get_key() + } + + pub(crate) fn get_nonce(&self) -> Result, ProtocolError> { + self.state.get_nonce() + } + + pub fn create_psk_binder( + &self, + transcript_hash: &CipherSuite::Hash, + ) -> Result>, ProtocolError> { + let key = self + .binder_key + .make_expanded_hkdf_label::>( + b"finished", + ContextType::None, + )?; + + let mut hmac = SimpleHmac::::new_from_slice(&key) + .map_err(|_| ProtocolError::CryptoError)?; + Mac::update(&mut hmac, &transcript_hash.clone().finalize()); + let verify = hmac.finalize().into_bytes(); + Ok(PskBinder { verify }) + } +} + +pub struct ReadKeySchedule +where + CipherSuite: TlsCipherSuite, +{ + state: KeyScheduleState, + transcript_hash: CipherSuite::Hash, +} + +impl ReadKeySchedule +where + CipherSuite: TlsCipherSuite, +{ + pub(crate) fn increment_counter(&mut self) { + self.state.increment_counter(); + } + + pub(crate) fn transcript_hash(&mut self) -> &mut CipherSuite::Hash { + &mut self.transcript_hash + } + + pub(crate) fn get_key(&self) -> Result<&KeyArray, ProtocolError> { + self.state.get_key() + } + + pub(crate) fn get_nonce(&self) -> Result, ProtocolError> { + self.state.get_nonce() + } + + pub fn verify_server_finished( + &self, + finished: &Finished>, + ) -> Result { + //info!("verify server finished: {:x?}", finished.verify); + //self.client_traffic_secret.as_ref().unwrap().expand() + //info!("size ===> {}", D::OutputSize::to_u16()); + let key = self + .state + .traffic_secret + .make_expanded_hkdf_label::>( + b"finished", + ContextType::None, + )?; + // info!("hmac sign key {:x?}", key); + let mut hmac = SimpleHmac::::new_from_slice(&key) + .map_err(|_| ProtocolError::InternalError)?; + Mac::update( + &mut hmac, + finished.hash.as_ref().ok_or_else(|| { + warn!("No hash in Finished"); + ProtocolError::InternalError + })?, + ); + //let code = hmac.clone().finalize().into_bytes(); + Ok(hmac.verify(&finished.verify).is_ok()) + //info!("verified {:?}", verified); + //unimplemented!() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0787858 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,169 @@ +#![cfg_attr(not(any(test, feature = "std")), no_std)] +#![warn(clippy::pedantic)] +#![allow( + clippy::module_name_repetitions, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::missing_errors_doc // TODO +)] + +/*! +# Example + +``` +use mote_tls::*; +use embedded_io_adapters::tokio_1::FromTokio; +use rand::rngs::OsRng; +use tokio::net::TcpStream; + +#[tokio::main] +async fn main() { + let stream = TcpStream::connect("google.com:443") + .await + .expect("error creating TCP connection"); + + println!("TCP connection opened"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("google.com").enable_rsa_signatures(); + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + // Allows disabling cert verification, in case you are using PSK and don't need it, or are just testing. + // otherwise, use mote_tls::cert_verify::CertVerifier, which only works on std for now. + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .await + .expect("error establishing TLS connection"); + + println!("TLS session opened"); +} +``` +*/ + +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + +use parse_buffer::ParseError; +pub mod alert; +mod application_data; +pub mod blocking; +mod buffer; +mod change_cipher_spec; +mod cipher_suites; +mod common; +mod config; +mod connection; +mod content_types; +mod cipher; +mod extensions; +pub mod send_policy; +mod handshake; +mod key_schedule; +mod parse_buffer; +pub mod read_buffer; +mod record; +mod record_reader; +mod write_buffer; + +pub use config::SkipVerifyProvider; +pub use extensions::extension_data::signature_algorithms::SignatureScheme; +pub use handshake::certificate_verify::HandshakeVerify; +pub use rand_core::{CryptoRng, CryptoRngCore}; + +#[cfg(feature = "webpki")] +pub mod cert_verify; + +#[cfg(feature = "native-pki")] +mod certificate; +#[cfg(feature = "native-pki")] +pub mod native_pki; + +mod asynch; +pub use asynch::*; + +pub use send_policy::*; + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProtocolError { + ConnectionClosed, + Unimplemented, + MissingHandshake, + HandshakeAborted(alert::AlertLevel, alert::AlertDescription), + AbortHandshake(alert::AlertLevel, alert::AlertDescription), + IoError, + InternalError, + InvalidRecord, + UnknownContentType, + InvalidNonceLength, + InvalidTicketLength, + UnknownExtensionType, + InsufficientSpace, + InvalidHandshake, + InvalidCipherSuite, + InvalidSignatureScheme, + InvalidSignature, + InvalidExtensionsLength, + InvalidSessionIdLength, + InvalidSupportedVersions, + InvalidApplicationData, + InvalidKeyShare, + InvalidCertificate, + InvalidCertificateEntry, + InvalidCertificateRequest, + InvalidPrivateKey, + UnableToInitializeCryptoEngine, + ParseError(ParseError), + OutOfMemory, + CryptoError, + EncodeError, + DecodeError, + Io(embedded_io::ErrorKind), +} + +impl embedded_io::Error for ProtocolError { + fn kind(&self) -> embedded_io::ErrorKind { + if let Self::Io(k) = self { + *k + } else { + error!("TLS error: {:?}", self); + embedded_io::ErrorKind::Other + } + } +} + +impl core::fmt::Display for ProtocolError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{self:?}") + } +} + +impl core::error::Error for ProtocolError {} + +#[cfg(feature = "std")] +mod stdlib { + use crate::config::TlsClock; + + use std::time::SystemTime; + impl TlsClock for SystemTime { + fn now() -> Option { + Some( + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), + ) + } + } +} + +/// An internal function to mark an unused value. +/// +/// All calls to this should be removed before 1.x. +fn unused(_: T) {} diff --git a/src/native_pki.rs b/src/native_pki.rs new file mode 100644 index 0000000..ab6abf5 --- /dev/null +++ b/src/native_pki.rs @@ -0,0 +1,468 @@ +use crate::ProtocolError; +use crate::config::{Certificate, TlsCipherSuite, TlsClock, Verifier}; +#[cfg(feature = "p384")] +use crate::certificate::ECDSA_SHA384; +#[cfg(feature = "ed25519")] +use crate::certificate::ED25519; +use crate::certificate::{DecodedCertificate, ECDSA_SHA256, Time}; +#[cfg(feature = "rsa")] +use crate::certificate::{RSA_PKCS1_SHA256, RSA_PKCS1_SHA384, RSA_PKCS1_SHA512}; +use crate::extensions::extension_data::signature_algorithms::SignatureScheme; +use crate::handshake::{ + certificate::{ + Certificate as OwnedCertificate, CertificateEntryRef, CertificateRef as ServerCertificate, + }, + certificate_verify::HandshakeVerifyRef, +}; +use crate::parse_buffer::ParseError; +use const_oid::ObjectIdentifier; +use core::marker::PhantomData; +use der::Decode; +use digest::Digest; +use heapless::{String, Vec}; + +const HOSTNAME_MAXLEN: usize = 64; +const COMMON_NAME_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.3"); + +pub struct CertificateChain<'a> { + prev: Option<&'a CertificateEntryRef<'a>>, + chain: &'a ServerCertificate<'a>, + idx: isize, +} + +impl<'a> CertificateChain<'a> { + pub fn new(ca: &'a CertificateEntryRef, chain: &'a ServerCertificate<'a>) -> Self { + Self { + prev: Some(ca), + chain, + idx: chain.entries.len() as isize - 1, + } + } +} + +impl<'a> Iterator for CertificateChain<'a> { + type Item = (&'a CertificateEntryRef<'a>, &'a CertificateEntryRef<'a>); + + fn next(&mut self) -> Option { + if self.idx < 0 { + return None; + } + + let cur = &self.chain.entries[self.idx as usize]; + let out = (self.prev.unwrap(), cur); + + self.prev = Some(cur); + self.idx -= 1; + + Some(out) + } +} + +pub struct CertVerifier<'a, CipherSuite, Clock, const CERT_SIZE: usize> +where + Clock: TlsClock, + CipherSuite: TlsCipherSuite, +{ + ca: Certificate<&'a [u8]>, + host: Option>, + certificate_transcript: Option, + certificate: Option>, + _clock: PhantomData, +} + +impl<'a, CipherSuite, Clock, const CERT_SIZE: usize> CertVerifier<'a, CipherSuite, Clock, CERT_SIZE> +where + Clock: TlsClock, + CipherSuite: TlsCipherSuite, +{ + #[must_use] + pub fn new(ca: Certificate<&'a [u8]>) -> Self { + Self { + ca, + host: None, + certificate_transcript: None, + certificate: None, + _clock: PhantomData, + } + } +} + +impl Verifier + for CertVerifier<'_, CipherSuite, Clock, CERT_SIZE> +where + CipherSuite: TlsCipherSuite, + Clock: TlsClock, +{ + fn set_hostname_verification(&mut self, hostname: &str) -> Result<(), ProtocolError> { + self.host.replace( + heapless::String::try_from(hostname).map_err(|_| ProtocolError::InsufficientSpace)?, + ); + Ok(()) + } + + fn verify_certificate( + &mut self, + transcript: &CipherSuite::Hash, + cert: ServerCertificate, + ) -> Result<(), ProtocolError> { + let mut cn = None; + for (p, q) in CertificateChain::new(&(&self.ca).into(), &cert) { + cn = verify_certificate(p, q, Clock::now())?; + } + if self.host.ne(&cn) { + error!( + "Hostname ({:?}) does not match CommonName ({:?})", + self.host, cn + ); + return Err(ProtocolError::InvalidCertificate); + } + + self.certificate.replace(cert.try_into()?); + self.certificate_transcript.replace(transcript.clone()); + Ok(()) + } + + fn verify_signature(&mut self, verify: HandshakeVerifyRef) -> Result<(), ProtocolError> { + let handshake_hash = unwrap!(self.certificate_transcript.take()); + let ctx_str = b"TLS 1.3, server HandshakeVerify\x00"; + let mut msg: Vec = Vec::new(); + msg.resize(64, 0x20).map_err(|_| ProtocolError::EncodeError)?; + msg.extend_from_slice(ctx_str) + .map_err(|_| ProtocolError::EncodeError)?; + msg.extend_from_slice(&handshake_hash.finalize()) + .map_err(|_| ProtocolError::EncodeError)?; + + let certificate = unwrap!(self.certificate.as_ref()).try_into()?; + verify_signature(&msg[..], &certificate, &verify)?; + Ok(()) + } +} + +fn verify_signature( + message: &[u8], + certificate: &ServerCertificate, + verify: &HandshakeVerifyRef, +) -> Result<(), ProtocolError> { + let verified; + + let certificate = + if let Some(CertificateEntryRef::X509(certificate)) = certificate.entries.first() { + certificate + } else { + return Err(ProtocolError::DecodeError); + }; + + let certificate = + DecodedCertificate::from_der(certificate).map_err(|_| ProtocolError::DecodeError)?; + + let public_key = certificate + .tbs_certificate + .subject_public_key_info + .public_key + .as_bytes() + .ok_or(ProtocolError::DecodeError)?; + + match verify.signature_scheme { + SignatureScheme::EcdsaSecp256r1Sha256 => { + use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; + let verifying_key = + VerifyingKey::from_sec1_bytes(public_key).map_err(|_| ProtocolError::DecodeError)?; + let signature = + Signature::from_der(&verify.signature).map_err(|_| ProtocolError::DecodeError)?; + verified = verifying_key.verify(message, &signature).is_ok(); + } + #[cfg(feature = "p384")] + SignatureScheme::EcdsaSecp384r1Sha384 => { + use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; + let verifying_key = + VerifyingKey::from_sec1_bytes(public_key).map_err(|_| ProtocolError::DecodeError)?; + let signature = + Signature::from_der(&verify.signature).map_err(|_| ProtocolError::DecodeError)?; + verified = verifying_key.verify(message, &signature).is_ok(); + } + #[cfg(feature = "ed25519")] + SignatureScheme::Ed25519 => { + use ed25519_dalek::{Signature, Verifier, VerifyingKey}; + let verifying_key: VerifyingKey = + VerifyingKey::from_bytes(public_key.try_into().unwrap()) + .map_err(|_| ProtocolError::DecodeError)?; + let signature = + Signature::try_from(verify.signature).map_err(|_| ProtocolError::DecodeError)?; + verified = verifying_key.verify(message, &signature).is_ok(); + } + #[cfg(feature = "rsa")] + SignatureScheme::RsaPssRsaeSha256 => { + use rsa::{ + RsaPublicKey, + pkcs1::DecodeRsaPublicKey, + pss::{Signature, VerifyingKey}, + signature::Verifier, + }; + use sha2::Sha256; + + let der_pubkey = RsaPublicKey::from_pkcs1_der(public_key).unwrap(); + let verifying_key = VerifyingKey::::from(der_pubkey); + + let signature = + Signature::try_from(verify.signature).map_err(|_| ProtocolError::DecodeError)?; + verified = verifying_key.verify(message, &signature).is_ok(); + } + #[cfg(feature = "rsa")] + SignatureScheme::RsaPssRsaeSha384 => { + use rsa::{ + RsaPublicKey, + pkcs1::DecodeRsaPublicKey, + pss::{Signature, VerifyingKey}, + signature::Verifier, + }; + use sha2::Sha384; + + let der_pubkey = + RsaPublicKey::from_pkcs1_der(public_key).map_err(|_| ProtocolError::DecodeError)?; + let verifying_key = VerifyingKey::::from(der_pubkey); + + let signature = + Signature::try_from(verify.signature).map_err(|_| ProtocolError::DecodeError)?; + verified = verifying_key.verify(message, &signature).is_ok(); + } + #[cfg(feature = "rsa")] + SignatureScheme::RsaPssRsaeSha512 => { + use rsa::{ + RsaPublicKey, + pkcs1::DecodeRsaPublicKey, + pss::{Signature, VerifyingKey}, + signature::Verifier, + }; + use sha2::Sha512; + + let der_pubkey = + RsaPublicKey::from_pkcs1_der(public_key).map_err(|_| ProtocolError::DecodeError)?; + let verifying_key = VerifyingKey::::from(der_pubkey); + + let signature = + Signature::try_from(verify.signature).map_err(|_| ProtocolError::DecodeError)?; + verified = verifying_key.verify(message, &signature).is_ok(); + } + _ => { + error!( + "InvalidSignatureScheme: {:?} Are you missing a feature?", + verify.signature_scheme + ); + return Err(ProtocolError::InvalidSignatureScheme); + } + } + + if !verified { + return Err(ProtocolError::InvalidSignature); + } + Ok(()) +} + +fn get_certificate_tlv_bytes<'a>(input: &[u8]) -> der::Result<&[u8]> { + use der::{Decode, Reader, SliceReader}; + + let mut reader = SliceReader::new(input)?; + let top_header = der::Header::decode(&mut reader)?; + top_header.tag().assert_eq(der::Tag::Sequence)?; + + let header = der::Header::peek(&mut reader)?; + header.tag().assert_eq(der::Tag::Sequence)?; + + // Should we read the remaining two fields and call reader.finish() just be certain here? + reader.tlv_bytes() +} + +fn get_cert_time(time: Time) -> u64 { + match time { + Time::UtcTime(utc_time) => utc_time.to_unix_duration().as_secs(), + Time::GeneralTime(generalized_time) => generalized_time.to_unix_duration().as_secs(), + } +} + +fn verify_certificate( + verifier: &CertificateEntryRef, + certificate: &CertificateEntryRef, + now: Option, +) -> Result>, ProtocolError> { + let mut verified = false; + let mut common_name = None; + + let ca_certificate = if let CertificateEntryRef::X509(verifier) = verifier { + DecodedCertificate::from_der(verifier).map_err(|_| ProtocolError::DecodeError)? + } else { + return Err(ProtocolError::DecodeError); + }; + + if let CertificateEntryRef::X509(certificate) = certificate { + let parsed_certificate = + DecodedCertificate::from_der(certificate).map_err(|_| ProtocolError::DecodeError)?; + + let ca_public_key = ca_certificate + .tbs_certificate + .subject_public_key_info + .public_key + .as_bytes() + .ok_or(ProtocolError::DecodeError)?; + + for elems in parsed_certificate.tbs_certificate.subject.iter() { + let attrs = elems + .get(0) + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?; + if attrs.oid == COMMON_NAME_OID { + let mut v: Vec = Vec::new(); + v.extend_from_slice(attrs.value.value()) + .map_err(|_| ProtocolError::ParseError(ParseError::InvalidData))?; + common_name = String::from_utf8(v).ok(); + debug!("CommonName: {:?}", common_name); + } + } + + if let Some(now) = now { + if get_cert_time(parsed_certificate.tbs_certificate.validity.not_before) > now + || get_cert_time(parsed_certificate.tbs_certificate.validity.not_after) < now + { + return Err(ProtocolError::InvalidCertificate); + } + debug!("Epoch is {} and certificate is valid!", now) + } + + let certificate_data = + get_certificate_tlv_bytes(certificate).map_err(|_| ProtocolError::DecodeError)?; + + match parsed_certificate.signature_algorithm { + ECDSA_SHA256 => { + use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; + let verifying_key = VerifyingKey::from_sec1_bytes(ca_public_key) + .map_err(|_| ProtocolError::DecodeError)?; + + let signature = Signature::from_der( + parsed_certificate + .signature + .as_bytes() + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?, + ) + .map_err(|_| ProtocolError::ParseError(ParseError::InvalidData))?; + + verified = verifying_key.verify(&certificate_data, &signature).is_ok(); + } + #[cfg(feature = "p384")] + ECDSA_SHA384 => { + use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; + let verifying_key = VerifyingKey::from_sec1_bytes(ca_public_key) + .map_err(|_| ProtocolError::DecodeError)?; + + let signature = Signature::from_der( + parsed_certificate + .signature + .as_bytes() + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?, + ) + .map_err(|_| ProtocolError::ParseError(ParseError::InvalidData))?; + + verified = verifying_key.verify(&certificate_data, &signature).is_ok(); + } + #[cfg(feature = "ed25519")] + ED25519 => { + use ed25519_dalek::{Signature, Verifier, VerifyingKey}; + let verifying_key: VerifyingKey = + VerifyingKey::from_bytes(ca_public_key.try_into().unwrap()) + .map_err(|_| ProtocolError::DecodeError)?; + + let signature = Signature::try_from( + parsed_certificate + .signature + .as_bytes() + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?, + ) + .map_err(|_| ProtocolError::ParseError(ParseError::InvalidData))?; + + verified = verifying_key.verify(certificate_data, &signature).is_ok(); + } + #[cfg(feature = "rsa")] + a if a == RSA_PKCS1_SHA256 => { + use rsa::{ + pkcs1::DecodeRsaPublicKey, + pkcs1v15::{Signature, VerifyingKey}, + signature::Verifier, + }; + use sha2::Sha256; + + let verifying_key = + VerifyingKey::::from_pkcs1_der(ca_public_key).map_err(|e| { + error!("VerifyingKey: {}", e); + ProtocolError::DecodeError + })?; + + let signature = Signature::try_from( + parsed_certificate + .signature + .as_bytes() + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?, + ) + .map_err(|e| { + error!("Signature: {}", e); + ProtocolError::ParseError(ParseError::InvalidData) + })?; + + verified = verifying_key.verify(certificate_data, &signature).is_ok(); + } + #[cfg(feature = "rsa")] + a if a == RSA_PKCS1_SHA384 => { + use rsa::{ + pkcs1::DecodeRsaPublicKey, + pkcs1v15::{Signature, VerifyingKey}, + signature::Verifier, + }; + use sha2::Sha384; + + let verifying_key = VerifyingKey::::from_pkcs1_der(ca_public_key) + .map_err(|_| ProtocolError::DecodeError)?; + + let signature = Signature::try_from( + parsed_certificate + .signature + .as_bytes() + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?, + ) + .map_err(|_| ProtocolError::ParseError(ParseError::InvalidData))?; + + verified = verifying_key.verify(certificate_data, &signature).is_ok(); + } + #[cfg(feature = "rsa")] + a if a == RSA_PKCS1_SHA512 => { + use rsa::{ + pkcs1::DecodeRsaPublicKey, + pkcs1v15::{Signature, VerifyingKey}, + signature::Verifier, + }; + use sha2::Sha512; + + let verifying_key = VerifyingKey::::from_pkcs1_der(ca_public_key) + .map_err(|_| ProtocolError::DecodeError)?; + + let signature = Signature::try_from( + parsed_certificate + .signature + .as_bytes() + .ok_or(ProtocolError::ParseError(ParseError::InvalidData))?, + ) + .map_err(|_| ProtocolError::ParseError(ParseError::InvalidData))?; + + verified = verifying_key.verify(certificate_data, &signature).is_ok(); + } + _ => { + error!( + "Unsupported signature alg: {:?}", + parsed_certificate.signature_algorithm + ); + return Err(ProtocolError::InvalidSignatureScheme); + } + } + } + + if !verified { + return Err(ProtocolError::InvalidCertificate); + } + + Ok(common_name) +} diff --git a/src/parse_buffer.rs b/src/parse_buffer.rs new file mode 100644 index 0000000..afe4275 --- /dev/null +++ b/src/parse_buffer.rs @@ -0,0 +1,173 @@ +use crate::ProtocolError; +use heapless::{CapacityError, Vec}; + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseError { + InsufficientBytes, + InsufficientSpace, + InvalidData, +} + +pub struct ParseBuffer<'b> { + pos: usize, + buffer: &'b [u8], +} + +impl<'b> From<&'b [u8]> for ParseBuffer<'b> { + fn from(val: &'b [u8]) -> Self { + ParseBuffer::new(val) + } +} + +impl<'b, const N: usize> From> for Result, CapacityError> { + fn from(val: ParseBuffer<'b>) -> Self { + Vec::from_slice(&val.buffer[val.pos..]) + } +} + +impl<'b> ParseBuffer<'b> { + pub fn new(buffer: &'b [u8]) -> Self { + Self { pos: 0, buffer } + } + + pub fn is_empty(&self) -> bool { + self.pos == self.buffer.len() + } + + pub fn remaining(&self) -> usize { + self.buffer.len() - self.pos + } + + pub fn offset(&self) -> usize { + self.pos + } + + pub fn as_slice(&self) -> &'b [u8] { + self.buffer + } + + pub fn slice(&mut self, len: usize) -> Result, ParseError> { + if self.pos + len <= self.buffer.len() { + let slice = ParseBuffer::new(&self.buffer[self.pos..self.pos + len]); + self.pos += len; + Ok(slice) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn read_u8(&mut self) -> Result { + if self.pos < self.buffer.len() { + let value = self.buffer[self.pos]; + self.pos += 1; + Ok(value) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn read_u16(&mut self) -> Result { + //info!("pos={} len={}", self.pos, self.buffer.len()); + if self.pos + 2 <= self.buffer.len() { + let value = u16::from_be_bytes([self.buffer[self.pos], self.buffer[self.pos + 1]]); + self.pos += 2; + Ok(value) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn read_u24(&mut self) -> Result { + if self.pos + 3 <= self.buffer.len() { + let value = u32::from_be_bytes([ + 0, + self.buffer[self.pos], + self.buffer[self.pos + 1], + self.buffer[self.pos + 2], + ]); + self.pos += 3; + Ok(value) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn read_u32(&mut self) -> Result { + if self.pos + 4 <= self.buffer.len() { + let value = u32::from_be_bytes([ + self.buffer[self.pos], + self.buffer[self.pos + 1], + self.buffer[self.pos + 2], + self.buffer[self.pos + 3], + ]); + self.pos += 4; + Ok(value) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn fill(&mut self, dest: &mut [u8]) -> Result<(), ParseError> { + if self.pos + dest.len() <= self.buffer.len() { + dest.copy_from_slice(&self.buffer[self.pos..self.pos + dest.len()]); + self.pos += dest.len(); + // info!("Copied {} bytes", dest.len()); + Ok(()) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn copy( + &mut self, + dest: &mut Vec, + num_bytes: usize, + ) -> Result<(), ParseError> { + let space = dest.capacity() - dest.len(); + if space < num_bytes { + error!( + "Insufficient space in destination buffer. Space: {} required: {}", + space, num_bytes + ); + Err(ParseError::InsufficientSpace) + } else if self.pos + num_bytes <= self.buffer.len() { + dest.extend_from_slice(&self.buffer[self.pos..self.pos + num_bytes]) + .map_err(|_| { + error!( + "Failed to extend destination buffer. Space: {} required: {}", + space, num_bytes + ); + ParseError::InsufficientSpace + })?; + self.pos += num_bytes; + Ok(()) + } else { + Err(ParseError::InsufficientBytes) + } + } + + pub fn read_list( + &mut self, + data_length: usize, + read: impl Fn(&mut ParseBuffer<'b>) -> Result, + ) -> Result, ParseError> { + let mut result = Vec::new(); + + let mut data = self.slice(data_length)?; + while !data.is_empty() { + result.push(read(&mut data)?).map_err(|_| { + error!("Failed to store parse result"); + ParseError::InsufficientSpace + })?; + } + + Ok(result) + } +} + +impl From for ProtocolError { + fn from(e: ParseError) -> Self { + ProtocolError::ParseError(e) + } +} diff --git a/src/read_buffer.rs b/src/read_buffer.rs new file mode 100644 index 0000000..99bfdff --- /dev/null +++ b/src/read_buffer.rs @@ -0,0 +1,180 @@ +/// A reference to consume bytes from the internal buffer. +#[must_use] +pub struct ReadBuffer<'a> { + data: &'a [u8], + consumed: usize, + used: bool, + + decrypted_consumed: &'a mut usize, +} + +impl<'a> ReadBuffer<'a> { + #[inline] + pub(crate) fn new(buffer: &'a [u8], decrypted_consumed: &'a mut usize) -> Self { + Self { + data: buffer, + consumed: 0, + used: false, + decrypted_consumed, + } + } + + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.data.len() - self.consumed + } + + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Consumes and returns a slice of at most `count` bytes. + #[inline] + pub fn peek(&mut self, count: usize) -> &'a [u8] { + let count = self.len().min(count); + let start = self.consumed; + + // We mark the buffer used to prevent dropping unconsumed bytes. + self.used = true; + + &self.data[start..start + count] + } + + /// Consumes and returns a slice of at most `count` bytes. + #[inline] + pub fn peek_all(&mut self) -> &'a [u8] { + self.peek(self.len()) + } + + /// Consumes and returns a slice of at most `count` bytes. + #[inline] + pub fn pop(&mut self, count: usize) -> &'a [u8] { + let count = self.len().min(count); + let start = self.consumed; + self.consumed += count; + self.used = true; + + &self.data[start..start + count] + } + + /// Consumes and returns the internal buffer. + #[inline] + pub fn pop_all(&mut self) -> &'a [u8] { + self.pop(self.len()) + } + + /// Drops the reference and restores internal buffer. + #[inline] + pub fn revert(self) { + core::mem::forget(self); + } + + /// Tries to fills the buffer by consuming and copying bytes into it. + #[inline] + pub fn pop_into(&mut self, buf: &mut [u8]) -> usize { + let to_copy = self.pop(buf.len()); + + buf[..to_copy.len()].copy_from_slice(to_copy); + + to_copy.len() + } +} + +impl Drop for ReadBuffer<'_> { + #[inline] + fn drop(&mut self) { + *self.decrypted_consumed += if self.used { + self.consumed + } else { + // Consume all if dropped unused + self.data.len() + }; + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn dropping_unused_buffer_consumes_all() { + let mut consumed = 1000; + let buffer = [0, 1, 2, 3]; + + _ = ReadBuffer::new(&buffer, &mut consumed); + + assert_eq!(consumed, 1004); + } + + #[test] + fn pop_moves_internal_cursor() { + let mut consumed = 0; + + let mut buffer = ReadBuffer::new(&[0, 1, 2, 3], &mut consumed); + + assert_eq!(buffer.pop(1), &[0]); + assert_eq!(buffer.pop(1), &[1]); + assert_eq!(buffer.pop(1), &[2]); + } + + #[test] + fn dropping_consumes_as_many_bytes_as_used() { + let mut consumed = 0; + + let mut buffer = ReadBuffer::new(&[0, 1, 2, 3], &mut consumed); + + assert_eq!(buffer.pop(1), &[0]); + assert_eq!(buffer.pop(1), &[1]); + assert_eq!(buffer.pop(1), &[2]); + + core::mem::drop(buffer); + + assert_eq!(consumed, 3); + } + + #[test] + fn pop_returns_fewer_bytes_if_requested_more_than_what_it_has() { + let mut consumed = 0; + + let mut buffer = ReadBuffer::new(&[0, 1, 2, 3], &mut consumed); + + assert_eq!(buffer.pop(1), &[0]); + assert_eq!(buffer.pop(1), &[1]); + assert_eq!(buffer.pop(4), &[2, 3]); + assert_eq!(buffer.pop(1), &[]); + + core::mem::drop(buffer); + + assert_eq!(consumed, 4); + } + + #[test] + fn peek_does_not_consume() { + let mut consumed = 0; + + let mut buffer = ReadBuffer::new(&[0, 1, 2, 3], &mut consumed); + + assert_eq!(buffer.peek(1), &[0]); + assert_eq!(buffer.peek(1), &[0]); + + core::mem::drop(buffer); + + assert_eq!(consumed, 0); + } + + #[test] + fn revert_undoes_pop() { + let mut consumed = 0; + + let mut buffer = ReadBuffer::new(&[0, 1, 2, 3], &mut consumed); + + assert_eq!(buffer.pop(4), &[0, 1, 2, 3]); + + buffer.revert(); + + assert_eq!(consumed, 0); + } +} diff --git a/src/record.rs b/src/record.rs new file mode 100644 index 0000000..e4f23dc --- /dev/null +++ b/src/record.rs @@ -0,0 +1,224 @@ +use crate::ProtocolError; +use crate::application_data::ApplicationData; +use crate::change_cipher_spec::ChangeCipherSpec; +use crate::config::{TlsCipherSuite, ConnectConfig}; +use crate::content_types::ContentType; +use crate::handshake::client_hello::ClientHello; +use crate::handshake::{ClientHandshake, ServerHandshake}; +use crate::key_schedule::WriteKeySchedule; +use crate::{CryptoBackend, buffer::CryptoBuffer}; +use crate::{ + alert::{Alert, AlertDescription, AlertLevel}, + parse_buffer::ParseBuffer, +}; +use core::fmt::Debug; + +pub type Encrypted = bool; + +#[allow(clippy::large_enum_variant)] +pub enum ClientRecord<'config, 'a, CipherSuite> +where + // N: ArrayLength, + CipherSuite: TlsCipherSuite, +{ + Handshake(ClientHandshake<'config, 'a, CipherSuite>, Encrypted), + Alert(Alert, Encrypted), +} + +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ClientRecordHeader { + Handshake(Encrypted), + Alert(Encrypted), + ApplicationData, +} + +impl ClientRecordHeader { + pub fn is_encrypted(self) -> bool { + match self { + ClientRecordHeader::Handshake(encrypted) | ClientRecordHeader::Alert(encrypted) => { + encrypted + } + ClientRecordHeader::ApplicationData => true, + } + } + + pub fn header_content_type(self) -> ContentType { + match self { + Self::Handshake(false) => ContentType::Handshake, + Self::Alert(false) => ContentType::ChangeCipherSpec, + Self::Handshake(true) | Self::Alert(true) | Self::ApplicationData => { + ContentType::ApplicationData + } + } + } + + pub fn trailer_content_type(self) -> ContentType { + match self { + Self::Handshake(_) => ContentType::Handshake, + Self::Alert(_) => ContentType::Alert, + Self::ApplicationData => ContentType::ApplicationData, + } + } + + pub fn version(self) -> [u8; 2] { + match self { + Self::Handshake(true) | Self::Alert(true) | Self::ApplicationData => [0x03, 0x03], + Self::Handshake(false) | Self::Alert(false) => [0x03, 0x01], + } + } + + pub fn encode(self, buf: &mut CryptoBuffer) -> Result<(), ProtocolError> { + buf.push(self.header_content_type() as u8) + .map_err(|_| ProtocolError::EncodeError)?; + buf.extend_from_slice(&self.version()) + .map_err(|_| ProtocolError::EncodeError)?; + + Ok(()) + } +} + +impl<'config, CipherSuite> ClientRecord<'config, '_, CipherSuite> +where + //N: ArrayLength, + CipherSuite: TlsCipherSuite, +{ + pub fn header(&self) -> ClientRecordHeader { + match self { + ClientRecord::Handshake(_, encrypted) => ClientRecordHeader::Handshake(*encrypted), + ClientRecord::Alert(_, encrypted) => ClientRecordHeader::Alert(*encrypted), + } + } + + pub fn client_hello( + config: &'config ConnectConfig<'config>, + provider: &mut CP, + ) -> Self + where + CP: CryptoBackend, + { + ClientRecord::Handshake( + ClientHandshake::ClientHello(ClientHello::new(config, provider)), + false, + ) + } + + pub fn close_notify(opened: bool) -> Self { + ClientRecord::Alert( + Alert::new(AlertLevel::Warning, AlertDescription::CloseNotify), + opened, + ) + } + + pub(crate) fn encode_payload(&self, buf: &mut CryptoBuffer) -> Result { + let record_length_marker = buf.len(); + + match self { + ClientRecord::Handshake(handshake, _) => handshake.encode(buf)?, + ClientRecord::Alert(alert, _) => alert.encode(buf)?, + }; + + Ok(buf.len() - record_length_marker) + } + + pub fn finish_record( + &self, + buf: &mut CryptoBuffer, + transcript: &mut CipherSuite::Hash, + write_key_schedule: &mut WriteKeySchedule, + ) -> Result<(), ProtocolError> { + match self { + ClientRecord::Handshake(handshake, false) => { + handshake.finalize(buf, transcript, write_key_schedule) + } + ClientRecord::Handshake(_, true) => { + ClientHandshake::::finalize_encrypted(buf, transcript); + Ok(()) + } + _ => Ok(()), + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[allow(clippy::large_enum_variant)] +pub enum ServerRecord<'a, CipherSuite: TlsCipherSuite> { + Handshake(ServerHandshake<'a, CipherSuite>), + ChangeCipherSpec(ChangeCipherSpec), + Alert(Alert), + ApplicationData(ApplicationData<'a>), +} + +pub struct RecordHeader { + header: [u8; 5], +} + +impl RecordHeader { + pub const LEN: usize = 5; + + pub fn content_type(&self) -> ContentType { + // Content type already validated in read + unwrap!(ContentType::of(self.header[0])) + } + + pub fn content_length(&self) -> usize { + // Content length already validated in read + u16::from_be_bytes([self.header[3], self.header[4]]) as usize + } + + pub fn data(&self) -> &[u8; 5] { + &self.header + } + + pub fn decode(header: [u8; 5]) -> Result { + match ContentType::of(header[0]) { + None => Err(ProtocolError::InvalidRecord), + Some(_) => Ok(RecordHeader { header }), + } + } +} + +impl<'a, CipherSuite: TlsCipherSuite> ServerRecord<'a, CipherSuite> { + pub fn content_type(&self) -> ContentType { + match self { + ServerRecord::Handshake(_) => ContentType::Handshake, + ServerRecord::ChangeCipherSpec(_) => ContentType::ChangeCipherSpec, + ServerRecord::Alert(_) => ContentType::Alert, + ServerRecord::ApplicationData(_) => ContentType::ApplicationData, + } + } + + pub fn decode( + header: RecordHeader, + data: &'a mut [u8], + digest: &mut CipherSuite::Hash, + ) -> Result, ProtocolError> { + assert_eq!(header.content_length(), data.len()); + match header.content_type() { + ContentType::Invalid => Err(ProtocolError::Unimplemented), + ContentType::ChangeCipherSpec => Ok(ServerRecord::ChangeCipherSpec( + ChangeCipherSpec::read(data)?, + )), + ContentType::Alert => { + let mut parse = ParseBuffer::new(data); + let alert = Alert::parse(&mut parse)?; + Ok(ServerRecord::Alert(alert)) + } + ContentType::Handshake => { + let mut parse = ParseBuffer::new(data); + Ok(ServerRecord::Handshake(ServerHandshake::read( + &mut parse, digest, + )?)) + } + ContentType::ApplicationData => { + let buf = CryptoBuffer::wrap_with_pos(data, data.len()); + Ok(ServerRecord::ApplicationData(ApplicationData::new( + buf, header, + ))) + } + } + } + + //pub fn parse(buf: &[u8]) -> Result {} +} diff --git a/src/record_reader.rs b/src/record_reader.rs new file mode 100644 index 0000000..e19e673 --- /dev/null +++ b/src/record_reader.rs @@ -0,0 +1,479 @@ +use crate::key_schedule::ReadKeySchedule; +use embedded_io::{Error as _, Read as BlockingRead}; +use embedded_io_async::Read as AsyncRead; + +use crate::{ + ProtocolError, + config::TlsCipherSuite, + record::{RecordHeader, ServerRecord}, +}; + +pub struct RecordReader<'a> { + pub(crate) buf: &'a mut [u8], + /// The number of decoded bytes in the buffer + decoded: usize, + /// The number of read but not yet decoded bytes in the buffer + pending: usize, +} + +pub struct RecordReaderBorrowMut<'a> { + pub(crate) buf: &'a mut [u8], + /// The number of decoded bytes in the buffer + decoded: &'a mut usize, + /// The number of read but not yet decoded bytes in the buffer + pending: &'a mut usize, +} + +impl<'a> RecordReader<'a> { + pub fn new(buf: &'a mut [u8]) -> Self { + if buf.len() < 16640 { + warn!("Read buffer is smaller than 16640 bytes, which may cause problems!"); + } + Self { + buf, + decoded: 0, + pending: 0, + } + } + + pub fn reborrow_mut(&mut self) -> RecordReaderBorrowMut<'_> { + RecordReaderBorrowMut { + buf: self.buf, + decoded: &mut self.decoded, + pending: &mut self.pending, + } + } + + pub async fn read<'m, CipherSuite: TlsCipherSuite>( + &'m mut self, + transport: &mut impl AsyncRead, + key_schedule: &mut ReadKeySchedule, + ) -> Result, ProtocolError> { + read( + self.buf, + &mut self.decoded, + &mut self.pending, + transport, + key_schedule, + ) + .await + } + + pub fn read_blocking<'m, CipherSuite: TlsCipherSuite>( + &'m mut self, + transport: &mut impl BlockingRead, + key_schedule: &mut ReadKeySchedule, + ) -> Result, ProtocolError> { + read_blocking( + self.buf, + &mut self.decoded, + &mut self.pending, + transport, + key_schedule, + ) + } +} + +impl RecordReaderBorrowMut<'_> { + pub async fn read<'m, CipherSuite: TlsCipherSuite>( + &'m mut self, + transport: &mut impl AsyncRead, + key_schedule: &mut ReadKeySchedule, + ) -> Result, ProtocolError> { + read( + self.buf, + self.decoded, + self.pending, + transport, + key_schedule, + ) + .await + } + + pub fn read_blocking<'m, CipherSuite: TlsCipherSuite>( + &'m mut self, + transport: &mut impl BlockingRead, + key_schedule: &mut ReadKeySchedule, + ) -> Result, ProtocolError> { + read_blocking( + self.buf, + self.decoded, + self.pending, + transport, + key_schedule, + ) + } +} + +pub async fn read<'m, CipherSuite: TlsCipherSuite>( + buf: &'m mut [u8], + decoded: &mut usize, + pending: &mut usize, + transport: &mut impl AsyncRead, + key_schedule: &mut ReadKeySchedule, +) -> Result, ProtocolError> { + let header: RecordHeader = next_record_header(transport).await?; + + advance(buf, decoded, pending, transport, header.content_length()).await?; + consume( + buf, + decoded, + pending, + header, + key_schedule.transcript_hash(), + ) +} + +pub fn read_blocking<'m, CipherSuite: TlsCipherSuite>( + buf: &'m mut [u8], + decoded: &mut usize, + pending: &mut usize, + transport: &mut impl BlockingRead, + key_schedule: &mut ReadKeySchedule, +) -> Result, ProtocolError> { + let header: RecordHeader = next_record_header_blocking(transport)?; + + advance_blocking(buf, decoded, pending, transport, header.content_length())?; + consume( + buf, + decoded, + pending, + header, + key_schedule.transcript_hash(), + ) +} + +async fn next_record_header(transport: &mut impl AsyncRead) -> Result { + let mut buf: [u8; RecordHeader::LEN] = [0; RecordHeader::LEN]; + let mut total_read: usize = 0; + while total_read != RecordHeader::LEN { + let read: usize = transport + .read(&mut buf[total_read..]) + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + if read == 0 { + return Err(ProtocolError::IoError); + } + total_read += read; + } + RecordHeader::decode(buf) +} + +fn next_record_header_blocking( + transport: &mut impl BlockingRead, +) -> Result { + let mut buf: [u8; RecordHeader::LEN] = [0; RecordHeader::LEN]; + let mut total_read: usize = 0; + while total_read != RecordHeader::LEN { + let read: usize = transport + .read(&mut buf[total_read..]) + .map_err(|e| ProtocolError::Io(e.kind()))?; + if read == 0 { + return Err(ProtocolError::IoError); + } + total_read += read; + } + RecordHeader::decode(buf) +} + +async fn advance( + buf: &mut [u8], + decoded: &mut usize, + pending: &mut usize, + transport: &mut impl AsyncRead, + amount: usize, +) -> Result<(), ProtocolError> { + ensure_contiguous(buf, decoded, pending, amount)?; + + let mut remain: usize = amount; + while *pending < amount { + let read = transport + .read(&mut buf[*decoded + *pending..][..remain]) + .await + .map_err(|e| ProtocolError::Io(e.kind()))?; + if read == 0 { + return Err(ProtocolError::IoError); + } + remain -= read; + *pending += read; + } + + Ok(()) +} + +fn advance_blocking( + buf: &mut [u8], + decoded: &mut usize, + pending: &mut usize, + transport: &mut impl BlockingRead, + amount: usize, +) -> Result<(), ProtocolError> { + ensure_contiguous(buf, decoded, pending, amount)?; + + let mut remain: usize = amount; + while *pending < amount { + let read = transport + .read(&mut buf[*decoded + *pending..][..remain]) + .map_err(|e| ProtocolError::Io(e.kind()))?; + if read == 0 { + return Err(ProtocolError::IoError); + } + remain -= read; + *pending += read; + } + + Ok(()) +} + +fn consume<'m, CipherSuite: TlsCipherSuite>( + buf: &'m mut [u8], + decoded: &mut usize, + pending: &mut usize, + header: RecordHeader, + digest: &mut CipherSuite::Hash, +) -> Result, ProtocolError> { + let content_len = header.content_length(); + + let slice = &mut buf[*decoded..][..content_len]; + + *decoded += content_len; + *pending -= content_len; + + ServerRecord::decode(header, slice, digest) +} + +fn ensure_contiguous( + buf: &mut [u8], + decoded: &mut usize, + pending: &mut usize, + len: usize, +) -> Result<(), ProtocolError> { + if *decoded + len > buf.len() { + if len > buf.len() { + error!( + "Record too large for buffer. Size: {} Buffer size: {}", + len, + buf.len() + ); + return Err(ProtocolError::InsufficientSpace); + } + buf.copy_within(*decoded..*decoded + *pending, 0); + *decoded = 0; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use core::convert::Infallible; + + use super::*; + use crate::{Aes128GcmSha256, content_types::ContentType, key_schedule::KeySchedule}; + + struct ChunkRead<'a>(&'a [u8], usize); + + impl embedded_io::ErrorType for ChunkRead<'_> { + type Error = Infallible; + } + + impl BlockingRead for ChunkRead<'_> { + fn read(&mut self, buf: &mut [u8]) -> Result { + let len = usize::min(self.1, buf.len()); + let len = usize::min(len, self.0.len()); + buf[..len].copy_from_slice(&self.0[..len]); + self.0 = &self.0[len..]; + Ok(len) + } + } + + #[test] + fn can_read_blocking() { + can_read_blocking_case(1); + can_read_blocking_case(2); + can_read_blocking_case(3); + can_read_blocking_case(4); + can_read_blocking_case(5); + can_read_blocking_case(6); + can_read_blocking_case(7); + can_read_blocking_case(8); + can_read_blocking_case(9); + can_read_blocking_case(10); + can_read_blocking_case(11); + can_read_blocking_case(12); + can_read_blocking_case(13); + can_read_blocking_case(14); + can_read_blocking_case(15); + can_read_blocking_case(16); + } + + fn can_read_blocking_case(chunk_size: usize) { + let mut transport = ChunkRead( + &[ + // Header + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x04, + // Data + 0xde, + 0xad, + 0xbe, + 0xef, + // Header + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x02, + // Data + 0xaa, + 0xbb, + ], + chunk_size, + ); + + let mut buf = [0; 32]; + let mut reader = RecordReader::new(&mut buf); + let mut key_schedule = KeySchedule::::new(); + + { + if let ServerRecord::ApplicationData(data) = reader + .read_blocking(&mut transport, key_schedule.read_state()) + .unwrap() + { + assert_eq!([0xde, 0xad, 0xbe, 0xef], data.data.as_slice()); + } else { + panic!("Wrong server record"); + } + + assert_eq!(4, reader.decoded); + assert_eq!(0, reader.pending); + } + + { + if let ServerRecord::ApplicationData(data) = reader + .read_blocking(&mut transport, key_schedule.read_state()) + .unwrap() + { + assert_eq!([0xaa, 0xbb], data.data.as_slice()); + } else { + panic!("Wrong server record"); + } + + assert_eq!(6, reader.decoded); + assert_eq!(0, reader.pending); + } + } + + #[test] + fn can_read_blocking_must_rotate_buffer() { + let mut transport = [ + // Header + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x04, + // Data + 0xde, + 0xad, + 0xbe, + 0xef, + // Header + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x02, + // Data + 0xaa, + 0xbb, + ] + .as_slice(); + + let mut buf = [0; 4]; // cannot contain both data portions + let mut reader = RecordReader::new(&mut buf); + let mut key_schedule = KeySchedule::::new(); + + { + if let ServerRecord::ApplicationData(data) = reader + .read_blocking(&mut transport, key_schedule.read_state()) + .unwrap() + { + assert_eq!([0xde, 0xad, 0xbe, 0xef], data.data.as_slice()); + } else { + panic!("Wrong server record"); + } + + assert_eq!(4, reader.decoded); + assert_eq!(0, reader.pending); + } + + { + if let ServerRecord::ApplicationData(data) = reader + .read_blocking(&mut transport, key_schedule.read_state()) + .unwrap() + { + assert_eq!([0xaa, 0xbb], data.data.as_slice()); + } else { + panic!("Wrong server record"); + } + + assert_eq!(2, reader.decoded); + assert_eq!(0, reader.pending); + } + } + + #[test] + fn can_read_empty_record() { + let mut transport = [ + // Header + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x00, + // Header + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x00, + ] + .as_slice(); + + let mut buf = [0; 32]; + let mut reader = RecordReader::new(&mut buf); + let mut key_schedule = KeySchedule::::new(); + + { + if let ServerRecord::ApplicationData(data) = reader + .read_blocking(&mut transport, key_schedule.read_state()) + .unwrap() + { + assert!(data.data.is_empty()); + } else { + panic!("Wrong server record"); + } + + assert_eq!(0, reader.decoded); + assert_eq!(0, reader.pending); + } + + { + if let ServerRecord::ApplicationData(data) = reader + .read_blocking(&mut transport, key_schedule.read_state()) + .unwrap() + { + assert!(data.data.is_empty()); + } else { + panic!("Wrong server record"); + } + + assert_eq!(0, reader.decoded); + assert_eq!(0, reader.pending); + } + } +} diff --git a/src/send_policy.rs b/src/send_policy.rs new file mode 100644 index 0000000..68e4ba8 --- /dev/null +++ b/src/send_policy.rs @@ -0,0 +1,37 @@ +//! Flush policy for TLS sockets. +//! +//! Two strategies are provided: +//! - `Relaxed`: close the TLS encryption buffer and hand the data to the transport +//! delegate without forcing a transport-level flush. +//! - `Strict`: in addition to handing the data to the transport delegate, also +//! request a flush of the transport. For TCP transports this typically means +//! waiting for an ACK (e.g. on embassy TCP sockets) before considering the +//! data fully flushed. + +/// Policy controlling how TLS layer flushes encrypted data to the transport. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlushPolicy { + /// Close the TLS encryption buffer and pass bytes to the transport delegate. + /// Do not force a transport-level flush or wait for an ACK. + Relaxed, + + /// In addition to passing bytes to the transport delegate, request a + /// transport-level flush and wait for confirmation (ACK) before returning. + Strict, +} + +impl FlushPolicy { + /// Returns true when the transport delegate should be explicitly flushed. + /// + /// Relaxed -> false, Strict -> true. + pub fn flush_transport(&self) -> bool { + matches!(self, Self::Strict) + } +} + +impl Default for FlushPolicy { + /// Default to `Strict` for compatibility with mote-tls 0.17.0. + fn default() -> Self { + FlushPolicy::Strict + } +} diff --git a/src/write_buffer.rs b/src/write_buffer.rs new file mode 100644 index 0000000..929c23c --- /dev/null +++ b/src/write_buffer.rs @@ -0,0 +1,287 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + config::{TLS_RECORD_OVERHEAD, TlsCipherSuite}, + connection::encrypt, + key_schedule::{ReadKeySchedule, WriteKeySchedule}, + record::{ClientRecord, ClientRecordHeader}, +}; + +pub struct WriteBuffer<'a> { + buffer: &'a mut [u8], + pos: usize, + current_header: Option, +} + +pub(crate) struct WriteBufferBorrow<'a> { + buffer: &'a [u8], + pos: &'a usize, + current_header: &'a Option, +} + +pub(crate) struct WriteBufferBorrowMut<'a> { + buffer: &'a mut [u8], + pos: &'a mut usize, + current_header: &'a mut Option, +} + +impl<'a> WriteBuffer<'a> { + pub fn new(buffer: &'a mut [u8]) -> Self { + debug_assert!( + buffer.len() > TLS_RECORD_OVERHEAD, + "The write buffer must be sufficiently large to include the tls record overhead" + ); + Self { + buffer, + pos: 0, + current_header: None, + } + } + + pub(crate) fn reborrow_mut(&mut self) -> WriteBufferBorrowMut<'_> { + WriteBufferBorrowMut { + buffer: self.buffer, + pos: &mut self.pos, + current_header: &mut self.current_header, + } + } + + pub(crate) fn reborrow(&self) -> WriteBufferBorrow<'_> { + WriteBufferBorrow { + buffer: self.buffer, + pos: &self.pos, + current_header: &self.current_header, + } + } + + pub fn is_full(&self) -> bool { + self.reborrow().is_full() + } + + pub fn append(&mut self, buf: &[u8]) -> usize { + self.reborrow_mut().append(buf) + } + + pub fn is_empty(&self) -> bool { + self.reborrow().is_empty() + } + + pub fn contains(&self, header: ClientRecordHeader) -> bool { + self.reborrow().contains(header) + } + + pub(crate) fn start_record(&mut self, header: ClientRecordHeader) -> Result<(), ProtocolError> { + self.reborrow_mut().start_record(header) + } + + pub(crate) fn close_record( + &mut self, + write_key_schedule: &mut WriteKeySchedule, + ) -> Result<&[u8], ProtocolError> + where + CipherSuite: TlsCipherSuite, + { + close_record( + self.buffer, + &mut self.pos, + &mut self.current_header, + write_key_schedule, + ) + } + + pub fn write_record( + &mut self, + record: &ClientRecord, + write_key_schedule: &mut WriteKeySchedule, + read_key_schedule: Option<&mut ReadKeySchedule>, + ) -> Result<&[u8], ProtocolError> + where + CipherSuite: TlsCipherSuite, + { + write_record( + self.buffer, + &mut self.pos, + &mut self.current_header, + record, + write_key_schedule, + read_key_schedule, + ) + } +} + +impl WriteBufferBorrow<'_> { + fn max_block_size(&self) -> usize { + self.buffer.len() - TLS_RECORD_OVERHEAD + } + + pub fn is_full(&self) -> bool { + *self.pos == self.max_block_size() + } + + pub fn len(&self) -> usize { + *self.pos + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn space(&self) -> usize { + self.max_block_size() - *self.pos + } + + pub fn contains(&self, header: ClientRecordHeader) -> bool { + self.current_header.as_ref() == Some(&header) + } +} + +impl WriteBufferBorrowMut<'_> { + fn reborrow(&self) -> WriteBufferBorrow<'_> { + WriteBufferBorrow { + buffer: self.buffer, + pos: self.pos, + current_header: self.current_header, + } + } + + pub fn is_full(&self) -> bool { + self.reborrow().is_full() + } + + pub fn is_empty(&self) -> bool { + self.reborrow().is_empty() + } + + pub fn contains(&self, header: ClientRecordHeader) -> bool { + self.reborrow().contains(header) + } + + pub fn append(&mut self, buf: &[u8]) -> usize { + let buffered = usize::min(buf.len(), self.reborrow().space()); + if buffered > 0 { + self.buffer[*self.pos..*self.pos + buffered].copy_from_slice(&buf[..buffered]); + *self.pos += buffered; + } + buffered + } + + pub(crate) fn start_record(&mut self, header: ClientRecordHeader) -> Result<(), ProtocolError> { + start_record(self.buffer, self.pos, self.current_header, header) + } + + pub fn close_record( + &mut self, + write_key_schedule: &mut WriteKeySchedule, + ) -> Result<&[u8], ProtocolError> + where + CipherSuite: TlsCipherSuite, + { + close_record( + self.buffer, + self.pos, + self.current_header, + write_key_schedule, + ) + } +} + +fn start_record( + buffer: &mut [u8], + pos: &mut usize, + current_header: &mut Option, + header: ClientRecordHeader, +) -> Result<(), ProtocolError> { + debug_assert!(current_header.is_none()); + + debug!("start_record({:?})", header); + *current_header = Some(header); + + with_buffer(buffer, pos, |mut buf| { + header.encode(&mut buf)?; + buf.push_u16(0)?; + Ok(buf.rewind()) + }) +} + +fn with_buffer( + buffer: &mut [u8], + pos: &mut usize, + op: impl FnOnce(CryptoBuffer) -> Result, +) -> Result<(), ProtocolError> { + let buf = CryptoBuffer::wrap_with_pos(buffer, *pos); + + match op(buf) { + Ok(buf) => { + *pos = buf.len(); + Ok(()) + } + Err(err) => Err(err), + } +} + +fn close_record<'a, CipherSuite>( + buffer: &'a mut [u8], + pos: &mut usize, + current_header: &mut Option, + write_key_schedule: &mut WriteKeySchedule, +) -> Result<&'a [u8], ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + const HEADER_SIZE: usize = 5; + + let header = current_header.take().unwrap(); + with_buffer(buffer, pos, |mut buf| { + if !header.is_encrypted() { + return Ok(buf); + } + + buf.push(header.trailer_content_type() as u8) + .map_err(|_| ProtocolError::EncodeError)?; + + let mut buf = buf.offset(HEADER_SIZE); + encrypt(write_key_schedule, &mut buf)?; + Ok(buf.rewind()) + })?; + let [upper, lower] = ((*pos - HEADER_SIZE) as u16).to_be_bytes(); + + buffer[3] = upper; + buffer[4] = lower; + + let slice = &buffer[..*pos]; + + *pos = 0; + *current_header = None; + + Ok(slice) +} + +fn write_record<'a, CipherSuite>( + buffer: &'a mut [u8], + pos: &mut usize, + current_header: &mut Option, + record: &ClientRecord, + write_key_schedule: &mut WriteKeySchedule, + read_key_schedule: Option<&mut ReadKeySchedule>, +) -> Result<&'a [u8], ProtocolError> +where + CipherSuite: TlsCipherSuite, +{ + if current_header.is_some() { + return Err(ProtocolError::InternalError); + } + + start_record(buffer, pos, current_header, record.header())?; + with_buffer(buffer, pos, |buf| { + let mut buf = buf.forward(); + record.encode_payload(&mut buf)?; + + let transcript = read_key_schedule + .ok_or(ProtocolError::InternalError)? + .transcript_hash(); + + record.finish_record(&mut buf, transcript, write_key_schedule)?; + Ok(buf.rewind()) + })?; + close_record(buffer, pos, current_header, write_key_schedule) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..7cd3328 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,369 @@ +use std::{path::PathBuf, sync::Arc}; + +use mio::net::{TcpListener, TcpStream}; + +use std::collections::HashMap; +use std::fs; +use std::io; +use std::io::{BufReader, Read, Write}; +use std::net; + +// Token for our listening socket. +pub const LISTENER: mio::Token = mio::Token(0); + +// Which mode the server operates in. +#[derive(Clone)] +pub enum ServerMode { + /// Write back received bytes + Echo, +} + +/// This binds together a TCP listening socket, some outstanding +/// connections, and a TLS server configuration. +pub struct EchoServer { + server: TcpListener, + connections: HashMap, + next_id: usize, + tls_config: Arc, + mode: ServerMode, +} + +impl EchoServer { + pub fn new(server: TcpListener, mode: ServerMode, cfg: Arc) -> EchoServer { + EchoServer { + server, + connections: HashMap::new(), + next_id: 2, + tls_config: cfg, + mode, + } + } + + pub fn accept(&mut self, registry: &mio::Registry) -> Result<(), io::Error> { + loop { + match self.server.accept() { + Ok((socket, addr)) => { + log::debug!("Accepting new connection from {:?}", addr); + + let tls_session = + rustls::ServerConnection::new(self.tls_config.clone()).unwrap(); + let mode = self.mode.clone(); + + let token = mio::Token(self.next_id); + self.next_id += 1; + + let mut connection = Connection::new(socket, token, mode, tls_session); + connection.register(registry); + self.connections.insert(token, connection); + } + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => return Ok(()), + Err(err) => { + println!( + "encountered error while accepting connection; err={:?}", + err + ); + return Err(err); + } + } + } + } + + pub fn conn_event(&mut self, registry: &mio::Registry, event: &mio::event::Event) { + let token = event.token(); + + if self.connections.contains_key(&token) { + self.connections + .get_mut(&token) + .unwrap() + .ready(registry, event); + + if self.connections[&token].is_closed() { + self.connections.remove(&token); + } + } + } +} + +/// This is a connection which has been accepted by the server, +/// and is currently being served. +/// +/// It has a TCP-level stream, a TLS-level session, and some +/// other state/metadata. +struct Connection { + socket: TcpStream, + token: mio::Token, + closing: bool, + closed: bool, + mode: ServerMode, + tls_session: rustls::ServerConnection, + back: Option, +} + +/// Open a plaintext TCP-level connection for forwarded connections. +fn open_back(_mode: &ServerMode) -> Option { + None +} + +/// This used to be conveniently exposed by mio: map EWOULDBLOCK +/// errors to something less-errory. +fn try_read(r: io::Result) -> io::Result> { + match r { + Ok(len) => Ok(Some(len)), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None), + Err(e) => Err(e), + } +} + +impl Connection { + fn new( + socket: TcpStream, + token: mio::Token, + mode: ServerMode, + tls_session: rustls::ServerConnection, + ) -> Connection { + let back = open_back(&mode); + Connection { + socket, + token, + closing: false, + closed: false, + mode, + tls_session, + back, + } + } + + /// We're a connection, and we have something to do. + fn ready(&mut self, registry: &mio::Registry, ev: &mio::event::Event) { + if ev.is_readable() { + self.do_tls_read(); + self.try_plain_read(); + self.try_back_read(); + } + + if ev.is_writable() { + self.do_tls_write_and_handle_error(); + } + + if self.closing { + let _ = self.socket.shutdown(net::Shutdown::Both); + self.close_back(); + self.closed = true; + self.deregister(registry); + } else { + self.reregister(registry); + } + } + + fn close_back(&mut self) { + if self.back.is_some() { + let back = self.back.as_mut().unwrap(); + back.shutdown(net::Shutdown::Both).unwrap(); + } + self.back = None; + } + + fn do_tls_read(&mut self) { + let rc = self.tls_session.read_tls(&mut self.socket); + if rc.is_err() { + let err = rc.unwrap_err(); + if let io::ErrorKind::WouldBlock = err.kind() { + return; + } + log::warn!("read error {:?}", err); + self.closing = true; + return; + } + if rc.unwrap() == 0 { + log::debug!("eof"); + self.closing = true; + return; + } + let processed = self.tls_session.process_new_packets(); + if processed.is_err() { + log::warn!("cannot process packet: {:?}", processed); + self.do_tls_write_and_handle_error(); + self.closing = true; + } + } + + fn try_plain_read(&mut self) { + let mut buf = Vec::new(); + let rc = self.tls_session.reader().read_to_end(&mut buf); + if let Err(ref e) = rc { + if e.kind() != io::ErrorKind::WouldBlock { + log::warn!("plaintext read failed: {:?}", rc); + self.closing = true; + return; + } + } + if !buf.is_empty() { + log::debug!("plaintext read {:?}", buf.len()); + self.incoming_plaintext(&buf); + } + } + + fn try_back_read(&mut self) { + if self.back.is_none() { + return; + } + let mut buf = [0u8; 1024]; + let back = self.back.as_mut().unwrap(); + let rc = try_read(back.read(&mut buf)); + if rc.is_err() { + log::warn!("backend read failed: {:?}", rc); + self.closing = true; + return; + } + let maybe_len = rc.unwrap(); + match maybe_len { + Some(0) => { + log::debug!("back eof"); + self.closing = true; + } + Some(len) => { + self.tls_session.writer().write_all(&buf[..len]).unwrap(); + } + None => {} + }; + } + + fn incoming_plaintext(&mut self, buf: &[u8]) { + match self.mode { + ServerMode::Echo => { + self.tls_session.writer().write_all(buf).unwrap(); + } + } + } + + fn tls_write(&mut self) -> io::Result { + self.tls_session.write_tls(&mut self.socket) + } + + fn do_tls_write_and_handle_error(&mut self) { + let rc = self.tls_write(); + if rc.is_err() { + log::warn!("write failed {:?}", rc); + self.closing = true; + } + } + + fn register(&mut self, registry: &mio::Registry) { + let event_set = self.event_set(); + registry + .register(&mut self.socket, self.token, event_set) + .unwrap(); + if self.back.is_some() { + registry + .register( + self.back.as_mut().unwrap(), + self.token, + mio::Interest::READABLE, + ) + .unwrap(); + } + } + + fn reregister(&mut self, registry: &mio::Registry) { + let event_set = self.event_set(); + registry + .reregister(&mut self.socket, self.token, event_set) + .unwrap(); + } + + fn deregister(&mut self, registry: &mio::Registry) { + registry.deregister(&mut self.socket).unwrap(); + if self.back.is_some() { + registry.deregister(self.back.as_mut().unwrap()).unwrap(); + } + } + + fn event_set(&self) -> mio::Interest { + let rd = self.tls_session.wants_read(); + let wr = self.tls_session.wants_write(); + if rd && wr { + mio::Interest::READABLE | mio::Interest::WRITABLE + } else if wr { + mio::Interest::WRITABLE + } else { + mio::Interest::READABLE + } + } + + fn is_closed(&self) -> bool { + self.closed + } +} + +pub fn load_certs(filename: &PathBuf) -> Vec { + let certfile = fs::File::open(filename).expect("cannot open certificate file"); + let mut reader = BufReader::new(certfile); + rustls_pemfile::certs(&mut reader) + .unwrap() + .iter() + .map(|v| rustls::Certificate(v.clone())) + .collect() +} + +pub fn load_private_key(filename: &PathBuf) -> rustls::PrivateKey { + let keyfile = fs::File::open(filename).expect("cannot open private key file"); + let mut reader = BufReader::new(keyfile); + loop { + match rustls_pemfile::read_one(&mut reader).expect("cannot parse private key .pem file") { + Some(rustls_pemfile::Item::RSAKey(key)) => return rustls::PrivateKey(key), + Some(rustls_pemfile::Item::PKCS8Key(key)) => return rustls::PrivateKey(key), + Some(rustls_pemfile::Item::ECKey(key)) => return rustls::PrivateKey(key), + None => break, + _ => {} + } + } + panic!( + "no keys found in {:?} (encrypted keys not supported)", + filename + ); +} + +#[allow(dead_code)] +pub fn run(listener: TcpListener) { + let versions = &[&rustls::version::TLS13]; + let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + let certs = load_certs(&test_dir.join("fixtures").join("leaf-server.pem")); + let privkey = load_private_key(&test_dir.join("fixtures").join("leaf-server-key.pem")); + let config = rustls::ServerConfig::builder() + .with_cipher_suites(rustls::ALL_CIPHER_SUITES) + .with_kx_groups(&rustls::ALL_KX_GROUPS) + .with_protocol_versions(versions) + .unwrap() + .with_no_client_auth() + .with_single_cert(certs, privkey) + .unwrap(); + run_with_config(listener, config) +} + +pub fn run_with_config(mut listener: TcpListener, config: rustls::ServerConfig) { + let mut poll = mio::Poll::new().unwrap(); + poll.registry() + .register(&mut listener, LISTENER, mio::Interest::READABLE) + .unwrap(); + let mut tlsserv = EchoServer::new(listener, ServerMode::Echo, Arc::new(config)); + let mut events = mio::Events::with_capacity(256); + loop { + if let Err(e) = poll.poll(&mut events, None) { + if e.kind() == std::io::ErrorKind::Interrupted { + log::debug!("I/O error {:?}", e); + continue; + } + panic!("I/O error {:?}", e); + } + for event in events.iter() { + match event.token() { + LISTENER => { + tlsserv + .accept(poll.registry()) + .expect("error accepting socket"); + } + _ => tlsserv.conn_event(poll.registry(), event), + } + } + } +} diff --git a/tests/fixtures/chain.pem b/tests/fixtures/chain.pem new file mode 100644 index 0000000..ef6e1e9 --- /dev/null +++ b/tests/fixtures/chain.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIBXzCCAQUCFHzubCyE5kiBqAHQGfctkQN2q3TFMAoGCCqGSM49BAMCME4xCzAJ +BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UE +CgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzM4WhgPMjA1 +MzA3MDkwNzMzMzhaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABDz1/dQDHQeTjpDudlzUhWzO9RoRyMD7WHVFfhKmnTPwfNpo +4pBI6zWzPaX+1yIESrFKMB/3z6Kg5XvUYzghiBQwCgYIKoZIzj0EAwIDSAAwRQIh +AOfUKudqSEH+qWvddhwNCzJxVSjTVYM6UUb1y+6gYLmVAiAhEIdRb9+4EkFMyE69 +j/eLxFsQw9SDJVW1ikFddk3bDA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBmjCCAT8CFGjUADT9CW6hCEQDXIVshzmwrcMgMAoGCCqGSM49BAMCME4xCzAJ +BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UE +CgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzM3WhgPMjA1 +MzA3MDkwNzMzMzdaME4xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsG +A1UEBwwEQ2l0eTEMMAoGA1UECgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAQMWMHHlN9e0LjSJf/DincyIZWJqPixcNbY +GfsF09vGm8spLZqE01yp+ZfaRBwAEDEnXRjGy4x3pvbUeYw6S6pwMAoGCCqGSM49 +BAMCA0kAMEYCIQCGre8m+M4rLIT99ME+LqO7A4YSojffdutsOgRlf4x3SQIhAJjO +FjLActbhIsCFG17eN6XU+0KXj+6riRP0cSjwrKcH +-----END CERTIFICATE----- diff --git a/tests/fixtures/intermediate-ca-key.pem b/tests/fixtures/intermediate-ca-key.pem new file mode 100644 index 0000000..1bfa861 --- /dev/null +++ b/tests/fixtures/intermediate-ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJ866me3sarFhXHOC+YMybl2kXIb0n/HcxkMyaROp7zToAoGCCqGSM49 +AwEHoUQDQgAEDFjBx5TfXtC40iX/w4p3MiGViaj4sXDW2Bn7BdPbxpvLKS2ahNNc +qfmX2kQcABAxJ10YxsuMd6b21HmMOkuqcA== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/intermediate-ca.pem b/tests/fixtures/intermediate-ca.pem new file mode 100644 index 0000000..b501634 --- /dev/null +++ b/tests/fixtures/intermediate-ca.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmjCCAT8CFGjUADT9CW6hCEQDXIVshzmwrcMgMAoGCCqGSM49BAMCME4xCzAJ +BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UE +CgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzM3WhgPMjA1 +MzA3MDkwNzMzMzdaME4xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsG +A1UEBwwEQ2l0eTEMMAoGA1UECgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAQMWMHHlN9e0LjSJf/DincyIZWJqPixcNbY +GfsF09vGm8spLZqE01yp+ZfaRBwAEDEnXRjGy4x3pvbUeYw6S6pwMAoGCCqGSM49 +BAMCA0kAMEYCIQCGre8m+M4rLIT99ME+LqO7A4YSojffdutsOgRlf4x3SQIhAJjO +FjLActbhIsCFG17eN6XU+0KXj+6riRP0cSjwrKcH +-----END CERTIFICATE----- diff --git a/tests/fixtures/intermediate-server-key.pem b/tests/fixtures/intermediate-server-key.pem new file mode 100644 index 0000000..e2604bf --- /dev/null +++ b/tests/fixtures/intermediate-server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDrbR20jHToI8OfzNbHDPwVeL7W7U1QJZL5fFZM2k9SOoAoGCCqGSM49 +AwEHoUQDQgAEPPX91AMdB5OOkO52XNSFbM71GhHIwPtYdUV+EqadM/B82mjikEjr +NbM9pf7XIgRKsUowH/fPoqDle9RjOCGIFA== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/intermediate-server.pem b/tests/fixtures/intermediate-server.pem new file mode 100644 index 0000000..78c3333 --- /dev/null +++ b/tests/fixtures/intermediate-server.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBXzCCAQUCFHzubCyE5kiBqAHQGfctkQN2q3TFMAoGCCqGSM49BAMCME4xCzAJ +BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UE +CgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzM4WhgPMjA1 +MzA3MDkwNzMzMzhaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABDz1/dQDHQeTjpDudlzUhWzO9RoRyMD7WHVFfhKmnTPwfNpo +4pBI6zWzPaX+1yIESrFKMB/3z6Kg5XvUYzghiBQwCgYIKoZIzj0EAwIDSAAwRQIh +AOfUKudqSEH+qWvddhwNCzJxVSjTVYM6UUb1y+6gYLmVAiAhEIdRb9+4EkFMyE69 +j/eLxFsQw9SDJVW1ikFddk3bDA== +-----END CERTIFICATE----- diff --git a/tests/fixtures/leaf-client-key.pem b/tests/fixtures/leaf-client-key.pem new file mode 100644 index 0000000..9532a30 --- /dev/null +++ b/tests/fixtures/leaf-client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOc/3AzIjB+19/geWYKEmKba86WdhAsluZaPqkjSqERroAoGCCqGSM49 +AwEHoUQDQgAEGgqtk8CddQsQKgtS471EXARUxDCEGKputhbgIxYWROyqXnnZ6V6o +as2YGkmj//2MjLRW3R2Po0cuOUzxiVgfqw== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/leaf-client.pem b/tests/fixtures/leaf-client.pem new file mode 100644 index 0000000..e1d122f --- /dev/null +++ b/tests/fixtures/leaf-client.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB6jCCAZCgAwIBAgIUaNQANP0JbqEIRANchWyHObCtwyMwCgYIKoZIzj0EAwIw +TjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MQww +CgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNjAyMjEwNzQ2MjNa +GA8yMDUzMDcwOTA3NDYyM1owSzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRl +MQ0wCwYDVQQHDARDaXR5MQwwCgYDVQQKDANPcmcxDzANBgNVBAMMBmNsaWVudDBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBoKrZPAnXULECoLUuO9RFwEVMQwhBiq +brYW4CMWFkTsql552eleqGrNmBpJo//9jIy0Vt0dj6NHLjlM8YlYH6ujTTBLMAkG +A1UdEwQCMAAwHQYDVR0OBBYEFBqdjBB1UFsM78XwtpXVL7HZzjpYMB8GA1UdIwQY +MBaAFPPmBpkb78hFjPF859+Foy9YAgBTMAoGCCqGSM49BAMCA0gAMEUCIQDNpDHS +mKnhKjYN4FcF1jY6jP849bp1iVRXLohUZiV97AIgKQzXd0i5crkxYZxiuV8+FanV +0AnPOzEyfEJJcVxQKJ0= +-----END CERTIFICATE----- diff --git a/tests/fixtures/leaf-server-key.pem b/tests/fixtures/leaf-server-key.pem new file mode 100644 index 0000000..5eb1723 --- /dev/null +++ b/tests/fixtures/leaf-server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIO5y72YFvsXbun4zTR/R0uyXuVEjj85qCd4rNyyFf8n5oAoGCCqGSM49 +AwEHoUQDQgAET/XXCFooruxTjbFsUbUx61dj8cJP9u9fiZQjBlArTYHOzhmzOSAE +c/KugBPBgjs/tv0XN+gzytvO8ZP66fm/og== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/leaf-server.pem b/tests/fixtures/leaf-server.pem new file mode 100644 index 0000000..9c3ab58 --- /dev/null +++ b/tests/fixtures/leaf-server.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBYDCCAQUCFGjUADT9CW6hCEQDXIVshzmwrcMiMAoGCCqGSM49BAMCME4xCzAJ +BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UE +CgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzM3WhgPMjA1 +MzA3MDkwNzMzMzdaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABE/11whaKK7sU42xbFG1MetXY/HCT/bvX4mUIwZQK02Bzs4Z +szkgBHPyroATwYI7P7b9FzfoM8rbzvGT+un5v6IwCgYIKoZIzj0EAwIDSQAwRgIh +APbdoZeyZRjCUbfdRxo2IcEEUqBqbUSx41JYSXLhpg7QAiEA6Gs+IfcsUzIeQWOR +MDTn7Ra0gqglMNf126nb1OXFwAg= +-----END CERTIFICATE----- diff --git a/tests/fixtures/root-ca-key.pem b/tests/fixtures/root-ca-key.pem new file mode 100644 index 0000000..192a63b --- /dev/null +++ b/tests/fixtures/root-ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKNp0iOcAtuUqCVQO9091W12EehsnotOFDD0VQS5PYUooAoGCCqGSM49 +AwEHoUQDQgAE54OuvyHlxoS7qItpOUiY9gdqOPLbsMwkHY81yvAvftR7waKI0TIZ +81Gqg9komHxXja3UP4ZgcrhBprXdBui0ZQ== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/root-ca.pem b/tests/fixtures/root-ca.pem new file mode 100644 index 0000000..085e982 --- /dev/null +++ b/tests/fixtures/root-ca.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB8zCCAZmgAwIBAgIUMKnFU92lhc6e2Bp7EEx67/f9OfIwCgYIKoZIzj0EAwIw +TjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MQww +CgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNjAyMjEwNzMzMzda +GA8yMDUzMDcwOTA3MzMzN1owTjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRl +MQ0wCwYDVQQHDARDaXR5MQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9z +dDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOeDrr8h5caEu6iLaTlImPYHajjy +27DMJB2PNcrwL37Ue8GiiNEyGfNRqoPZKJh8V42t1D+GYHK4Qaa13QbotGWjUzBR +MB0GA1UdDgQWBBTz5gaZG+/IRYzxfOffhaMvWAIAUzAfBgNVHSMEGDAWgBTz5gaZ +G+/IRYzxfOffhaMvWAIAUzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gA +MEUCIQDaqbbPvBGAvOuGuKkAUxLqeTvvYhUPDxp61ACmTjt4NQIgBRa8b1v7XO5/ +A3QagCSPLBLQNGe1l1fSyuVyYkQ4NHI= +-----END CERTIFICATE----- diff --git a/tests/fixtures/root-ca.srl b/tests/fixtures/root-ca.srl new file mode 100644 index 0000000..7da4903 --- /dev/null +++ b/tests/fixtures/root-ca.srl @@ -0,0 +1 @@ +68D40034FD096EA10844035C856C8739B0ADC323 diff --git a/tests/fixtures/rsa-leaf-client-key.pem b/tests/fixtures/rsa-leaf-client-key.pem new file mode 100644 index 0000000..005f390 --- /dev/null +++ b/tests/fixtures/rsa-leaf-client-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCEhoycxa265DP1 +9TRtPJANnUXOzgFEJWyxHov3DLPClJjpTldyUdpWzMdgHvqBy3RWSXfvXKo3NBE+ +3gUOdNjWlQDPRHLEeZbmbfYJR34Fq/2qEDIJAPBJIbutGJ7mHN/uptky1mTxkG93 +7Yg+iOYe92itA9F/Hp7S30vTHF+BGeGFVySSFANMzVByYApOn5b3aU0Is9yqB2JH +z2/FJK442u1wbDUMlUiu5u0Z2mi83YIaD48zPACqvTJPpOE8dlhwm0T3J/4DeC35 +mP+H4WYTomThOgOxXmcwBEvyfhp+AM3bg7P/fkLieOL8V39bCBkFxuQQ2XUSiP1b +UavPH/1FAgMBAAECggEAAxKvo1moxqL7Ws0Icy6qqct8zpJ+UFs3IwBHVJTfJ42U +jMxmlghfMhC7isy3wwIPEjUlP8/77nMZofCPWMoi1mRMHteX0FsqwZseemaa/dIC +0XULWU7D7kjcS4487lm7Dj32NAnpUKSoa7UpVJyODCfGg0SJu2iKsZnAmLDuvKqZ +bMVkH/5l3WubHgwlla1K8z+B+pCB9Vo1nUy0vumgl3R6MUeD+VYMuKC/lC2ii34b +OLCzJtTym9WLcH5uM9EHuzzgJkNkLo5KZ2dwibjOfML5iFl3mMIskFsvN558tJdN +U5yyR73qlzWnCWG/7Zdk3zxFXlxnajDqkfkuo2p9oQKBgQC67HlH09q2osol1hxt +RKcWThjpaCvL+9/g3ZxOi7nQ05ICwqG63rPw9BLCgQ+/ojPvgQzOl71uxfhJUn0G +YubdEys1jxmsq6Xomr+JlqjiVvq7VQb9jui33oNMlFNblCwRkv4WNfRf2osClbJy +kEKzFp+mUiHE/TqW8XdWQ3ZMGQKBgQC1f9qCuL699GQ4UFt78EX/+w0i3vKOCV/G +rXkIPpOoteiP3WbDe8KPtlBI/OA2sx6WOcoCN2njPUvMg1OJYwoG6lW3KtCH7q4d +jbUHA3fujk6qungZzmGyQuP0SF+WPuapfJJTMudv8/Q8iqyw0A0jrika0uomk9GO +hmuN4NMgDQKBgDsps61XUady2Pam0TKIgzYdG+dscEhM/WxH2DxIH1UIUfOLtPLX +oC6IohNsFBb8eOG6f9o3zt5rzI0wjZ/i191rPsbh9yde4NFBgZRD2kpha1S1sdO5 +UtE3nWk0nTmkKVqaos9W3nUkT9FOnj+Ch4n1hCx5XHTkDZJO9Q2ZpqypAoGAXxaH +xVrC28QRSYuYElu0YMMHg9BoJU/19KHnuhEGzSnYmJ62+w14xlAOyd5qHV5EVRIb +qoObnyj68D+RYXYYx7y3gYoVzFGYuPUH9Y+0oq+9uSaOS37bokf3I4FTSuTTddJE +2v/dTsLxn1JL3Spy59GTyXDcqa9h55i2+pLiKXUCgYBFBdZcjsTkjubzYgt2eZdF +i1cBwhOHqzXf8YDXlGo+FFHMyi4BsK4ZiMyhclCYe81+qT7fJG2AXq0sU8QppfeT +q/W55cosZsGXbMfN+RG+IXP7WoFL5pO72vu7IHx1h81SxWNG9EijSYr6A9d4coRF +soksgMdIMuQQU1UF1a5m8A== +-----END PRIVATE KEY----- diff --git a/tests/fixtures/rsa-leaf-client.pem b/tests/fixtures/rsa-leaf-client.pem new file mode 100644 index 0000000..84e4391 --- /dev/null +++ b/tests/fixtures/rsa-leaf-client.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJTCCAg0CFDvurK/y+VjruSv6kiAH1rRUsRNbMA0GCSqGSIb3DQEBCwUAME4x +CzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoG +A1UECgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzQxWhgP +MjA1MzA3MDkwNzMzNDFaME4xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTEN +MAsGA1UEBwwEQ2l0eTEMMAoGA1UECgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3Qw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEhoycxa265DP19TRtPJAN +nUXOzgFEJWyxHov3DLPClJjpTldyUdpWzMdgHvqBy3RWSXfvXKo3NBE+3gUOdNjW +lQDPRHLEeZbmbfYJR34Fq/2qEDIJAPBJIbutGJ7mHN/uptky1mTxkG937Yg+iOYe +92itA9F/Hp7S30vTHF+BGeGFVySSFANMzVByYApOn5b3aU0Is9yqB2JHz2/FJK44 +2u1wbDUMlUiu5u0Z2mi83YIaD48zPACqvTJPpOE8dlhwm0T3J/4DeC35mP+H4WYT +omThOgOxXmcwBEvyfhp+AM3bg7P/fkLieOL8V39bCBkFxuQQ2XUSiP1bUavPH/1F +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBABUFayU+Mn+rmHSXMkNabzhjzwBoTuVR +9TxyvlFcMcoEoVqek4JlXNCS4Ipw7vky4ukh6KoQIDzHt7z8rKo5LlALW3pWNxtd +aZI5w4WPSmuMTvUJHfUgCC9b2cmzoKH2YdFfD64KxSBO3KvRlMP7xrLgdedyVojg +pd4igTPVHfUil1gRWemiuPLYtxyuw3CXqzEJ03qyVuisDiDxyxAGKZ7Kh80pMyl5 +EzOl5D7zv+8GaTQciR2O1n5+sQwr6i0gQ3zBsHbasL/dryP8ISUyto8blemVt7rJ +8pOH7L23s+nFugPL2LXgyhQKp+1ONS4MvJJnD+0s5OgAYHdpn1AMX/Y= +-----END CERTIFICATE----- diff --git a/tests/fixtures/rsa-leaf-server-key.pem b/tests/fixtures/rsa-leaf-server-key.pem new file mode 100644 index 0000000..d8176e7 --- /dev/null +++ b/tests/fixtures/rsa-leaf-server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPieSeFN92AlKa +sDv0K5SN7GSPpUypPTFLbyZyXj09THeXs65cGrH1hGeVCmvneLk8rYzDvL1RBygQ +DHKentw6XApNtUQd6iRfm4vCjHecj351OQw1ongn9lmf5DLoo3jxQ+1YDSHsKM0F +t22ZMX/PAvZkv6EoNwU85hn7jUUIN8sWGLb64MFjlszQkkU1d4xhB+eM8j0S8nwl +3+/yj/iefvLUt3U0HxCQgRN34Ok5ifDyDL6qIYIY080GZ1sSb1QQx2GqJ+t3UxYU +jK8LMk+9H7FiNojaV1aryA+T68ny4xtd0u3+n3k3qu03/aKQCQQ30Yto/HmvdcRM +HrUybCT1AgMBAAECggEAVMh7Wc0jIN2AC2uVgoFPNc9toXUg5T53Zd6pcS1ojSMw +9AtWXpAVRTxq6GsSe8RIwC4SBGCVYoWu6yd6p6w0Qa8aEnbLtgDSDTDJHpgtwQqp +2Y+NmleeCI1OPdCGr50gqkdarY7n3ccpQn2Vy3B6SwJlz/CtEHWxsRg7YxVqLxmH +c681YifQHIMKoEq18bH0Kv1aZb62qHTViwhLXgl5wER0g5bB6DGdQBO/MaEuvk3m +bM1doT11AO0HWAOiSL+cqjbKPhvG/+4w6eKjxKbmlf8WaC74/QaT/2PPbrCHRJy2 +JK5iWtC1hrMeolr2bQ7/poGsyf12vXUu2Gdvd3my3QKBgQDuA4CetV2OBpvtqPAa +mCOrW+9xL5UhhxLVCnaf5ASUKiYlnXLIJIPXZYXWQfZmkm3IR60HrVKYFRMz7dlt +K17jufDi2J30yWxDhfgt4hW7b+2GaGYV2IB9J6JgsKP7A+xEtui0Fsewl5n4Vcg4 +qXV1fR8/7SEMftaYfWMabriKHwKBgQDfONXVzbDYscXvlDSOdKvqnGqw1akZ/HXB +SewAJSQFYhxEew8xFQrEksj9JutgfjyE0O3qMfPKu+7MZRtigYcWkIBM4LkVXbur +1xeQF+wswcvrPk1kgHilWNCLwyS3d59oNWkVTQapi5aNRrUw6WcUF+vuVdPAPEwr +0jEI/pxWawKBgG9/iohOtBXteKxb4KbEsKj8t22Rec4sBaFxdmKuoYp7OEWUvYmO +uYh1Eb5uRiyE2hLhqOgYxMFj1gwUly9yCtCpcXQDP/PFTAdRwhJUgBO/ekjlrTT8 +qCx5HbMn7JmRm+QQv3Bl34QVcNaJ8PLCR7kTNUlwH3RIEuV6j6t5RM/HAoGBAK6E +2vnrdMYWRvBGaMivgvFMFUXn5euBK/dQegirAPyMdhk8NOZk0yRYtnblhMTOLTaR +ulCNeVMZl1uJ+N0M87a6hvSUBWAlBmMVKUDo0ycy5OEoto1KvAhZ5cI/cWdXSPPK +Pjv/GqRXk/8kNujkskhNY5HU3FbBTbQ9A0VK+qO9AoGBANS7QFVT1eOgDfRintAY +58XCf+rB36jjxDPmbLXflV6LdSJfhK8HFQIhgu3n2tmyh78mlP7pQZVIA9vHOf4m +MePga9YsjsY8fpkTntje3uOXyDmiCasUK/1YwNmJgpbg9aaScbKDtLAAYRpV5wIj +7TvrmVKy7wOiT51KCbyZaESG +-----END PRIVATE KEY----- diff --git a/tests/fixtures/rsa-leaf-server.pem b/tests/fixtures/rsa-leaf-server.pem new file mode 100644 index 0000000..c1e9aa0 --- /dev/null +++ b/tests/fixtures/rsa-leaf-server.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6zCCAdMCFDvurK/y+VjruSv6kiAH1rRUsRNaMA0GCSqGSIb3DQEBCwUAME4x +CzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoG +A1UECgwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjYwMjIxMDczMzQxWhgP +MjA1MzA3MDkwNzMzNDFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAM+J5J4U33YCUpqwO/QrlI3sZI+lTKk9MUtv +JnJePT1Md5ezrlwasfWEZ5UKa+d4uTytjMO8vVEHKBAMcp6e3DpcCk21RB3qJF+b +i8KMd5yPfnU5DDWieCf2WZ/kMuijePFD7VgNIewozQW3bZkxf88C9mS/oSg3BTzm +GfuNRQg3yxYYtvrgwWOWzNCSRTV3jGEH54zyPRLyfCXf7/KP+J5+8tS3dTQfEJCB +E3fg6TmJ8PIMvqohghjTzQZnWxJvVBDHYaon63dTFhSMrwsyT70fsWI2iNpXVqvI +D5PryfLjG13S7f6feTeq7Tf9opAJBDfRi2j8ea91xEwetTJsJPUCAwEAATANBgkq +hkiG9w0BAQsFAAOCAQEAqDx0KKVOXHTe5QHxtmq8PDmRNoixHoMjf1QLSBnup8Os +quVQ1jLrZPdtUgVS+dJjMoEk3yDLya66+WADJDa03PGJ5vis/6Sn0reo0hpaTIxx +bB7h65v8N5AHCIA5iJPPdYo9D7qKOfsnZ3iShWEoD4SjjmcnkLQ4/NFiv2gBF5Xa +LO23STWOIeNvOvNYAwBTJ614EIyI9n1qAKx5whVF1jIoTp1DFda2MYNunSk9tHUE +eoPBolaegzDImX4PVkwDLthlSqzOOueQwHjS4jRqjwO5GVUo3JD4Hs6dMY2T4YO8 +n+9Q7JvU9Vei0/3RD7xOPpY47VG38pfi+iPAS7SsbQ== +-----END CERTIFICATE----- diff --git a/tests/fixtures/rsa-root-ca-key.pem b/tests/fixtures/rsa-root-ca-key.pem new file mode 100644 index 0000000..c9250f6 --- /dev/null +++ b/tests/fixtures/rsa-root-ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCtW62UGSYLPB4V +ZurqL7bE2nJNyLrcvMQV3zkOVbnR9hrzofCELRCpaIZyQzg2OqX56kyEu94SPNcM +513U+Ega2ij0xmucF3vrB6DjDuusIZzxu771n//QcS2vuIF6GJn1c4P5Zcc0hJGk +nqHmHZ2Z9srEqmTKGgK8ar7HK7pKuBHqR4umdSdPXyjhx+7OPiVQpNY3oWtdCUsE +CqGMIlRNDGpW/JVhYd47KfeI1syhNj08zEX8CvBgXmbNWeGgvCN6FYbF8Pqyq527 +seALs2ZiY5FX2QekI3StQoXl2S5QY740OGHt+NkIS7BEtRjSW6fTeg3CI12uyS03 +PLDCPVI9AgMBAAECggEABe3gSnPvvqugNcgu2bKnFCWKikmGxpz4Me1QFMLu15UM +x+9ue/7Ulo66JndRGV25JoqScmnAhywoiMrzBSE2yiBTDUgqDw0oklnQ8WMlwV3m +8cejmOAPjlAIqX1shEIKJgB8749BGHU+S/yW+fliVg1AUyimyNjJ5iraepJuzTZf +ECn+Ho4rN2MnOvFolJxVitVA2vrEj2soiysNKmwP9ykxRI6vGUZxIZgRpm+MX/Gg +C+lxAZe+aqikpwTHDLz9fliajubAfoONLJGihlUfWCy3p58I7TPR1FG3neXovQG2 +FBXeyByvYlgjaCY1qERu0lEBVjvwk7fgnbfgaff1MQKBgQDYKTyOLtlSF7zEp2/V +he6a7cDQ99z2xGMHf7cwajeq+rOVRZiWRIierGVluXq3YkIpQYt8ATTF36KXgs/N +i0ciitRrIUZGkPGx1vG8tUT0RnSDPRSjN/bWB6heAc4Ok2k+4952ir5YnauZJiMX +hMyExcpFCkoKpkJtD2P0hXwYKQKBgQDNTvEIQSe1ShgSGbi3kwNnE5iBtecdCJtN +0Mc2QrnK7kgb2WBhkNLOX0qCVYhq9flGkmkOnZ0A098B+0lBxAVzrSxVND/fcZxD +P/frJoD8Cjlsvs6byXSCw5OnOI4p2vbVOPG3/tenamT1VnZjd+ui4dmfzc132QVB +mdQJobr79QKBgBQxNfDqO27JvN557Z56lmFumDZtEP0UN1P6ADjLk0urg58ME0bm +PATmgcpQ2z/KM/f6oXcB/dYGIAAbPiIrQofdhB5Fy3TIEWvVclt4a2qOMlAYIpdk +oPA5Yub1MDR4XLp9OsjECSfqAp+ZymlmBFzaxRxR67y27zmU2Hd9CKyRAoGAZ2hY +bLDsgBo2r383E3c/on4zNTnakzwPhQ0gGYtYKwcDWMuPCPU5yGokjCrqj/0eNdZu +hccLGiycyVG43yANIutZRf0QIsoFS7X/d/gnxUqdC9G7HKpGPcqmJvaMXDaGVnTd +ArCgDBnBifSnoof3Lk4VH7E3ySKMzDLfoo5MMLUCgYAzp9jwHOW0LIfC+jtsg6OZ +R3t94M1zDsmqsom9ybA8slJ1lJ1kelRFe/rMq0A1Mgk0NIu2SqVIQMpaZwClyCgz +UU5idCghdDEz+YMZzwGWaXhRONvQ6pjfxRXGHJTP09BXXv9yRMW/VaCv3Ep29pZP +wBRBeeD/nNl1NZRkqDT2bQ== +-----END PRIVATE KEY----- diff --git a/tests/fixtures/rsa-root-ca.pem b/tests/fixtures/rsa-root-ca.pem new file mode 100644 index 0000000..76f2528 --- /dev/null +++ b/tests/fixtures/rsa-root-ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIUXcX7sKMszgJxJBxXW38sZQ4PTMAwDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNjAyMjEwNzMz +NDBaGA8yMDUzMDcwOTA3MzM0MFowTjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0 +YXRlMQ0wCwYDVQQHDARDaXR5MQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1brZQZJgs8HhVm +6uovtsTack3Iuty8xBXfOQ5VudH2GvOh8IQtEKlohnJDODY6pfnqTIS73hI81wzn +XdT4SBraKPTGa5wXe+sHoOMO66whnPG7vvWf/9BxLa+4gXoYmfVzg/llxzSEkaSe +oeYdnZn2ysSqZMoaArxqvscrukq4EepHi6Z1J09fKOHH7s4+JVCk1jeha10JSwQK +oYwiVE0Malb8lWFh3jsp94jWzKE2PTzMRfwK8GBeZs1Z4aC8I3oVhsXw+rKrnbux +4AuzZmJjkVfZB6QjdK1CheXZLlBjvjQ4Ye342QhLsES1GNJbp9N6DcIjXa7JLTc8 +sMI9Uj0CAwEAAaNTMFEwHQYDVR0OBBYEFM5Oy6DzdnkIIXkcHJeex2+qqyv0MB8G +A1UdIwQYMBaAFM5Oy6DzdnkIIXkcHJeex2+qqyv0MA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAB8IgDLmfHEQ5dYZlNfDdpAz2WwqJzMicByRqXPx +IaKHyM2yBOExKWwcMJfjJtxAHMQ69HRLm6lC4ny5KOAVCuhpHtnHCNH8sC+/1kjx +K8BrKWXtgssci9NiPCHGtapvJVx5woB6BPYESggoYlHSNisAsauUAI7rG1bohO6C +2dqZu2FyQm95ICJQlGNZ/nMdcB41iHzL0NVcDeoxPVj5+lQiFG+GoVZxcK753Nca ++inzlYfoCIYUlU8/JGXIHmKkjwaHW25zN/g5734WPddSGC/4IFYVx+G9NcTNLZph +A030PCCEq5Ws4eP1ztCGDJ6/q5a3kDRFsPmsukJXcf3CXt0= +-----END CERTIFICATE----- diff --git a/tests/fixtures/setup_fixtures.sh b/tests/fixtures/setup_fixtures.sh new file mode 100755 index 0000000..6ceda73 --- /dev/null +++ b/tests/fixtures/setup_fixtures.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -e + +SUBJ_CA="/C=US/ST=State/L=City/O=TestOrg/CN=TestRootCA" +SUBJ_IM="/C=US/ST=State/L=City/O=TestOrg/CN=TestIntermediateCA" +SUBJ_SRV="/CN=localhost" +SUBJ_CLI="/C=US/ST=State/L=City/O=TestOrg/CN=TestClient" +SUBJ_RSA_CA="/C=US/ST=State/L=City/O=TestOrg/CN=TestRsaRootCA" + +EXT_CA="basicConstraints=critical,CA:TRUE\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always" +EXT_LEAF="basicConstraints=CA:FALSE\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid,issuer" + +# Root CA +openssl ecparam -name prime256v1 -genkey -noout -out root-ca-key.pem +openssl req -new -x509 -sha256 -key root-ca-key.pem -days 3650 -out root-ca.pem -subj "$SUBJ_CA" + +# Intermediate CA +openssl ecparam -name prime256v1 -genkey -noout -out intermediate-ca-key.pem +openssl req -new -sha256 -key intermediate-ca-key.pem -out _im.csr -subj "$SUBJ_IM" +openssl x509 -req -in _im.csr -CA root-ca.pem -CAkey root-ca-key.pem \ + -CAcreateserial -out intermediate-ca.pem -days 3650 -sha256 \ + -extfile <(printf "$EXT_CA") +rm _im.csr + +# Server leaf cert (signed by root CA) +openssl ecparam -name prime256v1 -genkey -noout -out leaf-server-key.pem +openssl req -new -sha256 -key leaf-server-key.pem -out _srv.csr -subj "$SUBJ_SRV" +openssl x509 -req -in _srv.csr -CA root-ca.pem -CAkey root-ca-key.pem \ + -CAcreateserial -out leaf-server.pem -days 3650 -sha256 \ + -extfile <(printf "$EXT_LEAF") +rm _srv.csr + +# Client leaf cert (signed by root CA) +openssl ecparam -name prime256v1 -genkey -noout -out leaf-client-key.pem +openssl req -new -sha256 -key leaf-client-key.pem -out _cli.csr -subj "$SUBJ_CLI" +openssl x509 -req -in _cli.csr -CA root-ca.pem -CAkey root-ca-key.pem \ + -CAcreateserial -out leaf-client.pem -days 3650 -sha256 \ + -extfile <(printf "$EXT_LEAF") +rm _cli.csr + +# Intermediate server cert + chain +openssl ecparam -name prime256v1 -genkey -noout -out intermediate-server-key.pem +openssl req -new -sha256 -key intermediate-server-key.pem -out _imsrv.csr -subj "$SUBJ_SRV" +openssl x509 -req -in _imsrv.csr -CA intermediate-ca.pem -CAkey intermediate-ca-key.pem \ + -CAcreateserial -out intermediate-server.pem -days 3650 -sha256 \ + -extfile <(printf "$EXT_LEAF") +rm _imsrv.csr +cat intermediate-server.pem intermediate-ca.pem > chain.pem + +# RSA root CA +openssl req -x509 -newkey rsa:2048 -keyout rsa-root-ca-key.pem -nodes \ + -out rsa-root-ca.pem -sha256 -days 3650 -subj "$SUBJ_RSA_CA" + +# RSA server cert +openssl req -newkey rsa:2048 -keyout rsa-leaf-server-key.pem -nodes \ + -out _rsasrv.csr -sha256 -subj "$SUBJ_SRV" +openssl x509 -req -CA rsa-root-ca.pem -CAkey rsa-root-ca-key.pem \ + -in _rsasrv.csr -out rsa-leaf-server.pem -days 3650 -CAcreateserial +rm _rsasrv.csr + +# RSA client cert +openssl req -newkey rsa:2048 -keyout rsa-leaf-client-key.pem -nodes \ + -out _rsacli.csr -sha256 -subj "$SUBJ_CLI" +openssl x509 -req -CA rsa-root-ca.pem -CAkey rsa-root-ca-key.pem \ + -in _rsacli.csr -out rsa-leaf-client.pem -days 3650 -CAcreateserial +rm _rsacli.csr diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..b0e39ba --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,1305 @@ +mod common; + +mod handshake { + use embedded_io::BufRead as _; + use embedded_io_adapters::{std::FromStd, tokio_1::FromTokio}; + use embedded_io_async::BufRead as _; + use rand::rngs::OsRng; + use std::net::SocketAddr; + use std::sync::Once; + + static LOG_INIT: Once = Once::new(); + static INIT: Once = Once::new(); + static mut ADDR: Option = None; + + fn init_log() { + LOG_INIT.call_once(|| { + let _ = env_logger::try_init(); + }); + } + + fn setup() -> SocketAddr { + use mio::net::TcpListener; + init_log(); + INIT.call_once(|| { + let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + crate::common::run(listener); + }); + #[allow(static_mut_refs)] + unsafe { + ADDR.replace(addr) + }; + }); + unsafe { ADDR.unwrap() } + } + + #[tokio::test] + async fn connect_and_echo() { + use mote_tls::*; + use tokio::net::TcpStream; + let addr = setup(); + + let stream = TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + log::info!("SIZE of connection is {}", core::mem::size_of_val(&tls)); + + let open_fut = tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )); + log::info!("SIZE of open fut is {}", core::mem::size_of_val(&open_fut)); + open_fut.await.expect("error establishing TLS connection"); + log::info!("Established"); + + let write_fut = tls.write(b"ping"); + log::info!( + "SIZE of write fut is {}", + core::mem::size_of_val(&write_fut) + ); + write_fut.await.expect("error writing data"); + tls.flush().await.expect("error flushing data"); + + // Make sure reading into a 0 length buffer doesn't loop + let mut rx_buf = [0; 0]; + let read_fut = tls.read(&mut rx_buf); + log::info!("SIZE of read fut is {}", core::mem::size_of_val(&read_fut)); + let sz = read_fut.await.expect("error reading data"); + assert_eq!(sz, 0); + + let mut rx_buf = [0; 4096]; + let read_fut = tls.read(&mut rx_buf); + log::info!("SIZE of read fut is {}", core::mem::size_of_val(&read_fut)); + let sz = read_fut.await.expect("error reading data"); + assert_eq!(4, sz); + assert_eq!(b"ping", &rx_buf[..sz]); + log::info!("Read {} bytes: {:?}", sz, &rx_buf[..sz]); + + // Test that mote-tls doesn't block if the buffer is empty. + let mut rx_buf = [0; 0]; + let sz = tls.read(&mut rx_buf).await.expect("error reading data"); + assert_eq!(sz, 0); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[tokio::test] + async fn connect_buffered_read() { + use mote_tls::*; + use tokio::net::TcpStream; + let addr = setup(); + + let stream = TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + log::info!("SIZE of connection is {}", core::mem::size_of_val(&tls)); + + let open_fut = tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )); + log::info!("SIZE of open fut is {}", core::mem::size_of_val(&open_fut)); + open_fut.await.expect("error establishing TLS connection"); + log::info!("Established"); + + let write_fut = tls.write(b"data to echo"); + log::info!( + "SIZE of write fut is {}", + core::mem::size_of_val(&write_fut) + ); + write_fut.await.expect("error writing data"); + tls.flush().await.expect("error flushing data"); + + { + let mut buf = tls.read_buffered().await.expect("error reading data"); + log::info!("Read bytes: {:?}", buf.peek_all()); + + let read_bytes = buf.pop(2); + assert_eq!(b"da", read_bytes); + + let read_bytes = buf.pop(2); + assert_eq!(b"ta", read_bytes); + } + + { + let mut buf = tls.read_buffered().await.expect("error reading data"); + assert_eq!(b" to ", buf.pop(4)); + } + + { + let mut buf = tls.read_buffered().await.expect("error reading data"); + let read_bytes = buf.pop_all(); + assert_eq!(b"echo", read_bytes); + } + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[tokio::test] + async fn connect_bufread_trait() { + use mote_tls::*; + use tokio::net::TcpStream; + + let addr = setup(); + + let stream = TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .await + .expect("error establishing TLS connection"); + log::info!("Established"); + + tls.write(b"ping").await.expect("error writing data"); + tls.flush().await.expect("error flushing data"); + + let buf = tls.fill_buf().await.expect("error reading data"); + + assert_eq!(b"ping", buf); + log::info!("Read bytes: {:?}", buf); + + let len = buf.len(); + tls.consume(len); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[test] + fn blocking_connect_and_echo() { + use mote_tls::blocking::*; + use std::net::TcpStream; + + let addr = setup(); + let stream = TcpStream::connect(addr).expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls: SecureStream, Aes128GcmSha256> = SecureStream::new( + FromStd::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .expect("error establishing TLS connection"); + log::info!("Established"); + + tls.write(b"ping").expect("error writing data"); + tls.flush().expect("error flushing data"); + + // Make sure reading into a 0 length buffer doesn't loop + let mut rx_buf = [0; 0]; + let sz = tls.read(&mut rx_buf).expect("error reading data"); + assert_eq!(sz, 0); + + let mut rx_buf = [0; 4096]; + let sz = tls.read(&mut rx_buf).expect("error reading data"); + assert_eq!(4, sz); + assert_eq!(b"ping", &rx_buf[..sz]); + log::info!("Read {} bytes: {:?}", sz, &rx_buf[..sz]); + + // Test that mote-tls doesn't block if the buffer is empty. + let mut rx_buf = [0; 0]; + let sz = tls.read(&mut rx_buf).expect("error reading data"); + assert_eq!(sz, 0); + + tls.close() + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[test] + fn blocking_buffered_read() { + use mote_tls::blocking::*; + use std::net::TcpStream; + + let addr = setup(); + let stream = TcpStream::connect(addr).expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls: SecureStream, Aes128GcmSha256> = SecureStream::new( + FromStd::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .expect("error establishing TLS connection"); + log::info!("Established"); + + tls.write(b"ping").expect("error writing data"); + tls.flush().expect("error flushing data"); + + let mut buf = tls.read_buffered().expect("error reading data"); + log::info!("Read bytes: {:?}", buf.peek_all()); + + let read_bytes = buf.pop(2); + assert_eq!(b"pi", read_bytes); + let read_bytes = buf.pop_all(); + assert_eq!(b"ng", read_bytes); + + core::mem::drop(buf); + + tls.close() + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[test] + fn blocking_bufread_trait() { + use mote_tls::blocking::*; + use std::net::TcpStream; + + let addr = setup(); + let stream = TcpStream::connect(addr).expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls: SecureStream, Aes128GcmSha256> = SecureStream::new( + FromStd::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .expect("error establishing TLS connection"); + log::info!("Established"); + + tls.write(b"ping").expect("error writing data"); + tls.flush().expect("error flushing data"); + + let buf = tls.fill_buf().expect("error reading data"); + + assert_eq!(b"ping", buf); + log::info!("Read bytes: {:?}", buf); + + let len = buf.len(); + tls.consume(len); + + tls.close() + .map_err(|(_, e)| e) + .expect("error closing session"); + } +} + +mod psk { + use embedded_io_adapters::tokio_1::FromTokio; + use mote_tls::*; + use openssl::ssl; + use rand::rngs::OsRng; + use std::io::{Read, Write}; + use std::net::SocketAddr; + use std::net::TcpListener; + use std::sync::Once; + use tokio::net::TcpStream; + use tokio::task::JoinHandle; + use tokio::time::Duration; + use tokio::time::timeout; + + static INIT: Once = Once::new(); + + fn setup() -> (SocketAddr, JoinHandle<()>) { + INIT.call_once(|| { + let _ = env_logger::try_init(); + }); + + const DEFAULT_CIPHERS: &[&str] = &["PSK"]; + let mut builder = + ssl::SslAcceptor::mozilla_intermediate_v5(ssl::SslMethod::tls_server()).unwrap(); + builder + .set_private_key_file("tests/fixtures/leaf-server-key.pem", ssl::SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("tests/fixtures/leaf-server.pem") + .unwrap(); + builder + .set_min_proto_version(Some(ssl::SslVersion::TLS1_3)) + .unwrap(); + builder.set_cipher_list(&DEFAULT_CIPHERS.join(",")).unwrap(); + builder.set_psk_server_callback(move |_ssl, identity, secret_mut| { + if let Some(b"vader") = identity { + secret_mut[..4].copy_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + Ok(4) + } else { + Ok(0) + } + }); + let acceptor = builder.build(); + + let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + let h = tokio::task::spawn_blocking(move || { + let (stream, _) = listener.accept().unwrap(); + let mut conn = acceptor.accept(stream).unwrap(); + let mut buf = [0; 64]; + let len = conn.read(&mut buf[..]).unwrap(); + conn.write_all(&buf[..len]).unwrap(); + }); + (addr, h) + } + + #[tokio::test(flavor = "multi_thread")] + async fn connect_with_psk() { + let (addr, h) = setup(); + timeout(Duration::from_secs(120), async move { + println!("Connecting..."); + let stream = TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + println!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new() + .with_psk(&[0xaa, 0xbb, 0xcc, 0xdd], &[b"vader"]) + .with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + assert!( + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng) + )) + .await + .is_ok() + ); + println!("TLS session opened"); + + tls.write(b"ping").await.unwrap(); + tls.flush().await.unwrap(); + + println!("TLS data written"); + let mut rx = [0; 4]; + let l = tls.read(&mut rx[..]).await.unwrap(); + + println!("TLS data read"); + assert_eq!(4, l); + assert_eq!(b"ping", &rx[..l]); + + h.await.unwrap(); + }) + .await + .unwrap(); + } +} + +mod split { + use embedded_io::{Read, Write}; + use embedded_io_adapters::std::FromStd; + use rand_core::OsRng; + use std::net::{SocketAddr, TcpStream}; + use std::sync::Once; + + static INIT: Once = Once::new(); + static mut ADDR: Option = None; + + fn setup() -> SocketAddr { + use mio::net::TcpListener; + INIT.call_once(|| { + let _ = env_logger::try_init(); + + let addr: SocketAddr = "127.0.0.1:12346".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + crate::common::run(listener); + }); + #[allow(static_mut_refs)] + unsafe { + ADDR.replace(addr) + }; + }); + unsafe { ADDR.unwrap() } + } + + pub struct Clonable(std::sync::Arc); + + impl Clone for Clonable { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + + impl embedded_io::ErrorType for Clonable { + type Error = std::io::Error; + } + + impl embedded_io::Read for Clonable { + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut stream = FromStd::new(self.0.as_ref()); + stream.read(buf) + } + } + + impl embedded_io::Write for Clonable { + fn write(&mut self, buf: &[u8]) -> Result { + let mut stream = FromStd::new(self.0.as_ref()); + stream.write(buf) + } + fn flush(&mut self) -> Result<(), Self::Error> { + let mut stream = FromStd::new(self.0.as_ref()); + stream.flush() + } + } + + #[test] + fn blocking_split_io() { + use mote_tls::blocking::*; + use std::net::TcpStream; + use std::sync::Arc; + let addr = setup(); + let stream = TcpStream::connect(addr).expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + Clonable(Arc::new(stream)), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .expect("error establishing TLS connection"); + + let (mut reader, mut writer) = tls.split(); + + std::thread::scope(|scope| { + scope.spawn(|| { + let mut buffer = [0; 4]; + reader.read_exact(&mut buffer).expect("Failed to read data"); + }); + scope.spawn(|| { + writer.write(b"ping").expect("Failed to write data"); + writer.flush().expect("Failed to flush"); + }); + }); + + tls.close() + .map_err(|(_, e)| e) + .expect("error closing session"); + } +} + +mod early_data { + use embedded_io::{Read, Write}; + use embedded_io_adapters::std::FromStd; + use rand_core::OsRng; + use std::net::SocketAddr; + use std::sync::Once; + + static INIT: Once = Once::new(); + static mut ADDR: Option = None; + + fn setup() -> SocketAddr { + use mio::net::TcpListener; + INIT.call_once(|| { + let _ = env_logger::try_init(); + + let addr: SocketAddr = "127.0.0.1:12347".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + use crate::common::*; + + let versions = &[&rustls::version::TLS13]; + + let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + + let certs = load_certs(&test_dir.join("fixtures").join("leaf-server.pem")); + let privkey = load_private_key(&test_dir.join("fixtures").join("leaf-server-key.pem")); + + let mut config = rustls::ServerConfig::builder() + .with_cipher_suites(rustls::ALL_CIPHER_SUITES) + .with_kx_groups(&rustls::ALL_KX_GROUPS) + .with_protocol_versions(versions) + .unwrap() + .with_no_client_auth() + .with_single_cert(certs, privkey) + .unwrap(); + + config.max_early_data_size = 512; + + run_with_config(listener, config); + }); + #[allow(static_mut_refs)] + unsafe { + ADDR.replace(addr) + }; + }); + unsafe { ADDR.unwrap() } + } + + #[test] + fn handshake_skips_early_data() { + use mote_tls::blocking::*; + use std::net::TcpStream; + + let addr = setup(); + let stream = TcpStream::connect(addr).expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromStd::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + tls.open(ConnectContext::new( + &config, + SkipVerifyProvider::new::(OsRng), + )) + .expect("error establishing TLS connection"); + + tls.write_all(b"ping").expect("Failed to write data"); + tls.flush().expect("Failed to flush"); + + let mut buffer = [0; 4]; + tls.read_exact(&mut buffer).expect("Failed to read data"); + + tls.close() + .map_err(|(_, e)| e) + .expect("error closing session"); + } +} + +mod client_cert { + use ecdsa::elliptic_curve::SecretKey; + use embedded_io_adapters::tokio_1::FromTokio; + use mote_tls::{Certificate, CryptoBackend, SignatureScheme}; + use p256::ecdsa::SigningKey; + use rand::rngs::OsRng; + use rand_core::CryptoRngCore; + use rustls::server::AllowAnyAuthenticatedClient; + use std::net::SocketAddr; + use std::sync::Once; + + static LOG_INIT: Once = Once::new(); + static INIT: Once = Once::new(); + static mut ADDR: Option = None; + + fn init_log() { + LOG_INIT.call_once(|| { + let _ = env_logger::try_init(); + }); + } + + fn setup() -> SocketAddr { + use mio::net::TcpListener; + init_log(); + INIT.call_once(|| { + let addr: SocketAddr = "127.0.0.1:12348".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + use crate::common::*; + + let versions = &[&rustls::version::TLS13]; + + let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + + let ca = load_certs(&test_dir.join("fixtures").join("root-ca.pem")); + let certs = load_certs(&test_dir.join("fixtures").join("leaf-server.pem")); + let privkey = load_private_key(&test_dir.join("fixtures").join("leaf-server-key.pem")); + + let mut client_auth_roots = rustls::RootCertStore::empty(); + for root in ca.iter() { + client_auth_roots.add(root).unwrap() + } + + let client_cert_verifier = AllowAnyAuthenticatedClient::new(client_auth_roots); + + let config = rustls::ServerConfig::builder() + .with_cipher_suites(rustls::ALL_CIPHER_SUITES) + .with_kx_groups(&rustls::ALL_KX_GROUPS) + .with_protocol_versions(versions) + .unwrap() + .with_client_cert_verifier(client_cert_verifier.boxed()) + .with_single_cert(certs, privkey) + .unwrap(); + + run_with_config(listener, config); + }); + #[allow(static_mut_refs)] + unsafe { + ADDR.replace(addr) + }; + }); + unsafe { ADDR.unwrap() } + } + + struct Credentials<'a> { + rng: OsRng, + priv_key: &'a [u8], + client_cert: Option>, + } + + impl CryptoBackend for Credentials<'_> { + type CipherSuite = mote_tls::Aes128GcmSha256; + type Signature = p256::ecdsa::DerSignature; + + fn rng(&mut self) -> impl CryptoRngCore { + &mut self.rng + } + + fn signer( + &mut self, + ) -> Result<(impl signature::SignerMut, SignatureScheme), mote_tls::ProtocolError> + { + let secret_key = SecretKey::from_sec1_der(self.priv_key) + .map_err(|_| mote_tls::ProtocolError::InvalidPrivateKey)?; + + Ok(( + SigningKey::from(&secret_key), + SignatureScheme::EcdsaSecp256r1Sha256, + )) + } + + fn client_cert(&mut self) -> Option>> { + self.client_cert.clone() + } + } + + #[tokio::test] + async fn mutual_tls_auth() { + use mote_tls::*; + use tokio::net::TcpStream; + let addr = setup(); + + let client_cert_pem = include_str!("fixtures/leaf-client.pem"); + let client_cert_der = pem_parser::pem_to_der(client_cert_pem); + + let private_key_pem = include_str!("fixtures/leaf-client-key.pem"); + let private_key_der = pem_parser::pem_to_der(private_key_pem); + + let stream = TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + log::info!("Connected"); + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + log::info!("SIZE of connection is {}", core::mem::size_of_val(&tls)); + + let mut provider = Credentials { + rng: OsRng, + priv_key: &private_key_der, + client_cert: Some(Certificate::X509(&client_cert_der)), + }; + let open_fut = tls.open(ConnectContext::new(&config, &mut provider)); + log::info!("SIZE of open fut is {}", core::mem::size_of_val(&open_fut)); + open_fut.await.expect("error establishing TLS connection"); + log::info!("Established"); + + let write_fut = tls.write(b"ping"); + log::info!( + "SIZE of write fut is {}", + core::mem::size_of_val(&write_fut) + ); + write_fut.await.expect("error writing data"); + tls.flush().await.expect("error flushing data"); + + // Make sure reading into a 0 length buffer doesn't loop + let mut rx_buf = [0; 0]; + let read_fut = tls.read(&mut rx_buf); + log::info!("SIZE of read fut is {}", core::mem::size_of_val(&read_fut)); + let sz = read_fut.await.expect("error reading data"); + assert_eq!(sz, 0); + + let mut rx_buf = [0; 4096]; + let read_fut = tls.read(&mut rx_buf); + log::info!("SIZE of read fut is {}", core::mem::size_of_val(&read_fut)); + let sz = read_fut.await.expect("error reading data"); + assert_eq!(4, sz); + assert_eq!(b"ping", &rx_buf[..sz]); + log::info!("Read {} bytes: {:?}", sz, &rx_buf[..sz]); + + // Test that mote-tls doesn't block if the buffer is empty. + let mut rx_buf = [0; 0]; + let sz = tls.read(&mut rx_buf).await.expect("error reading data"); + assert_eq!(sz, 0); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } +} + +#[cfg(feature = "webpki")] +mod cert_verify { + use embedded_io_adapters::tokio_1::FromTokio; + use mote_tls::cert_verify::CertVerifier; + use mote_tls::{Aes128GcmSha256, CryptoBackend, Verifier}; + use std::net::SocketAddr; + use std::sync::OnceLock; + use std::time::SystemTime; + + static LOG_INIT: OnceLock<()> = OnceLock::new(); + + struct WebPkiProvider<'a> { + rng: rand::rngs::OsRng, + verifier: CertVerifier<'a, Aes128GcmSha256, SystemTime, 4096>, + } + + impl CryptoBackend for WebPkiProvider<'_> { + type CipherSuite = Aes128GcmSha256; + type Signature = &'static [u8]; + + fn rng(&mut self) -> impl mote_tls::CryptoRngCore { + &mut self.rng + } + + fn verifier( + &mut self, + ) -> Result<&mut impl Verifier, mote_tls::ProtocolError> { + Ok(&mut self.verifier) + } + } + + fn init_log() { + LOG_INIT.get_or_init(|| { + let _ = env_logger::try_init(); + }); + } + + async fn setup() -> SocketAddr { + init_log(); + + use mio::net::TcpListener; + use std::net::{IpAddr, Ipv4Addr}; + + let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)) + .expect("cannot listen on port"); + + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + crate::common::run(listener); + }); + + log::info!("Server at {:?}", addr); + addr + } + + #[tokio::test] + async fn verify_server_cert() { + use mote_tls::*; + + let addr = setup().await; + let pem = include_str!("fixtures/root-ca.pem"); + let der = pem_parser::pem_to_der(pem); + + let stream = tokio::net::TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + + // Hostname verification is not enabled + let config = ConnectConfig::new(); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + let open_fut = tls.open(ConnectContext::new( + &config, + WebPkiProvider { + rng: rand::rngs::OsRng, + verifier: CertVerifier::new(Certificate::X509(&der[..])), + }, + )); + + open_fut.await.expect("error establishing TLS connection"); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } +} + +#[cfg(feature = "native-pki")] +mod native_pki { + use embedded_io_adapters::tokio_1::FromTokio; + use mote_tls::native_pki::CertVerifier; + use mote_tls::{Aes128GcmSha256, CryptoBackend, SignatureScheme, ProtocolError as ConnectError, Verifier}; + use p256::SecretKey; + use p256::ecdsa::{DerSignature, SigningKey}; + use rand_core::OsRng; + use rustls::server::AllowAnyAnonymousOrAuthenticatedClient; + use signature::SignerMut; + use std::net::SocketAddr; + use std::sync::Once; + use std::time::SystemTime; + + static LOG_INIT: Once = Once::new(); + static INIT: Once = Once::new(); + static mut ADDR: Option = None; + + struct RustPkiProvider<'a> { + rng: rand::rngs::OsRng, + verifier: CertVerifier<'a, Aes128GcmSha256, SystemTime, 4096>, + priv_key: Option<&'a [u8]>, + client_cert: Option>, + } + + impl CryptoBackend for RustPkiProvider<'_> { + type CipherSuite = Aes128GcmSha256; + type Signature = DerSignature; + + fn rng(&mut self) -> impl mote_tls::CryptoRngCore { + &mut self.rng + } + + fn verifier(&mut self) -> Result<&mut impl Verifier, ConnectError> { + Ok(&mut self.verifier) + } + + fn signer(&mut self) -> Result<(impl SignerMut, SignatureScheme), ConnectError> { + let key_der = self.priv_key.ok_or(ConnectError::InvalidPrivateKey)?; + let secret_key = + SecretKey::from_sec1_der(key_der).map_err(|_| ConnectError::InvalidPrivateKey)?; + + Ok(( + SigningKey::from(&secret_key), + SignatureScheme::EcdsaSecp256r1Sha256, + )) + } + + fn client_cert(&mut self) -> Option>> { + self.client_cert.clone() + } + } + + fn init_log() { + LOG_INIT.call_once(|| { + let _ = env_logger::try_init(); + }); + } + + fn setup() -> SocketAddr { + use mio::net::TcpListener; + init_log(); + INIT.call_once(|| { + let addr: SocketAddr = "127.0.0.1:12349".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + use crate::common::*; + + let versions = &[&rustls::version::TLS13]; + + let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + + let ca = load_certs(&test_dir.join("fixtures").join("root-ca.pem")); + let certs = load_certs(&test_dir.join("fixtures").join("chain.pem")); + let privkey = load_private_key(&test_dir.join("fixtures").join("intermediate-server-key.pem")); + + let mut client_auth_roots = rustls::RootCertStore::empty(); + for root in ca.iter() { + client_auth_roots.add(root).unwrap() + } + + let client_cert_verifier = + AllowAnyAnonymousOrAuthenticatedClient::new(client_auth_roots); + + let config = rustls::ServerConfig::builder() + .with_cipher_suites(rustls::ALL_CIPHER_SUITES) + .with_kx_groups(&rustls::ALL_KX_GROUPS) + .with_protocol_versions(versions) + .unwrap() + .with_client_cert_verifier(client_cert_verifier.boxed()) + .with_single_cert(certs, privkey) + .unwrap(); + + run_with_config(listener, config); + }); + #[allow(static_mut_refs)] + unsafe { + ADDR.replace(addr) + }; + }); + unsafe { ADDR.unwrap() } + } + + #[tokio::test] + async fn native_pki_verify_server_cert() { + use mote_tls::*; + + let addr = setup(); + let pem = include_str!("fixtures/root-ca.pem"); + let der = pem_parser::pem_to_der(pem); + + let stream = tokio::net::TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + let open_fut = tls.open(ConnectContext::new( + &config, + RustPkiProvider { + rng: OsRng, + verifier: CertVerifier::new(Certificate::X509(&der[..])), + priv_key: None, + client_cert: None, + }, + )); + + open_fut.await.expect("error establishing TLS connection"); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[tokio::test] + async fn native_pki_mutual_cert() { + use mote_tls::*; + + let addr = setup(); + let ca_pem = include_str!("fixtures/root-ca.pem"); + let ca_der = pem_parser::pem_to_der(ca_pem); + + let cli_pem = include_str!("fixtures/leaf-client.pem"); + let cli_der = pem_parser::pem_to_der(cli_pem); + + let key_pem = include_str!("fixtures/leaf-client-key.pem"); + let key_der = pem_parser::pem_to_der(key_pem); + + let stream = tokio::net::TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + let open_fut = tls.open(ConnectContext::new( + &config, + RustPkiProvider { + rng: OsRng, + verifier: CertVerifier::new(Certificate::X509(&ca_der[..])), + priv_key: Some(&key_der), + client_cert: Some(Certificate::X509(&cli_der[..])), + }, + )); + + open_fut.await.expect("error establishing TLS connection"); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } + + #[cfg(feature = "rsa")] + mod rsa_pki { + use digest::FixedOutputReset; + use embedded_io_adapters::tokio_1::FromTokio; + use mote_tls::native_pki::CertVerifier; + use mote_tls::{Aes128GcmSha256, CryptoBackend, SignatureScheme, ProtocolError as ConnectError, Verifier}; + use rand_core::{CryptoRngCore, OsRng}; + use rsa::pkcs8::DecodePrivateKey; + use rustls::server::AllowAnyAnonymousOrAuthenticatedClient; + use sha2::{Digest, Sha256}; + use signature::RandomizedSigner; + use signature::SignerMut; + use std::net::SocketAddr; + use std::sync::Once; + use std::time::SystemTime; + + static LOG_INIT: Once = Once::new(); + static INIT: Once = Once::new(); + static mut ADDR: Option = None; + + struct RsaPssSigningKey { + rng: R, + key: rsa::pss::SigningKey, + } + + impl SignerMut> + for RsaPssSigningKey + { + fn try_sign(&mut self, msg: &[u8]) -> Result, rsa::signature::Error> { + let signature = self.key.try_sign_with_rng(&mut self.rng, msg)?; + Ok(signature.into()) + } + } + + struct RustPkiProvider<'a> { + rng: rand::rngs::OsRng, + verifier: CertVerifier<'a, Aes128GcmSha256, SystemTime, 4096>, + priv_key: Option<&'a [u8]>, + client_cert: Option>, + } + + impl CryptoBackend for RustPkiProvider<'_> { + type CipherSuite = Aes128GcmSha256; + type Signature = Box<[u8]>; + + fn rng(&mut self) -> impl mote_tls::CryptoRngCore { + &mut self.rng + } + + fn verifier(&mut self) -> Result<&mut impl Verifier, ConnectError> { + Ok(&mut self.verifier) + } + + fn signer(&mut self) -> Result<(impl SignerMut, SignatureScheme), ConnectError> { + let key_der = self.priv_key.ok_or(ConnectError::InvalidPrivateKey)?; + let private_key = + rsa::RsaPrivateKey::from_pkcs8_der(key_der).map_err(|_| ConnectError::InvalidPrivateKey)?; + let signer = RsaPssSigningKey { + rng: &mut self.rng, + key: rsa::pss::SigningKey::::new(private_key), + }; + + Ok((signer, SignatureScheme::RsaPssRsaeSha256)) + } + + fn client_cert(&mut self) -> Option>> { + self.client_cert.clone() + } + } + + fn init_log() { + LOG_INIT.call_once(|| { + let _ = env_logger::try_init(); + }); + } + + fn setup() -> SocketAddr { + use mio::net::TcpListener; + init_log(); + INIT.call_once(|| { + let addr: SocketAddr = "127.0.0.1:12350".parse().unwrap(); + + let listener = TcpListener::bind(addr).expect("cannot listen on port"); + let addr = listener + .local_addr() + .expect("error retrieving socket address"); + + std::thread::spawn(move || { + use crate::common::*; + + let versions = &[&rustls::version::TLS13]; + + let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + + let ca = load_certs(&test_dir.join("fixtures").join("rsa-root-ca.pem")); + let certs = load_certs(&test_dir.join("fixtures").join("rsa-leaf-server.pem")); + let privkey = load_private_key(&test_dir.join("fixtures").join("rsa-leaf-server-key.pem")); + + let mut client_auth_roots = rustls::RootCertStore::empty(); + for root in ca.iter() { + client_auth_roots.add(root).unwrap() + } + + let client_cert_verifier = + AllowAnyAnonymousOrAuthenticatedClient::new(client_auth_roots); + + let config = rustls::ServerConfig::builder() + .with_cipher_suites(rustls::ALL_CIPHER_SUITES) + .with_kx_groups(&rustls::ALL_KX_GROUPS) + .with_protocol_versions(versions) + .unwrap() + .with_client_cert_verifier(client_cert_verifier.boxed()) + .with_single_cert(certs, privkey) + .unwrap(); + + run_with_config(listener, config); + }); + #[allow(static_mut_refs)] + unsafe { + ADDR.replace(addr) + }; + }); + unsafe { ADDR.unwrap() } + } + + #[tokio::test] + async fn rsa_pki_verify_and_auth() { + use mote_tls::*; + + let addr = setup(); + let pem = include_str!("fixtures/rsa-root-ca.pem"); + let der = pem_parser::pem_to_der(pem); + + let cli_pem = include_str!("fixtures/rsa-leaf-client.pem"); + let cli_der = pem_parser::pem_to_der(cli_pem); + + let key_pem = include_str!("fixtures/rsa-leaf-client-key.pem"); + let key_der = pem_parser::pem_to_der(key_pem); + + let stream = tokio::net::TcpStream::connect(addr) + .await + .expect("error connecting to server"); + + let mut read_record_buffer = [0; 16640]; + let mut write_record_buffer = [0; 16640]; + + let config = ConnectConfig::new().with_server_name("localhost"); + + let mut tls = SecureStream::new( + FromTokio::new(stream), + &mut read_record_buffer, + &mut write_record_buffer, + ); + + let open_fut = tls.open(ConnectContext::new( + &config, + RustPkiProvider { + rng: OsRng, + verifier: CertVerifier::new(Certificate::X509(&der[..])), + priv_key: Some(&key_der), + client_cert: Some(Certificate::X509(&cli_der[..])), + }, + )); + + open_fut.await.expect("error establishing TLS connection"); + + tls.close() + .await + .map_err(|(_, e)| e) + .expect("error closing session"); + } + } +}