From 384f7c12b8222069e715accc190b792bbbe59ca6 Mon Sep 17 00:00:00 2001 From: Kris Kwiatkowski Date: Sat, 21 Feb 2026 08:51:26 +0000 Subject: [PATCH] Initial commit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitea/workflows/ci.yaml | 38 + .gitignore | 1 + Cargo.lock | 1755 +++++++++++++++++ Cargo.toml | 60 + LICENSE | 174 ++ README.md | 9 + src/alert.rs | 121 ++ src/application_data.rs | 30 + src/asynch.rs | 505 +++++ src/blocking.rs | 493 +++++ src/buffer.rs | 300 +++ src/cert_verify.rs | 279 +++ src/certificate.rs | 157 ++ src/change_cipher_spec.rs | 35 + src/cipher.rs | 15 + src/cipher_suites.rs | 26 + src/common/decrypted_buffer_info.rs | 24 + src/common/decrypted_read_handler.rs | 52 + src/common/mod.rs | 2 + src/config.rs | 382 ++++ src/connection.rs | 643 ++++++ src/content_types.rs | 22 + src/extensions/extension_data/alpn.rs | 39 + src/extensions/extension_data/key_share.rs | 128 ++ .../extension_data/max_fragment_length.rs | 34 + src/extensions/extension_data/mod.rs | 11 + .../extension_data/pre_shared_key.rs | 61 + .../extension_data/psk_key_exchange_modes.rs | 53 + src/extensions/extension_data/server_name.rs | 124 ++ .../extension_data/signature_algorithms.rs | 153 ++ .../signature_algorithms_cert.rs | 34 + .../extension_data/supported_groups.rs | 101 + .../extension_data/supported_versions.rs | 65 + .../extension_data/unimplemented.rs | 24 + src/extensions/extension_group_macro.rs | 93 + src/extensions/messages.rs | 99 + src/extensions/mod.rs | 81 + src/fmt.rs | 223 +++ src/handshake/binder.rs | 36 + src/handshake/certificate.rs | 176 ++ src/handshake/certificate_request.rs | 49 + src/handshake/certificate_verify.rs | 51 + src/handshake/client_hello.rs | 165 ++ src/handshake/encrypted_extensions.rs | 19 + src/handshake/finished.rs | 43 + src/handshake/mod.rs | 242 +++ src/handshake/new_session_ticket.rs | 33 + src/handshake/server_hello.rs | 80 + src/key_schedule.rs | 485 +++++ src/lib.rs | 126 ++ src/native_pki.rs | 468 +++++ src/parse_buffer.rs | 171 ++ src/read_buffer.rs | 171 ++ src/record.rs | 215 ++ src/record_reader.rs | 471 +++++ src/send_policy.rs | 20 + src/write_buffer.rs | 287 +++ tests/common/mod.rs | 373 ++++ tests/fixtures/chain.pem | 25 + tests/fixtures/intermediate-ca-key.pem | 5 + tests/fixtures/intermediate-ca.pem | 13 + tests/fixtures/intermediate-ca.srl | 1 + tests/fixtures/intermediate-server-key.pem | 5 + tests/fixtures/intermediate-server.pem | 12 + 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 | 12 + 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 | 21 + tests/fixtures/rsa-leaf-server-key.pem | 28 + tests/fixtures/rsa-leaf-server.pem | 20 + tests/fixtures/rsa-root-ca-key.pem | 28 + tests/fixtures/rsa-root-ca.pem | 22 + tests/fixtures/rsa-root-ca.srl | 1 + tests/fixtures/setup_fixtures.sh | 70 + tests/integration.rs | 1326 +++++++++++++ 80 files changed, 11786 insertions(+) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md 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-ca.srl 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 100644 tests/fixtures/rsa-root-ca.srl create mode 100755 tests/fixtures/setup_fixtures.sh create mode 100644 tests/integration.rs diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..5e56cf4 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: rustup update stable && rustup default stable + - run: cargo build + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: rustup update stable && rustup default stable + - run: cargo test + - run: cargo test --features webpki + - run: cargo test --features native-pki + - run: cargo test --features native-pki,rsa + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: rustup update stable && rustup default stable && rustup component add clippy + - run: cargo clippy -- -D warnings + + no-std: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: rustup update stable && rustup default stable && rustup target add thumbv7em-none-eabi + - run: cargo build --target thumbv7em-none-eabi --no-default-features 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/LICENSE b/LICENSE new file mode 100644 index 0000000..159fef7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship made available under + the License, as indicated by a copyright notice that is included in + or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other + transformations represent, as a whole, an original work of authorship. + For the purposes of this License, Derivative Works shall not include + works that remain separable from, or merely link (or bind by name) + to the interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean, as submitted to the Licensor for inclusion + in the Work by the copyright owner or by an individual or Legal Entity + authorized to submit on behalf of the copyright owner. For the + purposes of this definition, "submit" means any form of electronic, + verbal, or written communication sent to the Licensor or its + representatives, including but not limited to communication on + electronic mailing lists, source code control systems, and issue + tracking systems that is managed by, or on behalf of, the Licensor + for the purpose of discussing and improving the Work, but excluding + communication that is conspicuously marked or designated in writing + by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any Legal Entity on behalf of + whom a Contribution has been received by the Licensor and included + within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent contributions + Licensable by such Contributor that are necessarily infringed by + their Contribution(s) alone or by the combination of their + Contribution(s) with the Work to which such Contribution(s) was + submitted. If You institute patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that + the Work or any such Contribution embodied within the Work constitutes + direct or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate as of + the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, You must include a readable copy of the + attribution notices contained within such NOTICE file, in + at least one of the following places: within a NOTICE text + file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the + Derivative Works; or, within a display generated by the + Derivative Works, if and wherever such third-party notices + normally appear. The contents of the NOTICE file are for + informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative + Works that You distribute, alongside or in addition to the + NOTICE text from the Work, provided that such additional + attribution notices cannot be construed as modifying the + License. + + You may add Your own license statement for Your modifications and + may provide additional grant of rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the + Derivative Works, as separate terms and conditions for use, + reproduction, or distribution of Your modifications, or for such + Derivative Works as a whole, provided Your use, reproduction, and + distribution of the Work otherwise complies with the conditions + stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any conditions of TITLE, + MERCHANTIBILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely + responsible for determining the appropriateness of using or + reproducing the Work and assume any risks associated with Your + exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or exemplary damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or all other + commercial damages or losses), even if such Contributor has been + advised of the possibility of such damages. + + 9. Accepting Warranty or Liability. While redistributing the Work or + Derivative Works thereof, You may choose to offer, and charge a fee + for, acceptance of support, warranty, indemnity, or other liability + obligations and/or rights consistent with this License. However, in + accepting such obligations, You may offer such obligations only on + Your own behalf and on Your sole responsibility, not on behalf of + any other Contributor, and only if You agree to indemnify, defend, + and hold each Contributor harmless for any liability incurred by, + or claims asserted against, such Contributor by reason of your + accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..aefcef8 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# mote-tls + +TLS 1.3 client with `no_std` and no allocator support. + +Based on commit [426f327](https://github.com/drogue-iot/embedded-tls/commit/426f327) from [drogue-iot/embedded-tls](https://github.com/drogue-iot/embedded-tls). + +## License + +Apache-2.0 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..094eb2b --- /dev/null +++ b/src/asynch.rs @@ -0,0 +1,505 @@ +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::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::send_policy::FlushPolicy; +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::*; + +/// An async TLS 1.3 client stream wrapping an underlying async transport. +/// +/// Call [`open`](SecureStream::open) to perform the handshake before reading or writing. +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() + } + 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(), + } + } + + #[inline] + pub fn flush_policy(&self) -> FlushPolicy { + self.flush_policy + } + + #[inline] + pub fn set_flush_policy(&mut self, policy: FlushPolicy) { + self.flush_policy = policy; + } + + 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(()) + } + + pub async fn write(&mut self, buf: &[u8]) -> Result { + if self.is_opened() { + // Start a new ApplicationData record if none is in progress + 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) + } + } + + 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) + } + + 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) + } + + 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(()) + } + + 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(); + // Send a close_notify alert to signal clean shutdown (RFC 8446 §6.1) + 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 + } + + 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, + { + // Split requires a Clone socket so both halves can independently drive the same connection + 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) + } + + 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..f4f7603 --- /dev/null +++ b/src/blocking.rs @@ -0,0 +1,493 @@ +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::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::send_policy::FlushPolicy; +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::*; + +/// Blocking TLS 1.3 client stream wrapping a synchronous transport. +/// +/// Call [`open`](SecureStream::open) to perform the handshake before reading or writing. +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() + } + + 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(), + } + } + + #[inline] + pub fn flush_policy(&self) -> FlushPolicy { + self.flush_policy + } + + #[inline] + pub fn set_flush_policy(&mut self, policy: FlushPolicy) { + self.flush_policy = policy; + } + + 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(()) + } + + pub fn write(&mut self, buf: &[u8]) -> Result { + if self.is_opened() { + // Start a new ApplicationData record if none is in progress + 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) + } + } + + 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) + } + + 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) + } + + 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(); + // Send a close_notify alert to signal clean shutdown (RFC 8446 §6.1) + 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(()) + } + + 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) + } + + 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..87e0690 --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,300 @@ +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; + 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..ecb1ccc --- /dev/null +++ b/src/cert_verify.rs @@ -0,0 +1,279 @@ +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> { + #[allow(clippy::match_same_arms)] + match self { + SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::EcdsaSecp256r1Sha256 => Ok(&webpki::ECDSA_P256_SHA256), + SignatureScheme::EcdsaSecp384r1Sha384 => Ok(&webpki::ECDSA_P384_SHA384), + SignatureScheme::EcdsaSecp521r1Sha512 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::RsaPssRsaeSha256 + | SignatureScheme::RsaPssRsaeSha384 + | SignatureScheme::RsaPssRsaeSha512 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::Ed25519 => Ok(&webpki::ED25519), + SignatureScheme::Ed448 + | SignatureScheme::Sha224Ecdsa + | SignatureScheme::Sha224Rsa + | SignatureScheme::Sha224Dsa => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::RsaPssPssSha256 + | SignatureScheme::RsaPssPssSha384 + | SignatureScheme::RsaPssPssSha512 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::RsaPkcs1Sha1 | SignatureScheme::EcdsaSha1 => { + Err(ProtocolError::InvalidSignatureScheme) + } + + SignatureScheme::MlDsa44 | SignatureScheme::MlDsa65 | SignatureScheme::MlDsa87 => { + Err(ProtocolError::InvalidSignatureScheme) + } + + 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), + + SignatureScheme::EcdsaSecp256r1Sha256 => Ok(&webpki::ECDSA_P256_SHA256), + SignatureScheme::EcdsaSecp384r1Sha384 => Ok(&webpki::ECDSA_P384_SHA384), + SignatureScheme::EcdsaSecp521r1Sha512 => Err(ProtocolError::InvalidSignatureScheme), + + 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), + + 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), + + SignatureScheme::RsaPssPssSha256 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::RsaPssPssSha384 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::RsaPssPssSha512 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::RsaPkcs1Sha1 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::EcdsaSha1 => Err(ProtocolError::InvalidSignatureScheme), + + SignatureScheme::MlDsa44 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::MlDsa65 => Err(ProtocolError::InvalidSignatureScheme), + SignatureScheme::MlDsa87 => Err(ProtocolError::InvalidSignatureScheme), + + 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() { + 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() { + 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 { + 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..c276b7e --- /dev/null +++ b/src/certificate.rs @@ -0,0 +1,157 @@ +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 { + V1 = 0, + V2 = 1, + 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..9386b0f --- /dev/null +++ b/src/change_cipher_spec.rs @@ -0,0 +1,35 @@ +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)] +impl ChangeCipherSpec { + pub fn new() -> Self { + Self {} + } + + pub fn read(_rx_buf: &mut [u8]) -> Result { + 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..f6cfc19 --- /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)] +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..8083637 --- /dev/null +++ b/src/common/decrypted_read_handler.rs @@ -0,0 +1,52 @@ +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 { 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(_)) => 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..f585f1c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,382 @@ +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; + +/// Extra bytes required per record for the TLS 1.3 header, authentication tag, and inner content type. +pub const TLS_RECORD_OVERHEAD: usize = 128; + +type LongestLabel = U12; +type LabelOverhead = U10; +type LabelBuffer = Sum< + <::Hash as OutputSizeUser>::OutputSize, + Sum, +>; + +/// Associates a cipher, key/IV lengths, hash algorithm, and label buffer size for 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; +} + +/// Certificate and server-identity verification interface. Implement to enforce PKI validation. +pub trait Verifier +where + CipherSuite: TlsCipherSuite, +{ + fn set_hostname_verification(&mut self, hostname: &str) -> Result<(), crate::ProtocolError>; + + fn verify_certificate( + &mut self, + transcript: &CipherSuite::Hash, + cert: CertificateRef, + ) -> Result<(), ProtocolError>; + + fn verify_signature(&mut self, verify: HandshakeVerifyRef) -> Result<(), crate::ProtocolError>; +} + +/// A [`Verifier`] that accepts any certificate without validation. Useful for testing only. +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(()) + } +} + +/// Configuration for a single TLS client connection: server name, PSK, cipher preferences, etc. +#[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]]>, + // PSK value and the list of identity labels to offer in the ClientHello + 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 + } +} + +/// Provides the RNG, cipher suite, optional certificate verifier, and optional client signing key. +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) + } + + fn signer( + &mut self, + ) -> Result<(impl signature::SignerMut, SignatureScheme), crate::ProtocolError> + { + Err::<(NoSign, _), crate::ProtocolError>(crate::ProtocolError::Unimplemented) + } + + 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!() + } +} + +/// A [`CryptoBackend`] that skips certificate verification. Suitable for testing or constrained environments. +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> { + #[must_use] + pub fn with_priv_key(mut self, priv_key: &'a [u8]) -> Self { + self.priv_key = Some(priv_key); + self + } + + #[must_use] + 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, +{ + 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, + }; + + // RSA signature schemes are disabled by default to save code size; opt in via `alloc` feature + 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 + } + + 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 + } + + pub fn with_alpn(mut self, protocols: &'a [&'a [u8]]) -> Self { + self.alpn_protocols = Some(protocols); + self + } + + pub fn with_max_fragment_length(mut self, max_fragment_length: MaxFragmentLength) -> Self { + self.max_fragment_length = Some(max_fragment_length); + self + } + + 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 { + 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..b49eb2a --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,643 @@ +use crate::config::{ConnectConfig, TlsCipherSuite}; +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::{CryptoBackend, HandshakeVerify, 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}; + +// Decrypts an ApplicationData record in-place, then dispatches the inner content type to `cb`. +// Plaintext records (Handshake, ChangeCipherSpec) are forwarded to `cb` without decryption. +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)?; + + // Strip TLS 1.3 inner-content padding: trailing zero bytes before the real content type + let padding = app_data + .as_slice() + .iter() + .enumerate() + .rfind(|(_, b)| **b != 0); + if let Some((index, _)) = padding { + app_data.truncate(index + 1); + } + + // The last byte of the decrypted payload is the actual ContentType (RFC 8446 §5.4) + let content_type = ContentType::of(*app_data.as_slice().last().unwrap()) + .ok_or(ProtocolError::InvalidRecord)?; + + trace!("Decrypting: content type = {:?}", content_type); + + app_data.truncate(app_data.len() - 1); + + let mut buf = ParseBuffer::new(app_data.as_slice()); + match content_type { + ContentType::Handshake => { + 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(); + 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(); + // Additional data is the TLS record header (type=ApplicationData, legacy version 0x0303, length) + 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) +} + +/// Ephemeral state held between handshake steps — discarded once the handshake completes. +pub struct Handshake +where + CipherSuite: TlsCipherSuite, +{ + // Saved pre-master transcript hash used for Finished after a certificate exchange + 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, + } + } +} + +/// TLS handshake state machine. Drives the client through all stages of the TLS 1.3 handshake. +#[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); + } + + // If the server sent a CertificateRequest we must respond with a cert before Finished + 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; + + 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)) => { + // CertificateVerify message format: 64 spaces + context string + \0 + transcript hash (RFC 8446 §4.4.3) + let ctx_str = b"TLS 1.3, client CertificateVerify\x00"; + + 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, +{ + // Restore the transcript hash captured before the client cert exchange, then derive app traffic secrets + 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..2624e3a --- /dev/null +++ b/src/extensions/extension_data/alpn.rs @@ -0,0 +1,39 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +#[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 { + 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> { + 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..ddcff81 --- /dev/null +++ b/src/extensions/extension_data/key_share.rs @@ -0,0 +1,128 @@ +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, 0x00, 0x00]; + 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, 0x00, 0x02, 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..ebcdfb3 --- /dev/null +++ b/src/extensions/extension_data/max_fragment_length.rs @@ -0,0 +1,34 @@ +use crate::{ + ProtocolError, + buffer::CryptoBuffer, + parse_buffer::{ParseBuffer, ParseError}, +}; + +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaxFragmentLength { + Bits9 = 1, + Bits10 = 2, + Bits11 = 3, + 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..89836c1 --- /dev/null +++ b/src/extensions/extension_data/pre_shared_key.rs @@ -0,0 +1,61 @@ +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)?; + + buf.push_u32(0).map_err(|_| ProtocolError::EncodeError)?; + } + Ok(()) + }) + .map_err(|_| ProtocolError::EncodeError)?; + + 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..bbe22fe --- /dev/null +++ b/src/extensions/extension_data/server_name.rs @@ -0,0 +1,124 @@ +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(); + + 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(()) + }) + } +} + +#[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..6471e2b --- /dev/null +++ b/src/extensions/extension_data/signature_algorithms.rs @@ -0,0 +1,153 @@ +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 { + RsaPkcs1Sha256, + RsaPkcs1Sha384, + RsaPkcs1Sha512, + + EcdsaSecp256r1Sha256, + EcdsaSecp384r1Sha384, + EcdsaSecp521r1Sha512, + + RsaPssRsaeSha256, + RsaPssRsaeSha384, + RsaPssRsaeSha512, + + Ed25519, + Ed448, + + RsaPssPssSha256, + RsaPssPssSha384, + RsaPssPssSha512, + + Sha224Ecdsa, + Sha224Rsa, + Sha224Dsa, + + RsaPkcs1Sha1, + EcdsaSha1, + + Sha256BrainpoolP256r1, + Sha384BrainpoolP384r1, + Sha512BrainpoolP512r1, + + MlDsa44, + MlDsa65, + MlDsa87, +} + +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..db9ea8e --- /dev/null +++ b/src/extensions/extension_data/supported_groups.rs @@ -0,0 +1,101 @@ +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 { + Secp256r1, + Secp384r1, + Secp521r1, + X25519, + X448, + + Ffdhe2048, + Ffdhe3072, + Ffdhe4096, + Ffdhe6144, + Ffdhe8192, + + 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..a370b49 --- /dev/null +++ b/src/extensions/extension_group_macro.rs @@ -0,0 +1,93 @@ +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)] + pub enum $name$(<$lt>)? { + $($extension($extension_data)),+ + } + + #[allow(dead_code)] + 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 { + 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); + 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) => { + } + Err(err) => return Err(err), + } + } + + trace!("Read {} extensions", extensions.len()); + Ok(extensions) + } + } + }; +} + +pub(crate) use extension_group; diff --git a/src/extensions/messages.rs b/src/extensions/messages.rs new file mode 100644 index 0000000..e2e6e25 --- /dev/null +++ b/src/extensions/messages.rs @@ -0,0 +1,99 @@ +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, +}; + +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>) + } +} + +extension_group! { + pub enum ServerHelloExtension<'a> { + KeyShare(KeyShareServerHello<'a>), + PreSharedKey(PreSharedKeyServerHello), + Cookie(Unimplemented<'a>), + SupportedVersions(SupportedVersionsServerHello) + } +} + +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>) + } +} + +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>) + } +} + +extension_group! { + pub enum CertificateExtension<'a> { + StatusRequest(Unimplemented<'a>), + SignedCertificateTimestamp(Unimplemented<'a>) + } +} + +extension_group! { + pub enum NewSessionTicketExtension<'a> { + EarlyData(Unimplemented<'a>) + } +} + +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..f21d9c3 --- /dev/null +++ b/src/extensions/mod.rs @@ -0,0 +1,81 @@ +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..0d44a8e --- /dev/null +++ b/src/handshake/binder.rs @@ -0,0 +1,36 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use core::fmt::{Debug, Formatter}; +use generic_array::{ArrayLength, GenericArray}; + +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.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..3efbeb4 --- /dev/null +++ b/src/handshake/certificate.rs @@ -0,0 +1,176 @@ +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> { + #[must_use] + 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()); + + 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?"); + } + CertificateEntryRef::X509(cert) => { + buf.with_u24_length(|buf| buf.extend_from_slice(cert))?; + } + } + + 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..32b1a38 --- /dev/null +++ b/src/handshake/certificate_request.rs @@ -0,0 +1,49 @@ +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)?; + + 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..2c299b3 --- /dev/null +++ b/src/handshake/certificate_verify.rs @@ -0,0 +1,51 @@ +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(), + }) + } +} + +#[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..8609b99 --- /dev/null +++ b/src/handshake/client_hello.rs @@ -0,0 +1,165 @@ +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::{ConnectConfig, TlsCipherSuite}; +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)?; + + // Empty legacy session ID — TLS 1.3 doesn't use it, but the field must be present + buf.push(0).map_err(|_| ProtocolError::EncodeError)?; + + // Exactly one cipher suite entry (2-byte length prefix + 2-byte code point) + buf.push_u16(2).map_err(|_| ProtocolError::EncodeError)?; + buf.push_u16(CipherSuite::CODE_POINT) + .map_err(|_| ProtocolError::EncodeError)?; + + // Legacy compression methods: one entry, 0x00 = no compression + buf.push(1).map_err(|_| ProtocolError::EncodeError)?; + buf.push(0).map_err(|_| ProtocolError::EncodeError)?; + + buf.with_u16_length(|buf| { + 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)?; + } + + 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> { + if let Some((_, identities)) = &self.config.psk { + // PSK binders depend on the transcript up to (but not including) the binder values, + // so we hash the partial message, compute binders, then hash the remainder (RFC 8446 §4.2.11.2) + let binders_len = identities.len() * (1 + HashOutputSize::::to_usize()); + + let binders_pos = enc_buf.len() - binders_len; + + transcript.update(&enc_buf[0..binders_pos - 2]); + + let mut buf = CryptoBuffer::wrap(&mut enc_buf[binders_pos..]); + 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..c1e13ea --- /dev/null +++ b/src/handshake/finished.rs @@ -0,0 +1,43 @@ +use crate::ProtocolError; +use crate::buffer::CryptoBuffer; +use crate::parse_buffer::ParseBuffer; +use core::fmt::{Debug, Formatter}; +use generic_array::{ArrayLength, GenericArray}; + +/// TLS Finished message: contains an HMAC over the handshake transcript (RFC 8446 §4.4.4). +/// +/// `hash` holds the transcript hash snapshot taken just before this message was received; +/// it is `None` when the struct is used for a locally-generated Finished message. +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 { + let mut verify = GenericArray::default(); + buf.fill(&mut verify)?; + Ok(Self { verify, hash: None }) + } + + pub(crate) fn encode(&self, buf: &mut CryptoBuffer<'_>) -> Result<(), ProtocolError> { + 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..333c136 --- /dev/null +++ b/src/handshake/mod.rs @@ -0,0 +1,242 @@ +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; + +// TLS legacy_record_version field — always 0x0303 for TLS 1.3 compatibility (RFC 8446 §5.1) +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)?; + + // Handshake message body is preceded by a 3-byte (u24) length (RFC 8446 §4) + 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(); + + // Capture the current transcript hash into Finished before we update it with this message + 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::ServerHello => ServerHandshake::ServerHello(ServerHello::parse(buf)?), + HandshakeType::NewSessionTicket => { + ServerHandshake::NewSessionTicket(NewSessionTicket::parse(buf)?) + } + 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)?) + } + 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..1880c76 --- /dev/null +++ b/src/handshake/server_hello.rs @@ -0,0 +1,80 @@ +use heapless::Vec; + +use crate::cipher::CryptoEngine; +use crate::cipher_suites::CipherSuite; +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> { + // legacy_version is always 0x0303 in TLS 1.3; actual version is negotiated via extensions + 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)?; + + // Legacy session ID echo: TLS 1.3 servers echo the client's session ID for middlebox compatibility + let session_id = buf + .slice(session_id_length as usize) + .map_err(|_| ProtocolError::InvalidSessionIdLength)?; + + let cipher_suite = + CipherSuite::parse(buf).map_err(|_| ProtocolError::InvalidCipherSuite)?; + + // compression_method: always 0x00 in TLS 1.3 + buf.read_u8()?; + + let extensions = ServerHelloExtension::parse_vector(buf)?; + + 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 + } + }) + } + + /// Performs ECDH with the server's key share to derive the shared secret used in the handshake. + 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..0bf464c --- /dev/null +++ b/src/key_schedule.rs @@ -0,0 +1,485 @@ +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>; +/// Hash-sized byte array, used as the HKDF secret at each key schedule stage. +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), + } + } + + // HKDF-Expand-Label as defined in RFC 8446 §7.1 + fn make_expanded_hkdf_label>( + &self, + label: &[u8], + context_type: ContextType, + ) -> Result, ProtocolError> { + let mut hkdf_label = heapless_typenum::Vec::>::new(); + // Length field: desired output length as u16 big-endian + hkdf_label + .extend_from_slice(&N::to_u16().to_be_bytes()) + .map_err(|()| ProtocolError::InternalError)?; + + // TLS 1.3 labels are prefixed with "tls13 " (6 bytes) + 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(); + self.as_ref()? + .expand(&hkdf_label, &mut okm) + .map_err(|_| ProtocolError::CryptoError)?; + 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, + } + } + + // HKDF-Extract, chaining the previous stage's secret as the salt (RFC 8446 §7.1) + 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) + } + + // "Derive-Secret(Secret, "derived", "")" — used to chain stages per RFC 8446 §7.1 + 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) -> &KeyArray { + &self.key + } + + #[inline] + pub fn get_iv(&self) -> &IvArray { + &self.iv + } + + pub fn get_nonce(&self) -> IvArray { + let iv = self.get_iv(); + 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); + // Derive per-record key and IV from the traffic secret (RFC 8446 §7.3) + 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 { + // Per-record nonce: XOR the static IV with the zero-padded sequence counter (RFC 8446 §5.3) + let counter = Self::pad::(&counter.to_be_bytes()); + + 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; + } + + nonce + } + + // Right-aligns `input` bytes in a zero-padded array of length N (big-endian padding) + fn pad>(input: &[u8]) -> GenericArray { + let mut padded = GenericArray::default(); + for (index, byte) in input.iter().rev().enumerate() { + padded[(N::to_usize() - index) - 1] = *byte; + } + padded + } + + fn zero() -> HashArray { + GenericArray::default() + } + + pub fn initialize_early_secret(&mut self, psk: Option<&[u8]>) -> Result<(), ProtocolError> { + // IKM is 0-bytes when no PSK is used — still required to derive the binder key + 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> { + // IKM is all-zeros at the master secret stage (RFC 8446 §7.1) + self.shared.initialize(Self::zero().as_slice()); + + 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) -> &KeyArray { + self.state.get_key() + } + + pub(crate) fn get_nonce(&self) -> IvArray { + 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) -> &KeyArray { + self.state.get_key() + } + + pub(crate) fn get_nonce(&self) -> IvArray { + self.state.get_nonce() + } + + pub fn verify_server_finished( + &self, + finished: &Finished>, + ) -> Result { + let key = self + .state + .traffic_secret + .make_expanded_hkdf_label::>( + b"finished", + ContextType::None, + )?; + 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 + })?, + ); + Ok(hmac.verify(&finished.verify).is_ok()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ac4669d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,126 @@ +#![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 +)] + +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; +mod cipher_suites; +mod common; +mod config; +mod connection; +mod content_types; +mod extensions; +mod handshake; +mod key_schedule; +mod parse_buffer; +pub mod read_buffer; +mod record; +mod record_reader; +pub mod send_policy; +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(), + ) + } + } +} + +fn unused(_: T) {} diff --git a/src/native_pki.rs b/src/native_pki.rs new file mode 100644 index 0000000..a4746cd --- /dev/null +++ b/src/native_pki.rs @@ -0,0 +1,468 @@ +use crate::ProtocolError; +#[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::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 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 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 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)?; + + 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..f8b6f74 --- /dev/null +++ b/src/parse_buffer.rs @@ -0,0 +1,171 @@ +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 { + 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(); + 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..13118a3 --- /dev/null +++ b/src/read_buffer.rs @@ -0,0 +1,171 @@ +#[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 + } + + #[inline] + pub fn peek(&mut self, count: usize) -> &'a [u8] { + let count = self.len().min(count); + let start = self.consumed; + + self.used = true; + + &self.data[start..start + count] + } + + #[inline] + pub fn peek_all(&mut self) -> &'a [u8] { + self.peek(self.len()) + } + + #[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] + } + + #[inline] + pub fn pop_all(&mut self) -> &'a [u8] { + self.pop(self.len()) + } + + #[inline] + pub fn revert(self) { + core::mem::forget(self); + } + + #[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 { + 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..7ec7fa2 --- /dev/null +++ b/src/record.rs @@ -0,0 +1,215 @@ +use crate::ProtocolError; +use crate::application_data::ApplicationData; +use crate::change_cipher_spec::ChangeCipherSpec; +use crate::config::{ConnectConfig, TlsCipherSuite}; +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 + 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 + 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 { + unwrap!(ContentType::of(self.header[0])) + } + + pub fn content_length(&self) -> usize { + 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, + ))) + } + } + } +} diff --git a/src/record_reader.rs b/src/record_reader.rs new file mode 100644 index 0000000..52d9210 --- /dev/null +++ b/src/record_reader.rs @@ -0,0 +1,471 @@ +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}, +}; + +/// Stateful reader that reassembles TLS records from a byte stream into the shared receive buffer. +/// +/// `decoded` tracks how many bytes at the start of `buf` have already been handed to the caller; +/// `pending` tracks bytes that have been read from the transport but not yet consumed as a record. +pub struct RecordReader<'a> { + pub(crate) buf: &'a mut [u8], + decoded: usize, + pending: usize, +} + +pub struct RecordReaderBorrowMut<'a> { + pub(crate) buf: &'a mut [u8], + decoded: &'a mut usize, + pending: &'a mut usize, +} + +impl<'a> RecordReader<'a> { + pub fn new(buf: &'a mut [u8]) -> Self { + // TLS 1.3 max plaintext record is 16 384 bytes + 256 bytes overhead = 16 640 bytes + 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 the next record would overflow the end of the buffer, rotate unconsumed bytes to the front + 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( + &[ + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x04, + 0xde, + 0xad, + 0xbe, + 0xef, + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x02, + 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 = [ + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x04, + 0xde, + 0xad, + 0xbe, + 0xef, + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x02, + 0xaa, + 0xbb, + ] + .as_slice(); + + let mut buf = [0; 4]; + 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 = [ + ContentType::ApplicationData as u8, + 0x03, + 0x03, + 0x00, + 0x00, + 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..59bf22d --- /dev/null +++ b/src/send_policy.rs @@ -0,0 +1,20 @@ +/// Controls whether `flush()` calls also flush the underlying transport. +/// +/// `Strict` (the default) ensures bytes reach the network immediately after every record. +/// `Relaxed` leaves transport flushing to the caller, which can reduce syscall overhead. +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlushPolicy { + /// Only encrypt and hand bytes to the transport; do not call `transport.flush()`. + Relaxed, + + /// Call `transport.flush()` after writing each TLS record. + #[default] + Strict, +} + +impl FlushPolicy { + #[must_use] + pub fn flush_transport(&self) -> bool { + matches!(self, Self::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..b0983a7 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,373 @@ +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..8332cfe --- /dev/null +++ b/tests/fixtures/chain.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIBvjCCAWSgAwIBAgIUXXNrQ1jydxm9uhK7n0OmDUOPiCUwCgYIKoZIzj0EAwIw +WzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRswGQYDVQQDDBJUZXN0SW50ZXJtZWRpYXRlQ0EwHhcN +MjYwMjIxMDgzODU4WhcNMzYwMjE5MDgzODU4WjAUMRIwEAYDVQQDDAlsb2NhbGhv +c3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAStUYteJnlqMQQaVt8Y3GY92A4E +/E4/9tB9Y5w9OniFssXUzubEhFyWRUMzF/0plx3Q1LJpEFi+PHOUOMZIUplHo00w +SzAJBgNVHRMEAjAAMB0GA1UdDgQWBBTmipGtVpxknBpCkG5VwqycrALvuzAfBgNV +HSMEGDAWgBTRmjFBdHm/8OSJJ4GuT+NLNY6LsDAKBggqhkjOPQQDAgNIADBFAiB2 +ZslSroEj+F/JaLSbNNMTTiRzIeP8jz6Lw+18bWo0agIhAK6gPc06G4rghYixJrWI +348sgPEgLNyoDXPAIN+EabvM +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICAjCCAamgAwIBAgIUaNQANP0JbqEIRANchWyHObCtwycwCgYIKoZIzj0EAwIw +UzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApUZXN0Um9vdENBMB4XDTI2MDIyMTA4 +Mzg1N1oXDTM2MDIxOTA4Mzg1N1owWzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0 +YXRlMQ0wCwYDVQQHDARDaXR5MRAwDgYDVQQKDAdUZXN0T3JnMRswGQYDVQQDDBJU +ZXN0SW50ZXJtZWRpYXRlQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASrQQZ/ +9Om99T1hcWWBLRceIiPBy5y8AwzeG/30E+CipqpXcGfJJj6b9riPDueOnTbMhRH8 +BmNgJAZBXvKEQd4Po1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTRmjFB +dHm/8OSJJ4GuT+NLNY6LsDAfBgNVHSMEGDAWgBRkhR8ezN8mKycuLoqDnfSBhZid +mzAKBggqhkjOPQQDAgNHADBEAiB5ISXZkzQlbgZZ7q8vpWTb9Cfxx3rpKBg6IfIg +NKm5lgIgJhsTXYO4tWz3UfWDXub2NtXoDGXwMvTsE4UXDRDO15E= +-----END CERTIFICATE----- diff --git a/tests/fixtures/intermediate-ca-key.pem b/tests/fixtures/intermediate-ca-key.pem new file mode 100644 index 0000000..b5a8cc0 --- /dev/null +++ b/tests/fixtures/intermediate-ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIC94ig/ME0X0FOyu8Byk4tDkAdd/LmuUk3KjalEk/7ImoAoGCCqGSM49 +AwEHoUQDQgAEq0EGf/TpvfU9YXFlgS0XHiIjwcucvAMM3hv99BPgoqaqV3BnySY+ +m/a4jw7njp02zIUR/AZjYCQGQV7yhEHeDw== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/intermediate-ca.pem b/tests/fixtures/intermediate-ca.pem new file mode 100644 index 0000000..5e06543 --- /dev/null +++ b/tests/fixtures/intermediate-ca.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICAjCCAamgAwIBAgIUaNQANP0JbqEIRANchWyHObCtwycwCgYIKoZIzj0EAwIw +UzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApUZXN0Um9vdENBMB4XDTI2MDIyMTA4 +Mzg1N1oXDTM2MDIxOTA4Mzg1N1owWzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0 +YXRlMQ0wCwYDVQQHDARDaXR5MRAwDgYDVQQKDAdUZXN0T3JnMRswGQYDVQQDDBJU +ZXN0SW50ZXJtZWRpYXRlQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASrQQZ/ +9Om99T1hcWWBLRceIiPBy5y8AwzeG/30E+CipqpXcGfJJj6b9riPDueOnTbMhRH8 +BmNgJAZBXvKEQd4Po1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTRmjFB +dHm/8OSJJ4GuT+NLNY6LsDAfBgNVHSMEGDAWgBRkhR8ezN8mKycuLoqDnfSBhZid +mzAKBggqhkjOPQQDAgNHADBEAiB5ISXZkzQlbgZZ7q8vpWTb9Cfxx3rpKBg6IfIg +NKm5lgIgJhsTXYO4tWz3UfWDXub2NtXoDGXwMvTsE4UXDRDO15E= +-----END CERTIFICATE----- diff --git a/tests/fixtures/intermediate-ca.srl b/tests/fixtures/intermediate-ca.srl new file mode 100644 index 0000000..d0b675b --- /dev/null +++ b/tests/fixtures/intermediate-ca.srl @@ -0,0 +1 @@ +5D736B4358F27719BDBA12BB9F43A60D438F8825 diff --git a/tests/fixtures/intermediate-server-key.pem b/tests/fixtures/intermediate-server-key.pem new file mode 100644 index 0000000..6378df8 --- /dev/null +++ b/tests/fixtures/intermediate-server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINVUisAFH2zQI1zMfTTQ5Imx89obUgjLnhR1LNKR7xCGoAoGCCqGSM49 +AwEHoUQDQgAErVGLXiZ5ajEEGlbfGNxmPdgOBPxOP/bQfWOcPTp4hbLF1M7mxIRc +lkVDMxf9KZcd0NSyaRBYvjxzlDjGSFKZRw== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/intermediate-server.pem b/tests/fixtures/intermediate-server.pem new file mode 100644 index 0000000..25ba1f8 --- /dev/null +++ b/tests/fixtures/intermediate-server.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBvjCCAWSgAwIBAgIUXXNrQ1jydxm9uhK7n0OmDUOPiCUwCgYIKoZIzj0EAwIw +WzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRswGQYDVQQDDBJUZXN0SW50ZXJtZWRpYXRlQ0EwHhcN +MjYwMjIxMDgzODU4WhcNMzYwMjE5MDgzODU4WjAUMRIwEAYDVQQDDAlsb2NhbGhv +c3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAStUYteJnlqMQQaVt8Y3GY92A4E +/E4/9tB9Y5w9OniFssXUzubEhFyWRUMzF/0plx3Q1LJpEFi+PHOUOMZIUplHo00w +SzAJBgNVHRMEAjAAMB0GA1UdDgQWBBTmipGtVpxknBpCkG5VwqycrALvuzAfBgNV +HSMEGDAWgBTRmjFBdHm/8OSJJ4GuT+NLNY6LsDAKBggqhkjOPQQDAgNIADBFAiB2 +ZslSroEj+F/JaLSbNNMTTiRzIeP8jz6Lw+18bWo0agIhAK6gPc06G4rghYixJrWI +348sgPEgLNyoDXPAIN+EabvM +-----END CERTIFICATE----- diff --git a/tests/fixtures/leaf-client-key.pem b/tests/fixtures/leaf-client-key.pem new file mode 100644 index 0000000..099555f --- /dev/null +++ b/tests/fixtures/leaf-client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICyVJZJWFmk9fLUocyUgG1Vy3DN0tEK2C2akRjxu/9uUoAoGCCqGSM49 +AwEHoUQDQgAESmy9Dc5o67THYEEMOhf55AtrPfE/b9oECoHxsE08kAYiEhDNHF3b +fHAsG/8o8K0+D/nZBiHSVz7qOJEAYtI38g== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/leaf-client.pem b/tests/fixtures/leaf-client.pem new file mode 100644 index 0000000..bb15227 --- /dev/null +++ b/tests/fixtures/leaf-client.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9DCCAZugAwIBAgIUaNQANP0JbqEIRANchWyHObCtwykwCgYIKoZIzj0EAwIw +UzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApUZXN0Um9vdENBMB4XDTI2MDIyMTA4 +Mzg1OFoXDTM2MDIxOTA4Mzg1OFowUzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0 +YXRlMQ0wCwYDVQQHDARDaXR5MRAwDgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApU +ZXN0Q2xpZW50MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESmy9Dc5o67THYEEM +Ohf55AtrPfE/b9oECoHxsE08kAYiEhDNHF3bfHAsG/8o8K0+D/nZBiHSVz7qOJEA +YtI38qNNMEswCQYDVR0TBAIwADAdBgNVHQ4EFgQUmnmdVS/JiP0+xsswPzbU0Q5T +qxYwHwYDVR0jBBgwFoAUZIUfHszfJisnLi6Kg530gYWYnZswCgYIKoZIzj0EAwID +RwAwRAIgfuTkH23Somj9TonLSQScjTlX3wQOmnmK0tnopK81SdQCIHwFLvOtatXW +oYwcJFlV9VnsdM1Kl+XqxWotTKNGgGHQ +-----END CERTIFICATE----- diff --git a/tests/fixtures/leaf-server-key.pem b/tests/fixtures/leaf-server-key.pem new file mode 100644 index 0000000..75915e3 --- /dev/null +++ b/tests/fixtures/leaf-server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINmGVq3b5UlwxwkUApMi+qlCUXptr279ZWzkPuY06h8koAoGCCqGSM49 +AwEHoUQDQgAEzZQXKPiuG7YT3j8zFOz4SNJqoKWw8gKivlVqVpBNLcOvcfkrzFvd +vvT4FiaTUWxjNsiXDT1mj35Gbeip1HLabw== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/leaf-server.pem b/tests/fixtures/leaf-server.pem new file mode 100644 index 0000000..80f13e6 --- /dev/null +++ b/tests/fixtures/leaf-server.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIUaNQANP0JbqEIRANchWyHObCtwygwCgYIKoZIzj0EAwIw +UzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApUZXN0Um9vdENBMB4XDTI2MDIyMTA4 +Mzg1OFoXDTM2MDIxOTA4Mzg1OFowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEzZQXKPiuG7YT3j8zFOz4SNJqoKWw8gKivlVq +VpBNLcOvcfkrzFvdvvT4FiaTUWxjNsiXDT1mj35Gbeip1HLab6NNMEswCQYDVR0T +BAIwADAdBgNVHQ4EFgQUSvq+CjEju6CTX5ySu6YiKDTuCVswHwYDVR0jBBgwFoAU +ZIUfHszfJisnLi6Kg530gYWYnZswCgYIKoZIzj0EAwIDSAAwRQIgWzKWT9G3NwTM +wi0DJ9S+34oyL2WU7h+nPgJ0/ZFPR80CIQDx9pppEpipI83lB5A8wAB6Vi/Kugf9 +ZL3JtaYEl1gJzQ== +-----END CERTIFICATE----- diff --git a/tests/fixtures/root-ca-key.pem b/tests/fixtures/root-ca-key.pem new file mode 100644 index 0000000..c6558e8 --- /dev/null +++ b/tests/fixtures/root-ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILDaSJAPexCKKVxUwA2Obha08yO3FVDlzXwRPk+HDm7EoAoGCCqGSM49 +AwEHoUQDQgAEddxFuwZN+JLJFTiSKPb9DJPOdbLFMPzz66JkcxB28da2r2DqQKTK +EmramKjIsI9WuXGY06XF1tYDxSfe7lTZZQ== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/root-ca.pem b/tests/fixtures/root-ca.pem new file mode 100644 index 0000000..bf40f3b --- /dev/null +++ b/tests/fixtures/root-ca.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB+zCCAaGgAwIBAgIUfXCGhET97fQO0Q5r972wMTrwA3gwCgYIKoZIzj0EAwIw +UzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAw +DgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApUZXN0Um9vdENBMB4XDTI2MDIyMTA4 +Mzg1N1oXDTM2MDIxOTA4Mzg1N1owUzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0 +YXRlMQ0wCwYDVQQHDARDaXR5MRAwDgYDVQQKDAdUZXN0T3JnMRMwEQYDVQQDDApU +ZXN0Um9vdENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEddxFuwZN+JLJFTiS +KPb9DJPOdbLFMPzz66JkcxB28da2r2DqQKTKEmramKjIsI9WuXGY06XF1tYDxSfe +7lTZZaNTMFEwHQYDVR0OBBYEFGSFHx7M3yYrJy4uioOd9IGFmJ2bMB8GA1UdIwQY +MBaAFGSFHx7M3yYrJy4uioOd9IGFmJ2bMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZI +zj0EAwIDSAAwRQIhAJrkXZrhS2zAexShCh/Dh47d1LtOvMiq9uqbgP7xI10pAiAF +fMNptczj+DpN9nRTlELt8FxC8rTxGxjC/tL0aOomDw== +-----END CERTIFICATE----- diff --git a/tests/fixtures/root-ca.srl b/tests/fixtures/root-ca.srl new file mode 100644 index 0000000..ff1dfd3 --- /dev/null +++ b/tests/fixtures/root-ca.srl @@ -0,0 +1 @@ +68D40034FD096EA10844035C856C8739B0ADC329 diff --git a/tests/fixtures/rsa-leaf-client-key.pem b/tests/fixtures/rsa-leaf-client-key.pem new file mode 100644 index 0000000..4b36983 --- /dev/null +++ b/tests/fixtures/rsa-leaf-client-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCPYjGfNwKAhIsD +Fepzgc57jcHso5dO/hfs5k4JTRhACoGDA+6QFm9KAb8YC1NFbcJsExJAfvVlSr3S +82WJqvQOH5LQ7TXPRaG0tAIrWoB76jYb+TQhmi6o0WepOxqDR6w64O3bM6geFR7A +zbhl2bzOYlZwF+tdCn3L41LpjhzcEVUM8M/IUaCbzexq/jUsRUpFdpYSvEFCKMSR +BvbV/I0/zMWo+jZXG/YQqbOhNmgQugypPlVIYQLtAVye+92qpajLFlg5RCvXmAtE +0KGl6nhHPNutqOUJGgmPVmx0fAj1IIjTRws++kIuRYgncxS9qtcblFVy4Ji0NdKu ++Z6LeyLZAgMBAAECggEAA8wP8mtK9MUSFG+sLTxxfretka09IaqR128Q/2Ko+5Qe +2MdYk+eKD0joONvA/RaKd0FdX3muUHWzSbBuf7dT57mT/wDmgonef/3jL56qPjMt +rsqyu1ytObhrSR+oeOudSg/huXp3bmVQluC70JGewwkZDNiPc0Qo5OJo1mY6wxex +dps0A4fCmZtB9ZgmaejCiQaw7udgcpOGo5xdfVrgAtAqCBSaWOoZWNHzjsntSrHO +fRDa4ilLhwuWwrkIsg5om/zasfTDLM70YyXq4s0ccYyCEPkquPgN/mII/lK+78U6 +AXYfsvSfyVNRoMzMXdTKC+G1QlYDGWeqAoVCS9TyawKBgQDC1ihsohh6PSjYHS7Y ++QODmDXEbwXU/V5iEnFKf4/9+QSW+g12mbckjHS+njvNV4wzdx/ph+bNDAdI8bQU +Dr5bJXtGbHy1r8/PtNUP4SJmoRvmMVrr34g7QjaHYJ8Jy2m9JHpuRnr38pzg6vP6 +deDtxupT2xPTqV7ivwUDLK9SKwKBgQC8ZRGp2Ny17cH9aKnyjibXLKvtITdDc7H9 +kqs+LM3bcEXpXs0o5ED7J0uhpbNkpplV95Lcm7zdDAkHHiek4yWBUohtgqfbZ76d +vA+BtnbjDdCyH8B5mN5IDc1zEIJZB1x9CpIhIsDRrBCH/NXl3ldJLBF/ZCJTXWCf +nqRgwllRCwKBgQC4k7W8JFvYAfSduBfXiSAhHKNjMmJuApHViu80ymAZFD2a4cy7 +XKg5wa4fnzu8LoIth17+F7c47XpBSml0zvra0klU0BXc8W+HsCJgZsH2RA5wJrWh +2yPuL64E1i4UU1Yaz2IE8lQwbPDdyvfTgLTTzavUQSkpTb0MRjZzaXO1/QKBgG8X +0mCr5wrJF1nNfFnyBWlhiEifC62U7eKvuJdDaGj8Pd2t76EraD4yH+FEixLRQx50 +jX/VvntC+5fc6lfLMnSeLKEXKNCyzq7JFQPSiyy9GtHO83tA7+LhcMNnetXxB1Md +BqrPiZCavGzUZXXVtPcLK45JiAxMxguaSyhbsrudAoGAMjK2bcu7buE13KM3wbte +lysax9gxKKNlqFX68/qx4SUrHxhz5ugF95zlkiDciq/jG5TH1IGNX3q0C7Eid4FY +0XHwJAqBG5wFWnjx3U+Ozk2QYVXK6rfyk7drAWO2XlsAZVHwf6DlVhYCOT+a9O6W +60KKqnpWzmBnrc2fzOJgJTM= +-----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..ba56393 --- /dev/null +++ b/tests/fixtures/rsa-leaf-client.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIUPL2aFs+m1HMJy9P1zeCRHsgB/dUwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRAwDgYDVQQKDAdUZXN0T3JnMRYwFAYDVQQDDA1UZXN0UnNhUm9vdENBMB4XDTI2 +MDIyMTA4MzkwMFoXDTM2MDIxOTA4MzkwMFowUzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAwDgYDVQQKDAdUZXN0T3JnMRMwEQYD +VQQDDApUZXN0Q2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +j2IxnzcCgISLAxXqc4HOe43B7KOXTv4X7OZOCU0YQAqBgwPukBZvSgG/GAtTRW3C +bBMSQH71ZUq90vNliar0Dh+S0O01z0WhtLQCK1qAe+o2G/k0IZouqNFnqTsag0es +OuDt2zOoHhUewM24Zdm8zmJWcBfrXQp9y+NS6Y4c3BFVDPDPyFGgm83sav41LEVK +RXaWErxBQijEkQb21fyNP8zFqPo2Vxv2EKmzoTZoELoMqT5VSGEC7QFcnvvdqqWo +yxZYOUQr15gLRNChpep4RzzbrajlCRoJj1ZsdHwI9SCI00cLPvpCLkWIJ3MUvarX +G5RVcuCYtDXSrvmei3si2QIDAQABo00wSzAJBgNVHRMEAjAAMB0GA1UdDgQWBBQV +mkgyHQUCPp0zo8903lXsOeKE5DAfBgNVHSMEGDAWgBRHYiOHw458Ka7uZ2w7wkbk +WsD5MTANBgkqhkiG9w0BAQsFAAOCAQEAhrtvIHbrRPFc5QbaT/eelrAqWkhwpxxL +X/gVwbqtP2+uU2xUVGsfRJV5EI27kzyysq+ySdQLq3j8oe0X6poqDJiE8/zUg7KO +OYCkH/UGhr+L0hk6Ibvc7izu/LSFT3K5Mo78aB/C4cjDrLgA/cbFKT+OVQW4iw49 +2Pgw7+vuNLzYJSu2m5XzeR07FMcTpf2EAOGseDde6zFwduUBJtAbGj4YHkTQp+MD +dEE5ymyXl4+ehcafk0g4ZZARN5qVPMOqjRASWRhuZI+36Ihc6KBL22B1KzexyoYx +2sEyd04NU1PX9i5Zn7JXNbHIESNCkLk4IspSnk6DBEpNpQgEW6l3Cw== +-----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..c781277 --- /dev/null +++ b/tests/fixtures/rsa-leaf-server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCjK6TkX5PuTApT +uL5Z0p6Iofo2zXHOPJVcDDeKzVW83KRwK9oFhjIT9HHKRFkar5ezKQuKY+xtsvmz +JX2STGwg1itPIcHpngwvmtAy4/2lBB0wL1wxc+2sqpWYGmqLm5iqiuaVQo2n0uiL +Nwd4aZzpjtRXWjyr9sjfBcXu6/m3bN6nYkmh613qu9BAo4L1YPy+NFtVhKkBzD9Q +qPDZv7mVoqe+Y3Cyf8873+5T6PVrpV7+YCpFJA46fsA/UrmsyQfQPCOL5fbbLXqs +V9yMf2/7JCCbrO+6mpIJaEWnciS51sEIh15oqH7IiF5+6az/Oe11X/LbWHkLRUzr +k4uYGV61AgMBAAECggEABZz2Pq71CVEPV+L2lWNz9bJQx7rYi+Y0oyZ+cKVwqh8S +/xLbHK6JoXsawQEJ6auZtd2XGosmcn2iLmH/SF2dqKGFeuLn50/7DlYujFmge6FB +Gcu/Sao5xmNV4xYhjSzsmw1NMlxIQDo2qrdZZ/CGJ9i0gE7H4IiMT3PE49u1SvSD +OkKmcZW3suPYyZGPZxl2h37cARIjqsmw9D3OmDxUA0hfZg6sGoABZeNrGi7IScZe +D26DAhPhIWa0sDVUTfCZkezXtH+mFptwN0zSIbMRWozx49jEZF9hBF4QBmnNnUPm +VTs+Z2AzeRQpUZPQ8rzWAbcf1oCwhPqSknXlcsxJnwKBgQDartbfUMkJ2LgBLTkl +BSPxI2ngvb0ySzdzxfsPKZZe4EAz4KFl+1DqQ4EZifhdhJiqV+PicNPKNFo2AK6S +3Wd0X4GAHZDAamiJces1WxqHigHXOrdVqu8/YSpVPEk/jdSYgHqwG7ji0IKEVtpw +EZJ+LX6BDOvDpbedBGcZT7Mm/wKBgQC/A75DSRxHK48AeuCiG4Q6G72IvOTYqK/2 +hz45HgfUI22d41Bb/48ybH8xt/SDoJLibx8P88ej/81vvYg3eLIyH+QR0f18Ia2g +sZd4Lj+2zo1fJbmQKptTI+O8PqrZzx9UZ2UfGXxQ0sNSuF37hFw8hI8ALzk1+UW/ +OoX90IUOSwKBgA0FJ+n352BctOftB1/65F7xGta0tVUPQWf1O7N1aGyRsYDlOPbX +dcPc7QzWOCFpSaWqwfizewipAU4B0GMSJ5y4Kv+zwvCR5VN5ouV0XSoAv4dPCadi +HAiMAnc8tafBDA1gaO2fWOy4OW0jtrHBehVlJAkO+eKWNU51+qV5J1OFAoGANS+N +op6QySBPyQpt0bVns+ZVd+VgsxMFK9esc6rw8xiKRRQuI++cp6WeJPHbm2ryeyoF +tCNkyz1Grn5Pl2J7+4j1sCCQPCgEeGH6kvQNuZD5vCx85q92YEf1+UxZthv91TqU +5XvrKXYF/NppEMdiB1fBmYOMooKt8PkSpgGRitECgYAkF8zcg/zNCueYezn6iW7q +PeO30XvLRnDbh4DnhkwoLbDR3vaxmmbphCzLe3tX8TDAA13QeWON/8JL7QU35HcY +lBlVJo2Mr0crROKElPicXXXSZWDxXFjcdVj+Oa4ni5wwKsppE18HNo9Vlj+RNgjw +dOfKqK1UP8tgaDa1W9ERDQ== +-----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..1885072 --- /dev/null +++ b/tests/fixtures/rsa-leaf-server.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRTCCAi2gAwIBAgIUPL2aFs+m1HMJy9P1zeCRHsgB/dQwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRAwDgYDVQQKDAdUZXN0T3JnMRYwFAYDVQQDDA1UZXN0UnNhUm9vdENBMB4XDTI2 +MDIyMTA4Mzg1OVoXDTM2MDIxOTA4Mzg1OVowFDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoyuk5F+T7kwKU7i+WdKe +iKH6Ns1xzjyVXAw3is1VvNykcCvaBYYyE/RxykRZGq+XsykLimPsbbL5syV9kkxs +INYrTyHB6Z4ML5rQMuP9pQQdMC9cMXPtrKqVmBpqi5uYqormlUKNp9LoizcHeGmc +6Y7UV1o8q/bI3wXF7uv5t2zep2JJoetd6rvQQKOC9WD8vjRbVYSpAcw/UKjw2b+5 +laKnvmNwsn/PO9/uU+j1a6Ve/mAqRSQOOn7AP1K5rMkH0Dwji+X22y16rFfcjH9v ++yQgm6zvupqSCWhFp3IkudbBCIdeaKh+yIhefums/zntdV/y21h5C0VM65OLmBle +tQIDAQABo00wSzAJBgNVHRMEAjAAMB0GA1UdDgQWBBRsFCi7ZWnzarIhJkQzwWQm +7r27WTAfBgNVHSMEGDAWgBRHYiOHw458Ka7uZ2w7wkbkWsD5MTANBgkqhkiG9w0B +AQsFAAOCAQEAOuP/m6+3/XlGmospEkD3K8I3/zTNqYUQAeFaPcivdAO5WgqpBN7G +JCPVEY12XGHd8KNYe6h7J0w2weCAUONiaU/2bsXCcrOKNA2c4no1DY+NJR7YKJmi +QKHNXpw3qcDlqFzkz8/4GiIEf+NA+dtS6476iTr1d1OnJwYm9yjgVeWod/fp0kU/ +ECF52TIbB3qJKKYiZwhdEQm1ddjfMJAaM+kRkCf+53UZG5R/Z9ApX1DHALbIPlNe +49s7ulKuNJs1ZfhjcgruJn4b6XTgsTZ/1KJv+Wgd31G6at9I/gRmRvGBOwUjAkyH +O3ZKdDXZfHvhfoHiiO9Bjs5Hxt0F7xJ88g== +-----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..5b06329 --- /dev/null +++ b/tests/fixtures/rsa-root-ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCZKwbc7+1RFLyM +7sWOdHLHfGwyGQnD5zkYyGnMrHuewFgua6D52zoI3XmTEL4tdKgHLFIjOI3z/kQ3 +78BX727vsGj+/uMUo64cnC6xJIOQKPUaNQPvHQfiUjwxqP3rV0Bf+bVj5D14rFFw +ghWnp4KJ13dw50qjV2hPcGz2rf7HJtv/rNiTAoiVt5nc6NhMKFzJXPOMIzYAgTDL +ISRtjw6LR2LOR+guSMvpEPe+Kgle44OSHjz+SLABNE9oXIdqzrmE1AStmMoNl9n9 +FLQ3ci2yaEGMshYBB/AU3cwAXOgQvObDusmPp+hbcsGglauem/7WTZSWUXhdJG4M +mTzTf0GJAgMBAAECgf85I1PsF5TwKkwsRuZrvgUTZdb22WBLNHaYSCsvryhukFJU +/tGOY7nClNxFgHlxe5MzGdWKTg6mdrP8KfQW2bsIr0Z72ZncmTLaeWjxrC1oGd9V +Z3GQQcQvKX5LCD+xC1t4ci64lOxZl+7Jib2KTXLk+PwVojK1vGWtPMNpQn9IyyIz +b13zG/4cN7tHECjvNa3Xp0mJ9p7y34LiAl7YD+FGh8DHt+LG5Tcbr8fV3fzafNZK +S1cX8zBJlqPcmMuPxWwTEinW8Lo4A1P1E+8adxzDmIoD4VR3CR5zruzwGbAQv8GV +EGAQzbRSCNXprRi0vmDs2sJm465U5bPGNCimt7cCgYEA09KbmFD/GcUbTC9v/uvC +7xnbyMljwDFooxwYPLqe5FdCiBbLziY4U9D21BSvxZxDi11Ltbem1/kfj1Fpb5Xq +qP7bMqcCO/pwzXvb7px3QFBvp+jPDjKr3I3/c+rXu20Ua2sQl+8AyTqm1QpmP1w2 +EvXkXFaH7aJ7gf8hGcrX8IcCgYEAuRzMtoPAkmou0mGf4sPt1hcoW9YuRyI2Jcsg +WgTg0i2O1IX4TlEHG5pgIkM3uR3VEkSN0qJWB6V+1FP4CKnSrxgVq2V4422mQNtG +d2Gd3gZFhpQE9B3McAq/2G3pXHI8ZQNd6Oc1z7ecmEnBff/PlE2xGhMoe2Srgwxh +h/IzEW8CgYBg57LjJfrusSvh2Lnl57nQZQYVf3yxCmmSZWH5Nm9Gi10WoUcv0nBm +d+zT7XrUbr6/3Tirs48SsxfrGxWfRPiLw7xIGft9sP82InnlWZN8ys+qA2nmVuwl +BJlfUIrNZgO3eM2olGDJrplwUUehqO/cEL4eOEALSRAz0qI0CIZttQKBgFRV3KZi +jD+ohMBwnclQfnEFh+ufPuJFoenCC3E3u73F58bHaoMzw0s+IAI8IY0DHGoANaT7 +NLqzGX9e6if4RvZiwKyfxF3JPO9bd1U4chYPQWm40jDtypBZNWJDYQgvO3jB+ez8 +ObXy7zMqly7ydv4YD1HT3KOrD8DaySyImd+dAoGAI8QU3UEdge0cY+UtD1vHLUb/ +Z6rfcnoDL17azt33CooNH3lCVw+IqgE0t63lO6W/3DCi8A3D9PJsopnAdPK7rvdT +UrvmtretbgxNRGCBC8T2fVJV6Q9DGqmjMol2/rlNiYQNhgK8bEiAmPYw7GXH0LjO +PEayI0HRcYAWrpEwwvs= +-----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..29683e0 --- /dev/null +++ b/tests/fixtures/rsa-root-ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjTCCAnWgAwIBAgIUWjGv/hbVIjFa4t9qWs3sd5RCzDQwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRAwDgYDVQQKDAdUZXN0T3JnMRYwFAYDVQQDDA1UZXN0UnNhUm9vdENBMB4XDTI2 +MDIyMTA4Mzg1OVoXDTM2MDIxOTA4Mzg1OVowVjELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRAwDgYDVQQKDAdUZXN0T3JnMRYwFAYD +VQQDDA1UZXN0UnNhUm9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAmSsG3O/tURS8jO7FjnRyx3xsMhkJw+c5GMhpzKx7nsBYLmug+ds6CN15kxC+ +LXSoByxSIziN8/5EN+/AV+9u77Bo/v7jFKOuHJwusSSDkCj1GjUD7x0H4lI8Maj9 +61dAX/m1Y+Q9eKxRcIIVp6eCidd3cOdKo1doT3Bs9q3+xybb/6zYkwKIlbeZ3OjY +TChcyVzzjCM2AIEwyyEkbY8Oi0dizkfoLkjL6RD3vioJXuODkh48/kiwATRPaFyH +as65hNQErZjKDZfZ/RS0N3ItsmhBjLIWAQfwFN3MAFzoELzmw7rJj6foW3LBoJWr +npv+1k2UllF4XSRuDJk8039BiQIDAQABo1MwUTAfBgNVHSMEGDAWgBRHYiOHw458 +Ka7uZ2w7wkbkWsD5MTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHYiOHw458 +Ka7uZ2w7wkbkWsD5MTANBgkqhkiG9w0BAQsFAAOCAQEAiHXmwDY+9t5cnFZcuxJl +3lQSOpHQMmSYe7FiTpI+GglGW+ugahdyqc/c2GbNKqB1oxGA+mEv+KeKYgtTfC8J +gTtBLVjhquUOY4K6AkU69i5e7g8/41TFuAF0nt8LnkAYZmo12kbYjX1gKTfkl9wl +hMY+OSnoIoxBrw1cmqRtOu+Hn2wY9dVCQJVIgc89WuQr8USn9JXzq1bUCzOk6a8/ +dR6ZEPDVPo6RjEXuLNUuTOoTH1wCG3P1tHUs4Cwapr1EhgrEs7DC4uvB+9Y6EXqI +OLqn0V3FBsBPN19bdLWTtaOm5C0xbNmiXfJPBH9nF38Hpj4tk72GfxOzKI4J+rG7 +Kg== +-----END CERTIFICATE----- diff --git a/tests/fixtures/rsa-root-ca.srl b/tests/fixtures/rsa-root-ca.srl new file mode 100644 index 0000000..6f8eb75 --- /dev/null +++ b/tests/fixtures/rsa-root-ca.srl @@ -0,0 +1 @@ +3CBD9A16CFA6D47309CBD3F5CDE0911EC801FDD5 diff --git a/tests/fixtures/setup_fixtures.sh b/tests/fixtures/setup_fixtures.sh new file mode 100755 index 0000000..3235313 --- /dev/null +++ b/tests/fixtures/setup_fixtures.sh @@ -0,0 +1,70 @@ +#!/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" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "subjectKeyIdentifier=hash" + +# 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 -sha256 -CAcreateserial \ + -extfile <(printf "$EXT_LEAF") +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 -sha256 -CAcreateserial \ + -extfile <(printf "$EXT_LEAF") +rm _rsacli.csr diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..3d57781 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,1326 @@ +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, ProtocolError as ConnectError, SignatureScheme, 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, ProtocolError as ConnectError, SignatureScheme, + 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"); + } + } +}