Isolate TokenSpec such that it doesn't depend on flow headers

This commit is contained in:
Junhyun Shim 2022-11-23 14:17:23 +01:00
parent b28cb708a2
commit 03f0696a4b
11 changed files with 324 additions and 209 deletions

View File

@ -139,15 +139,6 @@ namespace authz {
using MessageDigestMethod = const EVP_MD*;
Algorithm algorithmFromString(StringRef s) noexcept {
if (s == "RS256"_sr)
return Algorithm::RS256;
else if (s == "ES256"_sr)
return Algorithm::ES256;
else
return Algorithm::UNKNOWN;
}
std::pair<PKeyAlgorithm, MessageDigestMethod> getMethod(Algorithm alg) {
if (alg == Algorithm::RS256) {
return { PKeyAlgorithm::RSA, ::EVP_sha256() };
@ -169,36 +160,6 @@ std::string_view getAlgorithmName(Algorithm alg) {
} // namespace authz
namespace authz::flatbuffers {
SignedTokenRef signToken(Arena& arena, TokenRef token, StringRef keyName, PrivateKey privateKey) {
auto ret = SignedTokenRef{};
auto writer = ObjectWriter([&arena](size_t len) { return new (arena) uint8_t[len]; }, IncludeVersion());
writer.serialize(token);
auto tokenStr = writer.toStringRef();
auto sig = privateKey.sign(arena, tokenStr, *::EVP_sha256());
ret.token = tokenStr;
ret.signature = sig;
ret.keyName = StringRef(arena, keyName);
return ret;
}
bool verifyToken(SignedTokenRef signedToken, PublicKey publicKey) {
return publicKey.verify(signedToken.token, signedToken.signature, *::EVP_sha256());
}
TokenRef makeRandomTokenSpec(Arena& arena, IRandom& rng) {
auto token = TokenRef{};
token.expiresAt = timer_monotonic() * (0.5 + rng.random01());
const auto numTenants = rng.randomInt(1, 3);
for (auto i = 0; i < numTenants; i++) {
token.tenants.push_back(arena, genRandomAlphanumStringRef(arena, rng, MinTenantNameLen, MaxTenantNameLenPlus1));
}
return token;
}
} // namespace authz::flatbuffers
namespace authz::jwt {
template <class FieldType, size_t NameLen>
@ -378,7 +339,7 @@ Optional<StringRef> parseHeaderPart(Arena& arena, TokenRef& token, StringRef b64
if (typValue != "JWT"_sr)
return "'typ' is not 'JWT'"_sr;
auto algValue = StringRef(reinterpret_cast<const uint8_t*>(alg.GetString()), alg.GetStringLength());
auto algType = algorithmFromString(algValue);
auto algType = algorithmFromString(algValue.toStringView());
if (algType == Algorithm::UNKNOWN)
return "Unsupported algorithm"_sr;
token.algorithm = algType;
@ -582,29 +543,6 @@ TokenRef makeRandomTokenSpec(Arena& arena, IRandom& rng, Algorithm alg) {
void forceLinkTokenSignTests() {}
TEST_CASE("/fdbrpc/TokenSign/FlatBuffer") {
const auto numIters = 100;
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);
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();
ASSERT(!authz::flatbuffers::verifyToken(signedToken, publicKey));
}
printf("%d runs OK\n", numIters);
return Void();
}
TEST_CASE("/fdbrpc/TokenSign/JWT") {
const auto numIters = 100;
for (auto i = 0; i < numIters; i++) {
@ -685,73 +623,42 @@ TEST_CASE("/fdbrpc/TokenSign/JWT/ToStringRef") {
return Void();
}
// This unit test takes too long to run in RandomUnitTests.toml
// FIXME: Move this to benchmark to flowbench
/*
TEST_CASE("/fdbrpc/TokenSign/bench") {
auto keyTypes = std::array<StringRef, 2>{ "EC"_sr, "RSA"_sr };
for (auto kty : keyTypes) {
constexpr auto repeat = 5;
constexpr auto numSamples = 10000;
fmt::print("=== {} keys case\n", kty.toString());
auto key = kty == "EC"_sr ? mkcert::makeEcP256() : mkcert::makeRsa4096Bit();
auto pubKey = key.toPublic();
auto& rng = *deterministicRandom();
auto arena = Arena();
auto jwtSpecs = new (arena) authz::jwt::TokenRef[numSamples];
auto fbSpecs = new (arena) authz::flatbuffers::TokenRef[numSamples];
auto jwts = new (arena) StringRef[numSamples];
auto fbs = new (arena) StringRef[numSamples];
for (auto i = 0; i < numSamples; i++) {
jwtSpecs[i] = authz::jwt::makeRandomTokenSpec(
arena, rng, kty == "EC"_sr ? authz::Algorithm::ES256 : authz::Algorithm::RS256);
fbSpecs[i] = authz::flatbuffers::makeRandomTokenSpec(arena, rng);
}
{
auto const jwtSignBegin = timer_monotonic();
for (auto i = 0; i < numSamples; i++) {
jwts[i] = authz::jwt::signToken(arena, jwtSpecs[i], key);
}
auto const jwtSignEnd = timer_monotonic();
fmt::print("JWT Sign : {:.2f} OPS\n", numSamples / (jwtSignEnd - jwtSignBegin));
}
{
auto const jwtVerifyBegin = timer_monotonic();
for (auto rep = 0; rep < repeat; rep++) {
for (auto i = 0; i < numSamples; i++) {
auto verifyOk = authz::jwt::verifyToken(jwts[i], pubKey);
ASSERT(verifyOk);
}
}
auto const jwtVerifyEnd = timer_monotonic();
fmt::print("JWT Verify : {:.2f} OPS\n", repeat * numSamples / (jwtVerifyEnd - jwtVerifyBegin));
}
{
auto tmpArena = Arena();
auto const fbSignBegin = timer_monotonic();
for (auto i = 0; i < numSamples; i++) {
auto fbToken = authz::flatbuffers::signToken(tmpArena, fbSpecs[i], "defaultKey"_sr, key);
auto wr = ObjectWriter([&arena](size_t len) { return new (arena) uint8_t[len]; }, Unversioned());
wr.serialize(fbToken);
fbs[i] = wr.toStringRef();
}
auto const fbSignEnd = timer_monotonic();
fmt::print("FlatBuffers Sign : {:.2f} OPS\n", numSamples / (fbSignEnd - fbSignBegin));
}
{
auto const fbVerifyBegin = timer_monotonic();
for (auto rep = 0; rep < repeat; rep++) {
for (auto i = 0; i < numSamples; i++) {
auto signedToken = ObjectReader::fromStringRef<Standalone<authz::flatbuffers::SignedTokenRef>>(
fbs[i], Unversioned());
auto verifyOk = authz::flatbuffers::verifyToken(signedToken, pubKey);
ASSERT(verifyOk);
}
}
auto const fbVerifyEnd = timer_monotonic();
fmt::print("FlatBuffers Verify : {:.2f} OPS\n", repeat * numSamples / (fbVerifyEnd - fbVerifyBegin));
}
}
return Void();
auto keyTypes = std::array<StringRef, 1>{ "EC"_sr };
for (auto kty : keyTypes) {
constexpr auto repeat = 5;
constexpr auto numSamples = 10000;
fmt::print("=== {} keys case\n", kty.toString());
auto key = kty == "EC"_sr ? mkcert::makeEcP256() : mkcert::makeRsa4096Bit();
auto pubKey = key.toPublic();
auto& rng = *deterministicRandom();
auto arena = Arena();
auto jwtSpecs = new (arena) authz::jwt::TokenRef[numSamples];
auto jwts = new (arena) StringRef[numSamples];
for (auto i = 0; i < numSamples; i++) {
jwtSpecs[i] = authz::jwt::makeRandomTokenSpec(
arena, rng, kty == "EC"_sr ? authz::Algorithm::ES256 : authz::Algorithm::RS256);
}
{
auto const jwtSignBegin = timer_monotonic();
for (auto i = 0; i < numSamples; i++) {
jwts[i] = authz::jwt::signToken(arena, jwtSpecs[i], key);
}
auto const jwtSignEnd = timer_monotonic();
fmt::print("JWT Sign : {:.2f} OPS\n", numSamples / (jwtSignEnd - jwtSignBegin));
}
{
auto const jwtVerifyBegin = timer_monotonic();
for (auto rep = 0; rep < repeat; rep++) {
for (auto i = 0; i < numSamples; i++) {
auto [verifyOk, errorMsg] = authz::jwt::verifyToken(jwts[i], pubKey);
ASSERT(!errorMsg.present());
ASSERT(verifyOk);
}
}
auto const jwtVerifyEnd = timer_monotonic();
fmt::print("JWT Verify : {:.2f} OPS\n", repeat * numSamples / (jwtVerifyEnd - jwtVerifyBegin));
}
}
return Void();
}
*/

View File

@ -18,6 +18,7 @@
* limitations under the License.
*/
#include "fdbrpc/TokenSign.h"
#include "fdbrpc/TokenSignStdTypes.h"
#include "flow/PKey.h"
#include "flow/MkCert.h"

View File

@ -18,94 +18,21 @@
* limitations under the License.
*/
#pragma once
#ifndef FDBRPC_TOKEN_SIGN_H
#define FDBRPC_TOKEN_SIGN_H
#pragma once
#include "flow/network.h"
#include "flow/Arena.h"
#include "flow/FileIdentifier.h"
#include "flow/PKey.h"
#include "fdbrpc/TokenSpec.h"
#include <string>
#include <vector>
namespace authz {
enum class Algorithm : int {
RS256,
ES256,
UNKNOWN,
};
Algorithm algorithmFromString(StringRef s) noexcept;
} // namespace authz
namespace authz::flatbuffers {
struct TokenRef {
static constexpr FileIdentifier file_identifier = 1523118;
double expiresAt;
VectorRef<StringRef> tenants;
template <class Ar>
void serialize(Ar& ar) {
serializer(ar, expiresAt, tenants);
}
};
struct SignedTokenRef {
static constexpr FileIdentifier file_identifier = 5916732;
StringRef token;
StringRef keyName;
StringRef signature;
template <class Ar>
void serialize(Ar& ar) {
serializer(ar, token, keyName, signature);
}
int expectedSize() const { return token.size() + keyName.size() + signature.size(); }
};
SignedTokenRef signToken(Arena& arena, TokenRef token, StringRef keyName, PrivateKey privateKey);
bool verifyToken(SignedTokenRef signedToken, PublicKey publicKey);
} // namespace authz::flatbuffers
namespace authz::jwt {
// 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 S and signature
template <bool IsArenaBased>
struct BasicTokenSpec {
using StringType = std::conditional_t<IsArenaBased, StringRef, std::string>;
template <class T>
using VectorType = std::conditional_t<IsArenaBased, VectorRef<T>, std::vector<T>>;
template <class T>
using OptionalType = std::conditional_t<IsArenaBased, Optional<T>, std::optional<T>>;
// header part ("typ": "JWT" implicitly enforced)
Algorithm algorithm; // alg
StringType keyId; // kid
// payload part
OptionalType<StringType> issuer; // iss
OptionalType<StringType> subject; // sub
OptionalType<VectorType<StringType>> audience; // aud
OptionalType<uint64_t> issuedAtUnixTime; // iat
OptionalType<uint64_t> expiresAtUnixTime; // exp
OptionalType<uint64_t> notBeforeUnixTime; // nbf
OptionalType<StringType> tokenId; // jti
OptionalType<VectorType<StringType>> tenants; // tenants
// signature part
StringType signature;
};
using TokenRef = BasicTokenSpec<true>;
using TokenRef = BasicTokenSpec<StringRef, VectorRef, Optional>;
// print each non-signature field in non-JSON, human-readable format e.g. for trace
StringRef toStringRef(Arena& arena, const TokenRef& tokenSpec);

View File

@ -20,8 +20,9 @@
#pragma once
#ifndef FDBRPC_TOKEN_SIGN_STD_TYPES_H
#define FDBRPC_TOKEN_SIGN_STD_TYPES_H
#include "fdbrpc/TokenSign.h"
#include "fdbrpc/TokenSpec.h"
#include <string>
#include <vector>
// Below functions build as a library separate from fdbrpc
// The intent is to re-use the key/token generation part in a way that the input, the output,
@ -30,7 +31,7 @@
namespace authz::jwt::stdtypes {
using TokenSpec = BasicTokenSpec<false /*IsArenaBased*/>;
using TokenSpec = BasicTokenSpec<std::string, std::vector>;
// Generate an elliptic curve private key on a P-256 curve, and serialize it as PEM.
std::string makeEcP256PrivateKeyPem();

View File

@ -0,0 +1,77 @@
/*
* TokenSpec.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 FDBRPC_TOKEN_SPEC_H
#define FDBRPC_TOKEN_SPEC_H
#pragma once
#include <cstdint>
#include <optional>
#include <string_view>
namespace authz {
enum class Algorithm : int {
RS256,
ES256,
UNKNOWN,
};
inline Algorithm algorithmFromString(std::string_view s) noexcept {
if (s == "RS256")
return Algorithm::RS256;
else if (s == "ES256")
return Algorithm::ES256;
else
return Algorithm::UNKNOWN;
}
} // namespace authz
namespace authz::jwt {
// 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 S and signature
template <class StringType, template <class> class VectorType, template <class> class OptionalType = std::optional>
struct BasicTokenSpec {
// header part ("typ": "JWT" implicitly enforced)
Algorithm algorithm; // alg
StringType keyId; // kid
// payload part
OptionalType<StringType> issuer; // iss
OptionalType<StringType> subject; // sub
OptionalType<VectorType<StringType>> audience; // aud
OptionalType<uint64_t> issuedAtUnixTime; // iat
OptionalType<uint64_t> expiresAtUnixTime; // exp
OptionalType<uint64_t> notBeforeUnixTime; // nbf
OptionalType<StringType> tokenId; // jti
OptionalType<VectorType<StringType>> tenants; // tenants
// signature part
StringType signature;
};
} // namespace authz::jwt
#endif /*FDBRPC_TOKEN_SPEC_H*/

View File

@ -45,6 +45,7 @@ void forceLinkCompressionUtilsTest();
void forceLinkAtomicTests();
void forceLinkIdempotencyIdTests();
void forceLinkBlobConnectionProviderTests();
void forceLinkArenaStringTests();
struct UnitTestWorkload : TestWorkload {
static constexpr auto NAME = "UnitTests";
@ -106,6 +107,7 @@ struct UnitTestWorkload : TestWorkload {
forceLinkAtomicTests();
forceLinkIdempotencyIdTests();
forceLinkBlobConnectionProviderTests();
forceLinkArenaStringTests();
}
Future<Void> setup(Database const& cx) override {

49
flow/ArenaString.cpp Normal file
View File

@ -0,0 +1,49 @@
#include "flow/UnitTest.h"
#include "flow/ArenaAllocator.h"
#include "flow/ArenaString.h"
TEST_CASE("/flow/ArenaString") {
Arena arena;
ArenaAllocator<char> alloc(arena);
{
ArenaString s("1", alloc);
auto shortStrBuf = s.data();
s.assign(100, '1');
auto longStrBuf = s.data();
ASSERT_NE(shortStrBuf, longStrBuf);
ArenaString t = s;
auto copiedStrBuf = t.data();
ASSERT_NE(copiedStrBuf, longStrBuf);
}
{
ArenaString s(alloc);
s.assign(100, 'a');
ArenaString t(100, 'a', alloc);
ASSERT(s == t);
}
{
// Default construction of string does not specify an allocator, and Arena by extension.
// Any modification that requires allocation will throw bad_allocator() when assigning beyond
// short-string-optimized length.
ArenaString s;
bool hit = false;
try {
s.assign(100, 'a');
} catch (Error& e) {
hit = true;
ASSERT_EQ(e.code(), error_code_bad_allocator);
}
ASSERT(hit);
}
{
// string_view may be used to bridge strings with different allocators
ArenaString s(100, 'a', alloc);
std::string_view sv(s);
std::string s2(sv);
std::string_view sv2(s2);
ASSERT(sv == sv2);
}
return Void();
}
void forceLinkArenaStringTests() {}

View File

@ -0,0 +1,90 @@
/*
* ArenaAllocator.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 FLOW_ARENA_ALLOCATOR_H
#define FLOW_ARENA_ALLOCATOR_H
#pragma once
#include "flow/Arena.h"
#include "flow/Error.h"
#include "flow/FastRef.h"
#include <functional>
#include <type_traits>
#include <variant>
template <class T>
class ArenaAllocator {
Arena* arenaPtr;
Arena& arena() noexcept { return *arenaPtr; }
public:
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using void_pointer = void*;
using const_void_pointer = const void*;
using self_type = ArenaAllocator<T>;
using size_type = size_t;
using value_type = T;
using difference_type = typename std::pointer_traits<pointer>::difference_type;
// Unfortunately this needs to exist due to STL's internal use of Allocator() in internal coding
ArenaAllocator() noexcept : arenaPtr(nullptr) {}
ArenaAllocator(Arena& arena) noexcept : arenaPtr(&arena) {}
ArenaAllocator(const self_type& other) noexcept = default;
// Rebind constructor does not modify
template <class U>
ArenaAllocator(const ArenaAllocator<U>& other) noexcept : arenaPtr(other.arenaPtr) {}
ArenaAllocator& operator=(const self_type& other) noexcept = default;
ArenaAllocator(self_type&& other) noexcept = default;
ArenaAllocator& operator=(self_type&& other) noexcept = default;
T* allocate(size_t n) {
if (!arenaPtr)
throw bad_allocator();
return new (arena()) T[n];
}
void deallocate(T*, size_t) noexcept {}
bool operator==(const self_type& other) const noexcept { return arenaPtr == other.arenaPtr; }
bool operator!=(const self_type& other) const noexcept { return !(*this == other); }
template <class U>
struct rebind {
using other = ArenaAllocator<U>;
};
using is_always_equal = std::false_type;
using propagate_on_container_copy_assignment = std::true_type;
using propagate_on_container_move_assignment = std::true_type;
using propagate_on_container_swap = std::true_type;
};
#endif /*FLOW_ARENA_ALLOCATOR_H*/

View File

@ -0,0 +1,30 @@
/*
* ArenaString.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 FLOW_ARENA_STRING_H
#define FLOW_ARENA_STRING_H
#pragma once
#include "flow/ArenaAllocator.h"
#include "flow/CustomAllocatorString.h"
using ArenaString = CustomAllocatorString<ArenaAllocator>;
#endif /*FLOW_ARENA_STRING_H*/

View File

@ -0,0 +1,30 @@
/*
* CustomAllocatorString.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 FLOW_CUSTOM_ALLOCATOR_STRING_H
#define FLOW_CUSTOM_ALLOCATOR_STRING_H
#pragma once
#include <string>
template <template <class> class Allocator>
using CustomAllocatorString = std::basic_string<char, std::char_traits<char>, Allocator<char>>;
#endif /*FLOW_CUSTOM_ALLOCATOR_STRING_H*/

View File

@ -140,6 +140,7 @@ ERROR( storage_quota_exceeded, 1225, "Exceeded the maximum storage quota allocat
ERROR( platform_error, 1500, "Platform error" )
ERROR( large_alloc_failed, 1501, "Large block allocation failed" )
ERROR( performance_counter_error, 1502, "QueryPerformanceCounter error" )
ERROR( bad_allocator, 1503, "Null allocator was used to allocate memory" )
ERROR( io_error, 1510, "Disk i/o operation failed" )
ERROR( file_not_found, 1511, "File not found" )