From f9a77c5c27aede4e5978eb55d9b7af781b680a1d Mon Sep 17 00:00:00 2001 From: João Augusto Costa Branco Marado Torres Date: Tue, 24 Jun 2025 12:08:41 -0300 Subject: feat!: initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Augusto Costa Branco Marado Torres --- src/lib/pgp/summary.ts | 232 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 src/lib/pgp/summary.ts (limited to 'src/lib/pgp/summary.ts') 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, Map>]> { + 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]>[] + >( + (verifications ?? []).map( + async ({ signatureCorrupted, verified, packet, key }) => { + const errors: Summary[] = []; + const keys: Map = 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]; + } + + 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]; + }, + ), + ); + + 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, + Map>, + ]; + } + + throw new Error("unreachable"); +} -- cgit v1.2.3