Make token's 'tenants' field base64-encoded (cf. base64url)

- Remove redundant operation from TokenSign
- Let the sign/verify API directly report errors
  instead of tracing at failing subroutine, which lacks context
This commit is contained in:
Junhyun Shim 2022-11-03 22:00:01 +01:00
parent 0091abd02b
commit 50f4021cf7
17 changed files with 1026 additions and 660 deletions

419
fdbrpc/Base64Decode.cpp Normal file
View File

@ -0,0 +1,419 @@
/*
* Base64Decode.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string_view>
#include <fmt/format.h>
#include "fdbrpc/Base64Encode.h"
#include "fdbrpc/Base64Decode.h"
#include "flow/flow.h"
#include "flow/Arena.h"
#include "flow/Error.h"
#include "flow/UnitTest.h"
namespace {
constexpr uint8_t _X = 0xff;
template <bool UrlDecode>
inline uint8_t decodeValue(uint8_t valueIn) noexcept {
if constexpr (UrlDecode) {
// clang-format off
constexpr const uint8_t decoding[] = { // 20x13
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, 62, _X, _X, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, _X, _X,
_X, _X, _X, _X, _X, 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, _X, _X, _X, _X, 63, _X, 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, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X };
// clang-format on
static_assert(sizeof(decoding) / sizeof(decoding[0]) == 256);
return decoding[valueIn];
} else {
// clang-format off
constexpr const uint8_t decoding[] = { // 20x13
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, 62, _X, _X, _X, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, _X, _X,
_X, _X, _X, _X, _X, 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, _X, _X, _X, _X, _X, _X, 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, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X };
// clang-format on
static_assert(sizeof(decoding) / sizeof(decoding[0]) == 256);
return decoding[valueIn];
}
}
template <bool UrlDecode>
int doDecode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept {
const uint8_t* codechar = codeIn;
const uint8_t* const codeEnd = codeIn + lengthIn;
uint8_t* plainchar = plaintextOut;
uint8_t fragment = 0;
while (1) {
// code 1 of 4
if (codechar == codeEnd) {
return plainchar - plaintextOut;
}
fragment = decodeValue<UrlDecode>(*codechar++);
if (fragment == _X)
return -1;
*plainchar = (fragment & 0x03f) << 2;
if (codechar == codeEnd) {
return -1; // requires at least 2 chars to decode 1 plain byte
}
// code 2 of 4
fragment = decodeValue<UrlDecode>(*codechar++);
if (fragment == _X)
return -1;
*plainchar++ |= (fragment & 0x030) >> 4;
if (codechar == codeEnd) {
return plainchar - plaintextOut;
}
*plainchar = (fragment & 0x00f) << 4;
// code 3 of 4
fragment = decodeValue<UrlDecode>(*codechar++);
if (fragment == _X)
return -1;
*plainchar++ |= (fragment >> 2);
if (codechar == codeEnd) {
return plainchar - plaintextOut;
}
*plainchar = (fragment & 0x003) << 6;
// code 4 of 4
fragment = decodeValue<UrlDecode>(*codechar++);
if (fragment == _X)
return -1;
*plainchar++ |= (fragment & 0x03f);
}
/* control should not reach here */
return plainchar - plaintextOut;
}
// assumes codeLength after padding stripped
int getDecodedLength(int codeLength) noexcept {
const auto r = (codeLength % 4);
if (r == 1)
return -1;
else if (r == 0)
return (codeLength / 4) * 3;
else
return (codeLength / 4) * 3 + (r - 1);
}
template <bool UrlDecode>
Optional<StringRef> decodeStringRef(Arena& arena, StringRef codeText) {
if constexpr (!UrlDecode) {
// check length alignment and strip padding, if any
if (codeText.size() % 4)
return {};
if (!codeText.empty() && codeText.back() == '=')
codeText.popBack();
if (!codeText.empty() && codeText.back() == '=')
codeText.popBack();
}
auto decodedLen = getDecodedLength(codeText.size());
if (decodedLen <= 0) {
if (decodedLen == 0)
return StringRef{};
return {};
}
auto out = new (arena) uint8_t[decodedLen];
auto actualLen = doDecode<UrlDecode>(codeText.begin(), codeText.size(), out);
if (actualLen == -1) {
return {};
}
ASSERT_EQ(decodedLen, actualLen);
return StringRef(out, decodedLen);
}
} // anonymous namespace
namespace base64 {
int decodedLength(int codeLength) noexcept {
// Non-urlencoded base64 cannot predict the decoded length on encoded length alone due to padding,
// which makes this a conservative estimate, not an exact one.
return (codeLength / 4) * 3;
}
int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept {
if (lengthIn % 4) {
return -1;
} else {
int actualLen = lengthIn;
if (actualLen > 0 && codeIn[actualLen - 1] == '=')
actualLen--;
if (actualLen > 0 && codeIn[actualLen - 1] == '=')
actualLen--;
return doDecode<false>(codeIn, actualLen, plaintextOut);
}
}
Optional<StringRef> decode(Arena& arena, StringRef codeText) {
return decodeStringRef<false>(arena, codeText);
}
namespace url {
int decodedLength(int codeLength) noexcept {
return getDecodedLength(codeLength);
}
int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept {
return doDecode<true>(codeIn, lengthIn, plaintextOut);
}
Optional<StringRef> decode(Arena& arena, StringRef codeText) {
return decodeStringRef<true>(arena, codeText);
}
} // namespace url
} // namespace base64
// transform the input on-the-fly to build testcase for non-urlencoded cases
StringRef urlEncodedTestData[][2] = {
{ ""_sr, ""_sr },
{ "f"_sr, "Zg"_sr },
{ "fo"_sr, "Zm8"_sr },
{ "foo"_sr, "Zm9v"_sr },
{ "foob"_sr, "Zm9vYg"_sr },
{ "fooba"_sr, "Zm9vYmE"_sr },
{ "foobar"_sr, "Zm9vYmFy"_sr },
{ "Q\xc2\x93\x86\x04H\xfd\"r\x9c\xf7\xafW\xd1\x87^"_sr, "UcKThgRI_SJynPevV9GHXg"_sr },
{ "\x93"_sr, "kw"_sr },
{ "\xfcpF\xd2\x15\x03\x8a\xcb\\#!\xa1\x95\x18\x13\xfcpoN\rh=\xa5\x05\xe5\x00\xf8<\xc3\x8b'C\x90\xfc\xa0x\x13"
"8q\r\xd4\xca\xc9Yjv"_sr,
"_HBG0hUDistcIyGhlRgT_HBvTg1oPaUF5QD4PMOLJ0OQ_KB4EzhxDdTKyVlqdg"_sr },
{ "\xdd\xbb\x91>@\x9d\x88\x01Qb\x97[\xc3Q\xf6Q\\LF\xe2}\xfb\xf0\xe8\x98\xba\x8c\xc7\xc9\x0e$\xe4q\xcf;\xe4"
"e\x02"
"DA\xa9\x9a\xf0r\xc9\xf0\xd2-\x98"_sr,
"3buRPkCdiAFRYpdbw1H2UVxMRuJ9-_DomLqMx8kOJORxzzvkZQJEQama8HLJ8NItmA"_sr },
{ "2\x7f\x98\xfe\xb4\x05\x18.%.\xd0\x14\xea\x8e+\xa5\xc5\xbd"
"F-Lm\x04\x1aQ\xde\x1e\x9c\x12\xe6\x81{\x9dj\xe8\x9cP\xf4\xf7\x8a<\x12"_sr,
"Mn-Y_rQFGC4lLtAU6o4rpcW9Ri1MbQQaUd4enBLmgXudauicUPT3ijwS"_sr },
{ "\xa3"
"b\xc9\x13|\x94\xab)}\xf4N\xbc\xb2\xc5$\x15\xed\xb0\x98\xa4v\x8b\x91\xe4M\xb8\xde!\x94"_sr,
"o2LJE3yUqyl99E68ssUkFe2wmKR2i5HkTbjeIZQ"_sr },
{ "\xa0\xe9\xb4=4\xba\xbd\xbd\xaa\xfd\x96\xcb\x03\xd3\xb7\xc9\xb7i7\x18$^\xba\xe5\xb3\x8a\xf4O\xdb"_sr,
"oOm0PTS6vb2q_ZbLA9O3ybdpNxgkXrrls4r0T9s"_sr },
{ "\x10\xf4zscNoE\xfd\xc5\x8d\x16\x82t|y\n\xcf\xe8\x98\xf8)\xcd\xefm\xe2\xe1%\x17\x05T9;Zb\x05\x02\xc7"
"B\x8c\xc5\xc5\x95"
"8\xf2"_sr,
"EPR6c2NOb0X9xY0WgnR8eQrP6Jj4Kc3vbeLhJRcFVDk7WmIFAsdCjMXFlTjy"_sr },
{ "NG\"hA\xff\\\xf5lD\xf7\x08"
"7\xb9\t\x07\xa5\xb9\xac\x0b\x9fT+\xfa"_sr,
"TkciaEH_XPVsRPcIN7kJB6W5rAufVCv6"_sr },
{ "\xbd\xf5\xb0\x8eh1\x1b\xd1\x13q\x88z0*b\x9cNg\x88\x88MBD\x17\xec\xb0yc3\xbb"_sr,
"vfWwjmgxG9ETcYh6MCpinE5niIhNQkQX7LB5YzO7"_sr },
{ "&\xe0> \xfc\xea\x8e\xc1[I\xec\xe8\x03\x15\tc\x9b\x0f"
"d\x13"
"d\xab\xa5\x16\xa2p\x91\xd5\x11\xf5X\xa7\xbd\xe1\xa1"
"B\x8e\xe8\xddn2\xbf\x97"_sr,
"JuA-IPzqjsFbSezoAxUJY5sPZBNkq6UWonCR1RH1WKe94aFCjujdbjK_lw"_sr },
{ "|W,\xa5\xce\x83\xb0\xec\x87\x86\xd0<O\x94\x97"
"0F"_sr,
"fFcspc6DsOyHhtA8T5SXMEY"_sr },
{ "\x06\xa7\xf1"_sr, "Bqfx"_sr },
{ "u\xc4"
"7\x8e&\xa7\x90v"_sr,
"dcQ3jiankHY"_sr },
{ "X\xfe\xcd\x1f\xc8\x8f\xe3\xca"
"6\x96\x8c\x87\xcd\xbaJ!\xabq\x8c\x97)#\xfb\xda\xb8\xa9\xe9"
"a\x0c\xe2\x10\xe9\xe7\x16\x96\xb5"_sr,
"WP7NH8iP48o2loyHzbpKIatxjJcpI_vauKnpYQziEOnnFpa1"_sr },
{ "\xf1\n@\x18\xc3"
"F\xc4\xf8\x1c\xa9\xa9\xdb\x15\xcb\xd0V\xe4P\x8b\x8b\xaf\xf2\xfc\xb7\x1d\xa6p\n\xa3\x13,.\x12#"_sr,
"8QpAGMNGxPgcqanbFcvQVuRQi4uv8vy3HaZwCqMTLC4SIw"_sr },
{ "h\x9e\xe0X_\x1c\x04\xe3\xaf\xac\xe3\x18JK/\xe7\xbc"
"D\xf5"
"B\xfbK(Q\x8c\n\xca\xfc^\xd9\xdb\xb4\xea\xae\xb8"_sr,
"aJ7gWF8cBOOvrOMYSksv57xE9UL7SyhRjArK_F7Z27Tqrrg"_sr },
{ "\x9aX\xb2Q\xf0\x85R`\xc1\xbb\x95\xe7\x10\xe3\xd0x\n"_sr, "mliyUfCFUmDBu5XnEOPQeAo"_sr },
{ "\xa7\x1f"
"0\x90P\xab\x1fwJ\x86\x82Tj\xd0Ob\xa4\xac+\xc9"
"4;\x86\\\x8b\xb6"_sr,
"px8wkFCrH3dKhoJUatBPYqSsK8k0O4Zci7Y"_sr },
{ "\x0e"
"C\x87u-\x12\x8a\xe4\x11"
"F\xb6"
"a5\xcds\x1ez?J4\x02?@g\xaa-\xc4\xe0\x80\xe1!\xc0\x1d\x16\x1e"
"Em\xa8"
"a\xc8\x9d<\xd1"_sr,
"DkOHdS0SiuQRRrZhNc1zHno_SjQCP0Bnqi3E4IDhIcAdFh5FbahhyJ080Q"_sr },
{ "\xb7\xf6\xb6\\\xd3&\xf5"
"F\xb1\x86\xd8n%\xb5"
"a\x1d^Iv\xbe\x9bO\xcb\xca\xd2"_sr,
"t_a2XNMm9UaxhthuJbVhHV5Jdr6bT8vK0g"_sr },
{ "\xc0+\xea\xb9\x01\xf2\xe8\x8e\xf2\x0ft\x8b\xa0\xa4\x0cq=\xa6"
"c\xf1v\x9b\x81\xc7*\xd0\xe8Z\x04\"\t\xe8>\xd5w\xddQI\xc9\xba\x8f"_sr,
"wCvquQHy6I7yD3SLoKQMcT2mY_F2m4HHKtDoWgQiCeg-1XfdUUnJuo8"_sr },
{ "\xac"
"5\x8aH\xc9q\xad\xbe\x1f\x80\xed\xe1"_sr,
"rDWKSMlxrb4fgO3h"_sr },
{ "0}\x82\x95\xbb'\xf2\xdf"
"dR\x8f\xc2\xac\xb3\xc7\x9f\xc0\xf0"
"C3L\xbe"
"E\xe5\xf1\xc4% \xec\xe9"_sr,
"MH2Clbsn8t9kUo_CrLPHn8DwQzNMvkXl8cQlIOzp"_sr },
{ "\x8b\xab\xd7\xf9\xa5\xd8H\x1d"_sr, "i6vX-aXYSB0"_sr },
{ "i!\xdc\xb7~9\x7f\xad\xa0\x9d\x1e\xcc\xedTj\xe3\xe2\x88Q\x1e\xaa\xf9\xc3\xc5\xc5\xcdq\x9e\x07~\x9e\xcb\xf3\xd3\xb2\xec\xe0[m+\x0c\x9c"_sr,
"aSHct345f62gnR7M7VRq4-KIUR6q-cPFxc1xngd-nsvz07Ls4FttKwyc"_sr },
{ "\x01-{.s\xa5qF4\x1f"
"a\x11\xe4\x1eN"_sr,
"AS17LnOlcUY0H2ER5B5O"_sr },
{ "\x1c\xab\xce"
"e\xfc\xa7"
"are\x1f\x9a\xb4\xcdr\xe2v95\x88"_sr,
"HKvOZfynYXJlH5q0zXLidjk1iA"_sr },
{ "\xde\x0e\x16V\x12\x0f\xa4\xaf"
"2\xe7k3\xe8\"\x0b\xcb\x80\xa5\x96,\xba\xef\x1c\xe3\xd8\x16"
"C1\xccI\x8a]W\xf0\xbf\xaf\x19"
"4\xf9\r*<^?8C\x81\xd3(\xc6"_sr,
"3g4WVhIPpK8y52sz6CILy4Clliy67xzj2BZDMcxJil1X8L-vGTT5DSo8Xj84Q4HTKMY"_sr },
{ "\xe1\xe2\x8a"
"8v\x1f\xe0|\xccIJ\t"_sr,
"4eKKOHYf4HzMSUoJ"_sr },
};
static Void runTest(std::function<StringRef(Arena&, StringRef)> conversionFn,
StringRef (&stringEncodeFn)(Arena&, StringRef),
Optional<StringRef> (&stringDecodeFn)(Arena&, StringRef),
std::function<void(StringRef, int)> encodeOutputCheckFn) {
const int testSetLen = sizeof(urlEncodedTestData) / sizeof(urlEncodedTestData[0]);
for (auto i = 0; i < testSetLen; i++) {
auto tmpArena = Arena();
auto [decodeOutputExpected, encodeOutputExpected] = urlEncodedTestData[i];
// optionally convert base64Url-encoded test input to regular base64
encodeOutputExpected = conversionFn(tmpArena, encodeOutputExpected);
auto encodeOutput = stringEncodeFn(tmpArena, decodeOutputExpected);
if (encodeOutput != encodeOutputExpected) {
fmt::print("Test case {} (encode): expected '{}' got '{}'\n",
i + 1,
encodeOutputExpected.toHexString(),
encodeOutput.toHexString());
ASSERT(false);
}
auto decodeOutput = stringDecodeFn(tmpArena, encodeOutputExpected);
ASSERT(decodeOutput.present());
if (decodeOutput.get() != decodeOutputExpected) {
fmt::print("Test case {} (decode): expected '{}' got '{}'\n",
i + 1,
decodeOutputExpected.toHexString(),
decodeOutput.get().toHexString());
ASSERT(false);
}
}
auto& rng = *deterministicRandom();
for (auto i = 0; i < 100; i++) {
auto tmpArena = Arena();
auto inputLen = rng.randomInt(1, 300);
auto inputBuf = new (tmpArena) uint8_t[inputLen];
for (auto i = 0; i < inputLen; i++)
inputBuf[i] = rng.randomInt(0, 256);
auto input = StringRef(inputBuf, inputLen);
auto output = stringEncodeFn(tmpArena, input);
encodeOutputCheckFn(output, i);
auto decodeOutput = stringDecodeFn(tmpArena, output);
ASSERT(decodeOutput.present());
if (input != decodeOutput.get()) {
fmt::print("Dynamic case {} (decode) failed, expected '{}', got '{}'\n",
input.toHexString(),
decodeOutput.get().toHexString());
ASSERT(false);
}
}
return Void();
}
TEST_CASE("/fdbrpc/Base64UrlEncode") {
return runTest([](Arena&, StringRef input) { return input; }, // no op (input already url-encoded)
base64::url::encode,
base64::url::decode,
[](StringRef encodeOutput, int n) {
ASSERT_NE(encodeOutput.size() % 4, 1);
// verify that output contains only base64url-legal characters
for (auto i = 0; i < encodeOutput.size(); ++i) {
auto const value = decodeValue<true /*urlencoded*/>(encodeOutput[i]);
if (value == _X) {
fmt::print(
"Random-generated case {} has illegal encoded output char: {}th byte, value {}\n",
n + 1,
i + 1,
static_cast<int>(value));
ASSERT(false);
}
}
});
}
static StringRef transformBase64UrlToBase64(Arena& arena, StringRef input) {
if (input.empty())
return StringRef();
const int len = ((input.size() + 3) / 4) * 4; // ceil_align(input.size(), 4)
auto output = new (arena) uint8_t[len];
for (auto i = 0; i < input.size(); i++) {
if (input[i] == '-') {
output[i] = '+';
} else if (input[i] == '_') {
output[i] = '/';
} else {
output[i] = input[i];
}
}
for (auto i = input.size(); i < len; i++)
output[i] = '=';
return StringRef(output, len);
}
TEST_CASE("/fdbrpc/Base64Encode") {
return runTest(transformBase64UrlToBase64, base64::encode, base64::decode, [](StringRef encodeOutput, int n) {
ASSERT_EQ(encodeOutput.size() % 4, 0);
if (!encodeOutput.empty() && encodeOutput.back() == '=')
encodeOutput.popBack();
if (!encodeOutput.empty() && encodeOutput.back() == '=')
encodeOutput.popBack();
for (auto i = 0; i < encodeOutput.size(); ++i) {
auto const value = decodeValue<false /*urlencoded*/>(encodeOutput[i]);
if (value == _X) {
fmt::print("Random-generated case {} has illegal encoded output char: {}th byte, value {}\n",
n + 1,
i + 1,
static_cast<int>(value));
ASSERT(false);
}
}
});
}

143
fdbrpc/Base64Encode.cpp Normal file
View File

@ -0,0 +1,143 @@
/*
* Base64Encode.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "fdbrpc/Base64Encode.h"
namespace {
// work around GCC bug 87476 (~9.0)
static const uint8_t urlEncodedTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static const uint8_t regularBase64Table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
template <bool UrlEncode>
uint8_t encodeValue(uint8_t valueIn) noexcept {
if constexpr (UrlEncode) {
return urlEncodedTable[valueIn];
} else {
return regularBase64Table[valueIn];
}
}
template <bool UrlEncode>
int doEncode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept {
const uint8_t* plainchar = plaintextIn;
const uint8_t* const plaintextEnd = plaintextIn + lengthIn;
uint8_t* codechar = codeOut;
uint8_t result = 0;
uint8_t fragment = 0;
while (1) {
if (plainchar == plaintextEnd) {
return codechar - codeOut;
}
// byte 1 of 3
fragment = *plainchar++;
result = (fragment & 0x0fc) >> 2;
*codechar++ = encodeValue<UrlEncode>(result);
result = (fragment & 0x003) << 4;
if (plainchar == plaintextEnd) {
*codechar++ = encodeValue<UrlEncode>(result);
if constexpr (!UrlEncode) {
*codechar++ = '=';
*codechar++ = '=';
}
return codechar - codeOut;
}
// byte 2 of 3
fragment = *plainchar++;
result |= (fragment & 0x0f0) >> 4;
*codechar++ = encodeValue<UrlEncode>(result);
result = (fragment & 0x00f) << 2;
if (plainchar == plaintextEnd) {
*codechar++ = encodeValue<UrlEncode>(result);
if constexpr (!UrlEncode) {
*codechar++ = '=';
}
return codechar - codeOut;
}
// byte 3 of 3
fragment = *plainchar++;
result |= (fragment & 0x0c0) >> 6;
*codechar++ = encodeValue<UrlEncode>(result);
result = (fragment & 0x03f) >> 0;
*codechar++ = encodeValue<UrlEncode>(result);
}
/* control should not reach here */
return codechar - codeOut;
}
template <bool UrlEncode>
int getEncodedLength(int dataLength) noexcept {
if constexpr (UrlEncode) {
auto r = dataLength % 3;
if (r == 0)
return (dataLength / 3) * 4;
else
return (dataLength / 3) * 4 + r + 1;
} else {
// any non-zero remainder after dividing the input length by 3 results in 4 extra output chars due to padding
return ((dataLength + 2) / 3) * 4;
}
}
template <bool UrlEncode>
StringRef doEncodeWithArena(Arena& arena, StringRef plainText) {
auto encodedLen = getEncodedLength<UrlEncode>(plainText.size());
if (encodedLen <= 0)
return StringRef();
auto out = new (arena) uint8_t[encodedLen];
auto actualLen = doEncode<UrlEncode>(plainText.begin(), plainText.size(), out);
ASSERT_EQ(encodedLen, actualLen);
return StringRef(out, encodedLen);
}
} // anonymous namespace
namespace base64 {
int encode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept {
return doEncode<false>(plaintextIn, lengthIn, codeOut);
}
int encodedLength(int dataLength) noexcept {
return getEncodedLength<false>(dataLength);
}
StringRef encode(Arena& arena, StringRef plainText) {
return doEncodeWithArena<false>(arena, plainText);
}
namespace url {
int encode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept {
return doEncode<true>(plaintextIn, lengthIn, codeOut);
}
int encodedLength(int dataLength) noexcept {
return getEncodedLength<true>(dataLength);
}
StringRef encode(Arena& arena, StringRef plainText) {
return doEncodeWithArena<true>(arena, plainText);
}
} // namespace url
} // namespace base64

View File

@ -1,277 +0,0 @@
/*
* Base64UrlDecode.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string_view>
#include <fmt/format.h>
#include "fdbrpc/Base64UrlEncode.h"
#include "fdbrpc/Base64UrlDecode.h"
#include "flow/flow.h"
#include "flow/Arena.h"
#include "flow/Error.h"
#include "flow/UnitTest.h"
namespace base64url {
constexpr uint8_t _X = 0xff;
inline uint8_t decodeValue(uint8_t valueIn) noexcept {
// clang-format off
constexpr const uint8_t decoding[] = { // 20x13
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, 62, _X, _X, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, _X, _X,
_X, _X, _X, _X, _X, 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, _X, _X, _X, _X, 63, _X, 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, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X,
_X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X, _X };
// clang-format on
static_assert(sizeof(decoding) / sizeof(decoding[0]) == 256);
return decoding[valueIn];
}
int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept {
const uint8_t* codechar = codeIn;
const uint8_t* const codeEnd = codeIn + lengthIn;
uint8_t* plainchar = plaintextOut;
uint8_t fragment = 0;
while (1) {
// code 1 of 4
if (codechar == codeEnd) {
return plainchar - plaintextOut;
}
fragment = decodeValue(*codechar++);
if (fragment == _X)
return -1;
*plainchar = (fragment & 0x03f) << 2;
if (codechar == codeEnd) {
return -1; // requires at least 2 chars to decode 1 plain byte
}
// code 2 of 4
fragment = decodeValue(*codechar++);
if (fragment == _X)
return -1;
*plainchar++ |= (fragment & 0x030) >> 4;
if (codechar == codeEnd) {
return plainchar - plaintextOut;
}
*plainchar = (fragment & 0x00f) << 4;
// code 3 of 4
fragment = decodeValue(*codechar++);
if (fragment == _X)
return -1;
*plainchar++ |= (fragment >> 2);
if (codechar == codeEnd) {
return plainchar - plaintextOut;
}
*plainchar = (fragment & 0x003) << 6;
// code 4 of 4
fragment = decodeValue(*codechar++);
if (fragment == _X)
return -1;
*plainchar++ |= (fragment & 0x03f);
}
/* control should not reach here */
return plainchar - plaintextOut;
}
int decodedLength(int codeLength) noexcept {
const auto r = (codeLength & 3);
if (r == 1)
return -1;
else if (r == 0)
return (codeLength / 4) * 3;
else
return (codeLength / 4) * 3 + (r - 1);
}
Optional<StringRef> decode(Arena& arena, StringRef base64UrlStr) {
auto decodedLen = decodedLength(base64UrlStr.size());
if (decodedLen <= 0) {
if (decodedLen == 0)
return StringRef{};
return {};
}
auto out = new (arena) uint8_t[decodedLen];
auto actualLen = decode(base64UrlStr.begin(), base64UrlStr.size(), out);
if (actualLen == -1) {
return {};
}
ASSERT_EQ(decodedLen, actualLen);
return StringRef(out, decodedLen);
}
} // namespace base64url
TEST_CASE("/fdbrpc/Base64UrlEncode") {
StringRef fixedSet[][2] = {
{ ""_sr, ""_sr },
{ "f"_sr, "Zg"_sr },
{ "fo"_sr, "Zm8"_sr },
{ "foo"_sr, "Zm9v"_sr },
{ "foob"_sr, "Zm9vYg"_sr },
{ "fooba"_sr, "Zm9vYmE"_sr },
{ "foobar"_sr, "Zm9vYmFy"_sr },
{ "Q\xc2\x93\x86\x04H\xfd\"r\x9c\xf7\xafW\xd1\x87^"_sr, "UcKThgRI_SJynPevV9GHXg"_sr },
{ "\x93"_sr, "kw"_sr },
{ "\xfcpF\xd2\x15\x03\x8a\xcb\\#!\xa1\x95\x18\x13\xfcpoN\rh=\xa5\x05\xe5\x00\xf8<\xc3\x8b'C\x90\xfc\xa0x\x13"
"8q\r\xd4\xca\xc9Yjv"_sr,
"_HBG0hUDistcIyGhlRgT_HBvTg1oPaUF5QD4PMOLJ0OQ_KB4EzhxDdTKyVlqdg"_sr },
{ "\xdd\xbb\x91>@\x9d\x88\x01Qb\x97[\xc3Q\xf6Q\\LF\xe2}\xfb\xf0\xe8\x98\xba\x8c\xc7\xc9\x0e$\xe4q\xcf;\xe4"
"e\x02"
"DA\xa9\x9a\xf0r\xc9\xf0\xd2-\x98"_sr,
"3buRPkCdiAFRYpdbw1H2UVxMRuJ9-_DomLqMx8kOJORxzzvkZQJEQama8HLJ8NItmA"_sr },
{ "2\x7f\x98\xfe\xb4\x05\x18.%.\xd0\x14\xea\x8e+\xa5\xc5\xbd"
"F-Lm\x04\x1aQ\xde\x1e\x9c\x12\xe6\x81{\x9dj\xe8\x9cP\xf4\xf7\x8a<\x12"_sr,
"Mn-Y_rQFGC4lLtAU6o4rpcW9Ri1MbQQaUd4enBLmgXudauicUPT3ijwS"_sr },
{ "\xa3"
"b\xc9\x13|\x94\xab)}\xf4N\xbc\xb2\xc5$\x15\xed\xb0\x98\xa4v\x8b\x91\xe4M\xb8\xde!\x94"_sr,
"o2LJE3yUqyl99E68ssUkFe2wmKR2i5HkTbjeIZQ"_sr },
{ "\xa0\xe9\xb4=4\xba\xbd\xbd\xaa\xfd\x96\xcb\x03\xd3\xb7\xc9\xb7i7\x18$^\xba\xe5\xb3\x8a\xf4O\xdb"_sr,
"oOm0PTS6vb2q_ZbLA9O3ybdpNxgkXrrls4r0T9s"_sr },
{ "\x10\xf4zscNoE\xfd\xc5\x8d\x16\x82t|y\n\xcf\xe8\x98\xf8)\xcd\xefm\xe2\xe1%\x17\x05T9;Zb\x05\x02\xc7"
"B\x8c\xc5\xc5\x95"
"8\xf2"_sr,
"EPR6c2NOb0X9xY0WgnR8eQrP6Jj4Kc3vbeLhJRcFVDk7WmIFAsdCjMXFlTjy"_sr },
{ "NG\"hA\xff\\\xf5lD\xf7\x08"
"7\xb9\t\x07\xa5\xb9\xac\x0b\x9fT+\xfa"_sr,
"TkciaEH_XPVsRPcIN7kJB6W5rAufVCv6"_sr },
{ "\xbd\xf5\xb0\x8eh1\x1b\xd1\x13q\x88z0*b\x9cNg\x88\x88MBD\x17\xec\xb0yc3\xbb"_sr,
"vfWwjmgxG9ETcYh6MCpinE5niIhNQkQX7LB5YzO7"_sr },
{ "&\xe0> \xfc\xea\x8e\xc1[I\xec\xe8\x03\x15\tc\x9b\x0f"
"d\x13"
"d\xab\xa5\x16\xa2p\x91\xd5\x11\xf5X\xa7\xbd\xe1\xa1"
"B\x8e\xe8\xddn2\xbf\x97"_sr,
"JuA-IPzqjsFbSezoAxUJY5sPZBNkq6UWonCR1RH1WKe94aFCjujdbjK_lw"_sr },
{ "|W,\xa5\xce\x83\xb0\xec\x87\x86\xd0<O\x94\x97"
"0F"_sr,
"fFcspc6DsOyHhtA8T5SXMEY"_sr },
{ "\x06\xa7\xf1"_sr, "Bqfx"_sr },
{ "u\xc4"
"7\x8e&\xa7\x90v"_sr,
"dcQ3jiankHY"_sr },
{ "X\xfe\xcd\x1f\xc8\x8f\xe3\xca"
"6\x96\x8c\x87\xcd\xbaJ!\xabq\x8c\x97)#\xfb\xda\xb8\xa9\xe9"
"a\x0c\xe2\x10\xe9\xe7\x16\x96\xb5"_sr,
"WP7NH8iP48o2loyHzbpKIatxjJcpI_vauKnpYQziEOnnFpa1"_sr },
{ "\xf1\n@\x18\xc3"
"F\xc4\xf8\x1c\xa9\xa9\xdb\x15\xcb\xd0V\xe4P\x8b\x8b\xaf\xf2\xfc\xb7\x1d\xa6p\n\xa3\x13,.\x12#"_sr,
"8QpAGMNGxPgcqanbFcvQVuRQi4uv8vy3HaZwCqMTLC4SIw"_sr },
{ "h\x9e\xe0X_\x1c\x04\xe3\xaf\xac\xe3\x18JK/\xe7\xbc"
"D\xf5"
"B\xfbK(Q\x8c\n\xca\xfc^\xd9\xdb\xb4\xea\xae\xb8"_sr,
"aJ7gWF8cBOOvrOMYSksv57xE9UL7SyhRjArK_F7Z27Tqrrg"_sr },
{ "\x9aX\xb2Q\xf0\x85R`\xc1\xbb\x95\xe7\x10\xe3\xd0x\n"_sr, "mliyUfCFUmDBu5XnEOPQeAo"_sr },
{ "\xa7\x1f"
"0\x90P\xab\x1fwJ\x86\x82Tj\xd0Ob\xa4\xac+\xc9"
"4;\x86\\\x8b\xb6"_sr,
"px8wkFCrH3dKhoJUatBPYqSsK8k0O4Zci7Y"_sr },
{ "\x0e"
"C\x87u-\x12\x8a\xe4\x11"
"F\xb6"
"a5\xcds\x1ez?J4\x02?@g\xaa-\xc4\xe0\x80\xe1!\xc0\x1d\x16\x1e"
"Em\xa8"
"a\xc8\x9d<\xd1"_sr,
"DkOHdS0SiuQRRrZhNc1zHno_SjQCP0Bnqi3E4IDhIcAdFh5FbahhyJ080Q"_sr },
{ "\xb7\xf6\xb6\\\xd3&\xf5"
"F\xb1\x86\xd8n%\xb5"
"a\x1d^Iv\xbe\x9bO\xcb\xca\xd2"_sr,
"t_a2XNMm9UaxhthuJbVhHV5Jdr6bT8vK0g"_sr },
{ "\xc0+\xea\xb9\x01\xf2\xe8\x8e\xf2\x0ft\x8b\xa0\xa4\x0cq=\xa6"
"c\xf1v\x9b\x81\xc7*\xd0\xe8Z\x04\"\t\xe8>\xd5w\xddQI\xc9\xba\x8f"_sr,
"wCvquQHy6I7yD3SLoKQMcT2mY_F2m4HHKtDoWgQiCeg-1XfdUUnJuo8"_sr },
{ "\xac"
"5\x8aH\xc9q\xad\xbe\x1f\x80\xed\xe1"_sr,
"rDWKSMlxrb4fgO3h"_sr },
{ "0}\x82\x95\xbb'\xf2\xdf"
"dR\x8f\xc2\xac\xb3\xc7\x9f\xc0\xf0"
"C3L\xbe"
"E\xe5\xf1\xc4% \xec\xe9"_sr,
"MH2Clbsn8t9kUo_CrLPHn8DwQzNMvkXl8cQlIOzp"_sr },
{ "\x8b\xab\xd7\xf9\xa5\xd8H\x1d"_sr, "i6vX-aXYSB0"_sr },
{ "i!\xdc\xb7~9\x7f\xad\xa0\x9d\x1e\xcc\xedTj\xe3\xe2\x88Q\x1e\xaa\xf9\xc3\xc5\xc5\xcdq\x9e\x07~\x9e\xcb\xf3\xd3\xb2\xec\xe0[m+\x0c\x9c"_sr,
"aSHct345f62gnR7M7VRq4-KIUR6q-cPFxc1xngd-nsvz07Ls4FttKwyc"_sr },
{ "\x01-{.s\xa5qF4\x1f"
"a\x11\xe4\x1eN"_sr,
"AS17LnOlcUY0H2ER5B5O"_sr },
{ "\x1c\xab\xce"
"e\xfc\xa7"
"are\x1f\x9a\xb4\xcdr\xe2v95\x88"_sr,
"HKvOZfynYXJlH5q0zXLidjk1iA"_sr },
{ "\xde\x0e\x16V\x12\x0f\xa4\xaf"
"2\xe7k3\xe8\"\x0b\xcb\x80\xa5\x96,\xba\xef\x1c\xe3\xd8\x16"
"C1\xccI\x8a]W\xf0\xbf\xaf\x19"
"4\xf9\r*<^?8C\x81\xd3(\xc6"_sr,
"3g4WVhIPpK8y52sz6CILy4Clliy67xzj2BZDMcxJil1X8L-vGTT5DSo8Xj84Q4HTKMY"_sr },
{ "\xe1\xe2\x8a"
"8v\x1f\xe0|\xccIJ\t"_sr,
"4eKKOHYf4HzMSUoJ"_sr },
};
const int fixedSetLen = sizeof(fixedSet) / sizeof(fixedSet[0]);
for (auto i = 0; i < fixedSetLen; i++) {
auto tmpArena = Arena();
auto [decodeOutputExpected, encodeOutputExpected] = fixedSet[i];
auto encodeOutput = base64url::encode(tmpArena, decodeOutputExpected);
if (encodeOutput != encodeOutputExpected) {
fmt::print("Fixed case {} (encode): expected '{}' got '{}'\n",
i + 1,
encodeOutputExpected.toHexString(),
encodeOutput.toHexString());
ASSERT(false);
}
auto decodeOutput = base64url::decode(tmpArena, encodeOutputExpected);
ASSERT(decodeOutput.present());
if (decodeOutput.get() != decodeOutputExpected) {
fmt::print("Fixed case {} (decode): expected '{}' got '{}'\n",
i + 1,
decodeOutputExpected.toHexString(),
decodeOutput.get().toHexString());
ASSERT(false);
}
}
auto& rng = *deterministicRandom();
for (auto i = 0; i < 100; i++) {
auto tmpArena = Arena();
auto inputLen = rng.randomInt(1, 300);
auto inputBuf = new (tmpArena) uint8_t[inputLen];
for (auto i = 0; i < inputLen; i++)
inputBuf[i] = rng.randomInt(0, 256);
auto input = StringRef(inputBuf, inputLen);
auto output = base64url::encode(tmpArena, input);
// make sure output only contains legal characters
for (auto i = 0; i < output.size(); i++)
ASSERT_NE(base64url::decodeValue(output[i]), base64url::_X);
auto decodeOutput = base64url::decode(tmpArena, output);
ASSERT(decodeOutput.present());
if (input != decodeOutput.get()) {
fmt::print("Dynamic case {} (decode) failed, expected '{}', got '{}'\n",
input.toHexString(),
decodeOutput.get().toHexString());
ASSERT(false);
}
}
return Void();
}

View File

@ -1,88 +0,0 @@
/*
* Base64UrlEncode.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "fdbrpc/Base64UrlEncode.h"
namespace base64url {
uint8_t encodeValue(uint8_t valueIn) noexcept {
constexpr const uint8_t encoding[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
return encoding[valueIn];
}
int encode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept {
const uint8_t* plainchar = plaintextIn;
const uint8_t* const plaintextEnd = plaintextIn + lengthIn;
uint8_t* codechar = codeOut;
uint8_t result = 0;
uint8_t fragment = 0;
while (1) {
if (plainchar == plaintextEnd) {
return codechar - codeOut;
}
// byte 1 of 3
fragment = *plainchar++;
result = (fragment & 0x0fc) >> 2;
*codechar++ = encodeValue(result);
result = (fragment & 0x003) << 4;
if (plainchar == plaintextEnd) {
*codechar++ = encodeValue(result);
return codechar - codeOut;
}
// byte 2 of 3
fragment = *plainchar++;
result |= (fragment & 0x0f0) >> 4;
*codechar++ = encodeValue(result);
result = (fragment & 0x00f) << 2;
if (plainchar == plaintextEnd) {
*codechar++ = encodeValue(result);
return codechar - codeOut;
}
// byte 3 of 3
fragment = *plainchar++;
result |= (fragment & 0x0c0) >> 6;
*codechar++ = encodeValue(result);
result = (fragment & 0x03f) >> 0;
*codechar++ = encodeValue(result);
}
/* control should not reach here */
return codechar - codeOut;
}
int encodedLength(int dataLength) noexcept {
auto r = dataLength % 3;
if (r == 0)
return (dataLength / 3) * 4;
else
return (dataLength / 3) * 4 + r + 1;
}
StringRef encode(Arena& arena, StringRef plainText) {
auto encodedLen = encodedLength(plainText.size());
if (encodedLen <= 0)
return StringRef();
auto out = new (arena) uint8_t[encodedLen];
auto actualLen = encode(plainText.begin(), plainText.size(), out);
ASSERT_EQ(encodedLen, actualLen);
return StringRef(out, encodedLen);
}
} // namespace base64url

View File

@ -25,8 +25,8 @@
#include "flow/MkCert.h"
#include "flow/PKey.h"
#include "flow/UnitTest.h"
#include "fdbrpc/Base64UrlEncode.h"
#include "fdbrpc/Base64UrlDecode.h"
#include "fdbrpc/Base64Encode.h"
#include "fdbrpc/Base64Decode.h"
#include "fdbrpc/JsonWebKeySet.h"
#if defined(HAVE_WOLFSSL)
#include <wolfssl/options.h>
@ -135,7 +135,7 @@ bool getJwkBigNumMember(Arena& arena,
} else {
data = b64Member.get();
}
auto decoded = base64url::decode(arena, data);
auto decoded = base64::url::decode(arena, data);
if (!decoded.present()) {
JWK_PARSE_ERROR("Base64URL decoding for parameter failed")
.detail("Algorithm", algorithm)
@ -170,7 +170,7 @@ StringRef bigNumToBase64Url(Arena& arena, const BIGNUM* bn) {
auto len = BN_num_bytes(bn);
auto buf = new (arena) uint8_t[len];
::BN_bn2bin(bn, buf);
return base64url::encode(arena, StringRef(buf, len));
return base64::url::encode(arena, StringRef(buf, len));
}
Optional<PublicOrPrivateKey> parseEcP256Key(StringRef b64x, StringRef b64y, Optional<StringRef> b64d, int keyIndex) {

View File

@ -1,3 +1,5 @@
#include "fdbrpc/Base64Encode.h"
#include "fdbrpc/Base64Decode.h"
#include "fdbrpc/FlowTransport.h"
#include "fdbrpc/TokenCache.h"
#include "fdbrpc/TokenSign.h"
@ -7,6 +9,10 @@
#include "flow/UnitTest.h"
#include "flow/network.h"
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <boost/unordered_map.hpp>
#include <boost/unordered_set.hpp>
@ -215,12 +221,20 @@ bool TokenCache::validate(TenantNameRef name, StringRef token) {
bool TokenCacheImpl::validateAndAdd(double currentTime, StringRef token, NetworkAddress const& peer) {
Arena arena;
authz::jwt::TokenRef t;
if (!authz::jwt::parseToken(arena, t, token)) {
StringRef signInput;
Optional<StringRef> err;
bool verifyOutcome;
if ((err = authz::jwt::parseToken(arena, token, t, signInput)).present()) {
CODE_PROBE(true, "Token can't be parsed");
TraceEvent(SevWarn, "InvalidToken")
.detail("From", peer)
.detail("Reason", "ParseError")
.detail("Token", token.toString());
TraceEvent te(SevWarn, "InvalidToken");
te.detail("From", peer);
te.detail("Reason", "ParseError");
te.detail("ErrorDetail", err.get());
if (signInput.empty()) { // unrecognizable token structure
te.detail("Token", token.toString());
} else { // trace with signature part taken out
te.detail("SignInput", signInput.toString());
}
return false;
}
auto key = FlowTransport::transport().getPublicKeyByName(t.keyId);
@ -245,14 +259,20 @@ bool TokenCacheImpl::validateAndAdd(double currentTime, StringRef token, Network
TRACE_INVALID_PARSED_TOKEN("NoNotBefore", t);
return false;
} else if (double(t.notBeforeUnixTime.get()) > currentTime) {
CODE_PROBE(true, "Tokens not-before is in the future");
CODE_PROBE(true, "Token's not-before is in the future");
TRACE_INVALID_PARSED_TOKEN("TokenNotYetValid", t);
return false;
} else if (!t.tenants.present()) {
CODE_PROBE(true, "Token with no tenants");
TRACE_INVALID_PARSED_TOKEN("NoTenants", t);
return false;
} else if (!authz::jwt::verifyToken(token, key.get())) {
}
std::tie(verifyOutcome, err) = authz::jwt::verifyToken(signInput, t, key.get());
if (err.present()) {
CODE_PROBE(true, "Error while verifying token");
TRACE_INVALID_PARSED_TOKEN("ErrorWhileVerifyingToken", t).detail("ErrorDetail", err.get());
return false;
} else if (!verifyOutcome) {
CODE_PROBE(true, "Token with invalid signature");
TRACE_INVALID_PARSED_TOKEN("InvalidSignature", t);
return false;
@ -314,7 +334,13 @@ extern TokenRef makeRandomTokenSpec(Arena&, IRandom&, authz::Algorithm);
}
TEST_CASE("/fdbrpc/authz/TokenCache/BadTokens") {
std::pair<void (*)(Arena&, IRandom&, authz::jwt::TokenRef&), char const*> badMutations[]{
auto const pubKeyName = "someEcPublicKey"_sr;
auto const rsaPubKeyName = "someRsaPublicKey"_sr;
auto privateKey = mkcert::makeEcP256();
auto publicKey = privateKey.toPublic();
auto rsaPrivateKey = mkcert::makeRsa4096Bit(); // to trigger unmatched sign algorithm
auto rsaPublicKey = rsaPrivateKey.toPublic();
std::pair<std::function<void(Arena&, IRandom&, authz::jwt::TokenRef&)>, char const*> badMutations[]{
{
[](Arena&, IRandom&, authz::jwt::TokenRef&) { FlowTransport::transport().removeAllPublicKeys(); },
"NoKeyWithSuchName",
@ -355,19 +381,21 @@ TEST_CASE("/fdbrpc/authz/TokenCache/BadTokens") {
},
"UnmatchedTenant",
},
{
[rsaPubKeyName](Arena& arena, IRandom&, authz::jwt::TokenRef& token) { token.keyId = rsaPubKeyName; },
"UnmatchedSignAlgorithm",
},
};
auto const pubKeyName = "somePublicKey"_sr;
auto privateKey = mkcert::makeEcP256();
auto const numBadMutations = sizeof(badMutations) / sizeof(badMutations[0]);
for (auto repeat = 0; repeat < 50; repeat++) {
auto arena = Arena();
auto& rng = *deterministicRandom();
auto validTokenSpec = authz::jwt::makeRandomTokenSpec(arena, rng, authz::Algorithm::ES256);
validTokenSpec.keyId = pubKeyName;
for (auto i = 0; i <= numBadMutations; i++) {
FlowTransport::transport().addPublicKey(pubKeyName, privateKey.toPublic());
auto publicKeyClearGuard =
ScopeExit([pubKeyName]() { FlowTransport::transport().removePublicKey(pubKeyName); });
for (auto i = 0; i <= numBadMutations + 1; i++) {
FlowTransport::transport().addPublicKey(pubKeyName, publicKey);
FlowTransport::transport().addPublicKey(rsaPubKeyName, rsaPublicKey);
auto publicKeyClearGuard = ScopeExit([]() { FlowTransport::transport().removeAllPublicKeys(); });
auto signedToken = StringRef();
auto tmpArena = Arena();
if (i < numBadMutations) {
@ -381,14 +409,37 @@ TEST_CASE("/fdbrpc/authz/TokenCache/BadTokens") {
mutatedTokenSpec.toStringRef(tmpArena).toStringView());
ASSERT(false);
}
} else {
} else if (i == numBadMutations) {
// squeeze in a bad signature case that does not fit into mutation interface
signedToken = authz::jwt::signToken(tmpArena, validTokenSpec, privateKey);
signedToken = signedToken.substr(0, signedToken.size() - 1);
signedToken.popBack();
if (TokenCache::instance().validate(validTokenSpec.tenants.get()[0], signedToken)) {
fmt::print("Unexpected successful validation with a token with truncated signature part\n");
ASSERT(false);
}
} else {
// test if badly base64-encoded tenant name causes validation to fail as expected
auto signInput = authz::jwt::makeSignInput(tmpArena, validTokenSpec);
auto b64Header = signInput.eat("."_sr);
auto payload = base64::url::decode(tmpArena, signInput).get();
rapidjson::Document d;
d.Parse(reinterpret_cast<const char*>(payload.begin()), payload.size());
ASSERT(!d.HasParseError());
rapidjson::StringBuffer wrBuf;
rapidjson::Writer<rapidjson::StringBuffer> wr(wrBuf);
auto tenantsField = d.FindMember("tenants");
ASSERT(tenantsField != d.MemberEnd());
tenantsField->value.PushBack("ABC#", d.GetAllocator()); // inject base64-illegal character
d.Accept(wr);
auto b64ModifiedPayload = base64::url::encode(
tmpArena, StringRef(reinterpret_cast<const uint8_t*>(wrBuf.GetString()), wrBuf.GetSize()));
signInput = b64Header.withSuffix("."_sr, tmpArena).withSuffix(b64ModifiedPayload, tmpArena);
signedToken = authz::jwt::signToken(tmpArena, signInput, validTokenSpec.algorithm, privateKey);
if (TokenCache::instance().validate(validTokenSpec.tenants.get()[0], signedToken)) {
fmt::print(
"Unexpected successful validation of a token with tenant name containing non-base64 chars)\n");
ASSERT(false);
}
}
}
}

View File

@ -47,8 +47,8 @@
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/error/en.h>
#include "fdbrpc/Base64UrlEncode.h"
#include "fdbrpc/Base64UrlDecode.h"
#include "fdbrpc/Base64Encode.h"
#include "fdbrpc/Base64Decode.h"
namespace {
@ -68,15 +68,11 @@ StringRef genRandomAlphanumStringRef(Arena& arena, IRandom& rng, int minLen, int
return StringRef(strRaw, len);
}
bool checkVerifyAlgorithm(PKeyAlgorithm algo, PublicKey key) {
Optional<StringRef> checkVerifyAlgorithm(PKeyAlgorithm algo, PublicKey key) {
if (algo != key.algorithm()) {
TraceEvent(SevWarnAlways, "TokenVerifyAlgoMismatch")
.suppressFor(10)
.detail("Expected", pkeyAlgorithmName(algo))
.detail("PublicKeyAlgorithm", key.algorithmName());
return false;
return "Token algorithm does not match key's"_sr;
} else {
return true;
return {};
}
}
@ -242,8 +238,11 @@ StringRef TokenRef::toStringRef(Arena& arena) {
return StringRef(str, buf.size());
}
template <class FieldType, class Writer>
void putField(Optional<FieldType> const& field, Writer& wr, const char* fieldName) {
template <class FieldType, class Writer, bool MakeStringArrayBase64 = false>
void putField(Optional<FieldType> const& field,
Writer& wr,
const char* fieldName,
std::bool_constant<MakeStringArrayBase64> _ = std::bool_constant<false>{}) {
if (!field.present())
return;
wr.Key(fieldName);
@ -256,14 +255,22 @@ void putField(Optional<FieldType> const& field, Writer& wr, const char* fieldNam
wr.Uint64(value);
} else {
wr.StartArray();
for (auto elem : value) {
wr.String(reinterpret_cast<const char*>(elem.begin()), elem.size());
if constexpr (MakeStringArrayBase64) {
Arena arena;
for (auto elem : value) {
auto encodedElem = base64::encode(arena, elem);
wr.String(reinterpret_cast<const char*>(encodedElem.begin()), encodedElem.size());
}
} else {
for (auto elem : value) {
wr.String(reinterpret_cast<const char*>(elem.begin()), elem.size());
}
}
wr.EndArray();
}
}
StringRef makeTokenPart(Arena& arena, TokenRef tokenSpec) {
StringRef makeSignInput(Arena& arena, const TokenRef& tokenSpec) {
using Buffer = rapidjson::StringBuffer;
using Writer = rapidjson::Writer<Buffer>;
auto headerBuffer = Buffer();
@ -288,222 +295,261 @@ StringRef makeTokenPart(Arena& arena, TokenRef tokenSpec) {
putField(tokenSpec.expiresAtUnixTime, payload, "exp");
putField(tokenSpec.notBeforeUnixTime, payload, "nbf");
putField(tokenSpec.tokenId, payload, "jti");
putField(tokenSpec.tenants, payload, "tenants");
putField(tokenSpec.tenants, payload, "tenants", std::bool_constant<true>{} /* encode tenants in base64 */);
payload.EndObject();
auto const headerPartLen = base64url::encodedLength(headerBuffer.GetSize());
auto const payloadPartLen = base64url::encodedLength(payloadBuffer.GetSize());
auto const headerPartLen = base64::url::encodedLength(headerBuffer.GetSize());
auto const payloadPartLen = base64::url::encodedLength(payloadBuffer.GetSize());
auto const totalLen = headerPartLen + 1 + payloadPartLen;
auto out = new (arena) uint8_t[totalLen];
auto cur = out;
cur += base64url::encode(reinterpret_cast<const uint8_t*>(headerBuffer.GetString()), headerBuffer.GetSize(), cur);
cur += base64::url::encode(reinterpret_cast<const uint8_t*>(headerBuffer.GetString()), headerBuffer.GetSize(), cur);
ASSERT_EQ(cur - out, headerPartLen);
*cur++ = '.';
cur += base64url::encode(reinterpret_cast<const uint8_t*>(payloadBuffer.GetString()), payloadBuffer.GetSize(), cur);
cur +=
base64::url::encode(reinterpret_cast<const uint8_t*>(payloadBuffer.GetString()), payloadBuffer.GetSize(), cur);
ASSERT_EQ(cur - out, totalLen);
return StringRef(out, totalLen);
}
StringRef signToken(Arena& arena, TokenRef tokenSpec, PrivateKey privateKey) {
StringRef signToken(Arena& arena, StringRef signInput, Algorithm algorithm, PrivateKey privateKey) {
auto tmpArena = Arena();
auto tokenPart = makeTokenPart(tmpArena, tokenSpec);
auto [signAlgo, digest] = getMethod(tokenSpec.algorithm);
auto [signAlgo, digest] = getMethod(algorithm);
if (!checkSignAlgorithm(signAlgo, privateKey)) {
throw digital_signature_ops_error();
}
auto plainSig = privateKey.sign(tmpArena, tokenPart, *digest);
if (tokenSpec.algorithm == Algorithm::ES256) {
auto plainSig = privateKey.sign(tmpArena, signInput, *digest);
if (algorithm == Algorithm::ES256) {
// Need to convert ASN.1/DER signature to IEEE-P1363
auto convertedSig = convertEs256DerToP1363(tmpArena, plainSig);
if (!convertedSig.present()) {
auto tmpArena = Arena();
TraceEvent(SevWarn, "TokenSigConversionFailure")
.detail("TokenSpec", tokenSpec.toStringRef(tmpArena).toString());
TraceEvent(SevWarn, "TokenSigConversionFailure").log();
throw digital_signature_ops_error();
}
plainSig = convertedSig.get();
}
auto const sigPartLen = base64url::encodedLength(plainSig.size());
auto const totalLen = tokenPart.size() + 1 + sigPartLen;
auto const sigPartLen = base64::url::encodedLength(plainSig.size());
auto const totalLen = signInput.size() + 1 + sigPartLen;
auto out = new (arena) uint8_t[totalLen];
auto cur = out;
::memcpy(cur, tokenPart.begin(), tokenPart.size());
cur += tokenPart.size();
::memcpy(cur, signInput.begin(), signInput.size());
cur += signInput.size();
*cur++ = '.';
cur += base64url::encode(plainSig.begin(), plainSig.size(), cur);
cur += base64::url::encode(plainSig.begin(), plainSig.size(), cur);
ASSERT_EQ(cur - out, totalLen);
return StringRef(out, totalLen);
}
bool parseHeaderPart(Arena& arena, TokenRef& token, StringRef b64urlHeader) {
StringRef signToken(Arena& arena, const TokenRef& tokenSpec, PrivateKey privateKey) {
auto tmpArena = Arena();
auto optHeader = base64url::decode(tmpArena, b64urlHeader);
auto signInput = makeSignInput(tmpArena, tokenSpec);
return signToken(arena, signInput, tokenSpec.algorithm, privateKey);
}
Optional<StringRef> parseHeaderPart(Arena& arena, TokenRef& token, StringRef b64urlHeader) {
auto tmpArena = Arena();
auto optHeader = base64::url::decode(tmpArena, b64urlHeader);
if (!optHeader.present())
return false;
return "Failed to decode base64 header"_sr;
auto header = optHeader.get();
auto d = rapidjson::Document();
d.Parse(reinterpret_cast<const char*>(header.begin()), header.size());
if (d.HasParseError()) {
TraceEvent(SevWarnAlways, "TokenHeaderJsonParseError")
.suppressFor(10)
.detail("Header", header.toString())
.detail("Message", GetParseError_En(d.GetParseError()))
.detail("Offset", d.GetErrorOffset());
return false;
return "Failed to parse header as JSON"_sr;
}
if (!d.IsObject())
return false;
return "Header is not a JSON object"_sr;
auto typItr = d.FindMember("typ");
if (typItr == d.MemberEnd() || !typItr->value.IsString())
return false;
return "No 'typ' field"_sr;
auto algItr = d.FindMember("alg");
if (algItr == d.MemberEnd() || !algItr->value.IsString())
return false;
return "No 'alg' field"_sr;
auto kidItr = d.FindMember("kid");
if (kidItr == d.MemberEnd() || !kidItr->value.IsString())
return false;
return "No 'kid' field"_sr;
auto const& typ = typItr->value;
auto const& alg = algItr->value;
auto const& kid = kidItr->value;
auto typValue = StringRef(reinterpret_cast<const uint8_t*>(typ.GetString()), typ.GetStringLength());
if (typValue != "JWT"_sr)
return false;
return "'typ' is not 'JWT'"_sr;
auto algValue = StringRef(reinterpret_cast<const uint8_t*>(alg.GetString()), alg.GetStringLength());
auto algType = algorithmFromString(algValue);
if (algType == Algorithm::UNKNOWN)
return false;
return "Unsupported algorithm"_sr;
token.algorithm = algType;
token.keyId = StringRef(arena, reinterpret_cast<const uint8_t*>(kid.GetString()), kid.GetStringLength());
return true;
return {};
}
template <class FieldType>
bool parseField(Arena& arena, Optional<FieldType>& out, const rapidjson::Document& d, const char* fieldName) {
template <class FieldType, bool ExpectBase64StringArray = false>
Optional<StringRef> parseField(Arena& arena,
Optional<FieldType>& out,
const rapidjson::Document& d,
const char* fieldName,
std::bool_constant<ExpectBase64StringArray> _ = std::bool_constant<false>{}) {
auto fieldItr = d.FindMember(fieldName);
if (fieldItr == d.MemberEnd())
return true;
return {};
auto const& field = fieldItr->value;
static_assert(std::is_same_v<StringRef, FieldType> || std::is_same_v<FieldType, uint64_t> ||
std::is_same_v<FieldType, VectorRef<StringRef>>);
if constexpr (std::is_same_v<FieldType, StringRef>) {
if (!field.IsString())
return false;
if (!field.IsString()) {
return StringRef(arena, fmt::format("'{}' is not a string", fieldName));
}
out = StringRef(arena, reinterpret_cast<const uint8_t*>(field.GetString()), field.GetStringLength());
} else if constexpr (std::is_same_v<FieldType, uint64_t>) {
if (!field.IsNumber())
return false;
if (!field.IsNumber()) {
return StringRef(arena, fmt::format("'{}' is not a number", fieldName));
}
out = static_cast<uint64_t>(field.GetDouble());
} else {
if (!field.IsArray())
return false;
if (!field.IsArray()) {
return StringRef(arena, fmt::format("'{}' is not an array", fieldName));
}
if (field.Size() > 0) {
auto vector = new (arena) StringRef[field.Size()];
for (auto i = 0; i < field.Size(); i++) {
if (!field[i].IsString())
return false;
vector[i] = StringRef(
arena, reinterpret_cast<const uint8_t*>(field[i].GetString()), field[i].GetStringLength());
if (!field[i].IsString()) {
return StringRef(arena, fmt::format("{}th element of '{}' is not a string", i + 1, fieldName));
}
if constexpr (ExpectBase64StringArray) {
Optional<StringRef> decodedString = base64::decode(
arena,
StringRef(reinterpret_cast<const uint8_t*>(field[i].GetString()), field[i].GetStringLength()));
if (decodedString.present()) {
vector[i] = decodedString.get();
} else {
CODE_PROBE(true, "Base64 token field has failed to be parsed");
return StringRef(arena,
fmt::format("Failed to base64-decode {}th element of '{}'", i + 1, fieldName));
}
} else {
vector[i] = StringRef(
arena, reinterpret_cast<const uint8_t*>(field[i].GetString()), field[i].GetStringLength());
}
}
out = VectorRef<StringRef>(vector, field.Size());
} else {
out = VectorRef<StringRef>();
}
}
return true;
return {};
}
bool parsePayloadPart(Arena& arena, TokenRef& token, StringRef b64urlPayload) {
Optional<StringRef> parsePayloadPart(Arena& arena, TokenRef& token, StringRef b64urlPayload) {
auto tmpArena = Arena();
auto optPayload = base64url::decode(tmpArena, b64urlPayload);
auto optPayload = base64::url::decode(tmpArena, b64urlPayload);
if (!optPayload.present())
return false;
return "Failed to base64-decode payload part"_sr;
auto payload = optPayload.get();
auto d = rapidjson::Document();
d.Parse(reinterpret_cast<const char*>(payload.begin()), payload.size());
if (d.HasParseError()) {
TraceEvent(SevWarnAlways, "TokenPayloadJsonParseError")
.suppressFor(10)
.detail("Payload", payload.toString())
.detail("Message", GetParseError_En(d.GetParseError()))
.detail("Offset", d.GetErrorOffset());
return false;
return "Token payload part is not valid JSON"_sr;
}
if (!d.IsObject())
return false;
if (!parseField(arena, token.issuer, d, "iss"))
return false;
if (!parseField(arena, token.subject, d, "sub"))
return false;
if (!parseField(arena, token.audience, d, "aud"))
return false;
if (!parseField(arena, token.tokenId, d, "jti"))
return false;
if (!parseField(arena, token.issuedAtUnixTime, d, "iat"))
return false;
if (!parseField(arena, token.expiresAtUnixTime, d, "exp"))
return false;
if (!parseField(arena, token.notBeforeUnixTime, d, "nbf"))
return false;
if (!parseField(arena, token.tenants, d, "tenants"))
return false;
return true;
return "Token payload is not a JSON object"_sr;
Optional<StringRef> err;
if ((err = parseField(arena, token.issuer, d, "iss")).present())
return err;
if ((err = parseField(arena, token.subject, d, "sub")).present())
return err;
if ((err = parseField(arena, token.audience, d, "aud")).present())
return err;
if ((err = parseField(arena, token.tokenId, d, "jti")).present())
return err;
if ((err = parseField(arena, token.issuedAtUnixTime, d, "iat")).present())
return err;
if ((err = parseField(arena, token.expiresAtUnixTime, d, "exp")).present())
return err;
if ((err = parseField(arena, token.notBeforeUnixTime, d, "nbf")).present())
return err;
if ((err = parseField(arena,
token.tenants,
d,
"tenants",
std::bool_constant<true>{} /* expect field elements encoded in base64 */))
.present())
return err;
return {};
}
bool parseSignaturePart(Arena& arena, TokenRef& token, StringRef b64urlSignature) {
auto optSig = base64url::decode(arena, b64urlSignature);
Optional<StringRef> parseSignaturePart(Arena& arena, TokenRef& token, StringRef b64urlSignature) {
auto optSig = base64::url::decode(arena, b64urlSignature);
if (!optSig.present())
return false;
return "Failed to base64url-decode signature part"_sr;
token.signature = optSig.get();
return true;
return {};
}
StringRef signaturePart(StringRef token) {
token.eat("."_sr);
token.eat("."_sr);
return token;
}
bool parseToken(Arena& arena, TokenRef& token, StringRef signedToken) {
auto b64urlHeader = signedToken.eat("."_sr);
auto b64urlPayload = signedToken.eat("."_sr);
auto b64urlSignature = signedToken;
Optional<StringRef> parseToken(Arena& arena,
StringRef signedTokenIn,
TokenRef& parsedTokenOut,
StringRef& signInputOut) {
signInputOut = StringRef();
auto fullToken = signedTokenIn;
auto b64urlHeader = signedTokenIn.eat("."_sr);
auto b64urlPayload = signedTokenIn.eat("."_sr);
auto b64urlSignature = signedTokenIn;
if (b64urlHeader.empty() || b64urlPayload.empty() || b64urlSignature.empty())
return false;
if (!parseHeaderPart(arena, token, b64urlHeader))
return false;
if (!parsePayloadPart(arena, token, b64urlPayload))
return false;
if (!parseSignaturePart(arena, token, b64urlSignature))
return false;
return true;
return "Token does not follow header.payload.signature structure"_sr;
signInputOut = fullToken.substr(0, b64urlHeader.size() + 1 + b64urlPayload.size());
auto err = Optional<StringRef>();
if ((err = parseHeaderPart(arena, parsedTokenOut, b64urlHeader)).present())
return err;
if ((err = parsePayloadPart(arena, parsedTokenOut, b64urlPayload)).present())
return err;
if ((err = parseSignaturePart(arena, parsedTokenOut, b64urlSignature)).present())
return err;
return err;
}
bool verifyToken(StringRef signedToken, PublicKey publicKey) {
std::pair<bool, Optional<StringRef>> verifyToken(StringRef signInput,
const TokenRef& parsedToken,
PublicKey publicKey) {
Arena tmpArena;
Optional<StringRef> err;
auto [verifyAlgo, digest] = getMethod(parsedToken.algorithm);
if ((err = checkVerifyAlgorithm(verifyAlgo, publicKey)).present())
return { false, err };
auto sig = parsedToken.signature;
if (parsedToken.algorithm == Algorithm::ES256) {
// Need to convert IEEE-P1363 signature to ASN.1/DER
auto convertedSig = convertEs256P1363ToDer(tmpArena, sig);
if (!convertedSig.present() || convertedSig.get().empty()) {
err = "Failed to convert signature for verification"_sr;
return { false, err };
}
sig = convertedSig.get();
}
return { publicKey.verify(signInput, sig, *digest), err };
}
std::pair<bool, Optional<StringRef>> verifyToken(StringRef signedToken, PublicKey publicKey) {
auto arena = Arena();
auto fullToken = signedToken;
auto b64urlHeader = signedToken.eat("."_sr);
auto b64urlPayload = signedToken.eat("."_sr);
auto b64urlSignature = signedToken;
if (b64urlHeader.empty() || b64urlPayload.empty() || b64urlSignature.empty())
return false;
auto b64urlTokenPart = fullToken.substr(0, b64urlHeader.size() + 1 + b64urlPayload.size());
auto optSig = base64url::decode(arena, b64urlSignature);
if (!optSig.present())
return false;
auto sig = optSig.get();
auto parsedToken = TokenRef();
if (!parseHeaderPart(arena, parsedToken, b64urlHeader))
return false;
auto [verifyAlgo, digest] = getMethod(parsedToken.algorithm);
if (!checkVerifyAlgorithm(verifyAlgo, publicKey))
return false;
if (parsedToken.algorithm == Algorithm::ES256) {
// Need to convert IEEE-P1363 signature to ASN.1/DER
auto convertedSig = convertEs256P1363ToDer(arena, sig);
if (!convertedSig.present())
return false;
sig = convertedSig.get();
auto err = Optional<StringRef>();
if (b64urlHeader.empty() || b64urlPayload.empty() || b64urlSignature.empty()) {
err = "Token does not follow header.payload.signature structure"_sr;
return { false, err };
}
return publicKey.verify(b64urlTokenPart, sig, *digest);
auto signInput = fullToken.substr(0, b64urlHeader.size() + 1 + b64urlPayload.size());
auto parsedToken = TokenRef();
if ((err = parseHeaderPart(arena, parsedToken, b64urlHeader)).present())
return { false, err };
auto optSig = base64::url::decode(arena, b64urlSignature);
if (!optSig.present()) {
err = "Failed to base64url-decode signature part"_sr;
return { false, err };
}
parsedToken.signature = optSig.get();
return verifyToken(signInput, parsedToken, publicKey);
}
TokenRef makeRandomTokenSpec(Arena& arena, IRandom& rng, Algorithm alg) {
@ -538,20 +584,19 @@ TEST_CASE("/fdbrpc/TokenSign/FlatBuffer") {
for (auto i = 0; i < numIters; i++) {
auto arena = Arena();
auto privateKey = mkcert::makeEcP256();
auto publicKey = privateKey.toPublic();
auto& rng = *deterministicRandom();
auto tokenSpec = authz::flatbuffers::makeRandomTokenSpec(arena, rng);
auto keyName = genRandomAlphanumStringRef(arena, rng, MinKeyNameLen, MaxKeyNameLenPlus1);
auto signedToken = authz::flatbuffers::signToken(arena, tokenSpec, keyName, privateKey);
const auto verifyExpectOk = authz::flatbuffers::verifyToken(signedToken, privateKey.toPublic());
ASSERT(verifyExpectOk);
ASSERT(authz::flatbuffers::verifyToken(signedToken, publicKey));
// try tampering with signed token by adding one more tenant
tokenSpec.tenants.push_back(arena,
genRandomAlphanumStringRef(arena, rng, MinTenantNameLen, MaxTenantNameLenPlus1));
auto writer = ObjectWriter([&arena](size_t len) { return new (arena) uint8_t[len]; }, IncludeVersion());
writer.serialize(tokenSpec);
signedToken.token = writer.toStringRef();
const auto verifyExpectFail = authz::flatbuffers::verifyToken(signedToken, privateKey.toPublic());
ASSERT(!verifyExpectFail);
ASSERT(!authz::flatbuffers::verifyToken(signedToken, publicKey));
}
printf("%d runs OK\n", numIters);
return Void();
@ -562,19 +607,24 @@ TEST_CASE("/fdbrpc/TokenSign/JWT") {
for (auto i = 0; i < numIters; i++) {
auto arena = Arena();
auto privateKey = mkcert::makeEcP256();
auto publicKey = privateKey.toPublic();
auto& rng = *deterministicRandom();
auto tokenSpec = authz::jwt::makeRandomTokenSpec(arena, rng, authz::Algorithm::ES256);
auto signedToken = authz::jwt::signToken(arena, tokenSpec, privateKey);
const auto verifyExpectOk = authz::jwt::verifyToken(signedToken, privateKey.toPublic());
ASSERT(verifyExpectOk);
auto verifyOk = false;
auto verifyErr = Optional<StringRef>();
std::tie(verifyOk, verifyErr) = authz::jwt::verifyToken(signedToken, publicKey);
ASSERT(!verifyErr.present());
ASSERT(verifyOk);
auto signaturePart = signedToken;
signaturePart.eat("."_sr);
signaturePart.eat("."_sr);
{
auto parsedToken = authz::jwt::TokenRef{};
auto tmpArena = Arena();
auto parseOk = parseToken(tmpArena, parsedToken, signedToken);
ASSERT(parseOk);
auto parsedToken = authz::jwt::TokenRef{};
auto signInput = StringRef();
auto parseError = parseToken(tmpArena, signedToken, parsedToken, signInput);
ASSERT(!parseError.present());
ASSERT_EQ(tokenSpec.algorithm, parsedToken.algorithm);
ASSERT(tokenSpec.issuer == parsedToken.issuer);
ASSERT(tokenSpec.subject == parsedToken.subject);
@ -585,17 +635,21 @@ TEST_CASE("/fdbrpc/TokenSign/JWT") {
ASSERT_EQ(tokenSpec.expiresAtUnixTime.get(), parsedToken.expiresAtUnixTime.get());
ASSERT_EQ(tokenSpec.notBeforeUnixTime.get(), parsedToken.notBeforeUnixTime.get());
ASSERT(tokenSpec.tenants == parsedToken.tenants);
auto optSig = base64url::decode(tmpArena, signaturePart);
auto optSig = base64::url::decode(tmpArena, signaturePart);
ASSERT(optSig.present());
ASSERT(optSig.get() == parsedToken.signature);
std::tie(verifyOk, verifyErr) = authz::jwt::verifyToken(signInput, parsedToken, publicKey);
ASSERT(!verifyErr.present());
ASSERT(verifyOk);
}
// try tampering with signed token by adding one more tenant
tokenSpec.tenants.get().push_back(
arena, genRandomAlphanumStringRef(arena, rng, MinTenantNameLen, MaxTenantNameLenPlus1));
auto tamperedTokenPart = makeTokenPart(arena, tokenSpec);
auto tamperedTokenPart = makeSignInput(arena, tokenSpec);
auto tamperedTokenString = fmt::format("{}.{}", tamperedTokenPart.toString(), signaturePart.toString());
const auto verifyExpectFail = authz::jwt::verifyToken(StringRef(tamperedTokenString), privateKey.toPublic());
ASSERT(!verifyExpectFail);
std::tie(verifyOk, verifyErr) = authz::jwt::verifyToken(StringRef(tamperedTokenString), publicKey);
ASSERT(!verifyErr.present());
ASSERT(!verifyOk);
}
printf("%d runs OK\n", numIters);
return Void();

View File

@ -0,0 +1,64 @@
/*
* Base64Decode.h
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef BASE64_DECODE_H
#define BASE64_DECODE_H
#include <cstdint>
#include <utility>
#include "flow/Arena.h"
namespace base64 {
// libb64 (https://github.com/libb64/libb64) adapted to support both base64 and URL-encoded base64
// URL-encoded base64 differs from the regular base64 in following aspects:
// - Replaces '+' with '-' and '/' with '_'
// - No '=' padding at the end
// NOTE: Unlike libb64, this implementation does NOT line wrap base64-encoded output every 72 chars,
// URL-encoded or otherwise. Also, every encoding/decoding is one-off: i.e. no streaming.
// Decodes base64-encoded input and returns the length of produced plaintext data if input is valid, -1 otherwise.
int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept;
// Assuming correctly encoded base64 code length, get the decoded length
int decodedLength(int codeLength) noexcept;
// Assuming a correct base64 string input, return the decoded plaintext. Returns an empty Optional if invalid.
// Note: even if decoding fails by bad encoding, StringRef memory still stays allocated from arena
Optional<StringRef> decode(Arena& arena, StringRef input);
namespace url {
// Decodes URL-encoded base64 input and returns the length of produced plaintext data if input is valid, -1 otherwise.
int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept;
// Assuming correctly URL-encoded base64 code length, get the decoded length
// Returns -1 for invalid length (4n-3)
int decodedLength(int codeLength) noexcept;
// Assuming a correct URL-encoded base64 string input, return the decoded plaintext. Returns an empty Optional if
// invalid. Note: even if decoding fails by bad encoding, StringRef memory still stays allocated from arena
Optional<StringRef> decode(Arena& arena, StringRef input);
} // namespace url
} // namespace base64
#endif /* BASE64_DECODE_H */

View File

@ -0,0 +1,60 @@
/*
* Base64Encode.h
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef BASE64_ENCODE_H
#define BASE64_ENCODE_H
#include <cstdint>
#include "flow/Arena.h"
namespace base64 {
// libb64 (https://github.com/libb64/libb64) adapted to support both base64 and URL-encoded base64
// URL-encoded base64 differs from the regular base64 in following aspects:
// - Replaces '+' with '-' and '/' with '_'
// - No '=' padding at the end
// NOTE: Unlike libb64, this implementation does NOT line wrap base64-encoded output every 72 chars,
// URL-encoded or otherwise. Also, every encoding/decoding is one-off: i.e. no streaming.
// Encodes plaintext into base64 string and returns the length of encoded output in bytes
int encode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept;
// Returns the number of bytes required to store the data of given length in base64 encoding.
int encodedLength(int dataLength) noexcept;
// Encodes passed plaintext into memory allocated from arena
StringRef encode(Arena& arena, StringRef plainText);
namespace url {
// Encodes plaintext into URL-encoded base64 string and returns the length of encoded output in bytes
int encode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept;
// Returns the number of bytes required to store the data of given length in URL-encoded base64
int encodedLength(int dataLength) noexcept;
// encode passed string into memory allocated from arena
StringRef encode(Arena& arena, StringRef plainText);
} // namespace url
} // namespace base64
#endif /* BASE64_ENCODE_H */

View File

@ -1,50 +0,0 @@
/*
* Base64UrlDecode.h
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef BASE64_URLDECODE_H
#define BASE64_URLDECODE_H
#include <cstdint>
#include <utility>
#include "flow/Arena.h"
namespace base64url {
// libb64 (https://github.com/libb64/libb64) adapted for decoding url-encoded base64.
// Key differences from libb64's base64 decoding functions:
// 1. Replace '+' with '-' and '/' with '_'
// 2. Expect no '=' padding at the end
// 3. Illegal sequence or character leads to return code -1, not silently skipped.
// 4. One-off decode: assumes one continuous string, no blocks.
// Returns the length of produced plaintext data if code is valid, -1 otherwise.
int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __restrict plaintextOut) noexcept;
// Assuming correctly url-encoded base64, get the decoded length
// Returns -1 for invalid length (4n-3)
int decodedLength(int codeLength) noexcept;
// return, if base64UrlStr is valid, a StringRef containing a valid decoded string
// Note: even if decoding fails by bad encoding, StringRef memory still stays allocated from arena
Optional<StringRef> decode(Arena& arena, StringRef base64UrlStr);
} // namespace base64url
#endif /* BASE64_URLDECODE_H */

View File

@ -1,46 +0,0 @@
/*
* Base64UrlEncode.h
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef BASE64_URLENCODE_H
#define BASE64_URLENCODE_H
#include <cstdint>
#include "flow/Arena.h"
namespace base64url {
// libb64 (https://github.com/libb64/libb64) adapted for url-encoded base64
// Key differences from libb64's base64 encoding functions:
// 1. Replace '+' with '-' and '/' with '_'
// 2. No '=' padding at the end
// 3. No line wrap every 72 chars
// 4. One-off encode: assumes one continuous string, no streaming.
int encode(const uint8_t* __restrict plaintextIn, int lengthIn, uint8_t* __restrict codeOut) noexcept;
// Return the number of bytes required to store the data of given length in b64url encoding.
int encodedLength(int dataLength) noexcept;
// encode passed string into memory allocated from arena
StringRef encode(Arena& arena, StringRef plainText);
} // namespace base64url
#endif /* BASE64_URLENCODE_H */

View File

@ -76,9 +76,9 @@ bool verifyToken(SignedTokenRef signedToken, PublicKey publicKey);
namespace authz::jwt {
// Given T = concat(B64UrlEnc(headerJson), ".", B64UrlEnc(payloadJson)),
// JWT is concat(T, ".", B64UrlEnc(sign(T, PrivateKey))).
// Below we refer to T as "token part"
// Given S = concat(B64UrlEnc(headerJson), ".", B64UrlEnc(payloadJson)),
// JWT is concat(S, ".", B64UrlEnc(sign(S, PrivateKey))).
// Below we refer to S as "sign input"
// This struct is not meant to be flatbuffer-serialized
// This is a parsed, flattened view of T and signature
@ -102,37 +102,51 @@ struct TokenRef {
StringRef toStringRef(Arena& arena);
};
// Make plain JSON token string with fields (except signature) from passed spec
StringRef makeTokenPart(Arena& arena, TokenRef tokenSpec);
StringRef makeSignInput(Arena& arena, const TokenRef& tokenSpec);
// Generate plaintext signature of token part
StringRef makePlainSignature(Arena& arena, Algorithm alg, StringRef tokenPart, PrivateKey privateKey);
// Sign the passed sign input
StringRef signToken(Arena& arena, StringRef signInput, Algorithm algorithm, PrivateKey privateKey);
// One-stop function to make JWT from spec
StringRef signToken(Arena& arena, TokenRef tokenSpec, PrivateKey privateKey);
StringRef signToken(Arena& arena, const TokenRef& tokenSpec, PrivateKey privateKey);
// Parse passed b64url-encoded header part and materialize its contents into tokenOut,
// using memory allocated from arena
bool parseHeaderPart(Arena& arena, TokenRef& tokenOut, StringRef b64urlHeaderIn);
// Returns a non-empty optional containing an error message if parsing failed
Optional<StringRef> parseHeaderPart(Arena& arena, TokenRef& tokenOut, StringRef b64urlHeaderIn);
// Parse passed b64url-encoded payload part and materialize its contents into tokenOut,
// using memory allocated from arena
bool parsePayloadPart(Arena& arena, TokenRef& tokenOut, StringRef b64urlPayloadIn);
// Returns a non-empty optional containing an error message if parsing failed
Optional<StringRef> parsePayloadPart(Arena& arena, TokenRef& tokenOut, StringRef b64urlPayloadIn);
// Parse passed b64url-encoded signature part and materialize its contents into tokenOut,
// using memory allocated from arena
bool parseSignaturePart(Arena& arena, TokenRef& tokenOut, StringRef b64urlSignatureIn);
// Returns a non-empty optional containing an error message if parsing failed
Optional<StringRef> parseSignaturePart(Arena& arena, TokenRef& tokenOut, StringRef b64urlSignatureIn);
// Returns the base64 encoded signature of the token
StringRef signaturePart(StringRef token);
// Parse passed token string and materialize its contents into tokenOut,
// Parses passed token string and materialize its contents into tokenOut,
// using memory allocated from arena
// Return whether the signed token string is well-formed
bool parseToken(Arena& arena, TokenRef& tokenOut, StringRef signedTokenIn);
// Returns a non-empty optional containing an error message if parsing failed
Optional<StringRef> parseToken(Arena& arena,
StringRef signedTokenIn,
TokenRef& parsedTokenOut,
StringRef& signInputOut);
// Verify only the signature part of signed token string against its token part, not its content
bool verifyToken(StringRef signedToken, PublicKey publicKey);
// Using the parsed token metadata and sign input, verify that the signature from parsedToken
// is a result of signing sign input with a private key that matches the provided public key
// Returns a tuple containing signature verification result,
// and an optional containing verification error message if any occurred.
// If the latter value is non-empty, the former value should not be used.
// NOTE: This is more efficient than the other overload, as it re-uses
// the parsed and base64url-decoded token metadata and signature from parseToken() step
std::pair<bool, Optional<StringRef>> verifyToken(StringRef signInput, const TokenRef& parsedToken, PublicKey publicKey);
// Verifies only the signature part of signed token string against its token part, not its content
// Returns a tuple containing signature verification result,
// and an optional containing verification error message if any occurred.
// If the latter value is non-empty, the former value should not be used.
std::pair<bool, Optional<StringRef>> verifyToken(StringRef signedToken, PublicKey publicKey);
} // namespace authz::jwt

View File

@ -532,6 +532,9 @@ public:
return 0;
}
// generate authz token for use in simulation environment
Standalone<StringRef> makeToken(StringRef tenantName, uint64_t ttlSecondsFromNow);
static thread_local ProcessInfo* currentProcess;
bool checkInjectedCorruption() {

View File

@ -141,6 +141,26 @@ void ISimulator::displayWorkers() const {
return;
}
Standalone<StringRef> ISimulator::makeToken(StringRef tenantName, uint64_t ttlSecondsFromNow) {
ASSERT_GT(authKeys.size(), 0);
auto tokenSpec = authz::jwt::TokenRef{};
auto [keyName, key] = *authKeys.begin();
tokenSpec.algorithm = key.algorithm() == PKeyAlgorithm::EC ? authz::Algorithm::ES256 : authz::Algorithm::RS256;
tokenSpec.keyId = keyName;
tokenSpec.issuer = "sim2_issuer"_sr;
tokenSpec.subject = "sim2_testing"_sr;
auto const now = static_cast<uint64_t>(g_network->timer());
tokenSpec.notBeforeUnixTime = now - 1;
tokenSpec.issuedAtUnixTime = now;
tokenSpec.expiresAtUnixTime = now + ttlSecondsFromNow;
auto const tokenId = deterministicRandom()->randomAlphaNumeric(10);
tokenSpec.tokenId = StringRef(tokenId);
tokenSpec.tenants = VectorRef<StringRef>(&tenantName, 1);
auto ret = Standalone<StringRef>();
ret.contents() = authz::jwt::signToken(ret.arena(), tokenSpec, key);
return ret;
}
int openCount = 0;
struct SimClogging {

View File

@ -39,8 +39,7 @@ template <>
struct CycleMembers<true> {
Arena arena;
TenantName tenant;
authz::jwt::TokenRef token;
StringRef signedToken;
Standalone<StringRef> signedToken;
bool useToken;
};
@ -68,21 +67,10 @@ struct CycleWorkload : TestWorkload, CycleMembers<MultiTenancy> {
if constexpr (MultiTenancy) {
ASSERT(g_network->isSimulated());
this->useToken = getOption(options, "useToken"_sr, true);
auto k = g_simulator->authKeys.begin();
this->tenant = getOption(options, "tenant"_sr, "CycleTenant"_sr);
// make it comfortably longer than the timeout of the workload
auto currentTime = uint64_t(lround(g_network->timer()));
this->token.algorithm = authz::Algorithm::ES256;
this->token.issuedAtUnixTime = currentTime;
this->token.expiresAtUnixTime =
currentTime + uint64_t(std::lround(getCheckTimeout())) + uint64_t(std::lround(testDuration)) + 100;
this->token.keyId = k->first;
this->token.notBeforeUnixTime = currentTime - 10;
VectorRef<StringRef> tenants;
tenants.push_back_deep(this->arena, this->tenant);
this->token.tenants = tenants;
// we currently don't support this workload to be run outside of simulation
this->signedToken = authz::jwt::signToken(this->arena, this->token, k->second);
this->signedToken = g_simulator->makeToken(
this->tenant, uint64_t(std::lround(getCheckTimeout())) + uint64_t(std::lround(testDuration)) + 100);
}
}

View File

@ -639,6 +639,16 @@ public:
return eatAny(StringRef((const uint8_t*)sep, strlen(sep)), foundSeparator);
}
uint8_t back() {
UNSTOPPABLE_ASSERT(!empty());
return data[length - 1];
}
void popBack() {
UNSTOPPABLE_ASSERT(!empty());
--length;
}
// Copies string contents to dst and returns a pointer to the next byte after
uint8_t* copyTo(uint8_t* dst) const {
if (length > 0) {

View File

@ -1,3 +1,4 @@
import base64
import fdb
import json
import random
@ -68,7 +69,7 @@ def token_claim_1h(tenant_name):
"nbf": now - 1,
"exp": now + 60 * 60,
"jti": random_alphanum_str(10),
"tenants": [to_str(tenant_name)],
"tenants": [to_str(base64.b64encode(tenant_name))],
}
# repeat try-wait loop up to max_repeat times until both read and write tr fails for tenant with permission_denied