#include "crypto/crypto_accel.h"
#include "crypto/crypto_plugin.h"
#include "rgw/rgw_kms.h"
+#include "rapidjson/document.h"
+#include "rapidjson/writer.h"
+#include "rapidjson/error/error.h"
+#include "rapidjson/error/en.h"
+#include <unicode/normalizer2.h> // libicu
#include <openssl/evp.h>
using namespace rgw;
+template<typename M>
+class canonical_char_sorter {
+private:
+ const icu::Normalizer2* normalizer;
+ CephContext *cct;
+
+public:
+ canonical_char_sorter(CephContext *cct) : cct(cct) {
+ UErrorCode status = U_ZERO_ERROR;
+ normalizer = icu::Normalizer2::getNFCInstance(status);
+ if (U_FAILURE(status)) {
+lderr(cct) << "ERROR: can't get nfc instance, error = " << status << dendl;
+ normalizer = 0;
+ }
+ }
+ bool compare_helper (const M *, const M *);
+ bool make_string_canonical(rapidjson::Value &,
+ rapidjson::Document::AllocatorType&);
+};
+
+template<typename M>
+bool
+canonical_char_sorter<M>::compare_helper (const M*a, const M*b)
+{
+ UErrorCode status = U_ZERO_ERROR;
+ const std::string as{a->name.GetString(), a->name.GetStringLength()},
+ bs{b->name.GetString(), b->name.GetStringLength()};
+ icu::UnicodeString aw{icu::UnicodeString::fromUTF8(as)}, bw{icu::UnicodeString::fromUTF8(bs)};
+ int32_t afl{ aw.countChar32()}, bfl{bw.countChar32()};
+ std::u32string af, bf;
+ af.resize(afl); bf.resize(bfl);
+ auto *astr{af.c_str()}, *bstr{bf.c_str()};
+ aw.toUTF32((int32_t*)astr, afl, status);
+ bw.toUTF32((int32_t*)bstr, bfl, status);
+ bool r{af < bf};
+ return r;
+}
+
+template<typename M>
+bool
+canonical_char_sorter<M>::make_string_canonical (rapidjson::Value &v, rapidjson::Document::AllocatorType&a)
+{
+ UErrorCode status = U_ZERO_ERROR;
+ const std::string as{v.GetString(), v.GetStringLength()};
+
+ if (!normalizer)
+ return false;
+ const icu::UnicodeString aw{icu::UnicodeString::fromUTF8(as)};
+ icu::UnicodeString an{normalizer->normalize(aw, status)};
+ if (U_FAILURE(status)) {
+ ldout(cct, 5) << "conversion error; code=" << status <<
+ " on string " << as << dendl;
+ return false;
+ }
+ std::string ans;
+ an.toUTF8String(ans);
+ v.SetString(ans.c_str(), ans.length(), a);
+ return true;
+}
+
+typedef
+rapidjson::GenericMember<rapidjson::UTF8<>, rapidjson::MemoryPoolAllocator<> >
+MyMember;
+
+template<typename H>
+bool
+sort_and_write(rapidjson::Value &d, H &writer, canonical_char_sorter<MyMember>& ccs)
+{
+ bool r;
+ switch(d.GetType()) {
+ case rapidjson::kObjectType: {
+ struct comparer {
+ canonical_char_sorter<MyMember> &r;
+ comparer(canonical_char_sorter<MyMember> &r) : r(r) {};
+ bool operator()(const MyMember*a, const MyMember*b) {
+ return r.compare_helper(a,b);
+ }
+ } cmp_functor{ccs};
+ if (!(r = writer.StartObject()))
+ break;
+ const auto &o{d.GetObject()};
+ auto b{o.begin()},e{o.end()};
+ std::vector<MyMember*> q;
+ for (auto &m: d.GetObject())
+ q.push_back(&m);
+ std::sort(q.begin(), q.end(), cmp_functor);
+ for (auto m: q) {
+ assert(m->name.IsString());
+ if (!(r = writer.Key(m->name.GetString(), m->name.GetStringLength())))
+ goto Done;
+ if (!(r = sort_and_write(m->value, writer, ccs)))
+ goto Done;
+ }
+ r = writer.EndObject();
+ break; }
+ case rapidjson::kArrayType:
+ if (!(r = writer.StartArray()))
+ break;
+ for (auto &v: d.GetArray()) {
+ if (!(r = sort_and_write(v, writer, ccs)))
+ goto Done;
+ }
+ r = writer.EndArray();
+ break;
+ default:
+ r = d.Accept(writer);
+ break;
+ }
+Done:
+ return r;
+}
+
+enum struct mec_option {
+empty = 0, number_ok = 1
+};
+
+enum struct mec_error {
+success = 0, conversion, number
+};
+
+mec_error
+make_everything_canonical(rapidjson::Value &d, rapidjson::Document::AllocatorType&a, canonical_char_sorter<MyMember>& ccs, mec_option f = mec_option::empty )
+{
+ mec_error r;
+ switch(d.GetType()) {
+ case rapidjson::kObjectType:
+ for (auto &m: d.GetObject()) {
+ assert(m.name.IsString());
+ if (!ccs.make_string_canonical(m.name, a)) {
+ r = mec_error::conversion;
+ goto Error;
+ }
+ if ((r = make_everything_canonical(m.value, a, ccs, f)) != mec_error::success)
+ goto Error;
+ }
+ break;
+ case rapidjson::kArrayType:
+ for (auto &v: d.GetArray()) {
+ if ((r = make_everything_canonical(v, a, ccs, f)) != mec_error::success)
+ goto Error;
+ }
+ break;
+ case rapidjson::kStringType:
+ if (!ccs.make_string_canonical(d, a)) {
+ r = mec_error::conversion;
+ goto Error;
+ }
+ break;
+ case rapidjson::kNumberType:
+ if (static_cast<int>(f) & static_cast<int>(mec_option::number_ok))
+ break;
+ r = mec_error::number;
+ goto Error;
+ default:
+ break;
+ }
+ r = mec_error::success;
+Error:
+ return r;
+}
+
+bool
+add_object_to_context(rgw_obj &obj, rapidjson::Document &d)
+{
+ ARN a{obj};
+ const char aws_s3_arn[] { "aws:s3:arn" };
+ std::string as{a.to_string()};
+ rapidjson::Document::AllocatorType &allocator { d.GetAllocator() };
+ rapidjson::Value name, val;
+
+ if (!d.IsObject())
+ return false;
+ if (d.HasMember(aws_s3_arn))
+ return true;
+ val.SetString(as.c_str(), as.length(), allocator);
+ name.SetString(aws_s3_arn, sizeof aws_s3_arn - 1, allocator);
+ d.AddMember(name, val, allocator);
+ return true;
+}
+
+static inline const std::string &
+get_tenant_or_id(req_state *s)
+{
+ const std::string &tenant{ s->user->get_tenant() };
+ if (!tenant.empty()) return tenant;
+ return s->user->get_id().id;
+}
+
+int
+make_canonical_context(struct req_state *s,
+ std::string_view &context,
+ std::string &cooked_context)
+{
+ rapidjson::Document d;
+ bool b = false;
+mec_option options {
+//mec_option::number_ok : SEE BOTTOM OF FILE
+mec_option::empty };
+ rgw_obj obj;
+ std::ostringstream oss;
+ canonical_char_sorter<MyMember> ccs{s->cct};
+
+ obj.bucket.tenant = get_tenant_or_id(s);
+ obj.bucket.name = s->bucket->get_name();
+ obj.key.name = s->object->get_name();
+ std::string iline;
+ rapidjson::Document::AllocatorType &allocator { d.GetAllocator() };
+
+ try {
+ iline = rgw::from_base64(context);
+ } catch (const std::exception& e) {
+ oss << "bad context: " << e.what();
+ s->err.message = oss.str();
+ return -ERR_INVALID_REQUEST;
+ }
+ rapidjson::StringStream isw(iline.c_str());
+ if (!iline.length())
+ d.SetObject();
+// else if (qflag) SEE BOTTOM OF FILE
+// d.ParseStream<rapidjson::kParseNumbersAsStringsFlag>(isw);
+ else
+ d.ParseStream<rapidjson::kParseFullPrecisionFlag>(isw);
+ if (isw.Tell() != iline.length()) {
+ oss << "bad context: did not consume all of input: @ "
+ << isw.Tell();
+ s->err.message = oss.str();
+ return -ERR_INVALID_REQUEST;
+ }
+ if (d.HasParseError()) {
+ oss << "bad context: parse error: @ " << d.GetErrorOffset()
+ << " " << rapidjson::GetParseError_En(d.GetParseError());
+ s->err.message = oss.str();
+ return -ERR_INVALID_REQUEST;
+ }
+ rapidjson::StringBuffer buf;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buf);
+ if (!add_object_to_context(obj, d)) {
+ lderr(s->cct) << "ERROR: can't add default value to context" << dendl;
+ s->err.message = "context: internal error adding defaults";
+ return -ERR_INVALID_REQUEST;
+ }
+ b = make_everything_canonical(d, allocator, ccs, options) == mec_error::success;
+ if (!b) {
+ lderr(s->cct) << "ERROR: can't make canonical json <"
+ << context << ">" << dendl;
+ s->err.message = "context: can't make canonical";
+ return -ERR_INVALID_REQUEST;
+ }
+ b = sort_and_write(d, writer, ccs);
+ if (!b) {
+ ldout(s->cct, 5) << "format error <" << context
+ << ">: partial.results=" << buf.GetString() << dendl;
+ s->err.message = "unable to reformat json";
+ return -ERR_INVALID_REQUEST;
+ }
+ cooked_context = rgw::to_base64(buf.GetString());
+ return 0;
+}
+
+
CryptoAccelRef get_crypto_accel(CephContext *cct)
{
CryptoAccelRef ca_impl = nullptr;
return std::string(random, sizeof(random));
}
-static inline void set_attr(map<string, bufferlist>& attrs,
- const char* key,
- std::string_view value)
-{
- bufferlist bl;
- bl.append(value.data(), value.size());
- attrs[key] = std::move(bl);
-}
-
-static inline std::string get_str_attribute(map<string, bufferlist>& attrs,
- const char *name)
-{
- auto iter = attrs.find(name);
- if (iter == attrs.end()) {
- return {};
- }
- return iter->second.to_str();
-}
-
typedef enum {
X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM=0,
X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY,
X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5,
X_AMZ_SERVER_SIDE_ENCRYPTION,
X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID,
+ X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT,
X_AMZ_SERVER_SIDE_ENCRYPTION_LAST
} crypt_option_e;
{"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5", "x-amz-server-side-encryption-customer-key-md5"},
{"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION", "x-amz-server-side-encryption"},
{"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID", "x-amz-server-side-encryption-aws-kms-key-id"},
+ {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT", "x-amz-server-side-encryption-context"},
};
static std::string_view get_crypt_attribute(
}
if (req_sse == "aws:kms") {
+ std::string_view context =
+ get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT);
+ std::string cooked_context;
+ if ((res = make_canonical_context(s, context, cooked_context)))
+ return res;
std::string_view key_id =
get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID);
if (key_id.empty()) {
}
/* try to retrieve actual key */
std::string key_selector = create_random_key_selector(s->cct);
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-KMS");
+ set_attr(attrs, RGW_ATTR_CRYPT_KEYID, key_id);
+ set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector);
+ set_attr(attrs, RGW_ATTR_CRYPT_CONTEXT, cooked_context);
std::string actual_key;
- res = get_actual_key_from_kms(s->cct, key_id, key_selector, actual_key);
+ res = make_actual_key_from_kms(s->cct, attrs, actual_key);
if (res != 0) {
ldout(s->cct, 5) << "ERROR: failed to retrieve actual key from key_id: " << key_id << dendl;
s->err.message = "Failed to retrieve the actual key, kms-keyid: " + std::string(key_id);
s->err.message = "KMS provided an invalid key for the given kms-keyid.";
return -ERR_INVALID_ACCESS_KEY;
}
- set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-KMS");
- set_attr(attrs, RGW_ATTR_CRYPT_KEYID, key_id);
- set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector);
if (block_crypt) {
auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct));
aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
*block_crypt = std::move(aes);
}
- actual_key.replace(0, actual_key.length(), actual_key.length(), '\000');
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
crypt_http_responses["x-amz-server-side-encryption"] = "aws:kms";
crypt_http_responses["x-amz-server-side-encryption-aws-kms-key-id"] = std::string(key_id);
+ crypt_http_responses["x-amz-server-side-encryption-context"] = std::move(cooked_context);
return 0;
} else if (req_sse == "AES256") {
/* if a default encryption key was provided, we will use it for SSE-S3 */
}
/* try to retrieve actual key */
std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID);
- std::string key_selector = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYSEL);
std::string actual_key;
- res = get_actual_key_from_kms(s->cct, key_id, key_selector, actual_key);
+ res = reconstitute_actual_key_from_kms(s->cct, attrs, actual_key);
if (res != 0) {
ldout(s->cct, 10) << "ERROR: failed to retrieve actual key from key_id: " << key_id << dendl;
s->err.message = "Failed to retrieve the actual key, kms-keyid: " + key_id;
/*no decryption*/
return 0;
}
+
+/*********************************************************************
+* "BOTTOM OF FILE"
+* I've left some commented out lines above. They are there for
+* a reason, which I will explain. The "canonical" json constructed
+* by the code above as a crypto context must take a json object and
+* turn it into a unique determinstic fixed form. For most json
+* types this is easy. The hardest problem that is handled above is
+* detailing with unicode strings; they must be turned into
+* NFC form and sorted in a fixed order. Numbers, however,
+* are another story. Json makes no distinction between integers
+* and floating point, and both types have their problems.
+* Integers can overflow, so very large numbers are a problem.
+* Floating point is even worse; not all floating point numbers
+* can be represented accurately in c++ data types, and there
+* are many quirks regarding how overflow, underflow, and loss
+* of significance are handled.
+*
+* In this version of the code, I took the simplest answer, I
+* reject all numbers altogether. This is not ideal, but it's
+* the only choice that is guaranteed to be future compatible.
+* AWS S3 does not guarantee to support numbers at all; but it
+* actually converts all numbers into strings right off.
+* This has the interesting property that 7 and 007 are different,
+* but that 007 and "007" are the same. I would rather
+* treat numbers as a string of digits and have logic
+* to produce the "most compact" equivalent form. This can
+* fix all the overflow/underflow problems, but it requires
+* fixing the json parser part, and I put that problem off.
+*
+* The commented code above indicates places in this code that
+* will need to be revised depending on future work in this area.
+* Removing those comments makes that work harder.
+* February 25, 2021
+*********************************************************************/
}
}
+typedef std::map<std::string, std::string> EngineParmMap;
class VaultSecretEngine: public SecretEngine {
return res;
}
- int send_request(std::string_view key_id, JSONParser* parser) override
+ int send_request(std::string_view key_id, JSONParser* parser)
{
bufferlist secret_bl;
int res;
this->cct = cct;
}
- virtual ~VaultSecretEngine(){}
+// virtual ~VaultSecretEngine(){}
};
class TransitSecretEngine: public VaultSecretEngine {
private:
+ EngineParmMap parms;
+
int get_key_version(std::string_view key_id, string& version)
{
size_t pos = 0;
}
public:
- TransitSecretEngine(CephContext *cct): VaultSecretEngine(cct){ }
+ TransitSecretEngine(CephContext *cct, EngineParmMap parms): VaultSecretEngine(cct), parms(parms) {
+ for (auto& e: parms) {
+ lderr(cct) << "ERROR: vault transit secrets engine : parameter "
+ << e.first << "=" << e.second << " ignored" << dendl;
+ }
+ }
int get_key(std::string_view key_id, std::string& actual_key)
{
public:
- KvSecretEngine(CephContext *cct): VaultSecretEngine(cct){ }
+ KvSecretEngine(CephContext *cct, EngineParmMap parms): VaultSecretEngine(cct){
+ if (!parms.empty()) {
+ lderr(cct) << "ERROR: vault kv secrets engine takes no parameters (ignoring them)" << dendl;
+ }
+ }
virtual ~KvSecretEngine(){}
protected:
CephContext *cct;
- int send_request(std::string_view key_id, JSONParser* parser) override
- {
- return -EINVAL;
- }
-
- int decode_secret(JSONObj* json_obj, std::string& actual_key){
- return -EINVAL;
- }
+// int send_request(std::string_view key_id, JSONParser* parser) override
+// {
+// return -EINVAL;
+// }
+//
+// int decode_secret(JSONObj* json_obj, std::string& actual_key){
+// return -EINVAL;
+// }
public:
}
+std::string config_to_engine_and_parms(CephContext *cct,
+ const char* which,
+ std::string& secret_engine_str,
+ EngineParmMap& secret_engine_parms)
+{
+ std::ostringstream oss;
+ std::vector<std::string> secret_engine_v;
+ std::string secret_engine;
+
+ get_str_vec(secret_engine_str, " ", secret_engine_v);
+
+ cct->_conf.early_expand_meta(secret_engine_str, &oss);
+ auto meta_errors {oss.str()};
+ if (meta_errors.length()) {
+ meta_errors.erase(meta_errors.find_last_not_of("\n")+1);
+ lderr(cct) << "ERROR: while expanding " << which << ": "
+ << meta_errors << dendl;
+ }
+ for (auto& e: secret_engine_v) {
+ if (!secret_engine.length()) {
+ secret_engine = std::move(e);
+ continue;
+ }
+ auto p { e.find('=') };
+ if (p == std::string::npos) {
+ secret_engine_parms.emplace(std::move(e), "");
+ continue;
+ }
+ std::string key{ e.substr(0,p) };
+ std::string val{ e.substr(p+1) };
+ secret_engine_parms.emplace(std::move(key), std::move(val));
+ }
+ return secret_engine;
+}
+
+
static int get_actual_key_from_vault(CephContext *cct,
- std::string_view key_id,
+ map<string, bufferlist>& attrs,
std::string& actual_key)
{
- std::string secret_engine = cct->_conf->rgw_crypt_vault_secret_engine;
+ std::string secret_engine_str = cct->_conf->rgw_crypt_vault_secret_engine;
+ std::string context = get_str_attribute(attrs, RGW_ATTR_CRYPT_CONTEXT);
+ EngineParmMap secret_engine_parms;
+ auto secret_engine { config_to_engine_and_parms(
+ cct, "rgw_crypt_vault_secret_engine",
+ secret_engine_str, secret_engine_parms) };
ldout(cct, 20) << "Vault authentication method: " << cct->_conf->rgw_crypt_vault_auth << dendl;
ldout(cct, 20) << "Vault Secrets Engine: " << secret_engine << dendl;
+lderr(cct) << "TEMP cooked_context<" << context << ">" << dendl;
if (RGW_SSE_KMS_VAULT_SE_KV == secret_engine){
- KvSecretEngine engine(cct);
+ std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID);
+ KvSecretEngine engine(cct, std::move(secret_engine_parms));
return engine.get_key(key_id, actual_key);
}
else if (RGW_SSE_KMS_VAULT_SE_TRANSIT == secret_engine){
- TransitSecretEngine engine(cct);
+ std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID);
+ TransitSecretEngine engine(cct, std::move(secret_engine_parms));
return engine.get_key(key_id, actual_key);
}
else{
}
+static int make_actual_key_from_vault(CephContext *cct,
+ map<string, bufferlist>& attrs,
+ std::string& actual_key)
+{
+ return get_actual_key_from_vault(cct, attrs, actual_key);
+}
+
+
+static int reconstitute_actual_key_from_vault(CephContext *cct,
+ map<string, bufferlist>& attrs,
+ std::string& actual_key)
+{
+ return get_actual_key_from_vault(cct, attrs, actual_key);
+}
+
+
static int get_actual_key_from_kmip(CephContext *cct,
std::string_view key_id,
std::string& actual_key)
}
-int get_actual_key_from_kms(CephContext *cct,
- std::string_view key_id,
- std::string_view key_selector,
+int reconstitute_actual_key_from_kms(CephContext *cct,
+ map<string, bufferlist>& attrs,
std::string& actual_key)
{
- std::string kms_backend;
+ std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID);
+ std::string kms_backend { cct->_conf->rgw_crypt_s3_kms_backend };
- kms_backend = cct->_conf->rgw_crypt_s3_kms_backend;
ldout(cct, 20) << "Getting KMS encryption key for key " << key_id << dendl;
ldout(cct, 20) << "SSE-KMS backend is " << kms_backend << dendl;
- if (RGW_SSE_KMS_BACKEND_BARBICAN == kms_backend)
+ if (RGW_SSE_KMS_BACKEND_BARBICAN == kms_backend) {
return get_actual_key_from_barbican(cct, key_id, actual_key);
+ }
- if (RGW_SSE_KMS_BACKEND_VAULT == kms_backend)
- return get_actual_key_from_vault(cct, key_id, actual_key);
+ if (RGW_SSE_KMS_BACKEND_VAULT == kms_backend) {
+ return reconstitute_actual_key_from_vault(cct, attrs, actual_key);
+ }
- if (RGW_SSE_KMS_BACKEND_KMIP == kms_backend)
+ if (RGW_SSE_KMS_BACKEND_KMIP == kms_backend) {
return get_actual_key_from_kmip(cct, key_id, actual_key);
+ }
- if (RGW_SSE_KMS_BACKEND_TESTING == kms_backend)
+ if (RGW_SSE_KMS_BACKEND_TESTING == kms_backend) {
+ std::string key_selector = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYSEL);
return get_actual_key_from_conf(cct, key_id, key_selector, actual_key);
+ }
ldout(cct, 0) << "ERROR: Invalid rgw_crypt_s3_kms_backend: " << kms_backend << dendl;
return -EINVAL;
}
+
+int make_actual_key_from_kms(CephContext *cct,
+ map<string, bufferlist>& attrs,
+ std::string& actual_key)
+{
+ std::string kms_backend { cct->_conf->rgw_crypt_s3_kms_backend };
+ if (RGW_SSE_KMS_BACKEND_VAULT == kms_backend)
+ return make_actual_key_from_vault(cct, attrs, actual_key);
+ return reconstitute_actual_key_from_kms(cct, attrs, actual_key);
+}