import { createMessage, PublicKey, readSignature, type Subkey, type UserIDPacket, verify, } from "openpgp"; import { armored, binary, createKeyFromArmor, createKeyFromBinary, createKeyFromFile, createKeysFromDir, DEFAULT_KEY_DISCOVERY_RULES, type KeyDiscoveryRules, type KeyFileFormat, } from "./create.ts"; import { getLastCommitForOneOfFiles } from "../git/log.ts"; import { get, instanciate } from "../../utils/anonymous.ts"; import { Packet, Signature } from "./sign.ts"; import type { Commit } from "../git/types.ts"; import { findMapAsync, type MaybeIterable } from "../../utils/iterator.ts"; import { getUserIDsFromKey } from "./user.ts"; import { env } from "../env.ts"; type DataURL = [URL, URL?]; type Corrupted = [false] | [true, Error]; export interface Verification { data: Uint8Array; dataCorrupted?: Promise; signatureCorrupted?: Corrupted; signature?: Signature; verifications?: { key: Promise; keyID: Awaited>["signatures"][number]["keyID"]; userID: Promise; packet: Promise; signatureCorrupted: Promise; verified: Promise; }[]; commit: Promise; } export class SignatureVerifier { static #instance: SignatureVerifier; private keys!: PublicKey[]; #encoder!: TextEncoder; #decoder!: TextDecoder; constructor() { this.keys = []; this.#encoder = new TextEncoder(); this.#decoder = new TextDecoder(); } /** * Let's test all the possible outcome situations that can happened when * verifying a signature of a file. A signature verification needs the message, * the signature (detached) and the public keys. * * **Possible verification outcomes** * * Legend: * * - "X" → This condition is definitely true for the outcome. * - "-" → This condition is not applicable or irrelevant. * - "?" → This condition may or may not be true; the outcome doesn't guarantee it. * * | Outcome Description | Data Exists | Data Corrupted | Signature Exists | Signature Corrupted/Malformed | Public Key Available | Public Key is Signing Key | Public Key Expired Before Signature | Public Key Expired After Signature | Public Key Revoked Before Signature | Public Key Revoked After Signature | Public Key Ultimately Trusted | GPG/OpenPGP Status Output | Notes | * | ------------------------------------------------------------------------------- | :---------: | :------------: | :--------------: | :---------------------------: | :------------------: | :-----------------------: | :---------------------------------: | :--------------------------------: | :---------------------------------: | :--------------------------------: | :---------------------------: | :------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | * | **No signature found** | X | ? | | | | | - | - | - | - | | (No status) | No signature file provided or found. Data state is independent of this. | * | **Signature cannot be checked (e.g., missing key, GPG error)** | X | ? | X | ? | | | - | - | - | - | ? | `E` | Verification failed before key or validity checks could be performed. Can be missing key, corrupted signature *format*, or GPG issue. | * | **Bad signature** | X | X | X | | X | X | ? | ? | ? | ? | ? | `B` | The signature does not match the data, usually due to data corruption or a manipulated signature. Key status is irrelevant to the mismatch itself. | * | **Good signature, unknown validity** | X | | X | | X | X | | | | | | `U` | Signature is cryptographically valid, key is available and is a signing key, but OpenPGP.js/GPG cannot determine the trust or validity of the key or signature attributes. | * | **Good signature** | X | | X | | X | X | | | | | X | `G` | The signature is cryptographically valid, the key is available, is a signing key, and is ultimately trusted in the local keyring. | * | **Good signature by an untrusted key** | X | | X | | X | X | | | | | | `G` (often with trust warning) | The signature is cryptographically valid, key is available and signing key, but not ultimately trusted. GPG might still report `G`. | * | **Good signature, key expired *after* signature time** | X | | X | | X | X | | X | | | ? | `X` | The signature was valid at the time of signing, but the key's validity period has since passed. | * | **Good signature, key expired *before* signature time** | X | | X | | X | X | X | | | | ? | `Y` | The signature was created *after* the key's validity period had passed. This signature is typically considered invalid. | * | **Good signature, key revoked *after* signature time** | X | | X | | X | X | ? | ? | | X | ? | `R` | The signature was valid at the time of signing, but the key has since been revoked. | * | **Good signature, key revoked *before* signature time** | X | | X | | X | X | ? | ? | X | | ? | `Y` (often, similar to expired before) | The signature was created *after* the key had been revoked. This signature is typically considered invalid. | * | **Signature cannot be checked (Public key available but not signing)** | X | ? | X | | X | | ? | ? | ? | ? | ? | `E` (or possibly `B`) | The key required for verification is found, but it does not have the 'sign' usage flag, making verification impossible with this key. | * | **Good signature, made by an expired signing subkey (primary key not expired)** | X | | X | | X | X | | X | | | ? | `X` | The signature was made by a subkey that expired *after* the signature time. The primary key might still be valid. | * | **Good signature, made by a revoked signing subkey (primary key not revoked)** | X | | X | | X | X | ? | ? | | X | ? | `R` | The signature was made by a subkey that was revoked *after* the signature time. The primary key might still be valid. | * | **Good signature, made by a signing subkey expired *before* signature** | X | | X | | X | X | X | | | | ? | `Y` | The signature was made by a subkey that was expired *before* the signature time. | * | **Good signature, made by a signing subkey revoked *before* signature** | X | | X | | X | X | ? | ? | X | | ? | `Y` | The signature was made by a subkey that was revoked *before* the signature time. | * * | Outcome Description (Combined Statuses) | Data Exists | Data Corrupted | Signature(s) Exist | At least one Signature Corrupted/Malformed | At least one Public Key Available | At least one Public Key is Signing Key | All Keys Good/Trusted? | Notes | * |---------------------------------------------------------------------------|-------------|----------------|--------------------|--------------------------------------------|-----------------------------------|----------------------------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| * | **No signature found** | X | ? | | | | | - | No signature file(s) provided or found. | * | **At least one signature cannot be checked (`E`), others unknown/not checked** | X | ? | X | ? | ? | ? | ? | One or more signatures failed verification before a status could be determined (missing key, GPG issue, etc.). Other signatures' statuses might be pending or unknown. | * | **All signatures are Bad (`B`)** | X | X | X | | X | X | ? | All provided signatures failed to match the data. Often due to data corruption or tampered signatures. | * | **Some signatures are Good (`G`), others are Bad (`B`)** | X | ? | X | | X | X | ? | At least one valid signature found, but also invalid ones. Indicates the file was signed correctly by some, but perhaps tampered with later or signed incorrectly. | * | **All signatures are Good (`G`)** | X | | X | | X | X | X | All provided signatures are cryptographically valid and from ultimately trusted keys. This is a strong indicator of data integrity and origin. | * | **Some signatures are Good (`G`), others Unknown Validity (`U`)** | X | ? | X | | X | X | ? | Some valid signatures, others valid but trust/validity could not be fully determined. | * | **All signatures Unknown Validity (`U`)** | X | ? | X | | X | X | | All provided signatures are cryptographically valid, but the validity or trust of the signing keys could not be determined for any of them. | * | **At least one Good signature (`G`), with others having Key Status issues (`X`, `Y`, `R`)** | X | ? | X | | X | X | ? | At least one valid and potentially trusted signature exists, but others are from expired or revoked keys. Indicates multiple signers, some with key lifecycle issues. | * | **All signatures have Key Status issues (`X`, `Y`, `R`)** | X | ? | X | | X | X | | All provided signatures are from keys that are expired or revoked. The data integrity might be verifiable for the time of signing, but the signers' keys are compromised or outdated. | * | **Combination of Bad (`B`), Unknown (`U`), and Key Status issues (`X`, `Y`, `R`)** | X | ? | X | ? | X | X | ? | A complex mix of verification outcomes for multiple signatures. Requires examining each individual signature's status to understand the situation fully. | * | **At least one signature is valid (`G`, `U`, `X`, `R`), but some Public Keys not available** | X | ? | X | | ? | ? | ? | Some signatures could be verified because their keys were available, but others could not be fully checked because their corresponding keys were missing. | * | **At least one signature is valid (`G`, `U`, `X`, `R`), but some Public Keys available but not Signing Keys** | X | ? | X | | X | ? | ? | Keys were found for some signatures, allowing some level of verification, but for others, the found key did not have the signing capability. | */ async verify( data: DataURL, type: KeyFileFormat = binary, ): Promise { // will throw if the file doesn't exist, not a file, ... // we need data. const dataBinary = await Deno.readFile(data[0], {}); const signatureURL = new URL( data[1] ?? `${data[0].href}.${type === binary ? "sig" : "asc"}`, ); const signatureData = await (type === binary ? Deno.readFile(signatureURL) : Deno.readTextFile(signatureURL)).catch(() => undefined); let signature: Signature | undefined; let signatureCorrupted: Corrupted | undefined = undefined; if (signatureData !== undefined) { try { signature = new Signature( await (typeof signatureData === "string" ? readSignature({ armoredSignature: signatureData }) : readSignature({ binarySignature: signatureData })), ); signatureCorrupted = [false]; } catch (e) { if ( !(e instanceof Error && [ "Error during parsing", "Packet not allowed in this context", "Unexpected end of packet", ].some( (x) => e.message.startsWith(x), )) ) { throw e; } signatureCorrupted = [true, e]; } } const commit = signature !== undefined ? getLastCommitForOneOfFiles([data[0], signatureURL]) : Promise.resolve(undefined); const verification: Verification = { data: dataBinary, signature, signatureCorrupted, commit, }; if (dataBinary === undefined || signature === undefined) { return verification; } const message = await createMessage({ binary: dataBinary }); const verificationResult = await verify({ message, signature: signature?.inner, verificationKeys: this.keys, format: "binary", }); verification.verifications = verificationResult.signatures.map( ({ verified, keyID, signature: sig }) => { const key = findMapAsync(this.keys, (x) => x.getSigningKey(keyID)); const packet = sig.then((x) => x.packets[0]).then(instanciate(Packet)); const userID = key.then((key) => key ? getUserIDsFromKey(signature, key) : undefined ); const signatureCorrupted = isSignatureCorrupted(verified); return { key, keyID, userID, packet, signatureCorrupted, verified }; }, ); verification.dataCorrupted = isDataCorrupted(verification.verifications); return verification; } async *verifyMultiple( data: Iterable, type: KeyFileFormat = binary, ): AsyncGenerator { for (const i of data) { yield this.verify(i, type); } } addKey(key: MaybeIterable): void { if (key instanceof PublicKey) { this.keys.push(key); } else { this.keys.push(...key); } } async addKeysFromDir( key: string | URL, rules: KeyDiscoveryRules = DEFAULT_KEY_DISCOVERY_RULES, ): Promise { for await ( const i of createKeysFromDir(key, rules, { encoder: this.#encoder, decoder: this.#decoder, }) ) { this.keys.push(i); } } async addKeyFromFile( key: string | URL, type: KeyFileFormat, ): Promise { switch (type) { case armored: { this.keys.push(await createKeyFromFile(key, type, this.#decoder)); break; } case binary: { this.keys.push(await createKeyFromFile(key, type, this.#encoder)); break; } } } async addKeyFromArmor( key: string | Uint8Array, ): Promise { this.keys.push( await createKeyFromArmor(key, this.#decoder).then((x) => x.toPublic()), ); } async addKeyFromBinary( key: string | Uint8Array, ): Promise { this.keys.push( await createKeyFromBinary(key, this.#encoder).then((x) => x.toPublic()), ); } public static async instance(): Promise { if (!SignatureVerifier.#instance) { SignatureVerifier.#instance = new SignatureVerifier(); await SignatureVerifier.#instance.addKeysFromDir(env.TRUSTED_KEYS_DIR); } return SignatureVerifier.#instance; } public clone(): this { const clone = new SignatureVerifier(); clone.keys = Object.create(this.keys); // clone.#decoder = Object.create(this.#decoder); // clone.#encoder = Object.create(this.#encoder); return clone as this; } } export const verifier = SignatureVerifier.instance(); async function isSignatureCorrupted( verified: Awaited< ReturnType >["signatures"][number]["verified"], ): Promise { return await verified.then(() => [false] as Corrupted).catch( (e) => { if (e instanceof Error) { if ( [ "Could not find signing key with key ID", "Signed digest did not match", ].some((x) => e.message.startsWith(x)) ) { return [false]; } return [true, e]; } throw e; }, ); } function isDataCorrupted( verifications: Verification["verifications"], ): Promise { return new Promise((resolve) => { if (verifications === undefined) { resolve([false]); } else { Promise.all(verifications.map(get("verified"))).then( () => resolve([false]), ).catch((e) => { if (e instanceof Error) { if ( e.message.startsWith("Signed digest did not match") ) { resolve([true, e]); } } resolve([false]); }); } }); }