runspec-webops-core 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/.gitignore +2 -0
  2. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/CHANGELOG.md +9 -0
  3. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/PKG-INFO +1 -1
  4. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/pyproject.toml +1 -1
  5. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/tls.py +10 -4
  6. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/tests/test_tls.py +27 -0
  7. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/README.md +0 -0
  8. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/__init__.py +0 -0
  9. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/dns.py +0 -0
  10. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/errors.py +0 -0
  11. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/http.py +0 -0
  12. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/openapi.py +0 -0
  13. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/runspec_webops_core/proxy.py +0 -0
  14. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/tests/__init__.py +0 -0
  15. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/tests/test_dns.py +0 -0
  16. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/tests/test_http.py +0 -0
  17. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/tests/test_openapi.py +0 -0
  18. {runspec_webops_core-0.3.1 → runspec_webops_core-0.3.2}/tests/test_proxy.py +0 -0
@@ -25,6 +25,8 @@ coverage.xml
25
25
  # Node
26
26
  node_modules/
27
27
  dist/
28
+ # runspec manifest output (written at npm pack time)
29
+ runspec-files.json
28
30
  *.js.map
29
31
  .npm
30
32
 
@@ -1,5 +1,14 @@
1
1
  # runspec-webops-core Changelog
2
2
 
3
+ ## [0.3.2] — 2026-11-12
4
+
5
+ ### Fixed
6
+
7
+ - `connect_peer` (behind `cert-check`, `cert-chain` and `tls-info`) failed on
8
+ Python 3.13+ with `'bytes' object has no attribute 'public_bytes'`: the public
9
+ `SSLSocket.get_unverified_chain()` returns DER bytes, not certificate objects.
10
+ Both shapes are read now.
11
+
3
12
  ## [0.3.1] — 2026-11-05
4
13
 
5
14
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: runspec-webops-core
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Pure-Python HTTPS/TLS certificate + web-endpoint helpers — the importable core behind the runspec web runnables (no runspec dependency, no runnables)
5
5
  Requires-Python: >=3.10
6
6
  Provides-Extra: dev
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-webops-core"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  requires-python = ">=3.10"
9
9
  description = "Pure-Python HTTPS/TLS certificate + web-endpoint helpers — the importable core behind the runspec web runnables (no runspec dependency, no runnables)"
10
10
  readme = "README.md"
@@ -201,8 +201,8 @@ def client_ssl_context(insecure: bool = False, ca_bundle: str | None = None) ->
201
201
  def _peer_chain(ssock: ssl.SSLSocket) -> list[CertInfo]:
202
202
  """Return the server-presented certificate chain (leaf first).
203
203
 
204
- Prefers the public ``SSLSocket.get_unverified_chain()`` (Python 3.13+),
205
- falls back to the underlying ``_sslobj`` method (3.10–3.12), and finally to
204
+ Prefers the public ``SSLSocket.get_unverified_chain()`` (Python 3.13+,
205
+ which returns DER bytes), falls back to the underlying ``_sslobj`` method (3.10–3.12), and finally to
206
206
  the leaf-only certificate via ``getpeercert`` when neither is available.
207
207
  """
208
208
  getter = getattr(ssock, "get_unverified_chain", None)
@@ -218,8 +218,14 @@ def _peer_chain(ssock: ssl.SSLSocket) -> list[CertInfo]:
218
218
  if raw:
219
219
  out: list[CertInfo] = []
220
220
  for cert in raw:
221
- der = ssl.PEM_cert_to_DER_cert(cert.public_bytes())
222
- out.append(cert_info_from_ssl(cert.get_info(), der))
221
+ if isinstance(cert, (bytes, bytearray)):
222
+ # Python 3.13+ public API: DER-encoded bytes.
223
+ der = bytes(cert)
224
+ out.append(cert_info_from_ssl(_decode_der(der), der))
225
+ else:
226
+ # 3.10–3.12 ``_sslobj`` method: ``_ssl.Certificate`` objects.
227
+ der = ssl.PEM_cert_to_DER_cert(cert.public_bytes())
228
+ out.append(cert_info_from_ssl(cert.get_info(), der))
223
229
  return out
224
230
 
225
231
  # Fallback: only the leaf is recoverable on this interpreter.
@@ -132,3 +132,30 @@ def test_chain_report_marks_root() -> None:
132
132
  rep = chain_report(peer, now=NOW)
133
133
  assert [link["self_signed"] for link in rep] == [False, True]
134
134
  assert rep[0]["days_remaining"] == 30
135
+
136
+
137
+ def _a_der_cert() -> bytes:
138
+ """Any real DER certificate: the first system CA (no network needed)."""
139
+ certs = ssl.create_default_context().get_ca_certs(binary_form=True)
140
+ if not certs:
141
+ pytest.skip("no system CA certificates to use as a sample")
142
+ return certs[0]
143
+
144
+
145
+ def test_peer_chain_reads_der_bytes_from_public_api() -> None:
146
+ # Python 3.13+ ``SSLSocket.get_unverified_chain()`` returns DER bytes, not
147
+ # ``_ssl.Certificate`` objects; this used to fail with
148
+ # "'bytes' object has no attribute 'public_bytes'".
149
+ from runspec_webops_core.tls import _peer_chain
150
+
151
+ der = _a_der_cert()
152
+
153
+ class FakeSocket:
154
+ def get_unverified_chain(self) -> list[bytes]:
155
+ return [der, der]
156
+
157
+ chain = _peer_chain(FakeSocket()) # type: ignore[arg-type]
158
+ assert len(chain) == 2
159
+ assert chain[0].subject
160
+ assert chain[0].valid_to
161
+ assert chain[0].fingerprint_sha256