Skip to main content

SAP RFC over WebSockets (wsRFC): Protocol and Authentication Explained

· 9 min read
Joachim Rosskopf
Co-Founder & CEO

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 layerwsRFC
Gateway 02 03 routing handshakegone — HTTP upgrade instead
4-byte NI length framegone — the WebSocket message is the frame
80-byte APPC record headergone
12-byte EBCDIC RFC000000000 headergone
RFC_PRO field sequence (the TLV codec)unchanged
8-byte trailer restating the lengthgone

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 RFC000000000 prefix; 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.