Rust in Everything
基于 rustls 0.23.46

certificate not valid yet / certificate expired

主线位置:校验主线的路径构建一步。客户端在 ExpectCertificateVerify 里向配置的 TimeProvider 要一个当前时间,交给 rustls-webpki 和每张证书的 notBefore / notAfter 比。有效期检查对叶子和每张中间证书都做、对信任锚不做,见路径构建;各种证书错误和告警的对照表见服务器证书。

flowchart TB
  tp["TimeProvider"] --> now{"current_time"}
  now -->|None| ftg["FailedToGetCurrentTime"]
  now -->|Some t| cmp["check_validity"]
  cmp -->|t 小于 notBefore| nvy["CertNotValidYet"]
  cmp -->|t 大于 notAfter| exp["CertExpired"]
  cmp -->|区间内| ok["继续校验"]
  nvy --> alert["告警 certificate_expired"]
  exp --> alert

实验 crate:exp-rustls/pita-lab,src/bin/p09_clock.rs。客户端用 ClientConfig::builder_with_details 换成固定时间的 TimeProvider;叶子证书由 rcgen 现场签发,notBefore 是运行时刻、有效 90 天(像 ACME 证书那样)。M2 Max、macOS 27、rustc 1.99.0,release,ring provider,TLS 1.3,内存握手。

现象

时钟为 0。 嵌入式设备或容器刚启动、还没对时,系统时间停在 1970-01-01:

== A: clock at 0 (1970-01-01)
  (server then sees: received fatal alert: CertificateExpired)
clock 0: client error: invalid peer certificate: certificate not valid yet: verification time 0 (UNIX), but certificate is not valid before 1793297050 (1793297050 seconds in future)
    debug: InvalidCertificate(NotValidYetContext { time: UnixTime(0), not_before: UnixTime(1793297050) })

报的是「not valid yet」,不是「expired」:1970 年在所有真实证书的 notBefore 之前。服务端那边看到的告警却叫 CertificateExpired。

时钟慢了 10 分钟。 证书刚签发、马上部署,客户端的时钟落后:

== B: client clock 10 minutes behind, certificate issued just now
  (server then sees: received fatal alert: CertificateExpired)
clock -600 s: client error: invalid peer certificate: certificate not valid yet: verification time 1793296450 (UNIX), but certificate is not valid before 1793297050 (600 seconds in future)
    debug: InvalidCertificate(NotValidYetContext { time: UnixTime(1793296450), not_before: UnixTime(1793297050) })
system clock: Ok

同一张证书,系统时钟下握手成功。差的 600 秒直接写在错误里。

测试证书过期。 测试夹具里写死了 2020-01-01 到 2021-01-01 的证书,某一天整个测试套件开始失败:

== C: test fixture made in 2020, valid one year, real clock
  (server then sees: received fatal alert: CertificateExpired)
system clock: client error: invalid peer certificate: certificate expired: verification time 1793297050 (UNIX), but certificate is not valid after 1609459200 (183837850 seconds ago)
    debug: InvalidCertificate(ExpiredContext { time: UnixTime(1793297050), not_after: UnixTime(1609459200) })

拿不到时间。 自己实现的 TimeProvider(比如 no_std 平台上读一个可能还没就绪的 RTC)返回 None:

== D: TimeProvider returns None
no clock: client error: failed to get current time
    debug: FailedToGetCurrentTime
client: Err(FailedToGetCurrentTime), then queued 6 TLS bytes for the server
compare clock 0: Err(InvalidCertificate(NotValidYetContext { time: UnixTime(0), not_before: UnixTime(1793297050) })), then queued 30 TLS bytes

这一种连告警都不发:客户端只排了 6 字节(CCS),服务端收不到任何解释,会一直等下去;证书错误时是 30 字节(CCS 加 24 字节加密的告警)。

原因

客户端在 ExpectCertificateVerify::handle 里取时间,再交给校验器:

        let now = self.config.current_time()?;

        let cert_verified = self
            .config
            .verifier
            .verify_server_cert(
                end_entity,
                intermediates,
                &self.server_name,
                &self.server_cert.ocsp_response,
                now,
            )
            .map_err(|err| {
                cx.common
                    .send_cert_verify_error_alert(err)
            })?;
  • current_time 就是 self.time_provider.current_time().ok_or(Error::FailedToGetCurrentTime)。它在 send_cert_verify_error_alert 之前用 ? 返回,所以拿不到时间时不发告警。
  • 默认的 DefaultTimeProvider 调 pki-types 的 UnixTime::now,即 SystemTime::now() 减去 1970 年,换成秒。系统时钟错了,rustls 就跟着错。
  • rustls-webpki 的 check_validity 先比 notBefore 再比 notAfter,时间早于前者是 CertNotValidYet,晚于后者是 CertExpired,等号两端都算有效。没有任何容差:差 1 秒也失败。

两种错误在线上是同一个告警 certificate_expired(RFC 的定义是「A certificate has expired or is not currently valid」),所以服务端日志里的 CertificateExpired 不能说明是哪一种,要看客户端的报错。

UnixTime::now 里有一个 unwrap,注释是「Safe: this code did not exist before 1970」:系统时间早于 1970 年时 duration_since 返回 Err,这里会 panic。本机没法把系统时钟拨到 1970 年之前,这一点只从代码看,没有实测。

📝NOTE
冷知识:0.23.0 之前,客户端的时间是写死的

96b217bb(2023-10-05,「no-std: add TimeProvider to ClientConfig」,首次发布于 0.23.0)之前,客户端在校验证书和保存会话时直接调 UnixTime::now(),配置里没有任何地方能换掉它。这个提交为 no_std 加了 TimeProvider trait 和 ClientConfig::time_provider 字段,说明是「For no-std users, the new ClientConfig::builder_with_details() must be used, which requires a TimeProvider implementation up-front. For std builds, the ClientConfig uses the DefaultTimeProvider for existing ClientConfig::builder* functions」。初衷是让没有系统时钟的平台也能用,顺带给了测试一个注入固定时间的入口:下面的修法二在 0.22 及之前做不到。

修法

一:先把时钟对准,再建 TLS 连接。 设备和容器在启动时先完成对时(NTP、RTC),再发第一个 TLS 连接。rustls 每次握手都重新取时间,不缓存,所以对时之后的连接自然就好了,不用重建配置。时钟偏差在几分钟量级时,刚签发的证书也会被拒(情形 B):notBefore 由签发方决定,rustls 这边没有容差可调。

二:测试里用固定时间的 TimeProvider。 测试证书写死了日期,就让客户端也活在那个日期里:

#[derive(Debug)]
struct FixedTime(u64);

impl TimeProvider for FixedTime {
  fn current_time(&self) -> Option<UnixTime> {
    Some(UnixTime::since_unix_epoch(Duration::from_secs(self.0)))
  }
}

let cfg = ClientConfig::builder_with_details(provider, Arc::new(FixedTime(1_590_969_600)))
  .with_protocol_versions(&[&rustls::version::TLS13])?
  .with_root_certificates(roots)
  .with_no_client_auth();
fixed 2020-06-01: Ok

情形 C 那张 2020 年的证书,在 2020-06-01 的客户端看来是有效的。builder_with_details 同时要求显式给 provider,所以它不经过进程级默认值。ServerConfig 有同名的构造函数,服务端校验客户端证书、处理票据也用它自己的 time_provider。

另一种做法是在测试里现场生成证书,不写死日期:rcgen 的默认有效期是 1975-01-01 到 4096-01-01(路径构建),本卷所有其他实验都这样做。

不要用跳过校验来绕开时间问题。 自定义校验器跳过有效期检查,往往会连签名一起跳过,见危险的校验器。

怎么检查

  • 看错误里的两个时间。 NotValidYetContext / ExpiredContext 把校验用的时间和证书的边界都带着,Display 还算好了差多少秒。verification time 接近 0 就是时钟没对;差几百秒是时钟偏差;seconds ago 很大就是证书真的过期了。
  • 在出问题的机器上看时间。 date -u +%s 和错误里的 verification time 对一下,就知道 rustls 用的是不是系统时间。
  • 看证书的有效期。 openssl x509 -noout -dates -in <证书> 打印 notBefore / notAfter。测试夹具建议加一个测试:读出证书的 notAfter,离现在不足 30 天就失败,比等它真的过期更早发现。

小结

  • 时钟为 0 或落后,报 certificate not valid yet(CertNotValidYet),不是 CertExpired;证书过期才是 certificate expired。两者在线上都是 certificate_expired 告警。
  • 有效期比较没有容差,差 1 秒也失败;错误里带着校验时间和证书边界。
  • TimeProvider 返回 None 时报 failed to get current time,而且不发告警。
  • 修法:对时之后再连;测试里用 builder_with_details 注入固定时间,或者现场生成证书。
  • TimeProvider 从 0.23.0(96b217bb)起才有,之前客户端直接读系统时间。

💬 关联讨论

发起讨论
加载中...