diff options
Diffstat (limited to 'src/components/templates')
-rw-r--r-- | src/components/templates/Authors.astro | 355 | ||||
-rw-r--r-- | src/components/templates/CopyrightNotice.astro | 70 | ||||
-rw-r--r-- | src/components/templates/licenses/CC.astro | 153 | ||||
-rw-r--r-- | src/components/templates/licenses/WTFPL.astro | 70 | ||||
-rw-r--r-- | src/components/templates/signature/Commit.astro | 86 | ||||
-rw-r--r-- | src/components/templates/signature/Downloads.astro | 63 | ||||
-rw-r--r-- | src/components/templates/signature/Signature.astro | 44 | ||||
-rw-r--r-- | src/components/templates/signature/Summary.astro | 279 |
8 files changed, 1120 insertions, 0 deletions
diff --git a/src/components/templates/Authors.astro b/src/components/templates/Authors.astro new file mode 100644 index 0000000..61ca026 --- /dev/null +++ b/src/components/templates/Authors.astro @@ -0,0 +1,355 @@ +--- +import { type RevocationReason, toPK } from "@lib/pgp"; +import type { Verification } from "@lib/pgp/verify"; +import { defined, get } from "@utils/anonymous"; +import type { getSigners } from "@lib/collection/helpers"; +import type { PublicKey, UserIDPacket } from "openpgp"; +import { + createVerificationSummary, + VerificationResult, +} from "@lib/pgp/summary"; +import Date from "@components/organisms/Date.astro"; + +interface Props { + verifications: NonNullable<Verification["verifications"]>; + expectedSigners: Map< + string, + { + signer: Awaited<ReturnType<typeof getSigners>>[number]; + users: UserIDPacket[]; + key: PublicKey; + } + >; + commitSignerKey?: string; +} + +const { + verifications: verificationsPromise, + expectedSigners, + commitSignerKey, +} = Astro.props; + +let verifications = await Promise.all( + verificationsPromise.map(async (verification) => { + const { key, keyID, userID, verified } = verification; + return { + key: await key, + keyID, + userID: await userID, + verified: await verified.catch(() => false), + summary: await createVerificationSummary(verification), + }; + }), +); + +const expectedKeys = Array.from(expectedSigners.values()).map(get("key")); + +const expectedFingerprints = new Set( + expectedKeys.map((key) => key.getFingerprint()), +); + +const verifiedFingerprints = new Set( + verifications.map((v) => v.key).filter(defined).map(toPK).map((key) => + key.getFingerprint() + ), +); + +if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) { + throw new Error( + `Missing signature from expected signers: ${[ + ...expectedFingerprints.difference(verifiedFingerprints).values(), + ]}`, + ); +} +--- + +<div> + <table> + <caption> + <strong>Assinaturas</strong> + <p> + Para verificar uma assinatura é necessário a <a href="#message" + >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em + >chaves públicas</em> dos assinantes. Esta tabela mostra algumas + informações sobre os assinantes, as suas chaves públicas e as suas + assinaturas. + </p> + </caption> + <colgroup> + <col /> + <col /> + </colgroup> + <colgroup> + <col /> + <col /> + <col /> + </colgroup> + <colgroup> + <col /> + </colgroup> + <thead> + <tr> + <th scope="col">Assinante</th> + <th scope="col">Função</th> + <th scope="col">Fingerprint</th> + <th scope="col">Válido</th> + <th scope="col">Commiter</th> + <th scope="col">Mais Informações</th> + </tr> + </thead> + <tbody> + { + verifications.map( + ({ userID, key, keyID, verified, summary }) => { + const fingerprint = key + ? toPK(key).getFingerprint() + : undefined; + const info = fingerprint + ? expectedSigners.get(fingerprint) + : undefined; + const primary = userID?.[0]; + const signer = info?.signer; + let role = ""; + switch (signer?.role) { + case "author": { + role = "Autor"; + break; + } + case "co-author": { + role = "Co-autor"; + break; + } + case "translator": { + role = "Tradutor"; + break; + } + } + const { + reasons, + created, + expired, + revoked, + revocationReason, + trusted, + } = (summary[1].get(keyID.toHex()) ?? []).reduce( + (acc, x) => { + if (!("key" in x || "keyID" in x)) { + return acc; + } + + switch (x.result) { + case VerificationResult.MISSING_KEY: + acc.reasons.push(x.reason); + acc.created = x.created ?? undefined; + break; + case VerificationResult.UNTRUSTED_KEY: + acc.created = x.created ?? undefined; + acc.trusted &&= false; + break; + case VerificationResult.TRUSTED_KEY: + acc.created = x.created ?? undefined; + acc.trusted = true; + break; + case VerificationResult + .EXPIRATION_AFTER_SIGNATURE: + acc.created = x.created ?? undefined; + acc.expired = x.expired; + break; + case VerificationResult + .EXPIRATION_BEFORE_SIGNATURE: + acc.created = x.created ?? undefined; + acc.expired = x.expired; + break; + case VerificationResult + .REVOCATION_AFTER_SIGNATURE: + acc.created = x.created ?? undefined; + acc.revoked = x.revoked; + acc.revocationReason = x.revocationReason; + break; + case VerificationResult + .REVOCATION_BEFORE_SIGNATURE: + acc.created = x.created ?? undefined; + acc.revoked = x.revoked; + acc.revocationReason = x.revocationReason; + break; + case VerificationResult.KEY_DOES_NOT_SIGN: + break; + } + + return acc; + }, + { + reasons: [], + created: undefined, + expired: undefined, + revoked: undefined, + revocationReason: undefined, + trusted: false, + } as { + reasons: Error[]; + created?: Date; + expired?: Date; + revoked?: Date; + revocationReason?: RevocationReason; + trusted: boolean; + }, + ); + return ( + <tr> + <th scope="row"> + <address + itemprop="author" + itemscope + itemtype="https://schema.org/Person" + > + { + primary?.name + ? signer?.entity?.data?.websites[0] + ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={signer?.entity?.data?.websites[0]} + ><span itemprop="name">{primary.name}</span></a> + ) + : <span itemprop="name">{primary.name}</span> + : primary?.email + ? ( + <><<a + itemprop="email" + rel="author external noreferrer" + target="_blank" + href={primary?.email && `mailto:${primary.email}`} + >{primary?.email}</a>></> + ) + : "" + } + </address> + {signer !== undefined && <><hr /><a href="#">Ver perfil</a></>} + </th> + <td>{role}</td> + <td> + <samp title={fingerprint?.replace(/(....)/g, "$1 ")}>{ + key + ? "0x" + toPK(key).getKeyID().toHex() + : "0x" + keyID.toHex() + }</samp> + </td> + <td>{verified ? "✅" : "❌"}</td> + <td> + { + commitSignerKey && + key?.getFingerprint().toUpperCase()?.endsWith( + commitSignerKey.toUpperCase(), + ) && "✅" + } + </td> + <td> + { + key && ( + <> + <button + type="button" + class="emoji" + popovertarget={`info-${fingerprint}`} + > + ➕ + </button> + <section popover id={`info-${fingerprint}`}> + <p>Erros</p> + { + summary[0].map((x) => { + if (!("reason" in x)) { + return undefined; + } + + let reason = x.reason; + + return <pre>{reason.message}</pre>; + }) + } + <p><strong>Informações</strong></p> + <dl class="divider"> + { + reasons.map(({ message }) => ( + <> <dt>Erro</dt> <dd>{message}</dd></> + )) + } + { + created && ( + <> + <dt>Data da assinatura</dt> + <dd><Date date={created} options={{}} /></dd> + </> + ) + } + { + expired && ( + <> + <dt>Data de expiração</dt> + <dd><Date date={expired} options={{}} /></dd> + </> + ) + } + { + revoked && ( + <> + <dt>Data de revogação</dt> + <dd><Date date={revoked} options={{}} /></dd> + </> + ) + } + { + revocationReason && ( + <> + <dt>Razão para a revogação</dt> + <dd> + {revocationReason.flag}: {revocationReason.msg} + </dd> + </> + ) + } + <dt>Chave confiável</dt> + <dd>{trusted ? "✅" : "❌"}</dd> + </dl> + </section></> + ) + } + </td> + </tr> + ); + }, + ) + } + </tbody> + </table> +</div> + +<style> + div { + overflow-x: auto; + } + + table { + table-layout: fixed; + border-collapse: collapse; + border: 3px solid; + margin-inline: auto; + max-width: 90svw; + } + + th, + td { + padding: 1rem; + text-align: center; + } + + tbody tr:nth-child(odd) { + background-color: oklch(from var(--color-light) l c h / calc(alpha * 0.25)); + } + + section[popover] { + text-align: initial; + } +</style> diff --git a/src/components/templates/CopyrightNotice.astro b/src/components/templates/CopyrightNotice.astro new file mode 100644 index 0000000..8335293 --- /dev/null +++ b/src/components/templates/CopyrightNotice.astro @@ -0,0 +1,70 @@ +--- +import { + CREATIVE_COMMONS_LICENSES, + type LICENSES, +} from "@lib/collection/schemas"; +import CC from "./licenses/CC.astro"; +import WTFPL from "./licenses/WTFPL.astro"; +import type { Person } from "@lib/collection/types"; + +export interface Props { + title: string; + holders: Person[]; + years: number[]; + license?: typeof LICENSES[number]; +} + +let { license = "public domain" } = Astro.props; + +let Notice = undefined; +if (license !== undefined) { + if (license === "WTFPL") { + Notice = WTFPL; + } else if ( + CREATIVE_COMMONS_LICENSES.some((x) => license.localeCompare(x) === 0) + ) { + Notice = CC; + } +} +--- + +{Notice && <div lang="en"><Notice {...Astro.props} /></div>} + +{ + /* + https://spdx.org/licenses/WTFPL.html + https://spdx.org/licenses/GFDL-1.3-or-later.html + https://spdx.org/licenses/FSFAP.html + https://artlibre.org/licence/lal/en/ + https://harmful.cat-v.org/software/ + + IPL-1.0 + IPA + Intel + HPND + EUPL-1.2 + EUPL-1.1 + EUDatagrid + EPL-2.0 + EPL-1.0 + EFL-2.0 + ECL-2.0 + CPL-1.0 + CPAL-1.0 + CDDL-1.0 + BSL-1.0 + BSD-3-Clause + BSD-2-Clause + Artistic-2.0 + APSL-2.0 + Apache-2.0 + Apache-1.1 + AGPL-3.0-or-later + AGPL-3.0-only + AFL-3.0 + AFL-2.1 + AFL-2.0 + AFL-1.2 + AFL-1.1 + */ +} diff --git a/src/components/templates/licenses/CC.astro b/src/components/templates/licenses/CC.astro new file mode 100644 index 0000000..2aea423 --- /dev/null +++ b/src/components/templates/licenses/CC.astro @@ -0,0 +1,153 @@ +--- +import type { ComponentProps } from "astro/types"; +import type CopyrightNotice from "@components/templates/CopyrightNotice.astro"; +import { listYearsWithRanges } from "@utils/datetime"; +interface Props extends ComponentProps<typeof CopyrightNotice> {} + +let { license, title, holders, years } = Astro.props; + +if (typeof license !== "string") throw new Error(); + +const publicdomain = license === "CC0"; +const sa = /SA/.test(license); +const nd = /ND/.test(license); +const nc = /NC/.test(license); +const licenseURL = `https://creativecommons.org/licenses/${ + license.slice(3).toLowerCase() +}/4.0/`; + +const firstYear = Math.min(...years); +const lastYears = years.sort((a, b) => a - b).slice(1); +--- + +<footer itemprop="copyrightNotice"> + { + publicdomain ? ( + <p> + <small> + <a href={Astro.url}>{title}</a> by { + holders.map(({ name, url }) => { + if (name === undefined) return undefined; + + const website = url?.[0]; + + return ( + <span + itemprop="copyrightHolder" + itemscope + itemtype="https://schema.org/Person" + >{ + website !== undefined ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={website} + content={website} + ><span itemprop="name">{name}</span></a> + ) : <span itemprop="name">{name}</span> + }</span> + ); + }) + } is marked <a + itemprop="license" + rel="license noreferrer" + target="_blank" + href="https://creativecommons.org/publicdomain/zero/1.0/" + content="https://creativecommons.org/publicdomain/zero/1.0/" + >CC0 1.0</a> + <img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + > + <img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/zero.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + > + </small> + </p> + ) : ( + <p> + <small> + <a href={Astro.url}>{title}</a> © <span itemprop="copyrightYear">{ + firstYear + }</span>, { + listYearsWithRanges(lastYears, { + list: { type: "unit", style: "narrow" }, + locale: "en", + }).replace(/^\d+/, "") + } by { + holders.map(({ name, url }) => { + if (name === undefined) return undefined; + + const website = url?.[0]; + + return ( + <span + itemprop="copyrightHolder" + itemscope + itemtype="https://schema.org/Person" + >{ + website !== undefined ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={website} + content={website} + ><span itemprop="name">{name}</span></a> + ) : <span itemprop="name">{name}</span> + }</span> + ); + }) + } is licensed under <a + itemprop="license" + rel="license noreferrer" + target="_blank" + href={licenseURL} + content={licenseURL} + >{license.replace("CC-", "CC ")} 4.0</a> + <img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + > + <img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/by.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + > + { + nc && ( + <img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + > + ) + } + { + sa && ( + <>{" "}<img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + ></> + ) + } + { + nd && ( + <>{" "}<img + alt="" + src="https://mirrors.creativecommons.org/presskit/icons/nd.svg" + style="max-width: 1em; max-height: 1em; margin-left: 0.2em" + ></> + ) + } + </small> + </p> + ) + } +</footer> diff --git a/src/components/templates/licenses/WTFPL.astro b/src/components/templates/licenses/WTFPL.astro new file mode 100644 index 0000000..d3546c7 --- /dev/null +++ b/src/components/templates/licenses/WTFPL.astro @@ -0,0 +1,70 @@ +--- +import { listYearsWithRanges } from "@utils/datetime"; +import type { Props as BaseProps } from "../CopyrightNotice.astro"; +interface Props extends BaseProps {} + +const { years, holders } = Astro.props; +const firstYear = Math.min(...years); +const lastYears = years.sort((a, b) => a - b).slice(1); +--- + +<footer itemprop="copyrightNotice"> + <p> + <small> + Copyright © <span itemprop="copyrightYear">{firstYear}</span>, { + listYearsWithRanges(lastYears, { + list: { type: "unit", style: "narrow" }, + locale: "en", + }).replace(/^\d+/, "") + } + + { + holders.map(({ name, url, email }) => { + if (name === undefined) return undefined; + + const website = url?.[0]; + + return ( + <span + itemprop="copyrightHolder" + itemscope + itemtype="https://schema.org/Person" + >{ + website !== undefined ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={website} + content={website} + ><span itemprop="name">{name}</span></a> + ) : <span itemprop="name">{name}</span> + } + { + email !== undefined && ( + <><<a + itemprop="email" + rel="author external noreferrer" + target="_blank" + href={`mailto:${email}`} + >{email}</a>></> + ) + }</span> + ); + }) + } + </small> + </p> + <p> + <small> + This work is free. You can redistribute it and/or modify it under the + terms of the Do What The Fuck You Want To Public License, Version 2, as + published by Sam Hocevar. See <a + itemprop="license" + href="http://www.wtfpl.net/" + rel="license noreferrer" + target="_blank" + >http://www.wtfpl.net/</a> for more details. + </small> + </p> +</footer> diff --git a/src/components/templates/signature/Commit.astro b/src/components/templates/signature/Commit.astro new file mode 100644 index 0000000..328a8f9 --- /dev/null +++ b/src/components/templates/signature/Commit.astro @@ -0,0 +1,86 @@ +--- +import { gitDir } from "@lib/git"; +import type { Commit } from "@lib/git/types"; +import { toIso8601Full } from "@utils/datetime"; + +type Props = { commit: Commit; lang: string }; + +const dir = await gitDir(); +const { hash, files, author, committer, signature } = Astro.props.commit; + +const formatter = new Intl.DateTimeFormat([Astro.props.lang], { + dateStyle: "short", + timeStyle: "short", +}); +--- + +<section> + <details> + <summary> + Informações sobre o último commit que modificou ficheiros relacionados a + este blog post: + </summary> + <dl class="divider"> + <dt>Hash</dt> + <dd><samp title={hash.long}>0x{hash.short.toUpperCase()}</samp></dd> + <dt>Ficheiros modificados</dt> + { + files.length > 0 + ? files.map((file) => ( + <dd><samp>{file.path.pathname.replace(dir.pathname, "")}</samp></dd> + )) + : <dd>Nenhum ficheiro modificado</dd> + } + <dt> + Autor (<time datetime={toIso8601Full(author.date)}>{ + formatter.format(author.date) + }</time>) + </dt> + <dd> + {author.name} <<a href={`mailto:${author.email}`}>{ + author.email + }</a>> + </dd> + <dt> + Commiter (<time datetime={toIso8601Full(committer.date)}>{ + formatter.format(committer.date) + }</time>) + </dt> + <dd> + {committer.name} <<a href={`mailto:${committer.email}`}>{ + committer.email + }</a>> + </dd> + { + signature && + ( + <dt>Assinatura do commit</dt> + <dd> + <dl> + <dt>Tipo</dt> + <dd><samp>{signature.type}</samp></dd> + <dt>Assinante</dt> + <dd>{signature.signer}</dd> + <dt>Fingerprint da chave</dt> + <dd><samp>0x{signature.key.short}</samp></dd> + </dl> + </dd> + ) + } + </dl> + </details> +</section> + +<style> + section { + font-size: smaller; + } + + dl { + margin-block: 0; + } + + details { + padding-block: 1rem; + } +</style> diff --git a/src/components/templates/signature/Downloads.astro b/src/components/templates/signature/Downloads.astro new file mode 100644 index 0000000..3497b37 --- /dev/null +++ b/src/components/templates/signature/Downloads.astro @@ -0,0 +1,63 @@ +--- +import { gitDir } from "@lib/git"; +import { get } from "@utils/anonymous"; + +interface Props { + lang: string; +} + +const { lang } = Astro.props; + +let source = new URL( + `${Astro.url.href.replace(/\/$/, "")}.md`, +); + +const dir = await gitDir(); + +const format: Intl.NumberFormatOptions = { + notation: "compact", + style: "unit", + unit: "byte", + unitDisplay: "narrow", +}; + +const formatter = new Intl.NumberFormat(lang, format); + +const sourceSize = formatter.format( + await Deno.stat( + new URL("public" + source.pathname, dir), + ).then(get("size")), +); +const sig = await Deno.stat( + new URL("public" + source.pathname + ".sig", dir), +).then(get("size")).catch(() => undefined); +const sigSize = formatter.format(sig); +--- + +<section> + <p>Ficheiros para descarregar:</p> + <dl> + <dt>Blog post</dt> + <dd> + <a + id="message" + href={source} + download + type="text/markdown; charset=utf-8" + ><samp>text/markdown</samp>, <samp>{sourceSize}</samp></a> + </dd> + { + sig && ( + <dt>Assinatura digital</dt> + <dd> + <a + id="signature" + href={`${source}.sig`} + download + type="application/pgp-signature" + ><samp>application/pgp-signature</samp>, <samp>{sigSize}</samp></a> + </dd> + ) + } + </dl> +</section> diff --git a/src/components/templates/signature/Signature.astro b/src/components/templates/signature/Signature.astro new file mode 100644 index 0000000..57e9902 --- /dev/null +++ b/src/components/templates/signature/Signature.astro @@ -0,0 +1,44 @@ +--- +import type { Verification } from "@lib/pgp/verify"; +import Summary from "./Summary.astro"; +import Downloads from "./Downloads.astro"; +import Commit from "./Commit.astro"; + +interface Props { + verification: Verification; + lang: string; +} + +const { verification, lang } = Astro.props; +const commit = await verification.commit; +--- + +<aside id="signatures"> + <p><strong>Verificação da assinatura digital</strong></p> + <Summary {...verification} /> + <Downloads {lang} /> + {commit && <Commit {commit} {lang} />} +</aside> + +<style is:global> + #signatures > section > p:first-child { + font-weight: bolder; + } +</style> +<style> + #signatures { + margin-inline: 1.5rem; + margin-block-end: 1.5rem; + box-shadow: 0 0 calc(1em) #e7e7e7; + border-radius: calc(1rem / 3); + padding: 1rem; + } + + #signatures > p:first-child { + font-size: larger; + + & > strong { + font-weight: bolder; + } + } +</style> diff --git a/src/components/templates/signature/Summary.astro b/src/components/templates/signature/Summary.astro new file mode 100644 index 0000000..3469a0f --- /dev/null +++ b/src/components/templates/signature/Summary.astro @@ -0,0 +1,279 @@ +--- +import { + createVerificationsSummary, + logLevel, + type Summary, + VerificationResult, +} from "@lib/pgp/summary"; +import type { Verification } from "@lib/pgp/verify"; +import { Level } from "@utils/index"; +import type { NonEmptyArray } from "@utils/iterator"; + +interface Props extends Verification {} + +let [errors, keys] = await createVerificationsSummary(Astro.props); +const failed = errors.filter((summary) => "reason" in summary); + +if (failed.length > 0) { + errors = failed as NonEmptyArray<Summary>; +} + +let worst; + +for (const summary of errors) { + if (worst === undefined) { + worst = summary; + } + + const { result } = summary; + const a = logLevel(worst.result); + const b = logLevel(result); + if (a[0] === b[0] && !a[1] && b[1]) { + worst = summary; + } else if (b[0] === Level.ERROR) { + worst = summary; + } else if (a[0] === Level.OK && b[0] === Level.WARN) { + worst = summary; + } +} + +let lvl: [Level, boolean] | undefined = undefined; + +let label; + +let title = ""; +let content; +const error = worst && "reason" in worst ? worst.reason : undefined; + +if (worst) { + lvl = logLevel(worst.result); + switch (lvl[0]) { + case Level.OK: { + label = "OK"; + break; + } + case Level.WARN: { + label = "Aviso"; + break; + } + case Level.ERROR: { + label = "Erro"; + break; + } + default: { + throw new Error("Unreachable"); + } + } + + switch (worst.result) { + case VerificationResult.NO_SIGNATURE: { + title = "Assinatura não encontrada"; + content = `<p> +Este blog post não foi assinado. +</p> +<p> +<strong>Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. +</p> +`; + break; + } + case VerificationResult.MISSING_KEY: { + title = "Chave não encontrada"; + content = `<p> +Este blog post está assinado digitalmente, porém a chave pública com <code>KeyID</code> <samp>0x${worst.keyID}</samp> com que foi assinado não foi encontrada no chaveiro sendo <strong>impossível verificar a assinatura, quer dizer, não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. +</p> +<p> +Procure a chave noutro sítio da internet para conseguir fazer a verificação manualmente. +</p> +`; + break; + } + case VerificationResult.SIGNATURE_CORRUPTED: { + title = "Assinatura corrumpida"; + content = `<p> +Exite um ficheiro que supostamente é a assinatura, mas ele está corrompido ou com um formato inválido. +</p> +<p> +<strong>Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. +</p> +`; + break; + } + case VerificationResult.SIGNATURE_COULD_NOT_BE_CHECKED: { + title = "Erro desconhecido"; + content = `<p> +A assinatura foi encontrada mas ocorreu um erro inesperado durante a verificação. +</p> +<p> +<strong>Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito</strong>. +</p> +`; + break; + } + case VerificationResult.BAD_SIGNATURE: { + title = "Assinatura inválida"; + content = `<p> +Existe uma assinatura digital porém o conteúdo da blog post não corresponde à assinatura. Talvez o texto tenha sido alterado sem ter sido criada uma nova assinatura. +</p> +<p> +Pode tentar verificar a assinatura com versões antigas do blog post, mas esta versão <strong> não pode ser verificada quanto à autentacidade do autor ou à integridade do texto escrito</strong>. +</p> +`; + break; + } + case VerificationResult.UNTRUSTED_KEY: { + title = "Assinatura válida (chave não confiada)"; + content = `<p> +A assinatura digital é criptograficamente válida, porém a chave utilizada não é suficientemente confiada pelo servidor. Mas podes ter a certeza que <strong>o dono da chave pública é a mesma pessoa que assinou este blog post</strong>. +</p> +`; + break; + } + case VerificationResult.TRUSTED_KEY: { + title = "Assinatura válida"; + content = `<p> +A assinatura digital é criptograficamente válida. <strong>O dono da chave pública é a mesma pessoa que assinou este blog post exatamente como ele está, sem alterações</strong>. +</p> +`; + break; + } + case VerificationResult.EXPIRATION_AFTER_SIGNATURE: { + break; + } + case VerificationResult.EXPIRATION_BEFORE_SIGNATURE: { + break; + } + case VerificationResult.REVOCATION_AFTER_SIGNATURE: { + break; + } + case VerificationResult.REVOCATION_BEFORE_SIGNATURE: { + break; + } + case VerificationResult.KEY_DOES_NOT_SIGN: { + break; + } + default: { + throw new Error("Unreachable"); + } + } +} +--- + +{ + lvl && + ( + <details + class:list={{ + ok: lvl[0] === Level.OK, + warn: lvl[0] === Level.WARN, + error: lvl[0] === Level.ERROR, + super: lvl[1], + }} + > + <summary>{label?.toUpperCase()}: {title.toUpperCase()}</summary> + <Fragment set:html={content} /> + {error && <pre><samp>{error}</samp></pre>} + </details> + ) +} + +<style> + pre { + overflow-x: auto; + } + details { + &.error { + --bg: #fff; + --fg: var(--color-active); + + &.super { + --bg: var(--color-active); + --fg: #fff; + } + } + + &.warn { + --bg: #fff; + --fg: #f46d43; + + &.super { + --bg: #f46d43; + --fg: #fff; + } + } + + &.ok { + --bg: #fff; + --fg: var(--color-visited); + + &.super { + --bg: var(--color-visited); + --fg: #fff; + } + } + + padding-inline: 0.5em; + padding-block: 0.5em; + + & > summary { + background-color: var(--bg); + padding-inline: 0.5em; + padding-block: calc(1em / 3); + color: var(--fg); + border-color: var(--fg); + border-width: 1px; + border-style: solid; + border-radius: calc(1em / 3); + font-weight: bolder; + + &:focus { + outline-color: var(--fg); + } + + &::marker { + color: var(--fg); + } + } + + & > :not(summary) { + padding-inline: 1em; + /* font-size: smaller; */ + } + + & > summary + * { + margin-block: 0.5em; + padding-block-start: 1em; + border-block-start: 1px solid var(--fg); + } + } + + @media (prefers-color-scheme: dark) { + details { + &.error { + --bg: #000; + + &.super { + --fg: #000; + } + } + + &.warn { + --bg: #000; + --fg: #f46d43; + + &.super { + --bg: #f46d43; + --fg: #000; + } + } + + &.ok { + --bg: #000; + + &.super { + --fg: #000; + } + } + } + } +</style> |