script: add a script to hash files

Right now, we provide signed SHA-256 hashes for our releases.  This is
fine and sufficient, and also cryptographically secure.  However, many
distributors use other algorithms, and it would be convenient if we
could provide easy access to those hashes as well.  For example, NetBSD
uses SHA-512 and BLAKE2s.

Let's add a script to hash files with various algorithms and output them
in the BSD format.  The advantage of the BSD format over the traditional
GNU format is that it includes the hash algorithm, which allows us to
distinguish between hashes of the same length, such as SHA-256,
SHA-512/256, and SHA3-256.  It is generated by shasum, sha*sum, sha3sum,
and b2sum with the --tag format, and all of these programs accept it for
verification with no problems.

Using the BSD format means that we need only provide one additional file
with all the additional algorithms.  There is therefore no need to add
multiple new files, and if we desire to add additional algorithms in the
future, that's easily done without modification.

For aesthetics, we sort first by hash name and then by filename in the
output.  Unlike sorting with `sort`, this keeps the SHA-2 and SHA-3
algorithms separate instead of interspersing them, which aids in
reading.  Add some comments because the algorithm, while logical,
is somewhat subtle.
This commit is contained in:
brian m. carlson 2022-04-11 20:11:33 +00:00
parent bb25ccf879
commit c7241259f4
No known key found for this signature in database
GPG Key ID: 2D0C9BC12F82B3A1

59
script/hash-files Executable file

@ -0,0 +1,59 @@
#!/usr/bin/env ruby
require "openssl"
# This maps the OpenSSL name to the name used in the output file.
# The order used is the order they should appear in the output file.
DIGESTS = {
'BLAKE2b512' => 'BLAKE2b',
'BLAKE2s256' => 'BLAKE2s',
'SHA256' => 'SHA256',
'SHA384' => 'SHA384',
'SHA512' => 'SHA512',
'SHA512-256' => 'SHA512/256',
'SHA3-256' => 'SHA3-256',
'SHA3-384' => 'SHA3-384',
'SHA3-512' => 'SHA3-512',
}
class Hasher
def initialize(file)
@file = file
@hashers = DIGESTS.map do |openssl, output|
[output, OpenSSL::Digest.new(openssl)]
end.to_h
end
def update(s)
@hashers.values.each { |h| h.update(s) }
end
def to_a
@hashers.map do |name, ctx|
"#{name} (#{@file}) = #{ctx.digest.unpack("H*")[0]}\n"
end.to_a
end
end
results = []
ARGV.each do |file|
f = File.open(file)
h = Hasher.new(file)
while chunk = f.read(65536) do
h.update(chunk)
end
results += h.to_a
end
# Sort entries first by order of algorithm name in DIGESTS, then by filename,
# then print them.
# Create a mapping of output name digest to order in the hash.
names = DIGESTS.values.each_with_index.to_a.to_h
results.sort_by do |s|
# Split into digest name and remainder. The remainder starts with the
# filename.
pair = s.split(' ', 2).to_a
# Order by the index of the digest and then the filename.
[names[pair[0]], pair[1]]
end.each { |l| puts l }