-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 Hi, Attached is a patch against current CVS that adds support for the GSSAPI SASL mechanism. It was written from scratch, after reading the patch from Colin Walters against a much older version of dovecot. Other then support for the 'GSSAPI' mechanism, it contains the following changes: - - Added 'auth_krb5_keytab' option for overriding default keytab location. The gssapi library already uses the environment variable KRB5_KTNAME to allow overriding of the keytab location, so I didn't have to make any changes on the login process side. - - Added 'need_passdb' member to mech_module and allow having no passdb's specified if no mechanisms need them (only useful for GSSAPI at the moment) Cheers, Jelmer - -- Jelmer Vernooij <jelmer@samba.org> -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.2 (GNU/Linux) iD8DBQFDVgaEPa9Uoh7vUnYRAscGAJ9Fa4E5ze5IX3UeAfjwg7kzj5tgtQCcDva7 O5VTeOeJ7zpmHullxQN6q1Y= =60w7 -----END PGP SIGNATURE----- Index: configure.in =================================================================== RCS file: /home/cvs/dovecot/configure.in,v retrieving revision 1.214 diff -u -r1.214 configure.in --- configure.in 23 Sep 2005 13:28:40 -0000 1.214 +++ configure.in 19 Oct 2005 08:25:31 -0000 @@ -118,6 +118,15 @@ fi, want_bsdauth=yes) +AC_ARG_WITH(gssapi, +[ --with-gssapi Build with GSSAPI authentication support (default)], + if test x$withval = xno; then + want_gssapi=no + else + want_gssapi_yes + fi, + want_gssapi=yes) + AC_ARG_WITH(ldap, [ --with-ldap Build with LDAP support], if test x$withval = xno; then @@ -1192,6 +1201,17 @@ ]) fi +have_gssapi=no +if test $want_gssapi = yes; then + AC_CHECK_PROG(KRB5CONFIG, krb5-config, YES, NO) + if test $KRB5CONFIG = YES; then + AUTH_LIBS="$AUTH_LIBS `krb5-config --libs gssapi`" + AUTH_CFLAGS="$AUTH_CFLAGS `krb5-config --cflags gssapi`" + AC_DEFINE(MECH_GSSAPI,, Build with GSSAPI support) + have_gssapi=yes + fi +fi + if test $want_ldap = yes; then AC_CHECK_LIB(ldap, ldap_init, [ AC_CHECK_HEADER(ldap.h, [ @@ -1486,5 +1506,6 @@ echo "Building with IPv6 support .......... : $want_ipv6" echo "Building with pop3 server ........... : $want_pop3d" echo "Building with mail delivery agent .. : $want_deliver" +echo "Building with GSSAPI support ........ : $have_gssapi" echo "Building with user database modules . :$userdb" echo "Building with password lookup modules :$passdb" Index: dovecot-example.conf =================================================================== RCS file: /home/cvs/dovecot/dovecot-example.conf,v retrieving revision 1.155 diff -u -r1.155 dovecot-example.conf --- dovecot-example.conf 16 Oct 2005 14:59:12 -0000 1.155 +++ dovecot-example.conf 19 Oct 2005 08:25:31 -0000 @@ -568,9 +568,13 @@ # automatically created and destroyed as needed. #auth_worker_max_count = 30 +# Kerberos keytab to use for the GSSAPI mechanism. Will use the system +# default (usually /etc/krb5.keytab) if not specified. +#auth_krb5_keytab = + auth default { # Space separated list of wanted authentication mechanisms: - # plain digest-md5 cram-md5 apop anonymous + # plain digest-md5 cram-md5 apop anonymous gssapi mechanisms = plain # Index: src/auth/Makefile.am =================================================================== RCS file: /home/cvs/dovecot/src/auth/Makefile.am,v retrieving revision 1.47 diff -u -r1.47 Makefile.am --- src/auth/Makefile.am 7 Aug 2005 11:41:19 -0000 1.47 +++ src/auth/Makefile.am 19 Oct 2005 08:25:31 -0000 @@ -54,6 +54,7 @@ mech-cram-md5.c \ mech-digest-md5.c \ mech-ntlm.c \ + mech-gssapi.c \ mech-rpa.c \ mech-apop.c \ passdb.c \ Index: src/auth/auth.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/auth.c,v retrieving revision 1.19 diff -u -r1.19 auth.c --- src/auth/auth.c 16 Oct 2005 14:06:59 -0000 1.19 +++ src/auth/auth.c 19 Oct 2005 08:25:31 -0000 @@ -56,8 +56,6 @@ } t_pop(); - if (auth->passdbs == NULL) - i_fatal("No password databases set"); if (auth->userdbs == NULL) i_fatal("No user databases set"); return auth; @@ -130,6 +128,9 @@ struct mech_module_list *list; for (list = auth->mech_modules; list != NULL; list = list->next) { + if (list->module.need_passdb && + !auth->passdbs) + break; if (list->module.passdb_need_plain && !auth_passdb_list_have_plain(auth)) break; Index: src/auth/mech-anonymous.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-anonymous.c,v retrieving revision 1.13 diff -u -r1.13 mech-anonymous.c --- src/auth/mech-anonymous.c 23 Apr 2005 09:11:49 -0000 1.13 +++ src/auth/mech-anonymous.c 19 Oct 2005 08:25:31 -0000 @@ -57,6 +57,7 @@ MEMBER(flags) MECH_SEC_ANONYMOUS, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) FALSE, MEMBER(passdb_need_credentials) FALSE, Index: src/auth/mech-apop.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-apop.c,v retrieving revision 1.16 diff -u -r1.16 mech-apop.c --- src/auth/mech-apop.c 23 Apr 2005 09:11:49 -0000 1.16 +++ src/auth/mech-apop.c 19 Oct 2005 08:25:31 -0000 @@ -162,6 +162,7 @@ MEMBER(flags) MECH_SEC_PRIVATE | MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) FALSE, MEMBER(passdb_need_credentials) TRUE, Index: src/auth/mech-cram-md5.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-cram-md5.c,v retrieving revision 1.20 diff -u -r1.20 mech-cram-md5.c --- src/auth/mech-cram-md5.c 8 Jan 2005 21:37:32 -0000 1.20 +++ src/auth/mech-cram-md5.c 19 Oct 2005 08:25:32 -0000 @@ -191,6 +191,7 @@ MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) FALSE, MEMBER(passdb_need_credentials) TRUE, Index: src/auth/mech-digest-md5.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-digest-md5.c,v retrieving revision 1.34 diff -u -r1.34 mech-digest-md5.c --- src/auth/mech-digest-md5.c 8 Jan 2005 21:37:32 -0000 1.34 +++ src/auth/mech-digest-md5.c 19 Oct 2005 08:25:32 -0000 @@ -619,6 +619,7 @@ MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | MECH_SEC_MUTUAL_AUTH, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) FALSE, MEMBER(passdb_need_credentials) TRUE, Index: src/auth/mech-login.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-login.c,v retrieving revision 1.12 diff -u -r1.12 mech-login.c --- src/auth/mech-login.c 23 Apr 2005 09:11:49 -0000 1.12 +++ src/auth/mech-login.c 19 Oct 2005 08:25:32 -0000 @@ -87,6 +87,7 @@ MEMBER(flags) MECH_SEC_PLAINTEXT, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) TRUE, MEMBER(passdb_need_credentials) FALSE, Index: src/auth/mech-ntlm.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-ntlm.c,v retrieving revision 1.19 diff -u -r1.19 mech-ntlm.c --- src/auth/mech-ntlm.c 23 Apr 2005 09:11:49 -0000 1.19 +++ src/auth/mech-ntlm.c 19 Oct 2005 08:25:32 -0000 @@ -281,6 +281,7 @@ MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) FALSE, MEMBER(passdb_need_credentials) TRUE, Index: src/auth/mech-plain.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-plain.c,v retrieving revision 1.31 diff -u -r1.31 mech-plain.c --- src/auth/mech-plain.c 23 Apr 2005 09:11:49 -0000 1.31 +++ src/auth/mech-plain.c 19 Oct 2005 08:25:32 -0000 @@ -103,6 +103,7 @@ MEMBER(flags) MECH_SEC_PLAINTEXT, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) TRUE, MEMBER(passdb_need_credentials) FALSE, Index: src/auth/mech-rpa.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech-rpa.c,v retrieving revision 1.19 diff -u -r1.19 mech-rpa.c --- src/auth/mech-rpa.c 23 Apr 2005 09:11:49 -0000 1.19 +++ src/auth/mech-rpa.c 19 Oct 2005 08:25:32 -0000 @@ -614,6 +614,7 @@ MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | MECH_SEC_MUTUAL_AUTH, + MEMBER(need_passdb) TRUE, MEMBER(passdb_need_plain) FALSE, MEMBER(passdb_need_credentials) TRUE, Index: src/auth/mech.c =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech.c,v retrieving revision 1.54 diff -u -r1.54 mech.c --- src/auth/mech.c 7 Jan 2005 19:55:50 -0000 1.54 +++ src/auth/mech.c 19 Oct 2005 08:25:32 -0000 @@ -54,6 +54,9 @@ extern struct mech_module mech_ntlm; extern struct mech_module mech_rpa; extern struct mech_module mech_anonymous; +#ifdef MECH_GSSAPI +extern struct mech_module mech_gssapi; +#endif void mech_init(void) { @@ -65,6 +68,9 @@ mech_register_module(&mech_ntlm); mech_register_module(&mech_rpa); mech_register_module(&mech_anonymous); +#ifdef MECH_GSSAPI + mech_register_module(&mech_gssapi); +#endif } void mech_deinit(void) @@ -77,4 +83,7 @@ mech_unregister_module(&mech_ntlm); mech_unregister_module(&mech_rpa); mech_unregister_module(&mech_anonymous); +#ifdef MECH_GSSAPI + mech_unregister_module(&mech_gssapi); +#endif } Index: src/auth/mech.h =================================================================== RCS file: /home/cvs/dovecot/src/auth/mech.h,v retrieving revision 1.35 diff -u -r1.35 mech.h --- src/auth/mech.h 9 Jan 2005 16:54:48 -0000 1.35 +++ src/auth/mech.h 19 Oct 2005 08:25:32 -0000 @@ -24,6 +24,7 @@ const char *mech_name; enum mech_security_flags flags; + unsigned int need_passdb:1; unsigned int passdb_need_plain:1; unsigned int passdb_need_credentials:1; Index: src/master/auth-process.c =================================================================== RCS file: /home/cvs/dovecot/src/master/auth-process.c,v retrieving revision 1.82 diff -u -r1.82 auth-process.c --- src/master/auth-process.c 1 Oct 2005 10:52:16 -0000 1.82 +++ src/master/auth-process.c 19 Oct 2005 08:25:33 -0000 @@ -458,6 +458,8 @@ env_put("SSL_REQUIRE_CLIENT_CERT=1"); if (set->ssl_username_from_cert) env_put("SSL_USERNAME_FROM_CERT=1"); + if (set->krb5_keytab) + env_put(t_strconcat("KRB5_KTNAME=", set->krb5_keytab, NULL)); restrict_process_size(set->process_size, (unsigned int)-1); } Index: src/master/master-settings.c =================================================================== RCS file: /home/cvs/dovecot/src/master/master-settings.c,v retrieving revision 1.92 diff -u -r1.92 master-settings.c --- src/master/master-settings.c 16 Oct 2005 14:59:17 -0000 1.92 +++ src/master/master-settings.c 19 Oct 2005 08:25:33 -0000 @@ -155,6 +155,7 @@ DEF(SET_STR, username_chars), DEF(SET_STR, username_translation), DEF(SET_STR, anonymous_username), + DEF(SET_STR, krb5_keytab), DEF(SET_BOOL, verbose), DEF(SET_BOOL, debug), @@ -353,6 +354,7 @@ MEMBER(username_chars) "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@", MEMBER(username_translation) "", MEMBER(anonymous_username) "anonymous", + MEMBER(krb5_keytab) NULL, MEMBER(verbose) FALSE, MEMBER(debug) FALSE, Index: src/master/master-settings.h =================================================================== RCS file: /home/cvs/dovecot/src/master/master-settings.h,v retrieving revision 1.60 diff -u -r1.60 master-settings.h --- src/master/master-settings.h 1 Oct 2005 10:52:16 -0000 1.60 +++ src/master/master-settings.h 19 Oct 2005 08:25:33 -0000 @@ -162,6 +162,7 @@ const char *username_chars; const char *username_translation; const char *anonymous_username; + const char *krb5_keytab; int verbose, debug; int ssl_require_client_cert; --- /dev/null 2005-10-17 12:49:06.896857000 +0200 +++ src/auth/mech-gssapi.c 2005-10-19 10:17:45.000000000 +0200 @@ -0,0 +1,321 @@ +/* + * GSSAPI Module + * + * Copyright (c) 2005 Jelmer Vernooij <jelmer@samba.org> + * + * Related standards: + * - draft-ietf-sasl-gssapi-03 + * - RFC2222 + * + * Some parts inspired by an older patch from Colin Walters + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "common.h" +#include "mech.h" +#include "passdb.h" +#include "str.h" +#include "buffer.h" +#include "hex-binary.h" +#include "safe-memset.h" +#include "hostpid.h" + +#ifdef MECH_GSSAPI + +#include <gssapi/gssapi.h> + +struct gssapi_auth_request { + struct auth_request auth_request; + gss_ctx_id_t gss_ctx; + gss_cred_id_t service_cred; + + enum { + GSS_STATE_SEC_CONTEXT, + GSS_STATE_WRAP, + GSS_STATE_UNWRAP + } sasl_gssapi_state; + + gss_name_t src_name; + + pool_t pool; +}; + +static void auth_request_log_gss_error(struct auth_request *request, OM_uint32 status_value, int status_type, const char *description) +{ + OM_uint32 message_context = 0; + OM_uint32 major_status, minor_status; + gss_buffer_desc status_string; + + do { + major_status = gss_display_status(&minor_status, status_value, + status_type, GSS_C_NO_OID, &message_context, + &status_string); + + auth_request_log_error(request, "gssapi", "While %s: %s", + description, (char *)status_string.value); + + major_status = gss_release_buffer(&minor_status, &status_string); + } while (message_context != 0); +} + +static struct auth_request *mech_gssapi_auth_new(void) +{ + struct gssapi_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create("gssapi_auth_request", 512); + request = p_new(pool, struct gssapi_auth_request, 1); + request->pool = pool; + + request->gss_ctx = GSS_C_NO_CONTEXT; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +static OM_uint32 obtain_service_credentials(struct auth_request *request, gss_cred_id_t *ret) +{ + OM_uint32 major_status, minor_status; + string_t *principal_name; + gss_buffer_desc inbuf; + gss_name_t gss_principal; + + principal_name = t_str_new(40); + str_append(principal_name, t_str_lcase(request->service)); + str_append(principal_name, "@"); + str_append(principal_name, my_hostname); + + auth_request_log_info(request, "gssapi", + "Obtaining credentials for %s", str_c(principal_name)); + + inbuf.length = str_len(principal_name); + inbuf.value = (void*) str_c(principal_name); + + major_status = gss_import_name(&minor_status, &inbuf, + GSS_C_NT_HOSTBASED_SERVICE, &gss_principal); + + str_free(principal_name); + + if (GSS_ERROR(major_status)) { + auth_request_log_gss_error(request, major_status, GSS_C_GSS_CODE, + "importing principal name"); + return major_status; + } + + major_status = gss_acquire_cred(&minor_status, gss_principal, 0, + GSS_C_NULL_OID_SET, GSS_C_ACCEPT, ret, NULL, NULL); + + if (GSS_ERROR(major_status)) { + auth_request_log_gss_error(request, major_status, GSS_C_GSS_CODE, + "acquiring service credentials"); + auth_request_log_gss_error(request, minor_status, GSS_C_MECH_CODE, + "acquiring service credentials"); + return major_status; + } + + gss_release_name(&minor_status, gss_principal); + + return major_status; +} + +static void gssapi_sec_context(struct gssapi_auth_request *request, + gss_buffer_desc inbuf) +{ + OM_uint32 major_status, minor_status; + gss_buffer_desc outbuf; + + major_status = gss_accept_sec_context ( + &minor_status, + &request->gss_ctx, + request->service_cred, + &inbuf, + GSS_C_NO_CHANNEL_BINDINGS, + &request->src_name, + NULL, /* mech_type */ + &outbuf, + NULL, /* ret_flags */ + NULL, /* time_rec */ + NULL /* delegated_cred_handle */ + ); + + if (GSS_ERROR(major_status)) { + auth_request_log_gss_error(&request->auth_request, major_status, GSS_C_GSS_CODE, + "processing incoming data"); + auth_request_log_gss_error(&request->auth_request, minor_status, GSS_C_MECH_CODE, + "processing incoming data"); + + auth_request_fail(&request->auth_request); + return; + } + + if (major_status == GSS_S_COMPLETE) { + request->sasl_gssapi_state = GSS_STATE_WRAP; + auth_request_log_info(&request->auth_request, "gssapi", + "security context state completed."); + } else { + auth_request_log_info(&request->auth_request, "gssapi", + "Processed incoming packet correctly, waiting for another."); + } + + request->auth_request.callback(&request->auth_request, + AUTH_CLIENT_RESULT_CONTINUE, + outbuf.value, outbuf.length); + + major_status = gss_release_buffer(&minor_status, &outbuf); +} + +static void gssapi_wrap(struct gssapi_auth_request *request, + gss_buffer_desc inbuf) +{ + OM_uint32 major_status, minor_status; + gss_buffer_desc outbuf; + char ret[4]; + + /* The clients return data should be empty here */ + + ret[0] = 0x01; /* Only authentication, no integrity or confidentiality protection (yet?) */ + ret[1] = 0xFF; + ret[2] = 0xFF; + ret[3] = 0xFF; + + inbuf.length = 4; + inbuf.value = ret; + + major_status = gss_wrap(&minor_status, request->gss_ctx, 0, + GSS_C_QOP_DEFAULT, + &inbuf, NULL, &outbuf); + + if (GSS_ERROR(major_status)) { + auth_request_log_gss_error(&request->auth_request, major_status, GSS_C_GSS_CODE, + "sending security layer negotiation"); + auth_request_log_gss_error(&request->auth_request, minor_status, GSS_C_MECH_CODE, + "sending security layer negotiation"); + auth_request_fail(&request->auth_request); + return; + } + + auth_request_log_info(&request->auth_request, "gssapi", + "Negotiated security layer"); + + request->auth_request.callback(&request->auth_request, + AUTH_CLIENT_RESULT_CONTINUE, + outbuf.value, outbuf.length); + + major_status = gss_release_buffer(&minor_status, &outbuf); + + request->sasl_gssapi_state = GSS_STATE_UNWRAP; +} + +static void gssapi_unwrap(struct gssapi_auth_request *request, + gss_buffer_desc inbuf) +{ + OM_uint32 major_status, minor_status; + gss_buffer_desc outbuf; + + major_status = gss_unwrap(&minor_status, request->gss_ctx, + &inbuf, &outbuf, NULL, NULL); + + if (GSS_ERROR(major_status)) { + auth_request_log_gss_error(&request->auth_request, major_status, GSS_C_GSS_CODE, + "final negotiation: gss_unwrap"); + auth_request_fail(&request->auth_request); + return; + } + + if (outbuf.length <= 4) { + auth_request_log_error(&request->auth_request, "gssapi", + "Invalid response length"); + auth_request_fail(&request->auth_request); + return; + } + + request->auth_request.user = p_strndup(request->auth_request.pool, + ((unsigned char*) outbuf.value)+4, + outbuf.length-4); + + auth_request_success(&request->auth_request, NULL, 0); +} + +static void +mech_gssapi_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + gss_buffer_desc inbuf; + + inbuf.value = (char*) data; + inbuf.length = data_size; + + switch (gssapi_request->sasl_gssapi_state) { + case GSS_STATE_SEC_CONTEXT: gssapi_sec_context(gssapi_request, inbuf); break; + case GSS_STATE_WRAP: gssapi_wrap(gssapi_request, inbuf); break; + case GSS_STATE_UNWRAP: gssapi_unwrap(gssapi_request, inbuf); break; + } +} + +static void +mech_gssapi_auth_initial(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + OM_uint32 major_status; + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + + major_status = obtain_service_credentials(request, &gssapi_request->service_cred); + + if (GSS_ERROR(major_status)) { + auth_request_internal_failure(request); + return; + } + gssapi_request->src_name = GSS_C_NO_NAME; + + gssapi_request->sasl_gssapi_state = GSS_STATE_SEC_CONTEXT; + + if (data_size == 0) { + /* The client should go first */ + request->callback(request, + AUTH_CLIENT_RESULT_CONTINUE, + NULL, 0); + } else { + mech_gssapi_auth_continue(request, data, data_size); + } +} + + +static void +mech_gssapi_auth_free(struct auth_request *request) +{ + OM_uint32 major_status, minor_status; + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + + major_status = gss_delete_sec_context(&minor_status, + &gssapi_request->gss_ctx, + GSS_C_NO_BUFFER); + + major_status = gss_release_cred(&minor_status, &gssapi_request->service_cred); + + pool_unref(request->pool); +} + +const struct mech_module mech_gssapi = { + "GSSAPI", + + MEMBER(flags) 0, + + MEMBER(need_passdb) FALSE, + MEMBER(passdb_need_plain) FALSE, + MEMBER(passdb_need_credentials) FALSE, + + mech_gssapi_auth_new, + mech_gssapi_auth_initial, + mech_gssapi_auth_continue, + mech_gssapi_auth_free +}; + +#endif Index: AUTHORS =================================================================== RCS file: /home/cvs/dovecot/AUTHORS,v retrieving revision 1.11 diff -u -r1.11 AUTHORS --- AUTHORS 19 Aug 2004 03:56:01 -0000 1.11 +++ AUTHORS 19 Oct 2005 08:38:49 -0000 @@ -12,6 +12,8 @@ Joshua Goodall <joshua@roughtrade.net> (src/auth/password-scheme-cram-md5.c, src/util/dovecotpw.c) +Jelmer Vernooij <jelmer@samba.org> (src/auth/mech-gssapi.c) + This product includes software developed by Computing Services at Carnegie Mellon University (http://www.cmu.edu/computing/). (src/lib/base64.c, src/lib/mkgmtime.c)