Files
reverse-engineering-anything/src/process/DetachedPgpSignature.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

102 lines
3.0 KiB
TypeScript

import { err, ok, type Result } from "../domain/result.js";
export class DetachedPgpSignatureError extends Error {
readonly _tag = "DetachedPgpSignatureError";
constructor(
readonly reason: "invalid_signature" | "wrong_publisher" | "cancelled",
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = this._tag;
}
}
/** Verify the complete binary payload and its signing subkey's primary identity. */
export const verifyDetachedPgpSignature = async (
bytes: Uint8Array,
detachedSignature: Uint8Array,
armoredPublicKey: string,
primaryFingerprint: string,
signal?: AbortSignal,
): Promise<
Result<
{
readonly primaryFingerprint: string;
readonly signingFingerprint: string;
},
DetachedPgpSignatureError
>
> => {
const cancelled = () =>
err(
new DetachedPgpSignatureError(
"cancelled",
"Package verification was cancelled.",
),
);
const isAborted = () => signal?.aborted === true;
if (isAborted()) return cancelled();
try {
// Keep the cryptographic implementation out of unrelated CLI cold starts.
const pgp = await import("openpgp");
const key = await pgp.readKey({ armoredKey: armoredPublicKey });
if (isAborted()) return cancelled();
const fingerprint = key.getFingerprint().toUpperCase();
if (fingerprint !== primaryFingerprint)
return err(
new DetachedPgpSignatureError(
"wrong_publisher",
"The package signing key does not belong to the pinned publisher.",
),
);
const signature = await pgp.readSignature({
binarySignature: detachedSignature,
});
if (isAborted()) return cancelled();
if (
signature.packets.length !== 1 ||
signature.packets[0]?.signatureType !== pgp.enums.signature.binary
)
return err(
new DetachedPgpSignatureError(
"invalid_signature",
"Expected one detached binary package signature.",
),
);
const verified = await pgp.verify({
message: await pgp.createMessage({ binary: bytes }),
signature,
verificationKeys: key,
format: "binary",
});
const signed = verified.signatures[0];
if (signed === undefined || verified.signatures.length !== 1)
return err(
new DetachedPgpSignatureError(
"invalid_signature",
"The package has no unique verified signature.",
),
);
await signed.verified;
const signingKey = await key.getSigningKey(
signed.keyID,
signature.packets[0].created,
);
if (isAborted()) return cancelled();
return ok({
primaryFingerprint: fingerprint,
signingFingerprint: signingKey.getFingerprint().toUpperCase(),
});
} catch (cause: unknown) {
if (isAborted()) return cancelled();
return err(
new DetachedPgpSignatureError(
"invalid_signature",
"The package signature could not be verified.",
{ cause },
),
);
}
};