Module: Clickwrap::Digest
- Defined in:
- lib/clickwrap/digest.rb
Overview
Digest helpers for document bytes, presentation manifests, receipts, and the optional event chain.
What a digest here does and does not mean is part of the public contract. A digest detects that the bytes it covers changed. It does not identify who produced them, when they were produced, or that a party with full control of the database and application could not have rewritten both the bytes and the digest. Stronger claims need the optional independent anchor or timestamp adapters, and even then Clickwrap reports exactly the assurance those adapters supply.
SHA-2 is a hash standard (NIST FIPS 180-4, https://csrc.nist.gov/pubs/fips/180-4/upd1/final). It is not a signature, not an identity, and not a time source.
Constant Summary collapse
- SUPPORTED_ALGORITHMS =
Algorithm name => the OpenSSL digest that computes it. Every value stored in evidence carries its algorithm name so a future release can add an algorithm without making old events unverifiable.
{ "sha256" => "SHA256", "sha384" => "SHA384", "sha512" => "SHA512" }.freeze
- DEFAULT_ALGORITHM =
"sha256"- HEX_LENGTH_BY_ALGORITHM =
{ "sha256" => 64, "sha384" => 96, "sha512" => 128 }.freeze
- PREFIXED_PATTERN =
Digests are written as "
: " everywhere they appear, so an auditor never has to guess which function produced a bare hex string. /\A(?<algorithm>[a-z0-9]+):(?<value>[0-9a-f]+)\z/
Class Method Summary collapse
- .algorithm_of(prefixed) ⇒ Object
-
.digest(bytes, algorithm: DEFAULT_ALGORITHM) ⇒ Object
Returns "sha256:
" for the given bytes. -
.digest_canonical(value, algorithm: DEFAULT_ALGORITHM) ⇒ Object
Canonicalizes
valueper RFC 8785 and digests the resulting bytes. -
.hex(bytes, algorithm: DEFAULT_ALGORITHM) ⇒ Object
Returns the bare lowercase hex digest.
-
.keyed_digest(bytes, key:, algorithm: DEFAULT_ALGORITHM) ⇒ Object
A keyed digest, used where an unkeyed one would be guessable.
-
.matches?(bytes, expected) ⇒ Boolean
Verifies that
bytesstill hash toexpected, which must be a prefixed digest so the algorithm travels with the value. -
.secure_compare?(left, right) ⇒ Boolean
Compares two digest strings without leaking timing information.
- .supported?(algorithm) ⇒ Boolean
-
.well_formed?(prefixed) ⇒ Boolean
Whether a stored digest has a supported algorithm prefix and the exact hexadecimal length that algorithm emits.
Class Method Details
.algorithm_of(prefixed) ⇒ Object
82 83 84 |
# File 'lib/clickwrap/digest.rb', line 82 def algorithm_of(prefixed) PREFIXED_PATTERN.match(prefixed.to_s)&.[](:algorithm) end |
.digest(bytes, algorithm: DEFAULT_ALGORITHM) ⇒ Object
Returns "sha256:
45 46 47 |
# File 'lib/clickwrap/digest.rb', line 45 def digest(bytes, algorithm: DEFAULT_ALGORITHM) "#{algorithm}:#{hex(bytes, algorithm:)}" end |
.digest_canonical(value, algorithm: DEFAULT_ALGORITHM) ⇒ Object
Canonicalizes value per RFC 8785 and digests the resulting bytes.
This is how manifests, compiled policy revisions, and receipts are
digested: the digest covers meaning, not formatting.
57 58 59 |
# File 'lib/clickwrap/digest.rb', line 57 def digest_canonical(value, algorithm: DEFAULT_ALGORITHM) digest(CanonicalJson.generate(value), algorithm:) end |
.hex(bytes, algorithm: DEFAULT_ALGORITHM) ⇒ Object
Returns the bare lowercase hex digest.
50 51 52 |
# File 'lib/clickwrap/digest.rb', line 50 def hex(bytes, algorithm: DEFAULT_ALGORITHM) OpenSSL::Digest.hexdigest(openssl_name(algorithm), bytes.to_s.b) end |
.keyed_digest(bytes, key:, algorithm: DEFAULT_ALGORITHM) ⇒ Object
A keyed digest, used where an unkeyed one would be guessable.
An IPv4 address is 32 bits (RFC 791), so an unsalted hash of one can be tested by enumerating every address in minutes. Clickwrap therefore binds request evidence to its event with a keyed construction and says plainly that the result is a linkable pseudonymous value, not an anonymous one.
68 69 70 71 72 73 |
# File 'lib/clickwrap/digest.rb', line 68 def keyed_digest(bytes, key:, algorithm: DEFAULT_ALGORITHM) raise ArgumentError, "A keyed digest needs a key" if key.nil? || key.to_s.empty? mac = OpenSSL::HMAC.hexdigest(openssl_name(algorithm), key.to_s, bytes.to_s.b) "hmac-#{algorithm}:#{mac}" end |
.matches?(bytes, expected) ⇒ Boolean
Verifies that bytes still hash to expected, which must be a
prefixed digest so the algorithm travels with the value.
104 105 106 107 108 109 110 111 112 |
# File 'lib/clickwrap/digest.rb', line 104 def matches?(bytes, expected) match = PREFIXED_PATTERN.match(expected.to_s) return false unless match algorithm = match[:algorithm] return false unless supported?(algorithm) secure_compare?(hex(bytes, algorithm:), match[:value]) end |
.secure_compare?(left, right) ⇒ Boolean
Compares two digest strings without leaking timing information.
76 77 78 79 80 |
# File 'lib/clickwrap/digest.rb', line 76 def secure_compare?(left, right) return false if left.nil? || right.nil? OpenSSL.secure_compare(left.to_s, right.to_s) end |
.supported?(algorithm) ⇒ Boolean
86 87 88 |
# File 'lib/clickwrap/digest.rb', line 86 def supported?(algorithm) SUPPORTED_ALGORITHMS.key?(algorithm.to_s) end |
.well_formed?(prefixed) ⇒ Boolean
Whether a stored digest has a supported algorithm prefix and the exact hexadecimal length that algorithm emits. This validates provenance identifiers whose source bytes are intentionally unavailable here; it does not claim the digest matches those unavailable bytes.
94 95 96 97 98 99 100 |
# File 'lib/clickwrap/digest.rb', line 94 def well_formed?(prefixed) match = PREFIXED_PATTERN.match(prefixed.to_s) return false unless match expected_length = HEX_LENGTH_BY_ALGORITHM[match[:algorithm]] !expected_length.nil? && match[:value].length == expected_length end |