Merge branch 'main' into feature-metacluster

# Conflicts:
#	bindings/java/src/main/com/apple/foundationdb/TenantManagement.java
#	fdbcli/TenantCommands.actor.cpp
#	fdbclient/NativeAPI.actor.cpp
#	fdbclient/TenantSpecialKeys.actor.cpp
#	fdbclient/include/fdbclient/KeyBackedTypes.h
#	fdbclient/include/fdbclient/RunTransaction.actor.h
#	fdbclient/include/fdbclient/SpecialKeySpace.actor.h
#	fdbserver/workloads/TenantManagementWorkload.actor.cpp
This commit is contained in:
A.J. Beamon 2022-07-06 15:44:21 -07:00
parent f8c518cab3
commit c9b553663e
34 changed files with 1965 additions and 568 deletions

View File

@ -30,7 +30,16 @@ public:
ApiCorrectnessWorkload(const WorkloadConfig& config) : ApiWorkload(config) {}
private:
enum OpType { OP_INSERT, OP_GET, OP_CLEAR, OP_GET_RANGE, OP_CLEAR_RANGE, OP_COMMIT_READ, OP_LAST = OP_COMMIT_READ };
enum OpType {
OP_INSERT,
OP_GET,
OP_GET_KEY,
OP_CLEAR,
OP_GET_RANGE,
OP_CLEAR_RANGE,
OP_COMMIT_READ,
OP_LAST = OP_COMMIT_READ
};
void randomCommitReadOp(TTaskFct cont) {
int numKeys = Random::get().randomInt(1, maxKeysPerTransaction);
@ -125,6 +134,70 @@ private:
});
}
void randomGetKeyOp(TTaskFct cont) {
int numKeys = Random::get().randomInt(1, maxKeysPerTransaction);
auto keysWithSelectors = std::make_shared<std::vector<std::pair<fdb::Key, fdb::KeySelector>>>();
auto results = std::make_shared<std::vector<fdb::Key>>();
keysWithSelectors->reserve(numKeys);
for (int i = 0; i < numKeys; i++) {
auto key = randomKey(readExistingKeysRatio);
fdb::KeySelector selector;
selector.keyLength = key.size();
selector.orEqual = Random::get().randomBool(0.5);
selector.offset = Random::get().randomInt(0, 4);
keysWithSelectors->emplace_back(std::move(key), std::move(selector));
// We would ideally do the following above:
// selector.key = key.data();
// but key.data() may become invalid after the key is moved to the vector.
// So instead, we update the pointer here to the string already in the vector.
keysWithSelectors->back().second.key = keysWithSelectors->back().first.data();
}
execTransaction(
[keysWithSelectors, results](auto ctx) {
auto futures = std::make_shared<std::vector<fdb::Future>>();
for (const auto& keyWithSelector : *keysWithSelectors) {
auto key = keyWithSelector.first;
auto selector = keyWithSelector.second;
futures->push_back(ctx->tx().getKey(selector, false));
}
ctx->continueAfterAll(*futures, [ctx, futures, results]() {
results->clear();
for (auto& f : *futures) {
results->push_back(fdb::Key(f.get<fdb::future_var::KeyRef>()));
}
ASSERT(results->size() == futures->size());
ctx->done();
});
},
[this, keysWithSelectors, results, cont]() {
ASSERT(results->size() == keysWithSelectors->size());
for (int i = 0; i < keysWithSelectors->size(); i++) {
auto const& key = (*keysWithSelectors)[i].first;
auto const& selector = (*keysWithSelectors)[i].second;
auto expected = store.getKey(key, selector.orEqual, selector.offset);
auto actual = (*results)[i];
// Local store only contains data for the current client, while fdb contains data from multiple
// clients. If getKey returned a key outside of the range for the current client, adjust the result
// to match what would be expected in the local store.
if (actual.substr(0, keyPrefix.size()) < keyPrefix) {
actual = store.startKey();
} else if ((*results)[i].substr(0, keyPrefix.size()) > keyPrefix) {
actual = store.endKey();
}
if (actual != expected) {
error(fmt::format("randomGetKeyOp mismatch. key: {}, orEqual: {}, offset: {}, expected: {} "
"actual: {}",
fdb::toCharsRef(key),
selector.orEqual,
selector.offset,
fdb::toCharsRef(expected),
fdb::toCharsRef(actual)));
}
}
schedule(cont);
});
}
void getRangeLoop(std::shared_ptr<ITransactionContext> ctx,
fdb::KeySelector begin,
fdb::KeySelector end,
@ -199,6 +272,9 @@ private:
case OP_GET:
randomGetOp(cont);
break;
case OP_GET_KEY:
randomGetKeyOp(cont);
break;
case OP_CLEAR:
randomClearOp(cont);
break;

View File

@ -197,16 +197,6 @@ inline int maxApiVersion() {
return native::fdb_get_max_api_version();
}
inline Error selectApiVersionNothrow(int version) {
return Error(native::fdb_select_api_version(version));
}
inline void selectApiVersion(int version) {
if (auto err = selectApiVersionNothrow(version)) {
throwError(fmt::format("ERROR: fdb_select_api_version({}): ", version), err);
}
}
namespace network {
inline Error setOptionNothrow(FDBNetworkOption option, BytesRef str) noexcept {
@ -595,14 +585,15 @@ class Tenant final {
friend class Database;
std::shared_ptr<native::FDBTenant> tenant;
static constexpr CharsRef tenantManagementMapPrefix = "\xff\xff/management/tenant_map/";
explicit Tenant(native::FDBTenant* tenant_raw) {
if (tenant_raw)
tenant = std::shared_ptr<native::FDBTenant>(tenant_raw, &native::fdb_tenant_destroy);
}
public:
// This should only be mutated by API versioning
static inline CharsRef tenantManagementMapPrefix = "\xff\xff/management/tenant/map/";
Tenant(const Tenant&) noexcept = default;
Tenant& operator=(const Tenant&) noexcept = default;
Tenant() noexcept : tenant(nullptr) {}
@ -691,6 +682,19 @@ public:
}
};
inline Error selectApiVersionNothrow(int version) {
if (version < 720) {
Tenant::tenantManagementMapPrefix = "\xff\xff/management/tenant_map/";
}
return Error(native::fdb_select_api_version(version));
}
inline void selectApiVersion(int version) {
if (auto err = selectApiVersionNothrow(version)) {
throwError(fmt::format("ERROR: fdb_select_api_version({}): ", version), err);
}
}
} // namespace fdb
template <>

View File

@ -27,6 +27,8 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import com.apple.foundationdb.tuple.ByteArrayUtil;
/**
* The starting point for accessing FoundationDB.
* <br>
@ -189,6 +191,11 @@ public class FDB {
Select_API_version(version);
singleton = new FDB(version);
if (version < 720) {
TenantManagement.TENANT_MAP_PREFIX = ByteArrayUtil.join(new byte[] { (byte)255, (byte)255 },
"/management/tenant_map/".getBytes());
}
return singleton;
}

View File

@ -40,8 +40,8 @@ import com.apple.foundationdb.tuple.Tuple;
* The FoundationDB API includes function to manage the set of tenants in a cluster.
*/
public class TenantManagement {
static final byte[] TENANT_MAP_PREFIX = ByteArrayUtil.join(new byte[] { (byte)255, (byte)255 },
"/management/tenant/map/".getBytes());
static byte[] TENANT_MAP_PREFIX = ByteArrayUtil.join(new byte[] { (byte)255, (byte)255 },
"/management/tenant/map/".getBytes());
/**
* Creates a new tenant in the cluster. If the tenant already exists, this operation will complete

View File

@ -102,6 +102,8 @@ def api_version(ver):
if ver >= 710:
import fdb.tenant_management
if ver < 720:
fdb.tenant_management._tenant_map_prefix = b'\xff\xff/management/tenant_map/'
if ver < 610:
globals()["init"] = getattr(fdb.impl, "init")

View File

@ -204,7 +204,7 @@ that process, and wait for necessary data to be moved away.
#. ``\xff\xff/management/failed_locality/<locality>`` Read/write. Indicates that the cluster should consider matching processes as permanently failed. This allows the cluster to avoid maintaining extra state and doing extra work in the hope that these processes come back. See :ref:`removing machines from a cluster <removing-machines-from-a-cluster>` for documentation for the corresponding fdbcli command.
#. ``\xff\xff/management/options/excluded_locality/force`` Read/write. Setting this key disables safety checks for writes to ``\xff\xff/management/excluded_locality/<locality>``. Setting this key only has an effect in the current transaction and is not persisted on commit.
#. ``\xff\xff/management/options/failed_locality/force`` Read/write. Setting this key disables safety checks for writes to ``\xff\xff/management/failed_locality/<locality>``. Setting this key only has an effect in the current transaction and is not persisted on commit.
#. ``\xff\xff/management/tenant_map/<tenant>`` Read/write. Setting a key in this range to any value will result in a tenant being created with name ``<tenant>``. Clearing a key in this range will delete the tenant with name ``<tenant>``. Reading all or a portion of this range will return the list of tenants currently present in the cluster, excluding any changes in this transaction. Values read in this range will be JSON objects containing the metadata for the associated tenants.
#. ``\xff\xff/management/tenant/map/<tenant>`` Read/write. Setting a key in this range to any value will result in a tenant being created with name ``<tenant>``. Clearing a key in this range will delete the tenant with name ``<tenant>``. Reading all or a portion of this range will return the list of tenants currently present in the cluster, excluding any changes in this transaction. Values read in this range will be JSON objects containing the metadata for the associated tenants.
An exclusion is syntactically either an ip address (e.g. ``127.0.0.1``), or
an ip address and port (e.g. ``127.0.0.1:4500``) or any locality (e.g ``locality_dcid:primary-satellite`` or
@ -270,7 +270,8 @@ Deprecated Keys
Listed below are the special keys that have been deprecated. Special key(s) will no longer be accessible when the client specifies an API version equal to or larger than the version where they were deprecated. Clients specifying older API versions will be able to continue using the deprecated key(s).
#. ``\xff\xff/management/profiling/<client_txn_sample_rate|client_txn_size_limit>`` Deprecated as of API version 7.2. The corresponding functionalities are now covered by the global configuration module. For details, see :doc:`global-configuration`. Read/write. Changing these two keys will change the corresponding system keys ``\xff\x02/fdbClientInfo/<client_txn_sample_rate|client_txn_size_limit>``, respectively. The value of ``\xff\xff/management/client_txn_sample_rate`` is a literal text of ``double``, and the value of ``\xff\xff/management/client_txn_size_limit`` is a literal text of ``int64_t``. A special value ``default`` can be set to or read from these two keys, representing the client profiling is disabled. In addition, ``clear`` in this range is not allowed. For more details, see help text of ``fdbcli`` command ``profile client``.
#. ``\xff\xff/management/profiling/<client_txn_sample_rate|client_txn_size_limit>`` Deprecated as of API version 720. The corresponding functionalities are now covered by the global configuration module. For details, see :doc:`global-configuration`. Read/write. Changing these two keys will change the corresponding system keys ``\xff\x02/fdbClientInfo/<client_txn_sample_rate|client_txn_size_limit>``, respectively. The value of ``\xff\xff/management/client_txn_sample_rate`` is a literal text of ``double``, and the value of ``\xff\xff/management/client_txn_size_limit`` is a literal text of ``int64_t``. A special value ``default`` can be set to or read from these two keys, representing the client profiling is disabled. In addition, ``clear`` in this range is not allowed. For more details, see help text of ``fdbcli`` command ``profile client``.
#. ``\xff\xff/management/tenant_map/<tenant>`` Removed as of API version 720 and renamed to ``\xff\xff/management/tenant/map/<tenant>``.
Versioning
==========

View File

@ -31,7 +31,7 @@ FoundationDB clusters support the following tenant modes:
Creating and deleting tenants
=============================
Tenants can be created and deleted using the ``\xff\xff/management/tenant_map/<tenant_name>`` :doc:`special key <special-keys>` range as well as by using APIs provided in some language bindings.
Tenants can be created and deleted using the ``\xff\xff/management/tenant/map/<tenant_name>`` :doc:`special key <special-keys>` range as well as by using APIs provided in some language bindings.
Tenants can be created with any byte-string name that does not begin with the ``\xff`` character. Once created, a tenant will be assigned an ID and a prefix where its data will reside.

View File

@ -0,0 +1,77 @@
#############
Trace Logging
#############
Overview
========
FoundationDB processes generate log files known as trace logs that include details about the sequence of events that a process performed and record a variety of metrics about that process. These log files are useful for observing the state of a cluster over time and for debugging issues that occur in the cluster, a client, or various tools.
Each file contains a sequence of events for a single process, ordered by time. Clients that use the multi-version or multi-threaded client features will generate :ref:`multiple log files simultaneously <mvc-logging>`. Each entry in the log file will be either an XML event or a JSON object that containts several mandatory fields and zero or more arbitrary fields. For example, the following is an event generated in the XML format::
<Event Severity="10" Time="1579736072.656689" Type="Net2Starting" ID="0000000000000000" Machine="1.1.1.1:4000" LogGroup="default"/>
Most FoundationDB processes generate trace logs in the format described in this document. This includes server and client processes, fdbcli, and backup and DR tooling.
Configuration
=============
File Format
-----------
FoundationDB trace logging
* Parameters
* Format
Trace Files
===========
* Filenames
* Rolling
Mandatory Fields
================
* Severity
* Time
* Type
* Machine
* LogGroup
* ID
Common Fields
=============
* Roles
Event Suppression
-----------------
* SuppressedEventCount
Errors
------
* Error
* ErrorDescription
* ErrorCode
Clients
-------
*
Rolled Events
=============
Counters
========
PeriodicEvents
==============
.. mvc-logging::
Multi-Version and Multi-Threaded Client Logging
===============================================

View File

@ -61,6 +61,7 @@
#include "fdbclient/SpecialKeySpace.actor.h"
#include "fdbclient/StorageServerInterface.h"
#include "fdbclient/SystemData.h"
#include "fdbclient/TenantSpecialKeys.actor.h"
#include "fdbclient/TransactionLineage.h"
#include "fdbclient/versions.h"
#include "fdbrpc/WellKnownEndpoints.h"
@ -1499,13 +1500,13 @@ DatabaseContext::DatabaseContext(Reference<AsyncVar<Reference<IClusterConnection
registerSpecialKeysImpl(
SpecialKeySpace::MODULE::MANAGEMENT,
SpecialKeySpace::IMPLTYPE::READWRITE,
std::make_unique<TenantRangeImpl>(SpecialKeySpace::getManagementApiCommandRange("tenant")));
std::make_unique<TenantRangeImpl<true>>(SpecialKeySpace::getManagementApiCommandRange("tenant")));
}
if (apiVersionAtLeast(710) && !apiVersionAtLeast(720)) {
registerSpecialKeysImpl(
SpecialKeySpace::MODULE::MANAGEMENT,
SpecialKeySpace::IMPLTYPE::READWRITE,
std::make_unique<TenantRangeImpl>(SpecialKeySpace::getManagementApiCommandRange("tenantmap")));
std::make_unique<TenantRangeImpl<false>>(SpecialKeySpace::getManagementApiCommandRange("tenantmap")));
}
if (apiVersionAtLeast(700)) {
registerSpecialKeysImpl(SpecialKeySpace::MODULE::ERRORMSG,

View File

@ -1,334 +0,0 @@
/*
* TenantSpecialKeys.actor.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 "fdbclient/ActorLineageProfiler.h"
#include "fdbclient/FDBOptions.g.h"
#include "fdbclient/Knobs.h"
#include "fdbclient/DatabaseContext.h"
#include "fdbclient/SpecialKeySpace.actor.h"
#include "fdbclient/TenantManagement.actor.h"
#include "flow/Arena.h"
#include "flow/UnitTest.h"
#include "flow/actorcompiler.h" // This must be the last #include.
const KeyRangeRef TenantRangeImpl::submoduleRange = KeyRangeRef("tenant/"_sr, "tenant0"_sr);
TenantRangeImpl::TenantRangeImpl(KeyRangeRef kr) : SpecialKeyRangeRWImpl(kr) {}
const KeyRangeRef TenantRangeImpl::mapSubRange = KeyRangeRef("map/"_sr, "map0"_sr);
const KeyRangeRef TenantRangeImpl::configureSubRange = KeyRangeRef("configure/"_sr, "configure0"_sr);
KeyRangeRef removePrefix(KeyRangeRef range, KeyRef prefix, KeyRef defaultEnd) {
KeyRef begin = range.begin.removePrefix(prefix);
KeyRef end;
if (range.end.startsWith(prefix)) {
end = range.end.removePrefix(TenantRangeImpl::mapSubRange.begin);
} else {
end = defaultEnd;
}
return KeyRangeRef(begin, end);
}
KeyRef withTenantMapPrefix(Database db, KeyRef key, Arena& ar) {
int keySize = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.size() +
TenantRangeImpl::submoduleRange.begin.size() + key.size();
if (db->apiVersionAtLeast(720)) {
keySize += TenantRangeImpl::mapSubRange.begin.size();
}
KeyRef prefixedKey = makeString(keySize, ar);
uint8_t* mutableKey = mutateString(prefixedKey);
mutableKey = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.copyTo(mutableKey);
mutableKey = TenantRangeImpl::submoduleRange.begin.copyTo(mutableKey);
if (db->apiVersionAtLeast(720)) {
mutableKey = TenantRangeImpl::mapSubRange.begin.copyTo(mutableKey);
}
key.copyTo(mutableKey);
return prefixedKey;
}
ACTOR Future<Void> getTenantList(ReadYourWritesTransaction* ryw,
KeyRangeRef kr,
RangeResult* results,
GetRangeLimits limitsHint) {
std::map<TenantName, TenantMapEntry> tenants =
wait(TenantAPI::listTenantsTransaction(&ryw->getTransaction(), kr.begin, kr.end, limitsHint.rows));
for (auto tenant : tenants) {
json_spirit::mObject tenantEntry;
tenantEntry["id"] = tenant.second.id;
tenantEntry["prefix"] = tenant.second.prefix.toString();
tenantEntry["tenant_state"] = TenantMapEntry::tenantStateToString(tenant.second.tenantState);
if (tenant.second.assignedCluster.present()) {
tenantEntry["assigned_cluster"] = tenant.second.assignedCluster.get().toString();
}
if (tenant.second.tenantGroup.present()) {
tenantEntry["tenant_group"] = tenant.second.tenantGroup.get().toString();
}
std::string tenantEntryString = json_spirit::write_string(json_spirit::mValue(tenantEntry));
ValueRef tenantEntryBytes(results->arena(), tenantEntryString);
results->push_back(
results->arena(),
KeyValueRef(withTenantMapPrefix(ryw->getDatabase(), tenant.first, results->arena()), tenantEntryBytes));
}
return Void();
}
ACTOR Future<RangeResult> getTenantRange(ReadYourWritesTransaction* ryw, KeyRangeRef kr, GetRangeLimits limitsHint) {
state RangeResult results;
kr = kr.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin);
if (ryw->getDatabase()->apiVersionAtLeast(720)) {
kr = kr.removePrefix(TenantRangeImpl::submoduleRange.begin);
if (kr.intersects(TenantRangeImpl::mapSubRange)) {
GetRangeLimits limits = limitsHint;
limits.decrement(results);
wait(getTenantList(
ryw,
removePrefix(kr & TenantRangeImpl::mapSubRange, TenantRangeImpl::mapSubRange.begin, "\xff"_sr),
&results,
limits));
}
} else {
wait(getTenantList(
ryw, removePrefix(kr, TenantRangeImpl::submoduleRange.begin, "\xff"_sr), &results, limitsHint));
}
return results;
}
Future<RangeResult> TenantRangeImpl::getRange(ReadYourWritesTransaction* ryw,
KeyRangeRef kr,
GetRangeLimits limitsHint) const {
return getTenantRange(ryw, kr, limitsHint);
}
ACTOR Future<Void> deleteTenantRange(ReadYourWritesTransaction* ryw, TenantName beginTenant, TenantName endTenant) {
state Future<Optional<Value>> tenantModeFuture =
ryw->getTransaction().get(configKeysPrefix.withSuffix("tenant_mode"_sr));
state Future<ClusterType> clusterTypeFuture = TenantAPI::getClusterType(&ryw->getTransaction());
state std::map<TenantName, TenantMapEntry> tenants = wait(TenantAPI::listTenantsTransaction(
&ryw->getTransaction(), beginTenant, endTenant, CLIENT_KNOBS->MAX_TENANTS_PER_CLUSTER + 1));
state Optional<Value> tenantMode = wait(safeThreadFutureToFuture(tenantModeFuture));
ClusterType clusterType = wait(clusterTypeFuture);
if (!TenantAPI::checkTenantMode(tenantMode, clusterType, ClusterType::STANDALONE)) {
throw tenants_disabled();
}
if (tenants.size() > CLIENT_KNOBS->MAX_TENANTS_PER_CLUSTER) {
TraceEvent(SevWarn, "DeleteTenantRangeTooLange")
.detail("BeginTenant", beginTenant)
.detail("EndTenant", endTenant);
ryw->setSpecialKeySpaceErrorMsg(
ManagementAPIError::toJsonString(false, "delete tenants", "too many tenants to range delete"));
throw special_keys_api_failure();
}
std::vector<Future<Void>> deleteFutures;
for (auto tenant : tenants) {
deleteFutures.push_back(TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenant.first));
}
wait(waitForAll(deleteFutures));
return Void();
}
ACTOR Future<bool> checkTenantGroup(ReadYourWritesTransaction* ryw,
Optional<TenantGroupName> currentGroup,
Optional<TenantGroupName> desiredGroup) {
if (!desiredGroup.present() || currentGroup == desiredGroup) {
return true;
}
// TODO: check where desired group is assigned and allow if the cluster is the same
// SOMEDAY: It should also be possible to change the tenant group when we support tenant movement.
wait(delay(0));
return false;
}
ACTOR Future<Void> applyTenantConfig(ReadYourWritesTransaction* ryw,
TenantNameRef tenantName,
std::vector<std::pair<StringRef, Optional<Value>>> configEntries,
TenantMapEntry* tenantEntry,
bool creatingTenant) {
state std::vector<std::pair<StringRef, Optional<Value>>>::iterator configItr;
for (configItr = configEntries.begin(); configItr != configEntries.end(); ++configItr) {
if (configItr->first == "tenant_group"_sr) {
state bool isValidTenantGroup = true;
if (!creatingTenant) {
bool result = wait(checkTenantGroup(ryw, tenantEntry->tenantGroup, configItr->second));
isValidTenantGroup = result;
}
if (isValidTenantGroup) {
tenantEntry->tenantGroup = configItr->second;
} else {
TraceEvent(SevWarn, "CannotChangeTenantGroup")
.detail("TenantName", tenantName)
.detail("CurrentTenantGroup", tenantEntry->tenantGroup)
.detail("DesiredTenantGroup", configItr->second);
ryw->setSpecialKeySpaceErrorMsg(ManagementAPIError::toJsonString(
false,
"set tenant configuration",
format("cannot change tenant group for tenant `%s'", tenantName.toString().c_str())));
throw special_keys_api_failure();
}
} else {
TraceEvent(SevWarn, "InvalidTenantConfig")
.detail("TenantName", tenantName)
.detail("ConfigName", configItr->first);
ryw->setSpecialKeySpaceErrorMsg(
ManagementAPIError::toJsonString(false,
"set tenant configuration",
format("invalid tenant configuration option `%s' for tenant `%s'",
configItr->first.toString().c_str(),
tenantName.toString().c_str())));
throw special_keys_api_failure();
}
}
return Void();
}
ACTOR Future<Void> createTenant(ReadYourWritesTransaction* ryw,
TenantNameRef tenantName,
Optional<std::vector<std::pair<StringRef, Optional<Value>>>> configMutations,
int64_t tenantId) {
state TenantMapEntry tenantEntry;
tenantEntry.id = tenantId;
if (configMutations.present()) {
wait(applyTenantConfig(ryw, tenantName, configMutations.get(), &tenantEntry, true));
}
std::pair<Optional<TenantMapEntry>, bool> entry =
wait(TenantAPI::createTenantTransaction(&ryw->getTransaction(), tenantName, tenantEntry));
return Void();
}
ACTOR Future<Void> createTenants(
ReadYourWritesTransaction* ryw,
std::map<TenantNameRef, Optional<std::vector<std::pair<StringRef, Optional<Value>>>>> tenants) {
Optional<Value> lastIdVal = wait(ryw->getTransaction().get(tenantLastIdKey));
int64_t previousId = lastIdVal.present() ? TenantMapEntry::prefixToId(lastIdVal.get()) : -1;
std::vector<Future<Void>> createFutures;
for (auto const& [tenant, config] : tenants) {
createFutures.push_back(createTenant(ryw, tenant, config, ++previousId));
}
ryw->getTransaction().set(tenantLastIdKey, TenantMapEntry::idToPrefix(previousId));
wait(waitForAll(createFutures));
return Void();
}
ACTOR Future<Void> changeTenantConfig(ReadYourWritesTransaction* ryw,
TenantNameRef tenantName,
std::vector<std::pair<StringRef, Optional<Value>>> configEntries) {
state Optional<TenantMapEntry> tenantEntry = wait(TenantAPI::tryGetTenantTransaction(ryw, tenantName));
if (!tenantEntry.present()) {
TraceEvent(SevWarn, "ConfigureUnknownTenant").detail("TenantName", tenantName);
ryw->setSpecialKeySpaceErrorMsg(ManagementAPIError::toJsonString(
false,
"set tenant configuration",
format("cannot configure tenant `%s': tenant not found", tenantName.toString().c_str())));
throw special_keys_api_failure();
}
TenantMapEntry& entry = tenantEntry.get();
wait(applyTenantConfig(ryw, tenantName, configEntries, &entry, false));
TenantAPI::configureTenantTransaction(ryw, tenantName, tenantEntry.get());
return Void();
}
Future<Optional<std::string>> TenantRangeImpl::commit(ReadYourWritesTransaction* ryw) {
auto ranges = ryw->getSpecialKeySpaceWriteMap().containedRanges(range);
std::vector<Future<Void>> tenantManagementFutures;
std::vector<std::pair<KeyRangeRef, Optional<Value>>> mapMutations;
std::map<TenantNameRef, std::vector<std::pair<StringRef, Optional<Value>>>> configMutations;
for (auto range : ranges) {
if (!range.value().first) {
continue;
}
KeyRangeRef adjustedRange =
range.range().removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin);
if (ryw->getDatabase()->apiVersionAtLeast(720)) {
adjustedRange = adjustedRange.removePrefix(submoduleRange.begin);
}
if (mapSubRange.intersects(adjustedRange) || !ryw->getDatabase()->apiVersionAtLeast(720)) {
if (ryw->getDatabase()->apiVersionAtLeast(720)) {
adjustedRange = mapSubRange & adjustedRange;
adjustedRange = removePrefix(adjustedRange, mapSubRange.begin, "\xff"_sr);
} else {
adjustedRange = removePrefix(adjustedRange, submoduleRange.begin, "\xff"_sr);
}
mapMutations.push_back(std::make_pair(adjustedRange, range.value().second));
} else if (configureSubRange.contains(adjustedRange) && adjustedRange.singleKeyRange()) {
StringRef tenantName = adjustedRange.begin.removePrefix(configureSubRange.begin);
StringRef configName = tenantName.eat("/");
configMutations[tenantName].push_back(std::make_pair(configName, range.value().second));
}
}
std::map<TenantNameRef, Optional<std::vector<std::pair<StringRef, Optional<Value>>>>> tenantsToCreate;
for (auto mapMutation : mapMutations) {
TenantNameRef tenantName = mapMutation.first.begin;
if (mapMutation.second.present()) {
Optional<std::vector<std::pair<StringRef, Optional<Value>>>> createMutations;
auto itr = configMutations.find(tenantName);
if (itr != configMutations.end()) {
createMutations = itr->second;
configMutations.erase(itr);
}
tenantsToCreate[tenantName] = createMutations;
} else {
// For a single key clear, just issue the delete
if (mapMutation.first.singleKeyRange()) {
tenantManagementFutures.push_back(
TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenantName));
} else {
tenantManagementFutures.push_back(deleteTenantRange(ryw, tenantName, mapMutation.first.end));
}
}
}
if (!tenantsToCreate.empty()) {
tenantManagementFutures.push_back(createTenants(ryw, tenantsToCreate));
}
for (auto configMutation : configMutations) {
tenantManagementFutures.push_back(changeTenantConfig(ryw, configMutation.first, configMutation.second));
}
return tag(waitForAll(tenantManagementFutures), Optional<std::string>());
}

View File

@ -0,0 +1,43 @@
/*
* TenantSpecialKeys.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 "fdbclient/TenantSpecialKeys.actor.h"
template <>
const KeyRangeRef TenantRangeImpl<true>::submoduleRange = KeyRangeRef("tenant/"_sr, "tenant0"_sr);
template <>
const KeyRangeRef TenantRangeImpl<true>::mapSubRange = KeyRangeRef("map/"_sr, "map0"_sr);
template <>
const KeyRangeRef TenantRangeImpl<false>::submoduleRange = KeyRangeRef(""_sr, "\xff"_sr);
template <>
const KeyRangeRef TenantRangeImpl<false>::mapSubRange = KeyRangeRef("tenant_map/"_sr, "tenant_map0"_sr);
template <>
bool TenantRangeImpl<true>::subRangeIntersects(KeyRangeRef subRange, KeyRangeRef range) {
return subRange.intersects(range);
}
template <>
bool TenantRangeImpl<false>::subRangeIntersects(KeyRangeRef subRange, KeyRangeRef range) {
return subRange == mapSubRange;
}

View File

@ -25,7 +25,6 @@
#include "fdbclient/GenericTransactionHelper.h"
#include "fdbclient/IClientApi.h"
#include "fdbclient/ReadYourWrites.h"
#include "fdbclient/DatabaseContext.h"
#include "fdbclient/Subspace.h"
#include "flow/ObjectSerializer.h"
@ -197,7 +196,7 @@ public:
typename std::enable_if<is_transaction_creator<DB>, Future<Optional<T>>>::type get(
Reference<DB> db,
Snapshot snapshot = Snapshot::False) const {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
@ -209,7 +208,7 @@ public:
typename std::enable_if<is_transaction_creator<DB>, Future<T>>::type getD(Reference<DB> db,
Snapshot snapshot = Snapshot::False,
T defaultValue = T()) const {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
@ -221,7 +220,7 @@ public:
typename std::enable_if<is_transaction_creator<DB>, Future<T>>::type getOrThrow(Reference<DB> db,
Snapshot snapshot = Snapshot::False,
Error err = key_not_found()) const {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
@ -236,7 +235,7 @@ public:
template <class DB>
typename std::enable_if<is_transaction_creator<DB>, Future<Void>>::type set(Reference<DB> db, T const& val) {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
self->set(tr, val);
@ -430,7 +429,7 @@ public:
typename std::enable_if<is_transaction_creator<DB>, Future<Optional<T>>>::type get(
Reference<DB> db,
Snapshot snapshot = Snapshot::False) const {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
@ -442,7 +441,7 @@ public:
typename std::enable_if<is_transaction_creator<DB>, Future<T>>::type getD(Reference<DB> db,
Snapshot snapshot = Snapshot::False,
T defaultValue = T()) const {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
@ -454,7 +453,7 @@ public:
typename std::enable_if<is_transaction_creator<DB>, Future<T>>::type getOrThrow(Reference<DB> db,
Snapshot snapshot = Snapshot::False,
Error err = key_not_found()) const {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
@ -469,7 +468,7 @@ public:
template <class DB>
typename std::enable_if<is_transaction_creator<DB>, Future<Void>>::type set(Reference<DB> db, T const& val) {
return runTransaction(db, [=, self = *this](Reference<ReadYourWritesTransaction> tr) {
return runTransaction(db, [=, self = *this](Reference<typename DB::TransactionT> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
self.set(tr, val);

View File

@ -41,7 +41,7 @@ Future<decltype(std::declval<Function>()(Reference<ReadYourWritesTransaction>())
state Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
loop {
try {
// func should be idempodent; otherwise, retry will get undefined result
// func should be idempotent; otherwise, retry will get undefined result
state decltype(std::declval<Function>()(Reference<ReadYourWritesTransaction>()).getValue()) result =
wait(func(tr));
wait(tr->commit());
@ -59,7 +59,7 @@ Future<decltype(std::declval<Function>()(Reference<typename DB::TransactionT>())
state Reference<typename DB::TransactionT> tr = db->createTransaction();
loop {
try {
// func should be idempodent; otherwise, retry will get undefined result
// func should be idempotent; otherwise, retry will get undefined result
state decltype(std::declval<Function>()(Reference<typename DB::TransactionT>()).getValue()) result =
wait(func(tr));
wait(safeThreadFutureToFuture(tr->commit()));

View File

@ -538,18 +538,5 @@ public:
Future<Optional<std::string>> commit(ReadYourWritesTransaction* ryw) override;
};
class TenantRangeImpl : public SpecialKeyRangeRWImpl {
public:
const static KeyRangeRef submoduleRange;
const static KeyRangeRef mapSubRange;
const static KeyRangeRef configureSubRange;
explicit TenantRangeImpl(KeyRangeRef kr);
Future<RangeResult> getRange(ReadYourWritesTransaction* ryw,
KeyRangeRef kr,
GetRangeLimits limitsHint) const override;
Future<Optional<std::string>> commit(ReadYourWritesTransaction* ryw) override;
};
#include "flow/unactorcompiler.h"
#endif

View File

@ -181,7 +181,7 @@ Future<std::pair<Optional<TenantMapEntry>, bool>> createTenantTransaction(
tenantEntry.assignedCluster = Optional<ClusterName>();
}
tr->set(tenantMapKey, newTenant.encode());
tr->set(tenantMapKey, tenantEntry.encode());
if (tenantEntry.tenantGroup.present()) {
tr->set(getTenantGroupIndexKey(tenantEntry.tenantGroup.get(), name), ""_sr);
@ -338,7 +338,7 @@ Future<Void> deleteTenant(Reference<DB> db,
template <class Transaction>
void configureTenantTransaction(Transaction tr, TenantNameRef tenantName, TenantMapEntry tenantEntry) {
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
tr->set(tenantName.withPrefix(tenantMapPrefix), encodeTenantEntry(tenantEntry));
tr->set(tenantName.withPrefix(tenantMapPrefix), tenantEntry.encode());
}
ACTOR template <class Transaction>

View File

@ -0,0 +1,343 @@
/*
* TenantSpecialKeys.actor.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.
*/
#pragma once
#if defined(NO_INTELLISENSE) && !defined(FDBCLIENT_TENANT_SPECIAL_KEYS_ACTOR_G_H)
#define FDBCLIENT_TENANT_SPECIAL_KEYS_ACTOR_G_H
#include "fdbclient/TenantSpecialKeys.actor.g.h"
#elif !defined(FDBCLIENT_TENANT_SPECIAL_KEYS_ACTOR_H)
#define FDBCLIENT_TENANT_SPECIAL_KEYS_ACTOR_H
#include "fdbclient/ActorLineageProfiler.h"
#include "fdbclient/FDBOptions.g.h"
#include "fdbclient/Knobs.h"
#include "fdbclient/DatabaseContext.h"
#include "fdbclient/SpecialKeySpace.actor.h"
#include "fdbclient/TenantManagement.actor.h"
#include "flow/Arena.h"
#include "flow/UnitTest.h"
#include "flow/actorcompiler.h" // This must be the last #include.
template <bool HasSubRanges>
class TenantRangeImpl : public SpecialKeyRangeRWImpl {
private:
static bool subRangeIntersects(KeyRangeRef subRange, KeyRangeRef range);
static KeyRangeRef removePrefix(KeyRangeRef range, KeyRef prefix, KeyRef defaultEnd) {
KeyRef begin = range.begin.removePrefix(prefix);
KeyRef end;
if (range.end.startsWith(prefix)) {
end = range.end.removePrefix(prefix);
} else {
end = defaultEnd;
}
return KeyRangeRef(begin, end);
}
static KeyRef withTenantMapPrefix(KeyRef key, Arena& ar) {
int keySize = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.size() +
submoduleRange.begin.size() + mapSubRange.begin.size() + key.size();
KeyRef prefixedKey = makeString(keySize, ar);
uint8_t* mutableKey = mutateString(prefixedKey);
mutableKey = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.copyTo(mutableKey);
mutableKey = submoduleRange.begin.copyTo(mutableKey);
mutableKey = mapSubRange.begin.copyTo(mutableKey);
key.copyTo(mutableKey);
return prefixedKey;
}
ACTOR static Future<Void> getTenantList(ReadYourWritesTransaction* ryw,
KeyRangeRef kr,
RangeResult* results,
GetRangeLimits limitsHint) {
std::map<TenantName, TenantMapEntry> tenants =
wait(TenantAPI::listTenantsTransaction(&ryw->getTransaction(), kr.begin, kr.end, limitsHint.rows));
for (auto tenant : tenants) {
json_spirit::mObject tenantEntry;
tenantEntry["id"] = tenant.second.id;
tenantEntry["prefix"] = tenant.second.prefix.toString();
tenantEntry["tenant_state"] = TenantMapEntry::tenantStateToString(tenant.second.tenantState);
if (tenant.second.assignedCluster.present()) {
tenantEntry["assigned_cluster"] = tenant.second.assignedCluster.get().toString();
}
if (tenant.second.tenantGroup.present()) {
tenantEntry["tenant_group"] = tenant.second.tenantGroup.get().toString();
}
std::string tenantEntryString = json_spirit::write_string(json_spirit::mValue(tenantEntry));
ValueRef tenantEntryBytes(results->arena(), tenantEntryString);
results->push_back(results->arena(),
KeyValueRef(withTenantMapPrefix(tenant.first, results->arena()), tenantEntryBytes));
}
return Void();
}
ACTOR template <bool B>
static Future<RangeResult> getTenantRange(ReadYourWritesTransaction* ryw,
KeyRangeRef kr,
GetRangeLimits limitsHint) {
state RangeResult results;
kr = kr.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin)
.removePrefix(TenantRangeImpl<B>::submoduleRange.begin);
if (kr.intersects(TenantRangeImpl<B>::mapSubRange)) {
GetRangeLimits limits = limitsHint;
limits.decrement(results);
wait(getTenantList(
ryw,
removePrefix(kr & TenantRangeImpl<B>::mapSubRange, TenantRangeImpl<B>::mapSubRange.begin, "\xff"_sr),
&results,
limits));
}
return results;
}
ACTOR static Future<bool> checkTenantGroup(ReadYourWritesTransaction* ryw,
Optional<TenantGroupName> currentGroup,
Optional<TenantGroupName> desiredGroup) {
if (!desiredGroup.present() || currentGroup == desiredGroup) {
return true;
}
// TODO: check where desired group is assigned and allow if the cluster is the same
// SOMEDAY: It should also be possible to change the tenant group when we support tenant movement.
wait(delay(0));
return false;
}
ACTOR static Future<Void> applyTenantConfig(ReadYourWritesTransaction* ryw,
TenantNameRef tenantName,
std::vector<std::pair<StringRef, Optional<Value>>> configEntries,
TenantMapEntry* tenantEntry,
bool creatingTenant) {
state std::vector<std::pair<StringRef, Optional<Value>>>::iterator configItr;
for (configItr = configEntries.begin(); configItr != configEntries.end(); ++configItr) {
if (configItr->first == "tenant_group"_sr) {
state bool isValidTenantGroup = true;
if (!creatingTenant) {
bool result = wait(checkTenantGroup(ryw, tenantEntry->tenantGroup, configItr->second));
isValidTenantGroup = result;
}
if (isValidTenantGroup) {
tenantEntry->tenantGroup = configItr->second;
} else {
TraceEvent(SevWarn, "CannotChangeTenantGroup")
.detail("TenantName", tenantName)
.detail("CurrentTenantGroup", tenantEntry->tenantGroup)
.detail("DesiredTenantGroup", configItr->second);
ryw->setSpecialKeySpaceErrorMsg(ManagementAPIError::toJsonString(
false,
"set tenant configuration",
format("cannot change tenant group for tenant `%s'", tenantName.toString().c_str())));
throw special_keys_api_failure();
}
} else {
TraceEvent(SevWarn, "InvalidTenantConfig")
.detail("TenantName", tenantName)
.detail("ConfigName", configItr->first);
ryw->setSpecialKeySpaceErrorMsg(
ManagementAPIError::toJsonString(false,
"set tenant configuration",
format("invalid tenant configuration option `%s' for tenant `%s'",
configItr->first.toString().c_str(),
tenantName.toString().c_str())));
throw special_keys_api_failure();
}
}
return Void();
}
ACTOR static Future<Void> createTenant(ReadYourWritesTransaction* ryw,
TenantNameRef tenantName,
Optional<std::vector<std::pair<StringRef, Optional<Value>>>> configMutations,
int64_t tenantId) {
state TenantMapEntry tenantEntry;
tenantEntry.id = tenantId;
if (configMutations.present()) {
wait(applyTenantConfig(ryw, tenantName, configMutations.get(), &tenantEntry, true));
}
std::pair<Optional<TenantMapEntry>, bool> entry =
wait(TenantAPI::createTenantTransaction(&ryw->getTransaction(), tenantName, tenantEntry));
return Void();
}
ACTOR static Future<Void> createTenants(
ReadYourWritesTransaction* ryw,
std::map<TenantNameRef, Optional<std::vector<std::pair<StringRef, Optional<Value>>>>> tenants) {
Optional<Value> lastIdVal = wait(ryw->getTransaction().get(tenantLastIdKey));
int64_t previousId = lastIdVal.present() ? TenantMapEntry::prefixToId(lastIdVal.get()) : -1;
std::vector<Future<Void>> createFutures;
for (auto const& [tenant, config] : tenants) {
createFutures.push_back(createTenant(ryw, tenant, config, ++previousId));
}
ryw->getTransaction().set(tenantLastIdKey, TenantMapEntry::idToPrefix(previousId));
wait(waitForAll(createFutures));
return Void();
}
ACTOR static Future<Void> changeTenantConfig(ReadYourWritesTransaction* ryw,
TenantNameRef tenantName,
std::vector<std::pair<StringRef, Optional<Value>>> configEntries) {
state Optional<TenantMapEntry> tenantEntry = wait(TenantAPI::tryGetTenantTransaction(ryw, tenantName));
if (!tenantEntry.present()) {
TraceEvent(SevWarn, "ConfigureUnknownTenant").detail("TenantName", tenantName);
ryw->setSpecialKeySpaceErrorMsg(ManagementAPIError::toJsonString(
false,
"set tenant configuration",
format("cannot configure tenant `%s': tenant not found", tenantName.toString().c_str())));
throw special_keys_api_failure();
}
TenantMapEntry& entry = tenantEntry.get();
wait(applyTenantConfig(ryw, tenantName, configEntries, &entry, false));
TenantAPI::configureTenantTransaction(ryw, tenantName, tenantEntry.get());
return Void();
}
ACTOR static Future<Void> deleteTenantRange(ReadYourWritesTransaction* ryw,
TenantName beginTenant,
TenantName endTenant) {
state Future<Optional<Value>> tenantModeFuture =
ryw->getTransaction().get(configKeysPrefix.withSuffix("tenant_mode"_sr));
state Future<ClusterType> clusterTypeFuture = TenantAPI::getClusterType(&ryw->getTransaction());
state std::map<TenantName, TenantMapEntry> tenants = wait(TenantAPI::listTenantsTransaction(
&ryw->getTransaction(), beginTenant, endTenant, CLIENT_KNOBS->MAX_TENANTS_PER_CLUSTER + 1));
state Optional<Value> tenantMode = wait(safeThreadFutureToFuture(tenantModeFuture));
ClusterType clusterType = wait(clusterTypeFuture);
if (!TenantAPI::checkTenantMode(tenantMode, clusterType, ClusterType::STANDALONE)) {
throw tenants_disabled();
}
if (tenants.size() > CLIENT_KNOBS->MAX_TENANTS_PER_CLUSTER) {
TraceEvent(SevWarn, "DeleteTenantRangeTooLange")
.detail("BeginTenant", beginTenant)
.detail("EndTenant", endTenant);
ryw->setSpecialKeySpaceErrorMsg(
ManagementAPIError::toJsonString(false, "delete tenants", "too many tenants to range delete"));
throw special_keys_api_failure();
}
std::vector<Future<Void>> deleteFutures;
for (auto tenant : tenants) {
deleteFutures.push_back(TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenant.first));
}
wait(waitForAll(deleteFutures));
return Void();
}
public:
// These ranges vary based on the template parameter
const static KeyRangeRef submoduleRange;
const static KeyRangeRef mapSubRange;
// These sub-ranges should only be used if HasSubRanges=true
const inline static KeyRangeRef configureSubRange = KeyRangeRef("configure/"_sr, "configure0"_sr);
explicit TenantRangeImpl(KeyRangeRef kr) : SpecialKeyRangeRWImpl(kr) {}
Future<RangeResult> getRange(ReadYourWritesTransaction* ryw,
KeyRangeRef kr,
GetRangeLimits limitsHint) const override {
return getTenantRange<HasSubRanges>(ryw, kr, limitsHint);
}
Future<Optional<std::string>> commit(ReadYourWritesTransaction* ryw) override {
auto ranges = ryw->getSpecialKeySpaceWriteMap().containedRanges(range);
std::vector<Future<Void>> tenantManagementFutures;
std::vector<std::pair<KeyRangeRef, Optional<Value>>> mapMutations;
std::map<TenantNameRef, std::vector<std::pair<StringRef, Optional<Value>>>> configMutations;
for (auto range : ranges) {
if (!range.value().first) {
continue;
}
KeyRangeRef adjustedRange =
range.range()
.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin)
.removePrefix(submoduleRange.begin);
if (subRangeIntersects(mapSubRange, adjustedRange)) {
adjustedRange = mapSubRange & adjustedRange;
adjustedRange = removePrefix(adjustedRange, mapSubRange.begin, "\xff"_sr);
mapMutations.push_back(std::make_pair(adjustedRange, range.value().second));
} else if (subRangeIntersects(configureSubRange, adjustedRange) && adjustedRange.singleKeyRange()) {
StringRef tenantName = adjustedRange.begin.removePrefix(configureSubRange.begin);
StringRef configName = tenantName.eat("/");
configMutations[tenantName].push_back(std::make_pair(configName, range.value().second));
}
}
std::map<TenantNameRef, Optional<std::vector<std::pair<StringRef, Optional<Value>>>>> tenantsToCreate;
for (auto mapMutation : mapMutations) {
TenantNameRef tenantName = mapMutation.first.begin;
if (mapMutation.second.present()) {
Optional<std::vector<std::pair<StringRef, Optional<Value>>>> createMutations;
auto itr = configMutations.find(tenantName);
if (itr != configMutations.end()) {
createMutations = itr->second;
configMutations.erase(itr);
}
tenantsToCreate[tenantName] = createMutations;
} else {
// For a single key clear, just issue the delete
if (mapMutation.first.singleKeyRange()) {
tenantManagementFutures.push_back(
TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenantName));
} else {
tenantManagementFutures.push_back(deleteTenantRange(ryw, tenantName, mapMutation.first.end));
}
}
}
if (!tenantsToCreate.empty()) {
tenantManagementFutures.push_back(createTenants(ryw, tenantsToCreate));
}
for (auto configMutation : configMutations) {
tenantManagementFutures.push_back(changeTenantConfig(ryw, configMutation.first, configMutation.second));
}
return tag(waitForAll(tenantManagementFutures), Optional<std::string>());
}
};
#include "flow/unactorcompiler.h"
#endif

View File

@ -32,6 +32,7 @@
static const int InvalidEncodedSize = 0;
struct VersionVector {
constexpr static FileIdentifier file_identifier = 5253554;
friend struct serializable_traits<VersionVector>;
boost::container::flat_map<Tag, Version> versions; // An ordered map. (Note:
// changing this to an unordered

View File

@ -108,15 +108,17 @@ int decodedLength(int codeLength) noexcept {
return (codeLength / 4) * 3 + (r - 1);
}
std::pair<StringRef, bool> decode(Arena& arena, StringRef base64UrlStr) {
Optional<StringRef> decode(Arena& arena, StringRef base64UrlStr) {
auto decodedLen = decodedLength(base64UrlStr.size());
if (decodedLen <= 0) {
return { StringRef(), decodedLen == 0 };
if (decodedLen == 0)
return StringRef{};
return {};
}
auto out = new (arena) uint8_t[decodedLen];
auto actualLen = decode(base64UrlStr.begin(), base64UrlStr.size(), out);
ASSERT_EQ(decodedLen, actualLen);
return { StringRef(out, decodedLen), true };
return StringRef(out, decodedLen);
}
} // namespace base64url
@ -237,13 +239,13 @@ TEST_CASE("/fdbrpc/Base64UrlEncode") {
encodeOutput.toHexString());
ASSERT(false);
}
auto [decodeOutput, decodeOk] = base64url::decode(tmpArena, encodeOutputExpected);
ASSERT(decodeOk);
if (decodeOutput != decodeOutputExpected) {
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.toHexString());
decodeOutput.get().toHexString());
ASSERT(false);
}
}
@ -259,12 +261,12 @@ TEST_CASE("/fdbrpc/Base64UrlEncode") {
// make sure output only contains legal characters
for (auto i = 0; i < output.size(); i++)
ASSERT_NE(base64url::decodeValue(output[i]), base64url::_X);
auto [decodedOutput, decodeOk] = base64url::decode(tmpArena, output);
ASSERT(decodeOk);
if (input != decodedOutput) {
auto decodeOutput = base64url::decode(tmpArena, output);
ASSERT(decodeOutput.present());
if (input != decodeOutput.get()) {
fmt::print("Dynamic case {} (decode) failed, expected '{}', got '{}'\n",
input.toHexString(),
decodedOutput.toHexString());
decodeOutput.get().toHexString());
ASSERT(false);
}
}

840
fdbrpc/JsonWebKeySet.cpp Normal file
View File

@ -0,0 +1,840 @@
/*
* JsonWebKeySet.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 "flow/Arena.h"
#include "flow/AutoCPointer.h"
#include "flow/Error.h"
#include "flow/IRandom.h"
#include "flow/MkCert.h"
#include "flow/PKey.h"
#include "flow/UnitTest.h"
#include "fdbrpc/Base64UrlEncode.h"
#include "fdbrpc/Base64UrlDecode.h"
#include "fdbrpc/JsonWebKeySet.h"
#if defined(HAVE_WOLFSSL)
#include <wolfssl/options.h>
#endif
#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/opensslv.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(_WIN32)
#define USE_V3_API 1
#else
#define USE_V3_API 0
#endif
#if USE_V3_API
#include <openssl/core_names.h>
#include <openssl/param_build.h>
#endif
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <array>
#include <string_view>
#include <type_traits>
#define JWKS_ERROR(issue, op) \
TraceEvent(SevWarnAlways, "JsonWebKeySet" #op "Error").suppressFor(10).detail("Issue", issue)
#define JWKS_PARSE_ERROR(issue) JWKS_ERROR(issue, Parse)
#define JWKS_WRITE_ERROR(issue) JWKS_ERROR(issue, Write)
#define JWK_PARSE_ERROR(issue) \
TraceEvent(SevWarnAlways, "JsonWebKeyParseError") \
.suppressFor(10) \
.detail("Issue", issue) \
.detail("KeyIndexBase0", keyIndex)
#define JWK_WRITE_ERROR(issue) \
TraceEvent(SevWarnAlways, "JsonWebKeyWriteError") \
.suppressFor(10) \
.detail("Issue", issue) \
.detail("KeyName", keyName.toString())
#define JWK_ERROR_OSSL(issue, op) \
do { \
char buf[256]{ \
0, \
}; \
if (auto err = ::ERR_get_error()) { \
::ERR_error_string_n(err, buf, sizeof(buf)); \
} \
JWK_##op##_ERROR(issue).detail("OpenSSLError", static_cast<char const*>(buf)); \
} while (0)
#define JWK_PARSE_ERROR_OSSL(issue) JWK_ERROR_OSSL(issue, PARSE)
#define JWK_WRITE_ERROR_OSSL(issue) JWK_ERROR_OSSL(issue, WRITE)
namespace {
template <bool Required, class JsonValue>
bool getJwkStringMember(JsonValue const& value,
char const* memberName,
std::conditional_t<Required, StringRef, Optional<StringRef>>& out,
int keyIndex) {
auto itr = value.FindMember(memberName);
if (itr == value.MemberEnd()) {
if constexpr (Required) {
JWK_PARSE_ERROR("Missing required member").detail("Member", memberName);
return false;
} else {
return true;
}
}
auto const& member = itr->value;
if (!member.IsString()) {
JWK_PARSE_ERROR("Expected member is not a string").detail("MemberName", memberName);
return false;
}
out = StringRef(reinterpret_cast<uint8_t const*>(member.GetString()), member.GetStringLength());
return true;
}
#define DECLARE_JWK_REQUIRED_STRING_MEMBER(value, member) \
auto member = StringRef(); \
if (!getJwkStringMember<true>(value, #member, member, keyIndex)) \
return {}
#define DECLARE_JWK_OPTIONAL_STRING_MEMBER(value, member) \
auto member = Optional<StringRef>(); \
if (!getJwkStringMember<false>(value, #member, member, keyIndex)) \
return {}
template <bool Required, class AutoPtr>
bool getJwkBigNumMember(Arena& arena,
std::conditional_t<Required, StringRef, Optional<StringRef>> const& b64Member,
AutoPtr& ptr,
char const* memberName,
char const* algorithm,
int keyIndex) {
if constexpr (!Required) {
if (!b64Member.present())
return true;
}
auto data = StringRef();
if constexpr (Required) {
data = b64Member;
} else {
data = b64Member.get();
}
auto decoded = base64url::decode(arena, data);
if (!decoded.present()) {
JWK_PARSE_ERROR("Base64URL decoding for parameter failed")
.detail("Algorithm", algorithm)
.detail("Parameter", memberName);
return false;
}
data = decoded.get();
auto bn = ::BN_bin2bn(data.begin(), data.size(), nullptr);
if (!bn) {
JWK_PARSE_ERROR_OSSL("BN_bin2bn");
return false;
}
ptr.reset(bn);
return true;
}
#define DECL_DECODED_BN_MEMBER_REQUIRED(member, algo) \
auto member = AutoCPointer(nullptr, &::BN_free); \
if (!getJwkBigNumMember<true /*Required*/>(arena, b64##member, member, #member, algo, keyIndex)) \
return {}
#define DECL_DECODED_BN_MEMBER_OPTIONAL(member, algo) \
auto member = AutoCPointer(nullptr, &::BN_clear_free); \
if (!getJwkBigNumMember<false /*Required*/>(arena, b64##member, member, #member, algo, keyIndex)) \
return {}
#define EC_DECLARE_DECODED_REQUIRED_BN_MEMBER(member) DECL_DECODED_BN_MEMBER_REQUIRED(member, "EC")
#define EC_DECLARE_DECODED_OPTIONAL_BN_MEMBER(member) DECL_DECODED_BN_MEMBER_OPTIONAL(member, "EC")
#define RSA_DECLARE_DECODED_REQUIRED_BN_MEMBER(member) DECL_DECODED_BN_MEMBER_REQUIRED(member, "RSA")
#define RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(member) DECL_DECODED_BN_MEMBER_OPTIONAL(member, "RSA")
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));
}
Optional<PublicOrPrivateKey> parseEcP256Key(StringRef b64x, StringRef b64y, Optional<StringRef> b64d, int keyIndex) {
auto arena = Arena();
EC_DECLARE_DECODED_REQUIRED_BN_MEMBER(x);
EC_DECLARE_DECODED_REQUIRED_BN_MEMBER(y);
EC_DECLARE_DECODED_OPTIONAL_BN_MEMBER(d);
#if USE_V3_API
// avoid deprecated API
auto bld = AutoCPointer(::OSSL_PARAM_BLD_new(), &::OSSL_PARAM_BLD_free);
if (!bld) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_new() for EC");
return {};
}
// since OSSL_PKEY_PARAM_EC_PUB_{X|Y} are not settable params, we'll need to build a EC_GROUP and serialize it
auto group = AutoCPointer(::EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1), &::EC_GROUP_free);
if (!group) {
JWK_PARSE_ERROR_OSSL("EC_GROUP_new_by_curve_name()");
return {};
}
auto point = AutoCPointer(::EC_POINT_new(group), &::EC_POINT_free);
if (!point) {
JWK_PARSE_ERROR_OSSL("EC_POINT_new()");
return {};
}
if (1 != ::EC_POINT_set_affine_coordinates(group, point, x, y, nullptr)) {
JWK_PARSE_ERROR_OSSL("EC_POINT_set_affine_coordinates()");
return {};
}
auto pointBufLen = ::EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, nullptr, 0, nullptr);
if (!pointBufLen) {
JWK_PARSE_ERROR_OSSL("EC_POINT_point2oct() for length");
return {};
}
auto pointBuf = new (arena) uint8_t[pointBufLen];
::EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, pointBuf, pointBufLen, nullptr);
if (!::OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_GROUP_NAME, "prime256v1", sizeof("prime256v1") - 1) ||
!::OSSL_PARAM_BLD_push_octet_string(bld, OSSL_PKEY_PARAM_PUB_KEY, pointBuf, pointBufLen)) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_push_*() for EC (group, point)");
return {};
}
if (d && !::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_PRIV_KEY, d)) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_push_BN() for EC (d)");
return {};
}
auto params = AutoCPointer(::OSSL_PARAM_BLD_to_param(bld), &OSSL_PARAM_free);
if (!params) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_to_param() for EC");
return {};
}
auto pctx = AutoCPointer(::EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr), &::EVP_PKEY_CTX_free);
if (!pctx) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_CTX_new_from_name(EC)");
return {};
}
if (1 != ::EVP_PKEY_fromdata_init(pctx)) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_fromdata_init() for EC");
return {};
}
auto pkey = std::add_pointer_t<EVP_PKEY>();
if (1 != ::EVP_PKEY_fromdata(pctx, &pkey, (d ? EVP_PKEY_KEYPAIR : EVP_PKEY_PUBLIC_KEY), params) || !pkey) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_fromdata() for EC");
return {};
}
auto pkeyAutoPtr = AutoCPointer(pkey, &::EVP_PKEY_free);
#else // USE_V3_API
auto key = AutoCPointer(::EC_KEY_new_by_curve_name(NID_X9_62_prime256v1), &::EC_KEY_free);
if (!key) {
JWK_PARSE_ERROR_OSSL("EC_KEY_new()");
return {};
}
if (d) {
if (1 != ::EC_KEY_set_private_key(key, d)) {
JWK_PARSE_ERROR_OSSL("EC_KEY_set_private_key()");
return {};
}
}
if (1 != ::EC_KEY_set_public_key_affine_coordinates(key, x, y)) {
JWK_PARSE_ERROR_OSSL("EC_KEY_set_public_key_affine_coordinates(key, x, y)");
return {};
}
auto pkey = AutoCPointer(::EVP_PKEY_new(), &::EVP_PKEY_free);
if (!pkey) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_new() for EC");
return {};
}
if (1 != EVP_PKEY_set1_EC_KEY(pkey, key)) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_set1_EC_KEY()");
return {};
}
#endif // USE_V3_API
if (d) {
auto len = ::i2d_PrivateKey(pkey, nullptr);
if (len <= 0) {
JWK_PARSE_ERROR_OSSL("i2d_PrivateKey() for EC");
return {};
}
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PrivateKey(pkey, &out);
// assign through public API, even if it means some parsing overhead
return PrivateKey(DerEncoded{}, StringRef(buf, len));
} else {
auto len = ::i2d_PUBKEY(pkey, nullptr);
if (len <= 0) {
JWK_PARSE_ERROR_OSSL("i2d_PUBKEY() for EC");
return {};
}
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PUBKEY(pkey, &out);
// assign through public API, even if it means some parsing overhead
return PublicKey(DerEncoded{}, StringRef(buf, len));
}
}
Optional<PublicOrPrivateKey> parseRsaKey(StringRef b64n,
StringRef b64e,
Optional<StringRef> b64d,
Optional<StringRef> b64p,
Optional<StringRef> b64q,
Optional<StringRef> b64dp,
Optional<StringRef> b64dq,
Optional<StringRef> b64qi,
int keyIndex) {
auto arena = Arena();
RSA_DECLARE_DECODED_REQUIRED_BN_MEMBER(n);
RSA_DECLARE_DECODED_REQUIRED_BN_MEMBER(e);
RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(d);
RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(p);
RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(q);
RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(dp);
RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(dq);
RSA_DECLARE_DECODED_OPTIONAL_BN_MEMBER(qi);
auto const isPublic = !d || !p || !q || !dp || !dq || !qi;
#if USE_V3_API
// avoid deprecated, algo-specific API
auto bld = AutoCPointer(::OSSL_PARAM_BLD_new(), &::OSSL_PARAM_BLD_free);
if (!bld) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_new() for EC");
return {};
}
if (!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_N, n) ||
!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_E, e)) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_push_BN() for RSA (n, e)");
return {};
}
if (!isPublic) {
if (!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_D, d) ||
!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_FACTOR1, p) ||
!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_FACTOR2, q) ||
!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_EXPONENT1, dp) ||
!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_EXPONENT2, dq) ||
!::OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, qi)) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_push_BN() for RSA (d, p, q, dp, dq, qi)");
return {};
}
}
auto params = AutoCPointer(::OSSL_PARAM_BLD_to_param(bld), &::OSSL_PARAM_free);
if (!params) {
JWK_PARSE_ERROR_OSSL("OSSL_PARAM_BLD_to_param() for RSA");
return {};
}
auto pctx = AutoCPointer(::EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr), &::EVP_PKEY_CTX_free);
if (!pctx) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_CTX_new_from_name(RSA)");
return {};
}
if (1 != ::EVP_PKEY_fromdata_init(pctx)) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_fromdata_init() for RSA");
return {};
}
auto pkey = std::add_pointer_t<EVP_PKEY>();
if (1 != ::EVP_PKEY_fromdata(pctx, &pkey, (!isPublic ? EVP_PKEY_KEYPAIR : EVP_PKEY_PUBLIC_KEY), params)) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_fromdata() for EC");
return {};
}
auto pkeyAutoPtr = AutoCPointer(pkey, &::EVP_PKEY_free);
#else // USE_V3_API
auto rsa = AutoCPointer(RSA_new(), &::RSA_free);
if (!rsa) {
JWK_PARSE_ERROR_OSSL("RSA_new()");
return {};
}
if (1 != ::RSA_set0_key(rsa, n, e, d)) {
JWK_PARSE_ERROR_OSSL("RSA_set0_key()");
return {};
}
// set0 == ownership taken by rsa, no need to free
n.release();
e.release();
d.release();
if (!isPublic) {
if (1 != ::RSA_set0_factors(rsa, p, q)) {
JWK_PARSE_ERROR_OSSL("RSA_set0_factors()");
return {};
}
p.release();
q.release();
if (1 != ::RSA_set0_crt_params(rsa, dp, dq, qi)) {
JWK_PARSE_ERROR_OSSL("RSA_set0_crt_params()");
return {};
}
dp.release();
dq.release();
qi.release();
}
auto pkey = AutoCPointer(::EVP_PKEY_new(), &::EVP_PKEY_free);
if (!pkey) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_new() for RSA");
return {};
}
if (1 != ::EVP_PKEY_set1_RSA(pkey, rsa)) {
JWK_PARSE_ERROR_OSSL("EVP_PKEY_set1_RSA()");
return {};
}
#endif // USE_V3_API
if (!isPublic) {
auto len = ::i2d_PrivateKey(pkey, nullptr);
if (len <= 0) {
JWK_PARSE_ERROR_OSSL("i2d_PrivateKey() for RSA");
return {};
}
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PrivateKey(pkey, &out);
// assign through public API, even if it means some parsing overhead
return PrivateKey(DerEncoded{}, StringRef(buf, len));
} else {
auto len = ::i2d_PUBKEY(pkey, nullptr);
if (len <= 0) {
JWK_PARSE_ERROR_OSSL("i2d_PUBKEY() for RSA");
return {};
}
auto buf = new (arena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PUBKEY(pkey, &out);
// assign through public API, even if it means some parsing overhead
return PublicKey(DerEncoded{}, StringRef(buf, len));
}
}
template <class Value>
Optional<PublicOrPrivateKey> parseKey(const Value& key, StringRef kty, int keyIndex) {
if (kty == "EC"_sr) {
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, alg);
if (alg != "ES256"_sr) {
JWK_PARSE_ERROR("Unsupported EC algorithm").detail("Algorithm", alg.toString());
return {};
}
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, crv);
if (crv != "P-256"_sr) {
JWK_PARSE_ERROR("Unsupported EC curve").detail("Curve", crv.toString());
return {};
}
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, x);
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, y);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, d);
return parseEcP256Key(x, y, d, keyIndex);
} else if (kty == "RSA"_sr) {
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, alg);
if (alg != "RS256"_sr) {
JWK_PARSE_ERROR("Unsupported RSA algorithm").detail("Algorithm", alg.toString());
return {};
}
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, n);
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, e);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, d);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, p);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, q);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, dp);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, dq);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, qi);
auto privKeyArgs = 0;
privKeyArgs += d.present();
privKeyArgs += p.present();
privKeyArgs += q.present();
privKeyArgs += dp.present();
privKeyArgs += dq.present();
privKeyArgs += qi.present();
if (privKeyArgs == 0 || privKeyArgs == 6) {
return parseRsaKey(n, e, d, p, q, dp, dq, qi, keyIndex);
} else {
JWK_PARSE_ERROR("Private key arguments partially exist").detail("NumMissingArgs", 6 - privKeyArgs);
return {};
}
} else {
JWK_PARSE_ERROR("Unsupported key type").detail("KeyType", kty.toString());
return {};
}
}
bool encodeEcKey(rapidjson::Writer<rapidjson::StringBuffer>& writer,
StringRef keyName,
EVP_PKEY* pKey,
const bool isPublic) {
auto arena = Arena();
writer.StartObject();
writer.Key("kty");
writer.String("EC");
writer.Key("alg");
writer.String("ES256");
writer.Key("kid");
writer.String(reinterpret_cast<char const*>(keyName.begin()), keyName.size());
#if USE_V3_API
auto curveNameBuf = std::array<char, 64>{};
auto curveNameLen = 0ul;
if (1 != EVP_PKEY_get_utf8_string_param(
pKey, OSSL_PKEY_PARAM_GROUP_NAME, curveNameBuf.begin(), sizeof(curveNameBuf), &curveNameLen)) {
JWK_WRITE_ERROR_OSSL("Get group name from EC PKey");
return false;
}
auto curveName = std::string_view(curveNameBuf.cbegin(), curveNameLen);
if (curveName != std::string_view("prime256v1")) {
JWK_WRITE_ERROR("Unsupported EC curve").detail("CurveName", curveName);
return false;
}
writer.Key("crv");
writer.String("P-256");
#define JWK_WRITE_BN_EC_PARAM(x, param) \
do { \
auto x = AutoCPointer(nullptr, &::BN_clear_free); \
auto rawX = std::add_pointer_t<BIGNUM>(); \
if (1 != ::EVP_PKEY_get_bn_param(pKey, param, &rawX)) { \
JWK_WRITE_ERROR_OSSL("EVP_PKEY_get_bn_param(" #param ")"); \
return false; \
} \
x.reset(rawX); \
auto b64##x = bigNumToBase64Url(arena, x); \
writer.Key(#x); \
writer.String(reinterpret_cast<char const*>(b64##x.begin()), b64##x.size()); \
} while (0)
// Get and write affine coordinates, X and Y
JWK_WRITE_BN_EC_PARAM(x, OSSL_PKEY_PARAM_EC_PUB_X);
JWK_WRITE_BN_EC_PARAM(y, OSSL_PKEY_PARAM_EC_PUB_Y);
if (!isPublic) {
JWK_WRITE_BN_EC_PARAM(d, OSSL_PKEY_PARAM_PRIV_KEY);
}
#undef JWK_WRITE_BN_EC_PARAM
#else // USE_V3_API
auto ecKey = ::EVP_PKEY_get0_EC_KEY(pKey); // get0 == no refcount, no need to free
if (!ecKey) {
JWK_WRITE_ERROR_OSSL("Could not extract EC_KEY from EVP_PKEY");
return false;
}
auto group = ::EC_KEY_get0_group(ecKey);
if (!group) {
JWK_WRITE_ERROR("Could not get EC_GROUP from EVP_PKEY");
return false;
}
auto curveName = ::EC_GROUP_get_curve_name(group);
if (curveName == NID_undef) {
JWK_WRITE_ERROR("Could not match EC_GROUP to known curve");
return false;
}
if (curveName != NID_X9_62_prime256v1) {
JWK_WRITE_ERROR("Unsupported curve, expected P-256 (prime256v1)").detail("curveName", ::OBJ_nid2sn(curveName));
return false;
}
writer.Key("crv");
writer.String("P-256");
auto point = ::EC_KEY_get0_public_key(ecKey);
if (!point) {
JWK_WRITE_ERROR_OSSL("EC_KEY_get0_public_key() returned null");
return false;
}
auto x = AutoCPointer(::BN_new(), &::BN_free);
if (!x) {
JWK_WRITE_ERROR_OSSL("x = BN_new()");
return false;
}
auto y = AutoCPointer(::BN_new(), &::BN_free);
if (!y) {
JWK_WRITE_ERROR_OSSL("y = BN_new()");
return false;
}
if (1 !=
#ifdef OPENSSL_IS_BORINGSSL
::EC_POINT_get_affine_coordinates_GFp(group, point, x, y, nullptr)
#else
::EC_POINT_get_affine_coordinates(group, point, x, y, nullptr)
#endif
) {
JWK_WRITE_ERROR_OSSL("EC_POINT_get_affine_coordinates()");
return false;
}
auto b64X = bigNumToBase64Url(arena, x);
auto b64Y = bigNumToBase64Url(arena, y);
writer.Key("x");
writer.String(reinterpret_cast<char const*>(b64X.begin()), b64X.size());
writer.Key("y");
writer.String(reinterpret_cast<char const*>(b64Y.begin()), b64Y.size());
if (!isPublic) {
auto d = ::EC_KEY_get0_private_key(ecKey);
if (!d) {
JWK_WRITE_ERROR("EC_KEY_get0_private_key()");
return false;
}
auto b64D = bigNumToBase64Url(arena, d);
writer.Key("d");
writer.String(reinterpret_cast<char const*>(b64D.begin()), b64D.size());
}
#endif // USE_V3_API
writer.EndObject();
return true;
}
bool encodeRsaKey(rapidjson::Writer<rapidjson::StringBuffer>& writer,
StringRef keyName,
EVP_PKEY* pKey,
const bool isPublic) {
auto arena = Arena();
writer.StartObject();
writer.Key("kty");
writer.String("RSA");
writer.Key("alg");
writer.String("RS256");
writer.Key("kid");
writer.String(reinterpret_cast<char const*>(keyName.begin()), keyName.size());
#if USE_V3_API
#define JWK_WRITE_BN_RSA_PARAM_V3(x, param) \
do { \
auto x = AutoCPointer(nullptr, &::BN_clear_free); \
auto rawX = std::add_pointer_t<BIGNUM>(); \
if (1 != ::EVP_PKEY_get_bn_param(pKey, param, &rawX)) { \
JWK_WRITE_ERROR_OSSL("EVP_PKEY_get_bn_param(" #x ")"); \
return false; \
} \
x.reset(rawX); \
auto b64##x = bigNumToBase64Url(arena, x); \
writer.Key(#x); \
writer.String(reinterpret_cast<char const*>(b64##x.begin()), b64##x.size()); \
} while (0)
JWK_WRITE_BN_RSA_PARAM_V3(n, OSSL_PKEY_PARAM_RSA_N);
JWK_WRITE_BN_RSA_PARAM_V3(e, OSSL_PKEY_PARAM_RSA_E);
if (!isPublic) {
JWK_WRITE_BN_RSA_PARAM_V3(d, OSSL_PKEY_PARAM_RSA_D);
JWK_WRITE_BN_RSA_PARAM_V3(p, OSSL_PKEY_PARAM_RSA_FACTOR1);
JWK_WRITE_BN_RSA_PARAM_V3(q, OSSL_PKEY_PARAM_RSA_FACTOR2);
JWK_WRITE_BN_RSA_PARAM_V3(dp, OSSL_PKEY_PARAM_RSA_EXPONENT1);
JWK_WRITE_BN_RSA_PARAM_V3(dq, OSSL_PKEY_PARAM_RSA_EXPONENT2);
JWK_WRITE_BN_RSA_PARAM_V3(qi, OSSL_PKEY_PARAM_RSA_COEFFICIENT1);
}
#undef JWK_WRITE_BN_RSA_PARAM_V3
#else // USE_V3_API
#define JWK_WRITE_BN_RSA_PARAM_V1(x) \
do { \
if (!x) { \
JWK_WRITE_ERROR_OSSL("RSA_get0_* returned null " #x); \
return false; \
} \
auto b64##x = bigNumToBase64Url(arena, x); \
writer.Key(#x); \
writer.String(reinterpret_cast<char const*>(b64##x.begin()), b64##x.size()); \
} while (0)
auto rsaKey = ::EVP_PKEY_get0_RSA(pKey); // get0 == no refcount, no need to free
if (!rsaKey) {
JWK_WRITE_ERROR_OSSL("Could not extract RSA key from EVP_PKEY");
return false;
}
auto n = std::add_pointer_t<const BIGNUM>();
auto e = std::add_pointer_t<const BIGNUM>();
auto d = std::add_pointer_t<const BIGNUM>();
auto p = std::add_pointer_t<const BIGNUM>();
auto q = std::add_pointer_t<const BIGNUM>();
auto dp = std::add_pointer_t<const BIGNUM>();
auto dq = std::add_pointer_t<const BIGNUM>();
auto qi = std::add_pointer_t<const BIGNUM>();
::RSA_get0_key(rsaKey, &n, &e, &d);
JWK_WRITE_BN_RSA_PARAM_V1(n);
JWK_WRITE_BN_RSA_PARAM_V1(e);
if (!isPublic) {
::RSA_get0_factors(rsaKey, &p, &q);
::RSA_get0_crt_params(rsaKey, &dp, &dq, &qi);
JWK_WRITE_BN_RSA_PARAM_V1(d);
JWK_WRITE_BN_RSA_PARAM_V1(p);
JWK_WRITE_BN_RSA_PARAM_V1(q);
JWK_WRITE_BN_RSA_PARAM_V1(dp);
JWK_WRITE_BN_RSA_PARAM_V1(dq);
JWK_WRITE_BN_RSA_PARAM_V1(qi);
}
#undef JWK_WRITE_BN_RSA_PARAM_V1
#endif // USE_V3_API
writer.EndObject();
return true;
}
// Add exactly one object to context of writer. Object shall contain JWK-encoded public or private key
bool encodeKey(rapidjson::Writer<rapidjson::StringBuffer>& writer, StringRef keyName, const PublicOrPrivateKey& key) {
auto const isPublic = key.isPublic();
auto pKey = std::add_pointer_t<EVP_PKEY>();
auto alg = PKeyAlgorithm{};
if (isPublic) {
auto const& keyObj = key.getPublic();
pKey = keyObj.nativeHandle();
alg = keyObj.algorithm();
} else {
auto const& keyObj = key.getPrivate();
pKey = key.getPrivate().nativeHandle();
alg = keyObj.algorithm();
}
if (!pKey) {
JWK_WRITE_ERROR("PKey object to encode is null");
return false;
}
if (alg == PKeyAlgorithm::EC) {
return encodeEcKey(writer, keyName, pKey, isPublic);
} else if (alg == PKeyAlgorithm::RSA) {
return encodeRsaKey(writer, keyName, pKey, isPublic);
} else {
JWK_WRITE_ERROR("Attempted to encode PKey with unsupported algorithm");
return false;
}
return true;
}
void testPublicKey(PrivateKey (*factory)()) {
// stringify-deserialize public key.
// sign some data using private key to see whether deserialized public key can verify it.
auto& rng = *deterministicRandom();
auto pubKeyName = Standalone<StringRef>("somePublicKey"_sr);
auto privKey = factory();
auto pubKey = privKey.toPublic();
auto jwks = JsonWebKeySet{};
jwks.keys.emplace(pubKeyName, pubKey);
auto arena = Arena();
auto jwksStr = jwks.toStringRef(arena).get();
fmt::print("Test JWKS: {}\n", jwksStr.toString());
auto jwksClone = JsonWebKeySet::parse(jwksStr, {});
ASSERT(jwksClone.present());
auto pubKeyClone = jwksClone.get().keys[pubKeyName].getPublic();
auto randByteStr = [&rng, &arena](int len) {
auto buf = new (arena) uint8_t[len];
for (auto i = 0; i < len; i++)
buf[i] = rng.randomUInt32() % 255u;
return StringRef(buf, len);
};
auto randData = randByteStr(rng.randomUInt32() % 128 + 16);
auto signature = privKey.sign(arena, randData, *::EVP_sha256());
ASSERT(pubKeyClone.verify(randData, signature, *::EVP_sha256()));
const_cast<uint8_t&>(*randData.begin())++;
ASSERT(!pubKeyClone.verify(randData, signature, *::EVP_sha256()));
fmt::print("TESTED OK FOR OPENSSL V{} API\n", (OPENSSL_VERSION_NUMBER >> 28));
}
void testPrivateKey(PrivateKey (*factory)()) {
// stringify-deserialize private key.
// sign some data using deserialized private key to see whether public key can verify it.
auto& rng = *deterministicRandom();
auto privKeyName = Standalone<StringRef>("somePrivateKey"_sr);
auto privKey = factory();
auto pubKey = privKey.toPublic();
auto jwks = JsonWebKeySet{};
jwks.keys.emplace(privKeyName, privKey);
auto arena = Arena();
auto jwksStr = jwks.toStringRef(arena).get();
fmt::print("Test JWKS: {}\n", jwksStr.toString());
auto jwksClone = JsonWebKeySet::parse(jwksStr, {});
ASSERT(jwksClone.present());
auto privKeyClone = jwksClone.get().keys[privKeyName].getPrivate();
auto randByteStr = [&rng, &arena](int len) {
auto buf = new (arena) uint8_t[len];
for (auto i = 0; i < len; i++)
buf[i] = rng.randomUInt32() % 255u;
return StringRef(buf, len);
};
auto randData = randByteStr(rng.randomUInt32() % 128 + 16);
auto signature = privKeyClone.sign(arena, randData, *::EVP_sha256());
ASSERT(pubKey.verify(randData, signature, *::EVP_sha256()));
const_cast<uint8_t&>(*randData.begin())++;
ASSERT(!pubKey.verify(randData, signature, *::EVP_sha256()));
fmt::print("TESTED OK FOR OPENSSL V{} API\n", (OPENSSL_VERSION_NUMBER >> 28));
}
} // anonymous namespace
Optional<JsonWebKeySet> JsonWebKeySet::parse(StringRef jwksString, VectorRef<StringRef> allowedUses) {
auto d = rapidjson::Document();
d.Parse(reinterpret_cast<const char*>(jwksString.begin()), jwksString.size());
if (d.HasParseError()) {
JWKS_PARSE_ERROR("ParseError")
.detail("Message", GetParseError_En(d.GetParseError()))
.detail("Offset", d.GetErrorOffset());
return {};
}
auto keysItr = d.FindMember("keys");
if (!d.IsObject() || keysItr == d.MemberEnd() || !keysItr->value.IsArray()) {
JWKS_PARSE_ERROR("JWKS must be an object and have 'keys' array member");
return {};
}
auto const& keys = keysItr->value;
auto ret = JsonWebKeySet{};
for (auto keyIndex = 0; keyIndex < keys.Size(); keyIndex++) {
if (!keys[keyIndex].IsObject()) {
JWKS_PARSE_ERROR("element of 'keys' array must be an object");
return {};
}
auto const& key = keys[keyIndex];
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, kty);
DECLARE_JWK_REQUIRED_STRING_MEMBER(key, kid);
DECLARE_JWK_OPTIONAL_STRING_MEMBER(key, use);
if (use.present() && !allowedUses.empty()) {
auto allowed = false;
for (auto allowedUse : allowedUses) {
if (allowedUse == use.get()) {
allowed = true;
break;
}
}
if (!allowed) {
JWK_PARSE_ERROR("Illegal optional 'use' member found").detail("Use", use.get().toString());
return {};
}
}
auto parsedKey = parseKey(key, kty, keyIndex);
if (!parsedKey.present())
return {};
auto [iter, inserted] = ret.keys.insert({ Standalone<StringRef>(kid), parsedKey.get() });
if (!inserted) {
JWK_PARSE_ERROR("Duplicate key name").detail("KeyName", kid.toString());
return {};
}
}
return ret;
}
Optional<StringRef> JsonWebKeySet::toStringRef(Arena& arena) {
using Buffer = rapidjson::StringBuffer;
using Writer = rapidjson::Writer<Buffer>;
auto buffer = Buffer();
auto writer = Writer(buffer);
writer.StartObject();
writer.Key("keys");
writer.StartArray();
for (const auto& [keyName, key] : keys) {
if (!encodeKey(writer, keyName, key)) {
return {};
}
}
writer.EndArray();
writer.EndObject();
auto buf = new (arena) uint8_t[buffer.GetSize()];
::memcpy(buf, buffer.GetString(), buffer.GetSize());
return StringRef(buf, buffer.GetSize());
}
void forceLinkJsonWebKeySetTests() {}
TEST_CASE("/fdbrpc/JsonWebKeySet/EC/PublicKey") {
testPublicKey(&mkcert::makeEcP256);
return Void();
}
TEST_CASE("/fdbrpc/JsonWebKeySet/EC/PrivateKey") {
testPrivateKey(&mkcert::makeEcP256);
return Void();
}
TEST_CASE("/fdbrpc/JsonWebKeySet/RSA/PublicKey") {
testPublicKey(&mkcert::makeRsa2048Bit);
return Void();
}
TEST_CASE("/fdbrpc/JsonWebKeySet/RSA/PrivateKey") {
testPrivateKey(&mkcert::makeRsa2048Bit);
return Void();
}

View File

@ -54,23 +54,6 @@ constexpr int MaxIssuerNameLenPlus1 = 25;
constexpr int MaxTenantNameLenPlus1 = 17;
constexpr int MaxKeyNameLenPlus1 = 21;
void trace(const char* type) {
auto te = TraceEvent(SevWarnAlways, type);
te.suppressFor(60);
if (auto err = ::ERR_get_error()) {
char buf[256]{
0,
};
::ERR_error_string_n(err, buf, sizeof(buf));
te.detail("OpenSSLError", static_cast<const char*>(buf));
}
}
[[noreturn]] void traceAndThrow(const char* type) {
trace(type);
throw digital_signature_ops_error();
}
StringRef genRandomAlphanumStringRef(Arena& arena, IRandom& rng, int maxLenPlusOne) {
const auto len = rng.randomInt(1, maxLenPlusOne);
auto strRaw = new (arena) uint8_t[len];
@ -79,6 +62,30 @@ StringRef genRandomAlphanumStringRef(Arena& arena, IRandom& rng, int maxLenPlusO
return StringRef(strRaw, len);
}
bool checkVerifyAlgorithm(PKeyAlgorithm algo, PublicKey key) {
if (algo != key.algorithm()) {
TraceEvent(SevWarnAlways, "TokenVerifyAlgoMismatch")
.suppressFor(10)
.detail("Expected", pkeyAlgorithmName(algo))
.detail("PublicKeyAlgorithm", key.algorithmName());
return false;
} else {
return true;
}
}
bool checkSignAlgorithm(PKeyAlgorithm algo, PrivateKey key) {
if (algo != key.algorithm()) {
TraceEvent(SevWarnAlways, "TokenSignAlgoMismatch")
.suppressFor(10)
.detail("Expected", pkeyAlgorithmName(algo))
.detail("PublicKeyAlgorithm", key.algorithmName());
return false;
} else {
return true;
}
}
} // namespace
namespace authz {
@ -94,21 +101,13 @@ Algorithm algorithmFromString(StringRef s) noexcept {
return Algorithm::UNKNOWN;
}
StringRef signString(Arena& arena, StringRef string, PrivateKey privateKey, int keyAlgNid, MessageDigestMethod digest);
bool verifyStringSignature(StringRef string,
StringRef signature,
PublicKey publicKey,
int keyAlgNid,
MessageDigestMethod digest);
std::pair<int /*key algorithm nid*/, MessageDigestMethod> getMethod(Algorithm alg) {
std::pair<PKeyAlgorithm, MessageDigestMethod> getMethod(Algorithm alg) {
if (alg == Algorithm::RS256) {
return { EVP_PKEY_RSA, ::EVP_sha256() };
return { PKeyAlgorithm::RSA, ::EVP_sha256() };
} else if (alg == Algorithm::ES256) {
return { EVP_PKEY_EC, ::EVP_sha256() };
return { PKeyAlgorithm::EC, ::EVP_sha256() };
} else {
return { NID_undef, nullptr };
return { PKeyAlgorithm::UNSUPPORTED, nullptr };
}
}
@ -121,71 +120,6 @@ std::string_view getAlgorithmName(Algorithm alg) {
UNREACHABLE();
}
StringRef signString(Arena& arena, StringRef string, PrivateKey privateKey, int keyAlgNid, MessageDigestMethod digest) {
ASSERT_NE(keyAlgNid, NID_undef);
auto key = privateKey.nativeHandle();
auto const privateKeyAlgNid = ::EVP_PKEY_base_id(key);
if (privateKeyAlgNid != keyAlgNid) {
TraceEvent(SevWarnAlways, "TokenSignAlgoMismatch")
.suppressFor(10)
.detail("ExpectedAlg", OBJ_nid2sn(keyAlgNid))
.detail("PublicKeyAlg", OBJ_nid2sn(privateKeyAlgNid));
throw digital_signature_ops_error();
}
auto mdctx = ::EVP_MD_CTX_create();
if (!mdctx)
traceAndThrow("SignTokenInitFail");
auto mdctxGuard = ScopeExit([mdctx]() { ::EVP_MD_CTX_free(mdctx); });
if (1 != ::EVP_DigestSignInit(mdctx, nullptr, digest, nullptr, key))
traceAndThrow("SignTokenInitFail");
if (1 != ::EVP_DigestSignUpdate(mdctx, string.begin(), string.size()))
traceAndThrow("SignTokenUpdateFail");
auto sigLen = size_t{};
if (1 != ::EVP_DigestSignFinal(mdctx, nullptr, &sigLen)) // assess the length first
traceAndThrow("SignTokenGetSigLenFail");
auto sigBuf = new (arena) uint8_t[sigLen];
if (1 != ::EVP_DigestSignFinal(mdctx, sigBuf, &sigLen))
traceAndThrow("SignTokenFinalizeFail");
return StringRef(sigBuf, sigLen);
}
bool verifyStringSignature(StringRef string,
StringRef signature,
PublicKey publicKey,
int keyAlgNid,
MessageDigestMethod digest) {
ASSERT_NE(keyAlgNid, NID_undef);
auto key = publicKey.nativeHandle();
auto const publicKeyAlgNid = ::EVP_PKEY_base_id(key);
if (keyAlgNid != publicKeyAlgNid) {
TraceEvent(SevWarnAlways, "TokenVerifyAlgoMismatch")
.suppressFor(10)
.detail("ExpectedAlg", OBJ_nid2sn(keyAlgNid))
.detail("PublicKeyAlg", OBJ_nid2sn(publicKeyAlgNid));
return false; // public key's algorithm doesn't match string's
}
auto mdctx = ::EVP_MD_CTX_create();
if (!mdctx) {
trace("VerifyTokenInitFail");
return false;
}
auto mdctxGuard = ScopeExit([mdctx]() { ::EVP_MD_CTX_free(mdctx); });
if (1 != ::EVP_DigestVerifyInit(mdctx, nullptr, digest, nullptr, key)) {
trace("VerifyTokenInitFail");
return false;
}
if (1 != ::EVP_DigestVerifyUpdate(mdctx, string.begin(), string.size())) {
trace("VerifyTokenUpdateFail");
return false;
}
if (1 != ::EVP_DigestVerifyFinal(mdctx, signature.begin(), signature.size())) {
auto te = TraceEvent(SevWarnAlways, "VerifyTokenFail");
te.suppressFor(5);
return false;
}
return true;
}
} // namespace authz
namespace authz::flatbuffers {
@ -195,8 +129,11 @@ SignedTokenRef signToken(Arena& arena, TokenRef token, StringRef keyName, Privat
auto writer = ObjectWriter([&arena](size_t len) { return new (arena) uint8_t[len]; }, IncludeVersion());
writer.serialize(token);
auto tokenStr = writer.toStringRef();
auto [keyAlgNid, digest] = getMethod(Algorithm::ES256);
auto sig = signString(arena, tokenStr, privateKey, keyAlgNid, digest);
auto [signAlgo, digest] = getMethod(Algorithm::ES256);
if (!checkSignAlgorithm(signAlgo, privateKey)) {
throw digital_signature_ops_error();
}
auto sig = privateKey.sign(arena, tokenStr, *digest);
ret.token = tokenStr;
ret.signature = sig;
ret.keyName = StringRef(arena, keyName);
@ -204,8 +141,10 @@ SignedTokenRef signToken(Arena& arena, TokenRef token, StringRef keyName, Privat
}
bool verifyToken(SignedTokenRef signedToken, PublicKey publicKey) {
auto [keyAlgNid, digest] = getMethod(Algorithm::ES256);
return verifyStringSignature(signedToken.token, signedToken.signature, publicKey, keyAlgNid, digest);
auto [keyAlg, digest] = getMethod(Algorithm::ES256);
if (!checkVerifyAlgorithm(keyAlg, publicKey))
return false;
return publicKey.verify(signedToken.token, signedToken.signature, *digest);
}
TokenRef makeRandomTokenSpec(Arena& arena, IRandom& rng) {
@ -281,15 +220,14 @@ StringRef makeTokenPart(Arena& arena, TokenRef tokenSpec) {
return StringRef(out, totalLen);
}
StringRef makePlainSignature(Arena& arena, Algorithm alg, StringRef tokenPart, PrivateKey privateKey) {
auto [keyAlgNid, digest] = getMethod(alg);
return signString(arena, tokenPart, privateKey, keyAlgNid, digest);
}
StringRef signToken(Arena& arena, TokenRef tokenSpec, PrivateKey privateKey) {
auto tmpArena = Arena();
auto tokenPart = makeTokenPart(tmpArena, tokenSpec);
auto plainSig = makePlainSignature(tmpArena, tokenSpec.algorithm, tokenPart, privateKey);
auto [signAlgo, digest] = getMethod(tokenSpec.algorithm);
if (!checkSignAlgorithm(signAlgo, privateKey)) {
throw digital_signature_ops_error();
}
auto plainSig = privateKey.sign(tmpArena, tokenPart, *digest);
auto const sigPartLen = base64url::encodedLength(plainSig.size());
auto const totalLen = tokenPart.size() + 1 + sigPartLen;
auto out = new (arena) uint8_t[totalLen];
@ -304,9 +242,10 @@ StringRef signToken(Arena& arena, TokenRef tokenSpec, PrivateKey privateKey) {
bool parseHeaderPart(TokenRef& token, StringRef b64urlHeader) {
auto tmpArena = Arena();
auto [header, valid] = base64url::decode(tmpArena, b64urlHeader);
if (!valid)
auto optHeader = base64url::decode(tmpArena, b64urlHeader);
if (!optHeader.present())
return false;
auto header = optHeader.get();
auto d = rapidjson::Document();
d.Parse(reinterpret_cast<const char*>(header.begin()), header.size());
if (d.HasParseError()) {
@ -317,9 +256,11 @@ bool parseHeaderPart(TokenRef& token, StringRef b64urlHeader) {
.detail("Offset", d.GetErrorOffset());
return false;
}
if (d.IsObject() && d.HasMember("alg") && d.HasMember("typ")) {
auto const& alg = d["alg"];
auto const& typ = d["typ"];
auto algItr = d.FindMember("alg");
auto typItr = d.FindMember("typ");
if (d.IsObject() && algItr != d.MemberEnd() && typItr != d.MemberEnd()) {
auto const& alg = algItr->value;
auto const& typ = typItr->value;
if (alg.IsString() && typ.IsString()) {
auto algValue = StringRef(reinterpret_cast<const uint8_t*>(alg.GetString()), alg.GetStringLength());
auto algType = algorithmFromString(algValue);
@ -337,9 +278,10 @@ bool parseHeaderPart(TokenRef& token, StringRef b64urlHeader) {
template <class FieldType>
bool parseField(Arena& arena, Optional<FieldType>& out, const rapidjson::Document& d, const char* fieldName) {
if (!d.HasMember(fieldName))
auto fieldItr = d.FindMember(fieldName);
if (fieldItr == d.MemberEnd())
return true;
auto const& field = d[fieldName];
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>) {
@ -371,7 +313,10 @@ bool parseField(Arena& arena, Optional<FieldType>& out, const rapidjson::Documen
bool parsePayloadPart(Arena& arena, TokenRef& token, StringRef b64urlPayload) {
auto tmpArena = Arena();
auto [payload, valid] = base64url::decode(tmpArena, b64urlPayload);
auto optPayload = base64url::decode(tmpArena, b64urlPayload);
if (!optPayload.present())
return false;
auto payload = optPayload.get();
auto d = rapidjson::Document();
d.Parse(reinterpret_cast<const char*>(payload.begin()), payload.size());
if (d.HasParseError()) {
@ -406,10 +351,11 @@ bool parsePayloadPart(Arena& arena, TokenRef& token, StringRef b64urlPayload) {
}
bool parseSignaturePart(Arena& arena, TokenRef& token, StringRef b64urlSignature) {
auto [sig, valid] = base64url::decode(arena, b64urlSignature);
if (valid)
token.signature = sig;
return valid;
auto optSig = base64url::decode(arena, b64urlSignature);
if (!optSig.present())
return false;
token.signature = optSig.get();
return true;
}
bool parseToken(Arena& arena, TokenRef& token, StringRef signedToken) {
@ -436,14 +382,17 @@ bool verifyToken(StringRef signedToken, PublicKey publicKey) {
if (b64urlHeader.empty() || b64urlPayload.empty() || b64urlSignature.empty())
return false;
auto b64urlTokenPart = fullToken.substr(0, b64urlHeader.size() + 1 + b64urlPayload.size());
auto [sig, valid] = base64url::decode(arena, b64urlSignature);
if (!valid)
auto optSig = base64url::decode(arena, b64urlSignature);
if (!optSig.present())
return false;
auto sig = optSig.get();
auto parsedToken = TokenRef();
if (!parseHeaderPart(parsedToken, b64urlHeader))
return false;
auto [keyAlgNid, digest] = getMethod(parsedToken.algorithm);
return verifyStringSignature(b64urlTokenPart, sig, publicKey, keyAlgNid, digest);
auto [verifyAlgo, digest] = getMethod(parsedToken.algorithm);
if (!checkVerifyAlgorithm(verifyAlgo, publicKey))
return false;
return publicKey.verify(b64urlTokenPart, sig, *digest);
}
TokenRef makeRandomTokenSpec(Arena& arena, IRandom& rng, Algorithm alg) {
@ -485,14 +434,14 @@ TEST_CASE("/fdbrpc/TokenSign/FlatBuffer") {
auto tokenSpec = authz::flatbuffers::makeRandomTokenSpec(arena, rng);
auto keyName = genRandomAlphanumStringRef(arena, rng, MaxKeyNameLenPlus1);
auto signedToken = authz::flatbuffers::signToken(arena, tokenSpec, keyName, privateKey);
const auto verifyExpectOk = authz::flatbuffers::verifyToken(signedToken, privateKey.toPublicKey());
const auto verifyExpectOk = authz::flatbuffers::verifyToken(signedToken, privateKey.toPublic());
ASSERT(verifyExpectOk);
// try tampering with signed token by adding one more tenant
tokenSpec.tenants.push_back(arena, genRandomAlphanumStringRef(arena, rng, 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.toPublicKey());
const auto verifyExpectFail = authz::flatbuffers::verifyToken(signedToken, privateKey.toPublic());
ASSERT(!verifyExpectFail);
}
printf("%d runs OK\n", numIters);
@ -507,7 +456,7 @@ TEST_CASE("/fdbrpc/TokenSign/JWT") {
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.toPublicKey());
const auto verifyExpectOk = authz::jwt::verifyToken(signedToken, privateKey.toPublic());
ASSERT(verifyExpectOk);
auto signaturePart = signedToken;
signaturePart.eat("."_sr);
@ -527,15 +476,15 @@ 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 [sig, sigValid] = base64url::decode(tmpArena, signaturePart);
ASSERT(sigValid);
ASSERT(sig == parsedToken.signature);
auto optSig = base64url::decode(tmpArena, signaturePart);
ASSERT(optSig.present());
ASSERT(optSig.get() == parsedToken.signature);
}
// try tampering with signed token by adding one more tenant
tokenSpec.tenants.get().push_back(arena, genRandomAlphanumStringRef(arena, rng, MaxTenantNameLenPlus1));
auto tamperedTokenPart = makeTokenPart(arena, tokenSpec);
auto tamperedTokenString = fmt::format("{}.{}", tamperedTokenPart.toString(), signaturePart.toString());
const auto verifyExpectFail = authz::jwt::verifyToken(StringRef(tamperedTokenString), privateKey.toPublicKey());
const auto verifyExpectFail = authz::jwt::verifyToken(StringRef(tamperedTokenString), privateKey.toPublic());
ASSERT(!verifyExpectFail);
}
printf("%d runs OK\n", numIters);
@ -543,13 +492,13 @@ TEST_CASE("/fdbrpc/TokenSign/JWT") {
}
TEST_CASE("/fdbrpc/TokenSign/bench") {
constexpr auto repeat = 10;
constexpr auto repeat = 5;
constexpr auto numSamples = 10000;
auto keys = std::vector<PrivateKey>(numSamples);
auto pubKeys = std::vector<PublicKey>(numSamples);
for (auto i = 0; i < numSamples; i++) {
keys[i] = mkcert::makeEcP256();
pubKeys[i] = keys[i].toPublicKey();
pubKeys[i] = keys[i].toPublic();
}
fmt::print("{} keys generated\n", numSamples);
auto& rng = *deterministicRandom();
@ -581,7 +530,8 @@ TEST_CASE("/fdbrpc/TokenSign/bench") {
auto fbBegin = timer_monotonic();
for (auto rep = 0; rep < repeat; rep++) {
for (auto i = 0; i < numSamples; i++) {
auto signedToken = ObjectReader::fromStringRef<authz::flatbuffers::SignedTokenRef>(fbs[i], Unversioned());
auto signedToken =
ObjectReader::fromStringRef<Standalone<authz::flatbuffers::SignedTokenRef>>(fbs[i], Unversioned());
auto verifyOk = authz::flatbuffers::verifyToken(signedToken, pubKeys[i]);
ASSERT(verifyOk);
}

View File

@ -41,9 +41,9 @@ int decode(const uint8_t* __restrict codeIn, const int lengthIn, uint8_t* __rest
// Returns -1 for invalid length (4n-3)
int decodedLength(int codeLength) noexcept;
// return a) decoded valid string and b) whether input string was a valid URL-encoded base64 string
// 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
std::pair<StringRef, bool> decode(Arena& arena, StringRef base64UrlStr);
Optional<StringRef> decode(Arena& arena, StringRef base64UrlStr);
} // namespace base64url

View File

@ -0,0 +1,67 @@
/*
* JsonWebKeySet.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_JSON_WEB_KEY_SET_H
#define FDBRPC_JSON_WEB_KEY_SET_H
#include "flow/Arena.h"
#include "flow/PKey.h"
#include <map>
#include <variant>
struct PublicOrPrivateKey {
std::variant<PublicKey, PrivateKey> key;
PublicOrPrivateKey() noexcept = default;
PublicOrPrivateKey(PublicKey key) noexcept : key(std::in_place_type<PublicKey>, key) {}
PublicOrPrivateKey(PrivateKey key) noexcept : key(std::in_place_type<PrivateKey>, key) {}
bool isPublic() const noexcept { return std::holds_alternative<PublicKey>(key); }
bool isPrivate() const noexcept { return std::holds_alternative<PrivateKey>(key); }
PublicKey getPublic() const { return std::get<PublicKey>(key); }
PrivateKey getPrivate() const { return std::get<PrivateKey>(key); }
};
// Implements JWKS standard in restricted scope:
// - Parses and stores public/private keys (but not shared keys) as OpenSSL internal types
// - Accept only a) EC algorithm with P-256 curve or b) RSA algorithm.
// - Each key object must meet following requirements:
// - "alg" field is set to either "ES256" or "RS256"
// - "kty" field is set to "EC" or "RSA"
// - "kty" field matches the "alg" field: i.e. EC for "alg":"ES256" and RSA for "alg":"RS256"
struct JsonWebKeySet {
using KeyMap = std::map<Standalone<StringRef>, PublicOrPrivateKey>;
KeyMap keys;
// Parse JWKS string to map of KeyName-PKey
// If allowedUses is not empty, JWK's optional "use" member is verified against it.
// Otherwise, uses are all-inclusive.
static Optional<JsonWebKeySet> parse(StringRef jwksString, VectorRef<StringRef> allowedUses);
// Returns JSON string representing the JSON Web Key Set.
// Inverse operation of parse(). Only allows keys expected/accepted by parse().
Optional<StringRef> toStringRef(Arena& arena);
};
#endif // FDBRPC_JSON_WEB_KEY_SET_H

View File

@ -100,10 +100,10 @@ struct TokenRef {
StringRef makeTokenPart(Arena& arena, TokenRef tokenSpec);
// Generate plaintext signature of token part
StringRef makePlainSignature(Arena& arena, Algorithm alg, StringRef tokenPart, StringRef privateKeyDer);
StringRef makePlainSignature(Arena& arena, Algorithm alg, StringRef tokenPart, PrivateKey privateKey);
// One-stop function to make JWT from spec
StringRef signToken(Arena& arena, TokenRef tokenSpec, StringRef privateKeyDer);
StringRef signToken(Arena& arena, TokenRef tokenSpec, PrivateKey privateKey);
// Parse passed b64url-encoded header part and materialize its contents into tokenOut,
// using memory allocated from arena
@ -123,7 +123,7 @@ bool parseSignaturePart(Arena& arena, TokenRef& tokenOut, StringRef b64urlSignat
bool parseToken(Arena& arena, TokenRef& tokenOut, StringRef signedTokenIn);
// Verify only the signature part of signed token string against its token part, not its content
bool verifyToken(StringRef signedToken, StringRef publicKeyDer);
bool verifyToken(StringRef signedToken, PublicKey publicKey);
} // namespace authz::jwt

View File

@ -762,7 +762,7 @@ ACTOR static Future<Void> handleApplyToDBRequest(RestoreVersionBatchRequest req,
ASSERT(batchData->dbApplier.present());
ASSERT(!batchData->dbApplier.get().isError()); // writeMutationsToDB actor cannot have error.
// We cannot blindly retry because it is not idempodent
// We cannot blindly retry because it is not idempotent
wait(batchData->dbApplier.get());

View File

@ -24,6 +24,7 @@
#include "fdbclient/GenericManagementAPI.actor.h"
#include "fdbclient/MetaclusterManagement.actor.h"
#include "fdbclient/TenantManagement.actor.h"
#include "fdbclient/TenantSpecialKeys.actor.h"
#include "fdbclient/ThreadSafeTransaction.h"
#include "fdbrpc/simulator.h"
#include "fdbserver/workloads/workloads.actor.h"
@ -54,12 +55,12 @@ struct TenantManagementWorkload : TestWorkload {
const TenantName tenantNamePrefix = "tenant_management_workload_"_sr;
TenantName localTenantNamePrefix;
const Key specialKeysTenantMapPrefix =
TenantRangeImpl::mapSubRange.begin.withPrefix(TenantRangeImpl::submoduleRange.begin.withPrefix(
SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin));
const Key specialKeysTenantConfigPrefix =
TenantRangeImpl::configureSubRange.begin.withPrefix(TenantRangeImpl::submoduleRange.begin.withPrefix(
SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin));
const Key specialKeysTenantMapPrefix = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT)
.begin.withSuffix(TenantRangeImpl<true>::submoduleRange.begin)
.withSuffix(TenantRangeImpl<true>::mapSubRange.begin);
const Key specialKeysTenantConfigPrefix = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT)
.begin.withSuffix(TenantRangeImpl<true>::submoduleRange.begin)
.withSuffix(TenantRangeImpl<true>::configureSubRange.begin);
int maxTenants;
int maxTenantGroups;

View File

@ -36,6 +36,7 @@ void forceLinkMutationLogReaderTests();
void forceLinkSimKmsConnectorTests();
void forceLinkIThreadPoolTests();
void forceLinkTokenSignTests();
void forceLinkJsonWebKeySetTests();
void forceLinkVersionVectorTests();
void forceLinkRESTClientTests();
void forceLinkRESTUtilsTests();
@ -86,6 +87,7 @@ struct UnitTestWorkload : TestWorkload {
forceLinkSimKmsConnectorTests();
forceLinkIThreadPoolTests();
forceLinkTokenSignTests();
forceLinkJsonWebKeySetTests();
forceLinkVersionVectorTests();
forceLinkRESTClientTests();
forceLinkRESTUtilsTests();

View File

@ -19,6 +19,7 @@
*/
#include "flow/Arena.h"
#include "flow/AutoCPointer.h"
#include "flow/IRandom.h"
#include "flow/MkCert.h"
#include "flow/PKey.h"
@ -34,6 +35,7 @@
#include <openssl/evp.h>
#include <openssl/objects.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
@ -107,26 +109,23 @@ void printCert(FILE* out, StringRef certPem) {
void printPrivateKey(FILE* out, StringRef privateKeyPem) {
auto key = PrivateKey(PemEncoded{}, privateKeyPem);
auto bio = ::BIO_new_fp(out, BIO_NOCLOSE);
auto bio = AutoCPointer(::BIO_new_fp(out, BIO_NOCLOSE), &::BIO_free);
OSSL_ASSERT(bio);
auto bioGuard = ScopeExit([bio]() { ::BIO_free(bio); });
OSSL_ASSERT(0 < ::EVP_PKEY_print_private(bio, key.nativeHandle(), 0, nullptr));
}
std::shared_ptr<X509> readX509CertPem(StringRef x509CertPem) {
ASSERT(!x509CertPem.empty());
auto bio_mem = ::BIO_new_mem_buf(x509CertPem.begin(), x509CertPem.size());
auto bio_mem = AutoCPointer(::BIO_new_mem_buf(x509CertPem.begin(), x509CertPem.size()), &::BIO_free);
OSSL_ASSERT(bio_mem);
auto bioGuard = ScopeExit([bio_mem]() { ::BIO_free(bio_mem); });
auto ret = ::PEM_read_bio_X509(bio_mem, nullptr, nullptr, nullptr);
OSSL_ASSERT(ret);
return std::shared_ptr<X509>(ret, &::X509_free);
}
StringRef writeX509CertPem(Arena& arena, const std::shared_ptr<X509>& nativeCert) {
auto mem = ::BIO_new(::BIO_s_mem());
auto mem = AutoCPointer(::BIO_new(::BIO_s_mem()), &::BIO_free);
OSSL_ASSERT(mem);
auto memGuard = ScopeExit([mem]() { ::BIO_free(mem); });
OSSL_ASSERT(::PEM_write_bio_X509(mem, nativeCert.get()));
auto bioBuf = std::add_pointer_t<char>{};
auto const len = ::BIO_get_mem_data(mem, &bioBuf);
@ -137,26 +136,54 @@ StringRef writeX509CertPem(Arena& arena, const std::shared_ptr<X509>& nativeCert
}
PrivateKey makeEcP256() {
auto params = std::add_pointer_t<EVP_PKEY>();
auto params = AutoCPointer(nullptr, &::EVP_PKEY_free);
{
auto pctx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
auto paramsRaw = std::add_pointer_t<EVP_PKEY>();
auto pctx = AutoCPointer(::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), &::EVP_PKEY_CTX_free);
OSSL_ASSERT(pctx);
auto ctxGuard = ScopeExit([pctx]() { ::EVP_PKEY_CTX_free(pctx); });
OSSL_ASSERT(0 < ::EVP_PKEY_paramgen_init(pctx));
OSSL_ASSERT(0 < ::EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1));
OSSL_ASSERT(0 < ::EVP_PKEY_paramgen(pctx, &params));
OSSL_ASSERT(params);
OSSL_ASSERT(0 < ::EVP_PKEY_paramgen(pctx, &paramsRaw));
OSSL_ASSERT(paramsRaw);
params.reset(paramsRaw);
}
auto paramsGuard = ScopeExit([params]() { ::EVP_PKEY_free(params); });
// keygen
auto kctx = ::EVP_PKEY_CTX_new(params, nullptr);
auto kctx = AutoCPointer(::EVP_PKEY_CTX_new(params, nullptr), &::EVP_PKEY_CTX_free);
OSSL_ASSERT(kctx);
auto kctxGuard = ScopeExit([kctx]() { ::EVP_PKEY_CTX_free(kctx); });
auto key = std::add_pointer_t<EVP_PKEY>();
auto key = AutoCPointer(nullptr, &::EVP_PKEY_free);
auto keyRaw = std::add_pointer_t<EVP_PKEY>();
OSSL_ASSERT(0 < ::EVP_PKEY_keygen_init(kctx));
OSSL_ASSERT(0 < ::EVP_PKEY_keygen(kctx, &key));
OSSL_ASSERT(key);
return std::shared_ptr<EVP_PKEY>(key, &::EVP_PKEY_free);
OSSL_ASSERT(0 < ::EVP_PKEY_keygen(kctx, &keyRaw));
OSSL_ASSERT(keyRaw);
key.reset(keyRaw);
auto len = 0;
len = ::i2d_PrivateKey(key, nullptr);
ASSERT_LT(0, len);
auto tmpArena = Arena();
auto buf = new (tmpArena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PrivateKey(key, &out);
return PrivateKey(DerEncoded{}, StringRef(buf, len));
}
PrivateKey makeRsa2048Bit() {
auto kctx = AutoCPointer(::EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr), &::EVP_PKEY_CTX_free);
OSSL_ASSERT(kctx);
auto key = AutoCPointer(nullptr, &::EVP_PKEY_free);
auto keyRaw = std::add_pointer_t<EVP_PKEY>();
OSSL_ASSERT(0 < ::EVP_PKEY_keygen_init(kctx));
OSSL_ASSERT(0 < ::EVP_PKEY_CTX_set_rsa_keygen_bits(kctx, 2048));
OSSL_ASSERT(0 < ::EVP_PKEY_keygen(kctx, &keyRaw));
OSSL_ASSERT(keyRaw);
key.reset(keyRaw);
auto len = 0;
len = ::i2d_PrivateKey(key, nullptr);
ASSERT_LT(0, len);
auto tmpArena = Arena();
auto buf = new (tmpArena) uint8_t[len];
auto out = std::add_pointer_t<uint8_t>(buf);
len = ::i2d_PrivateKey(key, &out);
return PrivateKey(DerEncoded{}, StringRef(buf, len));
}
CertAndKeyNative makeCertNative(CertSpecRef spec, CertAndKeyNative issuer) {

View File

@ -18,13 +18,16 @@
* limitations under the License.
*/
#include "flow/AutoCPointer.h"
#include "flow/Error.h"
#include "flow/PKey.h"
#include "flow/ScopeExit.h"
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/objects.h>
#include <openssl/opensslv.h>
namespace {
@ -54,14 +57,47 @@ void traceAndThrowEncode(const char* type) {
throw pkey_encode_error();
}
void traceAndThrowDsa(const char* type) {
auto te = TraceEvent(SevWarnAlways, type);
te.suppressFor(10);
if (auto err = ::ERR_get_error()) {
char buf[256]{
0,
};
::ERR_error_string_n(err, buf, sizeof(buf));
te.detail("OpenSSLError", static_cast<const char*>(buf));
}
throw digital_signature_ops_error();
}
inline PKeyAlgorithm getPKeyAlgorithm(const EVP_PKEY* key) noexcept {
auto id = ::EVP_PKEY_base_id(key);
if (id == EVP_PKEY_RSA)
return PKeyAlgorithm::RSA;
else if (id == EVP_PKEY_EC)
return PKeyAlgorithm::EC;
else
return PKeyAlgorithm::UNSUPPORTED;
}
} // anonymous namespace
std::string_view pkeyAlgorithmName(PKeyAlgorithm alg) noexcept {
switch (alg) {
case PKeyAlgorithm::EC:
return "EC";
case PKeyAlgorithm::RSA:
return "RSA";
default:
return "UNSUPPORTED";
}
}
StringRef doWritePublicKeyPem(Arena& arena, EVP_PKEY* key) {
ASSERT(key);
auto mem = ::BIO_new(::BIO_s_mem());
auto mem = AutoCPointer(::BIO_new(::BIO_s_mem()), &::BIO_free);
if (!mem)
traceAndThrowEncode("PublicKeyPemWriteInitError");
auto memGuard = ScopeExit([mem]() { ::BIO_free(mem); });
if (1 != ::PEM_write_bio_PUBKEY(mem, key))
traceAndThrowEncode("PublicKeyPemWrite");
auto bioBuf = std::add_pointer_t<char>{};
@ -82,16 +118,38 @@ StringRef doWritePublicKeyDer(Arena& arena, EVP_PKEY* key) {
return StringRef(buf, len);
}
bool doVerifyStringSignature(StringRef data, StringRef signature, const EVP_MD& digest, EVP_PKEY* key) {
auto mdctx = AutoCPointer(::EVP_MD_CTX_create(), &::EVP_MD_CTX_free);
if (!mdctx) {
traceAndThrowDsa("PKeyVerifyInitFail");
}
if (1 != ::EVP_DigestVerifyInit(mdctx, nullptr, &digest, nullptr, key)) {
traceAndThrowDsa("PKeyVerifyInitFail");
}
if (1 != ::EVP_DigestVerifyUpdate(mdctx, data.begin(), data.size())) {
traceAndThrowDsa("PKeyVerifyUpdateFail");
}
if (1 != ::EVP_DigestVerifyFinal(mdctx, signature.begin(), signature.size())) {
return false;
}
return true;
}
PublicKey::PublicKey(PemEncoded, StringRef pem) {
ASSERT(!pem.empty());
auto mem = ::BIO_new_mem_buf(pem.begin(), pem.size());
auto mem = AutoCPointer(::BIO_new_mem_buf(pem.begin(), pem.size()), &::BIO_free);
if (!mem)
traceAndThrowDecode("PemMemBioInitError");
auto bioGuard = ScopeExit([mem]() { ::BIO_free(mem); });
auto key = ::PEM_read_bio_PUBKEY(mem, nullptr, nullptr, nullptr);
if (!key)
traceAndThrowDecode("PemReadPublicKeyError");
ptr = std::shared_ptr<EVP_PKEY>(key, &::EVP_PKEY_free);
if (algorithm() == PKeyAlgorithm::UNSUPPORTED) {
TraceEvent(SevWarnAlways, "UnsupportedPKeyAlgorithm")
.suppressFor(10)
.detail("Algorithm", ::OBJ_nid2sn(EVP_PKEY_base_id(ptr.get())));
throw pkey_decode_error();
}
}
PublicKey::PublicKey(DerEncoded, StringRef der) {
@ -101,6 +159,12 @@ PublicKey::PublicKey(DerEncoded, StringRef der) {
if (!key)
traceAndThrowDecode("DerReadPublicKeyError");
ptr = std::shared_ptr<EVP_PKEY>(key, &::EVP_PKEY_free);
if (algorithm() == PKeyAlgorithm::UNSUPPORTED) {
TraceEvent(SevWarnAlways, "UnsupportedPKeyAlgorithm")
.suppressFor(10)
.detail("Algorithm", ::OBJ_nid2sn(EVP_PKEY_base_id(ptr.get())));
throw pkey_decode_error();
}
}
StringRef PublicKey::writePem(Arena& arena) const {
@ -111,16 +175,35 @@ StringRef PublicKey::writeDer(Arena& arena) const {
return doWritePublicKeyDer(arena, nativeHandle());
}
PKeyAlgorithm PublicKey::algorithm() const {
auto key = nativeHandle();
ASSERT(key);
return getPKeyAlgorithm(key);
}
std::string_view PublicKey::algorithmName() const {
return pkeyAlgorithmName(this->algorithm());
}
bool PublicKey::verify(StringRef data, StringRef signature, const EVP_MD& digest) const {
return doVerifyStringSignature(data, signature, digest, nativeHandle());
}
PrivateKey::PrivateKey(PemEncoded, StringRef pem) {
ASSERT(!pem.empty());
auto mem = ::BIO_new_mem_buf(pem.begin(), pem.size());
auto mem = AutoCPointer(::BIO_new_mem_buf(pem.begin(), pem.size()), &::BIO_free);
if (!mem)
traceAndThrowDecode("PrivateKeyDecodeInitError");
auto bioGuard = ScopeExit([mem]() { ::BIO_free(mem); });
auto key = ::PEM_read_bio_PrivateKey(mem, nullptr, nullptr, nullptr);
if (!key)
traceAndThrowDecode("PemReadPrivateKeyError");
ptr = std::shared_ptr<EVP_PKEY>(key, &::EVP_PKEY_free);
if (algorithm() == PKeyAlgorithm::UNSUPPORTED) {
TraceEvent(SevWarnAlways, "UnsupportedPKeyAlgorithm")
.suppressFor(10)
.detail("Algorithm", ::OBJ_nid2sn(EVP_PKEY_base_id(ptr.get())));
throw pkey_decode_error();
}
}
PrivateKey::PrivateKey(DerEncoded, StringRef der) {
@ -130,16 +213,19 @@ PrivateKey::PrivateKey(DerEncoded, StringRef der) {
if (!key)
traceAndThrowDecode("DerReadPrivateKeyError");
ptr = std::shared_ptr<EVP_PKEY>(key, &::EVP_PKEY_free);
if (algorithm() == PKeyAlgorithm::UNSUPPORTED) {
TraceEvent(SevWarnAlways, "UnsupportedPKeyAlgorithm")
.suppressFor(10)
.detail("Algorithm", ::OBJ_nid2sn(EVP_PKEY_base_id(ptr.get())));
throw pkey_decode_error();
}
}
PrivateKey::PrivateKey(std::shared_ptr<EVP_PKEY> key) : ptr(std::move(key)) {}
StringRef PrivateKey::writePem(Arena& arena) const {
ASSERT(ptr);
auto mem = ::BIO_new(::BIO_s_mem());
auto mem = AutoCPointer(::BIO_new(::BIO_s_mem()), &::BIO_free);
if (!mem)
traceAndThrowEncode("PrivateKeyPemWriteInitError");
auto memGuard = ScopeExit([mem]() { ::BIO_free(mem); });
if (1 != ::PEM_write_bio_PrivateKey(mem, nativeHandle(), nullptr, nullptr, 0, 0, nullptr))
traceAndThrowEncode("PrivateKeyDerPemWrite");
auto bioBuf = std::add_pointer_t<char>{};
@ -169,7 +255,40 @@ StringRef PrivateKey::writePublicKeyDer(Arena& arena) const {
return doWritePublicKeyDer(arena, nativeHandle());
}
PublicKey PrivateKey::toPublicKey() const {
PKeyAlgorithm PrivateKey::algorithm() const {
auto key = nativeHandle();
ASSERT(key);
return getPKeyAlgorithm(key);
}
std::string_view PrivateKey::algorithmName() const {
return pkeyAlgorithmName(this->algorithm());
}
StringRef PrivateKey::sign(Arena& arena, StringRef data, const EVP_MD& digest) const {
auto key = nativeHandle();
ASSERT(key);
auto mdctx = AutoCPointer(::EVP_MD_CTX_create(), &::EVP_MD_CTX_free);
if (!mdctx)
traceAndThrowDsa("PKeySignInitError");
if (1 != ::EVP_DigestSignInit(mdctx, nullptr, &digest, nullptr, nativeHandle()))
traceAndThrowDsa("PKeySignInitError");
if (1 != ::EVP_DigestSignUpdate(mdctx, data.begin(), data.size()))
traceAndThrowDsa("PKeySignUpdateError");
auto sigLen = size_t{};
if (1 != ::EVP_DigestSignFinal(mdctx, nullptr, &sigLen)) // assess the length first
traceAndThrowDsa("PKeySignFinalGetLengthError");
auto sigBuf = new (arena) uint8_t[sigLen];
if (1 != ::EVP_DigestSignFinal(mdctx, sigBuf, &sigLen))
traceAndThrowDsa("SignTokenFinalError");
return StringRef(sigBuf, sigLen);
}
bool PrivateKey::verify(StringRef data, StringRef signature, const EVP_MD& digest) const {
return doVerifyStringSignature(data, signature, digest, nativeHandle());
}
PublicKey PrivateKey::toPublic() const {
auto arena = Arena();
return PublicKey(DerEncoded{}, writePublicKeyDer(arena));
}

View File

@ -0,0 +1,55 @@
/*
* AutoCPointer.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_AUTO_C_POINTER_H
#define FLOW_AUTO_C_POINTER_H
#include <cstddef>
#include <memory>
/*
* Extend std::unique_ptr to apply scope semantics to C-style pointers with matching free functions
* Also, add implicit conversion to avoid calling get()s when invoking C functions
*
* e.g. EVP_PKEY_new() returns EVP_PKEY*, which needs to be freed by EVP_PKEY_free():
* AutoCPointer pkey(nullptr, &EVP_PKEY_free); // Null-initialized, won't invoke free
* pkey.reset(EVP_PKEY_new()); // Initialized. Freed when pkey goes out of scope
* ASSERT(!EVP_PKEY_is_a(pkey, "RSA")); // Implicit conversion from AutoCPointer<EVP_PKEY> to EVP_PKEY*
* pkey.release(); // Choose not to free (useful e.g. after passing as arg to set0 call,
* transferring ownership)
*/
template <class T, class R>
class AutoCPointer : protected std::unique_ptr<T, R (*)(T*)> {
using ParentType = std::unique_ptr<T, R (*)(T*)>;
public:
using DeleterType = R (*)(T*);
AutoCPointer(T* ptr, R (*deleter)(T*)) noexcept : ParentType(ptr, deleter) {}
AutoCPointer(std::nullptr_t, R (*deleter)(T*)) noexcept : ParentType(nullptr, deleter) {}
using ParentType::operator bool;
using ParentType::release;
using ParentType::reset;
operator T*() const { return ParentType::get(); }
};
#endif // FLOW_AUTO_C_POINTER_H

View File

@ -39,6 +39,8 @@ void printPrivateKey(FILE* out, StringRef privateKeyPem);
PrivateKey makeEcP256();
PrivateKey makeRsa2048Bit();
struct Asn1EntryRef {
// field must match one of ASN.1 object short/long names: e.g. "C", "countryName", "CN", "commonName",
// "subjectAltName", ...

View File

@ -22,9 +22,18 @@
#define FLOW_PKEY_H
#include <memory>
#include <string_view>
#include <openssl/evp.h>
#include "flow/Arena.h"
enum class PKeyAlgorithm {
UNSUPPORTED,
RSA,
EC,
};
std::string_view pkeyAlgorithmName(PKeyAlgorithm alg) noexcept;
struct PemEncoded {};
struct DerEncoded {};
@ -53,6 +62,14 @@ public:
// i2d_PUBKEY
StringRef writeDer(Arena& arena) const;
// EVP_PKEY_base_id()
PKeyAlgorithm algorithm() const;
std::string_view algorithmName() const;
// EVP_DigestVerify*
bool verify(StringRef data, StringRef signature, const EVP_MD& digest) const;
EVP_PKEY* nativeHandle() const noexcept { return ptr.get(); }
explicit operator bool() const noexcept { return static_cast<bool>(ptr); }
@ -70,9 +87,6 @@ public:
// d2i_AutoPrivateKey
PrivateKey(DerEncoded, StringRef der);
// Unsafe. Use when you're sure of unsafePtr's content & lifetime
PrivateKey(std::shared_ptr<EVP_PKEY> unsafePtr);
PrivateKey(const PrivateKey& other) noexcept = default;
PrivateKey& operator=(const PrivateKey& other) noexcept = default;
@ -89,11 +103,22 @@ public:
// i2d_PUBKEY
StringRef writePublicKeyDer(Arena& arena) const;
// EVP_PKEY_base_id()
PKeyAlgorithm algorithm() const;
std::string_view algorithmName() const;
EVP_PKEY* nativeHandle() const noexcept { return ptr.get(); }
explicit operator bool() const noexcept { return static_cast<bool>(ptr); }
// EVP_DigestSign*
StringRef sign(Arena& arena, StringRef data, const EVP_MD& digest) const;
// EVP_DigestVerify*
bool verify(StringRef data, StringRef signature, const EVP_MD& digest) const;
// Create a PublicKey independent of this key
PublicKey toPublicKey() const;
PublicKey toPublic() const;
};
#endif /*FLOW_PKEY_H*/

View File

@ -26,6 +26,7 @@
#include <stdarg.h>
#include <stdint.h>
#include <string>
#include <string_view>
#include <map>
#include <set>
#include <type_traits>
@ -289,6 +290,15 @@ struct TraceableString<std::string> {
}
};
template <>
struct TraceableString<std::string_view> {
static auto begin(const std::string_view& value) -> decltype(value.begin()) { return value.begin(); }
static bool atEnd(const std::string_view& value, decltype(value.begin()) iter) { return iter == value.end(); }
static std::string toString(const std::string_view& value) { return std::string(value); }
};
template <>
struct TraceableString<const char*> {
static const char* begin(const char* value) { return value; }
@ -387,6 +397,8 @@ template <size_t S>
struct Traceable<char[S]> : TraceableStringImpl<char[S]> {};
template <>
struct Traceable<std::string> : TraceableStringImpl<std::string> {};
template <>
struct Traceable<std::string_view> : TraceableStringImpl<std::string_view> {};
template <class T>
struct SpecialTraceMetricType

View File

@ -0,0 +1,88 @@
/*
* BenchVersionVector.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 "flow/Arena.h"
#include "benchmark/benchmark.h"
#include "fdbclient/VersionVector.h"
#include <cstdint>
struct TestContextArena {
Arena& _arena;
Arena& arena() { return _arena; }
ProtocolVersion protocolVersion() const { return g_network->protocolVersion(); }
uint8_t* allocate(size_t size) { return new (_arena) uint8_t[size]; }
};
static void bench_serializable_traits_version(benchmark::State& state) {
int tagCount = state.range(0);
Version version = 100000;
VersionVector serializedVV(version);
for (int i = 0; i < tagCount; i++) {
serializedVV.setVersion(Tag(0, i), ++version);
}
size_t size = 0;
VersionVector deserializedVV;
while (state.KeepRunning()) {
Standalone<StringRef> msg = ObjectWriter::toValue(serializedVV, Unversioned());
// Capture the serialized buffer size.
state.PauseTiming();
size = msg.size();
state.ResumeTiming();
ObjectReader rd(msg.begin(), Unversioned());
rd.deserialize(deserializedVV);
}
ASSERT(serializedVV.compare(deserializedVV));
state.SetItemsProcessed(static_cast<long>(state.iterations()));
state.counters.insert({ { "Tags", tagCount }, { "Size", size } });
}
static void bench_dynamic_size_traits_version(benchmark::State& state) {
Arena arena;
TestContextArena context{ arena };
int tagCount = state.range(0);
Version version = 100000;
VersionVector serializedVV(version);
for (int i = 0; i < tagCount; i++) {
serializedVV.setVersion(Tag(0, i), ++version);
}
size_t size = 0;
VersionVector deserializedVV;
while (state.KeepRunning()) {
size = dynamic_size_traits<VersionVector>::size(serializedVV, context);
uint8_t* buf = context.allocate(size);
dynamic_size_traits<VersionVector>::save(buf, serializedVV, context);
dynamic_size_traits<VersionVector>::load(buf, size, deserializedVV, context);
}
ASSERT(serializedVV.compare(deserializedVV));
state.SetItemsProcessed(static_cast<long>(state.iterations()));
state.counters.insert({ { "Tags", tagCount }, { "Size", size } });
}
BENCHMARK(bench_serializable_traits_version)->Ranges({ { 1 << 4, 1 << 10 } })->ReportAggregatesOnly(true);
BENCHMARK(bench_dynamic_size_traits_version)->Ranges({ { 1 << 4, 1 << 10 } })->ReportAggregatesOnly(true);

View File

@ -91,7 +91,7 @@ COPY website /tmp/website/
# Install FoundationDB Binaries
RUN for file in fdbserver fdbbackup fdbcli fdbmonitor; do \
curl --fail -Ls ${FDB_WEBSITE}/${FDB_VERSION}/$file.x86_64 -o $file; \
chmod u+x $file; \
chmod +x $file; \
mv $file /usr/bin; \
done