blob: 31e0dae22ffd5fd1f188e46489e77ed48362f7b4 [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
5#include "kerberos/krb5_interface.h"
6
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 {
49 explicit ScopedKrb5CCache(krb5_context ctx) : ctx(ctx) { DCHECK(ctx); }
50 ~ScopedKrb5CCache() {
51 if (ccache) {
52 krb5_cc_destroy(ctx, ccache);
53 ccache = nullptr;
54 }
55 }
56
57 krb5_ccache get() const { return ccache; }
58 krb5_ccache* get_mutable_ptr() { return &ccache; }
59
60 private:
61 // Pointer to parent data, not owned.
62 const krb5_context ctx = nullptr;
63 krb5_ccache ccache = nullptr;
64};
65
66// Maps some common krb5 error codes to our internal codes. If something is not
67// reported properly, add more cases here.
68ErrorType TranslateErrorCode(errcode_t code) {
69 switch (code) {
70 case KRB5KDC_ERR_NONE:
71 return ERROR_NONE;
72
73 case KRB5_KDC_UNREACH:
74 return ERROR_NETWORK_PROBLEM;
75
76 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
77 return ERROR_BAD_PRINCIPAL;
78
79 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
80 case KRB5KDC_ERR_PREAUTH_FAILED:
81 return ERROR_BAD_PASSWORD;
82
83 case KRB5KDC_ERR_KEY_EXP:
84 return ERROR_PASSWORD_EXPIRED;
85
86 // TODO(https://crbug.com/951741): Verify
87 case KRB5_KPASSWD_SOFTERROR:
88 return ERROR_PASSWORD_REJECTED;
89
90 // TODO(https://crbug.com/951741): Verify
91 case KRB5_FCC_NOFILE:
92 return ERROR_NO_CREDENTIALS_CACHE_FOUND;
93
94 // TODO(https://crbug.com/951741): Verify
95 case KRB5KRB_AP_ERR_TKT_EXPIRED:
96 return ERROR_KERBEROS_TICKET_EXPIRED;
97
98 case KRB5KDC_ERR_ETYPE_NOSUPP:
99 return ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE;
100
101 case KRB5_REALM_UNKNOWN:
102 return ERROR_CONTACTING_KDC_FAILED;
103
104 default:
105 return ERROR_UNKNOWN_KRB5_ERROR;
106 }
107}
108
109// Returns true if the string contained in |data| matches |str_to_match|.
110bool DataMatches(const krb5_data& data, const char* str_to_match) {
111 // It is not clear whether data.data is null terminated, so a strcmp might
112 // not work.
113 return strlen(str_to_match) == data.length &&
114 memcmp(str_to_match, data.data, data.length) == 0;
115}
116
117// Returns true if |creds| has a server that starts with "krbtgt".
118bool IsTgt(const krb5_creds& creds) {
119 return creds.server && creds.server->length > 0 &&
120 DataMatches(creds.server->data[0], "krbtgt");
121}
122
Lutz Justen09cd1c32019-02-15 14:31:49 +0100123enum class Action { AcquireTgt, RenewTgt };
124
125struct Options {
126 std::string principal_name;
127 std::string password;
128 std::string krb5cc_path;
129 std::string config_path;
130 Action action = Action::AcquireTgt;
131};
132
133// Encapsulates krb5 context data required for kinit.
134class KinitContext {
135 public:
136 explicit KinitContext(Options options) : options_(std::move(options)) {
137 memset(&k5_, 0, sizeof(k5_));
138 }
139
140 // Runs kinit with the options passed to the constructor. Only call once per
141 // context. While in principle it should be fine to run multiple times, the
142 // code path probably hasn't been tested (kinit does not call this multiple
143 // times).
144 ErrorType Run() {
145 DCHECK(!did_run_);
146 did_run_ = true;
147
148 ErrorType error = Initialize();
149 if (error == ERROR_NONE)
150 error = RunKinit();
151 Finalize();
152 return error;
153 }
154
155 private:
156 // The following code has been adapted from kinit.c in the mit-krb5 code base.
157 // It has been formatted to fit this screen.
158
159 struct Krb5Data {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100160 krb5_ccache out_cc;
161 krb5_principal me;
162 char* name;
163 };
164
165 // Wrapper around krb5 data to get rid of the gotos in the original code.
166 struct KInitData {
167 // Pointer to parent data, not owned.
Lutz Justencb8399d2019-03-08 14:30:17 +0100168 const krb5_context ctx = nullptr;
169 // Pointer to parent data, not owned.
Lutz Justen09cd1c32019-02-15 14:31:49 +0100170 const Krb5Data* k5 = nullptr;
171 krb5_creds my_creds;
172 krb5_get_init_creds_opt* options = nullptr;
173
174 // The lifetime of the |k5| pointer must exceed the lifetime of this object.
Lutz Justencb8399d2019-03-08 14:30:17 +0100175 explicit KInitData(const krb5_context ctx, const Krb5Data* k5)
176 : ctx(ctx), k5(k5) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100177 memset(&my_creds, 0, sizeof(my_creds));
178 }
179
180 ~KInitData() {
181 if (options)
Lutz Justencb8399d2019-03-08 14:30:17 +0100182 krb5_get_init_creds_opt_free(ctx, options);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100183 if (my_creds.client == k5->me)
184 my_creds.client = nullptr;
Lutz Justencb8399d2019-03-08 14:30:17 +0100185 krb5_free_cred_contents(ctx, &my_creds);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100186 }
187 };
188
Lutz Justen09cd1c32019-02-15 14:31:49 +0100189 // Initializes krb5 data.
190 ErrorType Initialize() {
Lutz Justencb8399d2019-03-08 14:30:17 +0100191 krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
Lutz Justen09cd1c32019-02-15 14:31:49 +0100192 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100193 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100194 return TranslateErrorCode(ret);
195 }
196
Lutz Justencb8399d2019-03-08 14:30:17 +0100197 ret = krb5_cc_resolve(ctx.get(), options_.krb5cc_path.c_str(), &k5_.out_cc);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100198 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100199 LOG(ERROR) << ctx.GetErrorMessage(ret) << " resolving ccache";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100200 return TranslateErrorCode(ret);
201 }
202
Lutz Justencb8399d2019-03-08 14:30:17 +0100203 ret = krb5_parse_name_flags(ctx.get(), options_.principal_name.c_str(),
Lutz Justen09cd1c32019-02-15 14:31:49 +0100204 0 /* flags */, &k5_.me);
205 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100206 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when parsing name";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100207 return TranslateErrorCode(ret);
208 }
209
Lutz Justencb8399d2019-03-08 14:30:17 +0100210 ret = krb5_unparse_name(ctx.get(), k5_.me, &k5_.name);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100211 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100212 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when unparsing name";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100213 return TranslateErrorCode(ret);
214 }
215
216 options_.principal_name = k5_.name;
217 return ERROR_NONE;
218 }
219
220 // Finalizes krb5 data.
221 void Finalize() {
Lutz Justencb8399d2019-03-08 14:30:17 +0100222 krb5_free_unparsed_name(ctx.get(), k5_.name);
223 krb5_free_principal(ctx.get(), k5_.me);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100224 if (k5_.out_cc != nullptr)
Lutz Justencb8399d2019-03-08 14:30:17 +0100225 krb5_cc_close(ctx.get(), k5_.out_cc);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100226 memset(&k5_, 0, sizeof(k5_));
227 }
228
229 // Runs the actual kinit code and acquires/renews tickets.
230 ErrorType RunKinit() {
231 krb5_error_code ret;
Lutz Justencb8399d2019-03-08 14:30:17 +0100232 KInitData d(ctx.get(), &k5_);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100233
Lutz Justencb8399d2019-03-08 14:30:17 +0100234 ret = krb5_get_init_creds_opt_alloc(ctx.get(), &d.options);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100235 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100236 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100237 return TranslateErrorCode(ret);
238 }
239
Lutz Justencb8399d2019-03-08 14:30:17 +0100240 ret = krb5_get_init_creds_opt_set_out_ccache(ctx.get(), d.options,
241 k5_.out_cc);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100242 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100243 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while getting options";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100244 return TranslateErrorCode(ret);
245 }
246
247 // To get notified of expiry, see
248 // krb5_get_init_creds_opt_set_expire_callback
249
250 switch (options_.action) {
251 case Action::AcquireTgt:
252 ret = krb5_get_init_creds_password(
Lutz Justencb8399d2019-03-08 14:30:17 +0100253 ctx.get(), &d.my_creds, k5_.me, options_.password.c_str(),
Lutz Justen09cd1c32019-02-15 14:31:49 +0100254 nullptr /* prompter */, nullptr /* data */, 0 /* start_time */,
255 nullptr /* in_tkt_service */, d.options);
256 break;
257 case Action::RenewTgt:
Lutz Justencb8399d2019-03-08 14:30:17 +0100258 ret = krb5_get_renewed_creds(ctx.get(), &d.my_creds, k5_.me, k5_.out_cc,
Lutz Justen09cd1c32019-02-15 14:31:49 +0100259 nullptr /* options_.in_tkt_service */);
260 break;
261 }
262
263 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100264 LOG(ERROR) << ctx.GetErrorMessage(ret);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100265 return TranslateErrorCode(ret);
266 }
267
268 if (options_.action != Action::AcquireTgt) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100269 ret = krb5_cc_initialize(ctx.get(), k5_.out_cc, k5_.me);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100270 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100271 LOG(ERROR) << ctx.GetErrorMessage(ret) << " when initializing cache";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100272 return TranslateErrorCode(ret);
273 }
274
Lutz Justencb8399d2019-03-08 14:30:17 +0100275 ret = krb5_cc_store_cred(ctx.get(), k5_.out_cc, &d.my_creds);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100276 if (ret) {
Lutz Justencb8399d2019-03-08 14:30:17 +0100277 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while storing credentials";
Lutz Justen09cd1c32019-02-15 14:31:49 +0100278 return TranslateErrorCode(ret);
279 }
280 }
281
282 return ERROR_NONE;
283 }
284
Lutz Justencb8399d2019-03-08 14:30:17 +0100285 ScopedKrb5Context ctx;
Lutz Justen09cd1c32019-02-15 14:31:49 +0100286 Krb5Data k5_;
287 Options options_;
288 bool did_run_ = false;
289};
290
291} // namespace
292
293Krb5Interface::Krb5Interface() = default;
294
295Krb5Interface::~Krb5Interface() = default;
296
297ErrorType Krb5Interface::AcquireTgt(const std::string& principal_name,
298 const std::string& password,
Lutz Justenb79da832019-03-08 14:52:53 +0100299 const base::FilePath& krb5cc_path,
300 const base::FilePath& krb5conf_path) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100301 Options options;
302 options.action = Action::AcquireTgt;
303 options.principal_name = principal_name;
304 options.password = password;
Lutz Justenb79da832019-03-08 14:52:53 +0100305 options.krb5cc_path = krb5cc_path.value();
306 setenv(kKrb5ConfigEnvVar, krb5conf_path.value().c_str(), 1);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100307 ErrorType error = KinitContext(std::move(options)).Run();
308 unsetenv(kKrb5ConfigEnvVar);
309 return error;
310}
311
312ErrorType Krb5Interface::RenewTgt(const std::string& principal_name,
Lutz Justenb79da832019-03-08 14:52:53 +0100313 const base::FilePath& krb5cc_path,
314 const base::FilePath& config_path) {
Lutz Justen09cd1c32019-02-15 14:31:49 +0100315 Options options;
316 options.action = Action::RenewTgt;
317 options.principal_name = principal_name;
Lutz Justenb79da832019-03-08 14:52:53 +0100318 options.krb5cc_path = krb5cc_path.value();
319 setenv(kKrb5ConfigEnvVar, config_path.value().c_str(), 1);
Lutz Justen09cd1c32019-02-15 14:31:49 +0100320 ErrorType error = KinitContext(std::move(options)).Run();
321 unsetenv(kKrb5ConfigEnvVar);
322 return error;
323}
324
Lutz Justencb8399d2019-03-08 14:30:17 +0100325ErrorType Krb5Interface::GetTgtStatus(const base::FilePath& krb5cc_path,
326 TgtStatus* status) {
327 DCHECK(status);
328
329 ScopedKrb5Context ctx;
330 krb5_error_code ret = krb5_init_context(ctx.get_mutable_ptr());
331 if (ret) {
332 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while initializing context";
333 return TranslateErrorCode(ret);
334 }
335
336 ScopedKrb5CCache cache(ctx.get());
337 std::string prefixed_krb5cc_path = "FILE:" + krb5cc_path.value();
338 ret = krb5_cc_resolve(ctx.get(), prefixed_krb5cc_path.c_str(),
339 cache.get_mutable_ptr());
340 if (ret) {
341 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while resolving ccache";
342 return TranslateErrorCode(ret);
343 }
344
345 krb5_cc_cursor cur;
346 ret = krb5_cc_start_seq_get(ctx.get(), cache.get(), &cur);
347 if (ret) {
348 LOG(ERROR) << ctx.GetErrorMessage(ret)
349 << " while starting to retrieve tickets";
350 return TranslateErrorCode(ret);
351 }
352
353 krb5_timestamp now = time(nullptr);
354
355 krb5_creds creds;
356 bool found_tgt = false;
357 while ((ret = krb5_cc_next_cred(ctx.get(), cache.get(), &cur, &creds)) == 0) {
358 if (IsTgt(creds)) {
359 if (creds.times.endtime)
360 status->validity_seconds =
361 std::max<int64_t>(creds.times.endtime - now, 0);
362
363 if (creds.times.renew_till) {
364 status->renewal_seconds =
365 std::max<int64_t>(creds.times.renew_till - now, 0);
366 }
367
368 if (found_tgt) {
369 LOG(WARNING) << "More than one TGT found in credential cache '"
370 << krb5cc_path.value() << ".";
371 }
372 found_tgt = true;
373 }
374 krb5_free_cred_contents(ctx.get(), &creds);
375 }
376 if (!found_tgt) {
377 LOG(WARNING) << "No TGT found in credential cache '" << krb5cc_path.value()
378 << ".";
379 }
380
381 if (ret != KRB5_CC_END) {
382 LOG(ERROR) << ctx.GetErrorMessage(ret) << " while retrieving a ticket";
383 return TranslateErrorCode(ret);
384 }
385
386 ret = krb5_cc_end_seq_get(ctx.get(), cache.get(), &cur);
387 if (ret) {
388 LOG(ERROR) << ctx.GetErrorMessage(ret)
389 << " while finishing ticket retrieval";
390 return TranslateErrorCode(ret);
391 }
392
393 return ERROR_NONE;
394}
395
Lutz Justen09cd1c32019-02-15 14:31:49 +0100396} // namespace kerberos