SNC (Secure Network Communications) Layer¶
Status: CONFIRMED — live SNC handshake against CommonCryptoLib X.509 passed 2026-08-05
(partner p:CN=A4H). The frame codec and GSS binding are additionally proven offline against
a mock GSS library.
Confidence: HIGH for every implemented path. SSO2 token mode remains unimplemented and
raises NotImplementedError — see Known Gaps.
Overview¶
SNC wraps SAP's Network Interface (NI) payloads in a GSS-API-protected frame. It sits at the
transport layer — below the RFC/CPIC protocol, above raw TCP. An SncTransport
wraps any inner Transport (see framing.md) and intercepts send_message /
recv_message; the Connection and all protocol layers above it are transparent to SNC.
RfcInvoke() / logon TLV — RFC application + handshake payloads
→ Connection.send_message(payload) — plain RFC/CPIC bytes (see framing.md)
→ SncTransport.send_message() — gss_wrap / gss_get_mic per negotiated QOP
→ build_snc_frame() — prepend the 0x18-byte SNC header
→ inner Transport.send_message() — NI framing + TCP writev (framing.md)
SNC is runtime-optional and dependency-free: ctypes is stdlib, and the GSS
provider .so is supplied by the user at connect time via snc_lib. There is no build-time
or install-time SNC dependency; pip install saprfclib works on any machine, and SNC activates
only when snc_lib is passed to connect().
Activation: the presence of snc_lib is the switch — there is no separate mode flag.
connect(..., snc_lib="/path/to/lib.so", snc_partnername="p:CN=...") opens an SNC channel;
omitting snc_lib leaves the plain TCP path byte-for-byte unchanged.
SNC Frame Format¶
The SNC frame is the only wire structure SNC itself owns — everything inside it is either a GSS token or protected application data.
Wire Layout¶
Offset Length Type Name Notes
0x00 8 bytes eye_catcher b"SNCFRAME" (no trailing NUL in the field)
0x08 1 uint8 frame_type 1=FR_INIT, 2=FR_ACCEPT, 7=plain, 8=integrity, 9=privacy
0x09 1 uint8 version Protocol version = 6
0x0a 2 uint16-BE hdr_len Total header length = 0x18 + extension-header size
0x0c 4 uint32-BE token_len GSS token byte count
0x10 4 uint32-BE data_len Application data byte count
0x14 2 uint16-BE ctx_id Context / adapter ID
0x16 2 uint16-BE qop_flags QOP flags (see QOP Levels)
[0x18] var bytes ext_headers Extension headers, ONLY if hdr_len > 0x18 — see Known Gaps
[hdr_len] token_len bytes gss_token GSS token bytes
[..] data_len bytes app_data Application data bytes
struct format: >8sBBHIIHH (24 bytes fixed header). Implemented in
src/saprfclib/snc.py as build_snc_frame / parse_snc_frame.
hdr_len includes the 8-byte eye-catcher, so the standard value is 0x18. When it is
larger, the frame carries extension headers and the GSS token starts at hdr_len rather
than at 0x18 — parse_snc_frame skips them by seeking to hdr_len, so an inbound frame
with extension headers parses correctly even though their internal layout is undocumented.
On the outbound side, _build_snc_ext_header() emits the mechanism extension header
(mechanism OID + context ID). What is not supported is SSO2 token mode — see
Known Gaps.
DoS Guard¶
parse_snc_frame validates the declared token_len + data_len against a 128 MiB cap
(_MAX_FRAME_BYTES) before slicing or allocating — mirrors the NI-length check in
framing.md. A declared length is attacker-controlled input: check it before you
allocate against it, not after.
Frame Detection (passthrough)¶
Every inbound frame's first bytes are checked against the eye-catcher. A frame that does
not begin with it is non-SNC NI traffic and is passed through unchanged — SncTransport
must never corrupt a non-SNC frame.
Handshake¶
The SNC context is established with a standard GSS-API initiator/acceptor loop.
Client Server
│ FR_INIT (type 1) + gss_init token │ gss_init_sec_context() → output token
│ ───────────────────────────────────▶│ gss_accept_sec_context()
│ FR_ACCEPT (type 2) + gss_accept tok │
│ ◀───────────────────────────────────│
│ (repeat while GSS_S_CONTINUE_NEEDED)│
│ ... │
│ handshake COMPLETE │ both sides: GSS_S_COMPLETE
- Client calls
gss_init_sec_context(input token empty on the first call →GSS_C_NO_BUFFER) and sends the output token in an FR_INIT (type 1) frame. - Server replies with an FR_ACCEPT (type 2) frame carrying its accept token.
- The loop repeats while
gss_init_sec_contextreturnsGSS_S_CONTINUE_NEEDED. - On
GSS_S_COMPLETE, the context is established and data frames (type 7/8/9) may flow.
Handshake before data: no application data is
wrapped or sent until the handshake reaches GSS_S_COMPLETE. SncTransport sets an
_established gate only inside _handshake and re-asserts it at the top of
send_message (belt-and-suspenders).
Per-connection lifecycle¶
Initialisation order, which matters — the credential must exist before the partner name is imported against it:
session init → set own name (snc_myname) → session start
→ import partner name (snc_partnername) → acquire credential
saprfclib implements the initiator (client) role only; the acceptor role is not
implemented. GssBinding.__init__ acquires an initiator credential (GSS_C_INITIATE), then
imports the partner name.
GSS-API Binding¶
An SNC implementation loads the GSS library at runtime and resolves a small set of standard
RFC 2744 entry points. saprfclib does the same with ctypes.CDLL(snc_lib) — there is no GSS
reimplementation here, and no cryptography of our own. The crypto is delegated wholesale to
the user-supplied .so.
The six required functions:
| Function | Used for |
|---|---|
gss_init_sec_context |
Client handshake init token (FR_INIT) |
gss_accept_sec_context |
Acceptor path (server role — deferred) |
gss_wrap |
QOP 3 privacy — encrypt payload |
gss_unwrap |
QOP 3 privacy — decrypt payload |
gss_get_mic |
QOP 2 integrity — produce MIC |
gss_verify_mic |
QOP 2 integrity — verify MIC |
Four helpers are also resolved: gss_import_name, gss_acquire_cred,
gss_release_buffer, gss_release_name. Every mech-allocated output buffer is copied to
Python bytes and freed via gss_release_buffer — a mech-allocated buffer that is never
released is a leak in a long-lived pooled connection.
This binding is provider-agnostic: CommonCryptoLib (libsapcrypto.so, X.509),
libgssapi_krb5.so (Kerberos), or any RFC 2744-compliant library works unchanged.
minikerberos is deliberately not used — it does not implement gss_wrap / gss_unwrap
for message protection above QOP 1.
ctypes struct definitions (gss_buffer_desc, gss_OID_desc) follow RFC 2744 §3.1–3.2
and were verified against libgssapi_krb5.so.2.
QOP Levels¶
Quality of protection:
| QOP | Name | Frame type | GSS calls |
|---|---|---|---|
| 1 | authentication | 7 (PLAIN) | none (no data protection) |
| 2 | integrity | 8 (INTEG) | gss_get_mic / gss_verify_mic |
| 3 | privacy | 9 (PRIV) | gss_wrap / gss_unwrap |
SAP's default is min=1, max=3, use=3. saprfclib defaults to snc_qop=3 (privacy).
No cleartext password: at QOP 3 every payload — including the logon
TLV carrying the scrambled password — passes through gss_wrap, so the raw payload bytes
never reach the socket in cleartext. No plain (type 7) frame is emitted at QOP ≥ 3. This is
confirmed offline by an opaque gss_wrap mock (the test asserts the plaintext never appears
in the wrapped output) and is re-verified on the wire at the live checkpoint.
Parameter API¶
saprfclib.connect() kwargs mirror SAP's SNC_* env var names in snake_case:
| kwarg | SAP env var | Meaning | Default |
|---|---|---|---|
snc_lib |
SNC_LIB |
Path to the GSS provider .so (required for SNC) |
— |
snc_partnername |
SNC_PARTNERNAME |
Server GSS identity, e.g. p:CN=SAP Server,... |
— |
snc_myname |
SNC_MYNAME |
Client identity (optional; lib default if absent) | None |
snc_qop |
SNC_QOP |
QOP level 1 / 2 / 3 | 3 |
snc_sso |
SNC_SSO |
SSO2 token mode — not implemented, raises NotImplementedError |
False |
Quote stripping: SNC_MYNAME and SNC_PARTNERNAME may arrive wrapped in double quotes
from the SAP environment. GssBinding strips a single enclosing quote pair before passing the
value to GSS — matching SAP's own behaviour, which is why a quoted partner name works there
and would otherwise fail here.
Credential discipline: snc_lib, snc_partnername, and snc_myname are
never logged, echoed into an exception message, or placed into any repr. SncError
carries the GSS major / minor status codes only — never token or name bytes.
Known Gaps¶
Values that could not be established without a live capture are recorded here rather than invented. Nothing below is guessed into the implementation.
SNC eye-catcher (8 bytes) — RESOLVED¶
The 8-byte eye-catcher at frame offset 0x00 is b"SNCFRAME" — eight characters, with no
trailing NUL inside the field. SncTransport uses this as its default; the eye_catcher
parameter remains injectable for testing.
SSO2 extension headers — OPEN¶
Extension headers beyond the mechanism header carry SNC SSO2 tokens and extended context data. That layout has not been established, so:
snc_sso=TrueraisesNotImplementedErroratSncTransport.__init__.- No extension-header byte values are invented beyond the mechanism header, whose OID and context ID are confirmed.
Inbound frames carrying extension headers are not rejected: parse_snc_frame seeks past
them using hdr_len and reads the GSS token from there, so an unrecognised extension header
costs nothing as long as the token position is right.
Consequence: SSO2 ticket-based SNC logon is unavailable. Certificate and Kerberos SNC are unaffected.
CommonCryptoLib GSS mechanism OID — RESOLVED¶
CommonCryptoLib (libsapcrypto.so) uses a SAP-specific GSS mechanism OID rather than a
standard Kerberos mechanism: 1.3.36.3.1.37.1, DER-encoded as
\x06\x06\x2b\x24\x03\x01\x25\x01 (8 bytes). This is _SAP_SNC_OID in snc.py, and
the context ID _SNC_CTX_ID = 3 was confirmed from a live capture.
_build_snc_ext_header() builds the mechanism extension header from these two values. The full
CommonCryptoLib X.509 handshake was confirmed end-to-end against a live system on 2026-08-05.
Cross-References¶
- Framing — the NI/CPIC framing SNC wraps
- Handshake — the logon TLV sequence SNC protects at QOP 3
- Serialization — ABAP type encoding inside the protected payload
src/saprfclib/snc.py— frame codec,GssBinding,SncTransport,connect_snc
Live SNC handshake confirmed 2026-08-05 against CommonCryptoLib X.509, partner p:CN=A4H.
Not implemented: SSO2 extension headers, and the acceptor (server) role.