mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
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>
129 lines
3.6 KiB
TypeScript
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" },
|
|
});
|
|
});
|
|
});
|