summaryrefslogtreecommitdiff
path: root/src/lib/pgp/summary.ts
diff options
context:
space:
mode:
authorJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:08:41 -0300
committerJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:50:43 -0300
commitf9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch)
treed545e325ba1ae756fc2eac66fac1001b6753c40d /src/lib/pgp/summary.ts
feat!: initial commit
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/lib/pgp/summary.ts')
-rw-r--r--src/lib/pgp/summary.ts232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/lib/pgp/summary.ts b/src/lib/pgp/summary.ts
new file mode 100644
index 0000000..5c8a81c
--- /dev/null
+++ b/src/lib/pgp/summary.ts
@@ -0,0 +1,232 @@
+import type { Key, PublicKey, Subkey } from "openpgp";
+import type { Verification } from "./verify.ts";
+import { Level } from "../../utils/index.ts";
+import type { NonEmptyArray } from "../../utils/iterator.ts";
+import { keyTrust } from "./trust.ts";
+import { isKeyExpired, isKeyRevoked, type RevocationReason } from "./index.ts";
+
+export const enum VerificationResult {
+ NO_SIGNATURE,
+ MISSING_KEY,
+ SIGNATURE_CORRUPTED,
+ SIGNATURE_COULD_NOT_BE_CHECKED,
+ BAD_SIGNATURE,
+ UNTRUSTED_KEY,
+ TRUSTED_KEY,
+ EXPIRATION_AFTER_SIGNATURE,
+ EXPIRATION_BEFORE_SIGNATURE,
+ REVOCATION_AFTER_SIGNATURE,
+ REVOCATION_BEFORE_SIGNATURE,
+ KEY_DOES_NOT_SIGN,
+}
+
+export function logLevel(result: VerificationResult): [Level, boolean] {
+ switch (result) {
+ case VerificationResult.NO_SIGNATURE:
+ return [Level.ERROR, true] as const;
+ case VerificationResult.MISSING_KEY:
+ return [Level.ERROR, false] as const;
+ case VerificationResult.SIGNATURE_CORRUPTED:
+ return [Level.ERROR, true] as const;
+ case VerificationResult.SIGNATURE_COULD_NOT_BE_CHECKED:
+ return [Level.ERROR, false] as const;
+ case VerificationResult.BAD_SIGNATURE:
+ return [Level.ERROR, false] as const;
+ case VerificationResult.UNTRUSTED_KEY:
+ return [Level.OK, false] as const;
+ case VerificationResult.TRUSTED_KEY:
+ return [Level.OK, true] as const;
+ case VerificationResult.EXPIRATION_AFTER_SIGNATURE:
+ return [Level.WARN, false] as const;
+ case VerificationResult.EXPIRATION_BEFORE_SIGNATURE:
+ return [Level.ERROR, true] as const;
+ case VerificationResult.REVOCATION_AFTER_SIGNATURE:
+ return [Level.WARN, true] as const;
+ case VerificationResult.REVOCATION_BEFORE_SIGNATURE:
+ return [Level.ERROR, true] as const;
+ case VerificationResult.KEY_DOES_NOT_SIGN:
+ return [Level.ERROR, true] as const;
+ }
+
+ throw new Error("unreachable");
+}
+
+export type Summary = {
+ result: VerificationResult.NO_SIGNATURE;
+} | {
+ result: VerificationResult.MISSING_KEY;
+ reason: Error;
+ keyID: string;
+ created: Date;
+} | {
+ result:
+ | VerificationResult.SIGNATURE_CORRUPTED
+ | VerificationResult.SIGNATURE_COULD_NOT_BE_CHECKED
+ | VerificationResult.BAD_SIGNATURE;
+ reason: Error;
+} | {
+ result: VerificationResult.TRUSTED_KEY;
+ key: PublicKey | Subkey;
+ created: Date;
+} | {
+ result: VerificationResult.UNTRUSTED_KEY;
+ key: PublicKey | Subkey;
+ created: Date;
+} | {
+ result: VerificationResult.EXPIRATION_AFTER_SIGNATURE;
+ key: PublicKey | Subkey;
+ expired: Date;
+ created: Date;
+} | {
+ result: VerificationResult.REVOCATION_AFTER_SIGNATURE;
+ key: PublicKey | Subkey;
+ revoked: Date;
+ revocationReason: RevocationReason;
+ created: Date;
+} | {
+ result: VerificationResult.EXPIRATION_BEFORE_SIGNATURE;
+ key: PublicKey | Subkey;
+ expired: Date;
+ created: Date;
+} | {
+ result: VerificationResult.REVOCATION_BEFORE_SIGNATURE;
+ key: PublicKey | Subkey;
+ revoked: Date;
+ revocationReason: RevocationReason;
+ created: Date;
+} | {
+ result: VerificationResult.KEY_DOES_NOT_SIGN;
+ key: PublicKey | Subkey;
+};
+
+export async function createVerificationSummary(
+ { dataCorrupted, verifications, signature }: Verification,
+): Promise<[NonEmptyArray<Summary>, Map<string, NonEmptyArray<Summary>>]> {
+ if (signature === undefined) {
+ return [[{ result: VerificationResult.NO_SIGNATURE }], new Map()];
+ }
+
+ const corrupted = await dataCorrupted;
+ if (corrupted?.[0]) {
+ return [[{
+ result: VerificationResult.BAD_SIGNATURE,
+ reason: corrupted[1],
+ }], new Map()];
+ }
+
+ const summaries = await Promise.all<
+ Promise<[Summary[], Map<string, Summary[]>]>[]
+ >(
+ (verifications ?? []).map(
+ async ({ signatureCorrupted, verified, packet, key }) => {
+ const errors: Summary[] = [];
+ const keys: Map<string, Summary[]> = new Map();
+
+ try {
+ await verified;
+ } catch (e) {
+ if (e instanceof Error) {
+ if (
+ e.message.startsWith("Could not find signing key with key ID")
+ ) {
+ const keyID = e.message.slice(e.message.lastIndexOf(" "));
+ const key = keys.get(keyID) ?? [];
+ key.push({
+ result: VerificationResult.MISSING_KEY,
+ keyID,
+ reason: e,
+ });
+ keys.set(keyID, key);
+ } else {
+ errors.push({
+ result: VerificationResult.SIGNATURE_COULD_NOT_BE_CHECKED,
+ reason: e,
+ });
+ }
+ } else {
+ throw e;
+ }
+ }
+
+ const corrupted = await signatureCorrupted;
+ if (corrupted[0]) {
+ errors.push({
+ result: VerificationResult.SIGNATURE_CORRUPTED,
+ reason: corrupted[1],
+ });
+ }
+
+ const sig = await packet;
+ const keyID = sig.issuerKeyID;
+
+ sig.created;
+
+ const keyAwaited = await key;
+
+ if (keyAwaited === undefined) {
+ const key = keys.get(keyID.toHex()) ?? [];
+ key.push({
+ result: VerificationResult.MISSING_KEY,
+ keyID: keyID.toHex(),
+ reason: new Error(
+ `Could not find signing key with key ID ${keyID.toHex()}`,
+ ),
+ });
+ keys.set(keyID.toHex(), key);
+
+ return [errors, keys] as [Summary[], Map<string, Summary[]>];
+ }
+
+ const keySummaries = keys.get(keyAwaited.getKeyID().toHex()) ?? [];
+ const expired = await isKeyExpired(keyAwaited);
+
+ if (expired !== null && sig.created !== null) {
+ keySummaries.push({
+ result: expired <= sig.created
+ ? VerificationResult.EXPIRATION_BEFORE_SIGNATURE
+ : VerificationResult.EXPIRATION_AFTER_SIGNATURE,
+ key: keyAwaited,
+ date: expired,
+ });
+ }
+
+ const revoked = isKeyRevoked(keyAwaited);
+ if (revoked?.date !== undefined && sig.created !== null) {
+ keySummaries.push({
+ result: revoked?.date <= sig.created
+ ? VerificationResult.REVOCATION_BEFORE_SIGNATURE
+ : VerificationResult.REVOCATION_AFTER_SIGNATURE,
+ key: keyAwaited,
+ date: revoked.date,
+ revocationReason: revoked.reason,
+ });
+ }
+
+ const trust = sig.trustAmount ?? await keyTrust(keyAwaited as Key);
+
+ keySummaries.push({
+ result: trust > 0
+ ? VerificationResult.TRUSTED_KEY
+ : VerificationResult.UNTRUSTED_KEY,
+ key: keyAwaited,
+ });
+
+ keys.set(keyAwaited.getKeyID().toHex(), keySummaries);
+
+ return [errors, keys] as [Summary[], Map<string, Summary[]>];
+ },
+ ),
+ );
+
+ const errors = summaries.flatMap(([x]) => x);
+ const keys = new Map(summaries.flatMap(([, x]) => x.entries().toArray()));
+
+ if (errors.length > 0 || keys.size > 0) {
+ return [errors, keys] as [
+ NonEmptyArray<Summary>,
+ Map<string, NonEmptyArray<Summary>>,
+ ];
+ }
+
+ throw new Error("unreachable");
+}