-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 28 ssl plan
Part of the Amiga port design log.
Landed in six commits, end-to-end verified against www.python.org
on 2026-05-31 with CERT_REQUIRED chain validation
(HTTP/1.1 200 OK, 53 912 B HTML). This page holds both the design
and rationale — what and why — and the step-by-step ship plan —
how it shipped: the per-step deliverables, measured heap costs, the
AmigaOS capath trailing-slash gotcha, and the modern-CDN TLS 1.3
limitation tracked as a follow-up.
Upstream MicroPython ships axTLS (small, dated, no SNI) and mbedTLS (~250 KB add). Neither fits the AmigaOS ethos when the platform already has a perfectly serviceable TLS implementation — AmiSSL — as a shared library. TLS code stays on the user's machine; we ship only the thin wrapper.
AmiSSL v5 is OpenSSL 3.x: TLS 1.3, ChaCha20-Poly1305, simpler
TLS_client_method(). v4 (OpenSSL 1.1.x) is EOL upstream and not worth a
deprecated-API codepath.
Usage looks like the upstream ssl module:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(capath="AmiSSL:certs/")
ws = ctx.wrap_socket(s, server_hostname="www.example.com")Phase 28 lands behind a build flag (MICROPY_PY_AMIGA_SSL,
defaulting to ON for the variants that already include
MICROPY_PY_AMIGA_SOCKET), so partial progress doesn't disturb
the variants that are currently green.
Unlike bsdsocket.library, AmiSSL has a master-indirection step. v5 has a
unified OpenAmiSSLTags() replacing the legacy v3/v4 triple:
AmiSSLMasterBase = OpenLibrary("amisslmaster.library",
AMISSLMASTER_MIN_VERSION);
OpenAmiSSLTags(AMISSL_CURRENT_VERSION, // positional
AmiSSL_UsesOpenSSLStructs, FALSE, // opaque-only
AmiSSL_GetAmiSSLBase, &AmiSSLBase,
AmiSSL_GetAmiSSLExtBase, &AmiSSLExtBase, // OS3 split
AmiSSL_SocketBase, (ULONG)SocketBase,
AmiSSL_ErrNoPtr, (ULONG)&errno,
TAG_DONE);-
AMISSL_CURRENT_VERSIONis positional, not a tag. Compiling against the v5 SDK produces a binary that requires AmiSSL v5 at runtime — that's the point of the master indirection. -
AmiSSL_UsesOpenSSLStructs = FALSE— MicroPython treatsSSL_CTX */SSL */X509 *as opaque pointers. -
AmiSSLExtBaseis OS3-only: v5's API surface is large enough they split it across two bases on m68k. -
SocketBaseis supplied in the same call —amiga_ssl_open()must run afteramiga_socket_open().
Link against -lamisslstubs (function stubs needed for callbacks like
SSL_CTX_set_verify(..., X509_free)); don't use -lamisslauto (its
constructor-magic open fires before SocketBase is valid).
Headers: straight #include <openssl/...>, same as desktop.
OS3 callbacks under AmiSSL take args on the stack (not registers) and need
STDARGS SAVEDS annotations:
STDARGS SAVEDS static int amiga_ssl_verify_cb(int preverify_ok,
X509_STORE_CTX *ctx) { ... }bebbo gcc has __attribute__((stkparm)) and __saveds. Worth a
AMISSL_CB macro in amiga_ssl.h.
ports/amiga/modssl.c follows the upstream ssl API:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(capath="AmiSSL:certs/")
ws = ctx.wrap_socket(s, server_hostname="www.example.com")SSLContext wraps SSL_CTX *; SSLSocket wraps SSL * + fd and
implements the stream protocol so it slots into readline()/with. Per
socket: SSL_new → SSL_set_fd → SSL_set_tlsext_host_name (SNI) →
SSL_connect → SSL_read/write → SSL_shutdown + SSL_free. Transient
SSL_ERROR_WANT_READ/WRITE → MP_EAGAIN; terminal errors raise OSError
with ERR_error_string text.
Two paths:
-
SSL_CTX_set_default_verify_paths()picks up theAmiSSL:certs/c_rehash dir (the hashed CAs the AmiSSL installer drops there). -
ctx.load_verify_locations(cafile=...)/cadata=...for custom roots.
Don't ship a CA bundle in the binary — ~200 KB, gets stale fast.
#define MICROPY_PY_AMIGA_SSL (MICROPY_PY_AMIGA_SOCKET)All three shipped variants link AmiSSL. If amisslmaster.library isn't
installed at runtime, import ssl raises ImportError.
OpenSSL 3.x is memory-hungry: SSL_CTX ~16 KB, each SSL ~48 KB, CA store
~200 KB. Two HTTPS connections push the 256 KB standard heap to the edge
— but AmiSSL allocates from system memory (MEMF_ANY), not MicroPython's
GC heap, so -X heap= doesn't help; the lever is total system Fast RAM.
(The measured numbers from the implemented port are in Step 5 below.)
- Server-side TLS — straightforward, but the Amiga isn't a webserver host.
- OpenSSL config-file integration — AmiSSL ships sane defaults.
- Asyncio-friendly handshake —
MICROPY_PY_ASYNCIO (0); revisit when asyncio lights up. - Touching upstream
extmod/modssl_*.c— AmiSSL-only, port-side module.
stefanreinauer/amiga-gcc:latest ships only vbcc-side AmiSSL stubs
(proto/amisslmaster.h, proto/amissl.h, inline/amissl_protos.h
under …/vbcc/…). The GCC tree has no <openssl/*> headers, no
<libraries/amissl[master].h>, and no libamisslstubs.a/
libamisslauto.a. Phase 28 needs the AmiSSL v5 SDK layered in. Three
options:
-
Extend the container image. Custom Dockerfile
FROM stefanreinauer/amiga-gcc:latestthat fetches the SDK from https://github.com/jens-maus/amissl/releases, unpacks headers into/opt/amiga/m68k-amigaos/include/and libs into/opt/amiga/m68k-amigaos/lib/. Push to GHCR; CI workflow andtools/amiga-build.shboth point at it. Cleanest, one-time setup. - Workflow step. Download + unpack the SDK on every CI run. Simpler setup, but adds 10–30 s per run and a network dependency on GitHub releases.
-
Vendored in-tree. Drop the headers + stubs under
ports/amiga/amissl-sdk/. No external deps; downside is shipping a few MB of third-party code in the repo.
Option 2 (workflow step + local script) was the route taken — vendor nothing; rely on the upstream release artefact. The mechanics are in Step 1 below.
- AmiSSL 4 fallback. Land v5-only initially.
-
Cert pinning helpers (
set_servername_callback, raw-public-key APIs) — punt to a follow-up. - Async-friendly handshake — tied to asyncio gating.
-
urequests/mip— pure-Python, freeze into variant manifests oncessllands. (Phase 29 picks upurequests.)
This is the step-by-step ship plan — how the work was chunked into landable, individually-testable PRs.
Step 1: SDK in build env → Step 2: library lifecycle → Step 3: SSLContext
│ │
↓ ↓
Step 5: variants/heap Step 4: SSLSocket
└─────────┬────────────────┘
↓
Step 6: docs + tests
| # | Step | Output | On-target smoke test |
|---|---|---|---|
| 1 ✅ | SDK fetched on every build | Headers <openssl/*> + libamisslstubs.a reachable from m68k-amigaos-gcc inside the bebbo container |
1-line C link test inside the container |
| 2 ✅ |
ports/amiga/amiga_ssl.{c,h} — library lifecycle |
OpenLibrary("amisslmaster.library") + OpenAmiSSLTags(AMISSL_CURRENT_VERSION, ...) filling AmiSSLBase / AmiSSLExtBase. Wired into main.c after amiga_socket_open(). Silent fallback if amisslmaster.library missing |
Build + boot under Amiberry, observe no crash on startup/shutdown |
| 3 ✅ |
ports/amiga/modssl.c — ssl module + SSLContext
|
PROTOCOL_TLS_CLIENT, CERT_REQUIRED, CERT_NONE; SSLContext.__init__/__del__ (SSL_CTX_new(TLS_client_method()) + SSL_CTX_free); verify_mode property; load_verify_locations(cafile=…)
|
import ssl; ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) |
| 4 ✅ |
SSLSocket + wrap_socket
|
New type wrapping SSL * + fd. SSL_new → SSL_set_fd → SSL_set_tlsext_host_name (SNI) → SSL_connect. Stream protocol: SSL_read/SSL_write with SSL_ERROR_WANT_READ/WRITE → MP_EAGAIN. Close via SSL_shutdown + SSL_free
|
End-to-end HTTPS GET against an external host |
| 5 ✅ | Variant + heap reality check | Confirm minimal excludes SSL (no socket → no SSL). Decide heap policy for standard given SSL_CTX ~16 KB + SSL ~48 KB allocations from system memory |
Two-context allocation under default heap; document -X heap= if needed |
| 6 ✅ | Docs + test wiring | Flip Phase 28 to ✅ in docs/amiga.md. Add SSL section to docs/amiga-testing.md. Run upstream tests/extmod/ssl* against our binary on-device |
tests/extmod/ssl_basic.py via tools/amiga-runtests.py
|
All six steps landed. The implementation log under each step section below records what shipped, the on-target verification output, and any gotchas surfaced during the work.
Per the "SDK provisioning" notes above, option 2 (workflow step + local script) is the chosen route. Vendor nothing; rely on the upstream release artefact.
-
tools/amiga-fetch-amissl-sdk.sh— downloads the AmiSSL SDK archive from https://github.com/jens-maus/amissl/releases (pinned version, checksum-verified), unpacks the headers + stubs into a host-side cache directory (e.g.~/.cache/amissl-sdk/<version>/), and publishes the path to a stable symlink. -
tools/amiga-build.sh— call the fetcher before launching the container, bind-mount the cache as a second volume at/sdk, passEXTRA_CFLAGS=-I/sdk/include EXTRA_LDFLAGS="-L/sdk/lib"into the make invocation. -
ports/amiga/Makefile— pick upEXTRA_CFLAGS/EXTRA_LDFLAGS(already standard MicroPython make variables, no change likely needed beyond confirming they're honoured). -
.github/workflows/ports_amiga.yml— add a step before the build that runstools/amiga-fetch-amissl-sdk.shand mounts the cache into the build container the same wayamiga-build.shdoes. - Pinned version chosen — propose
AmiSSL 5.20, the most recent stable at the time of writing; bump deliberately.
Inside the container, after the fetch step:
m68k-amigaos-gcc -I/sdk/include -L/sdk/lib \
-xc - -lamisslstubs <<'EOF'
#include <openssl/ssl.h>
int main(void) { return SSL_library_init(); }
EOFShould produce an AmigaOS HUNK executable (we don't run it; linking is the test).
- ~10–30 s added to each clean CI run; cached across runs via
actions/cachekeyed on SDK version. -
tools/amiga-build.sh cleanshould not purge the SDK cache — it's not a build artefact, it's a toolchain artefact.
-
ports/amiga/amiga_ssl.h— externs forAmiSSLBase,AmiSSLExtBase,AmiSSLMasterBase; prototypes foramiga_ssl_open()/amiga_ssl_close();AMISSL_CBmacro for callback annotations (STDARGS SAVEDS-equivalent for bebbo). -
ports/amiga/amiga_ssl.c:-
amiga_ssl_open()—OpenLibrary("amisslmaster.library", AMISSLMASTER_MIN_VERSION), thenOpenAmiSSLTags(AMISSL_CURRENT_VERSION, AmiSSL_UsesOpenSSLStructs, FALSE, AmiSSL_GetAmiSSLBase, &AmiSSLBase, AmiSSL_GetAmiSSLExtBase, &AmiSSLExtBase, AmiSSL_SocketBase, (ULONG)SocketBase, AmiSSL_ErrNoPtr, (ULONG)&errno, TAG_DONE). Silent (no error) ifamisslmaster.librarynot present. -
amiga_ssl_close()—CloseAmiSSL()if opened, thenCloseLibrary.
-
-
ports/amiga/main.c— callamiga_ssl_open()afteramiga_socket_open(),amiga_ssl_close()beforeamiga_socket_close()(reverse order: AmiSSL → socket → dos). -
ports/amiga/mpconfigport.h—MICROPY_PY_AMIGA_SSL (MICROPY_PY_AMIGA_SOCKET)so it autodisables on theminimalvariant. -
ports/amiga/Makefile— addamiga_ssl.ctoSRC_C(orSRC_QSTRif any qstrs land in Step 3), link-lamisslstubswhenMICROPY_PY_AMIGA_SSL=1.
- Build
standardvariant. Verify text segment grows by the expected ~5–10 KB (mostly stub trampolines). - Boot under Amiberry (AmiSSL already installed) — no startup error,
no shutdown error. Add a
print(amiga.ssl_base())debug helper temporarily if needed and confirm it returns non-zero. - Boot under vamos — silent fallback path (vamos has no
amisslmaster.library), confirm no crash and no warning.
-
ports/amiga/modssl.c— new file, registerssslmodule. - Constants:
PROTOCOL_TLS_CLIENT,CERT_NONE,CERT_OPTIONAL,CERT_REQUIRED. -
SSLContexttype:-
__init__(protocol)—SSL_CTX_new(TLS_client_method()); raiseOSErrorwithERR_error_stringtext on failure. -
verify_modeproperty (getter + setter callingSSL_CTX_set_verify). -
load_verify_locations(cafile=None, cadata=None)—SSL_CTX_load_verify_locationsorSSL_CTX_load_verify_diras appropriate. -
close()+__del__finaliser —SSL_CTX_free, then mark base as closed. Tolerate double-close.
-
- If
amiga_ssl_open()left the base NULL →import sslraisesImportError("amisslmaster.library not available").
Under Amiberry:
import ssl
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(capath="AmiSSL:certs/")
del ctx # GC finalises, SSL_CTX_free runsNo crash, no leak (run amiga.heap_info() before/after a loop of
constructing + finalising 10 contexts to confirm release).
-
SSLSockettype inmodssl.c, wrappingSSL *+ the underlying socket fd:-
SSLContext.wrap_socket(sock, server_hostname=...)→SSL_new(ctx)→SSL_set_fd(ssl, fd)→SSL_set_tlsext_host_name(ssl, server_hostname)(SNI) →SSL_connect(ssl)looping onWANT_READ/WANT_WRITE. - Stream protocol (
mp_stream_p_t):-
read→SSL_read;SSL_ERROR_WANT_READ/WANT_WRITE→ seterrcode = MP_EAGAIN, returnMP_STREAM_ERROR. -
write→SSL_write; same EAGAIN handling. -
ioctl(close)→SSL_shutdown(one round-trip), thenSSL_free.
-
-
- Error mapping: terminal
SSL_get_errorvalues →OSErrorwithERR_error_stringtext.
Under Amiberry with network:
import socket, ssl
s = socket.socket()
s.connect(socket.getaddrinfo("www.python.org", 443)[0][-1])
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.set_default_verify_paths()
ws = ctx.wrap_socket(s, server_hostname="www.python.org")
ws.write(b"GET / HTTP/1.0\r\nHost: www.python.org\r\n\r\n")
print(ws.read(2048)[:80])
ws.close()Should print the start of an HTTP/1.0 response — verified on
2026-05-31 with HTTP/1.1 200 OK followed by ~54 KB of HTML.
End-to-end HTTPS GET against www.python.org succeeds under
Amiberry with full CERT_REQUIRED chain validation: TCP connect,
TLS handshake + verify, write 104-byte request, read 53 912 bytes
of HTML, clean shutdown. The pure-protocol pipeline works.
Cloudflare-fronted (www.example.com) and GitHub
(api.github.com) endpoints complete the TLS 1.3 handshake
(CERT_REQUIRED validation passes — the chain is verified, with
all four certs verify return:1) but the connection then dies
before any application data can be written: OpenSSL reports
"CONNECTION CLOSED BY SERVER" followed by
tls_retry_write_records:Broken pipe
(ssl/record/methods/tls_common.c:1949). The underlying socket
recv returns errno 54 (ECONNRESET).
Confirmed reproducible with AmiSSL's own openssl s_client —
this is not in our MicroPython wrapper. From an AmigaShell:
1> AmiSSL:OpenSSL s_client -connect www.example.com:443 \
-servername www.example.com -CApath AmiSSL:certs -brief
... TLS 1.3 handshake completes, Verification: OK ...
CONNECTION CLOSED BY SERVER
tls_retry_write_records:Broken pipe ... tls_common.c:1949
The same trace appears against api.github.com. Workarounds
attempted via s_client:
| Attempt | Result |
|---|---|
-groups X25519 (classic, no MLKEM) |
same close-after-handshake |
-no_ticket (disable session tickets) |
same close-after-handshake |
-tls1_2 (force TLS 1.2) |
server rejects ClientHello: unexpected eof while reading
|
www.python.org (a direct origin that still negotiates TLS 1.2
or TLS 1.3 without strict-fronting heuristics) handshakes,
writes, and reads cleanly from both our wrapper and s_client,
which is what the Step 4 end-to-end verification used.
The pattern points to AmiSSL's TLS 1.3 ClientHello / handshake producing a fingerprint that modern fronts choose not to talk to post-handshake. It is below the MicroPython layer; we have no visible knob that helps. Reported upstream; not a Phase 28 blocker. See AmiSSL issue notes for the full investigation and upstream report.
SSL_CTX_load_verify_locations(capath=...) is sensitive to a
trailing slash on AmigaOS: "AmiSSL:certs/" fails (path gets
concatenated to "AmiSSL:certs//<hash>.0" which the AmigaDOS
handler interprets as parent-dir-reference and ends up nowhere),
"AmiSSL:certs" works. set_default_verify_paths() sidesteps
the issue entirely and is the recommended call.
Also: AmiSSL's c_rehash output uses the old (pre-1.0.0) subject
hash algorithm. New-hash filenames silently miss in lookups even
though byte-identical files exist under their old-hash names in
the same directory. Use set_default_verify_paths() and let
AmiSSL handle this internally.
- Confirm
MICROPY_PY_AMIGA_SSLcorrectly autodisables onminimal(no socket → no SSL). - Measure actual heap pressure of:
- 1 ×
SSLContext+ 1 ×SSLSocketconnected idle. - 2 × concurrent
SSLSockets mid-handshake.
- 1 ×
- Decision matrix:
- If a single connection comfortably fits in the existing 256 KB standard heap, leave heap defaults alone, document the cliff.
- If not, bump
standarddefault heap to 384 or 512 KB.
- Update
docs/amiga.mdPhase 27 build-variants table with any text-segment growth.
-
amiga.heap_info()snapshots around handshake. -
AvailMem(MEMF_ANY|MEMF_LARGEST)snapshot to size system-memory vs GC-heap consumption.
Measured under Amiberry on 2026-05-31, standard variant, end-to-end
HTTPS GET against www.python.org (53 912-byte response). Each row
is a snapshot from amiga.heap_info() + AvailMem(MEMF_ANY|*):
| Stage | GC used | GC free | sys total | sys largest |
|---|---|---|---|---|
| startup | 26 816 | 229 312 | 14 243 080 | 12 166 992 |
after SSLContext() + set_default_verify_paths()
|
27 008 | 229 120 | 13 919 952 | 11 835 784 |
after del ctx + gc.collect()
|
26 992 | 229 136 | 13 919 952 | 11 835 784 |
| ctx ready (second one) | 27 008 | 229 120 | 13 911 720 | 11 827 552 |
after wrap_socket (TLS handshake done) |
27 136 | 228 992 | 13 823 208 | 11 663 352 |
| after full read (53 912 B response) | 28 400 | 227 728 | 13 789 880 | 11 663 352 |
after ws.close() + s.close()
|
28 352 | 227 776 | 13 829 376 | 11 663 352 |
after del + gc.collect()
|
28 304 | 227 824 | 13 837 608 | 11 687 976 |
GC heap impact: negligible. Total growth across the entire session is well under 2 KB. The 256 KB default heap is plenty for SSL workloads; no heap bump needed.
System memory is where AmiSSL lives. It allocates from MEMF_ANY
via exec.library, completely outside the GC heap, so -X heap=
doesn't help. The observed costs:
-
SSL_CTX_new(TLS_client_method())+set_default_verify_paths()→ ~316 KB of system memory, persistent for the lifetime of the process.SSL_CTX_freedoesn't return it (likely caches the parsed default trust paths globally inside AmiSSL). - One HTTPS session (handshake + 54 KB read) → ~125 KB additional during the session.
-
ws.close()+delrecovers ~48 KB. - Net per-session leak after close: ~77 KB, plausibly the AmiSSL session-resumption cache for that endpoint.
For a stock 2 MB A1200 (chip RAM only), ~441 KB for one HTTPS
session is significant but workable if Fast RAM is added. The
minimal variant is the right target for tight-memory setups —
SSL autodisables there.
MICROPY_PY_AMIGA_SSL = 0 in variants/minimal/mpconfigvariant.mk
correctly drops amiga_ssl.c and modssl.c from SRC_C, omits
-lamisslstubs from the link, and excludes AmiSSL-related strings
from the binary (3 occurrences in standard, 0 in minimal).
| Variant | Text bytes | Total binary |
|---|---|---|
standard |
491 216 | 562 840 |
minimal |
459 808 | 527 220 |
68020fpu |
(built) | 530 388 |
68040 |
(built) | 540 716 |
Standard grew ~33 KB over the pre-Phase-28 baseline (488 596 → 491 216) for the SSL wiring + AmiSSL stub trampolines. Minimal is unchanged from pre-Phase 28.
-
docs/amiga.mdPhase 28 status → ✅; add link to this page. -
docs/amiga-testing.md— new "Step 4 — SSL tests" subsection under the Amiberry runner: requires AmiSSL v5 installed (amisslmaster.libraryreachable via LIBS:, e.g. viaAssign LIBS: AmiSSL:Libs ADD; CA dir atAmiSSL:certs/), not exercisable under vamos. -
tools/amiga-runtests.py— confirmtests/extmod/ssl*.pyaren't in any_SKIP_*list and run cleanly. - Possibly: a small host-side TLS echo server (Python
ssl.wrap_socket) so the on-device runner has a deterministic endpoint that doesn't depend on the external internet.
-
docs/amiga.mdPhase 28 row flipped to ✅; section header updated with the verified-status sentence and a forward pointer here. -
docs/amiga-testing.mdgained an SSL Tests subsection under the Amiberry runner — covers prereqs (AmiSSL v5 install + assigns in scope), the recommendedset_default_verify_paths()entry point, the two AmigaOS cert-load gotchas (trailing-slash capath, old-hash c_rehash filenames), and the TLS 1.3 modern-CDN limitation. -
tools/amiga-runtests.pyconfirmed not to gate outtests/extmod/ssl*.py/tls*.py— they walk naturally when the runner is pointed atextmod/. Tests that need surface we don't expose (ssl.wrap_socketmodule-level legacy form,cadata=, server-mode handshake with canned certs) will fail; theSSLContext-shape tests (ssl_sslcontext.py,ssl_sslcontext_verify_mode.py, etc.) exercise the API we land here. - Deferred: the host-side TLS echo server. Not built — external
hosts that negotiate TLS 1.2 (
www.python.orgetc.) are sufficient for the verification we needed.
-
Cleanup ordering. AmiSSL holds
SocketBase; closingbsdsocket.librarybefore AmiSSL would leak or double-fault.main.cmust shut down in reverse open order: AmiSSL → socket → dos. -
STDARGS SAVEDScallbacks. Phase 28 punts custom callback thunks. Step 4 stays onSSL_VERIFY_PEERwith OpenSSL's default verify callback, which doesn't need user code. If a future step exposesset_verify_callbackwe'll need the bebbo equivalent (__attribute__((stkparm)) __saveds) — verify at that point that bebbo's calling convention attributes still work as documented above. -
CA bundle distribution. Don't ship one in the binary. Rely on
AmiSSL:certs/(the c_rehash-style dir of hashed CAs the AmiSSL installer drops there) for the default trust store. - AmiSSL v4 fallback. Out of scope. v5-only initially.
-
Async-friendly handshake. Tied to asyncio gating
(
MICROPY_PY_ASYNCIO (0)today). Out of scope. -
urequests/mip. Once Step 4 lands, these become trivial to freeze into thestandardvariant manifest. Separate PR.
The "Deliberately not doing" list from the design section stands:
- Server-side TLS.
- OpenSSL config-file integration.
- Asyncio-friendly handshake.
- Touching upstream
extmod/modssl_*.c.
If any of those become relevant later, they get their own follow-up issue.
ports/amiga/modssl.c — module def, SSLContext, SSLSocket
ports/amiga/amiga_ssl.c — AmiSSL master open / init / cleanup
ports/amiga/amiga_ssl.h — shared prototypes + base pointers
Build / test tooling touched:
tools/amiga-fetch-amissl-sdk.sh — fetch + cache the AmiSSL v5 SDK
tools/amiga-build.sh — mount the SDK cache into the container
ports/amiga/Makefile — link -lamisslstubs when SSL is enabled
ports/amiga/mpconfigport.h — MICROPY_PY_AMIGA_SSL gate
ports/amiga/main.c — amiga_ssl_open/close lifecycle
.github/workflows/ports_amiga.yml — SDK fetch step in CI
docs/amiga.md, docs/amiga-testing.md — Phase 28 status + SSL test notes
SDK ref:
jens-maus/amissl:dist/README-SDK.
Examples/httpget.c and Examples/https.c are minimal working clients
worth mirroring.
✅ Done. All six steps landed; end-to-end HTTPS GET against
www.python.org verified under Amiberry on 2026-05-31 with full
CERT_REQUIRED chain validation. The one open follow-up is the TLS 1.3
modern-CDN close-after-handshake behaviour (Cloudflare / GitHub),
reproduced with AmiSSL's own s_client, below the MicroPython layer,
and tracked in AmiSSL issue notes — not a
Phase 28 blocker.
The original Step 4 wrap_socket only handled the client-over-real-
bsdsocket case via SSL_set_fd, which structurally can't wrap a
fileno-less stream object (io.BytesIO, TestSocket). The upstream
extmod/ssl_basic, ssl_ioctl, and ssl_keycert tests all drive the
legacy module-level ssl.wrap_socket(...) over such fake sockets, so
they failed. Extended modssl.c to make all three pass unmodified:
-
Module-level
ssl.wrap_socket(...)— the legacy one-shot helper (server_side/key/cert/cert_reqs/server_hostname/do_handshake), mirroring micropython-lib'sssl.pyshim natively. -
SSLContext.load_cert_chain(cert, key)— DER load viaSSL_CTX_use_PrivateKey_ASN1/SSL_CTX_use_certificate_ASN1, with the upstream error contract (bad key →ValueError('invalid key'), certNone→TypeError, bad cert →ValueError('invalid cert')). -
In-memory
cadata—load_verify_locationsnow also takes acadatablob (DER cert or PEM), parsed viaBIO_new_mem_buf+d2i_X509_bio/PEM_read_bio_X509into the context trust store; bad data →ValueError('invalid cert'). This was deferred at Step 3. -
Stream-BIO transport — the fd path (and so urequests' HTTPS
client) is left byte-for-byte unchanged; fileno-less stream objects
and
server_sidesockets instead drive AmiSSL through a memory BIO pair (BIO_new_bio_pair), with a pump that shuttles ciphertext to/from the Python stream in our frames — never inside an AmiSSL callback, so the stream methods are free to allocate/GC/raise. This is what theAMISSL_CBmacro was reserved for, now realised without needing a library-into-interpreter callback at all. -
setblockingpropagation,ioctlexposure, mbedtls-style POLL→NVAL/ CLOSE pass-down — to matchssl_basic/ssl_ioctl.
Verified under Amiberry (python.uae, real AmiSSL v5) on 2026-06-20:
ssl_basic, ssl_ioctl, ssl_keycert, and ssl_cadata match their
shipped .exp byte-for-byte; ssl_keycert_pkcs8 skips (it is gated on
hasattr(ssl, "MBEDTLS_VERSION"), an mbedTLS-only attribute).
Ran the rest of tests/extmod/ssl_*. Three more changes landed so the
whole suite is green:
-
Non-blocking, poll-driven stream path (
ssl_poll). The blocking pump was reworked into a non-blocking state machine mirroringmodtls_mbedtls:read/writereportEAGAIN(→None) onWANT_*, record the cross-direction inpoll_mask, and latch fatal errors inlast_error.ioctl(POLL)translates the caller's requested direction throughpoll_mask, short-circuits onSSL_pending()buffered data, and returnsNVALonce closed or errored. A whole TLS flight is drained from the BIO pair and written in one transport write (growableout_buf) —ssl_pollasserts the ClientHello is a single write, and OpenSSL 3.6's TLS 1.3 ClientHello exceeds 512 bytes.SSL_writesuccess now also flushes the BIO (it only queues ciphertext); without that, app data never reached the peer. -
Security level 0 (
ssl_pollcert load). AmiSSL is built withOPENSSL_TLS_SECURITY_LEVEL 2, which rejected the suite's 1024-bit test keys (0A00018F: ee key too small).new_contextdrops to level 0 to match mbedTLS/axtls, which impose no key-size floor. This only widens what we accept;verify_modestill governs whether a peer chain is checked, so real-HTTPS verification is unaffected. -
ssl.OPENSSL_VERSION(ssl_sslcontext_verify_mode2). Exposed fromOPENSSL_VERSION_TEXT; the test keys offhasattr(ssl, "OPENSSL_VERSION")to distinguish an OpenSSL backend from axtls, so it now runs (and passes) rather than skipping.
Full result under Amiberry on 2026-06-21: ssl_basic, ssl_ioctl,
ssl_keycert, ssl_cadata, ssl_poll, ssl_sslcontext, and both
ssl_sslcontext_verify_mode tests pass; ssl_keycert_pkcs8 skips
(mbedTLS-only PKCS#8 keys).