Add PGP Signature to Hugo/Hexo/Any Static Site

Aspiration

One killer feature of my previous blog framework bm is the ability to generate a signature for every output file in the built static site. It’s the only site generator I know about that has this feature, no matter dynamic or static.

Despair engulfs me during searching the relevant info. There is none, except the blog of bm’s own author.1 And even himself doesn’t use it any more :(

I really miss this so I spent a whole night and implemented it myself by checking its source code (mostly globals.sh and set-programs.sh ).

Conformation

First, run hugo --gc / hexo clean && hexo generate / bm build or whatever command to generate the static site.

Then the idea is pretty straightforward:

  • Add public key to folders like static / assets, it depends on your static site generator (and theme)
  • Allow *.sig/*.asc file listing in the build folder by changing the relevant site config
  • Match everything you wanna sign
  • Sign them using GnuPG

Disposition

Change config

In Hugo, enable Canonicalization (the default value switched to false in v0.11, most likely you need to change it). If you want a more obvious (and ugly) URL to indicate the site layout, you can enable Ugly URLs as well.

# config.toml
# Allow gpg-signed pages
#uglyurls = true
canonifyURLs = true

In Hexo, check Asset Folders or Date Files . You can prettify/uglify URL by changing Permalinks in _config.yml.

Find all pages

This is pretty simple, as with prettify URL enabled in Hugo, all pages are named index.html2. It’s possible to extend of course, as long as you know how to write the proper Regex.

SIG_FILES=$(find public -maxdepth 5 -name '*.html' -type f)

Explanation:

  • public: the site build folder. It can also be build (bm), _site (Jekyll) or any obscure destdir you set
  • -maxdepth 5: it depends on the build folder structure, check it using tree ./public
  • -name '*.html': match all files end with html extension. In Hugo, simply use -name index.html if prettify URL is enabled
  • -type f: match only file (not symbolic link, directory etc)

Make GPG & SSH be normal

This is the most difficult part for me: GPG always complains about non-tty environment, even if I add --no-tty param. It may be a WSL/bash profile issue idk. The good thing is I finally make it work after wasting like two hours on confusing GnuPG or OpenSSH setting.

For GnuPG, add the following to .bashrc or /etc/bash/bashrc3, which ensures GPG always knows which tty to use:

GPG_TTY=$(tty)
export GPG_TTY

For OpenSSH, add the follow to .ssh/config4, which does the same thing, but for SSH:

Match host * exec "gpg-connect-agent UPDATESTARTUPTTY /bye"

Sign pages

To sign a single page:

# Sign one page
gpg --local-user {GPG_FINGERPRINT} --armor --detach-sign public/index.html

To sign all pages, read ${SIG_FILES} line by line and do it in a while loop5:

# Sign all pages
while IFS= read -r line
do
   gpg --local-user {GPG_FINGERPRINT} --armor --detach-sign "$line"
done < <(printf '%s\n' "$SIG_FILES")

Conclusion

Put it together, and we’ve got the following script. It’s also possible to add lines like GPG_SIGN = true and GPG_FINGERPRINT = AWESOME0721 to config.toml and read them in the script.

PS: If you want to hide system timezone when signing with GPG, just append TZ=UTC in the beginning. This will NOT change the time & WILL still expose tz through metadata analysis anyway.

#!/bin/bash

# Make sure gpg knows which tty to use,
# or it'll complain like `curse not a tty xterm-256color` forever
export GPG_TTY=$(tty)
gpg-connect-agent updatestartuptty /bye >/dev/null

# Garbage clean
rm -r ./public/*
hugo --gc
# Find all pages, change "5" according to site content
SIG_FILES=$(find public -maxdepth 5 -name '*.html' -type f)

# Sign one page
gpg --local-user {GPG_FINGERPRINT} --armor --detach-sign public/index.html

# Sign all pages
while IFS= read -r line
do
   gpg --local-user {GPG_FINGERPRINT} --armor --detach-sign "$line"
done < <(printf '%s\n' "$SIG_FILES")

echo "Successfully signed all pages."

Notification

If you use serverless services like Vercel or CI like GitHub Actions, AND you tend to minify the resources, make sure they are minified BEFORE signing as otherwise the signature verification is bound to fail (as the original file is changed since you signed them).

Vinfall's Geekademy

Sine īrā et studiō