Projects/Timestamps after 2038
This project will make MIT krb5 timestamps work on platforms with 64-bit time_t for times after January 2038, up through January 2106, by repurposing the negative number range of the krb5_timestamp type.
Contents
Problem
krb5.h defines the type krb5_timestamp as krb5_int32. The range of this type extends to time values up through 2038-01-19 03:14:07 UTC. On most 32-bit Unix platforms, the native time_t type also has this limitation, and numerous applications other than MIT krb5 will fail to work after the year 2037. On platforms with a 64-bit time_t type, the limited range of krb5_timestamp will only cause MIT krb5 to fail after 2037.
The krb5_timestamp type is used in numerous libkrb5 structures and function signatures. Changing the size of the type would be an incompatible ABI change, which would pose a disruption to downstream packagers. Changing the type to unsigned would have a more subtle impact on the API, but could still create problems for existing code, particularly code which subtracts two timestamps to produce a possibly negative interval.
In the GSSAPI, intervals (always non-negative) are represented using OM_uint32. This is a limitation but not an important one, since GSSAPI does not make use of absolute timestamps.
The CCAPI (used on Windows) represents timestamps using cc_time_t, which is defined as uint32_t. This should work until 2106. An old, apparently unused CCAPI header defines CC_TIME_T to int.
(See also: [krbdev.mit.edu #8352])
Design
Kerberos does not generally need to represent time values before the year 1970. Therefore, negative krb5_timestamp values can be taken to represent times between 2038 and 2106, using twos-complement conversion as if the type were unsigned.
C language considerations
Conversion to a signed type from a value outside of the range of the signed type is specified in C99 section 6.3.1.1 as "the result is implementation-defined or an implementation-defined signal is raised". C99 section 3.4.1 defines implementation-defined behavior as "unspecified behavior where each implementation documents how the choice is made"; the result must be consistent for the same operands, and the compiler cannot simply assume that implementation-defined behavior never happens. We check in configure.in that conversion to signed types preserves the twos-complement bit representation and does not crash the program; therefore, we can safely rely (for example) on implicit conversions from time_t (if it is 64-bit) to krb5_timestamp to generate the appropriate negative value for times between years 2038 and 2106.
Arithmetic operations on a signed type which overflow or underflow are undefined by the C standard. Compilers frequently take advantage of the assumption that undefined behavior cannot occur when optimizing code, so we should try to avoid undefined behavior when operating on krb5_timestamp values.
Conversions to an unsigned integer type are well-specified (C99 section 6.3.1.3), as are arithmetic operations on unsigned types (C99 section 6.2.5). Values that cannot be represented in the range of the unsigned type are reduced modulo that range.
Platform considerations
Windows defaults to using a 64-bit time_t type unless _USE_32BIT_TIME_T is defined (which is not allowed on 64-bit Windows). We define this symbol internally when building Kerberos on 32-bit Windows, for reasons discussed in [krbdev.mit.edu #2883]. Although the code in win-mac.h is commented as being "to ensure backward compatibility of the ABI", time_t does not appear in our current ABI and the definition does not apply to external code (it only applies when k5-int.h is included).
Operations on timestamps
- Addition: adding a positive or negative interval to a krb5_timestamp value will always produce the correct result with twos-complement arithmetic, but doing so across the y2038 boundary results in undefined behavior in C. To be safe, timestamps should be cast to uint32_t before adding to them, and the result explicitly or implicitly converted back to krb5_timestamp.
- Subtraction: similarly, subtracting two krb5_timestamp values to produce a signed interval will always produce the correct result with twos-complement arithmetic, but the result in C is undefined behavior if the timestamps are on opposite sides of the y2038 boundary. To be safe, timestamps should be cast to uint32_t before subtracting, and the result explicitly cast back to krb5_timestamp (relying on twos-complement signed integer conversion).
- Comparison: comparing two signed timestamps across the y2038 boundary will produce the wrong result; timestamps before the boundary, being positive, will compare greater than timestamps after it. Therefore, timestamps must be cast to uint32_t before comparison.
- Conversion to time_t: we want negative krb5_timestamp values to convert to positive time_t values (on platforms with 64-bit time_t). This can be accomplished by casting the timestamp to uint32_t first.
- Conversion from time_t: twos-complement signed integer conversion will produce the correct result when converting from time_t to krb5_timestamp, so no change is required to code which performs this implicit conversion.
One correct approach would be to always convert from krb5_timestamp to time_t before performing addition, subtraction, or comparison, perhaps using a macro for brevity like:
#define TS(t) ((time_t)(uint32_t)(t))
This approach would produce correct behavior across the y2038 boundary on platforms with 64-bit time_t, and would preserve the ability to use standard C arithmetic and comparison operators. However, the consensus of krb5 developers is that expressions like "TS(t1) > TS(t2)" are confusing to read. Therefore, we will instead define static inline helper functions in k5-int.h to correctly add to, subtract, compare, and convert timestamps:
static inline time_t ts2tt(krb5_timestamp timestamp) { return (time_t)(uint32_t)timestamp; } static inline krb5_deltat ts_delta(krb5_timestamp a, krb5_timestamp b) { return (krb5_deltat)((uint32_t)a - (uint32_t)b); } static inline krb5_timestamp ts_incr(krb5_timestamp ts, krb5_deltat delta) { return (krb5_timestamp)((uint32_t)ts + (uint32_t)delta); } static inline krb5_boolean ts_after(krb5_timestamp a, krb5_timestamp b) { return (uint32_t)a > (uint32_t)b; }
Timestamps are also frequently serialized and deserialized, either to human representations in ASCII or to wire formats. Some serialization methods do not require changes (particularly wire formats that use store_32_be() and load_32_be()), but others will not work without changes for timestamps after the y2038 boundary.
Affected code
Making sure we find all of the affected code and devise test cases for it will be challenging. Any code which performs arithmetic or comparison operations on timestamps or marshals them to or from a string form could be affected. Some useful search terms (aside from "krb5_timestamp" itself):
- krb5_deltat: also a signed 32-bit type; this type is frequently used to hold the difference between two timestamps
- krb5_ticket_times: this type holds the start and end times of tickets, which are frequent targets for arithmetic operations
- "times.": krb5_ticket times is embedded in several other structures under the field name "times" or "krb_times".
- time_t: to catch assignments of krb5_timestamp values to time_t values
- "now" and "_int32" on the same line: some kadm5 code uses krb5_int32 directly instead of krb5_timestamp.
In the GSS krb5 mechanism:
- krb5_gss_inquire_context(), krb5_gss_context_time(), accept_sec_context.c, and s4u_gss_glue.c perform subtraction on krb5_timestamp values to compute the lifetime result.
- acquire_cred.c computes a refresh time using arithmetic operations, and marshals it into a ccache config variable. It also compares cred expiry times to the current time and uses subtraction to produce a cred expiration time.
- iakerb.c and init_sec_context.c add time_req to the current time to produce a requested ticket end time.
In libkrb5:
- valid_times.c performs subtractions and comparison on timestamp values to determine if a ticket is currently valid.
- The in_clock_skew() macro in int-proto.h performs subtraction and comparison on timestamp values to determine if two timestamps are within the clock skew of each other. (This macro is used only once and implicitly depends on a local variable "context", so it could perhaps be eliminated. Or it could be adjusted to make the context explicit and used in more places.)
- get_in_tkt.c:verify_as_reply() computes an offset from two timestamps, or checks whether the ticket start time is within the clock skew of the current time.
- get_in_tkt.c:set_request_times() adds offsets to timestamps to produce request timestamps. It uses krb5int_add32() to avoid signed overflow, and should instead convert to unsigned.
- get_in_tkt.c:note_req_timestamp() computes an offset from two timestamps.
- get_in_tkt.c:k5_populate_gic_opt() takes the difference of two timestamps to convert from a deprecated API which uses an absolute end time.
- get_in_tkt.c:verify_as_reply() and gc_via_tkt.c:krb5int_process_tgs_reply() compare timestamps to detect request alteration.
- get_creds.c:get_cached_local_tgt() compares timestamps to see if the TGT is expired.
- gic_pwd.c:warn_pw_expiry() compares and subtracts timestamps to determine whether the expiry time is less than a week from the current time.
- vfy_increds.c:get_vfy_creds() adds an interval to the current time to set the requested credential end time.
- toffset.c:krb5_set_real_time() subtracts timestamps to compute a time offset.
- timeofday.c:krb5_check_clockskew() subtracts timestamps to determine whether the difference is within the allowed clockskew.
- ustime.c contains code to adjust a timestamp by the context offset amount.
- kt_file.c:more_recent() compares timestamps to make inferences about kvno wraparound.
- rc_dfl.c:alive() adds an interval to a timestamp and compares it to the current timestamp to determine if a replay record is still active.
- cccursor.c:krb5_cccol_last_change_time() compares timestamps to compute a maximum.
- cc_memory.c:update_mcc_change_time() compares and increments timestamps in order to ensure that each call results in a different value.
- cc_keyring.c:krcc_update_change_time() does the same.
- cc_keyring.c:update_keyring_expiration() and krcc_store() compare and subtracts timestamps to compute an expiration time.
- cc_retr.c:times_match() and stdcc_util.c:times_match() compare timestamps to determine if a credential matches the template timestamp requirements.
- stdcc_util.c adjusts timestamps by the context time offset in several places.
- t_kerb.c converts a timestamp to a time_t in test_string_to_timestamp().
In libkadm5:
- chpass_util.c adds an interval to a timestamp to determine when the password can next be modified.
- server_acl.c adds and compares timestamps to enforce maximum lifetimes in restrictions.
- svr_principal.c adds timestamps to implement the policy pw_max_life field.
In libkdb5, timestamps are mostly just stored and retrieved, but kdb5.c:find_actkvno() compares timestamps to find the active master key.
Outside of the libraries:
- lockout.c (LDAP and DB2) compares timestamps to determine if a principal entry is locked or was administratively unlocked.
- kdc_preauth_ec.c and kdc_preauth_encts.c check if timestamps are within clock skew.
- fast_util.c:kdc_fast_read_cookie() compares the cookie time (a time_t) to the current time (a krb5_timestamp) to see if the cookie is expired. kdc_fast_make_cookie() converts a timestamp to a time_t to set the cookie time.
- tgs_policy.c contains several timestamp comparisons to determine if a ticket or principal is valid at the current time.
- do_as_req.c:get_key_exp() computes the minimum of two timestamps.
- do_tgs_req.c:process_tgs_req() subtracts and compares timestamps to compute the end time of a renewed ticket.
- kdc_util.c:kdc_get_ticket_endtime() and and kdc_get_ticket_renewtime() compute the minimum of several timestamps.
- kdc/extern.c:kdc_infinity is defined as KRB5_INT32_MAX; it should be changed to -1.
- kinit.c subtracts the current time from a timestamp to compute the relative start time.
- klist.c compares timestamps to determine if credentials are expired.
- ksu/ccache.c subtracts timestamps to determine if a ticket is expired.
- kdb5_mkey.c compares timestamps to determine if a master key entry is active.
- windows/cns/tktlist.c subtracts a modifier from the ticket start and end time.
- Leash stores timestamps within internal structures, and later subtracts the current time or converts them to localized strings. As the Leash code does not currently use k5-int.h, it may be simplest to store these timestamps as time_t, and cast them to unsigned 32-bit values as they come out of krb5 credential structures.
- ms2mit.c:cc_has_tickets() compares a timestamp to the current time to determine if a credential is expired.
Timestamps are serialized in:
- The libkrb5 and libgssapi_krb5 ser_ framework: uses store_32_be/load_32_be (no changes needed)
- The ASN.1 framework: encoding routines operate on time_t; proper conversion needed in glue functions (encode_kerberos_time())
- send_tgs.c: uses the current timestamp as a nonce. RFC 4120 specifies the nonce as being unsigned 32-bit, while MIT krb5 and Heimdal encode it as signed 32-bit. The AS code uses a random nonce in the range 0..2^32-1; the TGS code should probably be changed to do this as well. [Done in [krbdev.mit.edu #8582]]
- The kadmin protocol: xdr_krb5_timestamp() uses xdr_int32() which uses xdr_long(), which checks value ranges. Negative values should survive a round trip but it might be cleaner to use xdr_u_int32().
- The DB2 KDB module: uses store_32_be/load_32_be (no changes needed)
- The LDAP KDB module: uses strftime()/strptime(); getstringtime() needs to properly convert from krb5_timestamp to time_t
- JSON serialization of credentials in krb5 GSS mech: encoding uses k5_json_array_fmt() and may be nonconformant (values are provided as int32_t arguments but are processed from argv list as int); negative values should survive a round trip but would ideally be encoded as positive numbers
- The PAC processing code: k5_time_to_seconds_since_1970() won't produce a timestamp after 2038. k5_seconds_since_1970_to_time() shouldn't need changes at it starts by converting to an unsigned type.
- The refresh timestamp in krb5 GSS (acquire_cred.c): currently marshalled into ASCII as a signed long; should be marshalled as an unsigned value.
- krb5_timestamp_to_string() and krb5_timestamp_to_sfstring(): need to properly convert from krb5_timestamp to time_t. krb5_string_to_timestamp() shouldn't need changes.
- getdate.y (used by kadmin for user input): does not operate on krb5_timestamp values, but returns an error if given a specification for a date after 2038, even if the result could be expressed in a time_t value.
- t_replay.c: reads timestamps from argv using atol(). On a platform where long is 32-bit and time_t is 64-bit (Windows, Linux x32), this won't work for times after 2038. Using atoll() (specified in C99, POSIX-2001) is a simple workaround.
- kdc_log.c: marshals timestamps into ASCII as signed int; should use an unsigned type.
- kdb5_util: kdb5_mkey.c displays timestamps using localtime(); must convert the timestamp to time_t correctly. tabdump.c uses gmtime() and also must correctly convert the timestamp to time_t. dump.c converts timestamps to ASCII as signed integers, and should use unsigned integers.
- kadmin: displays timestamps using localtime(); must convert the timestamp to time_t correctly.
- tcl_kadm5.c: converts timestamps to ASCII as signed integers; should use unsigned integers.
- kinit and ksu print timestamps using similar functions which call krb5_timestamp_to_sfstring(). Both functions perform an unnecessary conversion from krb5_timestamp to time_t and back. This code does not strictly require changes but should be simplified.
- Several trace log messages print timestamps using {long} (this is the only current use of {long}, so it is easy to search for those messages in k5-trace.h). Although it is not critical, it would be nice if they could be displayed as unsigned, or perhaps in a condensed ISO format like YYYYMMDDTHHMMSS.
Test cases
Since we do not expect timestamps after 2038 to work on platforms with 32-bit time_t, all y2038 tests will be suppressed on those platforms, using an AC_CHECK_SIZEOF configure test feeding into a runenv.py variable.
Test cases for time intervals across the year 2038 boundary will be easiest to write by assuming that the current time is before the year 2038 boundary and that 21 years in the future will be afterwards. Such tests should continue to work after the year 2038 passes, but will not test the boundary case
For the GSSAPI krb5 mechanism:
- Inquire the lifetime of a credential which expires across the year 2038 boundary, with gss_acquire_cred() and gss_inquire_cred().
- Inquire the lifetime of a context which expires across the year 2038 boundary, with gss_init_sec_context(), gss_accept_sec_context(), gss_inquire_context(), and gss_context_time().
For libkrb5:
- Using a test harness in lib/krb5/krb, call krb5int_validate_times() using a context with the debugging time set and pre-filled krb5_ticket_times objects. Exercise the cases where the start time and current time are across the y2038 boundary, and where the current time and ticket end time are across the boundary.
- Start a KDC with a time offset 21 years in the future. Verify that KDC clock skew correction works, using t_skew.py as a reference. Set a password expiration time shortly after the offset KDC time and verify that password expiration calculations work for timestamps after y2038.
- Without any KDC time offset, acquire a ticket with a long lifetime so that it expires after y2038. Verify that it is usable and that klist displays the proper expiration time.
Related issues
kadmin policy objects contain intervals (pw_min_life and pw_max_life) which are represented using "long" and marshalled using xdr_long(), which has a 32-bit range. This is not a y2038 issue as it affects intervals greater than 2^31-1 seconds, not absolute times. [krbdev.mit.edu #8054]