Skip to content

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.

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). 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 (an AWS Lightsail server 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).
  • The certificate must chain to the Switchbench CA root. The root is issued by KMS. The TLS handshake itself rejects a certificate that is missing, expired, self-signed, or does not chain to that root.
  • 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>, after which SMP closes the connection. 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)
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. recv can return a short read on TCP, so every read goes through this helper:

def read_exact(sock, count):
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

The simplest round trip: no key material involved, just the outer length prefix, the payShield message header, and the ASCII command/response.

# outbound: outer 2-byte big-endian length, then [4-byte message header][2-byte command]
message_header = b"0001" # caller-assigned; echoed back as-is
command = message_header + b"B2"
sock.sendall(len(command).to_bytes(2, "big") + command)
# inbound: read the outer length, then read exactly that many bytes
header = read_exact(sock, 2)
length = int.from_bytes(header, "big")
response = read_exact(sock, length) # b"0001" + b"B3" + b"00" (header + response code + "00" success)

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.sendall(len(command).to_bytes(2, "big") + command)
header = read_exact(sock, 2)
length = int.from_bytes(header, "big")
response = read_exact(sock, length) # b"0002" + b"CZ" + b"00" (match) or b"01" (mismatch)

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.