• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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