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.html
2.
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 bebuild
(bm),_site
(Jekyll) or any obscure destdir you set-maxdepth 5
: it depends on the build folder structure, check it usingtree ./public
-name '*.html'
: match all files end withhtml
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/bashrc
3,
which ensures GPG always knows which tty to use:
GPG_TTY=$(tty)
export GPG_TTY
For OpenSSH, add the follow to .ssh/config
4,
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).