keyhog 0.5.73

GPU-accelerated secret scanner for code, Git history, cloud, containers, browser assets, and live credential verification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
use keyhog::testing::{CliTestApi as _, API};

/// Legacy updater compatibility must retain every historical signed-bundle filename.
#[test]
fn legacy_asset_name_matches_historical_release_convention() {
    assert_eq!(
        API.asset_name("linux", "x86_64").as_deref(),
        Some("keyhog-linux-x86_64")
    );
    assert_eq!(
        API.asset_name("macos", "aarch64").as_deref(),
        Some("keyhog-macos-aarch64")
    );
    assert_eq!(
        API.asset_name("macos", "x86_64").as_deref(),
        Some("keyhog-macos-x86_64")
    );
    // Historical Windows bundles used this exact filename. `update` and
    // `repair` retain it so operators can reinstall those older releases.
    assert_eq!(
        API.asset_name("windows", "x86_64").as_deref(),
        Some("keyhog-windows-x86_64.exe")
    );
    // Unsupported (os, arch) pairs still yield None.
    assert_eq!(API.asset_name("linux", "aarch64"), None);
    assert_eq!(API.asset_name("windows", "aarch64"), None);
    assert_eq!(API.asset_name("linux", "riscv64"), None);
}

/// A build newer than the newest published asset must never be reported as
/// "already on the latest release". The binary-asset channel stopped
/// publishing while crates.io kept releasing, so collapsing "no newer asset"
/// into "you are current" told a stale install it was up to date and gave
/// `update --check` a clean answer the channel could not support.
#[test]
fn release_channel_state_separates_a_stale_channel_from_a_current_build() {
    assert_eq!(
        API.release_channel_state("0.5.47", "v0.5.48"),
        "update-available"
    );
    assert_eq!(
        API.release_channel_state("0.5.47", "v0.5.47"),
        "on-newest-asset"
    );
    // The live state: the running build is many versions past the newest asset.
    assert_eq!(
        API.release_channel_state("0.5.68", "v0.5.47"),
        "channel-behind"
    );
    // Prerelease precedence is not discarded.
    assert_eq!(
        API.release_channel_state("0.5.47", "v0.5.47-rc.1"),
        "channel-behind"
    );
    // Unparseable input authorizes no install and claims no staleness.
    assert_eq!(
        API.release_channel_state("0.5.68", "not-a-tag"),
        "on-newest-asset"
    );
}

#[cfg(target_os = "linux")]
#[test]
fn release_selection_uses_the_single_linux_asset() {
    let assets = ["keyhog-linux-x86_64"];
    assert_eq!(
        API.select_release_asset_name("v9.9.9", &assets)
            .expect("Linux platform asset present"),
        "keyhog-linux-x86_64"
    );
}

#[test]
fn release_selector_has_no_alternate_asset_fallback() {
    let canonical = API
        .asset_name(std::env::consts::OS, std::env::consts::ARCH)
        .expect("test host has a supported release asset");
    let alternate = format!("{canonical}-alternate");
    let error = API
        .select_release_asset_name("v9.9.9", &[&alternate])
        .expect_err("an alternate filename must not satisfy exact platform selection");
    assert!(
        format!("{error:#}").contains(&canonical),
        "selection error must name the exact required asset: {error:#}"
    );
}

#[test]
fn release_selector_rejects_duplicate_asset_names() {
    let canonical = API
        .asset_name(std::env::consts::OS, std::env::consts::ARCH)
        .expect("test host has a supported release asset");
    let error = API
        .select_release_asset_name("v9.9.9", &[&canonical, &canonical])
        .expect_err("duplicate release asset names are ambiguous");
    assert!(format!("{error:#}").contains("duplicate assets"));
}

/// Current releases must direct installation and rollback through crates.io, not absent binary assets.
#[test]
fn installer_words_match_crates_only_release_contract() {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let release_yml =
        std::fs::read_to_string(root.join(".github/workflows/release.yml")).expect("release.yml");
    let readme = std::fs::read_to_string(root.join("README.md")).expect("README.md");
    let install_doc = std::fs::read_to_string(root.join("docs/src/install.md"))
        .expect("docs/src/install.md readable");

    assert!(
        release_yml.contains("bash scripts/publish.sh")
            && release_yml.contains("CARGO_REGISTRY_TOKEN")
            && !release_yml.contains("asset:")
            && !release_yml.contains("upload-release-asset"),
        "automatic releases must publish crates.io packages without claiming binary assets"
    );
    assert!(
        readme.contains("cargo install --locked keyhog")
            && install_doc.contains("cargo install --locked --force keyhog")
            && install_doc
                .contains("cargo install --locked --force --version '=MAJOR.MINOR.PATCH' keyhog")
            && install_doc.contains("does\nnot publish binary release assets or installer bundles")
            && !readme.contains("macOS release assets")
            && !readme.contains("Windows assets")
            && !install_doc.contains("macOS assets")
            && !install_doc.contains("Windows assets"),
        "README and install guide must describe crates.io install, update, and rollback truthfully"
    );
}

#[test]
fn semver_parsing_handles_v_prefix_and_suffix() {
    assert_eq!(API.parse_semver("v0.5.36"), Some((0, 5, 36)));
    assert_eq!(API.parse_semver("0.5.36"), Some((0, 5, 36)));
    assert_eq!(API.parse_semver("v1.2.3-rc1"), Some((1, 2, 3)));
    assert_eq!(API.parse_semver("garbage"), None);
    assert_eq!(API.parse_semver("v1.2"), None);
}

#[test]
fn is_newer_compares_correctly() {
    assert!(API.is_newer("0.5.35", "v0.5.36"));
    assert!(API.is_newer("0.5.35", "0.6.0"));
    assert!(API.is_newer("0.5.35", "1.0.0"));
    assert!(!API.is_newer("0.5.36", "v0.5.36"));
    assert!(!API.is_newer("0.5.36", "v0.5.35"));
    assert!(!API.is_newer("0.5.35", "garbage"));
    assert!(API.is_newer("1.0.0-rc.1", "1.0.0"));
    assert!(API.is_newer("1.0.0-rc.1", "1.0.0-rc.2"));
    assert!(!API.is_newer("1.0.0", "1.0.0-rc.2"));
    assert!(!API.is_newer("1.0.0+build.1", "1.0.0+build.2"));
}

#[test]
fn rejects_non_executable_download() {
    assert!(!API.looks_like_native_executable(b"<!DOCTYPE html><html>Not Found"));
    assert!(!API.looks_like_native_executable(b""));
    #[cfg(target_os = "linux")]
    assert!(API.looks_like_native_executable(&[0x7F, b'E', b'L', b'F', 2, 1, 1, 0]));

    assert!(API.looks_like_native_executable_for_os(&[0x7F, b'E', b'L', b'F', 2, 1, 1, 0], "linux"));
    assert!(API.looks_like_native_executable_for_os(&[0xFE, 0xED, 0xFA, 0xCF, 0, 0, 0, 0], "macos"));
    assert!(API.looks_like_native_executable_for_os(&[b'M', b'Z', 0x90, 0x00], "windows"));
    assert!(!API.looks_like_native_executable_for_os(b"<!DOCTYPE html><html>Not Found", "windows"));
    assert!(!API.looks_like_native_executable_for_os(b"<!DOCTYPE html><html>Not Found", "freebsd"));
}

#[test]
fn self_test_detects_planted_secret() {
    // The doctor/repair self-test must actually fire end-to-end.
    assert!(API.scan_engine_self_test().expect("self-test runs"));
}

// A real minisign signature of FIXTURE_DATA, produced by the keyhog release
// secret key whose public half is embedded as installer::RELEASE_PUBLIC_KEY.
// This proves the embedded key verifies a genuine release signature and that
// any tampering (wrong data, mangled signature) is rejected.
const FIXTURE_DATA: &[u8] = b"keyhog-signature-test-v1\n";
const FIXTURE_SIG: &str = "untrusted comment: signature from rsign secret key\n\
RUTPnJ/p6xVJ3REkJ9dhxwKQpEisq7Y2A4uIZlUzPRM0zDjWidV3sIXjHB8d558++9M0KpCpz6T8efYlVFl/RZhrKIznrUZSGww=\n\
trusted comment: timestamp:1780025193\tfile:/tmp/claude-1000/tmp.JTQWgRt5FO/fixture.bin\tprehashed\n\
L/wvGiwIhpaBlkEUaQ364Q8ph9ksqIxJyIMy1RQbs/QS4+q8biUaJGt+0weV4E0IV/pPHywDFtZhvUD03un2CA==\n";

#[test]
fn release_signature_verifies_against_embedded_key() {
    API.verify_release_signature(FIXTURE_DATA, FIXTURE_SIG)
        .expect("a genuine signature must verify against the embedded public key");
}

#[test]
fn release_signature_rejects_tampered_payload() {
    // Same signature, different bytes: the update must be refused.
    assert!(
        API.verify_release_signature(b"tampered binary contents", FIXTURE_SIG)
            .is_err(),
        "a signature must not verify against payload it didn't sign"
    );
}

#[test]
fn release_signature_rejects_malformed_signature() {
    assert!(API
        .verify_release_signature(FIXTURE_DATA, "not a minisig file")
        .is_err());
    assert!(API.verify_release_signature(FIXTURE_DATA, "").is_err());
}

#[test]
fn release_checksum_binds_digest_and_asset_name() {
    let digest = "79792c6a6ce7cccb7d14cadf57006754b70a3e90a944cdaaf111ae97f03fbee8";
    API.verify_release_checksum(
        FIXTURE_DATA,
        "keyhog-linux-x86_64",
        format!("{digest}  keyhog-linux-x86_64\n").as_bytes(),
    )
    .expect("matching release checksum");

    assert!(API
        .verify_release_checksum(
            b"tampered",
            "keyhog-linux-x86_64",
            format!("{digest}  keyhog-linux-x86_64\n").as_bytes(),
        )
        .is_err());
    assert!(API
        .verify_release_checksum(
            FIXTURE_DATA,
            "keyhog-linux-x86_64",
            format!("{digest}  another-asset\n").as_bytes(),
        )
        .is_err());
    assert!(API
        .verify_release_checksum(FIXTURE_DATA, "keyhog-linux-x86_64", b"not-a-digest\n")
        .is_err());
}

fn gpu_literal_sidecar(version: &str, name: &str, artifact: &[u8]) -> Vec<u8> {
    use flate2::{write::GzEncoder, Compression};
    use std::io::Cursor;

    let manifest = serde_json::json!({
        "format_version": 1,
        "keyhog_version": version,
        "artifacts": [{"file_name": name, "byte_len": artifact.len()}],
    })
    .to_string()
    .into_bytes();
    let encoder = GzEncoder::new(Vec::new(), Compression::default());
    let mut archive = tar::Builder::new(encoder);
    for (path, bytes) in [
        ("keyhog.gpu-literals/manifest.json", manifest.as_slice()),
        ("keyhog.gpu-literals/matcher.bin", artifact),
    ] {
        let mut header = tar::Header::new_gnu();
        header.set_size(bytes.len() as u64);
        header.set_mode(0o644);
        header.set_cksum();
        archive
            .append_data(&mut header, path, Cursor::new(bytes))
            .expect("append sidecar entry");
    }
    archive
        .into_inner()
        .expect("finish tar")
        .finish()
        .expect("finish gzip")
}

#[test]
fn gpu_literal_sidecar_binds_manifest_version_and_bytes() {
    let archive = gpu_literal_sidecar("0.5.41", "matcher.bin", b"matcher-v1");
    let files = API
        .parse_gpu_literal_sidecar(&archive, "v0.5.41")
        .expect("valid signed-sidecar payload shape");
    assert_eq!(
        files,
        vec![("matcher.bin".to_string(), b"matcher-v1".to_vec())]
    );
    assert!(API.parse_gpu_literal_sidecar(&archive, "v0.5.42").is_err());
}

#[test]
fn gpu_literal_cache_transaction_commits_or_restores_exact_bytes() {
    let dir = tempfile::tempdir().expect("cache tempdir");
    let existing = dir.path().join("matcher.bin");
    std::fs::write(&existing, b"old").expect("seed old artifact");

    API.install_gpu_literal_files_in_dir(
        dir.path(),
        &[("matcher.bin", b"new"), ("added.bin", b"added")],
        false,
    )
    .expect("stage transaction then roll back");
    assert_eq!(std::fs::read(&existing).unwrap(), b"old");
    assert!(!dir.path().join("added.bin").exists());
    assert!(
        !dir.path().join(".installed_manifest.json").exists(),
        "rollback must not leave an identity manifest for reverted artifacts"
    );
    assert!(dir.path().join(".keyhog-maintenance.lock").exists());

    API.install_gpu_literal_files_in_dir(
        dir.path(),
        &[("matcher.bin", b"new"), ("added.bin", b"added")],
        true,
    )
    .expect("commit cache transaction");
    assert_eq!(std::fs::read(&existing).unwrap(), b"new");
    assert_eq!(
        std::fs::read(dir.path().join("added.bin")).unwrap(),
        b"added"
    );
    assert!(dir.path().join(".keyhog-maintenance.lock").exists());
    let manifest_path = dir.path().join(".installed_manifest.json");
    let committed_manifest = std::fs::read(&manifest_path).expect("read committed manifest");
    let manifest: serde_json::Value =
        serde_json::from_slice(&committed_manifest).expect("parse committed manifest");
    assert_eq!(manifest["version"], 1);
    assert_eq!(
        manifest["artifacts"]
            .as_array()
            .expect("manifest artifacts")
            .len(),
        2
    );

    API.install_gpu_literal_files_in_dir(
        dir.path(),
        &[("matcher.bin", b"replacement"), ("added.bin", b"other")],
        false,
    )
    .expect("stage replacement then roll back");
    assert_eq!(
        std::fs::read(&manifest_path).expect("read restored manifest"),
        committed_manifest,
        "artifact rollback must restore the matching identity manifest"
    );
}

// ── Moved from src/installer.rs (#[cfg(test)] mod rename_away_tests) per the
//    no_inline_tests_in_src gate. Cross-platform rename-away self-replace that
//    backs `keyhog update`/`repair` on Windows; same std::fs::rename semantics
//    on every OS, so the Linux host exercises the exact Windows code path.

#[test]
fn replace_success_installs_new_and_returns_stash() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    std::fs::write(&exe, b"OLD-WORKING-BINARY").unwrap();

    let stash = API
        .replace_running_binary(&exe, b"NEW-GOOD-BINARY", |_| true)
        .expect("replace should succeed when verify passes");

    assert_eq!(std::fs::read(&exe).unwrap(), b"NEW-GOOD-BINARY");
    let stash = stash.expect("a prior binary existed, so a stash is returned");
    // The caller reaps the stash; until then it holds the old bytes.
    assert_eq!(std::fs::read(&stash).unwrap(), b"OLD-WORKING-BINARY");
    API.reap_stale_binaries(&exe);
    assert!(!stash.exists(), "reap must remove the stash");
}

#[test]
fn replace_failure_rolls_back_byte_for_byte() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    // Arbitrary bytes incl. NULs/high bytes: rollback must be exact.
    let original: Vec<u8> = (0u8..=255).cycle().take(4096).collect();
    std::fs::write(&exe, &original).unwrap();

    let err = API
        .replace_running_binary(&exe, b"NEW-BROKEN-BINARY", |_| false)
        .expect_err("replace must fail when verify rejects the new binary");
    assert!(format!("{err}").contains("rolled back"));
    assert_eq!(
        std::fs::read(&exe).unwrap(),
        original,
        "rollback must restore the original binary byte-for-byte"
    );
    // No stash left orphaned beside the exe after a rollback.
    API.reap_stale_binaries(&exe);
    let leftovers: Vec<_> = std::fs::read_dir(dir.path())
        .unwrap()
        .flatten()
        .filter(|e| e.file_name().to_string_lossy().contains("keyhog-old"))
        .collect();
    assert!(leftovers.is_empty(), "rollback must not leave a stash");
}

#[test]
fn fresh_install_failure_removes_broken_binary() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    // No prior binary: a failed verify must not leave a broken executable.
    let err = API
        .replace_running_binary(&exe, b"BROKEN", |_| false)
        .expect_err("fresh install must fail when verify rejects it");
    assert!(format!("{err}").contains("no prior binary"));
    assert!(!exe.exists(), "broken fresh install must be removed");
}

#[test]
fn reap_only_touches_this_binarys_stashes() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    std::fs::write(&exe, b"bin").unwrap();
    let mine = dir.path().join(".keyhog.keyhog-old-4294967295");
    let other = dir.path().join("unrelated.txt");
    std::fs::write(&mine, b"old").unwrap();
    std::fs::write(&other, b"keep").unwrap();

    API.reap_stale_binaries(&exe);
    assert!(!mine.exists(), "matching stash must be reaped");
    assert!(other.exists(), "unrelated files must be left alone");
    assert!(exe.exists(), "the live binary must never be reaped");
}

#[test]
#[cfg(unix)]
fn reap_stale_binaries_preserves_live_peer_pid_artifacts() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    std::fs::write(&exe, b"bin").unwrap();

    let mut child = std::process::Command::new("sh")
        .arg("-c")
        .arg("sleep 30")
        .spawn()
        .expect("spawn live peer process");
    let live_pid = child.id();
    let live_stash = dir.path().join(format!(".keyhog.keyhog-old-{live_pid}"));
    let live_backup = dir.path().join(format!(".keyhog.keyhog-bak-{live_pid}"));
    let live_tmp = dir.path().join(format!(".keyhog-update-{live_pid}.tmp"));
    for path in [&live_stash, &live_backup, &live_tmp] {
        std::fs::write(path, b"in-flight").unwrap();
    }

    API.reap_stale_binaries(&exe);

    assert!(live_stash.exists(), "live process stash must not be reaped");
    assert!(
        live_backup.exists(),
        "live process rollback backup must not be reaped"
    );
    assert!(
        live_tmp.exists(),
        "live process staging tmp must not be reaped"
    );

    child.kill().expect("stop live peer process");
    let _ = child.wait();
}

#[test]
fn reap_stale_binaries_requires_parseable_pid_suffix() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    std::fs::write(&exe, b"bin").unwrap();

    let malformed = [
        dir.path().join(".keyhog.keyhog-old-"),
        dir.path().join(".keyhog.keyhog-bak-not-a-pid"),
        dir.path().join(".keyhog-update-123.tmp.extra"),
    ];
    for path in &malformed {
        std::fs::write(path, b"keep").unwrap();
    }

    API.reap_stale_binaries(&exe);

    for path in &malformed {
        assert!(
            path.exists(),
            "malformed installer artifact name must not be reaped: {}",
            path.display()
        );
    }
}

#[test]
fn reap_stale_binaries_reaps_digit_only_overflow_pid_artifacts() {
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("keyhog");
    std::fs::write(&exe, b"bin").unwrap();

    let overflow_pid = "42949672950000000000000000000000000000000000000000";
    let artifacts = [
        dir.path()
            .join(format!(".keyhog.keyhog-old-{overflow_pid}")),
        dir.path()
            .join(format!(".keyhog.keyhog-bak-{overflow_pid}")),
        dir.path()
            .join(format!(".keyhog-update-{overflow_pid}.tmp")),
    ];
    for path in &artifacts {
        std::fs::write(path, b"stale").unwrap();
    }

    API.reap_stale_binaries(&exe);

    for path in &artifacts {
        assert!(
            !path.exists(),
            "numeric overflow PID installer artifact must be treated as stale: {}",
            path.display()
        );
    }
}

#[test]
fn install_with_rollback_bool_wrapper_has_one_owner() {
    let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/installer.rs"))
        .expect("installer source readable");
    let wrapper = "pub(crate) fn install_with_rollback<F>";
    let wrapper_count = src.matches(wrapper).count();
    assert_eq!(
        wrapper_count, 1,
        "install_with_rollback bool compatibility wrapper must have one cfg-neutral owner"
    );
    assert!(
        src.contains("install_with_rollback_checked(exe, bytes, bool_verify_as_result(verify))"),
        "install_with_rollback must delegate through the shared bool-to-Result verifier adapter"
    );
    assert!(
        src.matches("fn bool_verify_as_result<F>").count() == 1
            && src.matches("post-install verifier returned false").count() == 1,
        "boolean verifier compatibility text must live in one adapter, not per-platform wrappers"
    );
}

// Supply-chain: a missing `.minisig` must FAIL CLOSED. A forged 404 on the
// signature URL (active MITM / compromised CDN serving a tampered binary)
// otherwise bypassed the entire minisign gate. Linux-gated because the served
// asset must pass `looks_like_native_executable` (ELF magic) to reach the
// signature-fetch branch; the CI test/integration jobs run on linux.
#[cfg(target_os = "linux")]
#[tokio::test]
async fn unsigned_release_download_fails_closed() {
    use httpmock::prelude::*;
    let server = MockServer::start();
    let mut elf = vec![0x7F, b'E', b'L', b'F'];
    elf.extend_from_slice(&[0u8; 64]);
    let asset_path = "/download/keyhog-linux-x86_64";
    let body = elf.clone();
    server.mock(|when, then| {
        when.method(GET).path(asset_path);
        then.status(200).body(body);
    });
    server.mock(|when, then| {
        when.method(GET).path(format!("{asset_path}.minisig"));
        then.status(404).body("Not Found");
    });
    let res = API
        .download_verified_asset(
            &API.http_client().unwrap(),
            "keyhog-linux-x86_64",
            format!("{}{}", server.base_url(), asset_path),
        )
        .await;
    assert!(
        res.is_err(),
        "a missing .minisig must fail closed (refuse), not install on HTTPS-only trust"
    );
    let msg = format!("{:#}", res.unwrap_err());
    assert!(
        msg.contains(".minisig") || msg.to_lowercase().contains("signature"),
        "error must name the missing signature as the reason: {msg}"
    );
}