From f9a77c5c27aede4e5978eb55d9b7af781b680a1d Mon Sep 17 00:00:00 2001 From: João Augusto Costa Branco Marado Torres Date: Tue, 24 Jun 2025 12:08:41 -0300 Subject: feat!: initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Augusto Costa Branco Marado Torres --- src/components/signature/Authors.astro | 281 +++++++++++++++++++++++++++++++ src/components/signature/Commit.astro | 87 ++++++++++ src/components/signature/Downloads.astro | 63 +++++++ src/components/signature/Signature.astro | 44 +++++ src/components/signature/Summary.astro | 279 ++++++++++++++++++++++++++++++ 5 files changed, 754 insertions(+) create mode 100644 src/components/signature/Authors.astro create mode 100644 src/components/signature/Commit.astro create mode 100644 src/components/signature/Downloads.astro create mode 100644 src/components/signature/Signature.astro create mode 100644 src/components/signature/Summary.astro (limited to 'src/components/signature') diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro new file mode 100644 index 0000000..43a2b36 --- /dev/null +++ b/src/components/signature/Authors.astro @@ -0,0 +1,281 @@ +--- +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 { type CollectionEntry, z } from "astro:content"; +import type { EntityTypesEnum } from "src/consts"; +import qrcode from "yaqrcode"; + +interface Props { + verifications: NonNullable; + expectedSigners: { + entity: CollectionEntry<"entity">; + role: z.infer; + }[]; + commitSignerKey?: string; +} + +const { + verifications: verificationsPromise, + expectedSigners, + commitSignerKey, +} = Astro.props; + +const fingerprintToData = new Map< + string, + { websites: URL[]; role: z.infer } +>(); + +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(), + ]}`, + ); +} +--- + +
+ + + + + + + + + + + + + + + + + + + + + + { + 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 ( + + + + + + + + ); + }) + } + +
+ Assinaturas +

+ Para verificar uma assinatura é necessário a mensagem, a assinatura digital e as chaves públicas dos assinantes. Esta tabela mostra algumas + informações sobre os assinantes e as suas chaves públicas. +

+
AssinanteFunçãoFingerprintVálidoCommiter
+ + {role} + <>{ + key + ? "0x" + toPK(key).getKeyID().toHex() + : "0x" + keyID.toHex() + } + { + key && false && ( + + ) + } + { + key && + ( + +
+
{toPK(key).armor()}
+
+ ) + } + +
{verified ? "✅" : "❌"} + { + commitSignerKey && + key?.getFingerprint().toUpperCase()?.endsWith( + commitSignerKey.toUpperCase(), + ) && "✅" + } +
+
+ + diff --git a/src/components/signature/Commit.astro b/src/components/signature/Commit.astro new file mode 100644 index 0000000..9cc997a --- /dev/null +++ b/src/components/signature/Commit.astro @@ -0,0 +1,87 @@ +--- +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", +}); +--- + +
+
+ + Informações sobre o último commit que modificou ficheiros relacionados a + este blog post: + +
+
Hash
+
0x{hash.short.toUpperCase()}
+
Ficheiros modificados
+ { + files.length > 0 + ? files.map((file) => ( +
{file.path.pathname.replace(dir.pathname, "")}
+ )) + :
Nenhum ficheiro modificado
+ } +
+ Autor () +
+
+ {author.name} <{ + author.email + }> +
+
+ Commiter () +
+
+ {committer.name} <{ + committer.email + }> +
+ { + signature && + ( +
Assinatura do commit
+
+
+
Tipo
+
{signature.type}
+
Assinante
+
{signature.signer}
+
Fingerprint da chave
+
0x{signature.key.short}
+
+
+ ) + } +
+
+
+ + diff --git a/src/components/signature/Downloads.astro b/src/components/signature/Downloads.astro new file mode 100644 index 0000000..ac8215f --- /dev/null +++ b/src/components/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("read/", "").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); +--- + +
+

Ficheiros para descarregar:

+
+
Blog post
+
+ text/markdown, {sourceSize} +
+ { + sig && ( +
Assinatura digital
+
+ application/pgp-signature, {sigSize} +
+ ) + } +
+
diff --git a/src/components/signature/Signature.astro b/src/components/signature/Signature.astro new file mode 100644 index 0000000..57e9902 --- /dev/null +++ b/src/components/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; +--- + + + + + diff --git a/src/components/signature/Summary.astro b/src/components/signature/Summary.astro new file mode 100644 index 0000000..6ab6bf5 --- /dev/null +++ b/src/components/signature/Summary.astro @@ -0,0 +1,279 @@ +--- +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; +} + +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 = `

+Este blog post não foi assinado. +

+

+Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito. +

+`; + break; + } + case VerificationResult.MISSING_KEY: { + title = "Chave não encontrada"; + content = `

+Este blog post está assinado digitalmente, porém a chave pública com KeyID 0x${worst.keyID} com que foi assinado não foi encontrada no chaveiro sendo impossível verificar a assinatura, quer dizer, não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito. +

+

+Procure a chave noutro sítio da internet para conseguir fazer a verificação manualmente. +

+`; + break; + } + case VerificationResult.SIGNATURE_CORRUPTED: { + title = "Assinatura corrumpida"; + content = `

+Exite um ficheiro que supostamente é a assinatura, mas ele está corrompido ou com um formato inválido. +

+

+Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito. +

+`; + break; + } + case VerificationResult.SIGNATURE_COULD_NOT_BE_CHECKED: { + title = "Erro desconhecido"; + content = `

+A assinatura foi encontrada mas ocorreu um erro inesperado durante a verificação. +

+

+Não existe forma de verificar a autentacidade do autor ou a integridade do texto escrito. +

+`; + break; + } + case VerificationResult.BAD_SIGNATURE: { + title = "Assinatura inválida"; + content = `

+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. +

+

+Pode tentar verificar a assinatura com versões antigas do blog post, mas esta versão não pode ser verificada quanto à autentacidade do autor ou à integridade do texto escrito. +

+`; + break; + } + case VerificationResult.UNTRUSTED_KEY: { + title = "Assinatura válida (chave não confiada)"; + content = `

+A assinatura digital é criptograficamente válida, porém a chave utilizada não é suficientemente confiada pelo servidor. Mas podes ter a certeza que o dono da chave pública é a mesma pessoa que assinou este blog post. +

+`; + break; + } + case VerificationResult.TRUSTED_KEY: { + title = "Assinatura válida"; + content = `

+A assinatura digital é criptograficamente válida. O dono da chave pública é a mesma pessoa que assinou este blog post exatamente como ele está, sem alterações. +

+`; + 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 && + ( +
+ {label?.toUpperCase()}: {title.toUpperCase()} + + {error &&
{error}
} +
+ ) +} + + -- cgit v1.2.3