import { useState } from "react"; import { currentAppName, useAppName } from "@/lib/brand"; import { BadgeCheck, ShieldAlert, ShieldQuestion, ShieldX } from "lucide-react"; import { formatFingerprint } from "@/lib/smime/x509"; import type { SignatureState } from "@/lib/smime/useSignature"; import type { Reason } from "@/lib/smime/verify"; import { t, tNode } from "@/lib/i18n"; import { formatFullDate } from "@/lib/format"; /** * What a checked signature is allowed to say on screen. * * The wording here is the feature. ihasmail has no certificate authority to ask * and none is bundled, so the strong word — "verified", full stop — is never * used: the certificate arrives inside the message, and on its own a good * signature only shows that whoever wrote the message held the key attached to * it. What can honestly be said is whether this is the same signer as last * time, and that is what the banner leads with. * * Which means the *reassuring* case is deliberately the quiet one and the * changed-signer case is the loud one. A green tick on a first sighting would * be telling somebody that an unknown certificate is trustworthy because it * verified against itself. */ export function SignatureBanner({ state }: { state: SignatureState }) { const appName = useAppName(); const [open, setOpen] = useState(false); if (state.status !== "done") return null; const { crypto, trust, previous, warnings } = state.report; if (crypto.kind === "none") return null; if (crypto.kind === "unsupported") { return ( }> {t("This message is signed, and {app} could not check the signature.", { app: appName })} {explain(crypto.reason)} {crypto.detail && {crypto.detail}} ); } if (crypto.kind === "broken") { return ( }> {t("This signature does not check out.")} {explain(crypto.reason)} ); } const name = crypto.cert.subject.commonName || crypto.cert.emails[0] || t("an unnamed signer"); const changed = trust === "changed"; const mismatch = warnings.includes("address-mismatch"); const tone = changed || mismatch ? "danger" : warnings.length > 0 ? "warn" : trust === "same-as-before" ? "good" : "quiet"; return ( : trust === "same-as-before" ? : }> {changed ? ( <> {t("The signer has changed.")}{" "} {tNode("Earlier messages from this address were signed by {previous}. This one is signed by {current}.", { previous: {previous?.name ?? t("a different certificate")}, current: {name}, })}{" "} {t("That can mean a renewed certificate, and it can mean somebody else. Check with them by some other route before trusting it.")} ) : mismatch ? ( <> {t("The signature is not for this sender.")}{" "} {tNode("It was made with a certificate belonging to {name}, which does not cover this address.", { name: {name}, })} ) : trust === "same-as-before" ? ( tNode("Signed by {name} — the same signer as before.", { name: {name} }) ) : ( <> {tNode("Signed by {name}, seen here for the first time.", { name: {name} })}{" "} {t("{app} will tell you if a later message from this address is signed by anybody else.", { app: appName })} )} {warnings.includes("certificate-expired") && <> {t("The certificate has expired.")}} {warnings.includes("certificate-not-yet-valid") && <> {t("The certificate is not valid yet.")}} {open && ( {crypto.signer.signingTime && ( )} {previous && ( )}
{t("Signer")} {name}
{t("Certificate covers")} {crypto.cert.emails.join(", ") || t("no address")}
{t("Issued by")} {crypto.cert.issuer.commonName || crypto.cert.issuer.organization || t("itself, or an issuer it does not name")}
{t("Valid until")} {formatFullDate(crypto.cert.notAfter.toISOString())}
{t("Signed at")} {formatFullDate(crypto.signer.signingTime.toISOString())} {t("as claimed by the signer")}
{t("Fingerprint")} {formatFingerprint(crypto.cert.fingerprint)}
{t("Previous fingerprint")} {formatFingerprint(previous.fingerprint)} {t("first seen {date}", { date: formatFullDate(previous.firstSeen) })}
)}
); } /** The sayable version of why a check did not happen, or did not hold. */ function explain(reason: Reason): string { const appName = currentAppName(); switch (reason) { case "openpgp": return t("It is signed with OpenPGP, and {app} has no way to fetch the sender's public key.", { app: appName }); case "rsa-pss": return t("It uses a signature algorithm {app} cannot check yet.", { app: appName }); case "no-certificate": return t("The signature carries no certificate that can be read."); case "not-signed-properly": return t("The signed part is missing either the message or the signature."); case "digest-mismatch": return t("The message does not match what was signed — it was altered after signing, or damaged on the way."); case "signature-mismatch": return t("The signature does not match the certificate sent with it."); case "other": return t("The signature could not be read."); } } function Banner({ tone, icon, children }: { tone: "good" | "warn" | "danger" | "quiet"; icon: React.ReactNode; children: React.ReactNode }) { return (
{icon} {children}
); }