Background
#490 wired client cert + key PEM through reqwest::Identity::from_pkcs8_pem, which is the only PKCS path reqwest exposes on its native-tls backend. PKCS#8 PEM (-----BEGIN PRIVATE KEY-----) works; legacy PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) is rejected and surfaces as TlsError::InvalidClientIdentity.
Pnpm hands the cert / key to undici as separate PEM strings and lets Node's tls.createSecureContext accept any PEM format Node accepts — PKCS#1, PKCS#8, and EC keys are all valid. PKCS#12 (.p12 / .pfx) bundles are NOT supported by pnpm — pnpm's .npmrc allow-list at npmConfigTypes.ts declares only cert: [null, String] and key: [null, String]; there's no pfx option.
So the parity gap is PKCS#1 PEM keys, not PKCS#12.
A doc comment on apply_tls in crates/network/src/lib.rs (SHA 55083a4) documents the current PKCS#8-only limitation and the openssl pkcs8 -topk8 -nocrypt conversion workaround.
Options
- Switch reqwest's TLS backend to rustls.
Identity::from_pem exists under the rustls-tls feature and accepts PKCS#1 + PKCS#8 + EC keys. Trade-offs:
- Removes
native-tls-vendored.
- Changes platform-specific certificate-store behavior (rustls uses webpki-roots; native-tls uses the platform store). Adds
rustls-native-certs to read the system trust roots, otherwise self-signed corporate CAs that work today (because they're in the OS store) silently break.
- Probably surfaces in
cargo deny since the dep tree changes substantially.
- Existing
hickory-dns resolver is rustls-backend-agnostic; should keep working.
- Add a preprocessing step. Detect
-----BEGIN RSA PRIVATE KEY----- headers and convert PKCS#1 → PKCS#8 in pacquet before handing to Identity::from_pkcs8_pem. Possible via rsa or pkcs1 crates; adds a small new dep but no TLS-backend change.
- Document the limitation and stay on PKCS#8. Status quo. Users with PKCS#1 keys convert with
openssl pkcs8 -topk8 -nocrypt.
Per AGENTS.md ("match pnpm bug-for-bug"), option 1 is the closest to upstream behavior since pnpm/Node accept any PEM. Option 2 is the smallest diff that still closes the parity gap. Option 3 (status quo) ships with a documented limitation.
What to do
- Decide between option 1 (rustls switch) and option 2 (preprocessing).
- If option 1: also evaluate impact on:
- Platform certificate store lookup (rustls needs
rustls-native-certs to read system trust roots).
- DNS resolver — pacquet uses
hickory-dns via reqwest; verify it's compatible with the rustls backend.
cargo deny posture — the dep tree change is substantial.
- Existing tests that depend on native-tls error shapes.
- If option 2: add a
convert_pkcs1_to_pkcs8 helper that detects the header and runs the conversion. Add tests for both PEM formats.
Out of scope
- PKCS#12 (
.pfx) support. Pnpm doesn't expose it, so pacquet shouldn't invent it.
- EC keys. Native-tls's
from_pkcs8_pem already accepts them when they're PKCS#8-encoded; legacy -----BEGIN EC PRIVATE KEY----- would have the same problem as PKCS#1 and would benefit from the same fix.
References
Written by an agent (Claude Code, claude-opus-4-7).
Background
#490 wired client
cert+keyPEM throughreqwest::Identity::from_pkcs8_pem, which is the only PKCS path reqwest exposes on its native-tls backend. PKCS#8 PEM (-----BEGIN PRIVATE KEY-----) works; legacy PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) is rejected and surfaces asTlsError::InvalidClientIdentity.Pnpm hands the cert / key to undici as separate PEM strings and lets Node's
tls.createSecureContextaccept any PEM format Node accepts — PKCS#1, PKCS#8, and EC keys are all valid. PKCS#12 (.p12/.pfx) bundles are NOT supported by pnpm — pnpm's.npmrcallow-list atnpmConfigTypes.tsdeclares onlycert: [null, String]andkey: [null, String]; there's nopfxoption.So the parity gap is PKCS#1 PEM keys, not PKCS#12.
A doc comment on
apply_tlsincrates/network/src/lib.rs(SHA 55083a4) documents the current PKCS#8-only limitation and theopenssl pkcs8 -topk8 -nocryptconversion workaround.Options
Identity::from_pemexists under therustls-tlsfeature and accepts PKCS#1 + PKCS#8 + EC keys. Trade-offs:native-tls-vendored.rustls-native-certsto read the system trust roots, otherwise self-signed corporate CAs that work today (because they're in the OS store) silently break.cargo denysince the dep tree changes substantially.hickory-dnsresolver is rustls-backend-agnostic; should keep working.-----BEGIN RSA PRIVATE KEY-----headers and convert PKCS#1 → PKCS#8 in pacquet before handing toIdentity::from_pkcs8_pem. Possible viarsaorpkcs1crates; adds a small new dep but no TLS-backend change.openssl pkcs8 -topk8 -nocrypt.Per AGENTS.md ("match pnpm bug-for-bug"), option 1 is the closest to upstream behavior since pnpm/Node accept any PEM. Option 2 is the smallest diff that still closes the parity gap. Option 3 (status quo) ships with a documented limitation.
What to do
rustls-native-certsto read system trust roots).hickory-dnsvia reqwest; verify it's compatible with the rustls backend.cargo denyposture — the dep tree change is substantial.convert_pkcs1_to_pkcs8helper that detects the header and runs the conversion. Add tests for both PEM formats.Out of scope
.pfx) support. Pnpm doesn't expose it, so pacquet shouldn't invent it.from_pkcs8_pemalready accepts them when they're PKCS#8-encoded; legacy-----BEGIN EC PRIVATE KEY-----would have the same problem as PKCS#1 and would benefit from the same fix.References
tls.createSecureContextkey accepts PKCS#1, PKCS#8, EC, PFXWritten by an agent (Claude Code, claude-opus-4-7).