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