• 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(HttpClient & http_client)37 GceMetadataCredentialSource::GceMetadataCredentialSource(
38     HttpClient& http_client)
39     : http_client(http_client) {
40   latest_credential = "";
41   expiration = std::chrono::steady_clock::now();
42 }
43 
Credential()44 Result<std::string> GceMetadataCredentialSource::Credential() {
45   if (expiration - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
46     CF_EXPECT(RefreshCredential());
47   }
48   return latest_credential;
49 }
50 
RefreshCredential()51 Result<void> GceMetadataCredentialSource::RefreshCredential() {
52   auto response = CF_EXPECT(
53       http_client.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"}));
54   const auto& json = response.data;
55   CF_EXPECT(response.HttpSuccess(),
56             "Error fetching credentials. The server response was \""
57                 << json << "\", and code was " << response.http_code);
58   CF_EXPECT(!json.isMember("error"),
59             "Response had \"error\" but had http success status. Received \""
60                 << json << "\"");
61 
62   CF_EXPECT(json.isMember("access_token") && json.isMember("expires_in"),
63             "GCE credential was missing access_token or expires_in. "
64                 << "Full response was " << json << "");
65 
66   expiration = std::chrono::steady_clock::now() +
67                std::chrono::seconds(json["expires_in"].asInt());
68   latest_credential = json["access_token"].asString();
69   return {};
70 }
71 
make(HttpClient & http_client)72 std::unique_ptr<CredentialSource> GceMetadataCredentialSource::make(
73     HttpClient& http_client) {
74   return std::unique_ptr<CredentialSource>(
75       new GceMetadataCredentialSource(http_client));
76 }
77 
FixedCredentialSource(const std::string & credential)78 FixedCredentialSource::FixedCredentialSource(const std::string& credential) {
79   this->credential = credential;
80 }
81 
Credential()82 Result<std::string> FixedCredentialSource::Credential() { return credential; }
83 
make(const std::string & credential)84 std::unique_ptr<CredentialSource> FixedCredentialSource::make(
85     const std::string& credential) {
86   return std::unique_ptr<CredentialSource>(new FixedCredentialSource(credential));
87 }
88 
FromOauth2ClientFile(HttpClient & http_client,std::istream & stream)89 Result<RefreshCredentialSource> RefreshCredentialSource::FromOauth2ClientFile(
90     HttpClient& http_client, std::istream& stream) {
91   Json::CharReaderBuilder builder;
92   std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
93   Json::Value json;
94   std::string errorMessage;
95   CF_EXPECT(Json::parseFromStream(builder, stream, &json, &errorMessage),
96             "Failed to parse json: " << errorMessage);
97   CF_EXPECT(json.isMember("data"));
98   auto& data = json["data"];
99   CF_EXPECT(data.type() == Json::ValueType::arrayValue);
100 
101   CF_EXPECT(data.size() == 1);
102   auto& data_first = data[0];
103   CF_EXPECT(data_first.type() == Json::ValueType::objectValue);
104 
105   CF_EXPECT(data_first.isMember("credential"));
106   auto& credential = data_first["credential"];
107   CF_EXPECT(credential.type() == Json::ValueType::objectValue);
108 
109   CF_EXPECT(credential.isMember("client_id"));
110   auto& client_id = credential["client_id"];
111   CF_EXPECT(client_id.type() == Json::ValueType::stringValue);
112 
113   CF_EXPECT(credential.isMember("client_secret"));
114   auto& client_secret = credential["client_secret"];
115   CF_EXPECT(client_secret.type() == Json::ValueType::stringValue);
116 
117   CF_EXPECT(credential.isMember("refresh_token"));
118   auto& refresh_token = credential["refresh_token"];
119   CF_EXPECT(refresh_token.type() == Json::ValueType::stringValue);
120 
121   return RefreshCredentialSource(http_client, client_id.asString(),
122                                  client_secret.asString(),
123                                  refresh_token.asString());
124 }
125 
RefreshCredentialSource(HttpClient & http_client,const std::string & client_id,const std::string & client_secret,const std::string & refresh_token)126 RefreshCredentialSource::RefreshCredentialSource(
127     HttpClient& http_client, const std::string& client_id,
128     const std::string& client_secret, const std::string& refresh_token)
129     : http_client_(http_client),
130       client_id_(client_id),
131       client_secret_(client_secret),
132       refresh_token_(refresh_token) {}
133 
Credential()134 Result<std::string> RefreshCredentialSource::Credential() {
135   if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
136     CF_EXPECT(UpdateLatestCredential());
137   }
138   return latest_credential_;
139 }
140 
UpdateLatestCredential()141 Result<void> RefreshCredentialSource::UpdateLatestCredential() {
142   std::vector<std::string> headers = {
143       "Content-Type: application/x-www-form-urlencoded"};
144   std::stringstream data;
145   data << "client_id=" << http_client_.UrlEscape(client_id_) << "&";
146   data << "client_secret=" << http_client_.UrlEscape(client_secret_) << "&";
147   data << "refresh_token=" << http_client_.UrlEscape(refresh_token_) << "&";
148   data << "grant_type=refresh_token";
149 
150   static constexpr char kUrl[] = "https://oauth2.googleapis.com/token";
151   auto response = CF_EXPECT(http_client_.PostToJson(kUrl, data.str(), headers));
152   CF_EXPECT(response.HttpSuccess(), response.data);
153   auto& json = response.data;
154 
155   CF_EXPECT(!json.isMember("error"),
156             "Response had \"error\" but had http success status. Received \""
157                 << json << "\"");
158 
159   CF_EXPECT(json.isMember("access_token") && json.isMember("expires_in"),
160             "Refresh credential was missing access_token or expires_in."
161                 << " Full response was " << json << "");
162 
163   expiration_ = std::chrono::steady_clock::now() +
164                 std::chrono::seconds(json["expires_in"].asInt());
165   latest_credential_ = json["access_token"].asString();
166   return {};
167 }
168 
CollectSslErrors()169 static std::string CollectSslErrors() {
170   std::stringstream errors;
171   auto callback = [](const char* str, size_t len, void* stream) {
172     ((std::stringstream*)stream)->write(str, len);
173     return 1;  // success
174   };
175   ERR_print_errors_cb(callback, &errors);
176   return errors.str();
177 }
178 
179 Result<ServiceAccountOauthCredentialSource>
FromJson(HttpClient & http_client,const Json::Value & json,const std::string & scope)180 ServiceAccountOauthCredentialSource::FromJson(HttpClient& http_client,
181                                               const Json::Value& json,
182                                               const std::string& scope) {
183   ServiceAccountOauthCredentialSource source(http_client);
184   source.scope_ = scope;
185 
186   CF_EXPECT(json.isMember("client_email"));
187   CF_EXPECT(json["client_email"].type() == Json::ValueType::stringValue);
188   source.email_ = json["client_email"].asString();
189 
190   CF_EXPECT(json.isMember("private_key"));
191   CF_EXPECT(json["private_key"].type() == Json::ValueType::stringValue);
192   std::string key_str = json["private_key"].asString();
193 
194   std::unique_ptr<BIO, int (*)(BIO*)> bo(CF_EXPECT(BIO_new(BIO_s_mem())),
195                                          BIO_free);
196   CF_EXPECT(BIO_write(bo.get(), key_str.c_str(), key_str.size()) ==
197             key_str.size());
198 
199   auto pkey = CF_EXPECT(PEM_read_bio_PrivateKey(bo.get(), nullptr, 0, 0),
200                         CollectSslErrors());
201   source.private_key_.reset(pkey);
202 
203   return source;
204 }
205 
ServiceAccountOauthCredentialSource(HttpClient & http_client)206 ServiceAccountOauthCredentialSource::ServiceAccountOauthCredentialSource(
207     HttpClient& http_client)
208     : http_client_(http_client), private_key_(nullptr, EVP_PKEY_free) {}
209 
Base64Url(const char * data,std::size_t size)210 static Result<std::string> Base64Url(const char* data, std::size_t size) {
211   std::string base64;
212   CF_EXPECT(EncodeBase64(data, size, &base64));
213   base64 = android::base::StringReplace(base64, "+", "-", /* all */ true);
214   base64 = android::base::StringReplace(base64, "/", "_", /* all */ true);
215   return base64;
216 }
217 
JsonToBase64Url(const Json::Value & json)218 static Result<std::string> JsonToBase64Url(const Json::Value& json) {
219   Json::StreamWriterBuilder factory;
220   auto serialized = Json::writeString(factory, json);
221   return CF_EXPECT(Base64Url(serialized.c_str(), serialized.size()));
222 }
223 
CreateJwt(const std::string & email,const std::string & scope,EVP_PKEY * private_key)224 static Result<std::string> CreateJwt(const std::string& email,
225                                      const std::string& scope,
226                                      EVP_PKEY* private_key) {
227   using std::chrono::duration_cast;
228   using std::chrono::minutes;
229   using std::chrono::seconds;
230   using std::chrono::system_clock;
231   // https://developers.google.com/identity/protocols/oauth2/service-account
232   Json::Value header_json;
233   header_json["alg"] = "RS256";
234   header_json["typ"] = "JWT";
235   std::string header_str = CF_EXPECT(JsonToBase64Url(header_json));
236 
237   Json::Value claim_set_json;
238   claim_set_json["iss"] = email;
239   claim_set_json["scope"] = scope;
240   claim_set_json["aud"] = "https://oauth2.googleapis.com/token";
241   auto time = system_clock::now();
242   claim_set_json["iat"] =
243       (uint64_t)duration_cast<seconds>(time.time_since_epoch()).count();
244   auto exp = time + minutes(30);
245   claim_set_json["exp"] =
246       (uint64_t)duration_cast<seconds>(exp.time_since_epoch()).count();
247   std::string claim_set_str = CF_EXPECT(JsonToBase64Url(claim_set_json));
248 
249   std::string jwt_to_sign = header_str + "." + claim_set_str;
250 
251   std::unique_ptr<EVP_MD_CTX, void (*)(EVP_MD_CTX*)> sign_ctx(
252       EVP_MD_CTX_create(), EVP_MD_CTX_free);
253   CF_EXPECT(EVP_DigestSignInit(sign_ctx.get(), nullptr, EVP_sha256(), nullptr,
254                                private_key));
255   CF_EXPECT(EVP_DigestSignUpdate(sign_ctx.get(), jwt_to_sign.c_str(),
256                                  jwt_to_sign.size()));
257   size_t length;
258   CF_EXPECT(EVP_DigestSignFinal(sign_ctx.get(), nullptr, &length));
259   std::vector<uint8_t> sig_raw(length);
260   CF_EXPECT(EVP_DigestSignFinal(sign_ctx.get(), sig_raw.data(), &length));
261 
262   auto signature = CF_EXPECT(Base64Url((const char*)sig_raw.data(), length));
263   return jwt_to_sign + "." + signature;
264 }
265 
RefreshCredential()266 Result<void> ServiceAccountOauthCredentialSource::RefreshCredential() {
267   static constexpr char URL[] = "https://oauth2.googleapis.com/token";
268   static constexpr char GRANT[] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
269   std::stringstream content;
270   content << "grant_type=" << http_client_.UrlEscape(GRANT) << "&";
271   auto jwt = CF_EXPECT(CreateJwt(email_, scope_, private_key_.get()));
272   content << "assertion=" << http_client_.UrlEscape(jwt);
273   std::vector<std::string> headers = {
274       "Content-Type: application/x-www-form-urlencoded"};
275   auto response =
276       CF_EXPECT(http_client_.PostToJson(URL, content.str(), headers));
277   CF_EXPECT(response.HttpSuccess(),
278             "Error fetching credentials. The server response was \""
279                 << response.data << "\", and code was " << response.http_code);
280   Json::Value json = response.data;
281 
282   CF_EXPECT(!json.isMember("error"),
283             "Response had \"error\" but had http success status. Received \""
284                 << json << "\"");
285 
286   CF_EXPECT(json.isMember("access_token") && json.isMember("expires_in"),
287             "Service account credential was missing access_token or expires_in."
288                 << " Full response was " << json << "");
289 
290   expiration_ = std::chrono::steady_clock::now() +
291                 std::chrono::seconds(json["expires_in"].asInt());
292   latest_credential_ = json["access_token"].asString();
293   return {};
294 }
295 
Credential()296 Result<std::string> ServiceAccountOauthCredentialSource::Credential() {
297   if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
298     CF_EXPECT(RefreshCredential());
299   }
300   return latest_credential_;
301 }
302 
303 } // namespace cuttlefish
304