astrid-runtime/astrid · error
signed channel {label} is not canonical UTC RFC3339 seconds
Error message
signed channel {label} is not canonical UTC RFC3339 seconds What it means
`canonical_time` parses an RFC3339 timestamp with `chrono`, converts it to UTC, and requires that re-formatting it with `SecondsFormat::Secs` and `use_z=true` reproduces the input exactly. This error means the timestamp is parseable but not a canonical UTC RFC3339 second-precision value — e.g. it carries a non-UTC offset, fractional seconds, or an offset written as `+00:00` instead of `Z`. Signed channel pointers must use this canonical spelling so signatures are deterministic.
Source
Thrown at crates/astrid-cli/src/commands/update_channel.rs:328
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn canonical_version(value: &str) -> anyhow::Result<semver::Version> {
let parsed = semver::Version::parse(value)
.with_context(|| format!("signed channel version '{value}' is not valid semver"))?;
ensure!(
parsed.to_string() == value,
"signed channel version '{value}' is not canonical semver"
);
Ok(parsed)
}
fn canonical_time(value: &str, label: &str) -> anyhow::Result<DateTime<Utc>> {
let parsed = DateTime::parse_from_rfc3339(value)
.with_context(|| format!("signed channel {label} is not RFC3339"))?
.with_timezone(&Utc);
ensure!(
parsed.to_rfc3339_opts(SecondsFormat::Secs, true) == value,
"signed channel {label} is not canonical UTC RFC3339 seconds"
);
Ok(parsed)
}
fn validate_targets_for(
targets: &[TargetMetadata],
expected_targets: &[&str],
version: &str,
label: &str,
) -> anyhow::Result<()> {
ensure!(
targets.len() == expected_targets.len(),
"{label} must contain exactly {} targets",
expected_targets.len()
);
let mut seen = HashSet::new();View on GitHub (pinned to affd8760f4)
Solutions
- Re-emit the timestamp in canonical form: UTC, second precision, `Z` suffix — e.g. `2026-09-09T12:00:00Z`.
- Fix the generating code to use `DateTime<Utc>::to_rfc3339_opts(SecondsFormat::Secs, true)` (or equivalent) before signing.
- Regenerate and re-sign the channel pointer, since the signature covers the raw bytes of the timestamp.
- Convert any local-time timestamps to UTC first (keep the instant, change only the representation).
Example fix
// before "expires": "2026-09-09T14:00:00.000+02:00" // after (canonical UTC RFC3339 seconds) "expires": "2026-09-09T12:00:00Z"
Defensive patterns
Strategy: validation
Validate before calling
// Rust: canonicalize before signing
use chrono::{DateTime, Utc, SecondsFormat};
let t: DateTime<Utc> = DateTime::parse_from_rfc3339(input)?.with_timezone(&Utc);
assert_eq!(t.to_rfc3339_opts(SecondsFormat::Secs, true), input, "timestamp must be canonical UTC RFC3339 seconds"); Type guard
null
Try / catch
null
Prevention
- Always emit timestamps with `to_rfc3339_opts(SecondsFormat::Secs, true)`.
- Convert to UTC before serializing; never serialize local time into channel metadata.
- Avoid fractional seconds and `+00:00` offsets; use the `Z` suffix.
- Add a signing-time assertion that the serialized form round-trips.
When it happens
Trigger: `validate_pointer` receiving a timestamp field (labeled by `label`, e.g. valid-until/generated-at) like `2026-09-09T12:00:00.000Z`, `2026-09-09T14:00:00+02:00`, or `2026-09-09T12:00:00+00:00` instead of `2026-09-09T12:00:00Z`.
Common situations: A signing pipeline serializing timestamps with the local timezone or with millisecond precision; hand-written channel files; tooling using `to_rfc3339()` defaults (which emit `+00:00` and/or nanoseconds) instead of the canonical Z-terminated seconds form.
Related errors
- durable capsule {} has malformed contracts pin
- refusing to seed canonical from a non-content-address contra
- timeout exceeds upper bound ({TIMEOUT_SECS_UPPER_BOUND}s)
- invalid key: must not be empty
- --scope requires the capsule manifest to declare '{key}' as
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/0290456253f922e2.
Report an issue: GitHub.