Files
reverse-engineering-anything/src/process/DetachedPgpSignature.test.ts
T
70cd4cbfb8 fix(hopper)!: verify current Linux releases with the publisher key
Replace fixed Linux Hopper release pins with current vendor feed metadata,
complete detached-PGP package verification under the stable publisher primary
key, and authenticated package evidence for offline admission. Share parsed
publisher facts across approved setup, doctor and the pre-execution guard.
Preserve existing ownership, cancellation, private-display and cleanup rules.

Fixes #3577

Normalize empty XDG settings, preserve backups during approved cache repair,
and retain publisher cleanup through cancellation and shutdown. Exercise cold
offline verification without reusing process-local proof state. Serialize
approved cache publication across processes, restore old evidence on failure,
and share the existing lease owner from process without changing its identities.

Validation: 438 focused tests (six platform-related skips), full static/documentation checks, real Ubuntu
24.04 official Hopper 6.6.0 setup/CLI/MCP, offline ordinary-user admission,
launcher mutation rejection, and cold/warm verification measurements. Official
RPM metadata mismatch is tracked separately in #3578 and fails closed.

BREAKING CHANGE: Older Linux Hopper builds admitted by the fixed allowlist
now require retained publisher-signed package evidence or an approved update
to the current public demo. Update explicit HOPPER_LAUNCHER_PATH values when
migrating; the live feed exposes only its current release.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-15 12:15:55 +08:00

129 lines
3.6 KiB
TypeScript

import { beforeAll, describe, expect, it } from "vitest";
import { createMessage, generateKey, readPrivateKey, sign } from "openpgp";
import { verifyDetachedPgpSignature } from "./DetachedPgpSignature.js";
const bytes = Buffer.from("binary package fixture\0\xff", "latin1");
let publicKey: string;
let fingerprint: string;
let signatures: Uint8Array[];
let signingFingerprints: string[];
beforeAll(async () => {
const generated = await generateKey({
type: "ecc",
curve: "curve25519Legacy",
userIDs: [{ name: "Fixture publisher" }],
subkeys: [{ sign: true }, { sign: true }],
format: "armored",
});
const privateKey = await readPrivateKey({ armoredKey: generated.privateKey });
publicKey = generated.publicKey;
fingerprint = privateKey.getFingerprint().toUpperCase();
signingFingerprints = privateKey.subkeys.map((subkey) =>
subkey.getFingerprint().toUpperCase(),
);
signatures = [];
for (const subkey of privateKey.subkeys)
signatures.push(
await sign({
message: await createMessage({ binary: bytes }),
signingKeys: privateKey,
signingKeyIDs: [subkey.getKeyID()],
detached: true,
format: "binary",
}),
);
});
describe("detached binary PGP publisher verification", () => {
it("accepts independently selected signing subkeys through one pinned primary", async () => {
for (const [index, signature] of signatures.entries())
expect(
await verifyDetachedPgpSignature(
bytes,
signature,
publicKey,
fingerprint,
),
).toEqual({
ok: true,
value: {
primaryFingerprint: fingerprint,
signingFingerprint: signingFingerprints[index],
},
});
});
it("rejects a valid signature belonging to another primary", async () => {
expect(
await verifyDetachedPgpSignature(
bytes,
signatures[0]!,
publicKey,
"0".repeat(40),
),
).toMatchObject({ ok: false, error: { reason: "wrong_publisher" } });
});
it("rejects tampered payload bytes", async () => {
const tampered = Buffer.from(bytes);
tampered[0] = (tampered[0] ?? 0) ^ 1;
expect(
await verifyDetachedPgpSignature(
tampered,
signatures[0]!,
publicKey,
fingerprint,
),
).toMatchObject({ ok: false, error: { reason: "invalid_signature" } });
});
it("rejects malformed signatures and public keys", async () => {
expect(
await verifyDetachedPgpSignature(
bytes,
new Uint8Array([1, 2, 3]),
publicKey,
fingerprint,
),
).toMatchObject({ ok: false, error: { reason: "invalid_signature" } });
expect(
await verifyDetachedPgpSignature(
bytes,
signatures[0]!,
"malformed",
fingerprint,
),
).toMatchObject({ ok: false, error: { reason: "invalid_signature" } });
});
it("preserves cancellation", async () => {
expect(
await verifyDetachedPgpSignature(
bytes,
signatures[0]!,
publicKey,
fingerprint,
AbortSignal.abort(),
),
).toMatchObject({ ok: false, error: { reason: "cancelled" } });
});
it("preserves cancellation while loading the publisher key, before classifying its identity", async () => {
const controller = new AbortController();
const verification = verifyDetachedPgpSignature(
bytes,
signatures[0]!,
publicKey,
"0".repeat(40),
controller.signal,
);
queueMicrotask(() => controller.abort());
expect(await verification).toMatchObject({
ok: false,
error: { reason: "cancelled" },
});
});
});