Skip to content

Handle digest initialization failure when hashing - #1

Open
djbclark wants to merge 1 commit into
masterfrom
silent-digest-failure
Open

Handle digest initialization failure when hashing#1
djbclark wants to merge 1 commit into
masterfrom
silent-digest-failure

Conversation

@djbclark

Copy link
Copy Markdown
Owner

Three functions in this file called EVP_DigestInit() or
EVP_DigestInit_ex() without handling failure, each in a different way:

  • HashFile() and HashPubKey() zero their output digest and fill it in
    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.
  • HashNew() ignored the return value entirely and then called
    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 master
0c0620d6c5f8f8d1cae212f084b021cff0b86ce6 — this branch is 1 ahead, 0 behind.

Verification: 6 real EVP_DigestInit* call sites in libntech, all in
libutils/hash.c; 3 were unguarded, now 0 of 6. Builds clean; hash_test
passes (6 tests). 21 insertions, 3 deletions, one file.

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
djbclark force-pushed the silent-digest-failure branch from da7d3d9 to dc85a6f Compare August 16, 2026 00:42
@djbclark
djbclark force-pushed the silent-digest-failure branch from dc85a6f to e76700b Compare August 17, 2026 12:12
@djbclark

Copy link
Copy Markdown
Owner Author

Now tracked upstream in Jira as CFE-4717, alongside the upstream PR NorthernTechHQ#291.

@djbclark
djbclark force-pushed the silent-digest-failure branch from e76700b to 4642a50 Compare August 18, 2026 02:32
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant