use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const TRUSTED_ROOT_JSON: &[u8] =
include_bytes!("embedded/sigstore_trusted_root_2026-05-12.json");
pub const EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE: &str = env!("EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE");
pub const DEFAULT_MAX_TRUST_ROOT_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
pub const TRUSTED_ROOT_CACHE_FUTURE_MTIME_TOLERANCE: Duration = Duration::from_secs(60);
pub const STABLE_INVARIANT_TRUSTED_ROOT_CACHE_FUTURE_DATED: &str =
"audit.verify.trusted_root.cache_future_dated";
pub const TRUSTED_ROOT_STALE_INVARIANT: &str = "audit.verify.trusted_root.stale_beyond_max_age";
pub const TRUSTED_ROOT_SNAPSHOT_STALE_INVARIANT: &str = "audit.verify.trusted_root.snapshot_stale";
pub const TRUSTED_ROOT_CACHE_STALE_INVARIANT: &str = "audit.verify.trusted_root.cache_stale";
pub const TRUSTED_ROOT_PARSE_INVARIANT: &str = "audit.verify.trusted_root.parse_error";
pub const EMBEDDED_ROOT_STATUS: &str = "embedded_snapshot";
pub const CACHED_ROOT_STATUS: &str = "operator_cached";
pub const REKOR_TRUSTED_ROOT_TLOG_LOGID_NO_MATCH_INVARIANT: &str =
"rekor.trusted_root.tlog_logid_no_match";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustedRoot {
#[serde(rename = "mediaType")]
pub media_type: String,
#[serde(default)]
pub tlogs: Vec<TransparencyLogInstance>,
#[serde(rename = "certificateAuthorities", default)]
pub certificate_authorities: serde_json::Value,
#[serde(default)]
pub ctlogs: serde_json::Value,
#[serde(rename = "timestampAuthorities", default)]
pub timestamp_authorities: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransparencyLogInstance {
#[serde(rename = "baseUrl")]
pub base_url: String,
#[serde(rename = "logId", default)]
pub log_id: serde_json::Value,
#[serde(rename = "hashAlgorithm", default)]
pub hash_algorithm: serde_json::Value,
#[serde(rename = "publicKey")]
pub public_key: TransparencyLogPublicKey,
#[serde(default)]
pub log_index: serde_json::Value,
#[serde(rename = "treeId", default)]
pub tree_id: serde_json::Value,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransparencyLogPublicKey {
#[serde(rename = "rawBytes", default)]
pub raw_bytes: Option<String>,
#[serde(rename = "keyDetails", default)]
pub key_details: Option<String>,
#[serde(rename = "validFor", default)]
pub valid_for: ValidityPeriod,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidityPeriod {
#[serde(default)]
pub start: Option<DateTime<Utc>>,
#[serde(default)]
pub end: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub enum TrustRootStalenessAnchor<'a> {
EmbeddedSnapshotDate {
snapshot_iso_date: &'a str,
},
CacheFileMtime {
cache_path: &'a Path,
},
}
impl<'a> TrustRootStalenessAnchor<'a> {
#[must_use]
pub const fn embedded_snapshot() -> Self {
Self::EmbeddedSnapshotDate {
snapshot_iso_date: EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE,
}
}
#[must_use]
pub const fn cache_file_mtime(cache_path: &'a Path) -> Self {
Self::CacheFileMtime { cache_path }
}
fn diagnostic_label(&self) -> String {
match self {
Self::EmbeddedSnapshotDate { snapshot_iso_date } => {
format!("embedded_snapshot_date={snapshot_iso_date}")
}
Self::CacheFileMtime { cache_path } => {
format!("cache_file_mtime path={}", cache_path.display())
}
}
}
fn resolve(&self, _now: DateTime<Utc>) -> Result<DateTime<Utc>, TrustRootStalenessError> {
match self {
Self::EmbeddedSnapshotDate { snapshot_iso_date } => {
let date =
NaiveDate::parse_from_str(snapshot_iso_date, "%Y-%m-%d").map_err(|err| {
TrustRootStalenessError::MalformedEmbeddedSnapshotDate {
observed: (*snapshot_iso_date).to_string(),
reason: err.to_string(),
}
})?;
let utc = date
.and_hms_opt(0, 0, 0)
.ok_or(TrustRootStalenessError::EmbeddedSnapshotMidnightConstruction)?
.and_utc();
Ok(utc)
}
Self::CacheFileMtime { cache_path } => {
let meta = std::fs::metadata(cache_path).map_err(|source| {
TrustRootStalenessError::CacheMetadata {
path: cache_path.to_path_buf(),
source,
}
})?;
let mtime =
meta.modified()
.map_err(|source| TrustRootStalenessError::CacheMtime {
path: cache_path.to_path_buf(),
source,
})?;
Ok(DateTime::<Utc>::from(mtime))
}
}
}
}
#[derive(Debug, Error)]
pub enum TrustRootStalenessError {
#[error(
"EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE `{observed}` is not RFC 3339 YYYY-MM-DD: {reason}"
)]
MalformedEmbeddedSnapshotDate {
observed: String,
reason: String,
},
#[error("EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE midnight construction failed")]
EmbeddedSnapshotMidnightConstruction,
#[error("cannot stat trusted_root.json cache `{path}`: {source}", path = path.display())]
CacheMetadata {
path: PathBuf,
source: std::io::Error,
},
#[error("cannot read mtime on trusted_root.json cache `{path}`: {source}", path = path.display())]
CacheMtime {
path: PathBuf,
source: std::io::Error,
},
#[error(
"trusted_root anchor `{anchor}` is dated {anchor_ts} which is more than {tolerance_seconds}s ahead of now={now}: \
refusing freshness gate (future-dated bypass guard)"
)]
CacheFutureDated {
anchor: String,
anchor_ts: DateTime<Utc>,
now: DateTime<Utc>,
tolerance_seconds: u64,
},
}
impl TrustedRoot {
pub fn embedded() -> Result<Self, TrustedRootParseError> {
Self::parse_bytes(TRUSTED_ROOT_JSON)
}
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, TrustedRootParseError> {
let root: Self =
serde_json::from_slice(bytes).map_err(|source| TrustedRootParseError::Malformed {
reason: source.to_string(),
})?;
root.validate()?;
Ok(root)
}
pub fn load_cached(path: impl AsRef<Path>) -> Result<Option<Self>, TrustedRootIoError> {
let path = path.as_ref();
let bytes = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(TrustedRootIoError::Read {
path: path.to_path_buf(),
source,
});
}
};
Self::parse_bytes(&bytes)
.map(Some)
.map_err(|source| TrustedRootIoError::Parse {
path: path.to_path_buf(),
source,
})
}
pub fn validate(&self) -> Result<(), TrustedRootParseError> {
if !self
.media_type
.starts_with("application/vnd.dev.sigstore.trustedroot+json")
{
return Err(TrustedRootParseError::WrongMediaType {
observed: self.media_type.clone(),
});
}
if self.tlogs.is_empty() {
return Err(TrustedRootParseError::EmptyTlogs);
}
for (index, tlog) in self.tlogs.iter().enumerate() {
if tlog.base_url.is_empty() {
return Err(TrustedRootParseError::MissingBaseUrl { tlog_index: index });
}
if tlog.public_key.valid_for.start.is_none() {
return Err(TrustedRootParseError::MissingValidityStart { tlog_index: index });
}
}
Ok(())
}
#[must_use]
pub fn metadata_signed_at(&self) -> Option<DateTime<Utc>> {
self.tlogs
.iter()
.filter_map(|tlog| tlog.public_key.valid_for.start)
.max()
}
pub fn is_stale_at(
&self,
now: DateTime<Utc>,
max_age: Duration,
anchor: TrustRootStalenessAnchor<'_>,
) -> Result<bool, TrustRootStalenessError> {
let anchor_ts = anchor.resolve(now)?;
let tolerance = chrono::Duration::from_std(TRUSTED_ROOT_CACHE_FUTURE_MTIME_TOLERANCE)
.unwrap_or_default();
if anchor_ts > now + tolerance {
return Err(TrustRootStalenessError::CacheFutureDated {
anchor: anchor.diagnostic_label(),
anchor_ts,
now,
tolerance_seconds: TRUSTED_ROOT_CACHE_FUTURE_MTIME_TOLERANCE.as_secs(),
});
}
let age_ms = now
.timestamp_millis()
.saturating_sub(anchor_ts.timestamp_millis());
let max_age_ms = chrono::Duration::from_std(max_age)
.unwrap_or(chrono::Duration::MAX)
.num_milliseconds();
Ok(age_ms > max_age_ms)
}
pub fn is_stale(
&self,
now: DateTime<Utc>,
anchor: TrustRootStalenessAnchor<'_>,
) -> Result<bool, TrustRootStalenessError> {
self.is_stale_at(now, DEFAULT_MAX_TRUST_ROOT_AGE, anchor)
}
pub fn from_embedded() -> Result<Self, TrustedRootParseError> {
Self::embedded()
}
#[cfg(any(test, feature = "test-support"))]
pub fn from_fixture_rekor_pem(
pem: &str,
signed_at: DateTime<Utc>,
log_id: &str,
) -> Result<Self, TrustedRootKeyError> {
use base64::Engine;
use p256::pkcs8::{DecodePublicKey, EncodePublicKey};
let key = p256::ecdsa::VerifyingKey::from_public_key_pem(pem).map_err(|source| {
TrustedRootKeyError::DecodeKey {
reason: source.to_string(),
}
})?;
let encoded = key
.to_public_key_der()
.map_err(|source| TrustedRootKeyError::DecodeKey {
reason: source.to_string(),
})?;
let raw_b64 = base64::engine::general_purpose::STANDARD.encode(encoded.as_bytes());
Ok(Self {
media_type: "application/vnd.dev.sigstore.trustedroot+json;version=0.1".to_string(),
tlogs: vec![TransparencyLogInstance {
base_url: "https://rekor.test.fixture".to_string(),
log_id: serde_json::json!({ "keyId": log_id }),
hash_algorithm: serde_json::Value::String("SHA2_256".to_string()),
public_key: TransparencyLogPublicKey {
raw_bytes: Some(raw_b64),
key_details: Some("PKIX_ECDSA_P256_SHA_256".to_string()),
valid_for: ValidityPeriod {
start: Some(signed_at),
end: None,
},
extra: serde_json::Map::new(),
},
log_index: serde_json::Value::Null,
tree_id: serde_json::Value::Null,
extra: serde_json::Map::new(),
}],
ctlogs: serde_json::Value::Null,
certificate_authorities: serde_json::Value::Null,
timestamp_authorities: serde_json::Value::Null,
})
}
#[must_use]
pub fn signed_at(&self) -> DateTime<Utc> {
self.metadata_signed_at()
.unwrap_or_else(|| DateTime::<Utc>::from_timestamp(0, 0).expect("epoch is valid"))
}
pub fn rekor_verifying_key(
&self,
receipt_log_id: &str,
) -> Result<p256::ecdsa::VerifyingKey, TrustedRootKeyError> {
use p256::pkcs8::DecodePublicKey;
let ecdsa_tlogs = self
.tlogs
.iter()
.filter(|tlog| {
tlog.public_key
.key_details
.as_deref()
.is_some_and(|details| details.contains("ECDSA_P256"))
})
.collect::<Vec<_>>();
if ecdsa_tlogs.is_empty() {
return Err(TrustedRootKeyError::NoEcdsaP256Tlog);
}
let tlog = ecdsa_tlogs
.into_iter()
.find(|tlog| {
tlog_log_id_key_id(&tlog.log_id)
.as_deref()
.is_some_and(|tlog_key_id| log_id_matches(tlog_key_id, receipt_log_id))
})
.ok_or_else(|| TrustedRootKeyError::TlogLogIdNoMatch {
invariant: REKOR_TRUSTED_ROOT_TLOG_LOGID_NO_MATCH_INVARIANT,
receipt_log_id: receipt_log_id.to_string(),
tlog_log_ids: self
.tlogs
.iter()
.filter_map(|tlog| tlog_log_id_key_id(&tlog.log_id))
.collect(),
})?;
use base64::Engine;
let raw_b64 = tlog
.public_key
.raw_bytes
.as_deref()
.ok_or(TrustedRootKeyError::MissingRawBytes)?;
let der = base64::engine::general_purpose::STANDARD
.decode(raw_b64)
.map_err(|source| TrustedRootKeyError::Base64 {
reason: source.to_string(),
})?;
p256::ecdsa::VerifyingKey::from_public_key_der(&der).map_err(|source| {
TrustedRootKeyError::DecodeKey {
reason: source.to_string(),
}
})
}
pub fn write_atomic(&self, path: impl AsRef<Path>) -> Result<(), TrustedRootIoError> {
let path = path.as_ref();
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent).map_err(|source| TrustedRootIoError::Write {
path: path.to_path_buf(),
source,
})?;
let temp = path.with_extension("json.tmp");
let body = serde_json::to_vec(self).map_err(|source| TrustedRootIoError::Serialize {
path: path.to_path_buf(),
source,
})?;
{
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp)
.map_err(|source| TrustedRootIoError::Write {
path: temp.clone(),
source,
})?;
file.write_all(&body)
.map_err(|source| TrustedRootIoError::Write {
path: temp.clone(),
source,
})?;
file.sync_all()
.map_err(|source| TrustedRootIoError::Write {
path: temp.clone(),
source,
})?;
}
std::fs::rename(&temp, path).map_err(|source| TrustedRootIoError::Write {
path: path.to_path_buf(),
source,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveTrustedRoot {
pub root: TrustedRoot,
pub status: &'static str,
pub cache_path: Option<PathBuf>,
}
pub fn active_trusted_root(
cache_path: Option<&Path>,
) -> Result<ActiveTrustedRoot, TrustedRootIoError> {
if let Some(path) = cache_path {
if let Some(root) = TrustedRoot::load_cached(path)? {
return Ok(ActiveTrustedRoot {
root,
status: CACHED_ROOT_STATUS,
cache_path: Some(path.to_path_buf()),
});
}
}
let root = TrustedRoot::embedded().map_err(|source| TrustedRootIoError::Parse {
path: PathBuf::from("<embedded>"),
source,
})?;
Ok(ActiveTrustedRoot {
root,
status: EMBEDDED_ROOT_STATUS,
cache_path: cache_path.map(Path::to_path_buf),
})
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TrustedRootParseError {
#[error("malformed trusted_root.json: {reason}")]
Malformed {
reason: String,
},
#[error("trusted_root.json mediaType `{observed}` is not application/vnd.dev.sigstore.trustedroot+json")]
WrongMediaType {
observed: String,
},
#[error("trusted_root.json has no Rekor tlogs declared")]
EmptyTlogs,
#[error("trusted_root.json tlog at index {tlog_index} is missing baseUrl")]
MissingBaseUrl {
tlog_index: usize,
},
#[error("trusted_root.json tlog at index {tlog_index} is missing publicKey.validFor.start")]
MissingValidityStart {
tlog_index: usize,
},
}
#[derive(Debug, Error)]
pub enum TrustedRootIoError {
#[error("failed to read trusted_root.json {path:?}: {source}")]
Read {
path: PathBuf,
source: std::io::Error,
},
#[error("invalid trusted_root.json {path:?}: {source}")]
Parse {
path: PathBuf,
source: TrustedRootParseError,
},
#[error("failed to serialise trusted_root.json for {path:?}: {source}")]
Serialize {
path: PathBuf,
source: serde_json::Error,
},
#[error("failed to write trusted_root.json {path:?}: {source}")]
Write {
path: PathBuf,
source: std::io::Error,
},
}
#[derive(Debug, Error)]
pub enum TrustedRootKeyError {
#[error("trusted_root.json has no ECDSA P-256 tlog entry")]
NoEcdsaP256Tlog,
#[error(
"{invariant}: trusted_root.json has no tlog whose logId.keyId matches Rekor receipt logID `{receipt_log_id}` (declared tlogs: {})",
tlog_log_ids.join(", ")
)]
TlogLogIdNoMatch {
invariant: &'static str,
receipt_log_id: String,
tlog_log_ids: Vec<String>,
},
#[error("trusted_root.json tlog publicKey is missing rawBytes")]
MissingRawBytes,
#[error("trusted_root.json tlog publicKey rawBytes is not base64: {reason}")]
Base64 {
reason: String,
},
#[error("trusted_root.json tlog publicKey did not decode as P-256 SPKI: {reason}")]
DecodeKey {
reason: String,
},
}
fn tlog_log_id_key_id(log_id: &serde_json::Value) -> Option<String> {
log_id
.as_object()?
.get("keyId")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
}
fn log_id_matches(tlog_key_id: &str, receipt_log_id: &str) -> bool {
use base64::Engine;
let b64_std = base64::engine::general_purpose::STANDARD;
let b64_url = base64::engine::general_purpose::URL_SAFE;
let b64_url_nopad = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let mut tlog_candidates: Vec<Vec<u8>> = Vec::with_capacity(4);
if let Ok(bytes) = b64_std.decode(tlog_key_id) {
tlog_candidates.push(bytes);
}
if let Ok(bytes) = b64_url.decode(tlog_key_id) {
tlog_candidates.push(bytes);
}
if let Ok(bytes) = b64_url_nopad.decode(tlog_key_id) {
tlog_candidates.push(bytes);
}
tlog_candidates.push(tlog_key_id.as_bytes().to_vec());
let mut receipt_candidates: Vec<Vec<u8>> = Vec::with_capacity(5);
if let Ok(bytes) = decode_lowercase_hex(receipt_log_id) {
receipt_candidates.push(bytes);
}
if let Ok(bytes) = b64_std.decode(receipt_log_id) {
receipt_candidates.push(bytes);
}
if let Ok(bytes) = b64_url.decode(receipt_log_id) {
receipt_candidates.push(bytes);
}
if let Ok(bytes) = b64_url_nopad.decode(receipt_log_id) {
receipt_candidates.push(bytes);
}
receipt_candidates.push(receipt_log_id.as_bytes().to_vec());
for tlog_bytes in &tlog_candidates {
for receipt_bytes in &receipt_candidates {
if tlog_bytes == receipt_bytes {
return true;
}
}
}
false
}
fn decode_lowercase_hex(s: &str) -> Result<Vec<u8>, ()> {
if s.is_empty() || s.len() % 2 != 0 {
return Err(());
}
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(s.len() / 2);
for chunk in bytes.chunks_exact(2) {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
out.push((hi << 4) | lo);
}
Ok(out)
}
fn hex_nibble(b: u8) -> Result<u8, ()> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(10 + b - b'a'),
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use tempfile::tempdir;
#[test]
fn embedded_root_parses_and_has_rekor_tlogs() {
let root = TrustedRoot::embedded().expect("embedded root parses");
assert!(root
.media_type
.starts_with("application/vnd.dev.sigstore.trustedroot+json"));
assert!(!root.tlogs.is_empty(), "embedded root must declare tlogs");
let signed_at = root
.metadata_signed_at()
.expect("embedded root has a validity start");
let v1_launch = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
assert!(
signed_at >= v1_launch,
"tlog start {signed_at} predates v1 launch"
);
}
fn embedded_snapshot_anchor_date() -> DateTime<Utc> {
NaiveDate::parse_from_str(EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE, "%Y-%m-%d")
.expect("EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE parses")
.and_hms_opt(0, 0, 0)
.expect("midnight is valid")
.and_utc()
}
#[test]
fn fresh_root_is_not_stale_within_30_days_against_embedded_snapshot_anchor() {
let root = TrustedRoot::embedded().unwrap();
let anchor = embedded_snapshot_anchor_date();
let now = anchor + chrono::Duration::days(29);
let stale = root
.is_stale_at(
now,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::embedded_snapshot(),
)
.expect("anchor resolves");
assert!(!stale);
}
#[test]
fn root_is_stale_after_31_days_against_embedded_snapshot_anchor() {
let root = TrustedRoot::embedded().unwrap();
let anchor = embedded_snapshot_anchor_date();
let now = anchor + chrono::Duration::days(31);
let stale = root
.is_stale_at(
now,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::embedded_snapshot(),
)
.expect("anchor resolves");
assert!(stale);
}
#[test]
fn cache_mtime_anchor_resolves_to_file_mtime() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("trusted_root.json");
let root = TrustedRoot::embedded().unwrap();
root.write_atomic(&path).unwrap();
let now = Utc::now();
let old_mtime = now - chrono::Duration::days(40);
let mtime_systemtime = std::time::SystemTime::UNIX_EPOCH
+ std::time::Duration::from_secs(old_mtime.timestamp() as u64);
std::fs::File::options()
.write(true)
.open(&path)
.unwrap()
.set_modified(mtime_systemtime)
.expect("set mtime");
let stale = root
.is_stale_at(
now,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::cache_file_mtime(&path),
)
.expect("anchor resolves");
assert!(stale, "40-day-old cache must be stale");
}
#[test]
fn cache_mtime_anchor_fails_loud_when_cache_missing() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("does-not-exist.json");
let root = TrustedRoot::embedded().unwrap();
let err = root
.is_stale_at(
Utc::now(),
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::cache_file_mtime(&path),
)
.unwrap_err();
assert!(matches!(err, TrustRootStalenessError::CacheMetadata { .. }));
}
fn write_cache_with_systemtime(path: &Path, mtime: std::time::SystemTime) {
let root = TrustedRoot::embedded().unwrap();
root.write_atomic(path).unwrap();
std::fs::File::options()
.write(true)
.open(path)
.unwrap()
.set_modified(mtime)
.expect("set mtime");
}
#[test]
fn cache_mtime_anchor_refuses_far_future_dated_cache() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("trusted_root.json");
let now = Utc::now();
let future = now + chrono::Duration::days(365 * 70);
let future_systemtime = std::time::SystemTime::UNIX_EPOCH
+ std::time::Duration::from_secs(future.timestamp() as u64);
write_cache_with_systemtime(&path, future_systemtime);
let root = TrustedRoot::embedded().unwrap();
let err = root
.is_stale_at(
now,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::cache_file_mtime(&path),
)
.unwrap_err();
match err {
TrustRootStalenessError::CacheFutureDated {
anchor,
tolerance_seconds,
..
} => {
assert!(
anchor.contains("cache_file_mtime"),
"diagnostic label must name the cache_file_mtime anchor: got {anchor}"
);
assert_eq!(
tolerance_seconds,
TRUSTED_ROOT_CACHE_FUTURE_MTIME_TOLERANCE.as_secs(),
);
}
other => panic!("expected CacheFutureDated, got {other:?}"),
}
}
#[test]
fn cache_mtime_anchor_tolerates_small_clock_skew() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("trusted_root.json");
let now = Utc::now();
let half_tolerance = chrono::Duration::seconds(
(TRUSTED_ROOT_CACHE_FUTURE_MTIME_TOLERANCE.as_secs() / 2) as i64,
);
let skewed = now + half_tolerance;
let skewed_systemtime = std::time::SystemTime::UNIX_EPOCH
+ std::time::Duration::from_secs(skewed.timestamp() as u64);
write_cache_with_systemtime(&path, skewed_systemtime);
let root = TrustedRoot::embedded().unwrap();
let stale = root
.is_stale_at(
now,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::cache_file_mtime(&path),
)
.expect("small skew must not trigger the future-dated guard");
assert!(!stale);
}
#[test]
fn is_stale_at_saturates_instead_of_panic_on_backward_clock() {
let root = TrustedRoot::embedded().unwrap();
let anchor_date = embedded_snapshot_anchor_date();
let now = anchor_date;
let stale = root
.is_stale_at(
now,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::embedded_snapshot(),
)
.expect("anchor resolves");
assert!(
!stale,
"age_ms == 0 must never be stale; saturating_sub must clamp at 0 not wrap"
);
let just_before = anchor_date - chrono::Duration::milliseconds(1);
let stale_edge = root
.is_stale_at(
anchor_date,
DEFAULT_MAX_TRUST_ROOT_AGE,
TrustRootStalenessAnchor::EmbeddedSnapshotDate {
snapshot_iso_date: EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE,
},
)
.expect("anchor resolves");
assert!(!stale_edge);
let _ = just_before; }
#[test]
fn trusted_root_cache_future_dated_invariant_token_is_stable() {
assert_eq!(
STABLE_INVARIANT_TRUSTED_ROOT_CACHE_FUTURE_DATED,
"audit.verify.trusted_root.cache_future_dated"
);
}
#[test]
fn trusted_root_staleness_invariant_tokens_are_stable() {
assert_eq!(
TRUSTED_ROOT_STALE_INVARIANT,
"audit.verify.trusted_root.stale_beyond_max_age"
);
assert_eq!(
TRUSTED_ROOT_SNAPSHOT_STALE_INVARIANT,
"audit.verify.trusted_root.snapshot_stale"
);
assert_eq!(
TRUSTED_ROOT_CACHE_STALE_INVARIANT,
"audit.verify.trusted_root.cache_stale"
);
assert_ne!(
TRUSTED_ROOT_SNAPSHOT_STALE_INVARIANT, TRUSTED_ROOT_STALE_INVARIANT,
"snapshot_stale token must differ from the generic back-compat token"
);
assert_ne!(
TRUSTED_ROOT_CACHE_STALE_INVARIANT, TRUSTED_ROOT_STALE_INVARIANT,
"cache_stale token must differ from the generic back-compat token"
);
assert_ne!(
TRUSTED_ROOT_SNAPSHOT_STALE_INVARIANT, TRUSTED_ROOT_CACHE_STALE_INVARIANT,
"snapshot_stale and cache_stale tokens must be distinct branches"
);
}
#[test]
fn embedded_snapshot_stale_emit_precondition_pins_invariant_token() {
let root = TrustedRoot::embedded().expect("embedded root parses");
let anchor = embedded_snapshot_anchor_date();
let now = anchor + chrono::Duration::days(31);
let stale = root
.is_stale(now, TrustRootStalenessAnchor::embedded_snapshot())
.expect("anchor resolves");
assert!(
stale,
"embedded snapshot must be stale once now > snapshot+30d"
);
assert_eq!(
TRUSTED_ROOT_SNAPSHOT_STALE_INVARIANT, "audit.verify.trusted_root.snapshot_stale",
"CLI audit-verify emit site keys on this exact stable token"
);
assert_eq!(
TRUSTED_ROOT_STALE_INVARIANT,
"audit.verify.trusted_root.stale_beyond_max_age"
);
}
#[test]
fn embedded_snapshot_not_stale_emit_precondition_holds_within_30d() {
let root = TrustedRoot::embedded().expect("embedded root parses");
let anchor = embedded_snapshot_anchor_date();
let now = anchor + chrono::Duration::days(29);
let stale = root
.is_stale(now, TrustRootStalenessAnchor::embedded_snapshot())
.expect("anchor resolves");
assert!(
!stale,
"embedded snapshot must NOT be stale within snapshot+30d; \
CLI emit site for `{TRUSTED_ROOT_SNAPSHOT_STALE_INVARIANT}` must stay silent"
);
}
#[test]
fn parse_rejects_wrong_media_type() {
let body = serde_json::json!({
"mediaType": "application/json",
"tlogs": [],
});
let err = TrustedRoot::parse_bytes(body.to_string().as_bytes()).unwrap_err();
assert!(matches!(err, TrustedRootParseError::WrongMediaType { .. }));
}
#[test]
fn parse_rejects_empty_tlogs() {
let body = serde_json::json!({
"mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
"tlogs": [],
});
let err = TrustedRoot::parse_bytes(body.to_string().as_bytes()).unwrap_err();
assert_eq!(err, TrustedRootParseError::EmptyTlogs);
}
#[test]
fn parse_rejects_tlog_without_validity_start() {
let body = serde_json::json!({
"mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
"tlogs": [{
"baseUrl": "https://rekor.sigstore.dev",
"publicKey": {
"rawBytes": "AAAA",
"validFor": {}
}
}]
});
let err = TrustedRoot::parse_bytes(body.to_string().as_bytes()).unwrap_err();
assert!(matches!(
err,
TrustedRootParseError::MissingValidityStart { tlog_index: 0 }
));
}
#[test]
fn load_cached_returns_none_for_missing_path() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("does-not-exist.json");
let result = TrustedRoot::load_cached(&path).expect("missing path returns Ok(None)");
assert!(result.is_none());
}
#[test]
fn load_cached_returns_some_for_valid_file() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("trusted_root.json");
let root = TrustedRoot::embedded().unwrap();
root.write_atomic(&path).unwrap();
let loaded = TrustedRoot::load_cached(&path).unwrap().unwrap();
assert_eq!(loaded.media_type, root.media_type);
}
#[test]
fn write_atomic_replaces_existing_file() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("trusted_root.json");
std::fs::write(&path, b"old garbage").unwrap();
let root = TrustedRoot::embedded().unwrap();
root.write_atomic(&path).unwrap();
let loaded = TrustedRoot::load_cached(&path).unwrap().unwrap();
assert_eq!(loaded.media_type, root.media_type);
}
#[test]
fn active_trusted_root_prefers_cache_then_embedded() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("trusted_root.json");
let active = active_trusted_root(Some(&path)).expect("missing cache falls back");
assert_eq!(active.status, EMBEDDED_ROOT_STATUS);
assert_eq!(active.cache_path.as_deref(), Some(path.as_path()));
let root = TrustedRoot::embedded().unwrap();
root.write_atomic(&path).unwrap();
let active = active_trusted_root(Some(&path)).unwrap();
assert_eq!(active.status, CACHED_ROOT_STATUS);
}
#[test]
fn missing_embedded_snapshot_date_constant_fails_loud() {
let root = TrustedRoot::embedded().unwrap();
let anchor = TrustRootStalenessAnchor::EmbeddedSnapshotDate {
snapshot_iso_date: "not-a-date",
};
let err = root
.is_stale_at(Utc::now(), DEFAULT_MAX_TRUST_ROOT_AGE, anchor)
.unwrap_err();
assert!(matches!(
err,
TrustRootStalenessError::MalformedEmbeddedSnapshotDate { .. }
));
}
fn discover_embedded_snapshot_filename() -> (String, String) {
let dir = std::path::Path::new("src/external_sink/embedded");
let mut matches: Vec<(String, String)> = Vec::new();
for entry in std::fs::read_dir(dir)
.expect("embedded directory readable from crate-root CWD during cargo test")
{
let entry = entry.expect("read_dir entry");
let name_os = entry.file_name();
let name = match name_os.to_str() {
Some(s) => s.to_string(),
None => continue,
};
if let Some(date_and_suffix) = name.strip_prefix("sigstore_trusted_root_") {
if let Some(date) = date_and_suffix.strip_suffix(".json") {
matches.push((name.clone(), date.to_string()));
}
}
}
assert_eq!(
matches.len(),
1,
"embedded directory must contain exactly one sigstore_trusted_root_YYYY-MM-DD.json, found: {matches:?}"
);
matches.into_iter().next().unwrap()
}
#[test]
fn embedded_snapshot_date_constant_matches_embedded_filename() {
let (filename, date) = discover_embedded_snapshot_filename();
assert_eq!(
EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE, date,
"EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE = `{EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE}` but embedded filename `{filename}` encodes date `{date}`"
);
}
#[test]
fn embedded_filename_pin_is_documented() {
assert!(
EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE.len() == 10
&& EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE.as_bytes()[4] == b'-'
&& EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE.as_bytes()[7] == b'-',
"EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE must be YYYY-MM-DD; got `{EMBEDDED_TRUSTED_ROOT_SNAPSHOT_DATE}`"
);
}
fn synth_trust_root_with_log_id(log_id_key_id: &str, signed_at: DateTime<Utc>) -> TrustedRoot {
const PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE38RLcWzg03IUsJCRqVqTfrIsrZ16\n9knDnAIDtWs7oSxtQ7vlFDJwBMsFiyufFcxRRXotLozXlslNcujXkKAdzQ==\n-----END PUBLIC KEY-----\n";
TrustedRoot::from_fixture_rekor_pem(PEM, signed_at, log_id_key_id)
.expect("fixture key parses")
}
#[test]
fn rekor_verifying_key_returns_key_for_matching_logid() {
let signed_at = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let root = synth_trust_root_with_log_id("my-fixture-log-id", signed_at);
root.rekor_verifying_key("my-fixture-log-id")
.expect("matching log_id resolves to a verifying key");
}
#[test]
fn rekor_verifying_key_refuses_when_no_tlog_matches_receipt_logid() {
let signed_at = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let root = synth_trust_root_with_log_id("trust-root-log-id", signed_at);
let err = root
.rekor_verifying_key("a-different-receipt-log-id")
.unwrap_err();
match err {
TrustedRootKeyError::TlogLogIdNoMatch {
invariant,
receipt_log_id,
tlog_log_ids,
} => {
assert_eq!(invariant, REKOR_TRUSTED_ROOT_TLOG_LOGID_NO_MATCH_INVARIANT);
assert_eq!(receipt_log_id, "a-different-receipt-log-id");
assert_eq!(tlog_log_ids, vec!["trust-root-log-id".to_string()]);
}
other => panic!("expected TlogLogIdNoMatch, got {other:?}"),
}
}
#[test]
fn rekor_verifying_key_does_not_silently_fall_back_to_latest_when_logid_unknown() {
let old = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
let recent = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let matching_log_id = "matching-log-id";
let _decoy_log_id = "decoy-but-newer-log-id";
let mut root = synth_trust_root_with_log_id(matching_log_id, old);
const PEM2: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE38RLcWzg03IUsJCRqVqTfrIsrZ16\n9knDnAIDtWs7oSxtQ7vlFDJwBMsFiyufFcxRRXotLozXlslNcujXkKAdzQ==\n-----END PUBLIC KEY-----\n";
let decoy_root =
TrustedRoot::from_fixture_rekor_pem(PEM2, recent, "decoy-but-newer-log-id")
.expect("decoy tlog parses");
root.tlogs.extend(decoy_root.tlogs);
root.rekor_verifying_key(matching_log_id)
.expect("log-bound selector picks the matching (older) tlog despite a newer decoy");
let only_decoy = TrustedRoot {
media_type: root.media_type.clone(),
tlogs: root
.tlogs
.iter()
.filter(|tlog| tlog_log_id_key_id(&tlog.log_id).as_deref() != Some(matching_log_id))
.cloned()
.collect(),
certificate_authorities: root.certificate_authorities.clone(),
ctlogs: root.ctlogs.clone(),
timestamp_authorities: root.timestamp_authorities.clone(),
};
assert_eq!(
only_decoy.tlogs.len(),
1,
"decoy-only root must have exactly one tlog"
);
let err = only_decoy.rekor_verifying_key(matching_log_id).unwrap_err();
assert!(
matches!(
err,
TrustedRootKeyError::TlogLogIdNoMatch {
invariant: REKOR_TRUSTED_ROOT_TLOG_LOGID_NO_MATCH_INVARIANT,
..
}
),
"selector silently fell back to a non-matching tlog instead of refusing; got {err:?}"
);
}
#[test]
fn log_id_matches_decodes_hex_against_base64() {
use base64::Engine;
let key_id_bytes: [u8; 32] = [
0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x10, 0x20, 0x30, 0x40,
0x50, 0x60, 0x70, 0x80,
];
let b64 = base64::engine::general_purpose::STANDARD.encode(key_id_bytes);
let hex: String = key_id_bytes.iter().map(|b| format!("{b:02x}")).collect();
assert!(log_id_matches(&b64, &hex));
let mut tampered = key_id_bytes;
tampered[0] ^= 0x01;
let tampered_hex: String = tampered.iter().map(|b| format!("{b:02x}")).collect();
assert!(!log_id_matches(&b64, &tampered_hex));
}
#[test]
fn log_id_matches_accepts_url_safe_base64_both_sides() {
use base64::Engine;
let key_id_bytes: [u8; 32] = [
0xfb, 0xff, 0xbf, 0xfe, 0x3e, 0x3f, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x10, 0x20, 0x30, 0x40, 0x50,
0x60, 0x70, 0x80, 0x90,
];
let b64_std = base64::engine::general_purpose::STANDARD.encode(key_id_bytes);
let b64_url = base64::engine::general_purpose::URL_SAFE.encode(key_id_bytes);
let b64_url_nopad = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key_id_bytes);
let hex: String = key_id_bytes.iter().map(|b| format!("{b:02x}")).collect();
assert_ne!(
b64_std, b64_url,
"fixture bytes were chosen so the two alphabets differ; if they match the test is vacuous",
);
assert!(log_id_matches(&b64_std, &b64_url));
assert!(log_id_matches(&b64_url, &b64_std));
assert!(log_id_matches(&b64_url_nopad, &hex));
assert!(log_id_matches(&b64_url, &b64_url_nopad));
let mut tampered = key_id_bytes;
tampered[0] ^= 0x01;
let tampered_url = base64::engine::general_purpose::URL_SAFE.encode(tampered);
assert!(!log_id_matches(&b64_std, &tampered_url));
}
}