From 3bc0bbe2bc406a51b40ac19f42298415eb76938b Mon Sep 17 00:00:00 2001 From: Tor Brede Vekterli Date: Fri, 7 Sep 2018 11:08:20 +0000 Subject: Add TLS config file support with proposed JSON structure --- vespalib/CMakeLists.txt | 1 + .../tests/net/tls/transport_options/CMakeLists.txt | 10 ++ .../net/tls/transport_options/dummy_ca_certs.txt | 1 + .../net/tls/transport_options/dummy_certs.txt | 1 + .../net/tls/transport_options/dummy_privkey.txt | 1 + .../tests/net/tls/transport_options/ok_config.json | 7 ++ .../transport_options_reading_test.cpp | 65 +++++++++++++ vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt | 1 + .../net/tls/transport_security_options_reading.cpp | 102 +++++++++++++++++++++ .../net/tls/transport_security_options_reading.h | 20 ++++ 10 files changed, 209 insertions(+) create mode 100644 vespalib/src/tests/net/tls/transport_options/CMakeLists.txt create mode 100644 vespalib/src/tests/net/tls/transport_options/dummy_ca_certs.txt create mode 100644 vespalib/src/tests/net/tls/transport_options/dummy_certs.txt create mode 100644 vespalib/src/tests/net/tls/transport_options/dummy_privkey.txt create mode 100644 vespalib/src/tests/net/tls/transport_options/ok_config.json create mode 100644 vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp create mode 100644 vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp create mode 100644 vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.h diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt index a4b3f1e643c..4ae98be29b6 100644 --- a/vespalib/CMakeLists.txt +++ b/vespalib/CMakeLists.txt @@ -57,6 +57,7 @@ vespa_define_module( src/tests/net/socket src/tests/net/socket_spec src/tests/net/tls/openssl_impl + src/tests/net/tls/transport_options src/tests/objects/nbostream src/tests/optimized src/tests/printable diff --git a/vespalib/src/tests/net/tls/transport_options/CMakeLists.txt b/vespalib/src/tests/net/tls/transport_options/CMakeLists.txt new file mode 100644 index 00000000000..ee1e2477708 --- /dev/null +++ b/vespalib/src/tests/net/tls/transport_options/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_net_tls_transport_options_test_app TEST + SOURCES + transport_options_reading_test.cpp + DEPENDS + vespalib +) +vespa_add_test(NAME vespalib_net_tls_transport_options_test_app + COMMAND vespalib_net_tls_transport_options_test_app) + diff --git a/vespalib/src/tests/net/tls/transport_options/dummy_ca_certs.txt b/vespalib/src/tests/net/tls/transport_options/dummy_ca_certs.txt new file mode 100644 index 00000000000..b617f6f17e4 --- /dev/null +++ b/vespalib/src/tests/net/tls/transport_options/dummy_ca_certs.txt @@ -0,0 +1 @@ +My CA certificates diff --git a/vespalib/src/tests/net/tls/transport_options/dummy_certs.txt b/vespalib/src/tests/net/tls/transport_options/dummy_certs.txt new file mode 100644 index 00000000000..088b91ff770 --- /dev/null +++ b/vespalib/src/tests/net/tls/transport_options/dummy_certs.txt @@ -0,0 +1 @@ +My certificate chain diff --git a/vespalib/src/tests/net/tls/transport_options/dummy_privkey.txt b/vespalib/src/tests/net/tls/transport_options/dummy_privkey.txt new file mode 100644 index 00000000000..f29585fe31f --- /dev/null +++ b/vespalib/src/tests/net/tls/transport_options/dummy_privkey.txt @@ -0,0 +1 @@ +My private key diff --git a/vespalib/src/tests/net/tls/transport_options/ok_config.json b/vespalib/src/tests/net/tls/transport_options/ok_config.json new file mode 100644 index 00000000000..dd2591661dc --- /dev/null +++ b/vespalib/src/tests/net/tls/transport_options/ok_config.json @@ -0,0 +1,7 @@ +{ + "files":{ + "private-key": "dummy_privkey.txt", + "ca-certificates": "dummy_ca_certs.txt", + "certificates": "dummy_certs.txt" + } +} diff --git a/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp b/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp new file mode 100644 index 00000000000..859d2cc90f2 --- /dev/null +++ b/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp @@ -0,0 +1,65 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include +#include +#include +#include +#include + +using namespace vespalib; +using namespace vespalib::net::tls; + +TEST("can load TLS credentials via config file") { + auto opts = read_options_from_json_file("ok_config.json"); + ASSERT_TRUE(opts.get() != nullptr); + // Obviously we'd need to change this to actual PEM data if config reading started + // actually verifying the _content_ of files, not just reading them. + EXPECT_EQUAL("My private key\n", opts->private_key_pem()); + EXPECT_EQUAL("My CA certificates\n", opts->ca_certs_pem()); + EXPECT_EQUAL("My certificate chain\n", opts->cert_chain_pem()); +} + +TEST("missing JSON file throws exception") { + EXPECT_EXCEPTION(read_options_from_json_file("missing_config.json"), IllegalArgumentException, + "TLS config file 'missing_config.json' does not exist"); +} + +TEST("bad JSON content throws exception") { + const char* bad_json = "hello world :D"; + EXPECT_EXCEPTION(read_options_from_json_string(bad_json), IllegalArgumentException, + "Provided TLS config file is not valid JSON"); +} + +TEST("missing 'files' field throws exception") { + const char* incomplete_json = R"({})"; + EXPECT_EXCEPTION(read_options_from_json_string(incomplete_json), IllegalArgumentException, + "TLS config root field 'files' is missing or empty"); +} + +TEST("missing 'private-key' field throws exception") { + const char* incomplete_json = R"({"files":{"certificates":"dummy_certs.txt","ca-certificates":"dummy_ca_certs.txt"}})"; + EXPECT_EXCEPTION(read_options_from_json_string(incomplete_json), IllegalArgumentException, + "TLS config field 'private-key' has not been set"); +} + +TEST("missing 'certificates' field throws exception") { + const char* incomplete_json = R"({"files":{"private-key":"dummy_privkey.txt","ca-certificates":"dummy_ca_certs.txt"}})"; + EXPECT_EXCEPTION(read_options_from_json_string(incomplete_json), IllegalArgumentException, + "TLS config field 'certificates' has not been set"); +} + +TEST("missing 'ca-certificates' field throws exception") { + const char* incomplete_json = R"({"files":{"private-key":"dummy_privkey.txt","certificates":"dummy_certs.txt"}})"; + EXPECT_EXCEPTION(read_options_from_json_string(incomplete_json), IllegalArgumentException, + "TLS config field 'ca-certificates' has not been set"); +} + +TEST("missing file referenced by field throws exception") { + const char* incomplete_json = R"({"files":{"private-key":"missing_privkey.txt", + "certificates":"dummy_certs.txt", + "ca-certificates":"dummy_ca_certs.txt"}})"; + EXPECT_EXCEPTION(read_options_from_json_string(incomplete_json), IllegalArgumentException, + "File 'missing_privkey.txt' referenced by TLS config does not exist"); +} + +TEST_MAIN() { TEST_RUN_ALL(); } + diff --git a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt index 938ae0896a2..a501af89cfb 100644 --- a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt @@ -5,6 +5,7 @@ vespa_add_library(vespalib_vespalib_net_tls OBJECT crypto_exception.cpp tls_context.cpp transport_security_options.cpp + transport_security_options_reading.cpp DEPENDS ) find_package(OpenSSL) diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp new file mode 100644 index 00000000000..3d95e2327da --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp @@ -0,0 +1,102 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "transport_security_options_reading.h" +#include +#include +#include +#include +#include + +namespace vespalib::net::tls { + +/* + + Proposed JSON format for TLS configuration file: + +{ + "files": { + "private-key": "myhost.key", + "ca-certificates": "my_cas.pem", + "certificates": "certs.pem" + }, + // for later: + "peer-taggers": [ + { + "requirements":[ + { + "field": "SAN" + "must-match": "DNS:foo.bar.baz.*" + } + ], + "tags": ["cluster-peers", "config-server"] // or "roles"? Avoid ambiguities with Athenz concepts + }, + { + "requirements":[ + { "field":"CN", "must-match": "config.blarg.*"} + ], + "tags": ["config-server"] + } + ] +} + + */ + +using namespace slime; + +namespace { + +constexpr const char* files_field = "files"; +constexpr const char* private_key_field = "private-key"; +constexpr const char* ca_certs_field = "ca-certificates"; +constexpr const char* certs_field = "certificates"; + +void verify_referenced_file_exists(const vespalib::string& file_path) { + if (!fileExists(file_path)) { + throw IllegalArgumentException(make_string("File '%s' referenced by TLS config does not exist", file_path.c_str())); + } +} + +vespalib::string load_file_referenced_by_field(const Cursor& cursor, const char* field) { + auto file_path = cursor[field].asString().make_string(); + if (file_path.empty()) { + throw IllegalArgumentException(make_string("TLS config field '%s' has not been set", field)); + } + verify_referenced_file_exists(file_path); + return File::readAll(file_path); +} + +std::unique_ptr load_from_input(Input& input) { + Slime root; + auto parsed = JsonFormat::decode(input, root); + if (parsed == 0) { + throw IllegalArgumentException("Provided TLS config file is not valid JSON"); + } + auto& files = root[files_field]; + if (files.children() == 0) { + throw IllegalArgumentException("TLS config root field 'files' is missing or empty"); + } + // Note: we do no look at the _contents_ of the files; this is deferred to the + // TLS context code which actually tries to extract key and certificate material + // from them. + auto ca_certs = load_file_referenced_by_field(files, ca_certs_field); + auto certs = load_file_referenced_by_field(files, certs_field); + auto priv_key = load_file_referenced_by_field(files, private_key_field); + + return std::make_unique(std::move(ca_certs), std::move(certs), std::move(priv_key)); +} + +} // anon ns + +std::unique_ptr read_options_from_json_string(const vespalib::string& json_data) { + MemoryInput file_input(json_data); + return load_from_input(file_input); +} + +std::unique_ptr read_options_from_json_file(const vespalib::string& file_path) { + if (!fileExists(file_path)) { + throw IllegalArgumentException(make_string("TLS config file '%s' does not exist", file_path.c_str())); + } + MappedFileInput file_input(file_path); + return load_from_input(file_input); +} + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.h b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.h new file mode 100644 index 00000000000..800b3b5ed0d --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.h @@ -0,0 +1,20 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "transport_security_options.h" +#include + +namespace vespalib::net::tls { + +// TODO consider renaming TransportSecurityOptions -> TlsConfig + +/** + * Throws IoException if file_path or any files referenced by it can't be accessed + * Throws IllegalArgumentException if file is not parseable as a valid TLS config file or + * if mandatory JSON fields are missing or incomplete. + */ +std::unique_ptr read_options_from_json_file(const vespalib::string& file_path); +// Same properties as read_options_from_json_file() +std::unique_ptr read_options_from_json_string(const vespalib::string& json_data); + +} -- cgit v1.2.3