fix(release): 完成分发证明代际绑定与质量门禁收口
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-15 21:13:52 +08:00
parent 13b0bd5b45
commit 99355effe4
29 changed files with 1137 additions and 189 deletions
+109 -24
View File
@@ -120,8 +120,9 @@ use workflow_commands::{
const EXIT_ERROR: i32 = 1;
const EXIT_LOCKED: i32 = 75;
const DEFAULT_WATCH_INTERVAL_SECONDS: u64 = 60 * 60;
const DEFAULT_ERROR_RETRY_SECONDS: u64 = 60;
const DEFAULT_WATCH_INTERVAL_SECONDS: u64 =
bat_infrastructure::DEFAULT_OFFICIAL_VERIFICATION_INTERVAL_SECONDS;
const DEFAULT_ERROR_RETRY_SECONDS: u64 = bat_infrastructure::DEFAULT_OFFICIAL_ERROR_RETRY_SECONDS;
const DEFAULT_DAEMON_STATE_DIR: &str = "/tmp/bat-pid";
const DAEMON_PID_FILE: &str = "bat.pid";
const DAEMON_STATUS_FILE: &str = "bat-status.json";
@@ -184,10 +185,13 @@ fn run() -> anyhow::Result<i32> {
} else {
assert_no_live_daemon_output_conflict(&options, "run")?;
let mut logger = ProgressLogger::new(options.progress);
let report =
OfficialUpdateService::new().run_with_progress(&options.config, |event| {
logger.log(event);
})?;
let report = OfficialUpdateService::with_verification_cadence(
options.interval,
options.error_retry_interval,
)
.run_with_progress(&options.config, |event| {
logger.log(event);
})?;
if should_print_status(report.update_status, options.quiet_up_to_date) {
report_output::print_report(options.output_format, &report)?;
}
@@ -717,7 +721,10 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> {
}
install_shutdown_signal_handlers()?;
clear_shutdown_signal_request();
let service = OfficialUpdateService::new();
let service = OfficialUpdateService::with_verification_cadence(
options.interval,
options.error_retry_interval,
);
let mut logger = ProgressLogger::new(options.progress);
let daemon_state_dir = options.state_dir.clone();
if options.daemon_child {
@@ -747,9 +754,12 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> {
let registry = registry.clone();
let sync_lock = Arc::clone(&sync_lock);
let control = Arc::clone(control);
let worker_service = service.clone();
thread::Builder::new()
.name("bat-daemon-task-worker".to_string())
.spawn(move || run_task_worker(task_rx, registry, sync_lock, control))?
.spawn(move || {
run_task_worker(task_rx, registry, sync_lock, control, worker_service)
})?
};
let context = DaemonTaskContext {
registry,
@@ -792,6 +802,9 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> {
}
let mut iteration_config = options.config.clone();
// Watch 是 current release 的唯一周期性本地验证入口,不能被一次性
// 同步命令的 audit_local 选项关闭。
iteration_config.audit_local = true;
if pending_scheduled_force {
iteration_config.force = true;
}
@@ -1167,6 +1180,8 @@ struct ResourceManifestRpcParams {
#[serde(default)]
expected_manifest_identity: Option<String>,
#[serde(default)]
expected_verification_generation: Option<u64>,
#[serde(default)]
offset: usize,
#[serde(default = "default_rpc_manifest_limit")]
limit: usize,
@@ -3027,6 +3042,15 @@ fn build_resource_manifest_report(
record.id
));
}
if params.release_id.is_none()
|| params.expected_publication_identity.is_none()
|| params.expected_manifest_identity.is_none()
|| params.expected_verification_generation.is_none()
{
return Err(anyhow::anyhow!(
"resource.manifest 请求必须同时携带 release_id、publication_identity、manifest_identity 和 verification_generation"
));
}
let manifest_path = record.resource_root.join("official-download-manifest.json");
let Some(manifest_bytes) =
read_file_no_symlink(&manifest_path, "官方下载 manifest").map_err(anyhow::Error::msg)?
@@ -3047,18 +3071,59 @@ fn build_resource_manifest_report(
};
let manifest_identity = blake3::hash(&manifest_bytes).to_hex().to_string();
let mapping_identity = official_distribution_mapping_identity(&manifest);
let attestation = status_file.as_ref().and_then(|status| {
build_official_distribution_attestation(&status.resource_output_root).ok()
});
let publication_identity = attestation
let attestation = status_file
.as_ref()
.map(|report| report.publication_identity.clone())
.filter(|identity| !identity.is_empty())
.unwrap_or_else(|| format!("manifest-v1-{manifest_identity}"));
let verification_generation = attestation
.as_ref()
.map(|report| report.verification_generation)
.unwrap_or(0);
.map(|status| {
build_official_distribution_attestation(&status.resource_output_root).map_err(|error| {
anyhow::anyhow!("读取 resource.manifest attestation 失败:{error}")
})
})
.transpose()?;
if let Some(report) = attestation.as_ref() {
if report.resource_root != record.resource_root.display().to_string() {
return Err(anyhow::anyhow!(
"resource.manifest attestation root 与当前 release 不一致:attestation={} current={}",
report.resource_root,
record.resource_root.display()
));
}
}
let Some(report) = attestation.as_ref() else {
return Err(anyhow::anyhow!(
"resource.manifest 请求缺少当前 Rust attestation"
));
};
let publication_identity = report.publication_identity.clone();
let verification_generation = report.verification_generation;
if !report.available
|| !report.ready
|| report.integrity_status != "verified"
|| report.channel != "official"
{
return Err(anyhow::anyhow!(
"resource.manifest 当前 attestation 不可用:status={} integrity_status={}",
report.status,
report.integrity_status
));
}
if report.release_id != record.id
|| report.publication_identity != publication_identity
|| report.mapping_identity != mapping_identity
|| report.manifest_identity != manifest_identity
{
return Err(anyhow::anyhow!(
"resource.manifest 当前 attestation identity 与 manifest 不一致"
));
}
if params.release_id.as_deref() != Some(record.id.as_str())
|| params.expected_publication_identity.as_deref() != Some(publication_identity.as_str())
|| params.expected_manifest_identity.as_deref() != Some(manifest_identity.as_str())
|| params.expected_verification_generation != Some(verification_generation)
{
return Err(anyhow::anyhow!(
"resource.manifest 请求 identity 或 verification_generation 与当前 attestation 不一致"
));
}
if params
.expected_manifest_identity
.as_deref()
@@ -5378,8 +5443,11 @@ fn run_sync_command_foreground(
config.force = false;
}
let mut logger = ProgressLogger::new(options.progress);
let report =
OfficialUpdateService::new().run_with_progress(&config, |event| logger.log(event))?;
let report = OfficialUpdateService::with_verification_cadence(
options.interval,
options.error_retry_interval,
)
.run_with_progress(&config, |event| logger.log(event))?;
let command_report = CommandReport {
command: command_name,
status: "completed",
@@ -5533,8 +5601,22 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result<bool> {
config.force = false;
let mut logger = ProgressLogger::new(options.progress);
let update_report =
OfficialUpdateService::new().run_with_progress(&config, |event| logger.log(event))?;
let service = OfficialUpdateService::with_verification_cadence(
options.interval,
options.error_retry_interval,
);
let update_report = match service.run_with_progress(&config, |event| logger.log(event)) {
Ok(report) => report,
Err(error) => {
// A verifier error must revoke the previous ready generation
// before the command returns its original failure.
let _ = verify_and_record_official_distribution_attestation(
&config,
service.attestation_max_age_seconds(),
);
return Err(error);
}
};
let verified_resource_root = active_official_resource_root(&config.output_root)?;
let verification = bat_infrastructure::OfficialResourcePullService::with_curl_command(
&verified_resource_root,
@@ -5558,7 +5640,10 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result<bool> {
zip_error: item.zip_error.clone(),
})
.collect::<Vec<_>>();
verify_and_record_official_distribution_attestation(&config)?;
verify_and_record_official_distribution_attestation(
&config,
service.attestation_max_age_seconds(),
)?;
let healthy = update_report.update_status == OfficialUpdateStatus::UpToDate
&& update_report.local_manifest_repair_needed_count == 0
&& verification.is_clean();
+214 -5
View File
@@ -4181,7 +4181,7 @@ fn dispatch_catalog_status_reads_current_snapshot() {
"req-cat-1".to_string(),
);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["ok"], true, "{value}");
assert_eq!(value["data"]["available"], true);
assert_eq!(value["data"]["bundle_version"], "bundle-b2");
assert_eq!(value["data"]["status"], "published");
@@ -4405,6 +4405,8 @@ fn catalog_refresh_config_is_dry_run_plan_only() {
#[test]
fn dispatch_resource_manifest_paginates() {
use std::os::unix::fs::symlink;
let temp = tempfile::TempDir::new().unwrap();
let state_dir = temp.path().join("state");
let output_root = temp.path().join("output");
@@ -4432,16 +4434,80 @@ fn dispatch_resource_manifest_paginates() {
},
},
});
let mut manifest: bat_infrastructure::OfficialDownloadManifest =
serde_json::from_value(manifest).unwrap();
let mapping_identity = bat_infrastructure::official_distribution_mapping_identity(&manifest);
manifest.distribution_mapping_identity = Some(mapping_identity.clone());
manifest.destination_index = manifest
.entries
.values()
.map(|entry| (entry.destination.clone(), entry.url.clone()))
.collect();
let manifest_bytes = serde_json::to_vec(&manifest).unwrap();
fs::write(
current_dir.join("official-download-manifest.json"),
serde_json::to_vec(&manifest).unwrap(),
&manifest_bytes,
)
.unwrap();
let manifest_identity = blake3::hash(&manifest_bytes).to_hex().to_string();
let publication_identity = format!("odp-v1-{mapping_identity}-{manifest_identity}");
fs::write(
current_dir.join("official-distribution-publication.json"),
serde_json::to_vec(&serde_json::json!({
"version": 1,
"official_release_id": "v-current",
"mapping_identity": mapping_identity,
"manifest_identity": manifest_identity,
"entry_count": 3,
}))
.unwrap(),
)
.unwrap();
fs::write(
current_dir.join("official-distribution-attestation.json"),
serde_json::to_vec(&serde_json::json!({
"version": 1,
"channel": "official",
"official_release_id": "v-current",
"resource_root": current_dir,
"publication_identity": publication_identity,
"mapping_identity": mapping_identity,
"manifest_identity": manifest_identity,
"entry_count": 3,
"integrity_status": "verified",
"status": "ready",
"status_code": "distribution.ready",
"ready": true,
"verification_generation": 1,
"verified_at": unix_seconds_now(),
"max_age_seconds": 7260,
}))
.unwrap(),
)
.unwrap();
symlink(
Path::new("versions").join("v-current"),
output_root.join("current"),
)
.unwrap();
let bound_params = serde_json::json!({
"release_id": "v-current",
"expected_publication_identity": publication_identity,
"expected_manifest_identity": manifest_identity,
"expected_verification_generation": 1,
});
let envelope = dispatch_rpc_method(
&rpc_request(
"resource.manifest",
Some(serde_json::json!({ "offset": 1, "limit": 2 })),
Some(serde_json::json!({
"release_id": bound_params["release_id"],
"expected_publication_identity": bound_params["expected_publication_identity"],
"expected_manifest_identity": bound_params["expected_manifest_identity"],
"expected_verification_generation": bound_params["expected_verification_generation"],
"offset": 1,
"limit": 2,
})),
),
&state_dir,
&new_daemon_control(),
@@ -4463,7 +4529,16 @@ fn dispatch_resource_manifest_paginates() {
// 非法 limit → 参数错误。
let envelope = dispatch_rpc_method(
&rpc_request("resource.manifest", Some(serde_json::json!({ "limit": 0 }))),
&rpc_request(
"resource.manifest",
Some(serde_json::json!({
"release_id": bound_params["release_id"],
"expected_publication_identity": bound_params["expected_publication_identity"],
"expected_manifest_identity": bound_params["expected_manifest_identity"],
"expected_verification_generation": bound_params["expected_verification_generation"],
"limit": 0,
})),
),
&state_dir,
&new_daemon_control(),
&test_task_context(),
@@ -4476,7 +4551,14 @@ fn dispatch_resource_manifest_paginates() {
let envelope = dispatch_rpc_method(
&rpc_request(
"resource.list",
Some(serde_json::json!({ "offset": 2, "limit": 1 })),
Some(serde_json::json!({
"release_id": bound_params["release_id"],
"expected_publication_identity": bound_params["expected_publication_identity"],
"expected_manifest_identity": bound_params["expected_manifest_identity"],
"expected_verification_generation": bound_params["expected_verification_generation"],
"offset": 2,
"limit": 1,
})),
),
&state_dir,
&new_daemon_control(),
@@ -4495,7 +4577,9 @@ fn dispatch_resource_manifest_paginates() {
"resource.manifest",
Some(serde_json::json!({
"release_id": "v-current",
"expected_publication_identity": publication_identity,
"expected_manifest_identity": "wrong-generation",
"expected_verification_generation": 1,
"offset": 0,
"limit": 1,
})),
@@ -4509,6 +4593,131 @@ fn dispatch_resource_manifest_paginates() {
assert_eq!(value["ok"], false);
}
#[test]
fn dispatch_resource_manifest_rejects_previous_verification_generation() {
use std::os::unix::fs::symlink;
let temp = tempfile::TempDir::new().unwrap();
let state_dir = temp.path().join("state");
let output_root = temp.path().join("output");
let current_dir = write_catalog_fixture(&state_dir, &output_root, "bundle-b1", None);
let manifest = serde_json::json!({
"version": 1,
"entries": {
"https://prod-clientpatch.bluearchiveyostar.com/a": {
"url": "https://prod-clientpatch.bluearchiveyostar.com/a",
"destination": "a",
"bytes": 1,
"blake3": blake3::hash(b"a").to_hex().to_string(),
},
"https://prod-clientpatch.bluearchiveyostar.com/b": {
"url": "https://prod-clientpatch.bluearchiveyostar.com/b",
"destination": "b",
"bytes": 1,
"blake3": blake3::hash(b"b").to_hex().to_string(),
},
},
});
let mut manifest: bat_infrastructure::OfficialDownloadManifest =
serde_json::from_value(manifest).unwrap();
manifest.distribution_mapping_identity = Some(
bat_infrastructure::official_distribution_mapping_identity(&manifest),
);
manifest.destination_index = manifest
.entries
.values()
.map(|entry| (entry.destination.clone(), entry.url.clone()))
.collect();
let manifest_bytes = serde_json::to_vec(&manifest).unwrap();
fs::write(
current_dir.join("official-download-manifest.json"),
&manifest_bytes,
)
.unwrap();
fs::write(current_dir.join("a"), b"a").unwrap();
fs::write(current_dir.join("b"), b"b").unwrap();
symlink(
Path::new("versions").join("v-current"),
output_root.join("current"),
)
.unwrap();
let mapping_identity = bat_infrastructure::official_distribution_mapping_identity(&manifest);
let manifest_identity = blake3::hash(&manifest_bytes).to_hex().to_string();
let publication_identity = format!("odp-v1-{mapping_identity}-{manifest_identity}");
let write_attestation = |generation: u64| {
fs::write(
current_dir.join("official-distribution-publication.json"),
serde_json::to_vec(&serde_json::json!({
"version": 1,
"official_release_id": "v-current",
"mapping_identity": mapping_identity,
"manifest_identity": manifest_identity,
"entry_count": 2,
}))
.unwrap(),
)
.unwrap();
fs::write(
current_dir.join("official-distribution-attestation.json"),
serde_json::to_vec(&serde_json::json!({
"version": 1,
"channel": "official",
"official_release_id": "v-current",
"resource_root": current_dir,
"publication_identity": publication_identity,
"mapping_identity": mapping_identity,
"manifest_identity": manifest_identity,
"entry_count": 2,
"integrity_status": "verified",
"status": "ready",
"status_code": "distribution.ready",
"ready": true,
"verification_generation": generation,
"verified_at": unix_seconds_now(),
"max_age_seconds": 7260,
}))
.unwrap(),
)
.unwrap();
};
write_attestation(4);
let bound_params = serde_json::json!({
"release_id": "v-current",
"expected_publication_identity": publication_identity,
"expected_manifest_identity": manifest_identity,
"expected_verification_generation": 4,
"offset": 0,
"limit": 1,
});
let envelope = dispatch_rpc_method(
&rpc_request("resource.manifest", Some(bound_params.clone())),
&state_dir,
&new_daemon_control(),
&test_task_context(),
"req-generation-1".to_string(),
);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["generation"], 4);
assert_eq!(
value["data"]["resource_root"],
current_dir.to_string_lossy().as_ref()
);
write_attestation(5);
let envelope = dispatch_rpc_method(
&rpc_request("resource.manifest", Some(bound_params)),
&state_dir,
&new_daemon_control(),
&test_task_context(),
"req-generation-2".to_string(),
);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], false);
}
fn write_resource_index_fixture(repository_path: &Path) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
+23 -7
View File
@@ -537,8 +537,8 @@ pub(super) fn run_task_worker(
registry: TaskRegistry,
sync_lock: Arc<Mutex<()>>,
control: DaemonControl,
service: OfficialUpdateService,
) {
let service = OfficialUpdateService::new();
for job in receiver {
registry.update(&job.id, |record| {
record.status = "running";
@@ -602,15 +602,31 @@ pub(super) fn run_task_worker(
},
)
};
run_result
.and_then(|report| {
if job.kind == TaskKind::Verify {
let run_result = if job.kind == TaskKind::Verify {
match run_result {
Ok(report) => {
bat_infrastructure::verify_and_record_official_distribution_attestation(
&job.config,
)?;
service.attestation_max_age_seconds(),
)
.map(|_| report)
}
Ok(report)
})
Err(error) => {
// If the verifier failed before returning its report
// (for example, a malformed manifest), make a best
// effort to revoke the previous ready generation.
let _ =
bat_infrastructure::verify_and_record_official_distribution_attestation(
&job.config,
service.attestation_max_age_seconds(),
);
Err(error)
}
}
} else {
run_result
};
run_result
.map(|report| serde_json::to_value(&report).map_err(anyhow::Error::from))
.and_then(|result| result)
};
+4 -3
View File
@@ -80,7 +80,8 @@ pub use official_changes::{
OFFICIAL_RESOURCE_CHANGES_VERSION,
};
pub use official_download::{
official_distribution_mapping_identity, read_cas_reuse_reference_manifest_at,
official_distribution_mapping_identity, official_distribution_max_age_for_durations,
official_distribution_max_age_seconds, read_cas_reuse_reference_manifest_at,
read_download_manifest_at, release_cas_reuse_references, DownloadError,
OfficialCasReuseReferenceManifest, OfficialDistributionAttestation, OfficialDownloadManifest,
OfficialDownloadManifestEntry, OfficialLocalManifestAuditItem,
@@ -89,9 +90,9 @@ pub use official_download::{
OfficialResourceHashVerification, OfficialResourcePullItem, OfficialResourcePullProgress,
OfficialResourcePullProgressKind, OfficialResourcePullReport, OfficialResourcePullService,
OfficialResourcePullStatus, OfficialResourceReuseWarning, OfficialResourceVerification,
DEFAULT_OFFICIAL_ERROR_RETRY_SECONDS, DEFAULT_OFFICIAL_VERIFICATION_INTERVAL_SECONDS,
OFFICIAL_CAS_REUSE_REFERENCES_FILE, OFFICIAL_DISTRIBUTION_ATTESTATION_FILE,
OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS, OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION,
OFFICIAL_DISTRIBUTION_PUBLICATION_FILE,
OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION, OFFICIAL_DISTRIBUTION_PUBLICATION_FILE,
};
pub use official_game_main_config::OfficialGameMainConfigBootstrapService;
pub use official_launcher::{
+125 -18
View File
@@ -27,7 +27,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// 官方资源下载错误:携带统一错误码,便于 CLI/RPC 归类。
///
@@ -75,8 +75,10 @@ pub const OFFICIAL_DISTRIBUTION_PUBLICATION_FILE: &str = "official-distribution-
pub const OFFICIAL_DISTRIBUTION_ATTESTATION_FILE: &str = "official-distribution-attestation.json";
/// Persisted attestation schema version.
pub const OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION: u32 = 1;
/// Attestations older than this are no longer allowed to authorize current CDN.
pub const OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS: u64 = 900;
/// Default interval used by the Rust official watch verifier.
pub const DEFAULT_OFFICIAL_VERIFICATION_INTERVAL_SECONDS: u64 = 60 * 60;
/// Default retry interval used after an official watch failure.
pub const DEFAULT_OFFICIAL_ERROR_RETRY_SECONDS: u64 = 60;
/// 记录一个已发布官方 release 获取的 CAS 引用。
pub const OFFICIAL_CAS_REUSE_REFERENCES_FILE: &str = "official-cas-reuse-references.json";
const OFFICIAL_CAS_REUSE_REFERENCES_VERSION: u32 = 1;
@@ -87,6 +89,40 @@ const CAS_OWNER_SCOPE_FILE: &str = ".cas-owner-scope";
const CAS_OWNER_SCOPE_VERSION: u32 = 1;
static CAS_OWNERSHIP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
/// Returns the only freshness policy used for official distribution
/// attestations.
///
/// The window covers two complete verification intervals plus one retry
/// interval, so a healthy attestation cannot remain authoritative after the
/// verifier has missed an entire cycle and its retry.
pub const fn official_distribution_max_age_seconds(
verification_interval_seconds: u64,
error_retry_seconds: u64,
) -> u64 {
verification_interval_seconds
.saturating_mul(2)
.saturating_add(error_retry_seconds)
}
/// Converts watch durations to the seconds used by the persisted freshness
/// policy, rounding partial seconds up so a non-zero duration never becomes a
/// zero-second policy.
pub fn official_distribution_max_age_for_durations(
verification_interval: Duration,
error_retry: Duration,
) -> u64 {
fn ceil_seconds(duration: Duration) -> u64 {
duration
.as_secs()
.saturating_add(u64::from(duration.subsec_nanos() != 0))
}
official_distribution_max_age_seconds(
ceil_seconds(verification_interval),
ceil_seconds(error_retry),
)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CasOwnerScopeState {
version: u32,
@@ -664,10 +700,12 @@ pub(crate) fn read_official_distribution_attestation_at(
/// passed; this function itself never turns a partial audit into a healthy
/// result.
pub(crate) fn write_official_distribution_attestation_at(
release_root: &Path,
storage_root: &Path,
canonical_resource_root: &Path,
official_release_id: &str,
integrity_status: &str,
diagnostics: Vec<String>,
max_age_seconds: u64,
mut diagnostics: Vec<String>,
) -> Result<OfficialDistributionAttestation, String> {
if !matches!(
integrity_status,
@@ -677,23 +715,80 @@ pub(crate) fn write_official_distribution_attestation_at(
"不支持的官方 distribution attestation 状态:{integrity_status}"
));
}
ensure_safe_directory_path(release_root, "官方 distribution attestation 根目录")?;
let anchor = verify_official_distribution_publication_at(release_root, official_release_id)?
.ok_or_else(|| {
format!(
"官方 distribution attestation 缺少 publication anchor{}",
release_root.display()
)
})?;
let previous_generation = read_official_distribution_attestation_at(release_root)?
if max_age_seconds == 0 {
return Err("官方 distribution attestation freshness window 必须大于 0".to_string());
}
ensure_safe_directory_path(storage_root, "官方 distribution attestation 存储目录")?;
ensure_safe_directory_path(
canonical_resource_root,
"官方 distribution canonical 根目录",
)?;
ensure_path_within_root(&ownership_scope_root(storage_root), canonical_resource_root)?;
let previous = match read_official_distribution_attestation_at(storage_root) {
Ok(previous) => previous,
Err(error) if integrity_status == "invalid" => {
diagnostics.push(format!(
"上一代 distribution attestation 不可读取,重新记录 invalid{error}"
));
None
}
Err(error) => return Err(error),
};
let previous_generation = previous
.as_ref()
.map(|previous| previous.verification_generation)
.unwrap_or(0);
let anchor =
match verify_official_distribution_publication_at(storage_root, official_release_id) {
Ok(Some(anchor)) => anchor,
Ok(None) if integrity_status == "invalid" => {
let Some(previous) = previous.as_ref() else {
return Err(format!(
"官方 distribution attestation 缺少 publication anchor{}",
storage_root.display()
));
};
diagnostics
.push("publication anchor 缺失,沿用上一代 identity 记录 invalid".to_string());
OfficialDistributionPublicationAnchor {
version: OFFICIAL_DISTRIBUTION_PUBLICATION_VERSION,
official_release_id: official_release_id.to_string(),
mapping_identity: previous.mapping_identity.clone(),
manifest_identity: previous.manifest_identity.clone(),
entry_count: previous.entry_count,
}
}
Ok(None) => {
return Err(format!(
"官方 distribution attestation 缺少 publication anchor{}",
storage_root.display()
));
}
Err(error) if integrity_status == "invalid" => {
let Some(previous) = previous.as_ref() else {
return Err(format!(
"无法记录 invalid distribution attestation{error}"
));
};
diagnostics.push(format!(
"publication anchor 验证失败,沿用上一代 identity{error}"
));
OfficialDistributionPublicationAnchor {
version: OFFICIAL_DISTRIBUTION_PUBLICATION_VERSION,
official_release_id: official_release_id.to_string(),
mapping_identity: previous.mapping_identity.clone(),
manifest_identity: previous.manifest_identity.clone(),
entry_count: previous.entry_count,
}
}
Err(error) => return Err(error),
};
let verified_at = (integrity_status == "verified").then_some(unix_seconds_now());
let attestation = OfficialDistributionAttestation {
version: OFFICIAL_DISTRIBUTION_ATTESTATION_VERSION,
channel: "official".to_string(),
official_release_id: official_release_id.to_string(),
resource_root: release_root.to_path_buf(),
resource_root: canonical_resource_root.to_path_buf(),
publication_identity: official_distribution_publication_identity(&anchor),
mapping_identity: anchor.mapping_identity,
manifest_identity: anchor.manifest_identity,
@@ -704,11 +799,11 @@ pub(crate) fn write_official_distribution_attestation_at(
ready: integrity_status == "verified",
verification_generation: previous_generation.saturating_add(1),
verified_at,
max_age_seconds: OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
max_age_seconds,
diagnostics,
};
let path = release_root.join(OFFICIAL_DISTRIBUTION_ATTESTATION_FILE);
ensure_safe_file_target(release_root, &path, "官方 distribution attestation")?;
let path = storage_root.join(OFFICIAL_DISTRIBUTION_ATTESTATION_FILE);
ensure_safe_file_target(storage_root, &path, "官方 distribution attestation")?;
let bytes = serde_json::to_vec_pretty(&attestation)
.map_err(|error| format!("序列化官方 distribution attestation 失败:{error}"))?;
write_file_atomic(
@@ -4036,6 +4131,18 @@ exit 22
}
}
#[test]
fn official_distribution_freshness_policy_is_cadence_bound() {
assert_eq!(official_distribution_max_age_seconds(3600, 60), 7260);
assert_eq!(
official_distribution_max_age_for_durations(
Duration::from_millis(1500),
Duration::from_millis(1),
),
5
);
}
#[test]
fn official_distribution_mapping_identity_is_deterministic_and_sensitive() {
let entries = [
+353 -34
View File
@@ -14,9 +14,9 @@ use crate::official_changes::{
OfficialResourceChangeSummary,
};
use crate::official_download::{
write_official_distribution_attestation_at, write_official_distribution_publication_anchor_at,
OfficialDistributionAttestation, OFFICIAL_CAS_REUSE_REFERENCES_FILE,
OFFICIAL_DISTRIBUTION_PUBLICATION_FILE,
official_distribution_max_age_for_durations, write_official_distribution_attestation_at,
write_official_distribution_publication_anchor_at, OfficialDistributionAttestation,
OFFICIAL_CAS_REUSE_REFERENCES_FILE, OFFICIAL_DISTRIBUTION_PUBLICATION_FILE,
};
use crate::official_game_main_config::{
resolve_game_main_config_source, OfficialGameMainConfigSelectedSource,
@@ -1213,13 +1213,48 @@ impl OfficialPublishLayout {
}
/// Official update runner.
#[derive(Debug, Clone, Default)]
pub struct OfficialUpdateService;
#[derive(Debug, Clone)]
pub struct OfficialUpdateService {
attestation_max_age_seconds: u64,
}
impl Default for OfficialUpdateService {
fn default() -> Self {
Self::new()
}
}
impl OfficialUpdateService {
/// Creates an official update runner.
pub fn new() -> Self {
Self
Self {
attestation_max_age_seconds: official_distribution_max_age_for_durations(
std::time::Duration::from_secs(
crate::official_download::DEFAULT_OFFICIAL_VERIFICATION_INTERVAL_SECONDS,
),
std::time::Duration::from_secs(
crate::official_download::DEFAULT_OFFICIAL_ERROR_RETRY_SECONDS,
),
),
}
}
/// Creates an update runner using the daemon's actual watch cadence.
pub fn with_verification_cadence(
verification_interval: std::time::Duration,
error_retry: std::time::Duration,
) -> Self {
Self {
attestation_max_age_seconds: official_distribution_max_age_for_durations(
verification_interval,
error_retry,
),
}
}
/// Returns the freshness window persisted with each attestation.
pub fn attestation_max_age_seconds(&self) -> u64 {
self.attestation_max_age_seconds
}
/// Executes one official update run.
@@ -1607,11 +1642,24 @@ impl OfficialUpdateService {
fetcher.download_manifest_path().display()
),
));
Some(
fetcher
.audit_local_manifest(&pull_plan)
.map_err(anyhow::Error::msg)?,
)
match fetcher.audit_local_manifest(&pull_plan) {
Ok(audit) => Some(audit),
Err(error) => {
if !config.dry_run && has_current_pointer {
let release_id = version_id_from_path(&active_resource_root)
.unwrap_or_else(|| fallback_version_id(&current_update_snapshot));
let _ = write_official_distribution_attestation_at(
&active_resource_root,
&active_resource_root,
&release_id,
"invalid",
self.attestation_max_age_seconds,
vec![format!("本地 manifest 审计失败:{error}")],
);
}
return Err(anyhow::Error::msg(error));
}
}
} else if config.audit_local {
progress(OfficialUpdateProgress::new(
"audit",
@@ -1673,6 +1721,31 @@ impl OfficialUpdateService {
check_shutdown_requested(&mut should_cancel)?;
let active_release_id = version_id_from_path(&active_resource_root)
.unwrap_or_else(|| fallback_version_id(&current_update_snapshot));
let mut local_attestation_invalidated = false;
if !config.dry_run
&& has_current_pointer
&& local_audit.as_ref().is_some_and(|audit| !audit.is_clean())
{
let diagnostics = local_audit
.as_ref()
.map(|audit| {
vec![format!(
"本地 manifest 审计失败:{} 项需要修复",
audit.repair_needed_count()
)]
})
.unwrap_or_default();
write_official_distribution_attestation_at(
&active_resource_root,
&active_resource_root,
&active_release_id,
"invalid",
self.attestation_max_age_seconds,
diagnostics,
)
.map_err(anyhow::Error::msg)?;
local_attestation_invalidated = true;
}
let localized_info = localized_release_info_for(config, Some(active_release_id.as_str()));
let mut report = OfficialUpdateReport {
update_status: if should_download {
@@ -1786,27 +1859,31 @@ impl OfficialUpdateService {
&active_resource_root,
&snapshot_path,
)?;
if let Some(audit) = local_audit.as_ref() {
let integrity_status = if audit.is_clean() {
"verified"
} else {
"invalid"
};
let diagnostics = if audit.is_clean() {
Vec::new()
} else {
vec![format!(
"本地 manifest 审计失败:{} 项需要修复",
audit.repair_needed_count()
)]
};
write_official_distribution_attestation_at(
&active_resource_root,
&active_release_id,
integrity_status,
diagnostics,
)
.map_err(anyhow::Error::msg)?;
if !local_attestation_invalidated {
if let Some(audit) = local_audit.as_ref() {
let integrity_status = if audit.is_clean() {
"verified"
} else {
"invalid"
};
let diagnostics = if audit.is_clean() {
Vec::new()
} else {
vec![format!(
"本地 manifest 审计失败:{} 项需要修复",
audit.repair_needed_count()
)]
};
write_official_distribution_attestation_at(
&active_resource_root,
&active_resource_root,
&active_release_id,
integrity_status,
self.attestation_max_age_seconds,
diagnostics,
)
.map_err(anyhow::Error::msg)?;
}
}
let active_launcher_bootstrap_path =
active_resource_root.join(OFFICIAL_LAUNCHER_BOOTSTRAP_FILE);
@@ -2040,8 +2117,10 @@ impl OfficialUpdateService {
.map_err(anyhow::Error::msg)?;
write_official_distribution_attestation_at(
&publish_plan.staging_path,
&publish_plan.version_path,
&publish_plan.id,
"verified",
self.attestation_max_age_seconds,
Vec::new(),
)
.map_err(anyhow::Error::msg)?;
@@ -2218,6 +2297,7 @@ impl OfficialUpdateService {
/// resource artifacts.
pub fn verify_and_record_official_distribution_attestation(
config: &OfficialUpdateConfig,
max_age_seconds: u64,
) -> anyhow::Result<OfficialDistributionAttestation> {
let version_state = read_version_state(&config.version_state_path())?
.ok_or_else(|| anyhow::anyhow!("官方版本状态不存在,无法记录 distribution attestation"))?;
@@ -2237,11 +2317,27 @@ pub fn verify_and_record_official_distribution_attestation(
record.resource_root.display()
));
}
let verification =
let verification_result =
OfficialResourcePullService::with_curl_command(&resource_root, &config.curl_command)
.with_proxy_config(config.curl_proxy.clone())
.verify_local_download_manifest()
.verify_local_download_manifest();
let verification = match verification_result {
Ok(verification) => verification,
Err(error) => {
write_official_distribution_attestation_at(
&resource_root,
&resource_root,
&record.id,
"invalid",
max_age_seconds,
vec![format!("本地 manifest 验证失败:{error}")],
)
.map_err(anyhow::Error::msg)?;
return Err(anyhow::anyhow!(
"本地 manifest 验证失败,已立即使当前 distribution attestation 失效:{error}"
));
}
};
let diagnostics = verification
.items
.iter()
@@ -2254,9 +2350,11 @@ pub fn verify_and_record_official_distribution_attestation(
"invalid"
};
write_official_distribution_attestation_at(
&resource_root,
&resource_root,
&record.id,
integrity_status,
max_age_seconds,
diagnostics,
)
.map_err(anyhow::Error::msg)
@@ -4977,6 +5075,227 @@ mod tests {
assert_eq!(read_version_state(&path).unwrap(), Some(state));
}
#[cfg(unix)]
#[test]
fn publication_attestation_keeps_canonical_root_across_staging_rename() {
use crate::official_download::{
official_distribution_max_age_seconds, write_official_distribution_attestation_at,
write_official_distribution_publication_anchor_at, OfficialDownloadManifest,
OfficialDownloadManifestEntry,
};
let temp = tempfile::TempDir::new().unwrap();
let root = temp.path().join("official");
let layout = OfficialPublishLayout::new(&root);
let staging = layout.staging_dir.join("release-a");
let version = layout.versions_dir.join("release-a");
fs::create_dir_all(&staging).unwrap();
fs::create_dir_all(&layout.versions_dir).unwrap();
let payload = b"official";
fs::write(staging.join("data.bin"), payload).unwrap();
let url = "https://example.invalid/data.bin".to_string();
let mut manifest = OfficialDownloadManifest {
entries: [(
url.clone(),
OfficialDownloadManifestEntry {
url: url.clone(),
destination: "data.bin".to_string(),
bytes: payload.len() as u64,
blake3: blake3::hash(payload).to_hex().to_string(),
},
)]
.into_iter()
.collect(),
..OfficialDownloadManifest::default()
};
manifest.destination_index = [("data.bin".to_string(), url)].into_iter().collect();
manifest.distribution_mapping_identity =
Some(crate::official_download::official_distribution_mapping_identity(&manifest));
fs::write(
staging.join(OFFICIAL_DOWNLOAD_MANIFEST_FILE),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
write_official_distribution_publication_anchor_at(&staging, "release-a").unwrap();
let max_age = official_distribution_max_age_seconds(3600, 60);
let before_publish = write_official_distribution_attestation_at(
&staging,
&version,
"release-a",
"verified",
max_age,
Vec::new(),
)
.unwrap();
assert_eq!(before_publish.resource_root, version);
assert_eq!(before_publish.verification_generation, 1);
let plan = OfficialPublishPlan {
id: "release-a".to_string(),
staging_path: staging,
version_path: version.clone(),
reuse_existing_staging: false,
};
let published = layout.publish(&plan).unwrap();
assert_eq!(published, version);
let after_rename =
crate::official_download::read_official_distribution_attestation_at(&version)
.unwrap()
.unwrap();
assert_eq!(after_rename.resource_root, version);
assert_eq!(
after_rename.verification_generation,
before_publish.verification_generation
);
let snapshot_path = version.join(OFFICIAL_SYNC_SNAPSHOT_FILE);
let snapshot = OfficialUpdateSnapshot::new(fixture_base_snapshot(), Vec::new(), None);
write_snapshot(&snapshot_path, &snapshot).unwrap();
let state = OfficialVersionState {
current_completed_version: Some(OfficialVersionRecord {
id: "release-a".to_string(),
app_version: snapshot.app_version.clone(),
bundle_version: snapshot.bundle_version.clone(),
addressables_root: snapshot.addressables_root.clone(),
resource_root: version.clone(),
snapshot_path,
staging_path: None,
version_path: Some(version.clone()),
started_unix_seconds: Some(1),
completed_unix_seconds: Some(2),
}),
..OfficialVersionState::default()
};
write_version_state(&root.join(OFFICIAL_VERSION_STATE_FILE), &state).unwrap();
assert_eq!(
fs::read_link(root.join(OFFICIAL_CURRENT_LINK)).unwrap(),
Path::new(OFFICIAL_VERSIONS_DIR).join("release-a")
);
assert_eq!(
state
.current_completed_version
.as_ref()
.unwrap()
.resource_root,
after_rename.resource_root
);
let report = crate::release_ops::build_official_distribution_attestation(&root).unwrap();
assert!(report.available);
assert!(report.ready);
assert_eq!(report.release_id, "release-a");
assert_eq!(report.resource_root, version.display().to_string());
assert_eq!(report.verification_generation, 1);
// Keep this assertion explicit: current is the only pointer used by
// the read path, and the attestation never stores the staging path.
assert!(!after_rename.resource_root.starts_with(layout.staging_dir));
}
#[test]
fn failed_local_verification_invalidates_previous_ready_generation() {
use crate::official_download::{
official_distribution_max_age_seconds, write_official_distribution_attestation_at,
write_official_distribution_publication_anchor_at, OfficialDownloadManifest,
OfficialDownloadManifestEntry,
};
use std::os::unix::fs::symlink;
let temp = tempfile::TempDir::new().unwrap();
let root = temp.path().join("official");
let version = root.join(OFFICIAL_VERSIONS_DIR).join("release-a");
fs::create_dir_all(&version).unwrap();
let payload = b"official";
let url = "https://example.invalid/data.bin".to_string();
let mut manifest = OfficialDownloadManifest {
entries: [(
url.clone(),
OfficialDownloadManifestEntry {
url,
destination: "data.bin".to_string(),
bytes: payload.len() as u64,
blake3: blake3::hash(payload).to_hex().to_string(),
},
)]
.into_iter()
.collect(),
..OfficialDownloadManifest::default()
};
manifest.destination_index = [(
"data.bin".to_string(),
"https://example.invalid/data.bin".to_string(),
)]
.into_iter()
.collect();
manifest.distribution_mapping_identity =
Some(crate::official_download::official_distribution_mapping_identity(&manifest));
fs::write(
version.join(OFFICIAL_DOWNLOAD_MANIFEST_FILE),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
fs::write(version.join("data.bin"), payload).unwrap();
write_official_distribution_publication_anchor_at(&version, "release-a").unwrap();
let max_age = official_distribution_max_age_seconds(3600, 60);
let initial = write_official_distribution_attestation_at(
&version,
&version,
"release-a",
"verified",
max_age,
Vec::new(),
)
.unwrap();
symlink(
Path::new(OFFICIAL_VERSIONS_DIR).join("release-a"),
root.join(OFFICIAL_CURRENT_LINK),
)
.unwrap();
let snapshot_path = version.join(OFFICIAL_SYNC_SNAPSHOT_FILE);
write_snapshot(
&snapshot_path,
&OfficialUpdateSnapshot::new(fixture_base_snapshot(), Vec::new(), None),
)
.unwrap();
write_version_state(
&root.join(OFFICIAL_VERSION_STATE_FILE),
&OfficialVersionState {
current_completed_version: Some(OfficialVersionRecord {
id: "release-a".to_string(),
app_version: "app".to_string(),
bundle_version: None,
addressables_root: "root".to_string(),
resource_root: version.clone(),
snapshot_path,
staging_path: None,
version_path: Some(version.clone()),
started_unix_seconds: Some(1),
completed_unix_seconds: Some(2),
}),
..OfficialVersionState::default()
},
)
.unwrap();
fs::write(version.join(OFFICIAL_DOWNLOAD_MANIFEST_FILE), b"{malformed").unwrap();
let config = OfficialUpdateConfig {
output_root: root,
..OfficialUpdateConfig::default()
};
assert!(verify_and_record_official_distribution_attestation(&config, max_age).is_err());
let invalid = crate::official_download::read_official_distribution_attestation_at(&version)
.unwrap()
.unwrap();
assert_eq!(
invalid.verification_generation,
initial.verification_generation + 1
);
assert_eq!(invalid.integrity_status, "invalid");
assert!(!invalid.ready);
assert!(invalid.verified_at.is_none());
}
#[test]
fn version_state_tracks_in_progress_success_and_failure() {
let temp = tempfile::TempDir::new().unwrap();
+68 -8
View File
@@ -15,7 +15,6 @@ use crate::official_download::{
read_download_manifest_at, read_official_distribution_attestation_at,
release_cas_reuse_references, verify_official_distribution_publication_at,
OfficialDownloadManifest, OfficialDownloadManifestEntry,
OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
};
use crate::official_update::{read_version_state, OfficialVersionRecord, OfficialVersionState};
use crate::path_security::{
@@ -377,7 +376,7 @@ pub fn build_official_distribution_attestation(
ready: false,
verification_generation: 0,
verified_at: None,
max_age_seconds: OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
max_age_seconds: 0,
diagnostics: vec!["没有当前已发布官方 release".to_string()],
});
};
@@ -397,15 +396,17 @@ pub fn build_official_distribution_attestation(
ready: false,
verification_generation: 0,
verified_at: None,
max_age_seconds: OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS,
max_age_seconds: 0,
diagnostics: Vec::new(),
};
let mut structural_invalid = false;
let current_id = read_managed_current_id(
&official_root.join(OFFICIAL_CURRENT_LINK),
OFFICIAL_VERSIONS_DIR,
);
if current_id.as_deref() != Some(record.id.as_str()) {
structural_invalid = true;
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
@@ -415,6 +416,7 @@ pub fn build_official_distribution_attestation(
));
}
if let Err(error) = ensure_path_within_root(official_root, &record.resource_root) {
structural_invalid = true;
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
@@ -423,6 +425,7 @@ pub fn build_official_distribution_attestation(
if let Err(error) =
ensure_safe_directory_path(&record.resource_root, "当前官方 distribution 根目录")
{
structural_invalid = true;
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
@@ -478,13 +481,17 @@ pub fn build_official_distribution_attestation(
report.entry_count = attestation.entry_count;
report.verification_generation = attestation.verification_generation;
report.verified_at = attestation.verified_at;
report.max_age_seconds = if attestation.max_age_seconds == 0 {
OFFICIAL_DISTRIBUTION_ATTESTATION_MAX_AGE_SECONDS
} else {
attestation.max_age_seconds
};
report.max_age_seconds = attestation.max_age_seconds;
report.diagnostics.extend(attestation.diagnostics);
if structural_invalid {
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.ready = false;
return Ok(report);
}
let identity_matches = anchor.as_ref().is_some_and(|anchor| {
attestation.channel == "official"
&& attestation.official_release_id == record.id
@@ -506,6 +513,28 @@ pub fn build_official_distribution_attestation(
return Ok(report);
}
if report.max_age_seconds == 0 {
report.integrity_status = "stale".to_string();
report.status = "stale".to_string();
report.status_code = "distribution.attestation_stale".to_string();
report.ready = false;
report
.diagnostics
.push("attestation freshness window 缺失或为 0".to_string());
return Ok(report);
}
if report.verification_generation == 0 {
report.integrity_status = "invalid".to_string();
report.status = "invalid".to_string();
report.status_code = "distribution.attestation_invalid".to_string();
report.ready = false;
report
.diagnostics
.push("attestation verification generation 缺失或为 0".to_string());
return Ok(report);
}
if report.integrity_status == "verified" {
let fresh = report.verified_at.is_some_and(|verified_at| {
let now = SystemTime::now()
@@ -2049,9 +2078,11 @@ mod tests {
.unwrap();
crate::official_download::write_official_distribution_attestation_at(
&version,
&version,
"official-v1",
"verified",
crate::official_download::official_distribution_max_age_seconds(3600, 60),
Vec::new(),
)
.unwrap();
@@ -2061,6 +2092,22 @@ mod tests {
assert_eq!(healthy.release_id, "official-v1");
assert!(healthy.verification_generation > 0);
fs::remove_file(official_root.join(OFFICIAL_CURRENT_LINK)).unwrap();
symlink(
Path::new(OFFICIAL_VERSIONS_DIR).join("wrong-release"),
official_root.join(OFFICIAL_CURRENT_LINK),
)
.unwrap();
let pointer_mismatch = build_official_distribution_attestation(&official_root).unwrap();
assert!(!pointer_mismatch.ready);
assert_eq!(pointer_mismatch.integrity_status, "invalid");
fs::remove_file(official_root.join(OFFICIAL_CURRENT_LINK)).unwrap();
symlink(
Path::new(OFFICIAL_VERSIONS_DIR).join("official-v1"),
official_root.join(OFFICIAL_CURRENT_LINK),
)
.unwrap();
// The lightweight read path only rechecks the publication anchor and
// manifest bytes; artifact damage is recorded by explicit verify.
fs::write(version.join("other.bin"), b"tampered").unwrap();
@@ -2068,9 +2115,11 @@ mod tests {
assert!(still_authorized.ready);
crate::official_download::write_official_distribution_attestation_at(
&version,
&version,
"official-v1",
"invalid",
crate::official_download::official_distribution_max_age_seconds(3600, 60),
vec!["other.bin: size_or_hash_mismatch".to_string()],
)
.unwrap();
@@ -2096,6 +2145,17 @@ mod tests {
let stale = build_official_distribution_attestation(&official_root).unwrap();
assert!(!stale.ready);
assert_eq!(stale.integrity_status, "stale");
fresh.verified_at = Some(1);
fresh.max_age_seconds = 0;
fs::write(
version.join(crate::OFFICIAL_DISTRIBUTION_ATTESTATION_FILE),
serde_json::to_vec(&fresh).unwrap(),
)
.unwrap();
let zero_window = build_official_distribution_attestation(&official_root).unwrap();
assert!(!zero_window.ready);
assert_eq!(zero_window.integrity_status, "stale");
}
#[test]