Handle digest initialization failure when hashing - #1
Open
djbclark wants to merge 1 commit into
Open
Conversation
djbclark
added a commit
to frdminc/tendcf
that referenced
this pull request
Aug 15, 2026
The ticket text ended with "A pull request doing this is linked below" and then linked nothing, because the upstream PR does not exist yet and its title wants a ticket number that does not exist either. That deadlock only bound the *upstream* PR, so break it with a PR on the fork: djbclark/libntech#1. It gives the change a stable review URL and diff now, costs nothing upstream, and is not a gated artifact. The description now points at code instead of describing it. Permalinks pinned to master 0c0620d for all three unguarded sites (HashNew L151, HashFile_Stream L420, HashPubKey L562) and for the three siblings that already handle it correctly (HashString L511, HashNewFromDescriptor L186, HashNewFromKey L251) -- which is the whole argument for the fix, since the claim is that the file already contains the right answer three times over. A reviewer can now check the asymmetry in two clicks rather than taking it on faith. All nine URLs in the text were fetched and returned 200. Jira remains unfiled: basic auth returns 401 on /rest/api/3/myself even though the browser session has site access, and the operator confirmed the same error in the browser at that endpoint. Not chased further -- the text is ready to paste by hand, which was the cheaper path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
djbclark
force-pushed
the
silent-digest-failure
branch
from
August 16, 2026 00:42
da7d3d9 to
dc85a6f
Compare
This was referenced Aug 16, 2026
Tracking: our core branches are built and tested against an unmerged libntech commit
djbclark/core#7
Closed
djbclark
force-pushed
the
silent-digest-failure
branch
from
August 17, 2026 12:12
dc85a6f to
e76700b
Compare
Owner
Author
|
Now tracked upstream in Jira as CFE-4717, alongside the upstream PR NorthernTechHQ#291. |
djbclark
force-pushed
the
silent-digest-failure
branch
from
August 18, 2026 02:32
e76700b to
4642a50
Compare
EVP_DigestInit*() failure went unhandled in HashNew(), HashFile() and HashPubKey(), so callers received an all-zero digest indistinguishable from a real one. HashNew() now returns NULL, HashFile() returns false, and HashPubKey() logs. EVP_DigestUpdate(), EVP_DigestFinal*() and fread() failures are handled in the same functions. Reachable in practice, not just in theory: on OpenSSL 3, CryptoDeInitialize() unloads the default provider and every subsequent EVP_DigestInit() fails. tests/unit/hash_init_fail_test.c covers all three, as a separate program because forcing the failure requires draining the OpenSSL 3 providers before any digest has run. Changelog: Fixed hashing silently returning an all-zero digest when the hash algorithm could not be initialized. Ticket: CFE-4717
djbclark
force-pushed
the
silent-digest-failure
branch
from
August 18, 2026 14:59
4642a50 to
8023f45
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three functions in this file called EVP_DigestInit() or
EVP_DigestInit_ex() without handling failure, each in a different way:
only on success, so on failure the caller received an all-zero
digest indistinguishable from a real one. Both return void, so the
digest buffer alone carried no success indication, and neither
logged anything.
EVP_DigestUpdate() and EVP_DigestFinal_ex() on an uninitialized
context, returning a non-NULL Hash whose digest is meaningless.
Each has a sibling in this same file that already does the right thing,
which is what the fixes are modelled on: HashString() logs in exactly
this case, and HashNewFromDescriptor() logs, destroys the context and
returns NULL. The asymmetry dates from the commit that first moved
these functions into libntech and looks like an oversight rather than a
decision.
HashFile() and HashPubKey() now log an error. HashNew() now logs and
returns NULL, matching HashNewFromDescriptor(); its callers already
handle NULL, which it returns on four other paths. HashBasicInit() moved
below the check so the failure path has nothing to free.
The failure is reachable in practice, not just in theory. On OpenSSL 3,
CryptoDeInitialize() unloads the default provider and every subsequent
EVP_DigestInit() fails; anything hashing after that point was silently
affected. That is how this was found. HashPubKey() is the most serious
of the three, since a public key digest that silently becomes a constant
is an identity that collides across hosts, and it feeds lastseen and the
TLS paths.
Verified by exercising each function before and after
CryptoDeInitialize(). Before this change the second call returned an
all-zero digest from HashFile() and HashPubKey() and a non-NULL Hash
from HashNew(), all three logging nothing; after it, HashFile() and
HashPubKey() log an error and HashNew() returns NULL.
Making failure detectable by the callers of HashFile() and HashPubKey()
rather than merely visible in the log would mean changing the return
types of two void functions with callers across both repositories, and
is deliberately left out of this change.
No unit test: forcing EVP_DigestInit() to fail requires deinitializing
the crypto library, which lives in cfengine/core's libpromises and is
not available to libntech's own tests.
This PR is against the fork, not upstream. It exists to give the change a
stable review URL and diff while the upstream ticket and PR are prepared.
The upstream target is
NorthernTechHQ/libntech.Base is the fork's
master, which is identical to upstream master0c0620d6c5f8f8d1cae212f084b021cff0b86ce6— this branch is 1 ahead, 0 behind.Verification: 6 real
EVP_DigestInit*call sites in libntech, all inlibutils/hash.c; 3 were unguarded, now 0 of 6. Builds clean;hash_testpasses (6 tests). 21 insertions, 3 deletions, one file.