rustfs/rustfs · error · SignV4Error

failed to format signing timestamp: {reason}

Error message

failed to format signing timestamp: {reason}

What it means

SignV4Error::TimeFormat comes from format_amz_datetime (request_signature_v4.rs:84-88), which renders the signing instant as ISO 'YYYYMMDDThhmmssZ' using a compiled format_description. try_pre_sign_v4 accepts the timestamp t as a caller parameter, so an OffsetDateTime whose components fall outside the format's representable range (classically a year beyond four digits from a bad unix-timestamp conversion) is the realistic trigger. The header-signing path uses now_utc() and cannot hit it on a sane clock; streaming signing (request_signature_streaming.rs:65,164) reuses the same conversion.

Source

Thrown at crates/signer/src/request_signature_v4.rs:39

use std::sync::LazyLock;
use time::{OffsetDateTime, macros::format_description};
use tracing::warn;

use super::constants::UNSIGNED_PAYLOAD;
use super::request_signature_streaming_unsigned_trailer::streaming_unsigned_v4;
use super::utils::{HostAddrError, sign_v4_trim_all, try_get_host_addr};
use rustfs_utils::crypto::{hex, hex_sha256, hmac_sha256};
use s3s::Body;

pub const SIGN_V4_ALGORITHM: &str = "AWS4-HMAC-SHA256";
pub const SERVICE_TYPE_S3: &str = "s3";
pub const SERVICE_TYPE_STS: &str = "sts";

#[derive(Debug, thiserror::Error)]
pub enum SignV4Error {
    #[error("invalid UTF-8 header value for `{name}`")]
    InvalidHeaderValue { name: String },
    #[error("failed to format signing timestamp: {reason}")]
    TimeFormat { reason: String },
    #[error("failed to build signing timestamp: {reason}")]
    TimeComponent { reason: String },
    #[error("failed to encode query parameters: {reason}")]
    QueryEncode { reason: String },
    #[error("failed to parse uri: {reason}")]
    InvalidUri { reason: String },
    #[error("failed to build uri from parts: {reason}")]
    InvalidUriParts { reason: String },
    #[error("failed to convert canonical headers to UTF-8: {reason}")]
    CanonicalUtf8 { reason: String },
    #[error("failed to parse header value for `{name}`: {reason}")]
    HeaderValueParse { name: String, reason: String },
}

pub type SignResult<T> = std::result::Result<T, SignV4Error>;

#[derive(Debug)]

View on GitHub (pinned to 35af688cd9)

Solutions

  1. Clamp or validate the computed OffsetDateTime before passing it: reject years outside 0..=9999.
  2. Fix unit confusion: ensure the expires arithmetic uses seconds, not milliseconds.
  3. Use try_pre_sign_v4 so the failure carries the reason string rather than silently producing a broken URL.

Example fix

// before
let t = OffsetDateTime::from_unix_timestamp(expires_secs).unwrap_or(OffsetDateTime::MAX); // year 99999+ -> [year] format fails

// after
let t = OffsetDateTime::from_unix_timestamp(expires_secs)
    .ok()
    .filter(|t| (0..=9999).contains(&t.year()))
    .ok_or_else(|| anyhow::anyhow!("expiry out of representable range"))?;
Defensive patterns

Strategy: validation

Validate before calling

fn datetime_formattable(t: time::OffsetDateTime) -> bool {
    (0..=9999).contains(&t.year())
        && t.hour() <= 23 && t.minute() <= 59 && t.second() <= 59
}
anyhow::ensure!(datetime_formattable(t), "signing timestamp outside formattable range");

Type guard

fn is_time_format(e: &SignV4Error) -> bool {
    matches!(e, SignV4Error::TimeFormat { .. })
}

Try / catch

Err(SignV4Error::TimeFormat { reason }) => {
    // caller-supplied timestamp had out-of-range components; recompute from now
    tracing::warn!(%reason, "bad signing timestamp, falling back to now");
    return try_pre_sign_v4(req, ak, sk, token, region, expires, time::OffsetDateTime::now_utc())
        .map_err(Into::into);
}

Prevention

When it happens

Trigger: Calling try_pre_sign_v4/pre_sign_v4 with t built from unvalidated arithmetic (e.g. expiry = now + i64::MAX seconds then from_unix_timestamp), or a test fixture constructing OffsetDateTime with an out-of-range component. now_utc()-based calls only fail with a hardware clock centuries off.

Common situations: Presign-URL generation services computing expires timestamps in milliseconds-vs-seconds confusion, producing year-50000+ datetimes; property tests sweeping extreme timestamps; clock skew after VM migration. Symptom: presign returns the typed error (try_) or a warn! plus an invalid/unsigned URL (non-try).

Related errors


AI-assisted analysis of rustfs/rustfs@35af688cd9 (2026-08-20). Data as JSON: /api/errors/55293dae5c10e402. Report an issue: GitHub.