Signing HTML

As this site is all static HTML, I wanted to find a way to automatically add PGP signatures to each page, so that anybody could verify a page was actually published by me. While it’s possible to have inline signing for HTML documents, I didn’t like any of the systems I found, and decided it would be cleaner to use detached signatures in separate .sig or .asc files, as often done with binary files and software releases. I found a way to do this that I really like that just uses GNU Make, some Git hooks, and a little Bash.

The below assumes that you already have a private key set up with GnuPG, and know the basics of signing and verifying files. I’ve written a separate article about basic GnuPG key setup.


Creating the detached ASCII signature for each HTML file in the web root is straightforward:

$ gpg --armor --sign-detach public_html/document.html

I’m prompted by the GnuPG agent for my passphrase, and the document is signed. This creates a new file public_html/document.html.asc. Anyone with my key can then use this to verify the document:

gpg: Signature made Fri 29 Dec 2017 13:56:53 NZDT
gpg:                using RSA key 317990A14597A1FCF82D953AB5AF5F8925926609
gpg: Good signature from "Thomas Ryder (tyrmored, tejr) <>" [ultimate]


Of course, I don't want to have to do all that manually each time I change a file, so I use a Makefile to generate new and changed signatures automatically each time I run make(1). The file looks like this; I think it only works with GNU Make:

HTMLS = $(shell find public_html -type f -name \*.html)
SIGS = $(patsubst %.html,%.html.asc,$(HTMLS))
all: $(SIGS)
%.html.asc: %.html
        gpg --armor --detach-sign $<

The result is that whenever I add or edit an HTML file, I need only type make, and any new or updated files have their signatures rebuilt automatically.

Git pre-commit hook

To syntax-check HTML files with tidy(1) and to enforce valid signatures for all HTML files, I automated it with a Git pre-commit hook, in .git/hooks/pre-commit in my repository root:

#!/usr/bin/env bash

# Need extglob for the +(0) bit
shopt -s extglob

# Start counting errors
declare -i errors

# Read records supplied by git diff-index, null-terminated; we only want the
# sha1 object name of the staged file
while read -r -d '' _ _ _ sha1 _ ; do

    # git diff-files has a NULL both before and after the filename it prints,
    # so we need to run read again to get the filename out to move on to the
    # next record (this is a bit weird, but it does seem to work consistently)
    read -r -d '' filename _

    # Skip the file if its digest is empty or is all zeroes, the latter being
    # how diff-index shows deleted files or moves
    [[ $sha1 ]] || continue
    [[ $sha1 != +(0) ]] || continue

    # Skip the file if it's an ignored path
    [[ $filename != public_html/blinkenlights/* ]] || continue

    # Skip the file if it's not HTML
    [[ $filename == *.html ]] || continue

    # Show that we're checking it
    printf 'Checking modified %s ... \n' "$filename"

    # Check HTML formatting
    git cat-file -p "$sha1" |
    tidy -eq -utf8 ||

    # Check ASCII signature up to date
    git cat-file -p "$sha1" |
    gpg --verify -- "$filename".asc "$filename" >/dev/null ||

# Standard input for the while loop is here
done < <(git diff-index -z --cached HEAD)

# Exit 0 if there were no errors, 1 if there were
exit "$((errors > 0))"

I make this executable with chmod +x .git/hooks/pre-commit, and it all works. The result is that just as I’m running git commit, git iterates through all of the HTML files to make sure they all have a valid signature. If any of the checks fail, errors are accrued in the errors count, and the non-zero exit value means the commit won’t go through until I fix it by running make(1) and entering my passphrase for the signatures.