Skip to content

Cbest/multi identities - #10

Merged
ccbest merged 7 commits into
mainfrom
cbest/multi-identities
Jul 31, 2026
Merged

Cbest/multi identities#10
ccbest merged 7 commits into
mainfrom
cbest/multi-identities

Conversation

@ccbest

@ccbest ccbest commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Add support for PKCS#12 certificates containing multiple identities (file/windows/macos)
Explicitly does not add PEM support for the same despite tooling now being available - follow up branch needed
Raise AmbiguousCertificateError when multiple identities are present and one has not been specified

ccbest and others added 5 commits July 30, 2026 19:49
… present

Raise AmbiguousCertificateError when multiple identities are present and one has not been specified
Dual key pairs live in the platform stores as often as in files -- AD/Exchange
key archival puts both halves in CurrentUser\MY, and keychains collect them the
same way -- but the store constructors had nothing to select on. WinCert and
MacCert carried only a CN, a label, and a thumbprint, so a store holding both
halves under one subject raised AmbiguousCertificateError with no way out
except an exact thumbprint, which is machine-specific and changes at renewal.

Both dataclasses now carry the parsed certificate and its CertInfo, and a
shared _CertDetails mixin gives them (and P12Identity) identical key_usage /
extended_key_usage accessors, so one predicate reads the same across all three
surfaces. from_windows_cert_store, from_macos_keychain, their
build_*_ssl_context counterparts, and the select_* helpers all take the usage
selectors, which are recorded on the SourceRef so reload() re-selects.

The usage vocabulary moved from _pkcs12 to _select, which now owns selection
for every surface; both selectors share matches_usages so they cannot drift.

Behavior change: store selectors intersect instead of overriding. Passing name=
with thumbprint= used to ignore the name; now every selector must match, as the
PKCS#12 ones do.

macOS needed no new plumbing -- _certificate_details already parsed the DER and
discarded everything but the CN and thumbprint. Windows reads the DER off a
declared CERT_CONTEXT, then checks the parsed certificate's SHA-1 against the
thumbprint crypt32 reported separately: a wrong struct offset yields plausible
garbage rather than an error, so a mismatch drops to certificate=None and the
record keeps selecting by name and thumbprint as before. The extraction flow is
tested off-Windows against a mirrored struct; the real layout is asserted by the
new opt-in suite.

That suite (HTTPX_PKI_WINSTORE_TESTS=1, mirroring the macOS keychain gate, and
enabled for Windows CI runners) imports a throwaway dual pair into
CurrentUser\MY with Import-PfxCertificate -Exportable and removes it after. It
is the first time the crypt32 enumeration and export have run in CI at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017X8iUVVmXpS2rWcWmELzLM
ctypes.get_last_error() returns a signed int, so every NTE_* HRESULT arrives
with its high bit read as a sign: NTE_BAD_KEY_STATE (0x8009000B) -- what
Windows returns when a key was imported without the exportable flag, and the
most likely reason an export fails at all -- came back as -0x7ff6fff5 and
matched nothing in NON_EXPORTABLE_ERRORS. The message fell through to the
generic "PFX export failed (Windows error -0x7ff6fff5)", which reads like a
library fault rather than a fixable import setting.

Codes are now normalized to unsigned before comparing or formatting, the known
set also covers the CNG counterpart NTE_NOT_SUPPORTED, and the message names
the flags that mark a key exportable at import time -- along with the TPM and
smart-card case, where no flag will help and a key file is the way out.

The set moved to module scope and _raise_export_error lost its no-cover pragma:
the mapping is pure, so it is now tested off Windows in both the signed and
unsigned spelling of every code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017X8iUVVmXpS2rWcWmELzLM
Building a session from the macOS keychain emitted "PKCS#12 bundle could not
be parsed as DER, falling back to parsing as BER" on every construction. The
cause is an explicitly encoded ASN.1 DEFAULT: DER requires a field equal to its
default to be omitted, and MacData.iterations carries DEFAULT 1, so writing it
anyway is valid BER but not DER. Reproduced locally by encoding that field the
same way -- both load_pkcs12 and load_key_and_certificates warn identically, so
this predates identity selection.

The bytes are generated by the Security framework, consumed in-process, and
never written down, so there is nothing a caller could do about the encoding.
The filter is installed only in material_from_store_export, and only for that
message, so files a caller supplies still warn -- there, re-exporting the file
is a real fix.

cryptography says the fallback "may become an exception". If it does, the
generic load failure would blame an invalid password that this library
generated itself, so the store path now raises an error naming the encoding
instead.

make_pkcs12 grew strict_der=False to write that shape, which is what makes any
of this testable off macOS -- and is worth having for downstream suites that
need to cope with platform exports too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017X8iUVVmXpS2rWcWmELzLM
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 90.87948% with 56 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
httpx_pki/_pkcs12.py 84.55% 22 Missing and 20 partials ⚠️
httpx_pki/testing.py 95.74% 3 Missing and 3 partials ⚠️
httpx_pki/_source.py 66.66% 3 Missing ⚠️
httpx_pki/_material.py 94.73% 1 Missing and 1 partial ⚠️
httpx_pki/_select.py 97.77% 1 Missing and 1 partial ⚠️
httpx_pki/_ssl.py 88.88% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ccbest
ccbest merged commit a4c4521 into main Jul 31, 2026
15 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.

2 participants