From 710dc180b50ae3dab654f7e05cb735921faa957e Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Wed, 1 May 2024 11:30:36 -0400 Subject: [PATCH 1/2] utils: Added base64 URL decoding Added a utility function to perform Base64 URL decoding. This approach was selected because the base64 library we currently use does not support encoding/decoding Base64URL messages. The only use case we have for this is in OIDC and happens infrequently enough that performance is not a large concern. Signed-off-by: Michael Boquard --- src/v/utils/base64.cc | 33 +++++++++++++++ src/v/utils/base64.h | 15 +++++++ src/v/utils/tests/CMakeLists.txt | 2 +- src/v/utils/tests/base64_test.cc | 70 ++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/v/utils/base64.cc b/src/v/utils/base64.cc index 256b425d2830..a5d6752ae3fb 100644 --- a/src/v/utils/base64.cc +++ b/src/v/utils/base64.cc @@ -16,6 +16,16 @@ #include +const int decode_url_table[128] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, + 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, +}; + // Required length is ceil(4n/3) rounded up to 4 bytes static inline size_t encode_capacity(size_t input_size) { return (((4 * input_size) / 3) + 3) & ~0x3U; @@ -110,3 +120,26 @@ ss::sstring iobuf_to_base64(const iobuf& input) { output.resize(written); return output; } + +bytes base64url_to_bytes(std::string_view data) { + bytes rv{bytes::initialized_later{}, data.size()}; + unsigned int bits_collected = 0; + unsigned int accumulator = 0; + int pos = 0; + for (const unsigned char c : data) { + if (c > 127 || decode_url_table[c] < 0) { + throw base64_url_decoder_exception(); + } + accumulator = (accumulator << 6) + + static_cast(decode_url_table[c]); + bits_collected += 6; + if (bits_collected >= 8) { + auto val = static_cast( + (accumulator >> (bits_collected - 8)) & 0xffu); + rv[pos++] = val; + bits_collected -= 8; + } + } + rv.resize(pos); + return rv; +} diff --git a/src/v/utils/base64.h b/src/v/utils/base64.h index edd78e28a386..9eb268bebc96 100644 --- a/src/v/utils/base64.h +++ b/src/v/utils/base64.h @@ -22,6 +22,13 @@ class base64_decoder_exception final : public std::exception { } }; +class base64_url_decoder_exception final : public std::exception { +public: + const char* what() const noexcept final { + return "error decoding base64url string"; + } +}; + // base64 <-> bytes bytes base64_to_bytes(std::string_view); ss::sstring bytes_to_base64(bytes_view); @@ -31,3 +38,11 @@ ss::sstring base64_to_string(std::string_view); // base64 <-> iobuf ss::sstring iobuf_to_base64(const iobuf&); + +/// \brief Used to decode URL encoded base64 values +/// +/// URL encoded base64 values use '-' and '_' instead of +/// '+' and '/', respectively +/// \throws base64_url_decoder_exception if an invalid URL base64 encoded string +/// is provided +bytes base64url_to_bytes(std::string_view); diff --git a/src/v/utils/tests/CMakeLists.txt b/src/v/utils/tests/CMakeLists.txt index 756bfc614721..0b91aaeb23c7 100644 --- a/src/v/utils/tests/CMakeLists.txt +++ b/src/v/utils/tests/CMakeLists.txt @@ -20,7 +20,7 @@ rp_test( vint_test.cc waiter_queue_test.cc auto_fmt_test.cc - LIBRARIES v::seastar_testing_main v::utils v::bytes v::version absl::flat_hash_set + LIBRARIES v::seastar_testing_main v::utils v::bytes v::version absl::flat_hash_set cryptopp ARGS "-- -c 1" LABELS utils ) diff --git a/src/v/utils/tests/base64_test.cc b/src/v/utils/tests/base64_test.cc index 14285b7aa494..25cc85f05987 100644 --- a/src/v/utils/tests/base64_test.cc +++ b/src/v/utils/tests/base64_test.cc @@ -12,6 +12,7 @@ #include "utils/base64.h" #include +#include BOOST_AUTO_TEST_CASE(bytes_type) { auto encdec = [](const bytes& input, const auto expected) { @@ -49,3 +50,72 @@ BOOST_AUTO_TEST_CASE(iobuf_type) { auto decoded = base64_to_bytes(encoded); BOOST_REQUIRE_EQUAL(decoded, iobuf_to_bytes(buf)); } + +BOOST_AUTO_TEST_CASE(base64_url_decode_test_basic) { + auto dec = [](std::string_view input, const bytes& expected) { + auto decoded = base64url_to_bytes(input); + BOOST_REQUIRE_EQUAL(decoded, expected); + }; + + dec("UmVkcGFuZGEgUm9ja3M", "Redpanda Rocks"); + // ChatGPT was asked to describe the Redpanda product + dec( + "UmVkcGFuZGEgaXMgYSBjdXR0aW5nLWVkZ2UgZGF0YSBzdHJlYW1pbmcgcGxhdGZvcm0gZGVz" + "aWduZWQgdG8gb2ZmZXIgYSBoaWdoLXBlcmZvcm1hbmNlIGFsdGVybmF0aXZlIHRvIEFwYWNo" + "ZSBLYWZrYS4gSXQncyBjcmFmdGVkIHRvIGhhbmRsZSB2YXN0IGFtb3VudHMgb2YgcmVhbC10" + "aW1lIGRhdGEgZWZmaWNpZW50bHksIG1ha2luZyBpdCBhbiBleGNlbGxlbnQgY2hvaWNlIGZv" + "ciBtb2Rlcm4gZGF0YS1kcml2ZW4gYXBwbGljYXRpb25zLiAgT3ZlcmFsbCwgUmVkcGFuZGEg" + "cmVwcmVzZW50cyBhIGNvbXBlbGxpbmcgb3B0aW9uIGZvciBvcmdhbml6YXRpb25zIHNlZWtp" + "bmcgYSBoaWdoLXBlcmZvcm1hbmNlLCBzY2FsYWJsZSwgYW5kIHJlbGlhYmxlIGRhdGEgc3Ry" + "ZWFtaW5nIHNvbHV0aW9uLiBXaGV0aGVyIHlvdSdyZSBidWlsZGluZyByZWFsLXRpbWUgYW5h" + "bHl0aWNzIGFwcGxpY2F0aW9ucywgcHJvY2Vzc2luZyBJb1QgZGF0YSBzdHJlYW1zLCBvciBt" + "YW5hZ2luZyBldmVudC1kcml2ZW4gbWljcm9zZXJ2aWNlcywgUmVkcGFuZGEgaGFzIHlvdSBj" + "b3ZlcmVkLg", + "Redpanda is a cutting-edge data streaming platform designed to offer a " + "high-performance alternative to Apache Kafka. It's crafted to handle " + "vast amounts of real-time data efficiently, making it an excellent " + "choice for modern data-driven applications. Overall, Redpanda " + "represents a compelling option for organizations seeking a " + "high-performance, scalable, and reliable data streaming solution. " + "Whether you're building real-time analytics applications, processing " + "IoT data streams, or managing event-driven microservices, Redpanda has " + "you covered."); + + dec("", ""); + dec("YQ", "a"); + dec("YWI", "ab"); + dec("YWJj", "abc"); + dec("A", ""); +} + +BOOST_AUTO_TEST_CASE(base64_url_decode_test_random) { + const std::array test_sizes = {1, 10, 128, 256, 512}; + auto dec = [](std::string_view input, const bytes& expected) { + auto decoded = base64url_to_bytes(input); + BOOST_REQUIRE_EQUAL(decoded, expected); + }; + + auto enc = [](const bytes& msg) { + CryptoPP::Base64URLEncoder encoder; + encoder.Put(msg.data(), msg.size()); + encoder.MessageEnd(); + auto size = encoder.MaxRetrievable(); + BOOST_REQUIRE_NE(size, 0); + ss::sstring encoded(ss::sstring::initialized_later{}, size); + encoder.Get( + reinterpret_cast(encoded.data()), encoded.size()); + return encoded; + }; + + for (auto s : test_sizes) { + auto val = random_generators::get_bytes(s); + auto encoded = enc(val); + dec(encoded, val); + } +} + +BOOST_AUTO_TEST_CASE(base64_url_decode_invalid_character) { + const std::string invalid_encode = "abc+/"; + BOOST_REQUIRE_THROW( + base64url_to_bytes(invalid_encode), base64_url_decoder_exception); +} From f0dd8f464136de13d96498d30ad5def3678ee169 Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Wed, 1 May 2024 11:51:50 -0400 Subject: [PATCH 2/2] s/oidc: Replaced cryptopp with utility function Signed-off-by: Michael Boquard --- src/v/security/CMakeLists.txt | 2 +- src/v/security/jwt.cc | 23 +++++++++++++++++++ src/v/security/jwt.h | 42 +++++++++-------------------------- 3 files changed, 34 insertions(+), 33 deletions(-) create mode 100644 src/v/security/jwt.cc diff --git a/src/v/security/CMakeLists.txt b/src/v/security/CMakeLists.txt index 405ec27b79a2..beb51578a66a 100644 --- a/src/v/security/CMakeLists.txt +++ b/src/v/security/CMakeLists.txt @@ -18,6 +18,7 @@ v_cc_library( credential.cc gssapi_authenticator.cc gssapi_principal_mapper.cc + jwt.cc krb5.cc krb5_configurator.cc license.cc @@ -41,7 +42,6 @@ v_cc_library( v::rpc absl::flat_hash_map absl::flat_hash_set - cryptopp re2 gssapi_krb5 krb5 diff --git a/src/v/security/jwt.cc b/src/v/security/jwt.cc new file mode 100644 index 000000000000..594b1ef6c544 --- /dev/null +++ b/src/v/security/jwt.cc @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Redpanda Data, Inc. + * + * Licensed as a Redpanda Enterprise file under the Redpanda Community + * License (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://github.com/redpanda-data/redpanda/blob/master/licenses/rcl.md + */ +#include "security/jwt.h" + +namespace security::oidc::detail { +bytes base64_url_decode(std::string_view sv) { return base64url_to_bytes(sv); }; + +std::optional +base64_url_decode(json::Value const& v, std::string_view field) { + auto b64 = string_view<>(v, field); + if (!b64.has_value()) { + return std::nullopt; + } + return base64_url_decode(b64.value()); +} +} // namespace security::oidc::detail diff --git a/src/v/security/jwt.h b/src/v/security/jwt.h index e23c7ced2d49..00523c3548bd 100644 --- a/src/v/security/jwt.h +++ b/src/v/security/jwt.h @@ -19,6 +19,7 @@ #include "security/oidc_error.h" #include "strings/string_switch.h" #include "strings/utf8.h" +#include "utils/base64.h" #include #include @@ -26,7 +27,6 @@ #include #include #include -#include #include #include @@ -95,33 +95,10 @@ time_point(json::Value const& doc, std::string_view field) { typename Clock::time_point(std::chrono::seconds(it->value.GetInt64())); } -template -auto base64_url_decode(bytes_view sv) { - // TODO: Replace this with non-CryptoPP implementation - // TODO: https://github.com/redpanda-data/core-internal/issues/1132 - CryptoPP::Base64URLDecoder decoder; +bytes base64_url_decode(std::string_view sv); - decoder.Put(sv.data(), sv.size()); - decoder.MessageEnd(); - - StringT decoded; - if (auto size = decoder.MaxRetrievable(); size != 0) { - decoded.resize(size); - decoder.Get( - reinterpret_cast(decoded.data()), decoded.size()); - } - return decoded; -}; - -template -std::optional -base64_url_decode(json::Value const& v, std::string_view field) { - auto b64 = string_view(v, field); - if (!b64.has_value()) { - return std::nullopt; - } - return base64_url_decode(b64.value()); -} +std::optional +base64_url_decode(json::Value const& v, std::string_view field); } // namespace detail @@ -445,7 +422,7 @@ inline result make_rs256_verifier(json::Value const& jwk) { } auto key = crypto::key::load_rsa_public_key(n.value(), e.value()); return verifier{rs256_verifier{std::move(key)}}; - } catch (CryptoPP::Exception const& ex) { + } catch (base64_url_decoder_exception const&) { return errc::jwk_invalid; } catch (crypto::exception const&) { return errc::jwk_invalid; @@ -506,25 +483,26 @@ class verifier { // Verify the JWS signature and return the JWT result verify(jws const& jws) const { std::string_view sv(jws._encoded); - std::vector jose_enc; + std::vector jose_enc; jose_enc.reserve(3); boost::algorithm::split( jose_enc, - detail::char_view_cast(sv), + detail::char_view_cast(sv), [](char c) { return c == '.'; }); if (jose_enc.size() != 3) { return errc::jws_invalid_parts; } - constexpr auto make_dom = [](bytes_view bv) -> result { + constexpr auto make_dom = + [](std::string_view bv) -> result { try { auto bytes = detail::base64_url_decode(bv); auto str = detail::char_view_cast(bytes); json::Document dom; dom.Parse(str.data(), str.length()); return dom; - } catch (CryptoPP::Exception const& ex) { + } catch (base64_url_decoder_exception const&) { return errc::jws_invalid_b64; } };