Skip to content

Prevent OS certificate store fallback when CA pinning is enabled#54

Merged
jeffreyparker merged 3 commits into
masterfrom
no_os_certs
Jul 21, 2026
Merged

Prevent OS certificate store fallback when CA pinning is enabled#54
jeffreyparker merged 3 commits into
masterfrom
no_os_certs

Conversation

@jeffreyparker

Copy link
Copy Markdown
Contributor

Summary

  • Prevent OS cert store fallback: When CA pinning is active (the default), sets CURLOPT_CAPATH to a non-existent randomized directory (/dev/null/<random>), preventing the TLS library from falling back to system-installed CA certificates. Fails closed with a RuntimeException if curl_setopt cannot apply the option (e.g., unsupported TLS backend).
  • Remove FileRequester: The file_get_contents transport cannot provide equivalent CA pinning guarantees and ext-curl is already a hard requirement in composer.json, making it unreachable. Removing it eliminates a transport that could bypass pinning if injected.
  • Normalize ca=IGNORE to disable_ca_pinning: The legacy ca="IGNORE" magic string is now normalized at the Client layer into the disable_ca_pinning flag, ensuring the User-Agent accurately reports ca_pinning=disabled and CurlRequester doesn't need awareness of the IGNORE convention.

Design Decisions

  • /dev/null/<random> path: Uses a randomized suffix to prevent an attacker with filesystem write access from pre-creating the directory. On POSIX, /dev/null is a character device (subdirectories cannot exist), so this is belt-and-suspenders.
  • Fail-closed RuntimeException: If CURLOPT_CAPATH cannot be set (exotic TLS backends that don't support it), the client throws rather than silently proceeding without the fallback protection.
  • IGNORE normalization at Client layer: Rather than sprinkling ca="IGNORE" checks throughout CurlRequester, the Client entry point converts it to the canonical disable_ca_pinning flag. This keeps the requester logic simple and ensures reporting is always accurate.

Breaking Changes

  • DuoAPI\FileRequester class has been removed. Users who directly constructed and injected this class will need to switch to CurlRequester (which was already the only reachable path via the default constructor since ext-curl is required).

Test plan

  • Unit tests pass (75 tests, 123 assertions)
  • SSL integration tests (require stunnel infrastructure)
  • Verify on a system where CURLOPT_CAPATH is unsupported to confirm RuntimeException
  • Verify ca="IGNORE" correctly reports ca_pinning=disabled in User-Agent

Set CURLOPT_CAPATH to a non-existent directory (/dev/null/<random>)
when CA pinning is active, preventing the TLS library from falling
back to the OS certificate store. Fail closed with a RuntimeException
if the option cannot be applied.
The curl extension is already a hard requirement in composer.json,
making the file_get_contents fallback transport unreachable. Removing
it simplifies the codebase and eliminates a transport that cannot
provide the same CA pinning guarantees as CurlRequester.
Move the legacy ca="IGNORE" handling out of CurlRequester and into
Client::setRequesterOption(), where it sets disable_ca_pinning=true
and removes the ca option. This ensures consistent behavior: the
User-Agent correctly reports ca_pinning=disabled, and CurlRequester
no longer needs awareness of the IGNORE magic string.
@jeffreyparker
jeffreyparker marked this pull request as ready for review July 16, 2026 15:32
@jeffreyparker
jeffreyparker merged commit f5ee26d into master Jul 21, 2026
5 checks passed
@jeffreyparker
jeffreyparker deleted the no_os_certs branch July 21, 2026 15:32
@jeffreyparker jeffreyparker mentioned this pull request Jul 21, 2026
2 tasks
jeffreyparker added a commit that referenced this pull request Jul 22, 2026
All feature PRs for the 1.3.0 release are merged:
- Disable CA pinning option (#51)
- User-agent CA bundle/pinning status reporting (#52)
- appendToUserAgent() method (#53)
- CURLOPT_CAPATH OS fallback prevention (#54)
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.

2 participants