Skip to content
MCP Server

HSMaaS (raw payShield) over mTLS TCP

HSM-as-a-Service (HSMaaS) is a standalone product path, independent of payment processing: a fintech calls SHE (the HSM Engine) directly through SMP to run a raw Thales payShield command. It never touches SVE or SME. Complete Before you begin first — this transport needs a client certificate issued during onboarding.

tcp.switchbench.com:11014 # prod
test-tcp.switchbench.com:11014 # test

Non-production environments use a single-level environment prefix on the host. The port selects the scheme — 11014 is HSMaaS, routed to the HSM Engine (SHE). Visa (11012) and Mastercard (11013) ISO 8583 have their own ports.

The port selects the engine; the client certificate selects the tenant. There is no per-tenant port: every HSMaaS customer connects to the same host:11014, and Switchbench resolves your organization from the certificate you present.

This path never touches SVE or SME. HSMaaS is a direct fintech-to-SHE call bridged by SMP; it is not part of any ISO 8583 authorization flow.

Unlike the ISO 8583 transports, this listener is runnable end to end today — there is no delivery-loop or inbound-routing blocker. You connect, send a payShield command frame, and read the response frame back on the same socket.

The scope limit here is coverage, not a runtime block: SHE implements the canonical command subset copied from she.md, not the full Thales payShield field-level specification. A command outside that table returns error code 68 (unsupported command). Payment-side SHE integration is tracked separately and does not affect this direct path — Visa response-leg crypto in #549 and Mastercard SHE fidelity in #539.

Open a TLS socket to the port and present the client certificate issued to your org during onboarding (Certificate PEM + Private key PEM from the issuance screen — see Before you begin). To verify the server in the other direction, point your TLS client at the public Switchbench server CA root — see Before you begin → Server trust root for where to get it and which environment it covers. Client-side trust and tenant resolution work exactly as they do for the Visa ISO 8583 TCP transport:

  • mTLS terminates at SMP, not at the edge. Customer connections reach the platform through an FRP edge (a Hostinger VPS forwarding raw TCP to the Mac Mini host). That edge is a raw L4 passthrough — it never sees plaintext and injects no tenant metadata. The TLS handshake passes through it and terminates at the Message Processor (SMP).
  • Both directions of trust chain to separate roots. Your client certificate must chain to the Switchbench client CA root (issued by KMS; SMP rejects anything else in the handshake), and SMP’s server certificate chains to the separate Switchbench server CA root, which you add to your client’s trust store to verify the handshake and its hostname. See Before you begin — Server trust root for how to obtain the server CA root and Verify the handshake for a verification procedure — substitute port 11014 here.
  • Tenant resolution happens after the handshake. Once TLS completes, SMP computes the certificate’s SHA-256 fingerprint and resolves your org_id from it via the Control Plane Service (GET /api/resolve-cert, a bounded cache on the hot path).
  • A revoked or unknown certificate is closed after the handshake, before any payload is processed. Such a certificate still chains to the CA root, so TLS succeeds; the /api/resolve-cert lookup then returns a revoked or unknown status and SMP drops the connection.

The outer TCP frame uses the same 2-byte length prefix as the ISO 8583 transports, but the payload inside it is the payShield wire format, which carries its own fixed-width 4-byte message header ahead of the 2-character command code:

[2-byte big-endian length][4-byte payShield message header][2-byte command code][data]

A response frame has the same shape, with the header echoed back and the 2-byte command code replaced by a 2-byte response code followed by the 2-byte error code:

[2-byte big-endian length][4-byte payShield message header][2-byte response code][2-byte error code][data]
  • The outer length header is big-endian and counts the payload bytes only (the 4-byte payShield header, the command/response code, and any data) — it does not include the 2 outer header bytes.
  • The 4-byte payShield message header is caller-assigned and echoed back verbatim in the response — use it the way you would a request id. SHE’s PayShieldCodec.Decode reads exactly 4 header bytes before the 2-character command code; a payload shorter than 6 bytes (4 header + 2 command) is rejected as invalid input.
  • The payShield header and the command/response/error codes are ASCII; data may be raw/binary (it carries wrapped key material). The TCP payload itself is raw and binary-safe — see Command model for how it is carried internally.
  • A 2-byte outer header is the platform convention for ISO 8583 and payShield — send and expect 2 (a 4-byte outer-length variant exists in the framing layer but is not used here; don’t confuse it with the 4-byte payShield message header, which is a separate, inner field).
  • The reader reassembles partial reads until the full declared payload has arrived, so a frame split across TCP segments is handled transparently.
  • A frame whose declared length exceeds the configured maximum is rejected at the framing layer.

Implementation reference: switchbench_smp/src/Switchbench.Smp/Framing/MessageFramer.cs (outer 2-byte big-endian length, ReadExactlyAsync partial-read reassembly, max-frame-size guard) and switchbench_she/src/Switchbench.PayShield/PayShieldCodec.cs (4-byte payShield header, Hsm:HeaderLength configuration).

You build a Thales payShield command and send it as the frame payload. The LMK-wrapped key travels inside the command itself — SHE is stateless with respect to tenant key storage, and KMS is not on this data path. SMP’s role is limited to authenticating the connection, enforcing scope, and metering the call: it forwards the command body to SHE unparsed, without inspecting or modifying it. This mirrors how a real Thales payShield is used — the caller supplies the wrapped key in every command.

On the wire your bytes are raw and binary-safe. Internally, SMP hex-encodes the exact frame payload it read from you and forwards it to SHE as { "command": "<hex>", "encoding": "hex" }; SHE decodes the hex, processes the payShield command, and SMP hex-decodes SHE’s response before writing it back to you unchanged. This hex step is purely an implementation detail of the SMP↔SHE bridge — from your side of the TCP connection, you send and receive the raw payShield bytes described in Framing, never hex text. The payShield message header and command/response/error codes happen to be ASCII by convention; the worked examples below use ASCII data fields too, but data may be arbitrary binary (e.g. a wrapped key blob).

The following table is the full set of payShield command/response pairs SHE implements. It is copied verbatim from docs/architecture/she.md — that file is the source of truth; if this table and that one ever disagree, she.md wins. Anything not on this list returns error code 68 (unsupported command).

CommandResponseDescription
A0A1Generate a random key under LMK
A6A7Import ZPK under ZMK key block
A8A9Export key under ZMK
BABBEncrypt clear PIN under ZPK
B2B3Echo test
BKBLEncrypt key under ZMK
BUBVGenerate KCV for key
CCCDTranslate PIN block ZPK to ZPK
CWCXGenerate CVV
CYCZVerify CVV
EIEJGenerate RSA key set
GIGJImport key under RSA public key
GKGLExport key under RSA public key
JEJFTranslate PIN block ZPK to LMK
JGJHTranslate PIN block LMK to ZPK
KMKNGenerate ARQC
KQKRVerify ARQC and generate ARPC
KUKVGenerate issuer script
KWKXVerify ARQC EMV4 and generate ARPC
NCNDNetwork check / keep-alive
NONPHSM status query
CommandResponseDescriptionStatus
BXBYNormalize key referenceSwitchbench extension — not a real payShield command

BX/BY is a platform-internal helper retained for the KMS SHE-client. It is reachable over the same HSMaaS listener like any canonical command, but a real payShield HSM does not implement it — treat it as Switchbench-specific, not part of the payShield contract.

Every payShield response carries a 2-character error code immediately after the response code — i.e. as the third field of the response, following the 4-byte message header and the 2-byte response code (see Framing).

CodeNameMeaning
00SuccessCommand completed; response payload valid. For verify commands (CY, KQ, KW), the value matched.
01Verification failedA verify-and-compare command ran to completion but the supplied value did not match the recomputed one (CVV mismatch, ARQC mismatch). A normal negative result, not a fault.
15Invalid input dataThe command payload was malformed or unparseable (bad field structure, wrong length, unparseable/un-resolvable key token, invalid hex).
68Unsupported commandNo handler is registered for the decoded command code.

Today, your client certificate is the sole credential. The TCP listener resolves your org_id from the certificate fingerprint and does not carry any scope information — every connection on this path is certificate-only, and SMP meters every call.

The handler also has a scope-check branch for an hsm:* grant (via a bearer token attached to the connection), but nothing on the live TCP path populates that token today — it is target-state, for a future in-band token exchange. Finer-grained scopes (hsm:verify-pin, hsm:encrypt, …) are target-state as well. Do not build a client that expects to pass a bearer token over this listener; it has no effect yet.

A transport-level failure — one that prevents SHE from producing a payShield response at all — is reported as a length-prefixed frame whose ASCII payload is SMP-ERR:<CODE>. SMP then closes the connection, except for RATE_LIMITED, which refuses only that command and leaves the connection open. A normal payShield response frame never starts with SMP-ERR:, since a real payShield response always begins with the echoed command header.

CodeMeaning
FORBIDDEN_SCOPEThe connection carried a bearer token whose scopes do not include an hsm:* grant (target-state — no live client attaches a token today)
FORBIDDEN_PRODUCTThe organization is no longer entitled to hsm (no active trial or subscription), or the client certificate has been revoked or no longer resolves to the organization. Checked per command; the connection is closed
RATE_LIMITEDThe organization’s hsm trial rate cap is exceeded. Checked per command after the entitlement check; only that command is refused, and the connection stays open
FRAME_TOO_LARGEThe declared frame length exceeds the configured maximum
UPSTREAM_REJECTEDSHE returned a 4xx response (malformed envelope or codec failure)
UPSTREAM_UNAVAILABLESHE was unreachable, timed out, its circuit breaker is open, or it returned 5xx
UPSTREAM_INVALID_RESPONSESHE returned 200 but the response envelope could not be decoded
INTERNALAny other unexpected SMP-side failure

A payShield error code (00/01/15/68) inside a well-formed response frame is not one of these — it is SHE’s answer to your command, passed through untouched, and the connection stays open for the next command.

Implementation reference: switchbench_smp/src/Switchbench.Smp/She/HsmaasSessionHandler.cs.

Plaintext key material is never returned to you — only the operation result (the payShield response fields, e.g. a KCV, a match/no-match indicator, or a wrapped key blob). A key you export or generate comes back wrapped under a ZMK or the LMK, never in the clear.

The snippets below are runnable Python against a standard blocking ssl.SSLSocket (the object your mTLS handshake gives you) — plain socket methods (sendall, recv), not the asyncio stream API. The B2 echo test is a complete, self-contained script — every import and helper it needs lives in that one block, so a single Copy-to-clipboard yields a runnable file: socket creation, verified mTLS handshake, the read_exact read loop, request, response assertions, and cleanup. The CY sketch below it is a fragment, not a standalone script: it reuses the open_mtls_socket() and read_exact helpers defined in the B2 script and shows only the command-specific framing.

Every socket read in these snippets goes through the read_exact helper — recv can return a short read on TCP, and the helper also turns a premature disconnect (a zero-length recv) into an explicit EOFError rather than a silent short buffer. It is defined inline in the B2 script below.

The simplest round trip: no key material involved, just the outer length prefix, the payShield message header, and the ASCII command/response. It is the smallest thing you can run end to end with your issued client certificate to prove the transport works before you build real commands.

B2 carries no PAN, so it can never match a scenario behaviour: no PAN/BIN behaviour rule and no Behaviours permission is required to run it. The client certificate and server trust root are the only prerequisites. Scenario behaviours apply on this transport only to payShield commands that carry a clear PAN.

The script below is complete and self-contained — every name it uses, including the read_exact helper and every import, is defined in the single block below, so one copy of it is a runnable file. Fill in the four inputs at the top from your onboarding issuance screen and the server trust root (the public Switchbench server CA root — how to obtain it and which environment it belongs to is in Before you begin → Server trust root). TLS verification stays on: the server chain is checked against that root and the hostname is matched, so a wrong or missing trust root fails the handshake instead of connecting insecurely.

import socket
import ssl
import sys
# --- Inputs -----------------------------------------------------------------
HOST = "test-tcp.switchbench.com" # data-plane host from the issuance screen
PORT = 11014 # 11014 is HSMaaS
CLIENT_CERT = "client-cert.pem" # Certificate (PEM) from the issuance screen
CLIENT_KEY = "client-key.pem" # Private key (PEM) from the issuance screen
SERVER_TRUST = "switchbench-server-ca.pem" # public server CA root — see "Before you begin → Server trust root"
TIMEOUT_SECS = 10 # bounded wait; a stalled peer never hangs the client
def open_mtls_socket():
"""Open a verified mTLS connection to the HSMaaS listener.
``create_default_context`` already sets ``verify_mode = CERT_REQUIRED`` and
``check_hostname = True``; both are restated below so the security posture is
explicit. A missing file, an unrelated trust root, a hostname mismatch, or an
unreachable endpoint raises here — before any bytes are sent.
"""
ctx = ssl.create_default_context(cafile=SERVER_TRUST)
ctx.verify_mode = ssl.CERT_REQUIRED # server must present a chain that verifies
ctx.check_hostname = True # and that chain must match HOST
ctx.load_cert_chain(certfile=CLIENT_CERT, keyfile=CLIENT_KEY)
raw = socket.create_connection((HOST, PORT), timeout=TIMEOUT_SECS)
raw.settimeout(TIMEOUT_SECS) # bound every later recv/send too
return ctx.wrap_socket(raw, server_hostname=HOST)
def read_exact(sock, count):
"""Read exactly ``count`` bytes, reassembling short ``recv`` reads.
``recv`` can return fewer bytes than asked on TCP; loop until the buffer is
full. A zero-length ``recv`` means the peer closed early — raise ``EOFError``
rather than returning a silent short buffer.
"""
buf = b""
while len(buf) < count:
chunk = sock.recv(count - len(buf))
if not chunk:
raise EOFError("connection closed before the expected bytes arrived")
buf += chunk
return buf
def b2_echo():
message_header = b"0001" # caller-assigned; echoed back verbatim
command = message_header + b"B2"
sock = open_mtls_socket()
try:
# outbound: outer 2-byte big-endian length, then [4-byte header][2-byte command]
sock.sendall(len(command).to_bytes(2, "big") + command)
# inbound: read the outer length, then read exactly that many bytes.
# read_exact raises EOFError on a short read / premature disconnect.
length = int.from_bytes(read_exact(sock, 2), "big")
response = read_exact(sock, length) # header + 2-byte response code + 2-byte error code
finally:
sock.close() # deterministic cleanup on success or failure
# A transport-level failure comes back as an "SMP-ERR:<CODE>" frame (SHE
# down, cert out of scope, frame too large, ...) instead of a payShield
# response — surface its real cause rather than falling into the byte
# assertions below, which would misreport it as "header not echoed".
if response.startswith(b"SMP-ERR:"):
raise RuntimeError(f"transport error frame: {response.decode('ascii', 'replace')}")
# b"0001" + b"B3" + b"00" — echoed header, B3 response code, "00" success
assert response[0:4] == message_header, f"header not echoed: {response[0:4]!r}"
assert response[4:6] == b"B3", f"unexpected response code: {response[4:6]!r}"
assert response[6:8] == b"00", f"echo failed, payShield error code {response[6:8]!r}"
return response
if __name__ == "__main__":
try:
print("B2 echo OK:", b2_echo())
except (OSError, ssl.SSLError, EOFError, AssertionError, RuntimeError) as exc:
# OSError covers connect timeouts and a missing cert/key/trust file;
# ssl.SSLError covers handshake and certificate-verification failures;
# RuntimeError is the decoded "SMP-ERR:<CODE>" transport-error frame.
sys.exit(f"B2 echo failed: {exc}")

This fragment is not standalone: it depends on the open_mtls_socket() and read_exact() helpers (and the imports) from the B2 echo script — run it in the same file, or paste those helpers in first.

A CY command carries the wrapped CVK (CVV key) inside the command itself — there is no separate key-lookup step. The data portion after the command code follows CW_GenerateCvv.ParseRequest’s layout — {cvkToken}{pan(16)};{expiry(4)}{serviceCode(3)} — with the 3-digit candidate CVV appended for CY:

# wrapped_cvk is the CVK wrapped under SHE's LMK, encoded inline ahead of the PAN
message_header = b"0002"
data = wrapped_cvk + pan + b";" + expiry + service_code + cvv_to_verify
command = message_header + b"CY" + data
sock = open_mtls_socket() # helper from the B2 script; read_exact comes from there too
try:
sock.sendall(len(command).to_bytes(2, "big") + command)
length = int.from_bytes(read_exact(sock, 2), "big")
response = read_exact(sock, length) # b"0002" + b"CZ" + b"00" (match) or b"01" (mismatch)
finally:
sock.close()

pan is the 16-digit PAN, expiry is 4 digits (YYMM), and service_code is 3 digits — wrapped_cvk is everything before those final 16 PAN digits, so its own length is not fixed. The exact field layout for other commands follows the Thales payShield 10K command reference; this page documents the transport and the canonical subset SHE implements, not the full payShield field-level specification.