SAP RFC over WebSockets (wsRFC): Protocol and Authentication Explained
In Part 1 we recovered the classic RFC wire format from packet captures — the NI frame, the APPC header, the EBCDIC fossil, the self-terminating TLV fields — and validated a pure-Rust implementation byte-for-byte against SAP's own library and a live server. That protocol has been the way to talk to an ABAP system for two decades.
But it is no longer the only way. Recent NetWeaver kernels added a second transport for the same RFC protocol: wsRFC, RFC carried over a TLS WebSocket. Same payload, entirely different envelope. It is the transport SAP itself reaches for in cloud and reverse-proxy scenarios, where a raw gateway socket on port 33NN is awkward and an HTTPS port everyone already trusts is not.
erpl now speaks it too — the transport and every practical sign-on method,
validated live against a real ABAP system, in pure Rust. This post lays out how
wsRFC works on the wire and how a client authenticates.
What changed, and what didn't
The first thing to understand about wsRFC is that it is additive. erpl-proto
speaks classic CPI-C and wsRFC, and the two share almost everything above the
wire. wsRFC carries the exact same RFC_PRO payload — the same TLV codec from
Part 1, the same ProReader/ProWriter, the same value, metadata, and structure
machinery. What differs is only the box it travels in.
Here is the classic onion from Part 1, and what survives the move to WebSockets:
| Classic layer | wsRFC |
|---|---|
Gateway 02 03 routing handshake | gone — HTTP upgrade instead |
| 4-byte NI length frame | gone — the WebSocket message is the frame |
| 80-byte APPC record header | gone |
12-byte EBCDIC RFC000000000 header | gone |
| RFC_PRO field sequence (the TLV codec) | unchanged |
| 8-byte trailer restating the length | gone |
Five of the six layers evaporate. The record boundary that classic RFC computes
from an NI length field is now simply a WebSocket message boundary. A wsRFC
message begins directly with the first RFC_PRO field — a Start tag 0x0101 —
with no preamble at all. Everything the earlier post said about tags, lengths,
the repeated trailing tag, the 0xffff end marker, UTF-16LE strings, and the
one-character language code still holds, unchanged, inside the WebSocket message
body.
That is a good position to be in. Most of the hard-won codec from Part 1 is reused verbatim; only the outermost wrapper is new.
The WebSocket wrapper
wsRFC is a standard RFC 6455 WebSocket over TLS to the ICM's HTTPS port (on our
a4h trial, 50001). There is no plaintext ws:// — TLS is intrinsic. The
connection opens with an ordinary HTTP/1.1 upgrade, and two details matter.
First, HTTP/1.1 is required. The ICM offers HTTP/2 by default, but the RFC
client does not use HTTP/2's (RFC 8441) WebSocket upgrade; it wants the classic
Connection: Upgrade dance. Second, version negotiation happens in HTTP
headers, not in the RFC_PRO payload. The upgrade request looks like this:
GET /sap/bc/rfc?sap-apc-stateful=true HTTP/1.1
host: <wshost>:<wsport>
sec-websocket-protocol: rfc.sap.com
sap-client: 001
sap-language: E
sap-rfc-subtype: sync
sap-rfcpro-suppvers: 5-5
sap-rfcser-suppvers: 2-3
upgrade: websocket
connection: Upgrade
sec-websocket-version: 13
sec-websocket-key: <base64>
The subprotocol is rfc.sap.com. The sap-rfcpro-suppvers and
sap-rfcser-suppvers headers advertise the RFC_PRO protocol versions and the
serialization versions the client supports, as inclusive ranges. The server picks
one of each and answers:
HTTP/1.1 101 Switching Protocols
sap-rfcprot-vers: 5
sap-rfcser-vers: 3
sec-websocket-accept: <base64>
From 101 onward the socket carries binary WebSocket messages (opcode 0x2),
client frames masked per RFC 6455, and each message body is an RFC_PRO field
sequence decoded by the same ProReader from Part 1. The ?sap-apc-stateful=true
query parameter keeps the ABAP session pinned across messages — RFC is stateful,
and the ICM's connection pooling would otherwise hand each message to a different
work process.
One structural point about sign-on follows from this: the HTTP upgrade itself is
unauthenticated — an anonymous GET returns 101. Authentication happens
either in the TLS handshake (a client certificate) or in the first RFC_PRO
message (the logon record). That is the model for everything below.
Authenticating a wsRFC session
A wsRFC sign-on places the credential in one of two layers:
- The TLS layer — an X.509 client certificate presented during the handshake. The RFC_PRO logon record then carries no user and no password; identity comes from the certificate.
- The RFC_PRO logon record — the first WebSocket binary frame, a header-less
field sequence (no EBCDIC
RFC000000000prefix; that is a classic-NI first-record artifact wsRFC omits). The credential is a field in that record: a scrambled password (0x0117) or an SSO2 logon ticket (0x0670).
erpl implements all three, each a thin variant over the shared codec.
X.509 client certificate
The cleanest option. erpl stacks a mutual-TLS rustls config under the same
WebSocket codec and presents a client certificate during the handshake; the
logon record is a password-free Start / ProtocolVersion / Capabilities /
nonce / Client / Language / call sequence that erpl builds itself. Nothing
proprietary is involved.
The one server-side prerequisite is an ordinary, documented cert-logon setup: map
the certificate to a user with SUSR_CERT_ASSIGN, and trust it in the ICM
server PSE (SAPSSLS.pse), reloaded with ICM_SSL_PSE_CHANGED. The HTTPS
port runs verify_client = 1, so the ICM requests the certificate. Signing on as
DEVELOPER, RFC_PING returns subrc 0 and the reply carries
SessionUser = DEVELOPER — from a pure-Rust rustls client, with no
sap-r3auth header, no SDK, no ICU, no CommonCryptoLib.
User and password
Password sign-on over wsRFC is the same logon record as classic RFC, carried
over the WebSocket. The password travels in field 0x0117 — a 4-byte
little-endian seed followed by the password scrambled against that seed (the
transform from Part 1). The seed is self-contained in the field, so it can be any
value; the record also carries the session nonce (0x0514), User (0x0111),
Client, and Language. There is no sap-r3auth header for an ordinary
client — that header belongs to a different, kernel-internal mechanism (below).
Because the a4h HTTPS port requests a client certificate at the TLS layer, erpl
presents one for the handshake even for password logon — but there the
certificate is only transport, and the password in the logon record is the
credential. erpl signs on with user + password over wsRFC and round-trips
RFC_PING, STFC_CONNECTION, table reads, and metadata calls, live.
SSO2 logon ticket (MYSAPSSO2)
For single sign-on, a client can present a MYSAPSSO2 logon ticket instead
of a password. The base64 ticket rides in field 0x0670, with no user and no
password field — the identity is inside the signed ticket. erpl builds that
record directly.
Ticket sign-on has a server-side prerequisite of its own: the accepting system
must issue tickets (login/create_sso2_ticket ≥ 1), accept them
(login/accept_sso2_ticket = 1), and trust the issuer in its ACL (TWPSSO2ACL,
via STRUSTSSO2's "add own system"). This is the ticket analogue of the
certificate mapping above — configuration, not a protocol question.
The one boundary: bearer tokens and trusted RFC
There is a fourth path that a clean-room client cannot reproduce. When the ABAP
kernel itself acts as a wsRFC client — for bearer/OIDC tokens, SAML, trusted
RFC, or a same_user propagation — it authenticates with a proprietary
sap-r3auth: v=1U,… HTTP header. That header is not an encrypted password: it is
a fixed-size, per-session assertion the kernel mints from secrets held inside the
system, different on every sign-on. An external client cannot synthesize it, and
erpl does not try to — these credential types are the boundary of what a
clean-room client can originate over wsRFC. Bearer/OIDC via the standard OAuth2
route (an Authorization: Bearer header at the HTTP layer) is a separate,
potentially reproducible path and is on the roadmap.
For the credentials that matter to an integration client — certificate, password, SSO2 ticket — wsRFC sign-on is fully in reach, in pure Rust.
Where this leaves erpl
erpl talks RFC over WebSockets today, in pure Rust, with three sign-on
methods — X.509 certificate, user + password, and SSO2 logon ticket — none of
which touch an SAP SDK, ICU, or CommonCryptoLib. The transport (TLS, WebSocket,
HTTP/1.1 upgrade, version negotiation, RFC_PRO round-trip) and the sign-on are
validated live against a real ABAP system.
A single Transport trait puts classic NI and the WebSocket wire behind one
interface, so the same invoke / read_table / describe /
describe_structure code runs over either — the classic live suite passes
through it unchanged, and the wsRFC suite exercises the full surface (typed
integers, strings, xstrings, non-ASCII, metadata, DDIC structures, table reads)
over TLS. The remaining server-side step for any external RFC is releasing the
called function modules in UCON ("callable from other systems") — an
administrator setting on both transports.
Performance is what you'd expect from adding TLS: on a repeated-call loop wsRFC
runs about a quarter slower than raw CPI-C (encryption and WebSocket framing per
message), and on a bulk table read the gap narrows to ~15%. Memory is the part we
care about most — the reader streams: folding a table row-by-row holds one
page, not the whole result, so peak memory stays flat at tens of megabytes
whether you read fifty thousand rows or a million, on either transport. A
27-million-row D010TAB never has to fit in RAM.
Two transports now share one codec, and both authenticate. The envelope changed; the protocol did not.
