314 lines
12 KiB
C++
314 lines
12 KiB
C++
//
|
|
// Copyright (C) 2019 The Android Open Source Project
|
|
//
|
|
// 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 "credential_source.h"
|
|
|
|
#include <android-base/logging.h>
|
|
#include <android-base/strings.h>
|
|
#include <json/json.h>
|
|
#include <openssl/bio.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/pem.h>
|
|
|
|
#include "common/libs/utils/base64.h"
|
|
|
|
namespace cuttlefish {
|
|
namespace {
|
|
|
|
std::chrono::steady_clock::duration REFRESH_WINDOW =
|
|
std::chrono::minutes(2);
|
|
std::string REFRESH_URL = "http://metadata.google.internal/computeMetadata/"
|
|
"v1/instance/service-accounts/default/token";
|
|
|
|
} // namespace
|
|
|
|
GceMetadataCredentialSource::GceMetadataCredentialSource(CurlWrapper& curl)
|
|
: curl(curl) {
|
|
latest_credential = "";
|
|
expiration = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
std::string GceMetadataCredentialSource::Credential() {
|
|
if (expiration - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
|
|
RefreshCredential();
|
|
}
|
|
return latest_credential;
|
|
}
|
|
|
|
void GceMetadataCredentialSource::RefreshCredential() {
|
|
auto curl_response =
|
|
curl.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"});
|
|
const auto& json = curl_response.data;
|
|
if (!curl_response.HttpSuccess()) {
|
|
LOG(FATAL) << "Error fetching credentials. The server response was \""
|
|
<< json << "\", and code was " << curl_response.http_code;
|
|
}
|
|
CHECK(!json.isMember("error"))
|
|
<< "Response had \"error\" but had http success status. Received \""
|
|
<< json << "\"";
|
|
|
|
bool has_access_token = json.isMember("access_token");
|
|
bool has_expires_in = json.isMember("expires_in");
|
|
if (!has_access_token || !has_expires_in) {
|
|
LOG(FATAL) << "GCE credential was missing access_token or expires_in. "
|
|
<< "Full response was " << json << "";
|
|
}
|
|
|
|
expiration = std::chrono::steady_clock::now() +
|
|
std::chrono::seconds(json["expires_in"].asInt());
|
|
latest_credential = json["access_token"].asString();
|
|
}
|
|
|
|
std::unique_ptr<CredentialSource> GceMetadataCredentialSource::make(
|
|
CurlWrapper& curl) {
|
|
return std::unique_ptr<CredentialSource>(
|
|
new GceMetadataCredentialSource(curl));
|
|
}
|
|
|
|
FixedCredentialSource::FixedCredentialSource(const std::string& credential) {
|
|
this->credential = credential;
|
|
}
|
|
|
|
std::string FixedCredentialSource::Credential() {
|
|
return credential;
|
|
}
|
|
|
|
std::unique_ptr<CredentialSource> FixedCredentialSource::make(
|
|
const std::string& credential) {
|
|
return std::unique_ptr<CredentialSource>(new FixedCredentialSource(credential));
|
|
}
|
|
|
|
Result<RefreshCredentialSource> RefreshCredentialSource::FromOauth2ClientFile(
|
|
CurlWrapper& curl, std::istream& stream) {
|
|
Json::CharReaderBuilder builder;
|
|
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
|
Json::Value json;
|
|
std::string errorMessage;
|
|
CF_EXPECT(Json::parseFromStream(builder, stream, &json, &errorMessage),
|
|
"Failed to parse json: " << errorMessage);
|
|
CF_EXPECT(json.isMember("data"));
|
|
auto& data = json["data"];
|
|
CF_EXPECT(data.type() == Json::ValueType::arrayValue);
|
|
|
|
CF_EXPECT(data.size() == 1);
|
|
auto& data_first = data[0];
|
|
CF_EXPECT(data_first.type() == Json::ValueType::objectValue);
|
|
|
|
CF_EXPECT(data_first.isMember("credential"));
|
|
auto& credential = data_first["credential"];
|
|
CF_EXPECT(credential.type() == Json::ValueType::objectValue);
|
|
|
|
CF_EXPECT(credential.isMember("client_id"));
|
|
auto& client_id = credential["client_id"];
|
|
CF_EXPECT(client_id.type() == Json::ValueType::stringValue);
|
|
|
|
CF_EXPECT(credential.isMember("client_secret"));
|
|
auto& client_secret = credential["client_secret"];
|
|
CF_EXPECT(client_secret.type() == Json::ValueType::stringValue);
|
|
|
|
CF_EXPECT(credential.isMember("token_response"));
|
|
auto& token_response = credential["token_response"];
|
|
CF_EXPECT(token_response.type() == Json::ValueType::objectValue);
|
|
|
|
CF_EXPECT(token_response.isMember("refresh_token"));
|
|
auto& refresh_token = credential["refresh_token"];
|
|
CF_EXPECT(refresh_token.type() == Json::ValueType::stringValue);
|
|
|
|
return RefreshCredentialSource(curl, client_id.asString(),
|
|
client_secret.asString(),
|
|
refresh_token.asString());
|
|
}
|
|
|
|
RefreshCredentialSource::RefreshCredentialSource(
|
|
CurlWrapper& curl, const std::string& client_id,
|
|
const std::string& client_secret, const std::string& refresh_token)
|
|
: curl_(curl),
|
|
client_id_(client_id),
|
|
client_secret_(client_secret),
|
|
refresh_token_(refresh_token) {}
|
|
|
|
std::string RefreshCredentialSource::Credential() {
|
|
if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
|
|
UpdateLatestCredential();
|
|
}
|
|
return latest_credential_;
|
|
}
|
|
|
|
void RefreshCredentialSource::UpdateLatestCredential() {
|
|
std::vector<std::string> headers = {
|
|
"Content-Type: application/x-www-form-urlencoded"};
|
|
std::stringstream data;
|
|
data << "client_id=" << curl_.UrlEscape(client_id_) << "&";
|
|
data << "client_secret=" << curl_.UrlEscape(client_secret_) << "&";
|
|
data << "refresh_token=" << curl_.UrlEscape(refresh_token_) << "&";
|
|
data << "grant_type=refresh_token";
|
|
|
|
static constexpr char kUrl[] = "https://oauth2.googleapis.com/token";
|
|
auto response = curl_.PostToJson(kUrl, data.str(), headers);
|
|
CHECK(response.HttpSuccess()) << response.data;
|
|
auto& json = response.data;
|
|
|
|
CHECK(!json.isMember("error"))
|
|
<< "Response had \"error\" but had http success status. Received \""
|
|
<< json << "\"";
|
|
|
|
bool has_access_token = json.isMember("access_token");
|
|
bool has_expires_in = json.isMember("expires_in");
|
|
CHECK(has_access_token && has_expires_in)
|
|
<< "GCE credential was missing access_token or expires_in. "
|
|
<< "Full response was " << json << "";
|
|
|
|
expiration_ = std::chrono::steady_clock::now() +
|
|
std::chrono::seconds(json["expires_in"].asInt());
|
|
latest_credential_ = json["access_token"].asString();
|
|
}
|
|
|
|
static std::string CollectSslErrors() {
|
|
std::stringstream errors;
|
|
auto callback = [](const char* str, size_t len, void* stream) {
|
|
((std::stringstream*)stream)->write(str, len);
|
|
return 1; // success
|
|
};
|
|
ERR_print_errors_cb(callback, &errors);
|
|
return errors.str();
|
|
}
|
|
|
|
Result<ServiceAccountOauthCredentialSource>
|
|
ServiceAccountOauthCredentialSource::FromJson(CurlWrapper& curl,
|
|
const Json::Value& json,
|
|
const std::string& scope) {
|
|
ServiceAccountOauthCredentialSource source(curl);
|
|
source.scope_ = scope;
|
|
|
|
CF_EXPECT(json.isMember("client_email"));
|
|
CF_EXPECT(json["client_email"].type() == Json::ValueType::stringValue);
|
|
source.email_ = json["client_email"].asString();
|
|
|
|
CF_EXPECT(json.isMember("private_key"));
|
|
CF_EXPECT(json["private_key"].type() == Json::ValueType::stringValue);
|
|
std::string key_str = json["private_key"].asString();
|
|
|
|
std::unique_ptr<BIO, int (*)(BIO*)> bo(CF_EXPECT(BIO_new(BIO_s_mem())),
|
|
BIO_free);
|
|
CF_EXPECT(BIO_write(bo.get(), key_str.c_str(), key_str.size()) ==
|
|
key_str.size());
|
|
|
|
auto pkey = CF_EXPECT(PEM_read_bio_PrivateKey(bo.get(), nullptr, 0, 0),
|
|
CollectSslErrors());
|
|
source.private_key_.reset(pkey);
|
|
|
|
return source;
|
|
}
|
|
|
|
ServiceAccountOauthCredentialSource::ServiceAccountOauthCredentialSource(
|
|
CurlWrapper& curl)
|
|
: curl_(curl), private_key_(nullptr, EVP_PKEY_free) {}
|
|
|
|
static std::string Base64Url(const char* data, std::size_t size) {
|
|
std::string base64;
|
|
CHECK(EncodeBase64(data, size, &base64));
|
|
base64 = android::base::StringReplace(base64, "+", "-", /* all */ true);
|
|
base64 = android::base::StringReplace(base64, "/", "_", /* all */ true);
|
|
return base64;
|
|
}
|
|
|
|
static std::string JsonToBase64Url(const Json::Value& json) {
|
|
Json::StreamWriterBuilder factory;
|
|
auto serialized = Json::writeString(factory, json);
|
|
return Base64Url(serialized.c_str(), serialized.size());
|
|
}
|
|
|
|
static std::string CreateJwt(const std::string& email, const std::string& scope,
|
|
EVP_PKEY* private_key) {
|
|
using std::chrono::duration_cast;
|
|
using std::chrono::minutes;
|
|
using std::chrono::seconds;
|
|
using std::chrono::system_clock;
|
|
// https://developers.google.com/identity/protocols/oauth2/service-account
|
|
Json::Value header_json;
|
|
header_json["alg"] = "RS256";
|
|
header_json["typ"] = "JWT";
|
|
std::string header_str = JsonToBase64Url(header_json);
|
|
|
|
Json::Value claim_set_json;
|
|
claim_set_json["iss"] = email;
|
|
claim_set_json["scope"] = scope;
|
|
claim_set_json["aud"] = "https://oauth2.googleapis.com/token";
|
|
auto time = system_clock::now();
|
|
claim_set_json["iat"] =
|
|
(uint64_t)duration_cast<seconds>(time.time_since_epoch()).count();
|
|
auto exp = time + minutes(30);
|
|
claim_set_json["exp"] =
|
|
(uint64_t)duration_cast<seconds>(exp.time_since_epoch()).count();
|
|
std::string claim_set_str = JsonToBase64Url(claim_set_json);
|
|
|
|
std::string jwt_to_sign = header_str + "." + claim_set_str;
|
|
|
|
std::unique_ptr<EVP_MD_CTX, void (*)(EVP_MD_CTX*)> sign_ctx(
|
|
EVP_MD_CTX_create(), EVP_MD_CTX_free);
|
|
CHECK(EVP_DigestSignInit(sign_ctx.get(), nullptr, EVP_sha256(), nullptr,
|
|
private_key));
|
|
CHECK(EVP_DigestSignUpdate(sign_ctx.get(), jwt_to_sign.c_str(),
|
|
jwt_to_sign.size()));
|
|
size_t length;
|
|
CHECK(EVP_DigestSignFinal(sign_ctx.get(), nullptr, &length));
|
|
std::vector<uint8_t> sig_raw(length);
|
|
CHECK(EVP_DigestSignFinal(sign_ctx.get(), sig_raw.data(), &length));
|
|
|
|
return jwt_to_sign + "." + Base64Url((const char*)sig_raw.data(), length);
|
|
}
|
|
|
|
void ServiceAccountOauthCredentialSource::RefreshCredential() {
|
|
static constexpr char URL[] = "https://oauth2.googleapis.com/token";
|
|
static constexpr char GRANT[] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
std::stringstream content;
|
|
content << "grant_type=" << curl_.UrlEscape(GRANT) << "&";
|
|
auto jwt = CreateJwt(email_, scope_, private_key_.get());
|
|
content << "assertion=" << curl_.UrlEscape(jwt);
|
|
std::vector<std::string> headers = {
|
|
"Content-Type: application/x-www-form-urlencoded"};
|
|
auto curl_response = curl_.PostToJson(URL, content.str(), headers);
|
|
if (!curl_response.HttpSuccess()) {
|
|
LOG(FATAL) << "Error fetching credentials. The server response was \""
|
|
<< curl_response.data << "\", and code was "
|
|
<< curl_response.http_code;
|
|
}
|
|
Json::Value json = curl_response.data;
|
|
|
|
CHECK(!json.isMember("error"))
|
|
<< "Response had \"error\" but had http success status. Received \""
|
|
<< json << "\"";
|
|
|
|
bool has_access_token = json.isMember("access_token");
|
|
bool has_expires_in = json.isMember("expires_in");
|
|
if (!has_access_token || !has_expires_in) {
|
|
LOG(FATAL) << "GCE credential was missing access_token or expires_in. "
|
|
<< "Full response was " << json << "";
|
|
}
|
|
|
|
expiration_ = std::chrono::steady_clock::now() +
|
|
std::chrono::seconds(json["expires_in"].asInt());
|
|
latest_credential_ = json["access_token"].asString();
|
|
}
|
|
|
|
std::string ServiceAccountOauthCredentialSource::Credential() {
|
|
if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
|
|
RefreshCredential();
|
|
}
|
|
return latest_credential_;
|
|
}
|
|
|
|
} // namespace cuttlefish
|