Secure file verification with Ruby and SHA384
To verify a download in Ruby, hash its bytes with OpenSSL’s SHA384 digest and compare the result with a checksum you trust. The script below accepts one or more file/checksum pairs and returns a nonzero exit status if any check fails, so you can use it in an automated job.
Start with a trusted checksum
SHA-384 belongs to the SHA-2 family and produces a 384-bit digest: 48 bytes, or 96 hexadecimal characters. Use it when your publisher or existing workflow supplies SHA384 checksums; a reference made with SHA256 cannot be checked using SHA384.
Get the expected checksum from a source you trust, such as the publisher’s authenticated HTTPS release page or a signed checksum manifest whose signature you have verified. If someone can replace both the file and its reference checksum, they can make your check pass. Hashing detects changes against that reference; it does not make a file tamper-proof or establish that it is safe to execute. Ruby’s OpenSSL documentation explains how digests also form part of digital signatures.
Check Ruby and OpenSSL
You need Ruby with its OpenSSL extension. The commands here were tested on Linux with Ruby 3.4.10,
Ruby/OpenSSL 3.3.3, and OpenSSL 3.6.4. The shell examples use Bash; sha384sum from GNU Coreutils is
optional for the independent comparison below. If Ruby is missing, follow the official
Ruby installation guide for your platform.
Check the interpreter, linked library, and SHA384 availability before continuing:
ruby -ropenssl -e 'puts RUBY_DESCRIPTION; puts "Ruby/OpenSSL #{OpenSSL::VERSION}"; puts OpenSSL::OPENSSL_LIBRARY_VERSION; puts "SHA384: #{OpenSSL::Digest.new("SHA384").digest_length} bytes"'
The final line should be SHA384: 48 bytes. If loading OpenSSL or creating the digest fails, fix
your Ruby installation before running the verifier. The openssl command on your PATH can use a
different library from Ruby, so its version alone does not establish that this prerequisite works.
Save the verifier
Save this complete script as verify_sha384.rb. It reads each file in 8 KiB chunks and reuses the
buffer, so it does not load the whole file into memory. Ruby’s
IO#read returns nil at end-of-file when
given a positive length, including for an empty file. Each chunk is passed to
OpenSSL::Digest#update.
require 'openssl'
def sha384_hash(path)
digest = OpenSSL::Digest.new('SHA384')
File.open(path, 'rb') do |file|
buffer = ''.b
digest.update(buffer) while file.read(8192, buffer)
end
digest.hexdigest
end
if ARGV.empty? || ARGV.length.odd?
warn 'Usage: ruby verify_sha384.rb FILE SHA384 [FILE SHA384 ...]'
exit 2
end
failed = false
ARGV.each_slice(2) do |path, expected|
unless /\A[0-9a-fA-F]{96}\z/.match?(expected)
warn "#{path}: ERROR (expected 96 hexadecimal characters)"
failed = true
next
end
begin
actual = sha384_hash(path)
if actual == expected.downcase
puts "#{path}: OK"
else
warn "#{path}: FAILED (SHA384 mismatch)"
failed = true
end
rescue SystemCallError, IOError => error
warn "#{path}: ERROR (#{error.message})"
failed = true
end
end
exit(failed ? 1 : 0)
Pass the digest alone, without a filename, sha384- prefix, or surrounding whitespace. Uppercase
hexadecimal letters are accepted. The script compares public checksums with ==; there is no secret
checksum to protect from timing disclosure in this workflow. For secret authenticators such as
HMACs, Ruby provides constant-time comparison methods
instead of requiring a handwritten comparison loop.
Verify a file with a known result
In a scratch directory containing verify_sha384.rb, run the following commands in Bash. They create
example.txt containing exactly the three bytes abc, with no trailing newline. Creation refuses
to overwrite an existing file; if it fails, the && chain stops before verification. Use another
scratch directory if you want to repeat the setup.
ruby -e 'File.open("example.txt", "wbx") { |file| file.write("abc") }' &&
EXPECTED_SHA384='cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7' &&
ruby verify_sha384.rb example.txt "$EXPECTED_SHA384"
The result is:
example.txt: OK
This exits with status 0. For your own download, substitute its path and the trusted SHA384
checksum for that exact release. Quote paths containing spaces. The verifier opens files for
reading only and creates no verification artifact.
If you have GNU Coreutils, independently calculate the sample’s checksum with sha384sum:
sha384sum --binary -- example.txt
Its first field should equal EXPECTED_SHA384. Calculating a hash from a file you just downloaded
is useful for comparison, but does not by itself give you a trusted reference for that download.
Make a failed batch fail the job
Keep using the same Bash session so EXPECTED_SHA384 remains set. Create an empty file and check
both samples by passing another file/checksum pair:
ruby -e 'File.open("empty.bin", "wbx") {}' &&
EMPTY_SHA384='38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b' &&
ruby verify_sha384.rb example.txt "$EXPECTED_SHA384" empty.bin "$EMPTY_SHA384"
Both files should report OK, and the command exits with 0. Creating empty.bin also refuses
an existing destination. Now append a newline to the disposable first sample and repeat the batch:
ruby -e 'File.open("example.txt", "ab") { |file| file.write("\n") }' &&
ruby verify_sha384.rb example.txt "$EXPECTED_SHA384" empty.bin "$EMPTY_SHA384"
example.txt reports FAILED (SHA384 mismatch) on standard error, while empty.bin still reports
OK on standard output. The batch exits with 1: a later success cannot erase an earlier failure.
The script checks every supplied pair and does not rewrite either reference checksum.
The exit codes are 0 when every file matches, 1 when any file mismatches, has a malformed
expected checksum, or cannot be read, and 2 for a missing or incomplete list of pairs. In a job,
make the verifier the final command in its step, or explicitly propagate its nonzero status before
running later commands. Printing an error message alone does not fail a shell job.
Diagnose a failed check
- Expected 96 hexadecimal characters: copy only the SHA384 digest. A SHA256 checksum, Base64
value, or entire
sha384sumoutput line is not a valid argument. - SHA384 mismatch: check the release, filename, download completeness, and expected algorithm. Do not replace the reference with the new hash just to make verification pass.
- File or permission error: check the working directory, path, and read permissions of the
account running the job. A failed read returns
1even if other files pass.
Preserve the bytes you intend to verify
Binary mode preserves the bytes supplied to the digest. Ruby’s
file mode documentation
describes how 'b' disables Windows newline conversion. It does not make an LF file and a CRLF
file identical: those files contain different bytes and should have different checksums. Avoid
normalizing line endings, trimming whitespace, or re-encoding text before this check. Those steps
would verify transformed content instead of the downloaded file.
Use completed regular files that will stay unchanged during verification and subsequent use. This small CLI does not lock files or prevent another process from replacing them after a successful check. Keep the trusted reference protected as well as the file you verified.
For hashing inside a Transloadit workflow, see the /file/hash documentation. The same trusted-reference requirement applies when a checksum is generated during file processing.
