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"); }