blob: 05c2f07d5c54d6142032ecb1896516c2f37575fa [file] [log] [blame]
Lutz Justen09cd1c32019-02-15 14:31:49 +01001// Copyright 2019 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Lutz Justene39cbd42019-05-14 14:52:24 +02005#include "kerberos/krb5_interface_impl.h"
Lutz Justen09cd1c32019-02-15 14:31:49 +01006
Lutz Justencb8399d2019-03-08 14:30:17 +01007#include <algorithm>
Lutz Justen09cd1c32019-02-15 14:31:49 +01008#include <utility>
9
Lutz Justenb79da832019-03-08 14:52:53 +010010#include <base/files/file_path.h>
Lutz Justen09cd1c32019-02-15 14:31:49 +010011#include <base/logging.h>
12#include <base/strings/stringprintf.h>
13#include <krb5.h>
14
15namespace kerberos {
16
17namespace {
18
19// Environment variable for the Kerberos configuration (krb5.conf).
20constexpr char kKrb5ConfigEnvVar[] = "KRB5_CONFIG";
21
Lutz Justencb8399d2019-03-08 14:30:17 +010022// Wrapper classes for safe construction and destruction.
23struct ScopedKrb5Context {
24 ScopedKrb5Context() = default;
25 ~ScopedKrb5Context() {
26 if (ctx) {
27 krb5_free_context(ctx);
28 ctx = nullptr;
29 }
30 }
31
32 // Converts the krb5 |code| to a human readable error message.
33 std::string GetErrorMessage(errcode_t code) {
Lutz Justene5238762019-06-06 14:09:21 +020034 // Fallback if error happens during ctx initialization (e.g. bad config).
35 if (!ctx)
36 return base::StringPrintf("Error %ld", code);
37
Lutz Justencb8399d2019-03-08 14:30:17 +010038 const char* emsg = krb5_get_error_message(ctx, code);
39 std::string msg = base::StringPrintf("%s (%ld)", emsg, code);
40 krb5_free_error_message(ctx, emsg);
41 return msg;
42 }
43
44 krb5_context get() const { return ctx; }
45 krb5_context* get_mutable_ptr() { return &ctx; }
46
47 private:
48 krb5_context ctx = nullptr;
49};
50
51struct ScopedKrb5CCache {
Lutz Justen1cb62412019-05-09 10:09:45 +020052 // Prefer the constructor taking a context if possible.
53 ScopedKrb5CCache() {}
54 explicit ScopedKrb5CCache(krb5_context _ctx) { set_ctx(_ctx); }
55
Lutz Justencb8399d2019-03-08 14:30:17 +010056 ~ScopedKrb5CCache() {
57 if (ccache) {
Lutz Justen1cb62412019-05-09 10:09:45 +020058 DCHECK(ctx);
59 krb5_cc_close(ctx, ccache);
Lutz Justencb8399d2019-03-08 14:30:17 +010060 ccache = nullptr;
61 }
62 }
63
Lutz Justen1cb62412019-05-09 10:09:45 +020064 // The context must be set if |ccache| is set (though get_mutable_ptr())
65 // before this object is destroyed.
66 void set_ctx(krb5_context _ctx) {
67 ctx = _ctx;
68 DCHECK(ctx);
69 }
70
Lutz Justencb8399d2019-03-08 14:30:17 +010071 krb5_ccache get() const { return ccache; }
72 krb5_ccache* get_mutable_ptr() { return &ccache; }
73
74 private:
75 // Pointer to parent data, not owned.
Lutz Justen1cb62412019-05-09 10:09:45 +020076 krb5_context ctx = nullptr;
Lutz Justencb8399d2019-03-08 14:30:17 +010077 krb5_ccache ccache = nullptr;
78};
79
80// Maps some common krb5 error codes to our internal codes. If something is not
81// reported properly, add more cases here.
82ErrorType TranslateErrorCode(errcode_t code) {
83 switch (code) {
84 case KRB5KDC_ERR_NONE:
85 return ERROR_NONE;
86
87 case KRB5_KDC_UNREACH:
88 return ERROR_NETWORK_PROBLEM;
89
Lutz Justene5238762019-06-06 14:09:21 +020090 case KRB5_CONFIG_BADFORMAT:
91 return ERROR_BAD_CONFIG;
92
Lutz Justencb8399d2019-03-08 14:30:17 +010093 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
94 return ERROR_BAD_PRINCIPAL;
95
96 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
97 case KRB5KDC_ERR_PREAUTH_FAILED:
98 return ERROR_BAD_PASSWORD;
99
100 case KRB5KDC_ERR_KEY_EXP:
101 return ERROR_PASSWORD_EXPIRED;
102
103 // TODO(https://crbug.com/951741): Verify
104 case KRB5_KPASSWD_SOFTERROR:
105 return ERROR_PASSWORD_REJECTED;
106
107 // TODO(https://crbug.com/951741): Verify
108 case KRB5_FCC_NOFILE:
109 return ERROR_NO_CREDENTIALS_CACHE_FOUND;
110
111 // TODO(https://crbug.com/951741): Verify
112 case KRB5KRB_AP_ERR_TKT_EXPIRED:
113 return ERROR_KERBEROS_TICKET_EXPIRED;
114
115 case KRB5KDC_ERR_ETYPE_NOSUPP:
116 return ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE;
117
118 case KRB5_REALM_UNKNOWN:
119 return ERROR_CONTACTING_KDC_FAILED;
120
121 default:
122 return ERROR_UNKNOWN_KRB5_ERROR;
123 }
124}
125
126// Returns true if the string contained in |data| matches |str_to_match|.
127bool DataMatches(const krb5_data& data, const char* str_to_match) {
128 // It is not clear whether data.data is null terminated, so a strcmp might
129 // not work.
130 return strlen(str_to_match) == data.length &&
131 memcmp(str_to_match, data.data, data.length) == 0;
132}
133
134// Returns true if |creds| has a server that starts with "krbtgt".
135bool IsTgt(const krb5_creds& creds) {
136 return creds.server && creds.server->length > 0 &&
137 DataMatches(creds.server->data[0], "krbtgt");
138}
139
Lutz Justen09cd1c32019-02-15 14:31:49 +0100140enum class Action { AcquireTgt, RenewTgt };
141
142struct Options {
143 std::string principal_name;
144 std::string password;
145 std::string krb5cc_path;
146 std::string config_path;
147 Action action = Action::AcquireTgt;
148};
149
150// Encapsulates krb5 context data required for kinit.
151class KinitContext {
152 public:
153 explicit KinitContext(Options options) : options_(std::move(options)) {
154 memset(&k5_, 0, sizeof(k5_));
155 }
156
157 // Runs kinit with the options passed to the constructor. Only call once per
158 // context. While in principle it should be fine to run multiple times, the
159 // code path probably hasn't been tested (kinit does not call this multiple
160 // times).
161 ErrorType Run() {
162 DCHECK(!did_run_);
163 did_run_ = true;
164
165 ErrorType error = Initialize();
166 if (error == ERROR_NONE)
167 error = RunKinit();
168 Finalize();
169 return error;
170 }
171
172 private:
173 // The following code has been adapted from kinit.c in the mit-krb5 code base.
174 // It has been formatted to fit this screen.
175
176 struct Krb5Data {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100177 krb5_principal me;
178 char* name;
179 };
180
181 // Wrapper around krb5 data to get rid of the gotos in the original code.
182 struct KInitData {
183 // Pointer to parent data, not owned.
Lutz Justencb8399d2019-03-08 14:30:17 +0100184 const krb5_context ctx = nullptr;
185 // Pointer to parent data, not owned.
Lutz Justen09cd1c32019-02-15 14:31:49 +0100186 const Krb5Data* k5 = nullptr;
187 krb5_creds my_creds;
188 krb5_get_init_creds_opt* options = nullptr;
189
190 // The lifetime of the |k5| pointer must exceed the lifetime of this object.
Lutz Justencb8399d2019-03-08 14:30:17 +0100191 explicit KInitData(const krb5_context ctx, const Krb5Data* k5)
192 : ctx(ctx), k5(k5) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100193 memset(&my_creds, 0, sizeof(my_creds));
194 }
195
196 ~KInitData() {
197 if (options)
Lutz Justencb8399d2019-03-08 14:30:17 +0100198 krb5_get_init_creds_opt_free(ctx, options);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100199 if (my_creds.client == k5->me)
200 my_creds.client = nullptr;
Lutz Justencb8399d2019-03-08 14:30:17 +0100201 krb5_free_cred_contents(ctx, &my_creds);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100202 }
203 };
204
Lutz Justen09cd1c32019-02-15 14:31:49 +0100205 // Initializes krb5 data.
206 ErrorType Initialize() {
Lutz Justencb8399d2019-03-08 14:30:17 +0100207 krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100208 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100209 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100210 return TranslateErrorCode(ret);
211 }
212
Lutz Justen1cb62412019-05-09 10:09:45 +0200213 out_cc.set_ctx(ctx.get());
214 ret = krb5_cc_resolve(ctx.get(), options_.krb5cc_path.c_str(),
215 out_cc.get_mutable_ptr());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100216 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100217 LOG(ERROR) << ctx.GetErrorMessage(ret) << " resolving ccache";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100218 return TranslateErrorCode(ret);
219 }
220
Lutz Justencb8399d2019-03-08 14:30:17 +0100221 ret = krb5_parse_name_flags(ctx.get(), options_.principal_name.c_str(),
Lutz Justen09cd1c32019-02-15 14:31:49 +0100222 0 /* flags */, &k5_.me);
223 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100224 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when parsing name";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100225 return TranslateErrorCode(ret);
226 }
227
Lutz Justencb8399d2019-03-08 14:30:17 +0100228 ret = krb5_unparse_name(ctx.get(), k5_.me, &k5_.name);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100229 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100230 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when unparsing name";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100231 return TranslateErrorCode(ret);
232 }
233
234 options_.principal_name = k5_.name;
235 return ERROR_NONE;
236 }
237
238 // Finalizes krb5 data.
239 void Finalize() {
Lutz Justencb8399d2019-03-08 14:30:17 +0100240 krb5_free_unparsed_name(ctx.get(), k5_.name);
241 krb5_free_principal(ctx.get(), k5_.me);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100242 memset(&k5_, 0, sizeof(k5_));
243 }
244
245 // Runs the actual kinit code and acquires/renews tickets.
246 ErrorType RunKinit() {
247 krb5_error_code ret;
Lutz Justencb8399d2019-03-08 14:30:17 +0100248 KInitData d(ctx.get(), &k5_);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100249
Lutz Justencb8399d2019-03-08 14:30:17 +0100250 ret = krb5_get_init_creds_opt_alloc(ctx.get(), &d.options);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100251 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100252 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100253 return TranslateErrorCode(ret);
254 }
255
Lutz Justencb8399d2019-03-08 14:30:17 +0100256 ret = krb5_get_init_creds_opt_set_out_ccache(ctx.get(), d.options,
Lutz Justen1cb62412019-05-09 10:09:45 +0200257 out_cc.get());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100258 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100259 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100260 return TranslateErrorCode(ret);
261 }
262
263 // To get notified of expiry, see
264 // krb5_get_init_creds_opt_set_expire_callback
265
266 switch (options_.action) {
267 case Action::AcquireTgt:
268 ret = krb5_get_init_creds_password(
Lutz Justencb8399d2019-03-08 14:30:17 +0100269 ctx.get(), &d.my_creds, k5_.me, options_.password.c_str(),
Lutz Justen09cd1c32019-02-15 14:31:49 +0100270 nullptr /* prompter */, nullptr /* data */, 0 /* start_time */,
271 nullptr /* in_tkt_service */, d.options);
272 break;
273 case Action::RenewTgt:
Lutz Justen1cb62412019-05-09 10:09:45 +0200274 ret =
275 krb5_get_renewed_creds(ctx.get(), &d.my_creds, k5_.me, out_cc.get(),
276 nullptr /* options_.in_tkt_service */);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100277 break;
278 }
279
280 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100281 LOG(ERROR) << ctx.GetErrorMessage(ret);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100282 return TranslateErrorCode(ret);
283 }
284
285 if (options_.action != Action::AcquireTgt) {
Lutz Justen1cb62412019-05-09 10:09:45 +0200286 ret = krb5_cc_initialize(ctx.get(), out_cc.get(), k5_.me);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100287 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100288 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when initializing cache";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100289 return TranslateErrorCode(ret);
290 }
291
Lutz Justen1cb62412019-05-09 10:09:45 +0200292 ret = krb5_cc_store_cred(ctx.get(), out_cc.get(), &d.my_creds);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100293 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100294 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while storing credentials";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100295 return TranslateErrorCode(ret);
296 }
297 }
298
299 return ERROR_NONE;
300 }
301
Lutz Justencb8399d2019-03-08 14:30:17 +0100302 ScopedKrb5Context ctx;
Lutz Justen1cb62412019-05-09 10:09:45 +0200303 ScopedKrb5CCache out_cc;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100304 Krb5Data k5_;
305 Options options_;
306 bool did_run_ = false;
307};
308
309} // namespace
310
Lutz Justene39cbd42019-05-14 14:52:24 +0200311Krb5InterfaceImpl::Krb5InterfaceImpl() = default;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100312
Lutz Justene39cbd42019-05-14 14:52:24 +0200313Krb5InterfaceImpl::~Krb5InterfaceImpl() = default;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100314
Lutz Justene39cbd42019-05-14 14:52:24 +0200315ErrorType Krb5InterfaceImpl::AcquireTgt(const std::string& principal_name,
316 const std::string& password,
317 const base::FilePath& krb5cc_path,
318 const base::FilePath& krb5conf_path) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100319 Options options;
320 options.action = Action::AcquireTgt;
321 options.principal_name = principal_name;
322 options.password = password;
Lutz Justenb79da832019-03-08 14:52:53 +0100323 options.krb5cc_path = krb5cc_path.value();
324 setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100325 ErrorType error = KinitContext(std::move(options)).Run();
326 unsetenv(kKrb5ConfigEnvVar);
327 return error;
328}
329
Lutz Justene39cbd42019-05-14 14:52:24 +0200330ErrorType Krb5InterfaceImpl::RenewTgt(const std::string& principal_name,
331 const base::FilePath& krb5cc_path,
Lutz Justene6784c02019-07-03 14:08:43 +0200332 const base::FilePath& krb5conf_path) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100333 Options options;
334 options.action = Action::RenewTgt;
335 options.principal_name = principal_name;
Lutz Justenb79da832019-03-08 14:52:53 +0100336 options.krb5cc_path = krb5cc_path.value();
Lutz Justene6784c02019-07-03 14:08:43 +0200337 setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100338 ErrorType error = KinitContext(std::move(options)).Run();
339 unsetenv(kKrb5ConfigEnvVar);
340 return error;
341}
342
Lutz Justene39cbd42019-05-14 14:52:24 +0200343ErrorType Krb5InterfaceImpl::GetTgtStatus(const base::FilePath& krb5cc_path,
344 TgtStatus* status) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100345 DCHECK(status);
346
347 ScopedKrb5Context ctx;
348 krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
349 if (ret) {
350 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
351 return TranslateErrorCode(ret);
352 }
353
Lutz Justen1cb62412019-05-09 10:09:45 +0200354 ScopedKrb5CCache ccache(ctx.get());
Lutz Justencb8399d2019-03-08 14:30:17 +0100355 std::string prefixed_krb5cc_path = "FILE:" + krb5cc_path.value();
356 ret = krb5_cc_resolve(ctx.get(), prefixed_krb5cc_path.c_str(),
Lutz Justen1cb62412019-05-09 10:09:45 +0200357 ccache.get_mutable_ptr());
Lutz Justencb8399d2019-03-08 14:30:17 +0100358 if (ret) {
Lutz Justen1cb62412019-05-09 10:09:45 +0200359 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while resolving cache";
Lutz Justencb8399d2019-03-08 14:30:17 +0100360 return TranslateErrorCode(ret);
361 }
362
363 krb5_cc_cursor cur;
Lutz Justen1cb62412019-05-09 10:09:45 +0200364 ret = krb5_cc_start_seq_get(ctx.get(), ccache.get(), &cur);
Lutz Justencb8399d2019-03-08 14:30:17 +0100365 if (ret) {
366 LOG(ERROR) << ctx.GetErrorMessage(ret)
367 << " while starting to retrieve tickets";
368 return TranslateErrorCode(ret);
369 }
370
371 krb5_timestamp now = time(nullptr);
372
373 krb5_creds creds;
374 bool found_tgt = false;
Lutz Justen1cb62412019-05-09 10:09:45 +0200375 while ((ret = krb5_cc_next_cred(ctx.get(), ccache.get(), &cur, &creds)) ==
376 0) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100377 if (IsTgt(creds)) {
378 if (creds.times.endtime)
379 status->validity_seconds =
380 std::max<int64_t>(creds.times.endtime - now, 0);
381
382 if (creds.times.renew_till) {
383 status->renewal_seconds =
384 std::max<int64_t>(creds.times.renew_till - now, 0);
385 }
386
387 if (found_tgt) {
388 LOG(WARNING) << "More than one TGT found in credential cache '"
389 << krb5cc_path.value() << ".";
390 }
391 found_tgt = true;
392 }
393 krb5_free_cred_contents(ctx.get(), &creds);
394 }
395 if (!found_tgt) {
396 LOG(WARNING) << "No TGT found in credential cache '" << krb5cc_path.value()
397 << ".";
398 }
399
400 if (ret != KRB5_CC_END) {
401 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while retrieving a ticket";
402 return TranslateErrorCode(ret);
403 }
404
Lutz Justen1cb62412019-05-09 10:09:45 +0200405 ret = krb5_cc_end_seq_get(ctx.get(), ccache.get(), &cur);
Lutz Justencb8399d2019-03-08 14:30:17 +0100406 if (ret) {
407 LOG(ERROR) << ctx.GetErrorMessage(ret)
408 << " while finishing ticket retrieval";
409 return TranslateErrorCode(ret);
410 }
411
412 return ERROR_NONE;
413}
414
Lutz Justen09cd1c32019-02-15 14:31:49 +0100415} // namespace kerberos