ASN1_TIME to time_t conversion

Times are stored as a string internally, on the format YYmmddHHMMSS or YYYYmmddHHMMSS.

At the end of the string there is room for fractions of seconds and timezone, but let's ignore that for now, and have some (untested) code.

Note: also see Bryan Olson's answer below, which discusses the undefined behavior due to the i++'s. Also see Seak's answer which removes the undefined behavior.

static time_t ASN1_GetTimeT(ASN1_TIME* time)
{
    struct tm t;
    const char* str = (const char*) time->data;
    size_t i = 0;

    memset(&t, 0, sizeof(t));

    if (time->type == V_ASN1_UTCTIME) /* two digit year */
    {
        t.tm_year = (str[i++] - '0') * 10 + (str[++i] - '0');
        if (t.tm_year < 70)
        t.tm_year += 100;
    }
    else if (time->type == V_ASN1_GENERALIZEDTIME) /* four digit year */
    {
        t.tm_year = (str[i++] - '0') * 1000 + (str[++i] - '0') * 100 + (str[++i] - '0') * 10 + (str[++i] - '0');
        t.tm_year -= 1900;
    }
    t.tm_mon = ((str[i++] - '0') * 10 + (str[++i] - '0')) - 1; // -1 since January is 0 not 1.
    t.tm_mday = (str[i++] - '0') * 10 + (str[++i] - '0');
    t.tm_hour = (str[i++] - '0') * 10 + (str[++i] - '0');
    t.tm_min  = (str[i++] - '0') * 10 + (str[++i] - '0');
    t.tm_sec  = (str[i++] - '0') * 10 + (str[++i] - '0');

    /* Note: we did not adjust the time based on time zone information */
    return mktime(&t);
}

From the openssl code, it seems to be a bad idea:

/*
 * FIXME: mktime assumes the current timezone
 * instead of UTC, and unless we rewrite OpenSSL
 * in Lisp we cannot locally change the timezone
 * without possibly interfering with other parts
 * of the program. timegm, which uses UTC, is
 * non-standard.
 * Also time_t is inappropriate for general
 * UTC times because it may a 32 bit type.
 */

Note that you can use ASN1_TIME_diff() to get the number of days / seconds between two ASN1_TIME*. If you pass NULL as ASN1_TIME *from, you can get the difference from current time.


I have to disagree with Jan and Jack. Someone actually copied and used the given code where I work, and it fails. Here's why, from the C99 standard:

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression." -- ISO/IEC 9899:1999, "Programming Languages - C", Section 6.5, Clause 1.

When compiling the given code, gcc (version 4.1.2) says, nine times,

warning: operation on ‘i’ may be undefined.

The code has undefined behavior. The bug I actually saw was year "13" being read as 11. That's because:

The result of the postfix ++ operator is the value of the operand. After the result is obtained, the value of the operand is incremented. [...] The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point. -- Ibid, Section 6.5.2.4, Clause 2.

Both instances of str[i++] in:

t.tm_year = (str[i++] - '0') * 10 + (str[i++] - '0');

read the '1' in "13", because they both happened before the update of i. All the lines that update i multiple times have the same problems.

The easy fix is to get rid of 'i' and replace all those lines with a single call to sscanf().

Even with that fix, I wouldn't like the code. In addition to ignoring a timezone suffix, it doesn't check for errors or unexpected values. Certificates are a security mechanism, and security code has strict requirements for robustness. The corner cases that your program does not handled correctly are the ones your attackers fill feed it.


Well, I don't know about the rest, but that code is just wrong for the cases the ASN1_TIME is in UTCTime format : YYMMDDHHMMSSZ.

I tried and returns the value wrong, even with the correction from ++i to i++, nevertheless ... the code is not an example of good coding.

I manage to fix it, it was the sums of the char types:

static time_t ASN1_GetTimeT(ASN1_TIME* time){
    struct tm t;
    const char* str = (const char*) time->data;
    size_t i = 0;

    memset(&t, 0, sizeof(t));

    if (time->type == V_ASN1_UTCTIME) {/* two digit year */
        t.tm_year = (str[i++] - '0') * 10;
        t.tm_year += (str[i++] - '0');
        if (t.tm_year < 70)
            t.tm_year += 100;
    } else if (time->type == V_ASN1_GENERALIZEDTIME) {/* four digit year */
        t.tm_year = (str[i++] - '0') * 1000;
        t.tm_year+= (str[i++] - '0') * 100;
        t.tm_year+= (str[i++] - '0') * 10;
        t.tm_year+= (str[i++] - '0');
        t.tm_year -= 1900;
    }
    t.tm_mon  = (str[i++] - '0') * 10;
    t.tm_mon += (str[i++] - '0') - 1; // -1 since January is 0 not 1.
    t.tm_mday = (str[i++] - '0') * 10;
    t.tm_mday+= (str[i++] - '0');
    t.tm_hour = (str[i++] - '0') * 10;
    t.tm_hour+= (str[i++] - '0');
    t.tm_min  = (str[i++] - '0') * 10;
    t.tm_min += (str[i++] - '0');
    t.tm_sec  = (str[i++] - '0') * 10;
    t.tm_sec += (str[i++] - '0');

    /* Note: we did not adjust the time based on time zone information */
    return mktime(&t);
}