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 年之前,这一点只从代码看,没有实测。
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)起才有,之前客户端直接读系统时间。