分类
devops

X.509/ASN.1 notAfter 9999-12-31

为什么签名证书最晚只能到公元 9999 年?

一次 keytool 实验,以及 X.509、ASN.1、RFC 5280 的标准来源追踪

日期:2026-11-06

先说结论

签名证书的 notAfter 不能晚于:

9999-12-31T23:59:59Z

以 2026-11-06 计算,距离这个上限还有:

2,912,133 天 ≈ 7,973 年

keytool -validity 仍然按“天数”计算:

N 天
  → keytool 计算 notAfter = 当前时间 + N 天
  → X.509 要求日期能写进 ASN.1 GeneralizedTime
  → GeneralizedTime 使用四位年份
  → 最大只能是公元 9999 年

所以 1 亿年、上亿年 都不行,Integer.MAX_VALUE 天也不行。


1. 问题从哪里来

生成自签名证书时,常见的命令是:

keytool -genkeypair -v \
  -keystore release.jks \
  -alias release \
  -keyalg RSA \
  -keysize 4096 \
  -validity 10000

这里的 -validity 不是年,而是天。

10000 天约等于:

10000 / 365.2425 ≈ 27.38 年

于是很容易产生一个疑问:

既然证书是自签的,也没有 CA 限制,能不能写成 1 亿年?

答案是:不能。限制不在 CA,而在 X.509 证书的日期编码格式。


2. 先看 keytool 实测

本机环境:

openjdk version "21.0.2" 2024-01-16 LTS

2.1 Integer.MAX_VALUE 天会被拒绝

-validity 的参数是 Java int 天数,最大可以传到:

2,147,483,647 天

约等于:

2,147,483,647 / 365.2425 ≈ 5,879,610 年

但是 keytool 生成证书时会失败:

keytool error: java.security.cert.CertificateException:
Validity period ends at calendar year 5881637 which is greater than 9999

注意这里有两个完全不同的概念:

概念 上限 失败位置
keytool -validity 参数 Integer.MAX_VALUE 天 Java int 参数解析
X.509 证书日期 公元 9999 年 证书时间编码

参数能解析到 588 万年,不等于证书能表示 588 万年。

2.2 边界内可以生成

-validity 2910000 可以成功:

Generating 2,048 bit RSA key pair and self-signed certificate
Valid from: Fri Nov 06 12:50:30 CST 2026
Valid until: Sun Feb 27 12:50:30 CST 9994

2.3 超过公元 9999 年就失败

在 2026-11-06 当天,最大可用天数是:

2,912,133 天

它会结束于:

9999-12-31

再多一天:

2,912,134 天

就会失败:

Validity period ends at calendar year 10000 which is greater than 9999

3. 标准来源:不是 keytool 自己定的 9999

严格说,RFC 5280 和 X.509 里并没有一句原文写:

notAfter 最大是公元 9999 年。

这个上限是从下面的标准链推导出来的。

X.509 的 Validity
  → ASN.1 Time
  → GeneralizedTime
  → 四位年份 YYYY
  → 最大值 9999

下面逐条给出原文。


4. X.509:证书有效期使用 ASN.1 Time

标准:

  • ITU-T X.509 (10/2019)
  • ISO/IEC 9594-8:2020

位置:

  • §7.2.1 Public-key certificate syntax
  • 第 14 页

原文:

Validity ::= SEQUENCE {
  notBefore Time,
  notAfter   Time,
  ... }

Time ::= CHOICE {
  utcTime                   UTCTime,
  generalizedTime           GeneralizedTime }

X.509 还规定:

In no case shall UTCTime be used for representing dates beyond 2049.

所以 2050 年之后的证书有效期必须使用 GeneralizedTime。

官方页面:

https://www.itu.int/rec/T-REC-X.509-201910-I/en


5. ASN.1:GeneralizedTime 使用四位年份

标准:

  • ITU-T X.680 (02/2021)
  • ISO/IEC 8824-1:2021

位置:

  • §46 Generalized time
  • §46.3
  • 第 83 页

原文:

GeneralizedTime ::= [UNIVERSAL 24] IMPLICIT VisibleString

随后关于日期部分写明:

NOTE 1 – This specifies a four-digit representation of the year, a two-digit representation of the month and a two-digit representation of the day, without use of separators.

也就是说,GeneralizedTime 的年份格式是:

YYYY

四位年份能表示的范围是:

0000 ... 9999

10000 需要五位,无法满足这个类型约束。

官方页面:

https://www.itu.int/rec/T-REC-X.680-202102-I/en


6. X.690:DER 编码里仍然是 YYYY

证书最终使用 DER 编码。X.690 对 GeneralizedTime 的编码限制如下。

标准:

  • ITU-T X.690 (02/2021)
  • ISO/IEC 8825-1:2021

位置:

  • §11.7 GeneralizedTime
  • 第 21 页

原文:

11.7.5 Midnight (GMT) shall be represented in the form:

"YYYYMMDD000000Z"

这里再次明确年份是 YYYY,即四位。

官方页面:

https://www.itu.int/rec/T-REC-X.690-202102-I/en


7. RFC 5280:明确使用 YYYYMMDDHHMMSSZ

RFC 5280 是 X.509 公钥基础设施的互联网证书配置(PKIX profile)。

7.1 Validity

位置:

  • §4.1.2.5 Validity

原文:

To indicate that a certificate has no well-defined expiration date, the notAfter SHOULD be assigned the GeneralizedTime value of 99991231235959Z.

这正好对应:

9999-12-31 23:59:59Z

7.2 GeneralizedTime 格式

位置:

  • §4.1.2.5.2 GeneralizedTime

原文:

For the purposes of this profile, GeneralizedTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST include seconds (i.e., times are YYYYMMDDHHMMSSZ), even where the number of seconds is zero. GeneralizedTime values MUST NOT include fractional seconds.

链接:


8. 为什么 10000 不是“10000 年有效期”,而是“公元 10000 年”

这是最容易混淆的地方。

证书里没有“有效期 N 年”这个字段,只有两个绝对时间:

notBefore = 2026-11-06T...
notAfter  = 9999-12-31T23:59:59Z

所谓“最长 7973 年”,是:

9999-12-31 - 2026-11-06

并不是证书格式规定“有效期最多 7973 年”。

如果证书在 2020 年生成,同样的 9999-12-31 上限会得到约 7979 年的区间;如果证书在 9000 年生成,剩余可写区间就只剩约 999 年。

所以准确的表述是:

证书结束日期不能晚于公元 9999 年,而不是“有效期不能超过 9999 年”。


9. 四个实测值分别是多少年

-validity 全部按天计算,按平均公历年 365.2425 天换算:

编号 天数 约等于 结果
1 2,147,483,647 约 587.96 万年 超过公元 9999 年,失败
2 2,912,133 约 7973.15 年 成功,结束于 9999-12-31
3 2,912,134 约 7973.15 年 只多 1 天,进入公元 10000 年,失败
4 2,910,000 约 7967.31 年 成功,结束于 9994-02-27

注意第 2 项和第 3 项只差 1 天,四舍五入到“年”看起来相同;真正决定成败的是日历日期是否超过 9999-12-31。


10. 如何自己复现

将下面的完整脚本保存为 verify-keytool-cert-validity.sh,然后执行:

chmod +x verify-keytool-cert-validity.sh
./verify-keytool-cert-validity.sh

脚本内容:

#!/usr/bin/env bash
# Verify the X.509 / keytool validity limits used by the release signing docs.
#
# Claims under test:
#   1. keytool -validity accepts an int number of DAYS, but the generated
#      X.509 certificate cannot end after calendar year 9999.
#   2. Integer.MAX_VALUE days (~5.88 million years) is rejected.
#   3. The largest valid day count from "now" is bounded by 9999-12-31.
#   4. A large but valid value (2,910,000 days) produces a certificate in
#      year 9994 on 2026-11-06. The exact end date naturally moves with the
#      day the script is run; the assertion is that it succeeds and stays
#      inside year 9999.
#
# Usage:
#   ./verify-keytool-cert-validity.sh
#
# Requires: keytool, python3. Creates only temporary PKCS#12 keystores and
# removes them on exit. No repository file or release keystore is touched.

set -euo pipefail

# Keep keytool's diagnostics stable across locales.
export LANG=C
export LC_ALL=C

WORK="$(mktemp -d "${TMPDIR:-/tmp}/keytool-validity.XXXXXX")"
cleanup() {
    rm -rf -- "$WORK"
}
trap cleanup EXIT

PASSWORD="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
ALIAS="validity-test"

fail() {
    printf 'FAIL: %s\n' "$*" >&2
    exit 1
}

pass() {
    printf 'PASS: %s\n' "$*"
}

gen_key() {
    local name=$1
    local days=$2
    local ks="$WORK/$name.p12"
    local log="$WORK/$name.log"

    if keytool -genkeypair \
        -keystore "$ks" \
        -storetype PKCS12 \
        -storepass "$PASSWORD" \
        -keypass "$PASSWORD" \
        -alias "$ALIAS" \
        -keyalg RSA \
        -keysize 2048 \
        -dname "CN=keytool-validity-test" \
        -validity "$days" >"$log" 2>&1; then
        printf '%s' "$ks"
        return 0
    fi

    printf '%s' "$log"
    return 1
}

expect_failure() {
    local label=$1
    local days=$2
    local result

    if result="$(gen_key "$label" "$days")"; then
        fail "$label: keytool unexpectedly accepted -validity $days"
    fi

    if ! grep -q 'greater than 9999' "$result"; then
        printf '%s\n' '--- keytool output ---' >&2
        cat "$result" >&2
        fail "$label: failure did not mention the 9999 calendar-year limit"
    fi

    pass "$label rejected -validity $days at the X.509 year-9999 limit"
    printf '      %s\n' "$(grep -m1 'greater than 9999' "$result")"
}

validity_line() {
    local ks=$1
    keytool -list -v -keystore "$ks" -storepass "$PASSWORD" 2>/dev/null |
        grep -m1 'Valid from:'
}

end_year() {
    local line=$1
    printf '%s\n' "$line" | grep -oE '[0-9]{4}$' || true
}

expect_success() {
    local label=$1
    local days=$2
    local ks
    local line
    local year

    if ! ks="$(gen_key "$label" "$days")"; then
        printf '%s\n' '--- keytool output ---' >&2
        cat "$ks" >&2
        fail "$label: keytool rejected -validity $days"
    fi

    line="$(validity_line "$ks")"
    year="$(end_year "$line")"

    [[ -n "$line" ]] || fail "$label: cannot read the certificate validity"
    [[ -n "$year" ]] || fail "$label: cannot parse the end year: $line"
    (( year <= 9999 )) || fail "$label: certificate ends after year 9999: $line"

    pass "$label accepted -validity $days"
    printf '      %s\n' "$line"
}

command -v keytool >/dev/null 2>&1 || fail 'keytool is not installed'
command -v python3 >/dev/null 2>&1 || fail 'python3 is not installed'

printf 'Java keytool: %s\n' "$(keytool -J-version 2>&1 | head -1)"
printf 'Today:        %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')"

# Largest whole day count that still ends on or before 9999-12-31 23:59:59
# in the local timezone used by keytool.
MAX_DAYS="$(python3 -c 'from datetime import datetime; print((datetime(9999, 12, 31, 23, 59, 59) - datetime.now()).days)')"
printf 'Max days now: %s\n\n' "$MAX_DAYS"

printf '%s\n' '[1] Integer.MAX_VALUE days must be rejected'
expect_failure 'max-int' 2147483647

printf '\n%s\n' "[2] The computed maximum ($MAX_DAYS days) must succeed and end in year 9999"
expect_success 'max-valid' "$MAX_DAYS"

printf '\n%s\n' '[3] One day past the computed maximum must be rejected'
expect_failure 'max-plus-one' "$((MAX_DAYS + 1))"

printf '\n%s\n' '[4] 2,910,000 days must succeed'
expect_success 'year-9994' 2910000

printf '\nAll keytool / X.509 validity checks passed.\n'

脚本会:

  1. 动态计算从今天到 9999-12-31 的最大天数;
  2. 验证 Integer.MAX_VALUE 天失败;
  3. 验证最大合法天数成功;
  4. 验证最大合法天数 + 1 天失败;
  5. 验证 2,910,000 天成功;
  6. 所有 keystore 都放在临时目录,退出时自动删除。

2026-11-06 的实测输出:

Java keytool: openjdk version "21.0.2" 2024-01-16 LTS
Today:        2026-11-06 12:50:28 CST
Max days now: 2912133

[1] Integer.MAX_VALUE days must be rejected
PASS: max-int rejected -validity 2147483647 at the X.509 year-9999 limit
      keytool error: java.security.cert.CertificateException:
      Validity period ends at calendar year 5881637 which is greater than 9999

[2] The computed maximum (2912133 days) must succeed and end in year 9999
PASS: max-valid accepted -validity 2912133
      Valid from: Fri Nov 06 12:50:29 CST 2026 until: Fri Dec 31 12:50:29 CST 9999

[3] One day past the computed maximum must be rejected
PASS: max-plus-one rejected -validity 2912134 at the X.509 year-9999 limit
      keytool error: java.security.cert.CertificateException:
      Validity period ends at calendar year 10000 which is greater than 9999

[4] 2,910,000 days must succeed
PASS: year-9994 accepted -validity 2910000
      Valid from: Fri Nov 06 12:50:30 CST 2026 until: Sun Feb 27 12:50:30 CST 9994

All keytool / X.509 validity checks passed.

脚本中的最大天数会随着运行日期变化,不要把它当成固定常量。


11. 实际应该设置多少年?

不建议追求公元 9999 年,也不建议使用极端的 Integer.MAX_VALUE。实际发布证书建议:

-validity 10000

约 27.4 年。

签名证书的有效期通常应覆盖软件的预期维护周期;25~30 年通常足够。

另外注意:

  • 公开 HTTPS CA 证书常见的 398 天有效期限制,不适用于私有信任的自签名证书。
  • 软件分发平台真正关心的是应用标识、签名证书和证书指纹的连续性。
  • 已经发布的证书不要为了延长有效期重新生成,否则同包名升级会失败。
  • 证书和密码必须离线备份,不能只保存在 CI 或某台电脑上。

12. 最终答案

证书年限最大是多少年?

不是固定的“多少年”,而是:

notAfter 最晚为 9999-12-31 23:59:59 UTC。

以 2026-11-06 计算,约剩 7973 年。

上亿年可以吗?

不可以。原因不在 CA 或分发平台,而在 X.509 证书使用的 ASN.1 GeneralizedTime 四位年份格式。

这是 keytool 的限制还是 X.509 的限制?

  • 公元 9999 年上限:来自 ASN.1 / X.509 的日期表示规则,keytool 按规则报错。
  • Integer.MAX_VALUE 天参数上限:来自 keytool 的 Java int 参数。

两者叠加后的结果就是:

参数可以写 5,879,610 年
但证书最多只能表示到公元 9999 年

参考资料