Skip to content

test(dane): cover the fail-closed paths in certificate verification - #34

Merged
andris9 merged 1 commit into
masterfrom
test/dane-fail-closed-coverage
Aug 15, 2026
Merged

test(dane): cover the fail-closed paths in certificate verification#34
andris9 merged 1 commit into
masterfrom
test/dane-fail-closed-coverage

Conversation

@andris9

@andris9 andris9 commented Aug 15, 2026

Copy link
Copy Markdown
Member

Summary

Tests only. dane.js was the least covered file in lib/, at 83.76% of branches, and the gaps sat in the paths that decide what happens when certificate data is malformed. A bug in those does not make verification throw, it makes verification succeed, so an untested branch there is worth more than a percentage point.

Properties pinned

Each of these has to fail closed:

  • An unparseable chain entry refuses the whole chain, rather than being skipped over. That is a deliberate decision in buildVerifiedPath: the entry you skip might be the link joining the leaf to the pinned trust anchor
  • An unparseable leaf refuses a trust anchor record, for the same reason
  • Certificate data that cannot be read or hashed does not match, whether reading it throws or the data is of a type no hash accepts
  • A TLSA record that throws while being read is reported as a failed match, instead of escaping into the TLS handshake where it would surface as an unrelated crash
  • Association data given as a typed array or ArrayBuffer matches exactly as a Buffer does, and still fails on a wrong digest. This one is realistic rather than synthetic: a custom resolver could plausibly return either

Verified by mutation

The first property was checked rather than assumed. Weakening the chain refusal to continue past the unparseable entry fails the new test and nothing else, so it pins the behaviour rather than passing incidentally.

Coverage

Measured with Node's built-in coverage across both suites.

File Before After
dane.js 94.70% / 83.76% 99.27% / 93.55%
all of lib/ 98.72% / 93.23% 99.59% / 95.36%

(lines / branches; functions were already 100%.)

Deliberately not chased

Two branches remain in dane.js: a resolver fallback unreachable while every supported Node has native TLSA resolution, and a catch around Node's own checkIssued, which would need a certificate that makes Node itself throw. Reaching either takes inputs no resolver or TLS stack produces, which adds test weight without adding confidence.

196 unit and 7 integration tests pass, lint and prettier clean. No code changed.

dane.js was the least covered file in lib, at 83.76 percent of branches, and the
gaps were concentrated in exactly the wrong place: the paths that decide what
happens when certificate data is malformed. A bug in those does not make
verification throw, it makes verification succeed, so an untested branch there is
worth more than a percentage point.

Five properties are pinned now, each of which has to fail closed:

- a chain entry that cannot be parsed refuses the whole chain, rather than being
  skipped over. That is a deliberate decision in buildVerifiedPath, since the
  entry skipped might be the link joining the leaf to the pinned anchor
- an unparseable leaf refuses a trust anchor record for the same reason
- certificate data that cannot be read or hashed does not match, whether the read
  throws or the data is of a type no hash accepts
- a TLSA record that throws while being read is reported as a failed match instead
  of escaping into the TLS handshake, where it would surface as an unrelated crash
- association data supplied as a typed array or ArrayBuffer matches exactly as a
  Buffer does, and still fails on a wrong digest

The first was checked by mutation: weakening the chain refusal to skip past the
unparseable entry fails the new test and nothing else.

dane.js goes from 94.70 to 99.27 percent of lines and 83.76 to 93.55 percent of
branches; across lib it is now 99.59 and 95.36. The two branches left in dane.js
are a resolver fallback that cannot be reached while every supported Node has
native TLSA resolution, and a catch around Node's own checkIssued, which would
need a certificate that makes Node throw. Both are defensive, and reaching them
would take inputs no resolver or TLS stack produces.
@andris9
andris9 merged commit a93e240 into master Aug 15, 2026
2 checks passed
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