import { TAG_PUBLIC_KEY, TAG_PUBLIC_SUBKEY, TAG_SIGNATURE, anyVerifies, concat, dearmor, keyExpiry, keyFlags, keyPrefix, latest, parseKeyPacket, parseSignaturePacket, readPackets, userIdPrefix, verifySignature, } from "./openpgp"; import type { ParsedSignature, PgpKey } from "./openpgp"; const TAG_USER_ID = 13; const TAG_USER_ATTRIBUTE = 17; const SUBPACKET_EMBEDDED_SIGNATURE = 32; const FLAG_SIGN = 0x02; const SIG_CERTIFICATION_FIRST = 0x10; const SIG_CERTIFICATION_LAST = 0x13; const SIG_SUBKEY_BINDING = 0x18; const SIG_PRIMARY_BINDING = 0x19; const SIG_KEY_REVOCATION = 0x20; const SIG_SUBKEY_REVOCATION = 0x28; export type PgpKeyring = { primary: PgpKey; signingKeys: PgpKey[] }; type SubkeyEntry = { body: Uint8Array; key: PgpKey | null; signatures: ParsedSignature[]; }; async function validSigningSubkey(primary: PgpKey, primaryPrefix: Uint8Array, entry: SubkeyEntry) { const key = entry.key; if (!key) return null; const bound = concat(primaryPrefix, keyPrefix(entry.body)); const revocations = entry.signatures.filter((signature) => signature.type === SIG_SUBKEY_REVOCATION); if (await anyVerifies(revocations, primary, bound, true)) return null; const bindings: ParsedSignature[] = []; for (const signature of entry.signatures) { if (signature.type === SIG_SUBKEY_BINDING && (await verifySignature(signature, primary, bound))) { bindings.push(signature); } } const binding = latest(bindings); if (!binding || !((keyFlags(binding) ?? 0) & FLAG_SIGN)) return null; const backSignatures = [...binding.hashed, ...binding.unhashed] .filter((item) => item.type === SUBPACKET_EMBEDDED_SIGNATURE) .map((item) => parseSignaturePacket(item.data)) .filter((signature): signature is ParsedSignature => signature?.type === SIG_PRIMARY_BINDING); if (!(await anyVerifies(backSignatures, key, bound))) return null; const limits = [keyExpiry(key, binding), primary.expiresAt].filter((limit): limit is number => limit !== null); return { ...key, expiresAt: limits.length > 0 ? Math.min(...limits) : null }; } /** * Parses an armored public key block, keeping only keys the primary has validly certified for signing: * the primary needs a user ID self-signature, subkeys need a binding plus a back-signature from the subkey. */ export async function parsePgpPublicKeys(armored: string): Promise { const bytes = dearmor(armored); if (!bytes) throw new Error("Expected an armored PGP public key block"); const packets = readPackets(bytes); if (packets[0]?.tag !== TAG_PUBLIC_KEY) throw new Error("Expected a PGP public key"); const parsedPrimary = await parseKeyPacket(packets[0].body); if (!parsedPrimary) throw new Error("Unsupported primary key (RSA, Ed25519 and NIST ECDSA are supported)"); const primaryPrefix = keyPrefix(packets[0].body); const direct: ParsedSignature[] = []; const certifications: Array<{ userId: Uint8Array; signature: ParsedSignature; }> = []; const subkeys: SubkeyEntry[] = []; let context: | { kind: "primary" } | { kind: "user"; userId: Uint8Array } | { kind: "subkey"; entry: SubkeyEntry } | null = { kind: "primary" }; for (const packet of packets.slice(1)) { if (packet.tag === TAG_PUBLIC_KEY) break; if (packet.tag === TAG_USER_ID) context = { kind: "user", userId: userIdPrefix(packet.body) }; else if (packet.tag === TAG_USER_ATTRIBUTE) context = null; else if (packet.tag === TAG_PUBLIC_SUBKEY) { const entry: SubkeyEntry = { body: packet.body, key: await parseKeyPacket(packet.body), signatures: [], }; subkeys.push(entry); context = { kind: "subkey", entry }; } else if (packet.tag === TAG_SIGNATURE && context) { const signature = parseSignaturePacket(packet.body); if (!signature) continue; if (context.kind === "primary") direct.push(signature); else if (context.kind === "user") certifications.push({ userId: context.userId, signature }); else context.entry.signatures.push(signature); } } const revocations = direct.filter((signature) => signature.type === SIG_KEY_REVOCATION); if (await anyVerifies(revocations, parsedPrimary, primaryPrefix, true)) throw new Error("This key has been revoked"); const selfSignatures: ParsedSignature[] = []; for (const { userId, signature } of certifications) { if (signature.type < SIG_CERTIFICATION_FIRST || signature.type > SIG_CERTIFICATION_LAST) continue; if (await verifySignature(signature, parsedPrimary, concat(primaryPrefix, userId))) selfSignatures.push(signature); } const selfSignature = latest(selfSignatures); if (!selfSignature) throw new Error("The key has no valid SHA-2 self-signature on a user ID"); const primary = { ...parsedPrimary, expiresAt: keyExpiry(parsedPrimary, selfSignature), }; const signingKeys: PgpKey[] = []; const primaryFlags = keyFlags(selfSignature); if (primaryFlags === null || primaryFlags & FLAG_SIGN) signingKeys.push(primary); for (const entry of subkeys) { const subkey = await validSigningSubkey(primary, primaryPrefix, entry); if (subkey) signingKeys.push(subkey); } const now = Date.now(); if (!signingKeys.some((key) => key.expiresAt === null || key.expiresAt > now)) { throw new Error("No unexpired, validly bound signing key found (RSA, Ed25519 and NIST ECDSA are supported)"); } return { primary, signingKeys }; }