From d4a50c1ebd3aad03f7ea03f043a7cc6f85241784 Mon Sep 17 00:00:00 2001 From: Juliette T <36003517+JTKU@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:53:40 +0200 Subject: [PATCH] Add TLS support --- CHANGELOG.md | 2 +- Cargo.lock | 345 ++++++++++++++++++++++++++++-- README.md | 1 + doc/getting_started.md | 4 + doc/how_it_works.md | 2 - doc/issues.md | 8 +- doc/tls.md | 125 +++++++++++ openwec.conf.sample.toml | 22 +- server/Cargo.toml | 9 +- server/src/event.rs | 3 +- server/src/kerberos.rs | 40 +--- server/src/lib.rs | 395 +++++++++++++++++++++++++---------- server/src/logic.rs | 30 ++- server/src/soap.rs | 61 +++++- server/src/tls.rs | 371 ++++++++++++++++++++++++++++++++ tests/certs/key_ecdsa.pem | 9 + tests/certs/key_rsa.pem | 28 +++ tests/certs/no_key.pem | 25 +++ tests/certs/wrong_format.pem | 13 ++ 19 files changed, 1301 insertions(+), 192 deletions(-) create mode 100644 doc/tls.md create mode 100644 server/src/tls.rs create mode 100644 tests/certs/key_ecdsa.pem create mode 100644 tests/certs/key_rsa.pem create mode 100644 tests/certs/no_key.pem create mode 100644 tests/certs/wrong_format.pem diff --git a/CHANGELOG.md b/CHANGELOG.md index 3241632..f82d92f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add IgnoreChannelError option to subscriptions (#6) - Add Kerberos principals filter to subscriptions (#18) - Add a setting to configure `heartbeats_queue_size` (#37) +- Add Tls support for encryption and authentication (#36) ### Changed - - Server log responses payload in TRACE level (#37) - Remove `OperationID` from responses because we don't support "Robust Connection" (#37) - Clear in-memory subscriptions when a SIGHUP signal is received, resulting in all file descriptors used by subscriptions being closed (#37) diff --git a/Cargo.lock b/Cargo.lock index 26d4655..775c0c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,45 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -120,7 +159,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -382,6 +421,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "deadpool" version = "0.9.5" @@ -436,6 +481,26 @@ dependencies = [ "deadpool", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + [[package]] name = "digest" version = "0.10.7" @@ -447,6 +512,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "either" version = "1.8.1" @@ -600,7 +676,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -1073,6 +1149,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1122,6 +1219,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -1151,7 +1257,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -1297,9 +1403,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -1315,9 +1421,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -1428,6 +1534,21 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "roxmltree" version = "0.18.0" @@ -1463,6 +1584,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.37.20" @@ -1477,6 +1607,37 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.13" @@ -1489,6 +1650,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.164" @@ -1506,7 +1677,7 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -1551,7 +1722,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -1583,13 +1754,30 @@ dependencies = [ "rdkafka", "regex", "roxmltree", + "rustls", + "rustls-pemfile", "serde", "serde_json", + "sha1", + "tls-listener", "tokio", + "tokio-rustls", "uuid", + "x509-parser", "xmlparser", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.7" @@ -1657,6 +1845,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stringprep" version = "0.1.2" @@ -1692,15 +1886,27 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.6.0" @@ -1724,6 +1930,54 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1739,6 +1993,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls-listener" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81294c017957a1a69794f506723519255879e15a870507faf45dfed288b763dd" +dependencies = [ + "futures-util", + "hyper", + "pin-project-lite", + "thiserror", + "tokio", + "tokio-rustls", +] + [[package]] name = "tokio" version = "1.32.0" @@ -1766,7 +2034,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -1805,6 +2073,16 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -1923,6 +2201,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1987,7 +2277,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -2009,7 +2299,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2020,6 +2310,16 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "4.4.0" @@ -2155,6 +2455,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + [[package]] name = "xmlparser" version = "0.13.5" diff --git a/README.md b/README.md index 22c1d23..dd828f7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Subscriptions and their parameters are stored in a [database](doc/database.md) a # Documentation - [Getting started](doc/getting_started.md) +- [TLS authentication](doc/tls.md) - [Command Line Interface](doc/cli.md) - [Database](doc/database.md) - [Subscription query](doc/query.md) diff --git a/doc/getting_started.md b/doc/getting_started.md index dd50c85..3926ba8 100644 --- a/doc/getting_started.md +++ b/doc/getting_started.md @@ -19,6 +19,8 @@ $ strip target/release/openwecd ## Basic configuration example +This example uses Kerberos authentication. For a basic example using TLS, see [tls.md](tls.md). + In an Active Directory domain `DC=windomain,DC=local`, let's configure OpenWEC on a machine named `wec.windomain.local` using an SQLite database. Requirements: @@ -155,6 +157,8 @@ $ openwec subscriptions enable my-test-subscription ## Configuring Windows machines +This configuration works for Kerberos authentication. For a configuration using TLS, see [tls.md](tls.md/) first, then follow the following steps by applying local policies instead of GPOs (use `gpedit.msc`). + You can configure Windows machines using a GPO. This GPO will configure three things: diff --git a/doc/how_it_works.md b/doc/how_it_works.md index 91688ab..a4efd31 100644 --- a/doc/how_it_works.md +++ b/doc/how_it_works.md @@ -10,8 +10,6 @@ Two transport protocols are available: * Kerberos/HTTP (port 5985): SOAP messages are authenticated and encrypted using Kerberos and sent over HTTP. This is mainly used in Active Directory environments. * HTTPS (port 5986): SOAP messages are authenticated and encrypted using HTTPS. Each Windows machine must have a valid client certificate. -**OpenWEC only supports Kerberos/HTTP.** - ## Subscriptions Subscriptions are the heart of the Windows Event Forwarding protocol and thus of openwec :smile:. diff --git a/doc/issues.md b/doc/issues.md index b3e0d9c..7afb3cb 100644 --- a/doc/issues.md +++ b/doc/issues.md @@ -56,10 +56,4 @@ To support this, we would need to enable users to configure a list of Kerberos p The Microsoft implementation allows subscriptions to be set for a group of machines. The only "clean" way to do this is to parse the PAC contained in the machine's Kerberos service ticket, but this does not appear to be supported by GSSAPI. Alternatively, OpenWEC implements filters using the Kerberos principal names of machines, allowing you to allow or deny a set of principals to "see"/"use" a subscription. -In any case, it won't work with an intermediate forwarder. - -## TLS support - -Windows clients support TLS for authentication and encryption. This is not currently supported by OpenWEC, but it should. For now OpenWEC only supports authentication and encryption with Kerberos. There are two possibilities in order to add the TLS support: -- implement TLS support within OpenWEC web server (powered by `hyper`). -- use a reverse proxy which handles TLS and then send cleartext messages to OpenWEC service. +In any case, it won't work with an intermediate forwarder. \ No newline at end of file diff --git a/doc/tls.md b/doc/tls.md new file mode 100644 index 0000000..4a7d92a --- /dev/null +++ b/doc/tls.md @@ -0,0 +1,125 @@ +# Configuration for TLS authentication + +This feature is **EXPERIMENTAL**, please use carefully. + +This documentation describes how to set up TLS authentication. Certificates need to be generated first, then follow the rest of the documentation in parallel with [getting_started.md](getting_started.md/#configuring-windows-machines). + +## Certificates + +**The following certificates are needed**: +- A certificate for the server, with extensions Server Authentication and PKIs, +- A certificate for the client, with extensions Client Authentication and PKIs, +- A certificate of the CA that signed the client certificate and the server certificate (they can be distinct). + +**Requirements for the Windows client**: +- It needs to support TLS 1.2 or TLS 1.3 (this includes Windows servers from 2012 and Windows 8). + +For older versions of Windows, TLS 1.2 needs to be available at least. At least one of the following cipher suites has to be available: +- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 +- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + +For more recent versions using TLS 1.3, at least one of the following cipher suites has to be available for it to be used: +- TLS13_AES_256_GCM_SHA384 +- TLS13_AES_128_GCM_SHA256 +- TLS13_CHACHA20_POLY1305_SHA256 + +You can find the currently available suites of a Windows machine in: *Local Group Policy Editor > Computer Configuration > Administrative Templates > Network > SSL Configuration Settings* (cf. [this page](https://learn.microsoft.com/en-us/windows/win32/secauthn/cipher-suites-in-schannel) for more precisions). + +**Requirements for the certificates and keys**: +- Certificate and key files must be in PEM format +- Server FQDN must be used as `CommonName` of server certificate +- `CommonName` of client certificate must be unique and should be distinguishable (they are used to display connections and as authenticated name in the logs) + +Keys must be encoded using either RSA PKCS#1 (cf. RFC3447), PKCS#8 (cf. RFC5958) or Sec-1 (cf. RFC5915). + +Only the following curves are supported: +- ecdsa_secp256r1_sha256 (also called prime256v1) +- ecdsa_secp384r1_sha384 + + +You can find the currently available elliptic curves of a Windows machine with the command: `certutil.exe –displayEccCurve`. + +If the clients are only composed of recent Windows machines (more recent than Windows server 2012 R2 and Windows 8), [nxlog's scripts](https://gitlab.com/nxlog-public/contrib/-/tree/master/windows-event-forwarding) should work just fine for certificate generation, while modifying the subjects and making sure they follow the rules detailed before. + +It seems that older versions (particularly Windows Server 2012 R2) tend to support only TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 and +TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256. Certificates that are supported in this case can be created using the following (and largely inspired by the formely-mentioned) example script: + +```sh +#!/bin/bash +# Usage: `script []` +# Don't forget to modify "O=example.local/C=HU/ST=state/L=location" with relevant values as well +# Curve can only be 'secp384r1' or 'secp256r1', by default it will be assumed as being 'secp384r1' + +# by default, curve is secp384r1 +if [[ -z "$4" || "$4" != "secp256r1" ]] +then CURVE="secp384r1" +else CURVE="secp256r1" +fi + +# CA certificate +CA_NAME="$1" +SUBJ="/CN=$CA_NAME/O=example.local/C=HU/ST=state/L=location" +openssl ecparam -out ca-key.pem -name $CURVE -genkey +openssl req -x509 -nodes -key ca-key.pem -out ca-cert.pem -batch -subj "$SUBJ" -config gencert.cnf + +# Server certificate +SERVERNAME="$2" +ISSUERCA=`openssl x509 -in ca-cert.pem -noout -sha1 -fingerprint |sed s/^SHA1\ Fingerprint=//|sed s/://g` +SERVERSUBJ="/CN=$SERVERNAME/O=example.local/C=HU/ST=state/L=location" +CERTDIR=. +openssl ecparam -out server-key.pem -name $CURVE -genkey +openssl req -new -key server-key.pem -out req.pem -batch -subj "$SERVERSUBJ" -config gencert.cnf +openssl x509 -req -days 1024 -in req.pem -CA ca-cert.pem -CAkey ca-key.pem -out server-cert.pem -set_serial 01 -extensions server_cert -extfile gencert.cnf +rm -f req.pem +openssl x509 -outform der -in server-cert.pem -out server-cert.crt +echo "###############################################################" +echo "Use the following for the Subscription Manager string:" +echo "Server=HTTPS://$SERVERNAME:5986/wsman/,Refresh=14400,IssuerCA=$ISSUERCA" + +# Client certificate +CLIENTNAME="$3" +CLIENTSUBJ="/CN=$CLIENTNAME/O=example.local/C=HU/ST=state/L=location" +openssl ecparam -out client-key.pem -name $CURVE -genkey +openssl req -new -key client-key.pem -out req.pem -batch -subj "$CLIENTSUBJ" -config gencert.cnf +openssl x509 -req -days 1024 -in req.pem -CA ca-cert.pem -CAkey ca-key.pem -out client-cert.pem -set_serial 01 -extensions client_cert -extfile gencert.cnf +rm -f req.pem +openssl pkcs12 -export -out client.pfx -inkey client-key.pem -in client-cert.pem -certfile ca-cert.pem +``` + +The content of `gencert.cnf` used for this script can be found [here](https://gitlab.com/nxlog-public/contrib/-/blob/master/windows-event-forwarding/gencert.cnf). + +## Client configuration + +Once all certificates are generated, the service can finally be setup. [Windows' manual](https://learn.microsoft.com/en-us/windows/win32/wec/setting-up-a-source-initiated-subscription#event-source-computer-configuration) is a good reference for this part of the configuration: + +1. Install the client certificate on the Windows machine in the category `Personal` (from the certificate manager, right-click on a category and go to *All Tasks > Import...*, make sure to check the box next to "Include all extended properties") +2. Install the CA certificate (for the CA that signed the server certificate) in category `Trusted Root Certification Authorities` +3. Give `Network Service` the right to read the client certificate's private key (from the certificate manager, right-click on the client certificate and go to *Manage Private Keys*) +4. Modify local policy for event forwarding with the Subscription Manager string obtained when generating the certificates + +Make sure to follow [the Getting Started page](getting_started.md/#configuring-windows-machines) as well for generic instructions. + +If the client works on an older version of Windows, make sure to activate TLS 1.2 as explained [here](https://learn.microsoft.com/en-us/mem/configmgr/core/plan-design/security/enable-tls-1-2-client#bkmk_protocol). +(Some other references: [Enable TLS 1.1 and 1.2](https://support.microsoft.com/en-us/topic/update-to-enable-tls-1-1-and-tls-1-2-as-default-secure-protocols-in-winhttp-in-windows-c4bd73d2-31d7-761e-0178-11268bb10392) and [TLS/SSL Settings](https://learn.microsoft.com/fr-fr/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn786418(v=ws.11)?redirectedfrom=MSDN)). + +For troubleshooting, Windows Events from `Eventlog-ForwardingPlugin` (Operational) and `Windows Remote Management` (Analytics, needs to be enabled in the View drop-down) can be of great help. + +## Server configuration + +To configure OpenWEC on a machine named `wec.winserver.local`, the minimal options to configure in `/etc/openwec.conf.toml` are as follows: + +```toml +# /etc/openwec.conf.toml +[[collectors]] +hostname = "wec.winserver.local" + +[collectors.authentication] +type = "Tls" +ca_certificate = "/etc/ca-cert.pem" +server_certificate = "/etc/server-cert.pem" +server_private_key = "/etc/server-key.pem" +``` \ No newline at end of file diff --git a/openwec.conf.sample.toml b/openwec.conf.sample.toml index 7857276..8e1000a 100644 --- a/openwec.conf.sample.toml +++ b/openwec.conf.sample.toml @@ -118,7 +118,6 @@ # Each collector must listen on a different (address, port) pair. # All collectors share the same database. # This is useful if you want to support both Kerberos and TLS authentication -# (TLS authentication is not yet supported). # This defines one collector [[collectors]] @@ -142,11 +141,10 @@ # max_content_length = 512000 # Authentication settings for this collector -# For now, the only available authentication method is Kerberos. [collectors.authentication] # [Required] -# Authentication method: Kerberos -# type = "Kerberos" +# Authentication method: Kerberos, Tls +# type = "Kerberos" ## Kerberos configuration @@ -162,3 +160,19 @@ # keytab = "/etc/krb5.keytab" ## End of Kerberos configuration + +## TLS configuration + +# [Required] +# CA certificate used to sign client certificates +# ca_certificate = "/etc/ca-cert.pem" + +# [Required] +# Server certificate +# server_certificate = "/etc/server-cert.pem" + +# [Required] +# Server private key, corresponding to the certificate +# server_private_key = "/etc/server-key.pem" + +## End of TLS configuration \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml index 4cc3730..d797d31 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -39,7 +39,10 @@ xmlparser = "0.13.5" itertools = "0.11.0" futures = "0.3.28" bitreader = "0.3.7" - - -[dev-dependencies] +rustls = "0.21.6" +rustls-pemfile = "1.0.3" +tls-listener = { version = "0.7.0", features = ["hyper-h2", "rustls"] } +tokio-rustls = "0.24.1" +x509-parser = "0.15.1" +sha1 = "0.10.5" hex = "0.4.3" diff --git a/server/src/event.rs b/server/src/event.rs index db71577..feee3b0 100644 --- a/server/src/event.rs +++ b/server/src/event.rs @@ -85,7 +85,7 @@ impl Event { let mut event = Event::default(); event.additional = Additional { addr: metadata.addr().ip().to_string(), - principal: metadata.principal().to_owned(), + principal: metadata.principal().to_owned(), // TODO : change to something that works for TLS as well (modify db and output) node: metadata.node_name().cloned(), time_received: metadata.time_received().to_rfc3339(), subscription: SubscriptionType { @@ -465,6 +465,7 @@ impl RenderingInfo { #[derive(Debug, Clone)] pub struct EventMetadata { + // TODO : add authentication method (TLS or Kerberos) addr: SocketAddr, principal: String, node_name: Option, diff --git a/server/src/kerberos.rs b/server/src/kerberos.rs index 0d1530d..af3441c 100644 --- a/server/src/kerberos.rs +++ b/server/src/kerberos.rs @@ -1,10 +1,9 @@ use anyhow::{anyhow, bail, Context, Result}; use base64::Engine; -use common::encoding::{decode_utf16le, encode_utf16le}; -use common::settings::Collector; -use hyper::body::HttpBody; +use common::encoding::encode_utf16le; +use http::request::Parts; use hyper::header::AUTHORIZATION; -use hyper::{Body, Request}; +use hyper::{body::Bytes, Body, Request}; use libgssapi::{ context::{CtxFlags, SecurityContext, ServerCtx}, credential::{Cred, CredUsage}, @@ -202,35 +201,10 @@ fn encrypt_payload(mut payload: Vec, server_ctx: &mut ServerCtx) -> Result>, - req: Request, -) -> Result> { - let (parts, body) = req.into_parts(); - - let response_content_length = body - .size_hint() - .upper() - .ok_or_else(|| anyhow!("Header Content-Length is not present")) - .context("Could not check Content-Length header of request")?; - - let max_content_length = settings.max_content_length(); - - if response_content_length > max_content_length { - bail!( - "HTTP request body is too large ({} bytes larger than the maximum allowed {} bytes).", - response_content_length, - max_content_length - ); - } - - let data = hyper::body::to_bytes(body) - .await - .context("Could not retrieve request body")?; - if data.is_empty() { - return Ok(None); - } - + parts: Parts, + data: Bytes, +) -> Result>> { let content_type = match parts.headers.get("Content-Type") { Some(content_type) => content_type, None => bail!("Request does not contain 'Content-Type' header"), @@ -261,7 +235,7 @@ pub async fn get_request_payload( value => bail!("Unsupported Content-Encoding {:?}", value), }; - Ok(Some(decode_utf16le(message)?)) + Ok(Some(message)) } pub fn get_response_payload( diff --git a/server/src/lib.rs b/server/src/lib.rs index dabb0e1..12a0768 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -9,15 +9,23 @@ mod outputs; mod sldc; mod soap; mod subscription; +mod tls; use anyhow::{anyhow, bail, Context, Result}; use common::database::{db_from_settings, schema_is_up_to_date, Db}; +use common::encoding::decode_utf16le; +use common::settings::{Authentication, Kerberos, Tls}; use common::settings::{Collector, Server as ServerSettings, Settings}; -use futures_util::future::join_all; +use core::pin::Pin; +use futures::Future; +use futures_util::{future::join_all, StreamExt}; use heartbeat::{heartbeat_task, WriteHeartbeatMessage}; use http::response::Builder; use http::status::StatusCode; +use hyper::body::{to_bytes, HttpBody}; use hyper::header::{CONTENT_TYPE, WWW_AUTHENTICATE}; +use hyper::server::accept; +use hyper::server::conn::AddrIncoming; use hyper::server::conn::AddrStream; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; @@ -27,9 +35,11 @@ use log::{debug, error, info, trace, warn}; use quick_xml::writer::Writer; use regex::Regex; use soap::Serializable; +use std::boxed::Box; use std::collections::HashMap; use std::convert::Infallible; use std::env; +use std::future::ready; use std::io::Cursor; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; @@ -37,14 +47,13 @@ use std::sync::Mutex; use std::sync::{Arc, RwLock}; use std::time::Instant; use subscription::{reload_subscriptions_task, Subscriptions}; +use tls_listener::TlsListener; use tokio::signal::unix::SignalKind; use tokio::sync::{mpsc, oneshot}; +use tokio_rustls::server::TlsStream; +use tokio_rustls::TlsAcceptor; -#[derive(Copy, Clone)] -pub enum AuthenticationMechanism { - Kerberos, - Tls, -} +use crate::tls::{make_config, subject_from_cert}; pub enum RequestCategory { Enumerate(String), @@ -110,29 +119,77 @@ pub struct AuthenticationResult { token: Option, } +#[derive(Debug, Clone)] +/// Kerberos : state +/// Tls : subject, thumbprint +pub enum AuthenticationContext { + Kerberos(Arc>), + Tls(String, String), +} + async fn get_request_payload( - auth_mech: AuthenticationMechanism, collector: &Collector, - conn_state: &Arc>, + auth_ctx: &AuthenticationContext, req: Request, ) -> Result> { - match auth_mech { - AuthenticationMechanism::Tls => bail!("TLS is not supported yet"), - AuthenticationMechanism::Kerberos => { - kerberos::get_request_payload(collector, conn_state, req).await + let (parts, body) = req.into_parts(); + + let response_content_length = body + .size_hint() + .upper() + .ok_or_else(|| anyhow!("Header Content-Length is not present")) + .context("Could not check Content-Length header of request")?; + + let max_content_length = collector.max_content_length(); + + if response_content_length > max_content_length { + bail!( + "HTTP request body is too large ({} bytes larger than the maximum allowed {} bytes).", + response_content_length, + max_content_length + ); + } + + let data = to_bytes(body) + .await + .context("Could not retrieve request body")?; + + if data.is_empty() { + return Ok(None); + } + + let message = match auth_ctx { + AuthenticationContext::Tls(_, _) => tls::get_request_payload(parts, data).await?, + AuthenticationContext::Kerberos(conn_state) => { + kerberos::get_request_payload(conn_state, parts, data).await? } + }; + + match message { + Some(bytes) => Ok(Some(decode_utf16le(bytes)?)), + _ => Ok(None), } } fn create_response( - auth_mech: AuthenticationMechanism, - conn_state: &Arc>, + auth_ctx: &AuthenticationContext, mut response: Builder, payload: Option, ) -> Result> { - match auth_mech { - AuthenticationMechanism::Tls => bail!("TLS is not supported yet"), - AuthenticationMechanism::Kerberos => { + match auth_ctx { + AuthenticationContext::Tls(_, _) => { + if payload.is_some() { + response = response.header(CONTENT_TYPE, "application/soap+xml;charset=UTF-16"); + } + let body = match payload { + None => Body::empty(), + Some(payload) => Body::from( + tls::get_response_payload(payload).context("Failed to compute TLS payload")?, + ), + }; + Ok(response.body(body)?) + } + AuthenticationContext::Kerberos(conn_state) => { let boundary = "Encrypted Boundary"; if payload.is_some() { response = response.header(CONTENT_TYPE, "multipart/encrypted;protocol=\"application/HTTP-Kerberos-session-encrypted\";boundary=\"".to_owned() + boundary + "\""); @@ -150,17 +207,21 @@ fn create_response( } async fn authenticate( - auth_mech: AuthenticationMechanism, - conn_state: &Arc>, + auth_ctx: &AuthenticationContext, req: &Request, addr: &SocketAddr, ) -> Result<(String, Builder)> { - match auth_mech { - AuthenticationMechanism::Tls => { - error!("TLS is not supported yet"); - bail!("TLS is not supported yet") + match auth_ctx { + AuthenticationContext::Tls(subject, _) => { + // if subject is empty, show unauthorized error + if subject.is_empty() { + bail!("Empty certificate") + } + + let response = Response::builder(); + Ok((subject.to_owned(), response)) } - AuthenticationMechanism::Kerberos => { + AuthenticationContext::Kerberos(conn_state) => { let mut response = Response::builder(); let auth_result = kerberos::authenticate(conn_state, req) .await @@ -194,6 +255,7 @@ async fn handle_payload( heartbeat_tx: mpsc::Sender, request_data: RequestData, request_payload: Option, + auth_ctx: &AuthenticationContext, ) -> Result<(StatusCode, Option)> { match request_payload { None => Ok((StatusCode::OK, None)), @@ -208,6 +270,7 @@ async fn handle_payload( heartbeat_tx, request_data, &message, + auth_ctx, ) .await .context("Failed to handle SOAP message")?; @@ -249,8 +312,7 @@ async fn handle( db: Db, subscriptions: Subscriptions, heartbeat_tx: mpsc::Sender, - auth_mech: AuthenticationMechanism, - conn_state: Arc>, + auth_ctx: AuthenticationContext, addr: SocketAddr, req: Request, ) -> Result, Infallible> { @@ -268,26 +330,25 @@ async fn handle( let uri = req.uri().to_string(); // Check authentication - let (principal, mut response_builder) = - match authenticate(auth_mech, &conn_state, &req, &addr).await { - Ok((principal, builder)) => (principal, builder), - Err(e) => { - debug!( - "Authentication failed for {}:{} ({}:{}): {:?}", - addr.ip(), - addr.port(), - &method, - &uri, - e - ); - let status = StatusCode::UNAUTHORIZED; - log_response(&addr, &method, &uri, &start, status); - return Ok(Response::builder() - .status(status) - .body(Body::empty()) - .expect("Failed to build HTTP response")); - } - }; + let (principal, mut response_builder) = match authenticate(&auth_ctx, &req, &addr).await { + Ok((principal, builder)) => (principal, builder), + Err(e) => { + debug!( + "Authentication failed for {}:{} ({}:{}): {:?}", + addr.ip(), + addr.port(), + &method, + &uri, + e + ); + let status = StatusCode::UNAUTHORIZED; + log_response(&addr, &method, &uri, &start, status); + return Ok(Response::builder() + .status(status) + .body(Body::empty()) + .expect("Failed to build HTTP response")); + } + }; debug!("Successfully authenticated {}", principal); @@ -305,7 +366,7 @@ async fn handle( }; // Get request payload - let request_payload = match get_request_payload(auth_mech, &collector, &conn_state, req).await { + let request_payload = match get_request_payload(&collector, &auth_ctx, req).await { Ok(payload) => payload, Err(e) => { error!("Failed to retrieve request payload: {:?}", e); @@ -332,6 +393,7 @@ async fn handle( heartbeat_tx, request_data, request_payload, + &auth_ctx, ) .await { @@ -355,8 +417,7 @@ async fn handle( response_builder = response_builder.status(status); // Create HTTP response - let response = match create_response(auth_mech, &conn_state, response_builder, response_payload) - { + let response = match create_response(&auth_ctx, response_builder, response_payload) { Ok(response) => response, Err(e) => { error!("Failed to build HTTP response: {:?}", e); @@ -370,10 +431,159 @@ async fn handle( }; log_response(&addr, &method, &uri, &start, response.status()); - // debug!("Send response: {:?}", response); Ok(response) } +fn create_kerberos_server( + kerberos_settings: &Kerberos, + collector_settings: Collector, + collector_db: Db, + collector_subscriptions: Subscriptions, + collector_heartbeat_tx: mpsc::Sender, + collector_server_settings: ServerSettings, + addr: SocketAddr, +) -> Pin> + Send>> { + env::set_var("KRB5_KTNAME", kerberos_settings.keytab()); + + let principal = kerberos_settings.service_principal_name().to_owned(); + // Try to initialize a security context. This is to be sure that an error in + // Kerberos configuration will be reported as soon as possible. + let state = kerberos::State::new(&principal); + if state.context_is_none() { + panic!("Could not initialize Kerberos context"); + } + + // A `MakeService` that produces a `Service` to handle each connection. + let make_service = make_service_fn(move |conn: &AddrStream| { + // We have to clone the context to share it with each invocation of + // `make_service`. + + // Initialize Kerberos context once for each TCP connection + let collector_settings = collector_settings.clone(); + let svc_db = collector_db.clone(); + let svc_server_settings = collector_server_settings.clone(); + let auth_ctx = + AuthenticationContext::Kerberos(Arc::new(Mutex::new(kerberos::State::new(&principal)))); + let subscriptions = collector_subscriptions.clone(); + let collector_heartbeat_tx = collector_heartbeat_tx.clone(); + + let addr = conn.remote_addr(); + + debug!("Received TCP connection from {}", addr); + + // Create a `Service` for responding to the request. + let service = service_fn(move |req| { + handle( + svc_server_settings.clone(), + collector_settings.clone(), + svc_db.clone(), + subscriptions.clone(), + collector_heartbeat_tx.clone(), + auth_ctx.clone(), + addr, + req, + ) + }); + + // Return the service to hyper. + async move { Ok::<_, Infallible>(service) } + }); + + // Then bind and serve... + let server = Server::bind(&addr) + .serve(make_service) + .with_graceful_shutdown(shutdown_signal()); + + info!("Server listenning on {}", addr); + // XXX : because the 2 closures have different types we use this, but may be better way to do this + Box::pin(server) +} + +fn create_tls_server( + tls_settings: &Tls, + collector_settings: Collector, + collector_db: Db, + collector_subscriptions: Subscriptions, + collector_heartbeat_tx: mpsc::Sender, + collector_server_settings: ServerSettings, + addr: SocketAddr, +) -> Pin> + Send>> { + // make TLS connection config + let tls_config = make_config(tls_settings).expect("Error while configuring server"); + + // create the service per connection + let make_service = make_service_fn(move |conn: &TlsStream| { + // get peer certificate (= user certificate) + let cert = conn + .get_ref() + .1 + .peer_certificates() + .expect("Peer certificate should exist") // client auth has to happen, so this should not fail + .first() + .expect("Peer certificate should not be empty") // client cert cannot be empty if authentication succeeded + .clone(); + + let subject = subject_from_cert(cert.as_ref()).expect("Could not parse client certificate"); + let thumbprint = tls_config.thumbprint.clone(); + + let collector_settings = collector_settings.clone(); + let svc_db = collector_db.clone(); + let svc_server_settings = collector_server_settings.clone(); + let subscriptions = collector_subscriptions.clone(); + let collector_heartbeat_tx = collector_heartbeat_tx.clone(); + + let addr = conn.get_ref().0.remote_addr(); + let auth_ctx = AuthenticationContext::Tls(subject, thumbprint); + + // create service per request + let service = service_fn(move |req| { + handle( + svc_server_settings.clone(), + collector_settings.clone(), + svc_db.clone(), + subscriptions.clone(), + collector_heartbeat_tx.clone(), + auth_ctx.clone(), + addr, + req, + ) + }); + + async move { Ok::<_, Infallible>(service) } + }); + + // create acceptor from config + let tls_acceptor: TlsAcceptor = tls_config.server.into(); + + // configure listener on the address to use the acceptor + let incoming = TlsListener::new( + tls_acceptor, + AddrIncoming::bind(&addr).expect("Could not bind address to listener"), + ) + .filter(|conn| { + if let Err(err) = &conn { + match err { + tls_listener::Error::TlsAcceptError(e) if e.to_string() == "tls handshake eof" => { + // happens sometimes, not problematic + debug!("Error while establishing a connection : {:?}", err) + } + _ => warn!("Error while establishing a connection : {:?}", err), + }; + ready(false) + } else { + ready(true) + } + }); + + let server = Server::builder(accept::from_stream(incoming)) + .serve(make_service) + .with_graceful_shutdown(shutdown_signal()); + + info!("Server listenning on {}", addr); + // XXX : because the 2 closures have different types we use this, but may be better way to do this + Box::pin(server) +} + async fn shutdown_signal() { let ctrl_c = tokio::signal::ctrl_c(); let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate()) @@ -386,7 +596,8 @@ async fn shutdown_signal() { } pub async fn run(settings: Settings) { - let mut servers = Vec::new(); + // XXX : because the 2 closures have different types we use this, but may be better way to do this + let mut servers: Vec> + Send>>> = Vec::new(); let db: Db = db_from_settings(&settings) .await @@ -450,66 +661,32 @@ pub async fn run(settings: Settings) { trace!("Listen address is {}", addr); - // FIXME - let kerberos = match collector.authentication() { - common::settings::Authentication::Kerberos(kerberos) => kerberos, - _ => panic!("Unsupported authentication type"), - }; - - env::set_var("KRB5_KTNAME", kerberos.keytab()); - - let principal = kerberos.service_principal_name().to_owned(); - // Try to initialize a security context. This is to be sure that an error in - // Kerberos configuration will be reported as soon as possible. - let state = kerberos::State::new(&principal); - if state.context_is_none() { - panic!("Could not initialize Kerberos context"); - } - - // A `MakeService` that produces a `Service` to handle each connection. - let make_service = make_service_fn(move |conn: &AddrStream| { - // We have to clone the context to share it with each invocation of - // `make_service`. - - // Initialise Kerberos context once for each TCP connection - let conn_state = Arc::new(Mutex::new(kerberos::State::new(&principal))); - let collector_settings = collector_settings.clone(); - let svc_db = collector_db.clone(); - let svc_server_settings = collector_server_settings.clone(); - let auth_mec = AuthenticationMechanism::Kerberos; - let subscriptions = collector_subscriptions.clone(); - let collector_heartbeat_tx = collector_heartbeat_tx.clone(); - - let addr = conn.remote_addr(); - - debug!("Received TCP connection from {}", addr); - - // Create a `Service` for responding to the request. - let service = service_fn(move |req| { - handle( - svc_server_settings.clone(), - collector_settings.clone(), - svc_db.clone(), - subscriptions.clone(), - collector_heartbeat_tx.clone(), - auth_mec, - conn_state.clone(), + // create server depending on connection type it allows + match collector.authentication() { + Authentication::Kerberos(kerberos) => { + servers.push(create_kerberos_server( + kerberos, + collector_settings, + collector_db, + collector_subscriptions, + collector_heartbeat_tx, + collector_server_settings, addr, - req, - ) - }); - - // Return the service to hyper. - async move { Ok::<_, Infallible>(service) } - }); - - // Then bind and serve... - let server = Server::bind(&addr) - .serve(make_service) - .with_graceful_shutdown(shutdown_signal()); + )); + } - info!("Server listenning on {}", addr); - servers.push(server); + Authentication::Tls(tls) => { + servers.push(create_tls_server( + tls, + collector_settings, + collector_db, + collector_subscriptions, + collector_heartbeat_tx, + collector_server_settings, + addr, + )); + } + }; } let result = join_all(servers).await; diff --git a/server/src/logic.rs b/server/src/logic.rs index ffa39a6..055645f 100644 --- a/server/src/logic.rs +++ b/server/src/logic.rs @@ -8,7 +8,7 @@ use crate::{ ACTION_HEARTBEAT, ACTION_SUBSCRIBE, ACTION_SUBSCRIPTION_END, ANONYMOUS, RESOURCE_EVENT_LOG, }, subscription::{Subscription, Subscriptions}, - RequestCategory, RequestData, + RequestCategory, RequestData, AuthenticationContext, }; use common::{ database::Db, @@ -59,6 +59,7 @@ async fn handle_enumerate( db: &Db, subscriptions: Subscriptions, request_data: &RequestData, + auth_ctx: &AuthenticationContext, ) -> Result { // Check that URI corresponds to an enumerate Request let uri = match request_data.category() { @@ -190,16 +191,28 @@ async fn handle_enumerate( identifier: subscription_data.version().to_owned(), bookmark, query: subscription_data.query().to_owned(), - address: format!( - "http://{}:{}/wsman/subscriptions/{}", - collector.hostname(), - collector.listen_port(), - subscription_data.version() - ), + address: match auth_ctx { + AuthenticationContext::Kerberos(_) => format!( + "http://{}:{}/wsman/subscriptions/{}", + collector.hostname(), + collector.listen_port(), + subscription_data.version() + ), + AuthenticationContext::Tls(_,_) => format!( + "https://{}:{}/wsman/subscriptions/{}", + collector.hostname(), + collector.listen_port(), + subscription_data.version() + ) + }, connection_retry_count: subscription_data.connection_retry_count(), connection_retry_interval: subscription_data.connection_retry_interval(), max_time: subscription_data.max_time(), max_envelope_size: subscription_data.max_envelope_size(), + thumbprint: match auth_ctx { + AuthenticationContext::Tls(_, thumbprint) => Some(thumbprint.clone()), + AuthenticationContext::Kerberos(_) => None + } }; res_subscriptions.push(SoapSubscription { @@ -432,12 +445,13 @@ pub async fn handle_message( heartbeat_tx: mpsc::Sender, request_data: RequestData, message: &Message, + auth_ctx: &AuthenticationContext, ) -> Result { let action = message.action()?; debug!("Received {} request", action); if action == ACTION_ENUMERATE { - handle_enumerate(collector, &db, subscriptions, &request_data) + handle_enumerate(collector, &db, subscriptions, &request_data, auth_ctx) .await .context("Failed to handle Enumerate action") } else if action == ACTION_END || action == ACTION_SUBSCRIPTION_END { diff --git a/server/src/soap.rs b/server/src/soap.rs index 8d40f98..7d8311e 100644 --- a/server/src/soap.rs +++ b/server/src/soap.rs @@ -25,6 +25,7 @@ pub const ANONYMOUS: &str = "http://schemas.xmlsoap.org/ws/2004/08/addressing/ro pub const RESOURCE_EVENT_LOG: &str = "http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog"; pub const SPNEGO_KERBEROS: &str = "http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/http/spnego-kerberos"; +pub const HTTPS_MUTUAL: &str = "http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual"; pub const EVENT_QUERY: &str = "http://schemas.microsoft.com/win/2004/08/events/eventquery"; pub const ACTION_EVENTS: &str = "http://schemas.dmtf.org/wbem/wsman/1/wsman/Events"; @@ -91,6 +92,7 @@ pub struct SubscriptionBody { pub connection_retry_count: u16, pub max_time: u32, pub max_envelope_size: u32, + pub thumbprint: Option, } impl Serializable for SubscriptionBody { @@ -151,17 +153,56 @@ impl Serializable for SubscriptionBody { .write_inner_content(|writer| { writer .create_element("c:All") + // if thumbprint is defined, then we are using Tls .write_inner_content(|writer| { - writer - .create_element( - "auth:Authentication", - ) - .with_attribute(( - "Profile", - SPNEGO_KERBEROS, - )) - .write_empty()?; - Ok(()) + if let Some(tmb) = &self.thumbprint { + // ---- BEGIN TLS ---- // + writer + .create_element( + "auth:Authentication", + ) + .with_attribute(( + "Profile", + HTTPS_MUTUAL, + )) + .write_inner_content(|writer| { + writer + .create_element( + "auth:ClientCertificate", + ) + .write_inner_content(|writer| { + writer + .create_element( + "auth:Thumbprint", + ) + .with_attribute(( + "Role", + "issuer", + )) + .write_text_content(BytesText::new( + tmb, + ))?; + Ok(()) + })?; + Ok(()) + })?; + Ok(()) + // ----- END TLS ----- // + } + else { + // ---- BEGIN KRB ---- // + writer + .create_element( + "auth:Authentication", + ) + .with_attribute(( + "Profile", + SPNEGO_KERBEROS, + )) + .write_empty()?; + Ok(()) + // ----- END KRB ----- // + } })?; Ok(()) })?; diff --git a/server/src/tls.rs b/server/src/tls.rs new file mode 100644 index 0000000..3cfde94 --- /dev/null +++ b/server/src/tls.rs @@ -0,0 +1,371 @@ +use anyhow::{bail, Context, Result}; +use common::encoding::encode_utf16le; +use hex::ToHex; +use log::{debug, info}; +use rustls::server::AllowAnyAuthenticatedClient; +use rustls::{PrivateKey, RootCertStore, ServerConfig}; +use sha1::{Digest, Sha1}; +use std::fs; +use std::io::BufReader; +use std::sync::Arc; +use x509_parser::oid_registry::OidRegistry; +use x509_parser::prelude::{FromDer, X509Certificate}; + +use crate::sldc; + +/// Load certificates contained inside a PEM file +fn load_certs(filename: &str) -> Result> { + let certfile = fs::File::open(filename)?; + let mut reader = BufReader::new(certfile); + + debug!("Loaded certificate {:?}", filename); + + Ok(rustls_pemfile::certs(&mut reader)? + .iter() + .map(|v| rustls::Certificate(v.clone())) + .collect()) +} + +/// Load private key contained inside a file +fn load_priv_key(filename: &str) -> Result { + let keyfile = fs::File::open(filename).context("Cannot open private key file")?; + let mut reader = BufReader::new(keyfile); + + debug!("Loading private key {:?}", filename); + + loop { + match rustls_pemfile::read_one(&mut reader).context("Cannot parse private key file")? { + Some(rustls_pemfile::Item::RSAKey(key)) => return Ok(PrivateKey(key)), + Some(rustls_pemfile::Item::PKCS8Key(key)) => return Ok(PrivateKey(key)), + Some(rustls_pemfile::Item::ECKey(key)) => return Ok(PrivateKey(key)), + None => break, + _ => {} + } + } + + bail!( + "No keys found in {:?} (encrypted keys not supported)", + filename + ) +} + +/// Certificate thumbprint = SHA-1 of entire certificate, as a hexadecimal string +pub fn compute_thumbprint(cert_content: &[u8]) -> String { + let mut hasher = Sha1::new(); + hasher.update(cert_content); + let shasum_it = hasher.finalize(); + + shasum_it.encode_hex::() +} + +pub struct TlsConfig { + pub server: Arc, + pub thumbprint: String, +} + +/// Create configuration for TLS connection +pub fn make_config(args: &common::settings::Tls) -> Result { + let cert = + load_certs(args.server_certificate()).context("Could not load server certificate")?; + let priv_key = + load_priv_key(args.server_private_key()).context("Could not load private key")?; + let ca_certs = load_certs(args.ca_certificate()).context("Could not load CA certificate")?; + + let ca_cert_content: &[u8] = ca_certs + .first() + .context("CA certificate should contain at least one certificate")? + .as_ref(); + let thumbprint = compute_thumbprint(ca_cert_content); + + debug!("CA Thumbprint from certificate : {}", thumbprint); + + let mut client_auth_roots = RootCertStore::empty(); + + // stock all certificates from given CA certificate file into certificate store + for root in ca_certs { + client_auth_roots + .add(&root) + .context("Could not add certificate to root of trust")?; + } + + // create verifier : does not allow unauthenticated clients + // and authenticated clients must be certified by one of the listed CAs + let client_cert_verifier = AllowAnyAuthenticatedClient::new(client_auth_roots).boxed(); + + // make config + let mut config: ServerConfig = ServerConfig::builder() + // Allow everything available in rustls for maximum support + .with_cipher_suites(rustls::ALL_CIPHER_SUITES) + .with_safe_default_kx_groups() + .with_protocol_versions(rustls::ALL_VERSIONS) + .context("Could not build configuration defaults")? + .with_client_cert_verifier(client_cert_verifier) // add verifier + .with_single_cert(cert, priv_key) // add server vertification + .context("Bad configuration certificate or key")?; + + // any http version is ok + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()]; + + info!( + "Loaded TLS configuration with server certificate {}", + args.server_certificate() + ); + + Ok(TlsConfig { + server: Arc::new(config), + thumbprint, + }) +} + +/// Get machine name from certificate +pub fn subject_from_cert(cert: &[u8]) -> Result { + // load certificate to decompose its content + let cert = X509Certificate::from_der(cert)?.1; + + let oid_registry = OidRegistry::default().with_x509(); // registry of OIDs we will need + let rdn_iter = cert.subject.iter_rdn(); // iterator on RDNs + + for subject_attribute in rdn_iter { + // Each entry contains a list of AttributeTypeAndValue objects, + // so we fetch their attribute type (= the OID representing each of them) + let sn = subject_attribute.iter(); + + for set in sn { + // OID of the sub-entry + let typ = set.attr_type(); + + // get the SN corresponding to the OID (None if it does not exist) + let oid_reg = oid_registry.get(typ).map(|oid| oid.sn()); + + // the value we are interested in is only contained where the commonName is + if oid_reg == Some("commonName") { + // get data as text => FQDN of the client + if let Ok(name) = set.as_str() { + return Ok(name.to_string()); + } else { + bail!("CommonName is empty") + } + } + } + } + bail!("CommonName not found") +} + +/// Read and decode request payload +pub async fn get_request_payload( + parts: http::request::Parts, + data: hyper::body::Bytes, +) -> Result>> { + let payload = data.to_vec(); + + let message = match parts.headers.get("Content-Encoding") { + Some(value) if value == "SLDC" => sldc::decompress(&payload).unwrap_or(payload), + None => payload, + value => bail!("Unsupported Content-Encoding {:?}", value), + }; + + Ok(Some(message)) +} + +/// Encode payload for response +pub fn get_response_payload(payload: String) -> Result> { + encode_utf16le(payload).context("Failed to encode payload in utf16le") +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + #[test] + /// Test thumprint computation + fn test_thumbprint() { + let cert = b"0\x82\x02\xb50\x82\x02;\xa0\x03\x02\x01\x02\x02\x14g\x0c\x8d\xf7\t*\xb9\x04\x9b\xb2\x13\xf0H\xe9\x9a\x0fW5\xb0\x050\n\x06\x08*\x86H\xce=\x04\x03\x020W1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x1e\x17\r230825080210Z\x17\r230924080210Z0W1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0v0\x10\x06\x07*\x86H\xce=\x02\x01\x06\x05+\x81\x04\0\"\x03b\0\x04\xd8(@\xad\x9c\xa3\xe3\xb3\x14_\x8a-\xa3\x0fz\xbc_7|\x9cac\xc4`\x8f\xff\x0e\xe9\xadj:\x7fP\xdb\xf3\xb3\xdb$\xb5\xd9\xf4Xo\xae\xfa\xadlD\xdc+x\xf7s=\xbdi\x11\xc4u\0@\xdf\xc2\x86\xdb\xbe\xc4\x1f\x9bcc\xe8\xacnI\xe68\xa5vI\x9b\x99\xab\xc4\xa8\x10s\xe7\xcb\x7f\xa9\xc4\xf0\xc4\x97\x0b\xa3\x81\xc70\x81\xc40\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xfcL\x81rZ\x85\xd8\x1bI\xe4\xc3\xa7\x8bR\x1e\xb3\x19\xa9\xc4\x170\x81\x94\x06\x03U\x1d#\x04\x81\x8c0\x81\x89\x80\x14\xfcL\x81rZ\x85\xd8\x1bI\xe4\xc3\xa7\x8bR\x1e\xb3\x19\xa9\xc4\x17\xa1[\xa4Y0W1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location\x82\x14g\x0c\x8d\xf7\t*\xb9\x04\x9b\xb2\x13\xf0H\xe9\x9a\x0fW5\xb0\x050\x0c\x06\x03U\x1d\x13\x04\x050\x03\x01\x01\xff0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03h\00e\x021\0\x85)\xf7F\x88\xb5b\xdc&8\xfd\xae\xbe}\xd5Y\x83\x1ft\xe6\xf6\xdb!\xfco\x13\x17\xf0YM\\\xbb\x9c\xff\x12\xa2)\xc8\xc3\xb1u\x9eW,*1\xe2h\x020\x16\xe2|\xe0\x1cPJ\xde\x9d\"\xfa\xc3\ty\x06\x04\xf7\xe67z\x93\xa6tp9\xde\xa2\xee\xcfM\x95\x02DQzx$g\xc9\xf0\xaf\xf7;Vk\xef\xad{"; + let thumbprint = crate::tls::compute_thumbprint(cert).to_uppercase(); + assert_eq!(thumbprint, "6A4720A83504B818C86BAC099D3A4BEDE89945D9"); + + let cert = b"0\x82\x02\xde0\x82\x02c\xa0\x03\x02\x01\x02\x02\x01\x010\n\x06\x08*\x86H\xce=\x04\x03\x020W1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x1e\x17\r230825080339Z\x17\r260614080339Z0`1\x180\x16\x06\x03U\x04\x03\x13\x0fWIN-KD7H02SOMLJ1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0v0\x10\x06\x07*\x86H\xce=\x02\x01\x06\x05+\x81\x04\0\"\x03b\0\x04W\x1d\xca\xf7};\x1a\x9c1\xbe\x11\x81\x05B0\xc7%6^\xa9\xb9\x90\xcb\x94\xfc^\x9c\xd2\x0c\xb2{\xac\x1b]-\xcd\xb2\x90\x0fnW\t@8\x7f#\x9fM\xa6\x17\x12\xfc$\xa1w\xdb0\xf6\xb8\xa6[\xf9\x8b\xfa\xa5\xfda\xde\xa9\x82\xd1\xd9\xbc\xb2\xac\xfb\x96mX\xe7\x16;v\xbclf8\xfe)\xa2\x06\xba\x81~\xa4\xa7\xa3\x81\xf90\x81\xf60\t\x06\x03U\x1d\x13\x04\x020\00\x11\x06\t`\x86H\x01\x86\xf8B\x01\x01\x04\x04\x03\x02\x07\x800\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14uq\x8d+\xda\xd0\xb9\x9aA\xb3\xe4+ S\x06\x84\x05\xdc\xa1q0\x81\x94\x06\x03U\x1d#\x04\x81\x8c0\x81\x89\x80\x14\xfcL\x81rZ\x85\xd8\x1bI\xe4\xc3\xa7\x8bR\x1e\xb3\x19\xa9\xc4\x17\xa1[\xa4Y0W1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location\x82\x14g\x0c\x8d\xf7\t*\xb9\x04\x9b\xb2\x13\xf0H\xe9\x9a\x0fW5\xb0\x050\x0b\x06\x03U\x1d\x0f\x04\x04\x03\x02\x05\xa00\x13\x06\x03U\x1d%\x04\x0c0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x020\n\x06\x08*\x86H\xce=\x04\x03\x02\x03i\00f\x021\0\x94;\xa2s\x9czu\x9d$\xee\xe6:[\xfa+Y\xbaF\x01\xb2n\x12\x01\xb3F>\xe5\xf9B\xff\xa1\x96\xa7*\xe2\xf9\x01q\x8a\xc3k\xfc\xb3'\xba\x89\xe3\xdd\x021\0\xc9KU\xde\xb2\xe4\xcd[\x08c\x04*;\x93\x0bw\xeck\x94\xe41\xa6 M\xf0\xa7\xd4l\x04\x84\xc1\x80W\xb9\xcf\xb6BN\xe4\xcb\xa3\xc25\xd9\xcb1\x14\xcd"; + let thumbprint = crate::tls::compute_thumbprint(cert).to_uppercase(); + assert_eq!(thumbprint, "EAE93EA8A4CC386849A873D3B9E5EE57886A1603"); + } + + #[test] + /// Test retrieving subject name from certificate + fn test_get_subject_from_cert() { + use super::*; + + // Non-empty subject + let certificate = b"0\x82\x02\xae0\x82\x025\xa0\x03\x02\x01\x02\x02\x14qX\xbd\x8c\\\xbb\xa7y\xbd\x13\xf3\xcb\xb1f}\xc3\xca!\xdf\x9f0\n\x06\x08*\x86H\xce=\x04\x03\x020U1\r0\x0b\x06\x03U\x04\x03\x13\x04subj1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x1e\x17\r230828081803Z\x17\r230927081803Z0U1\r0\x0b\x06\x03U\x04\x03\x13\x04subj1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0v0\x10\x06\x07*\x86H\xce=\x02\x01\x06\x05+\x81\x04\0\"\x03b\0\x04\xd4\xf3G\xdeMT\xd1\x8d\x9bf\xb5m\xe4[%\xa7\xe7\xc6\xc2\xdd}\xef\x826\x95|gT\x8a\xcb\xbetP\xb7\x98\xd4St\x1ex%\x7fD\xf3\x0f|\x1c\xf6\xa5\xa6(\xc5\x9f\xbaJz8AV\xa9\x03&\xf8\x02\xa3\xf8\xd9#\x83\xe3\x0e\xd9;^\x87\xf6\xb0\xdc\x98\xc7Y\x15w\x18\x10\xbc\x11\xca\x08\x0c\xa2\x10\xfa\xa3\xf1X\xa3\x81\xc50\x81\xc20\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xbe\xce\x1f\xb7\xad\xe7\x8d\x07\xb5\xe6\xb2\xa1\xfd\xf5Z\xd0?\x0ey`0\x81\x92\x06\x03U\x1d#\x04\x81\x8a0\x81\x87\x80\x14\xbe\xce\x1f\xb7\xad\xe7\x8d\x07\xb5\xe6\xb2\xa1\xfd\xf5Z\xd0?\x0ey`\xa1Y\xa4W0U1\r0\x0b\x06\x03U\x04\x03\x13\x04subj1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location\x82\x14qX\xbd\x8c\\\xbb\xa7y\xbd\x13\xf3\xcb\xb1f}\xc3\xca!\xdf\x9f0\x0c\x06\x03U\x1d\x13\x04\x050\x03\x01\x01\xff0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03g\00d\x020Gn\xb7F\xf9\x06<\xf6\x19\xc0\xdaX\xa6\xf2\x19\x04h\x1bB\x0fIx\xfa\x18{\x80\x94$B\x93\xe9T\x91\xa7\xb53\xb6\xfa-\xe2\x17\xbb\x86PZJ\x98\x93\x020\x08}-\xa7b\x04~\xaa?\xc6\xe5\xec9k'\x8b\xc5\xa2\x15\x1c\x8b\xfb6w4\xf2\xceY<\x8f\t\xd1\xc3\x93l\xc6\x9d\x98\x8eBN>#6\xe9\xf8\x91\xbe"; + let subject = "subj"; + assert_eq!(subject, subject_from_cert(certificate).unwrap()); + + // Other type of private key + let certificate = b"0\x82\x04&0\x82\x03\x0e\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\00Z1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x170\x15\x06\x03U\x04\n\x13\x0eca.stage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x1e\x17\r230811075732Z\x17\r260531075732Z0T1\x0c0\n\x06\x03U\x04\x03\x13\x03win1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x82\x01\"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\0\x03\x82\x01\x0f\00\x82\x01\n\x02\x82\x01\x01\0\xf3\xed\x9a\x9f\xb0\xbd\x8a\xe3\x86\xa9\xe2)8\x1a\xb4[\r\xb3[\xf3\x81$\x83\xd1\xa8\x15\xe2\xe9\xf8\x8d\xd8\xcbH\xd5\xd0m\x8b-\xdd\xf8\xd2\xa1\xd8$\x11c&\x1fd\x81\xa6\xd4\x10=8\x17X\xa9\xfe\x06\xda*)x\x91y\xb9ZS\xea\x90\x17\xe2\xfdyYL\x8e\xc2\xdf\xe6\xfd\x0e\x11\xdat0\x08\x14\xc3\x91\xb4\x15$)#T)\xa1\x9cG|\x8e]Z\x08\xc6\xb0\x1c\x7f\xfd\xe7\xac\xbc\xba\xb6\x8am\xfc}1W\x0e\x95\x91\xc4p\xf5\x99F\x191\xdcB\xd0\xf3H\xcf>6\x9eR\xfc\xef=\x80\xe2\xaa\x18\xe2\x14\x9f5\xfe\0=\0\xd36\xcc\xe4\n\x0e\xc7%M\xe4X[XP\xe3\xf6\xd4~\x89\x1eC\x90\xc1\xa1\xa6\xb7oZ\x04l\xb2A\xf5\x94!&\xf1\xaf\xc0q\xc9F\xf8L\x8c1\xee\x84\x85>\xf4\xea\xcf\xf4\x1fp<\x83\xcb\xb0\x98\x18\xe84d\xf9\x90\xa2\xae\xd2X\x85\x94fS\x96\x03b\xd6\x83Y\xcf\xae\xc1\x90\x06\x80\xc4\x0e\xcf\x9f\xa9T\xf7\x15\x945y1\x02\x03\x01\0\x01\xa3\x81\xfc0\x81\xf90\t\x06\x03U\x1d\x13\x04\x020\00\x11\x06\t`\x86H\x01\x86\xf8B\x01\x01\x04\x04\x03\x02\x07\x800\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xd0\xe3W\xae\x93\xd2_\xc8o\xb0\xbb\xc9\xe7\x9fH\xa6\xeaU#\x0f0\x81\x97\x06\x03U\x1d#\x04\x81\x8f0\x81\x8c\x80\x14M\xf0-M\xaa\x02\xab\xc7\xa5\xc4\xed\xb2\xcc\xf5H\x7fC\xaeZ\xf7\xa1^\xa4\\0Z1\x0f0\r\x06\x03U\x04\x03\x13\x06WEF-CA1\x170\x15\x06\x03U\x04\n\x13\x0eca.stage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location\x82\x14\r\x08\xc4\xe1\xea)\x1f\xe0\n\x80\xcf\x8a\x9c.:)y7\xc1\xff0\x0b\x06\x03U\x1d\x0f\x04\x04\x03\x02\x05\xa00\x13\x06\x03U\x1d%\x04\x0c0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x020\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\0\x03\x82\x01\x01\0\x8b\xb8IE\xb8\x98)\x9e\r\xdc\xe1\xba\xca\xb5 -\xc50\x81\x81\x06\x03U\x1d#\x04z0x\x80\x14\xbe\xce\x1f\xb7\xad\xe7\x8d\x07\xb5\xe6\xb2\xa1\xfd\xf5Z\xd0?\x0ey`\xa1J\xa4H0F1\x140\x12\x06\x03U\x04\n\x0c\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x0c\x05state1\x110\x0f\x06\x03U\x04\x07\x0c\x08location\x82\x14\x13\ti\x0b\x0b\x95~\xe1X\x03\x99V3\x95\xceruHOx0\x0b\x06\x03U\x1d\x0f\x04\x04\x03\x02\x05\xa00\x13\x06\x03U\x1d%\x04\x0c0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x010\n\x06\x08*\x86H\xce=\x04\x03\x02\x03h\00e\x021\0\xd2\xad\xa3\x01\xc6\xee\xc80\"\x81\x14\xb7?A\xd9\xae\n\xe5`pne\xdd\x9b\xbc\xfevfO\xf6\x8e\x92i\xe2\x99\xa4s\x07r\xb4r\x13\xaeZ\nJ\xf6q\x020!\xf6\xc3\xe8\xec\xd6Z\xe90b\x08\xa3\x8c\xc2~\xb0y\xe2\xe1?V\x99\x87H\x96Q|\x14\x17\x14\x14x)}\xa3$z\xc7\xff\xbe\x080#?\x06\x85L4"; + let subject = "aaaaa"; + assert_eq!(subject, subject_from_cert(certificate).unwrap()); + + // CA + let certificate = b"0\x82\x04\x100\x82\x02\xf8\xa0\x03\x02\x01\x02\x02\x14~\xa1W\x8c\xb4\\\xe9S\x80\xa3\xb4\xce\xc2\xaa\xa3yg\xb9:u0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\00[1\x130\x11\x06\x03U\x04\x03\x13\nca-machine1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x1e\x17\r230829090243Z\x17\r230928090243Z0[1\x130\x11\x06\x03U\x04\x03\x13\nca-machine1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x82\x01\"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\0\x03\x82\x01\x0f\00\x82\x01\n\x02\x82\x01\x01\0\xb8\xe1\xb4\xd6\xa1\xe5\x1a\x0b3\xc5\xb2_\x04\x0f\xd7\x18\xb1d'\\\xa9X\xc8\xef\x9d72\xf7+\x0b\xed\xe4ik\xf4\x8e\x9a\x8a\xf5\xa8\xe1\x18>\xd1\xd6\xd3\xb0\xeb\xf2y\xaf\xdc\xb3\x04%\x87\xc7\xb7\xa5\x12\x1f\xcd\xa1=D\xc8x9\x03\xab``\xf79MO\xea\x15\xb0`Z\x96\x10\xb7\xb1\x99\xb1\xf9\xdd\x03q+\xf0\x16\x914\xda\xbcH\xa8\xa0j\xc2\xdeE\xe6iO\x9c\x07}x.\xfa\x91\x0c\xdb\xb0\xfcF\xe3\xd4m_\x03~\xbe\xd4[\xe2[\xa8\xa6\x99zZ\xdb\x9c\xa6\xa1\x04\x89\xe4\xd5\x99\xb7\xcd\xfb\xe5\xbcB\x13\x19\x87c\xb5\x11\x0bW\x8c\xf2\x83;|\"nq\x89\x17uU\xbaZX=\xbc\x99\xf5\xfc: \x0c\xf9\xc8\x11\x8d\xf0&\x82.L\xbe\xf7\xfe\xf3\xad\t\xfa\xfb\xc5\x1d\x81m\x93\xae<\x12\xb0\xc63\xba\xd2\xc5\xb0(\x06\x15X2\xc6\xeaS\xe9\xb4{\xf4:\xe0!\xea\xe1-K[I^\xe7\xa1a\x9f5$\x1fN\xd9\xa6ZX\xbe\x90\xed\xb9\x0f0\x89e\x02\x03\x01\0\x01\xa3\x81\xcb0\x81\xc80\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xd1k\x12/\xa1~\xe2I9C\x1d|\xb7\xcf\x8a\xf75\xe8\x02q0\x81\x98\x06\x03U\x1d#\x04\x81\x900\x81\x8d\x80\x14\xd1k\x12/\xa1~\xe2I9C\x1d|\xb7\xcf\x8a\xf75\xe8\x02q\xa1_\xa4]0[1\x130\x11\x06\x03U\x04\x03\x13\nca-machine1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location\x82\x14~\xa1W\x8c\xb4\\\xe9S\x80\xa3\xb4\xce\xc2\xaa\xa3yg\xb9:u0\x0c\x06\x03U\x1d\x13\x04\x050\x03\x01\x01\xff0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\0\x03\x82\x01\x01\0O\x16\x9f\x97\x1dZ\x82^\xb2\xaa\xd0\xd7mAF\x12\xcaC\xfd2_\xfd\x91\xfc\xe9\xfceG\xdb\xf8d\xdfc\xef\x9c\xfa\x84BI\x18\x19\x99\x19\xe2\xde\xaf\xdf\xad\xbc_\x16\xcd\xa4;\xd5'\xdbk\x15\0g\xd3\xd9\x9fs\xd4\xc6\xdf\t9\x07Z\x102\x80\x1fC\xd96;\x89\xe9\x05\xd7\x93T(*\xd5\x99\xaeR\xe4C\xdd\x89\x1f\x91\xaf\x95\xd3\x82l\xd3V\xdc\\:g\xc8\xe7`\xbcZo\xbe\xd8\xd3y\x1f\x82\xe2\xe8\x94\xcc\xa7KYg\xdb\xa1\xbc\x08\xfd\xd9+\xbaq\xb7-}\xf9k\x01_\xe2\x8a\xc4\x8d\xfad\xcf\xf4\x0c\xce\xa4\xff\x90K\x88{\x7fK\xacT\x85\xf7t\x18\xb4\x02 \xe5\xf7bK\x85\xc7|\xdb\xe1\x14\xd1\xba9=P\xf5M\xe1LOz\xa8i\xf1\x85\xc6\x108\xc9\xc8g\x7fk\xf5\xff|\xc9j\n\x92\x0c\\\xe1\xa7\xdf\xc5\x1c\xda\xcf\xe4\xda\x96#\xa5\xa79S\xd3&3\x85\xbaU \x8a\x8d\xb1\xe4*;\x9a\xddSuj6\xe1\xcab\xf8a\xf64d"; + let subject = "ca-machine"; + assert_eq!(subject, subject_from_cert(certificate).unwrap()); + } + + #[test] + #[should_panic(expected = "CommonName")] // XXX : panics not as we thought but still panics + /// Test retrieving subject name from certificate + fn test_get_subject_empty() { + use super::*; + + // Empty subject + let certificate = b"0\x82\x02\x1e0\x82\x01\xa4\xa0\x03\x02\x01\x02\x02\x14\x13\ti\x0b\x0b\x95~\xe1X\x03\x99V3\x95\xceruHOx0\n\x06\x08*\x86H\xce=\x04\x03\x020F1\x140\x12\x06\x03U\x04\n\x0c\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x0c\x05state1\x110\x0f\x06\x03U\x04\x07\x0c\x08location0\x1e\x17\r230828084524Z\x17\r230927084524Z0F1\x140\x12\x06\x03U\x04\n\x0c\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x0c\x05state1\x110\x0f\x06\x03U\x04\x07\x0c\x08location0v0\x10\x06\x07*\x86H\xce=\x02\x01\x06\x05+\x81\x04\0\"\x03b\0\x04\xd4\xf3G\xdeMT\xd1\x8d\x9bf\xb5m\xe4[%\xa7\xe7\xc6\xc2\xdd}\xef\x826\x95|gT\x8a\xcb\xbetP\xb7\x98\xd4St\x1ex%\x7fD\xf3\x0f|\x1c\xf6\xa5\xa6(\xc5\x9f\xbaJz8AV\xa9\x03&\xf8\x02\xa3\xf8\xd9#\x83\xe3\x0e\xd9;^\x87\xf6\xb0\xdc\x98\xc7Y\x15w\x18\x10\xbc\x11\xca\x08\x0c\xa2\x10\xfa\xa3\xf1X\xa3S0Q0\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xbe\xce\x1f\xb7\xad\xe7\x8d\x07\xb5\xe6\xb2\xa1\xfd\xf5Z\xd0?\x0ey`0\x1f\x06\x03U\x1d#\x04\x180\x16\x80\x14\xbe\xce\x1f\xb7\xad\xe7\x8d\x07\xb5\xe6\xb2\xa1\xfd\xf5Z\xd0?\x0ey`0\x0f\x06\x03U\x1d\x13\x01\x01\xff\x04\x050\x03\x01\x01\xff0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03h\00e\x021\0\xd54!\xddc\xb1\xa4\xf6S-Q_]\xa5\xe7\xd2\xf4B\x969R^\xcd\x9d\xa1\xa5\"\xf5\t1]c\x19\xeb)\xce\xc2jd\xf5#\x14t\x0e\x16\x8b_$\x020o\xe1\xd1\x94PC\x05\xb1<\xe3Ch\xd5\x19\x80\\gHH\xc5>d\xc5\x1a\x80\x18a\x82\xc4q\x94i\xa7\xd04$\x89\xdc\xfe\xc6Q\xe9\xdf`\xceM\xbd\xa9"; + subject_from_cert(certificate).unwrap(); + } + + #[test] + #[should_panic(expected = "CommonName not found")] + /// Test retrieving subject name from certificate + fn test_get_no_subject() { + use super::*; + + // No subject + let certificate = b"0\x82\x02\x810\x82\x02\x06\xa0\x03\x02\x01\x02\x02\x14C\x1e\xffg{G\x9a\xa9ovZ'E\xd8\r\x1cds.\x900\n\x06\x08*\x86H\xce=\x04\x03\x020F1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0\x1e\x17\r230830084659Z\x17\r230929084659Z0F1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location0v0\x10\x06\x07*\x86H\xce=\x02\x01\x06\x05+\x81\x04\0\"\x03b\0\x04\xed\x80@\x80!.`\x06\xcd\xae]&\xc0\x8c.87\xc2\x14\x9da\xe75\x8e\xbcET\xda\x93\x0bM\xa0\x19O7cyC/\xff\xf3\x81\x10\xe2o\xe44\x029XE\x0c\xaaa\x95n\xb6\xb7\xcf\x90\x05\x95\xdcDn \xad\x1f\xe5\xda\xf8\x0e|\08>4'W\xf1k`\x85\xdf\xff}<\x0c\x8d\xc0q.\x10&\xfa<\xa3\x81\xb40\x81\xb10\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x146\xcfC0\x06\x868%4\xd9\xad\\dV&\xec\x89z\xb0\xce0\x81\x81\x06\x03U\x1d#\x04z0x\x80\x146\xcfC0\x06\x868%4\xd9\xad\\dV&\xec\x89z\xb0\xce\xa1J\xa4H0F1\x140\x12\x06\x03U\x04\n\x13\x0bstage.local1\x0b0\t\x06\x03U\x04\x06\x13\x02FR1\x0e0\x0c\x06\x03U\x04\x08\x13\x05state1\x110\x0f\x06\x03U\x04\x07\x13\x08location\x82\x14C\x1e\xffg{G\x9a\xa9ovZ'E\xd8\r\x1cds.\x900\x0c\x06\x03U\x1d\x13\x04\x050\x03\x01\x01\xff0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03i\00f\x021\0\x9dY\x02N\x98\xacU\x92\x96\x0c\xe6\x9d\x9c\xb6\xc1\xf3\x1e\xb6\x8a]\xf5\xf9\x84\xb2p\xaa\x80\x80\xdd@\xa0nG\\\xf8\xb6\xb6P\r=\xd4\x95\xc7\xc4j\xed\xa8\x8a\x021\0\xefM8\x86?d\x8c\xbf\xf7\x8c\xdc\x97\xb0\xd1\xc3\x05\x88\xc7\xa8\x87?\xc1\xf8\xa98;N\xa1r\xb9,\xf0\x0c\xd1YZ\xf36{!\x8c\xa1\xe1gQ\xa4\x01\xca"; + subject_from_cert(certificate).unwrap(); + } + + #[test] + #[should_panic(expected = "(encrypted keys not supported)")] + /// Test trying to load private key from a file that contains none (for instance a certificate) + fn test_load_private_key_no_keys() { + use super::*; + + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + path.pop(); + path.push("tests/certs/no_key.pem"); + load_priv_key(&path.into_os_string().into_string().unwrap()).unwrap(); + } + + #[test] + #[should_panic(expected = "Cannot parse private key file")] + /// Test trying to load private key from a malformed file (for instance missing the '--- END KEY ---' line) + fn test_load_private_key_wrong_format() { + use super::*; + + // let mut file = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + // file.push_str("/../tests/certs/wrong_format.pem"); + // let _key = load_priv_key(&file); + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + path.pop(); + path.push("tests/certs/wrong_format.pem"); + load_priv_key(&path.into_os_string().into_string().unwrap()).unwrap(); + } + + #[test] + /// Test loading proper private keys from files + fn test_load_private_key() { + use super::*; + + // content = base64 -d <<< "" | hexdump => list of integers, without final '0x00' if present + + // ecdsa + // let mut file = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + path.pop(); + path.push("tests/certs/key_ecdsa.pem"); + let key = load_priv_key(&path.into_os_string().into_string().unwrap()); + // file.push_str("/../tests/certs/key_ecdsa.pem"); + // let key = load_priv_key(&path); + let content = [ + 48, 129, 164, 2, 1, 1, 4, 48, 187, 86, 151, 53, 184, 134, 70, 143, 168, 81, 239, 30, + 69, 203, 72, 236, 115, 154, 49, 20, 241, 27, 231, 109, 68, 27, 44, 190, 87, 125, 26, + 53, 211, 177, 30, 133, 50, 219, 198, 214, 162, 185, 145, 56, 34, 246, 96, 225, 160, 7, + 6, 5, 43, 129, 4, 0, 34, 161, 100, 3, 98, 0, 4, 212, 243, 71, 222, 77, 84, 209, 141, + 155, 102, 181, 109, 228, 91, 37, 167, 231, 198, 194, 221, 125, 239, 130, 54, 149, 124, + 103, 84, 138, 203, 190, 116, 80, 183, 152, 212, 83, 116, 30, 120, 37, 127, 68, 243, 15, + 124, 28, 246, 165, 166, 40, 197, 159, 186, 74, 122, 56, 65, 86, 169, 3, 38, 248, 2, + 163, 248, 217, 35, 131, 227, 14, 217, 59, 94, 135, 246, 176, 220, 152, 199, 89, 21, + 119, 24, 16, 188, 17, 202, 8, 12, 162, 16, 250, 163, 241, 88, + ]; + assert_eq!(key.unwrap(), PrivateKey(content.to_vec())); + + // rsa + // let mut file = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + path.pop(); + path.push("tests/certs/key_rsa.pem"); + let key = load_priv_key(&path.into_os_string().into_string().unwrap()); + // file.push_str("/../tests/certs/key_rsa.pem"); + // let key = load_priv_key(&path); + let content = [ + 48, 130, 4, 189, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, + 130, 4, 167, 48, 130, 4, 163, 2, 1, 0, 2, 130, 1, 1, 0, 208, 9, 205, 253, 201, 187, + 213, 10, 117, 191, 62, 81, 134, 149, 52, 173, 49, 12, 0, 71, 60, 5, 228, 179, 32, 90, + 14, 210, 195, 64, 177, 254, 103, 143, 136, 214, 113, 32, 49, 181, 45, 184, 163, 125, + 193, 193, 0, 239, 196, 45, 20, 129, 83, 88, 123, 89, 48, 18, 171, 121, 32, 212, 133, + 116, 208, 10, 194, 38, 103, 45, 47, 124, 151, 159, 131, 200, 55, 188, 47, 154, 64, 83, + 251, 251, 216, 255, 97, 178, 213, 205, 215, 104, 127, 119, 166, 131, 238, 245, 215, 63, + 30, 197, 72, 133, 230, 238, 162, 203, 246, 173, 215, 176, 48, 26, 143, 114, 105, 59, + 18, 215, 168, 76, 134, 113, 154, 28, 102, 140, 128, 127, 133, 160, 111, 124, 155, 24, + 104, 206, 145, 79, 98, 25, 37, 16, 49, 228, 27, 170, 5, 105, 198, 87, 43, 83, 79, 89, + 187, 182, 207, 196, 24, 114, 134, 89, 125, 90, 152, 154, 195, 152, 52, 14, 107, 96, 88, + 107, 33, 36, 236, 216, 56, 0, 122, 241, 129, 19, 59, 54, 103, 40, 209, 72, 211, 63, + 136, 64, 195, 207, 213, 146, 237, 124, 56, 30, 188, 132, 58, 231, 167, 102, 87, 219, + 167, 151, 29, 156, 249, 74, 215, 159, 185, 123, 99, 170, 246, 191, 239, 144, 19, 135, + 99, 92, 205, 37, 239, 173, 210, 163, 78, 83, 79, 146, 217, 160, 41, 212, 173, 147, 129, + 232, 112, 41, 6, 126, 84, 221, 2, 3, 1, 0, 1, 2, 130, 1, 0, 81, 20, 97, 42, 22, 35, + 148, 134, 61, 25, 201, 233, 240, 47, 218, 149, 221, 85, 182, 14, 13, 64, 166, 191, 129, + 78, 88, 20, 160, 112, 104, 110, 164, 97, 246, 140, 205, 14, 37, 17, 93, 190, 102, 73, + 174, 231, 207, 187, 162, 147, 135, 56, 88, 9, 86, 25, 142, 120, 216, 71, 159, 25, 244, + 225, 111, 235, 161, 123, 98, 30, 228, 49, 4, 206, 240, 135, 105, 225, 120, 20, 0, 26, + 59, 77, 14, 103, 137, 230, 47, 25, 200, 104, 59, 181, 160, 58, 47, 57, 181, 40, 46, + 143, 233, 17, 246, 204, 238, 185, 219, 108, 41, 113, 203, 109, 174, 150, 130, 152, 185, + 97, 63, 128, 131, 173, 102, 200, 198, 214, 43, 74, 69, 21, 38, 77, 110, 5, 98, 193, + 185, 124, 249, 97, 161, 89, 153, 30, 42, 243, 22, 136, 240, 213, 225, 58, 83, 131, 159, + 252, 105, 135, 234, 136, 216, 113, 35, 182, 1, 248, 191, 144, 176, 24, 115, 135, 85, + 155, 183, 221, 101, 199, 28, 4, 181, 230, 195, 105, 114, 81, 130, 38, 199, 170, 183, + 19, 52, 173, 54, 169, 78, 213, 162, 223, 66, 163, 13, 58, 56, 234, 220, 179, 202, 169, + 95, 253, 216, 188, 156, 178, 120, 145, 252, 162, 255, 112, 212, 32, 175, 146, 54, 130, + 8, 73, 141, 84, 71, 180, 50, 205, 225, 164, 11, 5, 204, 14, 105, 216, 77, 186, 103, + 246, 210, 68, 244, 252, 169, 233, 2, 129, 129, 0, 245, 159, 80, 103, 124, 221, 239, + 162, 237, 168, 180, 10, 81, 96, 79, 197, 207, 166, 136, 53, 127, 49, 147, 45, 229, 227, + 242, 148, 187, 205, 163, 91, 139, 14, 76, 61, 89, 195, 170, 154, 186, 162, 151, 29, + 145, 233, 69, 73, 13, 26, 5, 125, 78, 210, 70, 29, 243, 172, 30, 25, 105, 87, 29, 102, + 73, 111, 4, 169, 242, 65, 85, 50, 114, 219, 79, 170, 154, 34, 213, 30, 140, 189, 41, + 237, 4, 95, 190, 6, 212, 172, 16, 250, 16, 213, 186, 40, 7, 49, 112, 1, 242, 47, 160, + 137, 68, 23, 80, 110, 78, 185, 62, 239, 47, 227, 115, 220, 123, 58, 105, 32, 70, 228, + 190, 26, 38, 135, 126, 207, 2, 129, 129, 0, 216, 211, 249, 249, 89, 140, 21, 156, 123, + 76, 17, 133, 108, 36, 161, 76, 236, 193, 182, 246, 139, 69, 75, 186, 138, 43, 46, 52, + 91, 96, 176, 103, 120, 29, 158, 4, 52, 9, 13, 41, 198, 49, 156, 196, 223, 164, 42, 96, + 16, 98, 21, 183, 15, 202, 46, 55, 119, 48, 187, 179, 248, 5, 236, 60, 213, 227, 61, 42, + 86, 228, 189, 237, 71, 236, 185, 134, 50, 82, 231, 63, 127, 5, 142, 25, 47, 251, 166, + 154, 81, 115, 113, 243, 92, 221, 150, 140, 220, 209, 172, 104, 103, 107, 76, 31, 28, + 69, 183, 112, 41, 17, 83, 26, 243, 139, 247, 105, 137, 238, 174, 210, 243, 81, 217, + 171, 137, 136, 60, 147, 2, 129, 128, 19, 165, 108, 142, 250, 131, 221, 249, 16, 61, 96, + 57, 59, 13, 19, 20, 101, 105, 146, 151, 132, 214, 248, 72, 193, 140, 156, 8, 157, 132, + 243, 62, 13, 63, 85, 133, 202, 186, 69, 217, 30, 120, 134, 209, 204, 171, 245, 232, + 195, 237, 130, 230, 228, 249, 24, 182, 168, 152, 233, 199, 106, 143, 151, 64, 105, 59, + 66, 10, 61, 224, 79, 234, 59, 25, 163, 163, 167, 180, 133, 139, 110, 2, 107, 106, 19, + 225, 124, 151, 155, 71, 48, 12, 112, 112, 71, 245, 143, 173, 186, 161, 205, 55, 86, 5, + 228, 182, 96, 174, 146, 9, 107, 41, 66, 145, 84, 225, 27, 210, 46, 58, 112, 177, 55, + 43, 108, 77, 134, 45, 2, 129, 128, 33, 209, 190, 85, 164, 31, 243, 102, 250, 220, 60, + 135, 96, 252, 189, 163, 239, 241, 175, 5, 249, 103, 15, 142, 194, 234, 69, 68, 169, 84, + 5, 111, 190, 14, 112, 141, 27, 72, 166, 34, 243, 228, 221, 28, 223, 253, 13, 22, 250, + 183, 49, 199, 225, 208, 153, 48, 209, 136, 106, 94, 129, 186, 250, 195, 234, 96, 141, + 51, 195, 101, 222, 49, 218, 92, 19, 251, 216, 113, 145, 220, 23, 133, 216, 74, 25, 111, + 216, 230, 140, 249, 194, 182, 64, 175, 215, 65, 149, 87, 166, 218, 137, 246, 244, 98, + 141, 216, 89, 234, 70, 157, 139, 38, 211, 1, 235, 207, 44, 82, 108, 54, 62, 249, 111, + 72, 16, 37, 141, 189, 2, 129, 129, 0, 180, 17, 238, 19, 205, 160, 47, 8, 48, 75, 92, + 89, 210, 98, 132, 3, 108, 80, 209, 8, 225, 41, 79, 166, 226, 78, 35, 193, 4, 5, 32, 43, + 63, 148, 239, 69, 132, 107, 138, 65, 162, 162, 179, 100, 54, 92, 55, 3, 33, 104, 227, + 75, 201, 52, 134, 220, 88, 149, 189, 78, 68, 135, 180, 41, 237, 198, 230, 47, 44, 243, + 209, 93, 100, 146, 76, 57, 10, 59, 183, 24, 146, 242, 98, 177, 116, 7, 105, 163, 146, + 78, 251, 42, 139, 154, 212, 55, 81, 121, 95, 100, 162, 1, 37, 95, 68, 141, 203, 80, + 132, 223, 102, 86, 216, 5, 218, 125, 237, 212, 218, 133, 165, 97, 62, 73, 27, 106, 224, + 64, + ]; + assert_eq!(key.unwrap(), PrivateKey(content.to_vec())); + } +} diff --git a/tests/certs/key_ecdsa.pem b/tests/certs/key_ecdsa.pem new file mode 100644 index 0000000..38860ef --- /dev/null +++ b/tests/certs/key_ecdsa.pem @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDC7Vpc1uIZGj6hR7x5Fy0jsc5oxFPEb521EGyy+V30aNdOxHoUy28bW +ormROCL2YOGgBwYFK4EEACKhZANiAATU80feTVTRjZtmtW3kWyWn58bC3X3vgjaV +fGdUisu+dFC3mNRTdB54JX9E8w98HPalpijFn7pKejhBVqkDJvgCo/jZI4PjDtk7 +Xof2sNyYx1kVdxgQvBHKCAyiEPqj8Vg= +-----END EC PRIVATE KEY----- diff --git a/tests/certs/key_rsa.pem b/tests/certs/key_rsa.pem new file mode 100644 index 0000000..884ee93 --- /dev/null +++ b/tests/certs/key_rsa.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQCc39ybvVCnW/ +PlGGlTStMQwARzwF5LMgWg7Sw0Cx/mePiNZxIDG1LbijfcHBAO/ELRSBU1h7WTAS +q3kg1IV00ArCJmctL3yXn4PIN7wvmkBT+/vY/2Gy1c3XaH93poPu9dc/HsVIhebu +osv2rdewMBqPcmk7EteoTIZxmhxmjIB/haBvfJsYaM6RT2IZJRAx5BuqBWnGVytT +T1m7ts/EGHKGWX1amJrDmDQOa2BYayEk7Ng4AHrxgRM7Nmco0UjTP4hAw8/Vku18 +OB68hDrnp2ZX26eXHZz5StefuXtjqva/75ATh2NczSXvrdKjTlNPktmgKdStk4Ho +cCkGflTdAgMBAAECggEAURRhKhYjlIY9Gcnp8C/ald1Vtg4NQKa/gU5YFKBwaG6k +YfaMzQ4lEV2+Zkmu58+7opOHOFgJVhmOeNhHnxn04W/roXtiHuQxBM7wh2nheBQA +GjtNDmeJ5i8ZyGg7taA6Lzm1KC6P6RH2zO6522wpccttrpaCmLlhP4CDrWbIxtYr +SkUVJk1uBWLBuXz5YaFZmR4q8xaI8NXhOlODn/xph+qI2HEjtgH4v5CwGHOHVZu3 +3WXHHAS15sNpclGCJseqtxM0rTapTtWi30KjDTo46tyzyqlf/di8nLJ4kfyi/3DU +IK+SNoIISY1UR7QyzeGkCwXMDmnYTbpn9tJE9Pyp6QKBgQD1n1BnfN3vou2otApR +YE/Fz6aINX8xky3l4/KUu82jW4sOTD1Zw6qauqKXHZHpRUkNGgV9TtJGHfOsHhlp +Vx1mSW8EqfJBVTJy20+qmiLVHoy9Ke0EX74G1KwQ+hDVuigHMXAB8i+giUQXUG5O +uT7vL+Nz3Hs6aSBG5L4aJod+zwKBgQDY0/n5WYwVnHtMEYVsJKFM7MG29otFS7qK +Ky40W2CwZ3gdngQ0CQ0pxjGcxN+kKmAQYhW3D8ouN3cwu7P4Bew81eM9Klbkve1H +7LmGMlLnP38Fjhkv+6aaUXNx81zdlozc0axoZ2tMHxxFt3ApEVMa84v3aYnurtLz +UdmriYg8kwKBgBOlbI76g935ED1gOTsNExRlaZKXhNb4SMGMnAidhPM+DT9Vhcq6 +RdkeeIbRzKv16MPtgubk+Ri2qJjpx2qPl0BpO0IKPeBP6jsZo6OntIWLbgJrahPh +fJebRzAMcHBH9Y+tuqHNN1YF5LZgrpIJaylCkVThG9IuOnCxNytsTYYtAoGAIdG+ +VaQf82b63DyHYPy9o+/xrwX5Zw+OwupFRKlUBW++DnCNG0imIvPk3Rzf/Q0W+rcx +x+HQmTDRiGpegbr6w+pgjTPDZd4x2lwT+9hxkdwXhdhKGW/Y5oz5wrZAr9dBlVem +2on29GKN2FnqRp2LJtMB688sUmw2PvlvSBAljb0CgYEAtBHuE82gLwgwS1xZ0mKE +A2xQ0QjhKU+m4k4jwQQFICs/lO9FhGuKQaKis2Q2XDcDIWjjS8k0htxYlb1ORIe0 +Ke3G5i8s89FdZJJMOQo7txiS8mKxdAdpo5JO+yqLmtQ3UXlfZKIBJV9EjctQhN9m +VtgF2n3t1NqFpWE+SRtq4EA= +-----END PRIVATE KEY----- diff --git a/tests/certs/no_key.pem b/tests/certs/no_key.pem new file mode 100644 index 0000000..c373c52 --- /dev/null +++ b/tests/certs/no_key.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEJjCCAw6gAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQ8wDQYDVQQDEwZXRUYt +Q0ExFzAVBgNVBAoTDmNhLnN0YWdlLmxvY2FsMQswCQYDVQQGEwJGUjEOMAwGA1UE +CBMFc3RhdGUxETAPBgNVBAcTCGxvY2F0aW9uMB4XDTIzMDgxMTA3NTczMloXDTI2 +MDUzMTA3NTczMlowVDEMMAoGA1UEAxMDd2luMRQwEgYDVQQKEwtzdGFnZS5sb2Nh +bDELMAkGA1UEBhMCRlIxDjAMBgNVBAgTBXN0YXRlMREwDwYDVQQHEwhsb2NhdGlv +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPPtmp+wvYrjhqniKTga +tFsNs1vzgSSD0agV4un4jdjLSNXQbYst3fjSodgkEWMmH2SBptQQPTgXWKn+Btoq +KXiReblaU+qQF+L9eVlMjsLf5v0OEdp0MAgUw5G0FSQpI1QpoZxHfI5dWgjGsBx/ +/eesvLq2im38fTFXDpWRxHD1mUYZMdxC0PNIzz42nlL87z2A4qoY4hSfNf4APQDT +NszkCg7HJU3kWFtYUOP21H6JHkOQwaGmt29aBGyyQfWUISbxr8BxyUb4TIwx7oSF +PvTqz/QfcDyDy7CYGOg0ZPmQoq7SWIWUZlOWA2LWg1nPrsGQBoDEDs+fqVT3FZQ1 +eTECAwEAAaOB/DCB+TAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIHgDAdBgNV +HQ4EFgQU0ONXrpPSX8hvsLvJ559IpupVIw8wgZcGA1UdIwSBjzCBjIAUTfAtTaoC +q8elxO2yzPVIf0OuWvehXqRcMFoxDzANBgNVBAMTBldFRi1DQTEXMBUGA1UEChMO +Y2Euc3RhZ2UubG9jYWwxCzAJBgNVBAYTAkZSMQ4wDAYDVQQIEwVzdGF0ZTERMA8G +A1UEBxMIbG9jYXRpb26CFA0IxOHqKR/gCoDPipwuOil5N8H/MAsGA1UdDwQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAi7hJPGJg +dDjfodb5qnSYOvebLp63V6ycWUeLy+Hc8t1WJ9l8nLImXAN7ix6pVUNaddWYDOwh +Lbg0c7ffuOPweD2Az+8tJztmUOdcYasB6Flz3XCabxG4Jpm4vygVh5k6EynwFB/e +DkgWSzaLrw/GeUXbVtk0Gt6T6XTlZkaxMBt0j3GK6wKtvBNsUADoQUS1xxYPfGFL +5/ezM25ITn+VK6jC4yvhndZCe9+yl6kFUOs9MEs79xYGbElLfThP0g9Dn4yKwwW4 +l15/5sgoHCDeZzKZOEgzAZdxlcIGhi0RnEMBecN2hkgMUkBxGXL+QLfjOWDhNzJ/ +0A+m2wc0I4r63g== +-----END CERTIFICATE----- diff --git a/tests/certs/wrong_format.pem b/tests/certs/wrong_format.pem new file mode 100644 index 0000000..1596875 --- /dev/null +++ b/tests/certs/wrong_format.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIUEwlpCwuVfuFYA5lWM5XOcnVIT3gwCgYIKoZIzj0EAwIw +RjEUMBIGA1UECgwLc3RhZ2UubG9jYWwxCzAJBgNVBAYTAkZSMQ4wDAYDVQQIDAVz +dGF0ZTERMA8GA1UEBwwIbG9jYXRpb24wHhcNMjMwODI4MDg0NTI0WhcNMjMwOTI3 +MDg0NTI0WjBGMRQwEgYDVQQKDAtzdGFnZS5sb2NhbDELMAkGA1UEBhMCRlIxDjAM +BgNVBAgMBXN0YXRlMREwDwYDVQQHDAhsb2NhdGlvbjB2MBAGByqGSM49AgEGBSuB +BAAiA2IABNTzR95NVNGNm2a1beRbJafnxsLdfe+CNpV8Z1SKy750ULeY1FN0Hngl +f0TzD3wc9qWmKMWfukp6OEFWqQMm+AKj+Nkjg+MO2Tteh/aw3JjHWRV3GBC8EcoI +DKIQ+qPxWKNTMFEwHQYDVR0OBBYEFL7OH7et540Hteayof31WtA/DnlgMB8GA1Ud +IwQYMBaAFL7OH7et540Hteayof31WtA/DnlgMA8GA1UdEwEB/wQFMAMBAf8wCgYI +KoZIzj0EAwIDaAAwZQIxANU0Id1jsaT2Uy1RX12l59L0QpY5Ul7NnaGlIvUJMV1j +GespzsJqZPUjFHQOFotfJAIwb+HRlFBDBbE840No1RmAXGdISMU+ZMUagBhhgsRx +lGmn0DQkidz+xlHp32DOTb2p