Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORE-548 base64 url decode #18195

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/v/security/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,7 +42,6 @@ v_cc_library(
v::rpc
absl::flat_hash_map
absl::flat_hash_set
cryptopp
re2
gssapi_krb5
krb5
Expand Down
23 changes: 23 additions & 0 deletions src/v/security/jwt.cc
Original file line number Diff line number Diff line change
@@ -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<bytes>
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
42 changes: 10 additions & 32 deletions src/v/security/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
#include "security/oidc_error.h"
#include "strings/string_switch.h"
#include "strings/utf8.h"
#include "utils/base64.h"

#include <seastar/core/sstring.hh>
#include <seastar/util/variant_utils.hh>

#include <absl/algorithm/container.h>
#include <absl/container/flat_hash_map.h>
#include <boost/algorithm/string/split.hpp>
#include <cryptopp/base64.h>

#include <iosfwd>
#include <optional>
Expand Down Expand Up @@ -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<string_viewable StringT = bytes>
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<CryptoPP::byte*>(decoded.data()), decoded.size());
}
return decoded;
};

template<string_viewable StringT = bytes>
std::optional<StringT>
base64_url_decode(json::Value const& v, std::string_view field) {
auto b64 = string_view<bytes::value_type>(v, field);
if (!b64.has_value()) {
return std::nullopt;
}
return base64_url_decode(b64.value());
}
std::optional<bytes>
base64_url_decode(json::Value const& v, std::string_view field);

} // namespace detail

Expand Down Expand Up @@ -445,7 +422,7 @@ inline result<verifier> 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&) {
michael-redpanda marked this conversation as resolved.
Show resolved Hide resolved
return errc::jwk_invalid;
Expand Down Expand Up @@ -506,25 +483,26 @@ class verifier {
// Verify the JWS signature and return the JWT
result<jwt> verify(jws const& jws) const {
std::string_view sv(jws._encoded);
std::vector<bytes_view> jose_enc;
std::vector<std::string_view> jose_enc;
jose_enc.reserve(3);
boost::algorithm::split(
jose_enc,
detail::char_view_cast<bytes_view::value_type>(sv),
detail::char_view_cast<std::string_view::value_type>(sv),
[](char c) { return c == '.'; });

if (jose_enc.size() != 3) {
return errc::jws_invalid_parts;
}

constexpr auto make_dom = [](bytes_view bv) -> result<json::Document> {
constexpr auto make_dom =
[](std::string_view bv) -> result<json::Document> {
try {
auto bytes = detail::base64_url_decode(bv);
auto str = detail::char_view_cast<char>(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;
}
};
Expand Down
33 changes: 33 additions & 0 deletions src/v/utils/base64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@

#include <libbase64.h>

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;
Expand Down Expand Up @@ -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) {
pgellert marked this conversation as resolved.
Show resolved Hide resolved
bytes rv{bytes::initialized_later{}, data.size()};
BenPope marked this conversation as resolved.
Show resolved Hide resolved
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<unsigned int>(decode_url_table[c]);
bits_collected += 6;
if (bits_collected >= 8) {
auto val = static_cast<bytes::value_type>(
(accumulator >> (bits_collected - 8)) & 0xffu);
rv[pos++] = val;
bits_collected -= 8;
michael-redpanda marked this conversation as resolved.
Show resolved Hide resolved
}
pgellert marked this conversation as resolved.
Show resolved Hide resolved
}
rv.resize(pos);
return rv;
}
15 changes: 15 additions & 0 deletions src/v/utils/base64.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
BenPope marked this conversation as resolved.
Show resolved Hide resolved
};

// base64 <-> bytes
bytes base64_to_bytes(std::string_view);
ss::sstring bytes_to_base64(bytes_view);
Expand All @@ -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);
2 changes: 1 addition & 1 deletion src/v/utils/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
70 changes: 70 additions & 0 deletions src/v/utils/tests/base64_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "utils/base64.h"

#include <boost/test/unit_test.hpp>
#include <cryptopp/base64.h>

BOOST_AUTO_TEST_CASE(bytes_type) {
auto encdec = [](const bytes& input, const auto expected) {
Expand Down Expand Up @@ -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) {
michael-redpanda marked this conversation as resolved.
Show resolved Hide resolved
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<size_t, 5> 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<CryptoPP::byte*>(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);
}
Loading