summaryrefslogtreecommitdiff
path: root/src/components/templates/signature
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/templates/signature')
-rw-r--r--src/components/templates/signature/Commit.astro86
-rw-r--r--src/components/templates/signature/Downloads.astro63
-rw-r--r--src/components/templates/signature/Signature.astro44
-rw-r--r--src/components/templates/signature/Summary.astro279
4 files changed, 472 insertions, 0 deletions
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} &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/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>