feat: add sha256 and pgp verification

This commit is contained in:
Alexander Daichendt 2025-01-03 09:38:16 +01:00
parent a155a9b355
commit 799cf0611c
12 changed files with 433 additions and 67 deletions

View file

@ -1,6 +1,5 @@
<form
method="POST"
action="/admin/api/verifications"
class="flex flex-col gap-4 max-w-lg mx-auto p-6 rounded-lg shadow-md
bg-white dark:bg-gray-800 transition-colors"
>
@ -69,7 +68,7 @@
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
focus:outline-none
placeholder:text-gray-400 dark:placeholder:text-gray-500"
>Application
>Job Application
</textarea>
</div>

View file

@ -0,0 +1,101 @@
---
import { Icon } from "astro-icon/components";
const { uid } = Astro.props;
---
<div
class="max-w-lg mx-auto p-6 rounded-lg shadow-md bg-white dark:bg-gray-800 transition-colors mt-8"
>
<h2 class="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
Generated UID
</h2>
<div class="relative">
<div class="flex items-center gap-2 mb-1">
<code
class="uid-code bg-gray-50 dark:bg-gray-900 px-3 py-2 rounded-md text-gray-900 dark:text-white flex-grow font-mono"
>
{uid}
</code>
<button
class="copy-button p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white
bg-gray-50 dark:bg-gray-900 rounded-md
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
title="Copy to clipboard"
>
<Icon name="mdi:content-copy" />
</button>
</div>
</div>
<p class="text-gray-700 dark:text-gray-200 mb-4 mt-6">
Embed this uid in the CV typst document. After that, calculate the sha256sum
and enter it below along with your GPG signature
</p>
<form method="POST" class="flex flex-col gap-4">
<input type="hidden" name="uid" value={uid} />
<div class="flex flex-col gap-2">
<label for="sha256" class="font-medium text-gray-700 dark:text-gray-200">
SHA256 Hash
</label>
<input
type="text"
id="sha256"
name="sha256"
required
class="border rounded-md p-2
bg-gray-50 dark:bg-gray-900
text-gray-900 dark:text-white
border-gray-300 dark:border-gray-600
focus:border-blue-500 dark:focus:border-blue-400
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
focus:outline-none
placeholder:text-gray-400 dark:placeholder:text-gray-500"
/>
</div>
<div class="flex flex-col gap-2">
<label for="gpg" class="font-medium text-gray-700 dark:text-gray-200">
PGP Signature
</label>
<textarea
id="pgp_signature"
name="pgp_signature"
required
rows="4"
placeholder="Paste your PGP signature here"
class="border rounded-md p-2
bg-gray-50 dark:bg-gray-900
text-gray-900 dark:text-white
border-gray-300 dark:border-gray-600
focus:border-blue-500 dark:focus:border-blue-400
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
focus:outline-none
placeholder:text-gray-400 dark:placeholder:text-gray-500
resize-y"
></textarea>
</div>
<button
type="submit"
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600
text-white py-2 px-4 rounded-md
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
disabled:opacity-50 disabled:cursor-not-allowed"
>
Submit Hash and Signature
</button>
</form>
</div>
<script>
const buttons = document.querySelectorAll(".copy-button");
buttons.forEach((button) => {
button.addEventListener("click", async () => {
const uid = document.querySelectorAll(".uid-code")[0]?.textContent;
if (uid) {
await navigator.clipboard.writeText(uid);
}
});
});
</script>

View file

@ -1,6 +1,7 @@
---
import type { InferSelectModel } from "drizzle-orm";
import type { cvTable } from "../../db/schema";
import { Icon } from "astro-icon/components";
interface Props {
cv: InferSelectModel<typeof cvTable>;
@ -9,7 +10,7 @@ interface Props {
const { cv } = Astro.props;
---
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6">
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6 mb-4">
<ul class="space-y-4 mb-0">
<li class="flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-2">
<span
@ -61,5 +62,131 @@ const { cv } = Astro.props;
{cv.purpose}
</span>
</li>
<li class="flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-2">
<span
class="text-gray-500 dark:text-gray-400 sm:min-w-32 whitespace-nowrap"
>
SHA256:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium break-all">
{cv.sha256}
</span>
</li>
</ul>
</div>
<div class="">
<pre
id="signature"
class="p-6 rounded-lg overflow-x-auto text-sm font-mono whitespace-pre-wrap mb-0">{cv.pgp_signature}</pre>
</div>
<div class="mt-8">
<details class="group bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<summary class="flex items-center justify-between p-4 cursor-pointer">
<h3
class="flex items-center text-xl font-semibold text-gray-900 dark:text-gray-100 mb-0"
>
<Icon name="mdi:information-outline" class="mr-2" />
Verification Instructions
</h3>
<span class="relative ml-4 flex-shrink-0">
<svg
class="h-5 w-5 text-gray-500 dark:text-gray-400 transform group-open:rotate-180 transition-transform"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</summary>
<div class="p-6 pt-6 space-y-6">
<div>
<h4 class="text-xl font-bold mb-2 text-gray-800 dark:text-gray-200">
1. Verify SHA256 Hash
</h4>
<p class="text-gray-600 dark:text-gray-400 mb-2">
Compare the output of following command with the SHA256 hash shown
above.
</p>
<pre
class="bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 p-3 rounded text-sm font-mono">shasum -a 256 cv.pdf</pre>
</div>
<div>
<h4 class="text-xl font-bold mb-2 text-gray-800 dark:text-gray-200">
2. Verify PGP Signature
</h4>
<ol
class="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 mb-2"
>
<li>
Download Alexander Daichendt's public pgp key from <a
href="https://daichendt.one/pub.key"
class="text-blue-500 dark:text-blue-400 hover:underline">here</a
>
</li>
<li>
Import the key into your keyring with
<pre
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-sm font-mono sm:ml-6 mb-2">gpg --import ~/Downloads/pub.key</pre>
<li>
Download the above PGP signature by clicking <button
class="text-blue-500 dark:text-blue-400 hover:underline"
id="download-pgp"
>here
</button>
</li>
<li>Run the following command:</li>
</li>
<pre
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-sm font-mono sm:ml-6">gpg --verify signature.asc cv.pdf</pre>
<p class="text-gray-600 dark:text-gray-400 mt-2">
If the verification is successful, GPG will indicate so.
<pre
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs font-mono mt-2">gpg: Good signature from &quot;Alexander Daichendt &lt;alexander@daichendt.one&gt;&quot; [ultimate]</pre>
</p>
</ol>
</div>
</div>
</details>
</div>
<style>
details summary::-webkit-details-marker {
display: none;
}
</style>
<script>
const pgpElements = document.querySelectorAll("#download-pgp");
pgpElements.forEach((pgpElement) => {
pgpElement.addEventListener("click", () => {
console.log("Downloading PGP signature");
const pgpSignature = document.getElementById("signature")?.innerText;
if (!pgpSignature) {
console.error("No PGP signature found");
return;
}
const blob = new Blob([pgpSignature], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "signature.asc";
a.click();
URL.revokeObjectURL(url);
});
});
</script>

View file

@ -13,7 +13,9 @@
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400">Revoked CV</h2>
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400 mb-0">
Revoked CV
</h2>
</div>
<p class="text-red-600 dark:text-red-400 text-lg">
This CV has been revoked and is no longer valid.

View file

@ -24,7 +24,7 @@ const { cv } = Astro.props;
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100">
<h2 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-0">
Verified CV
</h2>
</div>