You have a collection of files that you want to sign with GPG. Should you sign each file individually, sign a file containing a list of SHA-256 hashes, or do something different altogether? Which method you choose has important consequences for security.

Problem 1: File name integrity

Signing files one at a time signs the contents of the files. It (typically) does not protect the file names from tampering. This could be disastrous in some situations (e.g. an attacker could change blacklist.txt to whitelist.txt).

Problem 2: Set membership integrity

Signing individual files does not prevent adding or removing files from the set of files that were transmitted. In some cases, this is important: if you’re transmitting a set of log files, an attacker could selectively remove one that captured incriminating information, without that change being detected. If an attacker has the opportunity to see multiple transmissions, they could add files from an old transmission to the new transmission (essentially a variant on replay attacks). The files would still have valid signatures individually, so the additions would not be detected. If you’re transmitting a set of user accounts (one per file), an attacker could revert their account to an earlier state (e.g. back to having admin status).

What should you do?

Given the above, I recommend protecting the transmission as a whole instead of part-by-part. There are too many things that can go wrong when each file is signed individually. The easiest way of getting everything right is to create a tarball or zip file of the files and sign the entire archive:

# Create a zip file
$ zip file1 file2 file3

# Sign the zip file
$ gpg --detach-sign

The above commands will create a GPG signature file that can be used to verify that has not been tampered with. When receiving the zip file, just verify the signature before unzipping:

# Verify signature
$ gpg --verify
# gpg should output 'Good signature from X'

# Unzip files
$ unzip

An alternative

This may not be possible or practical if, for example, zipping the files would be too resource-intensive. In that case, it is still possible to avoid most of the above problems. Do not sign the files individually. Instead, produce a SHA256SUMS file containing a list of file names and corresponding hashes, and sign that file. The sha256sum command on Linux makes this easy:

# Save file hashes to SHA256SUMS file
$ sha256sum file1 file2 file3 > SHA256SUMS

# Create SHA256SUMS.sig
$ gpg --detach-sign SHA256SUMS

# Transmit the files, SHA256SUMS, and SHA256SUMS.sig

When it’s time to check the signature:

# First, verify the GPG signature on SHA256SUMS
$ gpg --verify SHA256SUMS.sig
# should output 'Good signature from X'

# Next, verify the SHA-256 hashes
$ sha256sum --check SHA256SUMS
# should output 'fileX: OK' for each file

Make sure that you use sha256sum and not the older sha1sum or md5sum, as those hash functions are no longer safe to use.

There’s one catch with this method: sha256sum won’t notice if there are other files in the current directory that are not listed in the SHA256SUMS file. If an attacker adds a malicious file to the files you received, sha256sum won’t detect that anything is wrong, since it only checks the files whose hashes are listed. Consider whether this problem actually affects the security of your application: you may need to add additional security checks. This is why I generally recommend working with signed zip files instead.