1 //
2 // Copyright (C) 2019 The Android Open Source Project
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15
16 #include "credential_source.h"
17
18 #include <android-base/logging.h>
19 #include <android-base/strings.h>
20 #include <json/json.h>
21 #include <openssl/bio.h>
22 #include <openssl/evp.h>
23 #include <openssl/pem.h>
24
25 #include "common/libs/utils/base64.h"
26
27 namespace cuttlefish {
28 namespace {
29
30 std::chrono::steady_clock::duration REFRESH_WINDOW =
31 std::chrono::minutes(2);
32 std::string REFRESH_URL = "http://metadata.google.internal/computeMetadata/"
33 "v1/instance/service-accounts/default/token";
34
35 } // namespace
36
GceMetadataCredentialSource(CurlWrapper & curl)37 GceMetadataCredentialSource::GceMetadataCredentialSource(CurlWrapper& curl)
38 : curl(curl) {
39 latest_credential = "";
40 expiration = std::chrono::steady_clock::now();
41 }
42
Credential()43 std::string GceMetadataCredentialSource::Credential() {
44 if (expiration - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
45 RefreshCredential();
46 }
47 return latest_credential;
48 }
49
RefreshCredential()50 void GceMetadataCredentialSource::RefreshCredential() {
51 auto curl_response =
52 curl.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"});
53 const auto& json = curl_response.data;
54 if (!curl_response.HttpSuccess()) {
55 LOG(FATAL) << "Error fetching credentials. The server response was \""
56 << json << "\", and code was " << curl_response.http_code;
57 }
58 CHECK(!json.isMember("error"))
59 << "Response had \"error\" but had http success status. Received \""
60 << json << "\"";
61
62 bool has_access_token = json.isMember("access_token");
63 bool has_expires_in = json.isMember("expires_in");
64 if (!has_access_token || !has_expires_in) {
65 LOG(FATAL) << "GCE credential was missing access_token or expires_in. "
66 << "Full response was " << json << "";
67 }
68
69 expiration = std::chrono::steady_clock::now() +
70 std::chrono::seconds(json["expires_in"].asInt());
71 latest_credential = json["access_token"].asString();
72 }
73
make(CurlWrapper & curl)74 std::unique_ptr<CredentialSource> GceMetadataCredentialSource::make(
75 CurlWrapper& curl) {
76 return std::unique_ptr<CredentialSource>(
77 new GceMetadataCredentialSource(curl));
78 }
79
FixedCredentialSource(const std::string & credential)80 FixedCredentialSource::FixedCredentialSource(const std::string& credential) {
81 this->credential = credential;
82 }
83
Credential()84 std::string FixedCredentialSource::Credential() {
85 return credential;
86 }
87
make(const std::string & credential)88 std::unique_ptr<CredentialSource> FixedCredentialSource::make(
89 const std::string& credential) {
90 return std::unique_ptr<CredentialSource>(new FixedCredentialSource(credential));
91 }
92
FromOauth2ClientFile(CurlWrapper & curl,std::istream & stream)93 Result<RefreshCredentialSource> RefreshCredentialSource::FromOauth2ClientFile(
94 CurlWrapper& curl, std::istream& stream) {
95 Json::CharReaderBuilder builder;
96 std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
97 Json::Value json;
98 std::string errorMessage;
99 CF_EXPECT(Json::parseFromStream(builder, stream, &json, &errorMessage),
100 "Failed to parse json: " << errorMessage);
101 CF_EXPECT(json.isMember("data"));
102 auto& data = json["data"];
103 CF_EXPECT(data.type() == Json::ValueType::arrayValue);
104
105 CF_EXPECT(data.size() == 1);
106 auto& data_first = data[0];
107 CF_EXPECT(data_first.type() == Json::ValueType::objectValue);
108
109 CF_EXPECT(data_first.isMember("credential"));
110 auto& credential = data_first["credential"];
111 CF_EXPECT(credential.type() == Json::ValueType::objectValue);
112
113 CF_EXPECT(credential.isMember("client_id"));
114 auto& client_id = credential["client_id"];
115 CF_EXPECT(client_id.type() == Json::ValueType::stringValue);
116
117 CF_EXPECT(credential.isMember("client_secret"));
118 auto& client_secret = credential["client_secret"];
119 CF_EXPECT(client_secret.type() == Json::ValueType::stringValue);
120
121 CF_EXPECT(credential.isMember("token_response"));
122 auto& token_response = credential["token_response"];
123 CF_EXPECT(token_response.type() == Json::ValueType::objectValue);
124
125 CF_EXPECT(token_response.isMember("refresh_token"));
126 auto& refresh_token = credential["refresh_token"];
127 CF_EXPECT(refresh_token.type() == Json::ValueType::stringValue);
128
129 return RefreshCredentialSource(curl, client_id.asString(),
130 client_secret.asString(),
131 refresh_token.asString());
132 }
133
RefreshCredentialSource(CurlWrapper & curl,const std::string & client_id,const std::string & client_secret,const std::string & refresh_token)134 RefreshCredentialSource::RefreshCredentialSource(
135 CurlWrapper& curl, const std::string& client_id,
136 const std::string& client_secret, const std::string& refresh_token)
137 : curl_(curl),
138 client_id_(client_id),
139 client_secret_(client_secret),
140 refresh_token_(refresh_token) {}
141
Credential()142 std::string RefreshCredentialSource::Credential() {
143 if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
144 UpdateLatestCredential();
145 }
146 return latest_credential_;
147 }
148
UpdateLatestCredential()149 void RefreshCredentialSource::UpdateLatestCredential() {
150 std::vector<std::string> headers = {
151 "Content-Type: application/x-www-form-urlencoded"};
152 std::stringstream data;
153 data << "client_id=" << curl_.UrlEscape(client_id_) << "&";
154 data << "client_secret=" << curl_.UrlEscape(client_secret_) << "&";
155 data << "refresh_token=" << curl_.UrlEscape(refresh_token_) << "&";
156 data << "grant_type=refresh_token";
157
158 static constexpr char kUrl[] = "https://oauth2.googleapis.com/token";
159 auto response = curl_.PostToJson(kUrl, data.str(), headers);
160 CHECK(response.HttpSuccess()) << response.data;
161 auto& json = response.data;
162
163 CHECK(!json.isMember("error"))
164 << "Response had \"error\" but had http success status. Received \""
165 << json << "\"";
166
167 bool has_access_token = json.isMember("access_token");
168 bool has_expires_in = json.isMember("expires_in");
169 CHECK(has_access_token && has_expires_in)
170 << "GCE credential was missing access_token or expires_in. "
171 << "Full response was " << json << "";
172
173 expiration_ = std::chrono::steady_clock::now() +
174 std::chrono::seconds(json["expires_in"].asInt());
175 latest_credential_ = json["access_token"].asString();
176 }
177
CollectSslErrors()178 static std::string CollectSslErrors() {
179 std::stringstream errors;
180 auto callback = [](const char* str, size_t len, void* stream) {
181 ((std::stringstream*)stream)->write(str, len);
182 return 1; // success
183 };
184 ERR_print_errors_cb(callback, &errors);
185 return errors.str();
186 }
187
188 Result<ServiceAccountOauthCredentialSource>
FromJson(CurlWrapper & curl,const Json::Value & json,const std::string & scope)189 ServiceAccountOauthCredentialSource::FromJson(CurlWrapper& curl,
190 const Json::Value& json,
191 const std::string& scope) {
192 ServiceAccountOauthCredentialSource source(curl);
193 source.scope_ = scope;
194
195 CF_EXPECT(json.isMember("client_email"));
196 CF_EXPECT(json["client_email"].type() == Json::ValueType::stringValue);
197 source.email_ = json["client_email"].asString();
198
199 CF_EXPECT(json.isMember("private_key"));
200 CF_EXPECT(json["private_key"].type() == Json::ValueType::stringValue);
201 std::string key_str = json["private_key"].asString();
202
203 std::unique_ptr<BIO, int (*)(BIO*)> bo(CF_EXPECT(BIO_new(BIO_s_mem())),
204 BIO_free);
205 CF_EXPECT(BIO_write(bo.get(), key_str.c_str(), key_str.size()) ==
206 key_str.size());
207
208 auto pkey = CF_EXPECT(PEM_read_bio_PrivateKey(bo.get(), nullptr, 0, 0),
209 CollectSslErrors());
210 source.private_key_.reset(pkey);
211
212 return source;
213 }
214
ServiceAccountOauthCredentialSource(CurlWrapper & curl)215 ServiceAccountOauthCredentialSource::ServiceAccountOauthCredentialSource(
216 CurlWrapper& curl)
217 : curl_(curl), private_key_(nullptr, EVP_PKEY_free) {}
218
Base64Url(const char * data,std::size_t size)219 static std::string Base64Url(const char* data, std::size_t size) {
220 std::string base64;
221 CHECK(EncodeBase64(data, size, &base64));
222 base64 = android::base::StringReplace(base64, "+", "-", /* all */ true);
223 base64 = android::base::StringReplace(base64, "/", "_", /* all */ true);
224 return base64;
225 }
226
JsonToBase64Url(const Json::Value & json)227 static std::string JsonToBase64Url(const Json::Value& json) {
228 Json::StreamWriterBuilder factory;
229 auto serialized = Json::writeString(factory, json);
230 return Base64Url(serialized.c_str(), serialized.size());
231 }
232
CreateJwt(const std::string & email,const std::string & scope,EVP_PKEY * private_key)233 static std::string CreateJwt(const std::string& email, const std::string& scope,
234 EVP_PKEY* private_key) {
235 using std::chrono::duration_cast;
236 using std::chrono::minutes;
237 using std::chrono::seconds;
238 using std::chrono::system_clock;
239 // https://developers.google.com/identity/protocols/oauth2/service-account
240 Json::Value header_json;
241 header_json["alg"] = "RS256";
242 header_json["typ"] = "JWT";
243 std::string header_str = JsonToBase64Url(header_json);
244
245 Json::Value claim_set_json;
246 claim_set_json["iss"] = email;
247 claim_set_json["scope"] = scope;
248 claim_set_json["aud"] = "https://oauth2.googleapis.com/token";
249 auto time = system_clock::now();
250 claim_set_json["iat"] =
251 (uint64_t)duration_cast<seconds>(time.time_since_epoch()).count();
252 auto exp = time + minutes(30);
253 claim_set_json["exp"] =
254 (uint64_t)duration_cast<seconds>(exp.time_since_epoch()).count();
255 std::string claim_set_str = JsonToBase64Url(claim_set_json);
256
257 std::string jwt_to_sign = header_str + "." + claim_set_str;
258
259 std::unique_ptr<EVP_MD_CTX, void (*)(EVP_MD_CTX*)> sign_ctx(
260 EVP_MD_CTX_create(), EVP_MD_CTX_free);
261 CHECK(EVP_DigestSignInit(sign_ctx.get(), nullptr, EVP_sha256(), nullptr,
262 private_key));
263 CHECK(EVP_DigestSignUpdate(sign_ctx.get(), jwt_to_sign.c_str(),
264 jwt_to_sign.size()));
265 size_t length;
266 CHECK(EVP_DigestSignFinal(sign_ctx.get(), nullptr, &length));
267 std::vector<uint8_t> sig_raw(length);
268 CHECK(EVP_DigestSignFinal(sign_ctx.get(), sig_raw.data(), &length));
269
270 return jwt_to_sign + "." + Base64Url((const char*)sig_raw.data(), length);
271 }
272
RefreshCredential()273 void ServiceAccountOauthCredentialSource::RefreshCredential() {
274 static constexpr char URL[] = "https://oauth2.googleapis.com/token";
275 static constexpr char GRANT[] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
276 std::stringstream content;
277 content << "grant_type=" << curl_.UrlEscape(GRANT) << "&";
278 auto jwt = CreateJwt(email_, scope_, private_key_.get());
279 content << "assertion=" << curl_.UrlEscape(jwt);
280 std::vector<std::string> headers = {
281 "Content-Type: application/x-www-form-urlencoded"};
282 auto curl_response = curl_.PostToJson(URL, content.str(), headers);
283 if (!curl_response.HttpSuccess()) {
284 LOG(FATAL) << "Error fetching credentials. The server response was \""
285 << curl_response.data << "\", and code was "
286 << curl_response.http_code;
287 }
288 Json::Value json = curl_response.data;
289
290 CHECK(!json.isMember("error"))
291 << "Response had \"error\" but had http success status. Received \""
292 << json << "\"";
293
294 bool has_access_token = json.isMember("access_token");
295 bool has_expires_in = json.isMember("expires_in");
296 if (!has_access_token || !has_expires_in) {
297 LOG(FATAL) << "GCE credential was missing access_token or expires_in. "
298 << "Full response was " << json << "";
299 }
300
301 expiration_ = std::chrono::steady_clock::now() +
302 std::chrono::seconds(json["expires_in"].asInt());
303 latest_credential_ = json["access_token"].asString();
304 }
305
Credential()306 std::string ServiceAccountOauthCredentialSource::Credential() {
307 if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
308 RefreshCredential();
309 }
310 return latest_credential_;
311 }
312
313 } // namespace cuttlefish
314