Skip to content

Support TLS 1.3 mTLS for async cert/A2A auth (#41, #43) - #48

Merged
petrsnd merged 4 commits into
mainfrom
danpeterson-tls13-support
Sep 4, 2026
Merged

Support TLS 1.3 mTLS for async cert/A2A auth (#41, #43)#48
petrsnd merged 4 commits into
mainfrom
danpeterson-tls13-support

Conversation

@DanPeterson

@DanPeterson DanPeterson commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #41 and #43 in one change.

SPP 9.0 enables TLS 1.3. On TLS 1.3, certificate/A2A auth requires the client to answer a post-handshake CertificateRequest (RFC 8446 §4.6.2), which Python's ssl.SSLContext only does when post_handshake_auth = True. The async (aiohttp) path never set it, so async cert/A2A auth failed on 9.0 with 60094 Authorization is denied. The sync (requests/urllib3) path already enables it by default and was unaffected.

Changes

  • Bugfix (async): AsyncSafeguardClient._create_ssl_context now sets post_handshake_auth = True. This is the one change required for async cert/A2A auth to work on 9.0.
  • Opt-in TLS version pinning: new min_tls_version / max_tls_version (ssl.TLSVersion | None, default None = negotiate) on SafeguardClient, AsyncSafeguardClient, A2AContext, AsyncA2AContext, and the A2A quick_* classmethods.
    • Async applies the pins in _create_ssl_context.
    • Sync mounts a _TlsVersionAdapter using urllib3's native ssl_minimum_version / ssl_maximum_version, only when a pin is set — the default transport path is unchanged.
  • HTTP/1.1 preserved on both stacks (HTTP/2 disallows the post-handshake CertificateRequest).
  • Version bumped to 8.2.0 (backward-compatible minor).

Usage:

import ssl
# Require TLS 1.3
SafeguardClient(host, auth=auth, min_tls_version=ssl.TLSVersion.TLSv1_3)
# Interim: cap at TLS 1.2
A2AContext(host, cert, key, max_tls_version=ssl.TLSVersion.TLSv1_2)

Tests

  • New unit tests: tests/test_tls.py (post-handshake auth enabled; pins applied; sync adapter mounted only when pinned; defaults unchanged; A2A forwarding).
  • New integration tests (auto-skip without an appliance): TLS-1.3 cert-auth in test_certificate_auth.py and TLS-1.3 A2A retrieval in test_a2a.py.
  • Full unit suite green (336 passed); ruff, ruff format --check src/, mypy --strict clean.

Docs

AGENTS.md (TLS 1.3 section), README, and the a2a-workflow + api-patterns skills.

⚠️ Before merge — live validation still pending

I have not yet validated against live appliances. Please confirm on real hardware:

  • SPP 9.0 (TLS 1.3): async + sync certificate auth and A2A credential retrieval succeed (this is the core fix).
  • SPP 8.x (TLS 1.2): no regression — async + sync cert/A2A auth still work.
  • min_tls_version=TLSv1_3 succeeds on 9.0; max_tls_version=TLSv1_2 still works on both.

The sync side is a review-confident change (uses urllib3's documented version knobs), but the whole point is TLS behavior against real appliances, so live sign-off is required.

DanPeterson and others added 4 commits August 13, 2026 18:17
Enable post-handshake authentication (RFC 8446 4.6.2) on the aiohttp
SSLContext so async certificate and A2A credential retrieval succeed
over TLS 1.3 on SPP 9.0 (previously failed with error 60094). The sync
requests/urllib3 path already enabled this by default.

Add opt-in min_tls_version / max_tls_version (ssl.TLSVersion | None,
default None) to SafeguardClient, AsyncSafeguardClient, A2AContext,
AsyncA2AContext, and the A2A quick_* classmethods. Async applies them in
_create_ssl_context; sync mounts a _TlsVersionAdapter using urllib3's
native ssl_minimum_version / ssl_maximum_version only when a pin is set,
so the default transport path is unchanged. HTTP/1.1 is preserved.

Add unit tests (tests/test_tls.py) and TLS 1.3 cert-auth / A2A
integration tests. Update AGENTS.md, README, and the a2a-workflow and
api-patterns skills. Bump version to 8.2.0.
Promote the TLS 1.3 guidance to a discoverable top-level README section covering what changed (post-handshake auth), version pinning, and Python-specific gotchas (HTTP/2 incompatibility, OpenSSL 1.1.1+ requirement, ssl.TLSVersion usage, verify=False context behavior).
The A2A integration fixture assumed the appliance A2A service was already running. A freshly provisioned appliance ships with it stopped, so every A2A credential call fails with 503 'The A2A service is disabled.' Start it idempotently during fixture setup (ApplianceAdmin) so the suite can run end-to-end against a clean appliance.
@petrsnd
petrsnd marked this pull request as ready for review September 4, 2026 19:22
@petrsnd
petrsnd requested a review from a team as a code owner September 4, 2026 19:22
@petrsnd
petrsnd merged commit e31672e into main Sep 4, 2026
5 checks passed
@petrsnd
petrsnd deleted the danpeterson-tls13-support branch September 4, 2026 19:24
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.

Support TLS 1.3 mTLS in async (aiohttp) A2A: enable post-handshake auth + add TLS-version option

2 participants