mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(desktop): read the PPA key add-apt-repository writes into Signed-By On Ubuntu 24.04, add-apt-repository puts the PPA's armored key inside the deb822 Signed-By field, and REA refused such repositories, so PPAs added the standard way could not be exported. REA now reads that key by deb822 rules, verifies the release with it through the same private gpgv keyring as key files, as the entry's only candidate (as apt does), retains it as armored and exports it as apt-keys/<name>.asc. A held key that does not verify is refused with a reason that names no URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): check the held-key refusal's facts, not its sentence The PPA refusal tests now assert that the reason names the sources file and the release file and no URL, and that export carries that reason, instead of comparing whole sentences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import { access, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import {
|
|
A_ARMORED,
|
|
SIGNED_BY_BOTH,
|
|
VENDOR_A,
|
|
VENDOR_B,
|
|
} from "../domain/desktop/aptKeys.fixture.js";
|
|
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
|
import { withGpgvKeyCheck } from "./GpgvKeyCheck.js";
|
|
import { runHostQuery, type QueryRunner } from "./HostQuery.js";
|
|
|
|
const hasGpgv =
|
|
typeof (await runHostQuery("gpgv", ["--version"], undefined)) === "object";
|
|
|
|
/** The release both vendor keys signed, and keyring files holding the given keys. */
|
|
const files = async (keyrings: Readonly<Record<string, Buffer>>) => {
|
|
const folder = await createTestTempDirectory("desktop-gpgv-");
|
|
const release = join(folder, "InRelease");
|
|
await writeFile(release, SIGNED_BY_BOTH);
|
|
const paths: Record<string, string> = {};
|
|
for (const [name, bytes] of Object.entries(keyrings)) {
|
|
paths[name] = join(folder, name);
|
|
await writeFile(paths[name], bytes);
|
|
}
|
|
return { release, paths };
|
|
};
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe.skipIf(!hasGpgv)("withGpgvKeyCheck with gpgv", () => {
|
|
it("accepts a key whose signature is good although the release's other signature has no key here", async () => {
|
|
const { release, paths } = await files({ "a.gpg": VENDOR_A.key });
|
|
|
|
const verified = await withGpgvKeyCheck(runHostQuery, undefined, (check) =>
|
|
check.firstVerifying([paths["a.gpg"] ?? ""], [release]),
|
|
);
|
|
|
|
expect(verified).toMatchObject({
|
|
path: paths["a.gpg"],
|
|
format: "binary",
|
|
fingerprint: VENDOR_A.fingerprint,
|
|
});
|
|
});
|
|
|
|
it("retains only the verifying key of a keyring holding several", async () => {
|
|
const { release, paths } = await files({
|
|
"trusted.gpg": Buffer.concat([VENDOR_B.key, VENDOR_A.key]),
|
|
});
|
|
|
|
const verified = await withGpgvKeyCheck(runHostQuery, undefined, (check) =>
|
|
check.firstVerifying([paths["trusted.gpg"] ?? ""], [release]),
|
|
);
|
|
|
|
if ("reason" in verified) throw new Error(verified.reason);
|
|
expect(verified.fingerprint).toBe(VENDOR_A.fingerprint);
|
|
expect(verified.bytes.equals(VENDOR_A.key)).toBe(true);
|
|
});
|
|
|
|
it("verifies the armored key a sources entry holds and retains it as it is, named by the entry's file", async () => {
|
|
const { release } = await files({});
|
|
const sources = "/etc/apt/sources.list.d/a-ubuntu-b-noble.sources";
|
|
|
|
const verified = await withGpgvKeyCheck(runHostQuery, undefined, (check) =>
|
|
check.heldVerifying({ path: sources, bytes: Buffer.from(A_ARMORED) }, [
|
|
release,
|
|
]),
|
|
);
|
|
|
|
if ("reason" in verified) throw new Error(verified.reason);
|
|
expect(verified).toMatchObject({
|
|
path: sources,
|
|
format: "armored",
|
|
fingerprint: VENDOR_A.fingerprint,
|
|
});
|
|
expect(verified.bytes.toString()).toBe(A_ARMORED);
|
|
});
|
|
});
|
|
|
|
describe("withGpgvKeyCheck cleanup", () => {
|
|
it("keeps the private keyring folder's owner when its removal fails", async () => {
|
|
const create = PrivateRuntimeRoot.create.bind(PrivateRuntimeRoot);
|
|
let owned: PrivateRuntimeRoot | undefined;
|
|
vi.spyOn(PrivateRuntimeRoot, "create").mockImplementationOnce(
|
|
async (options) => {
|
|
owned = await create(options);
|
|
vi.spyOn(owned, "close").mockRejectedValueOnce(new Error("busy"));
|
|
return owned;
|
|
},
|
|
);
|
|
const { release, paths } = await files({ "a.gpg": VENDOR_A.key });
|
|
const runQuery: QueryRunner = async () => ({ stdout: "", exitCode: 2 });
|
|
|
|
const failure = await withGpgvKeyCheck(runQuery, undefined, (check) =>
|
|
check.firstVerifying([paths["a.gpg"] ?? ""], [release]),
|
|
).then(
|
|
() => undefined,
|
|
(cause: unknown) => cause,
|
|
);
|
|
|
|
if (!(failure instanceof ArtifactReaderFailure) || owned === undefined)
|
|
throw new Error("expected a cleanup failure");
|
|
expect(failure.cleanup?.resources).toEqual([owned.path]);
|
|
await access(owned.path);
|
|
await Promise.all(
|
|
failure.cleanupLeases?.map((lease) => lease.release()) ?? [],
|
|
);
|
|
await expect(access(owned.path)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("writes a held key's keyring into the private folder and removes it, saying why the key does not verify", async () => {
|
|
const { release } = await files({});
|
|
const keyrings: string[] = [];
|
|
const runQuery: QueryRunner = async (_command, argv) => {
|
|
keyrings.push(argv[argv.indexOf("--keyring") + 1] ?? "");
|
|
return { stdout: "[GNUPG:] NEWSIG\n", exitCode: 2 };
|
|
};
|
|
const sources = "/etc/apt/sources.list.d/a-ubuntu-b-noble.sources";
|
|
|
|
const verified = await withGpgvKeyCheck(runQuery, undefined, (check) =>
|
|
check.heldVerifying({ path: sources, bytes: Buffer.from(A_ARMORED) }, [
|
|
release,
|
|
]),
|
|
);
|
|
|
|
if (!("reason" in verified)) throw new Error("expected a refusal");
|
|
expect(verified.reason).toContain(sources);
|
|
expect(verified.reason).toContain(release);
|
|
expect(verified.reason).not.toMatch(/[a-z][a-z0-9+.-]*:\/\//iu);
|
|
expect(keyrings).toHaveLength(1);
|
|
await expect(access(keyrings[0] ?? "")).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
});
|
|
});
|