blob: 1000bf8d622e8dd26d1950fe5be149907cdc4498 [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) {
34 DCHECK(ctx);
35 const char* emsg = krb5_get_error_message(ctx, code);
36 std::string msg = base::StringPrintf("%s (%ld)", emsg, code);
37 krb5_free_error_message(ctx, emsg);
38 return msg;
39 }
40
41 krb5_context get() const { return ctx; }
42 krb5_context* get_mutable_ptr() { return &ctx; }
43
44 private:
45 krb5_context ctx = nullptr;
46};
47
48struct ScopedKrb5CCache {
Lutz Justen1cb62412019-05-09 10:09:45 +020049 // Prefer the constructor taking a context if possible.
50 ScopedKrb5CCache() {}
51 explicit ScopedKrb5CCache(krb5_context _ctx) { set_ctx(_ctx); }
52
Lutz Justencb8399d2019-03-08 14:30:17 +010053 ~ScopedKrb5CCache() {
54 if (ccache) {
Lutz Justen1cb62412019-05-09 10:09:45 +020055 DCHECK(ctx);
56 krb5_cc_close(ctx, ccache);
Lutz Justencb8399d2019-03-08 14:30:17 +010057 ccache = nullptr;
58 }
59 }
60
Lutz Justen1cb62412019-05-09 10:09:45 +020061 // The context must be set if |ccache| is set (though get_mutable_ptr())
62 // before this object is destroyed.
63 void set_ctx(krb5_context _ctx) {
64 ctx = _ctx;
65 DCHECK(ctx);
66 }
67
Lutz Justencb8399d2019-03-08 14:30:17 +010068 krb5_ccache get() const { return ccache; }
69 krb5_ccache* get_mutable_ptr() { return &ccache; }
70
71 private:
72 // Pointer to parent data, not owned.
Lutz Justen1cb62412019-05-09 10:09:45 +020073 krb5_context ctx = nullptr;
Lutz Justencb8399d2019-03-08 14:30:17 +010074 krb5_ccache ccache = nullptr;
75};
76
77// Maps some common krb5 error codes to our internal codes. If something is not
78// reported properly, add more cases here.
79ErrorType TranslateErrorCode(errcode_t code) {
80 switch (code) {
81 case KRB5KDC_ERR_NONE:
82 return ERROR_NONE;
83
84 case KRB5_KDC_UNREACH:
85 return ERROR_NETWORK_PROBLEM;
86
87 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
88 return ERROR_BAD_PRINCIPAL;
89
90 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
91 case KRB5KDC_ERR_PREAUTH_FAILED:
92 return ERROR_BAD_PASSWORD;
93
94 case KRB5KDC_ERR_KEY_EXP:
95 return ERROR_PASSWORD_EXPIRED;
96
97 // TODO(https://crbug.com/951741): Verify
98 case KRB5_KPASSWD_SOFTERROR:
99 return ERROR_PASSWORD_REJECTED;
100
101 // TODO(https://crbug.com/951741): Verify
102 case KRB5_FCC_NOFILE:
103 return ERROR_NO_CREDENTIALS_CACHE_FOUND;
104
105 // TODO(https://crbug.com/951741): Verify
106 case KRB5KRB_AP_ERR_TKT_EXPIRED:
107 return ERROR_KERBEROS_TICKET_EXPIRED;
108
109 case KRB5KDC_ERR_ETYPE_NOSUPP:
110 return ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE;
111
112 case KRB5_REALM_UNKNOWN:
113 return ERROR_CONTACTING_KDC_FAILED;
114
115 default:
116 return ERROR_UNKNOWN_KRB5_ERROR;
117 }
118}
119
120// Returns true if the string contained in |data| matches |str_to_match|.
121bool DataMatches(const krb5_data& data, const char* str_to_match) {
122 // It is not clear whether data.data is null terminated, so a strcmp might
123 // not work.
124 return strlen(str_to_match) == data.length &&
125 memcmp(str_to_match, data.data, data.length) == 0;
126}
127
128// Returns true if |creds| has a server that starts with "krbtgt".
129bool IsTgt(const krb5_creds& creds) {
130 return creds.server && creds.server->length > 0 &&
131 DataMatches(creds.server->data[0], "krbtgt");
132}
133
Lutz Justen09cd1c32019-02-15 14:31:49 +0100134enum class Action { AcquireTgt, RenewTgt };
135
136struct Options {
137 std::string principal_name;
138 std::string password;
139 std::string krb5cc_path;
140 std::string config_path;
141 Action action = Action::AcquireTgt;
142};
143
144// Encapsulates krb5 context data required for kinit.
145class KinitContext {
146 public:
147 explicit KinitContext(Options options) : options_(std::move(options)) {
148 memset(&k5_, 0, sizeof(k5_));
149 }
150
151 // Runs kinit with the options passed to the constructor. Only call once per
152 // context. While in principle it should be fine to run multiple times, the
153 // code path probably hasn't been tested (kinit does not call this multiple
154 // times).
155 ErrorType Run() {
156 DCHECK(!did_run_);
157 did_run_ = true;
158
159 ErrorType error = Initialize();
160 if (error == ERROR_NONE)
161 error = RunKinit();
162 Finalize();
163 return error;
164 }
165
166 private:
167 // The following code has been adapted from kinit.c in the mit-krb5 code base.
168 // It has been formatted to fit this screen.
169
170 struct Krb5Data {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100171 krb5_principal me;
172 char* name;
173 };
174
175 // Wrapper around krb5 data to get rid of the gotos in the original code.
176 struct KInitData {
177 // Pointer to parent data, not owned.
Lutz Justencb8399d2019-03-08 14:30:17 +0100178 const krb5_context ctx = nullptr;
179 // Pointer to parent data, not owned.
Lutz Justen09cd1c32019-02-15 14:31:49 +0100180 const Krb5Data* k5 = nullptr;
181 krb5_creds my_creds;
182 krb5_get_init_creds_opt* options = nullptr;
183
184 // The lifetime of the |k5| pointer must exceed the lifetime of this object.
Lutz Justencb8399d2019-03-08 14:30:17 +0100185 explicit KInitData(const krb5_context ctx, const Krb5Data* k5)
186 : ctx(ctx), k5(k5) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100187 memset(&my_creds, 0, sizeof(my_creds));
188 }
189
190 ~KInitData() {
191 if (options)
Lutz Justencb8399d2019-03-08 14:30:17 +0100192 krb5_get_init_creds_opt_free(ctx, options);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100193 if (my_creds.client == k5->me)
194 my_creds.client = nullptr;
Lutz Justencb8399d2019-03-08 14:30:17 +0100195 krb5_free_cred_contents(ctx, &my_creds);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100196 }
197 };
198
Lutz Justen09cd1c32019-02-15 14:31:49 +0100199 // Initializes krb5 data.
200 ErrorType Initialize() {
Lutz Justencb8399d2019-03-08 14:30:17 +0100201 krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100202 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100203 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100204 return TranslateErrorCode(ret);
205 }
206
Lutz Justen1cb62412019-05-09 10:09:45 +0200207 out_cc.set_ctx(ctx.get());
208 ret = krb5_cc_resolve(ctx.get(), options_.krb5cc_path.c_str(),
209 out_cc.get_mutable_ptr());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100210 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100211 LOG(ERROR) << ctx.GetErrorMessage(ret) << " resolving ccache";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100212 return TranslateErrorCode(ret);
213 }
214
Lutz Justencb8399d2019-03-08 14:30:17 +0100215 ret = krb5_parse_name_flags(ctx.get(), options_.principal_name.c_str(),
Lutz Justen09cd1c32019-02-15 14:31:49 +0100216 0 /* flags */, &k5_.me);
217 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100218 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when parsing name";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100219 return TranslateErrorCode(ret);
220 }
221
Lutz Justencb8399d2019-03-08 14:30:17 +0100222 ret = krb5_unparse_name(ctx.get(), k5_.me, &k5_.name);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100223 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100224 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when unparsing name";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100225 return TranslateErrorCode(ret);
226 }
227
228 options_.principal_name = k5_.name;
229 return ERROR_NONE;
230 }
231
232 // Finalizes krb5 data.
233 void Finalize() {
Lutz Justencb8399d2019-03-08 14:30:17 +0100234 krb5_free_unparsed_name(ctx.get(), k5_.name);
235 krb5_free_principal(ctx.get(), k5_.me);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100236 memset(&k5_, 0, sizeof(k5_));
237 }
238
239 // Runs the actual kinit code and acquires/renews tickets.
240 ErrorType RunKinit() {
241 krb5_error_code ret;
Lutz Justencb8399d2019-03-08 14:30:17 +0100242 KInitData d(ctx.get(), &k5_);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100243
Lutz Justencb8399d2019-03-08 14:30:17 +0100244 ret = krb5_get_init_creds_opt_alloc(ctx.get(), &d.options);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100245 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100246 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100247 return TranslateErrorCode(ret);
248 }
249
Lutz Justencb8399d2019-03-08 14:30:17 +0100250 ret = krb5_get_init_creds_opt_set_out_ccache(ctx.get(), d.options,
Lutz Justen1cb62412019-05-09 10:09:45 +0200251 out_cc.get());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100252 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100253 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100254 return TranslateErrorCode(ret);
255 }
256
257 // To get notified of expiry, see
258 // krb5_get_init_creds_opt_set_expire_callback
259
260 switch (options_.action) {
261 case Action::AcquireTgt:
262 ret = krb5_get_init_creds_password(
Lutz Justencb8399d2019-03-08 14:30:17 +0100263 ctx.get(), &d.my_creds, k5_.me, options_.password.c_str(),
Lutz Justen09cd1c32019-02-15 14:31:49 +0100264 nullptr /* prompter */, nullptr /* data */, 0 /* start_time */,
265 nullptr /* in_tkt_service */, d.options);
266 break;
267 case Action::RenewTgt:
Lutz Justen1cb62412019-05-09 10:09:45 +0200268 ret =
269 krb5_get_renewed_creds(ctx.get(), &d.my_creds, k5_.me, out_cc.get(),
270 nullptr /* options_.in_tkt_service */);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100271 break;
272 }
273
274 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100275 LOG(ERROR) << ctx.GetErrorMessage(ret);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100276 return TranslateErrorCode(ret);
277 }
278
279 if (options_.action != Action::AcquireTgt) {
Lutz Justen1cb62412019-05-09 10:09:45 +0200280 ret = krb5_cc_initialize(ctx.get(), out_cc.get(), k5_.me);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100281 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100282 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when initializing cache";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100283 return TranslateErrorCode(ret);
284 }
285
Lutz Justen1cb62412019-05-09 10:09:45 +0200286 ret = krb5_cc_store_cred(ctx.get(), out_cc.get(), &d.my_creds);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100287 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100288 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while storing credentials";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100289 return TranslateErrorCode(ret);
290 }
291 }
292
293 return ERROR_NONE;
294 }
295
Lutz Justencb8399d2019-03-08 14:30:17 +0100296 ScopedKrb5Context ctx;
Lutz Justen1cb62412019-05-09 10:09:45 +0200297 ScopedKrb5CCache out_cc;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100298 Krb5Data k5_;
299 Options options_;
300 bool did_run_ = false;
301};
302
303} // namespace
304
Lutz Justene39cbd42019-05-14 14:52:24 +0200305Krb5InterfaceImpl::Krb5InterfaceImpl() = default;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100306
Lutz Justene39cbd42019-05-14 14:52:24 +0200307Krb5InterfaceImpl::~Krb5InterfaceImpl() = default;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100308
Lutz Justene39cbd42019-05-14 14:52:24 +0200309ErrorType Krb5InterfaceImpl::AcquireTgt(const std::string& principal_name,
310 const std::string& password,
311 const base::FilePath& krb5cc_path,
312 const base::FilePath& krb5conf_path) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100313 Options options;
314 options.action = Action::AcquireTgt;
315 options.principal_name = principal_name;
316 options.password = password;
Lutz Justenb79da832019-03-08 14:52:53 +0100317 options.krb5cc_path = krb5cc_path.value();
318 setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100319 ErrorType error = KinitContext(std::move(options)).Run();
320 unsetenv(kKrb5ConfigEnvVar);
321 return error;
322}
323
Lutz Justene39cbd42019-05-14 14:52:24 +0200324ErrorType Krb5InterfaceImpl::RenewTgt(const std::string& principal_name,
325 const base::FilePath& krb5cc_path,
326 const base::FilePath& config_path) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100327 Options options;
328 options.action = Action::RenewTgt;
329 options.principal_name = principal_name;
Lutz Justenb79da832019-03-08 14:52:53 +0100330 options.krb5cc_path = krb5cc_path.value();
331 setenv(kKrb5ConfigEnvVar, config_path.value().c_str(), 1);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100332 ErrorType error = KinitContext(std::move(options)).Run();
333 unsetenv(kKrb5ConfigEnvVar);
334 return error;
335}
336
Lutz Justene39cbd42019-05-14 14:52:24 +0200337ErrorType Krb5InterfaceImpl::GetTgtStatus(const base::FilePath& krb5cc_path,
338 TgtStatus* status) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100339 DCHECK(status);
340
341 ScopedKrb5Context ctx;
342 krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
343 if (ret) {
344 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
345 return TranslateErrorCode(ret);
346 }
347
Lutz Justen1cb62412019-05-09 10:09:45 +0200348 ScopedKrb5CCache ccache(ctx.get());
Lutz Justencb8399d2019-03-08 14:30:17 +0100349 std::string prefixed_krb5cc_path = "FILE:" + krb5cc_path.value();
350 ret = krb5_cc_resolve(ctx.get(), prefixed_krb5cc_path.c_str(),
Lutz Justen1cb62412019-05-09 10:09:45 +0200351 ccache.get_mutable_ptr());
Lutz Justencb8399d2019-03-08 14:30:17 +0100352 if (ret) {
Lutz Justen1cb62412019-05-09 10:09:45 +0200353 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while resolving cache";
Lutz Justencb8399d2019-03-08 14:30:17 +0100354 return TranslateErrorCode(ret);
355 }
356
357 krb5_cc_cursor cur;
Lutz Justen1cb62412019-05-09 10:09:45 +0200358 ret = krb5_cc_start_seq_get(ctx.get(), ccache.get(), &cur);
Lutz Justencb8399d2019-03-08 14:30:17 +0100359 if (ret) {
360 LOG(ERROR) << ctx.GetErrorMessage(ret)
361 << " while starting to retrieve tickets";
362 return TranslateErrorCode(ret);
363 }
364
365 krb5_timestamp now = time(nullptr);
366
367 krb5_creds creds;
368 bool found_tgt = false;
Lutz Justen1cb62412019-05-09 10:09:45 +0200369 while ((ret = krb5_cc_next_cred(ctx.get(), ccache.get(), &cur, &creds)) ==
370 0) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100371 if (IsTgt(creds)) {
372 if (creds.times.endtime)
373 status->validity_seconds =
374 std::max<int64_t>(creds.times.endtime - now, 0);
375
376 if (creds.times.renew_till) {
377 status->renewal_seconds =
378 std::max<int64_t>(creds.times.renew_till - now, 0);
379 }
380
381 if (found_tgt) {
382 LOG(WARNING) << "More than one TGT found in credential cache '"
383 << krb5cc_path.value() << ".";
384 }
385 found_tgt = true;
386 }
387 krb5_free_cred_contents(ctx.get(), &creds);
388 }
389 if (!found_tgt) {
390 LOG(WARNING) << "No TGT found in credential cache '" << krb5cc_path.value()
391 << ".";
392 }
393
394 if (ret != KRB5_CC_END) {
395 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while retrieving a ticket";
396 return TranslateErrorCode(ret);
397 }
398
Lutz Justen1cb62412019-05-09 10:09:45 +0200399 ret = krb5_cc_end_seq_get(ctx.get(), ccache.get(), &cur);
Lutz Justencb8399d2019-03-08 14:30:17 +0100400 if (ret) {
401 LOG(ERROR) << ctx.GetErrorMessage(ret)
402 << " while finishing ticket retrieval";
403 return TranslateErrorCode(ret);
404 }
405
406 return ERROR_NONE;
407}
408
Lutz Justen09cd1c32019-02-15 14:31:49 +0100409} // namespace kerberos