ASN1_TIME转换为time_t

19

我该如何将 ASN1_TIME 转换为 time_t 格式?我想将 X509_get_notAfter() 的返回值转换为秒。

7个回答

12

从 OpenSSL 代码中看,这似乎不是一个好主意:

/*
 * 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.
 */
请注意,您可以使用 ASN1_TIME_diff() 函数来获取两个 ASN1_TIME* 之间的天数/秒数差。如果您将 ASN1_TIME *from 参数设置为 NULL,则可以获取与当前时间的差值。

11

时间以字符串形式内部存储,格式为YYmmddHHMMSSYYYYmmddHHMMSS

字符串末尾可以存储秒的小数和时区,但现在先忽略这些,看一下以下(未经测试的)代码。

注意:请参阅Bryan Olson下面的答案,该答案讨论了由于i++引起的未定义行为。还请参阅Seak的答案,该答案消除了未定义行为。

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);
}

13
由于i++(或++i)在同一表达式中的顺序不确定,因此此代码是错误的,依赖于未定义的行为。需要修改。 - Paul J. Lucas

9

嗯,我不知道其他情况,但是对于ASN1_TIME格式为UTCTime的情况,那段代码是错误的:YYMMDDHHMMSSZ。

我尝试过并返回了错误的值,即使进行了从++i到i++的更正, 然而...这段代码并不是一个好的编码示例。

我设法解决了它,那是char类型的总和问题:

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);
}

RFC 5280指出:1-输入时间为UTC,因此mktime()在这里可能返回错误的结果(mktime()期望本地时区的输入时间)。2- YY >= 50应解释为19YY。3- 99991231235959Z是一个特殊值。这里有一个代码示例,说明如何解决这些问题 - jfs

6
我不同意Jan和Jack的观点。在我的工作中,有人实际上复制并使用了给定的代码,但它失败了。原因在于C99标准中的规定:
“在前一个和下一个序列点之间,一个对象的存储值应该被一个表达式的求值修改最多一次。” -- ISO/IEC 9899:1999,“编程语言-C”,第6.5节,第1条。
当编译所提供的代码时,gcc(版本4.1.2)会九次显示警告:
“警告:对'i'的操作可能是未定义的。”
代码具有未定义的行为。我实际看到的错误是将年份“13”读取为“11”。这是因为:
后缀++运算符的结果是操作数的值。在获得结果之后,操作数的值被递增。 …… 更新操作数的存储值的副作用应该发生在前一个和下一个序列点之间 -- Ibid,第6.5.2.4节,第2款。
在所示两个str[i++]中,都读取“13”中的“1”,因为它们都发生在i更新之前。所有多次更新i的行都有相同的问题。
解决方法很简单,只需摆脱‘i’并替换所有这些行为单个sscanf()调用即可。
即使修复了这个问题,我也不喜欢这个代码。除了忽略时区后缀外,它还没有检查错误或意外值。证书是一种安全机制,而安全代码对鲁棒性有严格要求。程序无法正确处理的边缘情况就是攻击者会利用的地方。

“简单的解决方法是摆脱‘i’并用单个sscanf()调用替换所有这些行” - 你可能应该提供一个例子,因为使用sscanf不正确很容易。没有必要为了解决一个问题而引入另一个问题。 - jww

4

我知道现在可能太晚了,而且 openssl 已经引入了一个函数 ASN1_TIME_to_tm,但我必须使用旧版本的 openssl,因为它没有这个方法。

我看到了各种可能的答案,它们在代码中解析时间字符串,但我对这种方法不太满意,因为我认为在解析逻辑中可能会遗漏一些内容,我的代码可能会出错或无法处理所有边角情况。因此,我实现了一个 C++ 函数,仅使用 openssl 函数来进行转换。

它使用 ASN1_TIME_diff 计算自纪元以来的秒数。要获取纪元的 ASN1_TIME,我使用了 ASN1_TIME_SET,并将 time_t 参数传递为 0。

欢迎评论和测试。

bool _ASN1_TIME_to_tm(const ASN1_TIME *pTime, struct tm *pTm)
{
    int days = 0, seconds = 0;
    ASN1_TIME *epochTime = ASN1_TIME_new();
    ASN1_TIME_set(epochTime, time_t(0));

    if (!ASN1_TIME_diff(&days, &seconds, epochTime, pTime))
        return false;
    time_t sinceEpoch = time_t(86400LL * days + seconds); // No of seconds in a day = 86400
    gmtime_r(&sinceEpoch, pTm);
    std::cout << "DateTime: " << TOS::convertTmToStr(*pTm) << std::endl;
    ASN1_TIME_free(epochTime);
    return true;
}

或者使用更多的检查来编写代码:

bool _ASN1_TIME_to_tm(const ASN1_TIME *pTime, struct tm *pTm)
{
    bool result = false;
    time_t sinceEpoch = 0;
    int days = 0, seconds = 0;
    if (!pTime)
        return false;

    ASN1_TIME *epochTime = ASN1_TIME_new();
    if (!epochTime)
        return false;
    do {
        if (!ASN1_TIME_set(epochTime, time_t(0)))
            break;
        if (!ASN1_TIME_diff(&days, &seconds, epochTime, pTime))
            break;
        // No of seconds in a day = 86400
        sinceEpoch = time_t(86400LL * days + seconds);
        gmtime_r(&sinceEpoch, pTm);
        std::cout << "DateTime: " << TOS::convertTmToStr(*pTm) << std::endl;
        result = true;
    } while (0);

    ASN1_TIME_free(epochTime);
    return result;
}

我喜欢你的方法,尽管你的实现存在潜在的内存泄漏问题。如果出现错误,你没有释放epochTime - Gils

3

time_t可能比ASN1_TIME的范围更窄,因此ASN1_TIME_*函数可能是更健壮的选择。例如,要比较时间,可以使用ASN1_TIME_diff()(这避免了使用time_t可能存在的溢出安全问题)。要以人类可读的格式打印,请调用ASN1_TIME_print()等。

到目前为止,没有任何答案遵循rfc 5280,该规范指定输入时间为UTC(mktime()期望本地时区的时间,即如果本地时区不是UTC,则答案不正确)。另外

符合规范的系统必须按以下方式解释年份字段(YY): 当YY大于或等于50时,年份应解释为19YY; 当YY小于50时,年份应解释为20YY。

即,if (tm_year < 70) tm_year += 100; 违反了RFC。此答案使用 year += year < 50 ? 2000 : 1900

另外,输入中的99991231235959Z 意味着证书没有明确定义的到期日期(函数应返回(time_t)-1——一种错误)。

要将UTCTime或GeneralizedTime字符串(ASN1_TIME*)转换为自Epoch以来的秒数time_t):

typedef unsigned U;

time_t ASN1_TIME_to_posix_time(const ASN1_TIME* time) {
  if(!time) return -1;
  const char *s = (const char*)time->data;
  if (!s) return -1;

  U two_digits_to_uint() // nested function: gcc extension
  {
    U n = 10 * (*s++ - '0');
    return n + (*s++ - '0');
  }
  U year, month, day, hour, min, sec;
  switch(time->type) {
    // https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.5.1
  case V_ASN1_UTCTIME: // YYMMDDHHMMSSZ
    year = two_digits_to_uint();
    year += year < 50 ? 2000 : 1900;
    break;
  case V_ASN1_GENERALIZEDTIME: // YYYYMMDDHHMMSSZ
    year = 100 * two_digits_to_uint();
    year += two_digits_to_uint();
    break;
  default:
    return -1; // error
  }
  month = two_digits_to_uint();
  day   = two_digits_to_uint();
  hour  = two_digits_to_uint();
  min   = two_digits_to_uint();
  sec   = two_digits_to_uint();
  if (*s != 'Z') return -1;
  if (year == 9999 && month == 12 && day == 31 && hour == 23 && min == 59
      && sec == 59) // 99991231235959Z rfc 5280
    return -1;
  return posix_time(year, month, day, hour, min, sec);
}

其中posix_time()用于将分解的UTC时间转换为日历时间。自纪元以来的秒数
time_t posix_time(U year, U month, U day, U hour, U min, U sec)
{
  if (year < 1970 || month < 1 || month > 12 || day < 1 || day > 31
      || hour > 23 || min > 59 || sec > 60)
    return -1;

  // days upto months for non-leap years
  static const U month_day[13] =
    {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
  year -= 1900;
  // number of Februaries since 1900
  const U year_for_leap = (month > 2) ? year + 1 : year;
  // XXX may overflow
  return sec + min*60 + hour*3600 + (month_day[month] + day - 1)*86400 +
    (year-70)*31536000 + ((year_for_leap-69)/4)*86400 -
    ((year_for_leap-1)/100)*86400 + ((year_for_leap+299)/400)*86400;
}

"month_day"和"year_for_leap"来自@DTiedy的回答

太好了!我也已经添加了我的答案。这可能是一个解决方法,但如果您能检查并评论一下就太好了。 - abhiarora

2

在这种情况下Jan的答案大多数是可行的,但是累加器i应该一致使用i++:

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);
}

i++ 表示在语句完成后 i 会被递增。这意味着对于年份,比如说是2014年,它在 tm 结构体内部会变成322(2222),因此第一个应该使用 i++,而每个后续的应该使用 ++i。 - Brad Mitchell
6
不,你的代码仍然是错误的。由于评估顺序不能保证(从左到右与从右到左),因此你不能在同一表达式中有多个"i++"。 - Paul J. Lucas
太好了!我也已经添加了我的答案。它可能只是一个解决方法,但如果您能检查并评论一下就太好了:https://dev59.com/vmgu5IYBdhLWcg3w6rQ9#59721373 - abhiarora

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接