summaryrefslogtreecommitdiff
path: root/src/components/signature
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/signature')
-rw-r--r--src/components/signature/Authors.astro275
-rw-r--r--src/components/signature/Commit.astro86
-rw-r--r--src/components/signature/Downloads.astro63
-rw-r--r--src/components/signature/Signature.astro44
-rw-r--r--src/components/signature/Summary.astro279
5 files changed, 0 insertions, 747 deletions
diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro
deleted file mode 100644
index 4e52d4e..0000000
--- a/src/components/signature/Authors.astro
+++ /dev/null
@@ -1,275 +0,0 @@
----
-import { toPK } from "@lib/pgp";
-import { createKeyFromArmor } from "@lib/pgp/create";
-import type { Verification } from "@lib/pgp/verify";
-import { defined, get, instanciate } from "@utils/anonymous";
-import { z } from "astro:content";
-import type { EntityTypesEnum } from "src/consts";
-import qrcode from "yaqrcode";
-import type { getSigners } from "@lib/collection/helpers";
-
-interface Props {
- verifications: NonNullable<Verification["verifications"]>;
- expectedSigners: Awaited<ReturnType<typeof getSigners>>;
- commitSignerKey?: string;
-}
-
-const {
- verifications: verificationsPromise,
- expectedSigners,
- commitSignerKey,
-} = Astro.props;
-
-const fingerprintToData = new Map<
- string,
- { websites: URL[]; role: z.infer<typeof EntityTypesEnum> }
->();
-
-for (const { entity, role } of expectedSigners) {
- const key = await createKeyFromArmor(entity.data.publickey.armor);
- const fingerprint = key.getFingerprint();
- fingerprintToData.set(fingerprint, {
- websites: entity.data.websites?.map(instanciate(URL)) ?? [],
- role,
- });
-}
-
-let verifications = await Promise.all(
- verificationsPromise.map(async ({ key, keyID, userID, verified }) => {
- return {
- key: await key,
- keyID,
- userID: await userID,
- verified: await verified.catch(() => false),
- };
- }),
-);
-
-const expectedKeys = await Promise.all(
- expectedSigners.map(get("entity")).map(({ data }) =>
- createKeyFromArmor(data.publickey.armor)
- ),
-);
-
-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 e as suas chaves públicas.
- </p>
- </caption>
- <colgroup>
- <col />
- <col />
- </colgroup>
- <colgroup>
- <col />
- <col />
- <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>
- </tr>
- </thead>
- <tbody>
- {
- verifications.map(({ userID, key, keyID, verified }) => {
- const fingerprint = key
- ? toPK(key).getFingerprint()
- : undefined;
- const info = fingerprint
- ? fingerprintToData.get(fingerprint)
- : undefined;
- const primary = userID?.[0];
- let role = "";
- switch (info?.role) {
- case "author": {
- role = "Autor";
- break;
- }
- case "co-author": {
- role = "Co-autor";
- break;
- }
- case "translator": {
- role = "Tradutor";
- break;
- }
- }
- return (
- <tr>
- <th scope="row">
- <address
- itemprop="author"
- itemscope
- itemtype="https://schema.org/Person"
- >
- {
- primary?.name
- ? info?.websites[0] ? (
- <a
- itemprop="url"
- rel="author external noreferrer"
- target="_blank"
- href={info.websites[0]}
- ><span itemprop="name">{primary.name}</span></a>
- ) : <span itemprop="name">{primary.name}</span>
- : primary?.email
- ? (
- <>&lt;<a
- itemprop="email"
- rel="author external noreferrer"
- target="_blank"
- href={primary?.email && `mailto:${primary.email}`}
- >{primary?.email}</a>&gt;</>
- )
- : ""
- }
- {
- primary && (
- <>
- <button
- popovertarget={`user-id-${fingerprint}`}
- class="emoji"
- >
- ➕
- </button>
- <section
- class="user-id"
- popover
- id={`user-id-${fingerprint}`}
- >
- {
- userID && (
- <><p><code>UserID</code>s</p><ul>
- {userID.map((x) => <li>{x.userID}</li>)}
- </ul></>
- )
- }
- {
- info?.websites && (
- <><p>Websites</p><ul>
- {
- info.websites.map((
- x,
- ) => <li><a href={x}>{x}</a></li>)
- }
- </ul></>
- )
- }
- </section>
- </>
- )
- }
- </address>
- </th>
- <td>{role}</td>
- <td>
- <><span title={fingerprint?.replace(/(....)/g, "$1 ")}>{
- key
- ? "0x" + toPK(key).getKeyID().toHex()
- : "0x" + keyID.toHex()
- }</span>
- {
- key && false && (
- <img
- src={qrcode(toPK(key).armor(), {
- typeNumber: 40,
- errorCorrectLevel: "L",
- })}
- />
- )
- }
- {
- key &&
- (
- <button popovertarget={`armor-${fingerprint}`}>
- Armor
- </button>
- <section class="armor" popover id={`armor-${fingerprint}`}>
- <pre><code>{toPK(key).armor()}</code></pre>
- </section>
- )
- }
- </>
- </td>
- <td>{verified ? "✅" : "❌"}</td>
- <td>
- {
- commitSignerKey &&
- key?.getFingerprint().toUpperCase()?.endsWith(
- commitSignerKey.toUpperCase(),
- ) && "✅"
- }
- </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: #e7e7e7;
- }
-
- section[popover] {
- text-align: initial;
- }
- section[popover].armor {
- max-height: calc(200dvh / 3);
- }
- @media (prefers-color-scheme: dark) {
- tbody tr:nth-child(odd) {
- background-color: #181818;
- }
- }
-</style>
diff --git a/src/components/signature/Commit.astro b/src/components/signature/Commit.astro
deleted file mode 100644
index 328a8f9..0000000
--- a/src/components/signature/Commit.astro
+++ /dev/null
@@ -1,86 +0,0 @@
----
-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} &lt;<a href={`mailto:${author.email}`}>{
- author.email
- }</a>&gt;
- </dd>
- <dt>
- Commiter (<time datetime={toIso8601Full(committer.date)}>{
- formatter.format(committer.date)
- }</time>)
- </dt>
- <dd>
- {committer.name} &lt;<a href={`mailto:${committer.email}`}>{
- committer.email
- }</a>&gt;
- </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/signature/Downloads.astro b/src/components/signature/Downloads.astro
deleted file mode 100644
index 3497b37..0000000
--- a/src/components/signature/Downloads.astro
+++ /dev/null
@@ -1,63 +0,0 @@
----
-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/signature/Signature.astro b/src/components/signature/Signature.astro
deleted file mode 100644
index 57e9902..0000000
--- a/src/components/signature/Signature.astro
+++ /dev/null
@@ -1,44 +0,0 @@
----
-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/signature/Summary.astro b/src/components/signature/Summary.astro
deleted file mode 100644
index f25c1d1..0000000
--- a/src/components/signature/Summary.astro
+++ /dev/null
@@ -1,279 +0,0 @@
----
-import {
- createVerificationSummary,
- 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 createVerificationSummary(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>