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:
parent
0091abd02b
commit
50f4021cf7
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 */
|
|
@ -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 */
|
|
@ -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 */
|
|
@ -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 */
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue