Skip to content

Fenrir fixes (2026-06-23)#70

Open
julek-wolfssl wants to merge 8 commits into
wolfSSL:masterfrom
julek-wolfssl:fenrir/20260623
Open

Fenrir fixes (2026-06-23)#70
julek-wolfssl wants to merge 8 commits into
wolfSSL:masterfrom
julek-wolfssl:fenrir/20260623

Conversation

@julek-wolfssl

Copy link
Copy Markdown
Member

This branch collects a set of Fenrir-tracked fixes for the wolfSSL Python bindings.

Changes

  • Return None from getpeercert when peer has no certificate (F-5623) — match the stdlib ssl contract.
  • Send bytes-like data verbatim in SSLSocket.write (F-5622) — avoid mangling bytes-like inputs.
  • Enable hostname verification in client example (F-5621).
  • Drive DTLS handshake only until complete in I/O methods (F-4136).
  • Map WANT_WRITE from SSLSocket.recv_into() to SSLWantWriteError (F-3907).
  • Map WANT_WRITE from SSLSocket.read() to SSLWantWriteError (F-3906).
  • Map WANT_READ from SSLSocket.write() to SSLWantReadError (F-3905).
  • Fix DTLS server example consuming the ClientHello before handshake (F-3481).

Each fix ships with accompanying tests.

…-3481)

The DTLS branch called bind_socket.recvfrom(1) before creating the
context. On UDP that removes the entire first datagram (the client's
ClientHello) from the queue and discards everything past the first
byte, so wolfSSL_accept() had nothing to consume and the handshake
only recovered after the client's retransmit timer. The captured
from_addr was also reused for every iteration of the -i loop.

Replace it with a peek_peer_address() helper that uses MSG_PEEK to read
the source address without consuming the datagram, and move the peek
into the accept loop so the address is refreshed per connection.
wolfSSL_write can return WOLFSSL_ERROR_WANT_READ (e.g. during a
renegotiation that must read a record before progressing; secure
renegotiation is enabled by default). write() only handled WANT_WRITE,
so WANT_READ fell through to a generic SSLError and non-blocking
callers tore the session down. Add a WANT_READ branch raising
SSLWantReadError, matching do_handshake().
wolfSSL_read can return WOLFSSL_ERROR_WANT_WRITE when the SSL layer
must flush a handshake record (e.g. renegotiation) before returning
data. read() only handled WANT_READ, raising a generic SSLError
otherwise, which stops non-blocking callers from select()-ing on
writability. Add a WANT_WRITE branch raising SSLWantWriteError.
recv_into() shares read()'s error-mapping pattern and inherited the
same omission: wolfSSL_read returning WOLFSSL_ERROR_WANT_WRITE (during
a renegotiation needing a write) was reported as a generic SSLError
instead of SSLWantWriteError, breaking non-blocking callers that
distinguish readiness directions. Add the WANT_WRITE branch.
For DTLS, write()/read()/recv_into() called do_handshake() on every
call. do_handshake() runs wolfSSL_accept/connect, which on a
non-blocking socket can raise SSLWantReadError and abort an I/O long
after the handshake finished, and made DTLS write-side behaviour
inconsistent with TCP. Track completion with a _handshake_complete
flag set on a successful do_handshake(), and only drive the handshake
from I/O methods while that flag is False.
Copilot AI review requested due to automatic review settings June 23, 2026 13:19
@julek-wolfssl julek-wolfssl self-assigned this Jun 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bundles several Fenrir-tracked behavioral fixes to the wolfSSL Python bindings to better match CPython ssl semantics (peer cert handling, I/O error mapping, DTLS handshake behavior) and to harden the shipped client/server examples (hostname verification + DTLS UDP peeking), with accompanying regression tests.

Changes:

  • Adjust peer-certificate retrieval so getpeercert()/get_peer_x509() return None when the peer presented no certificate.
  • Improve SSLSocket I/O behavior: accept bytes-like inputs for write(), map WANT_READ/WANT_WRITE to the corresponding exceptions, and avoid re-driving DTLS handshakes after completion.
  • Update examples (client hostname verification, DTLS server peek) and add targeted pytest coverage for each fix.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
wolfssl/__init__.py Core binding behavior changes: peer cert retrieval, bytes-like write(), DTLS handshake completion tracking, WANT_* exception mapping.
examples/client.py Adds hostname-verification behavior (and opt-out flag) and passes server_hostname to wrap_socket().
examples/server.py Fixes DTLS UDP example to peek source address without consuming the ClientHello datagram.
tests/test_write_bytes.py Regression tests ensuring write() sends bytes-like contents verbatim and rejects non-bytes-like inputs.
tests/test_io_error_mapping.py Verifies WANT_READ/WANT_WRITE error-code mapping to SSLWantReadError/SSLWantWriteError.
tests/test_getpeercert.py Confirms getpeercert()/get_peer_x509() return None when the peer provides no certificate.
tests/test_dtls_server_example.py Ensures DTLS server peeking doesn’t consume the datagram needed for handshake.
tests/test_dtls_handshake_once.py Ensures DTLS handshake is driven only until completion (not on every I/O call).
tests/test_client_example.py Validates example client verification/hostname-check configuration behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread wolfssl/__init__.py Outdated
Comment thread examples/client.py
Comment thread examples/client.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread wolfssl/__init__.py Outdated
Comment thread examples/client.py
@julek-wolfssl julek-wolfssl removed their assignment Jun 24, 2026
The client example set CERT_REQUIRED and loaded CA roots but never set
check_hostname or passed server_hostname to wrap_socket, so wolfSSL
validated the chain to a trusted CA without binding the certificate to
the requested host. A peer presenting any CA-trusted certificate for a
different hostname would be accepted by anyone reusing this as a secure
client template. Make verification configure hostname checking by
default (via a new configure_verification helper) and add a -n flag to
opt out explicitly for IP literals or test certificates.
write() converted data with t2b(), which str()-encodes anything that is
not already bytes. Valid bytes-like inputs such as bytearray and
memoryview were transmitted as their Python repr ("bytearray(b'...')",
"<memory at ...>") instead of their contents, corrupting the stream.
Convert via the buffer protocol (bytes(memoryview(data))) and raise
TypeError for objects that are not bytes-like, matching the stdlib ssl
module.
get_peer_x509() checked only whether the session was NULL and then built
a WolfSSLX509, whose __init__ called wolfSSL_get_peer_certificate() and
raised SSLError on NULL. On a valid connection where the peer presented
no certificate (e.g. a server not requesting a client cert), this raised
instead of returning None as the stdlib ssl getpeercert() contract
requires. Fetch the certificate in get_peer_x509(), return None when it
is NULL, and have WolfSSLX509 wrap the already-obtained pointer.

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skoll Code Review

Scan type: reviewOverall recommendation: COMMENT
Findings: 3 total — 3 posted, 0 skipped
3 finding(s) posted as inline comments (see file-level comments below)

Posted findings

  • [Medium] Client example default host is an IP literal but hostname verification is now on by defaultexamples/client.py:46-48,134-158,183-191
  • [Low] Non-ASCII character in test source literaltests/test_write_bytes.py:80-84
  • [Low] WolfSSLX509 type discrimination relies on exact cffi cname string matchwolfssl/__init__.py:99-111

Review generated by Skoll

Comment thread examples/client.py
help="Disable client cert check"
)

parser.add_argument(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [Medium] Client example default host is an IP literal but hostname verification is now on by default

F-5621 enables hostname verification whenever cert checking is on (the default). But the client's default host is the IP literal -h 127.0.0.1 (line 46), and configure_verification returns args.h as server_hostname and sets check_hostname = True for that default. __init__ then calls wolfSSL_check_domain_name(ssl, "127.0.0.1"). wolfSSL_check_domain_name performs DNS/CN-style domain matching; the bundled certs/server-cert.pem carries 127.0.0.1 only as an IP Address SAN (SAN is DNS:example.com, IP Address:127.0.0.1), not a DNS-type entry. If wolfSSL's domain-name check does not match IP-type SANs, the out-of-the-box python client.py run will now fail with a hostname/domain mismatch and require the new -n flag — a behavior change for the example's default invocation. The function's own docstring even states IP literals need -n, which is exactly the default host. Additionally, sending an IP literal via SNI (use_sni("127.0.0.1")) is not RFC-6066 conformant.

Fix: Confirm that a default python client.py (host 127.0.0.1) still completes the handshake against the bundled certs with hostname verification enabled. If wolfSSL_check_domain_name does not match the IP-type SAN, either auto-disable the hostname check for IP-literal hosts, document that -n is required for the default host, or change the example default host to a DNS name in the cert SAN.

Comment thread tests/test_write_bytes.py


def test_write_str_is_utf8_encoded(monkeypatch):
# Backward compatibility: str is UTF-8 encoded (historical t2b()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] Non-ASCII character in test source literal

The test embeds a raw non-ASCII character é (U+00E9) in the source to verify UTF-8 encoding. The file declares # -*- coding: utf-8 -*- so it is functionally valid, but the wolfSSL project convention is 7-bit ASCII for committed code. The same assertion can be expressed with an ASCII-only escape while still exercising a multi-byte UTF-8 encoding.

Fix: Replace the literal é with the escape \u00e9 to keep the source 7-bit ASCII while testing the same multi-byte path.

Comment thread wolfssl/__init__.py
@@ -97,7 +97,15 @@ class WolfSSLX509(object):
"""

def __init__(self, session):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] WolfSSLX509 type discrimination relies on exact cffi cname string match

The constructor distinguishes a WOLFSSL* session from a WOLFSSL_X509* by comparing _ffi.typeof(session).cname == "WOLFSSL *". This is verified correct against the current cdef (wolfSSL_get_peer_certificate(WOLFSSL*) returns WOLFSSL_X509*), but it is brittle: it depends on cffi's exact rendering of the type name, will raise TypeError if a non-cdata is ever passed, and silently stores whatever is in the else branch without validating it is actually a WOLFSSL_X509*. A small comment is already present; a slightly more defensive check (e.g. comparing against the X509 type, or guarding the typeof call) would be more robust to future cdef changes.

Fix: Optional hardening: key the branch off the expected X509 type or guard the typeof lookup so the constructor degrades gracefully if an unexpected type is passed. Functionally correct as-is.

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.

4 participants