ADR-037 — SM Remote SM4 symmetric mode: ECB deprecation, IV transport, and vendor-contract versioning
- Status: Accepted — Option 3 (owner sign-off 2026-07-20; implementation tracked by
docs/implplan/SPRINT_20260720_007_SmRemote_sm4_negotiable_mode.md) - Date: 2026-07-12 (accepted 2026-07-20)
- Deciders: (owed) crypto reviewer + Product/operator authority — this decision changes a published vendor wire contract and needs a call only they can make (see “Decision owed to the operator” below)
- Sprint: SPRINT_20260713_002 (BL-5)
- Relates: ADR-008 (credential-storage regional algorithm routing), ADR-024 (SmRemote contracts-only split), ADR-030 (region-aware plugin signing);
docs/modules/sm-remote/architecture.md(§“⚠ SM4 mode”),docs/modules/cryptography/sm-remote-vendor-adapter-contract.md,docs/modules/cryptography/architecture.md§4.4 (GOST CBC/ECB-Interop precedent)
This record was produced as an analysis + draft (BL-5 is an analysis task). It confirms the defect, quantifies the interop risk, lays out three options with tradeoffs, and records a recommended decision plus the open questions that a human crypto/product owner must answer before any code lands. RFC-vs-ADR note: because the change touches an external vendor wire contract, the maintainer may prefer to promote this to an
docs/rfcs/proposal for vendor consultation before accepting it as an ADR. Filed underdecisions/per the BL-5 task instruction; flipStatustoAccepted(or move torfcs/) at sign-off.
Context
The defect (confirmed against src/, 2026-07-12)
The SmRemote service (src/SmRemote/StellaOps.SmRemote.Service/Program.cs) supports SM4-ECB as its only symmetric mode. ECB is not semantically secure: identical 16-byte plaintext blocks map to identical ciphertext blocks, leaking plaintext structure. There is no IV and no integrity tag.
Verified locations (file:line):
- Algorithm gate — ECB is forced, everything else rejected.
Program.cs:726-736TryGetSupportedSm4AlgorithmnormalizesSM4→SM4-ECBand accepts onlySM4/SM4-ECB; any other id (SM4-CBC,SM4-GCM) returnsfalse→ the/encryptand/decrypthandlers answer400. - Soft (local-harness) path builds raw ECB with no IV. Encrypt
Program.cs:379-385, decryptProgram.cs:458-462:new PaddedBufferedBlockCipher(new SM4Engine(), new Pkcs7Padding())withcipher.Init(…, new KeyParameter(keyBytes))— aKeyParameterwith noParametersWithIVis ECB. (Note: this inline path uses PKCS7 padding, while the in-processSmPluginECB path uses zero-padding — a second, latent inconsistency between the two SM4 code paths.) - Remote path bakes
SM4-ECBinto the wire contract. EncryptProgram.cs:339-346, decryptProgram.cs:416-423forward the normalizedalgorithmId(alwaysSM4-ECB) to the vendor bridge viaSmRemoteHttpClient.EncryptAsync/DecryptAsync. - Key-usage validation hardcodes the ECB id.
Program.cs:826-834(SupportsOperation) matches an encrypt/decrypt key only when the vendor descriptor advertisesSM4orSM4-ECB. - Wire DTOs carry no IV.
SmRemoteEncryptRequest/Response,SmRemoteDecryptRequest/Response(src/__Libraries/StellaOps.Cryptography.Sm.Contracts/SmRemoteHttpClient.cs:300-338) expose onlyKeyId/AlgorithmId/PayloadBase64/CiphertextBase64— there is nowhere on the wire to put an IV or a GCM tag today. This is why “move to CBC/GCM” is inherently a wire-contract change, not a local edit. - The published contract advertises ECB.
docs/modules/cryptography/sm-remote-vendor-adapter-contract.mdshows/status→supportedAlgorithms: ["SM2","SM4-ECB"]and/keysdescriptors withalgorithmId: "SM4-ECB"; the contract version constant issm-remote-vendor-adapter.v1(SmRemoteProviderOptions.DefaultVendorContractVersion).
Is this on a real data path or a key-wrap? (materiality)
The blast radius is narrow, which is what makes a fix affordable:
- No in-repo consumer calls the SmRemote
/encryptor/decryptendpoint. The only callers ofSmRemoteHttpClient.EncryptAsync/DecryptAsyncare SmRemote’s own two handlers (Program.cs:339,:416). A repo-wide grep finds no service that POSTs to SmRemote/encrypt. - The remote SM provider does not even expose symmetric encryption through the crypto abstraction.
SmRemoteHttpProvider.Supports(SmRemoteHttpClient-siblingSmRemoteHttpProvider.cs:53-62) returnstrueonly for SM2 signing/verification. SM4 encrypt/decrypt is reachable only by POSTing the raw HTTP endpoint directly. - The real SM4 symmetric confidentiality path bypasses SmRemote entirely. Per ADR-008 the credential-store
PluginRouterhands a per-tenant DEK to the in-processSmPlugin(src/Cryptography/StellaOps.Cryptography.Plugin.Sm/SmPlugin.cs), which already implementsSM4-CBC(with random-or-caller IV,SmPlugin.cs:290-316) andSM4-GCM(AEAD,:318-363). The credential-store tests exerciseSM4-GCM(CredentialStoreInlineKeyConventionTests.cs:144), and the DoctorSmProviderCheck(SmProviderCheck.cs:31-37) requires the local SM provider to advertiseSM4-CBC. So the platform’s actual secret-at-rest wrap is not ECB and not through SmRemote. - Intended-but-unwired production posture. Doctor’s
RemoteHsmConnectivityCheck(RemoteHsmConnectivityCheck.cs:18-27) documents the design intent that “all SM2/SM3/SM4 signing and encryption rides the SmRemote HTTP forwarder.” So SmRemote SM4 is a published, production-intended interop surface an operator may wire to a vendor SM-HSM bridge — it is not dead code — but it has no exercised confidentiality traffic in-repo today. That gap between “published contract” and “not yet adopted” is precisely the window in which the mode can be corrected cheaply, before external vendor bridges lockv1/ECB in.
Bottom line: the SM4-ECB exposure is a real weak default on a published contract, but not currently protecting any live in-repo data. SM4 through SmRemote is best understood today as a GM/T vendor-bridge interop primitive, not a general-purpose confidentiality API.
Interop risk of changing it — does the remote SM peer dictate the mode?
The mode is effectively co-dictated and is negotiable in principle but only through the contract-version mechanism:
- The
algorithmIdis a free string in the request body, so the service is what currently forcesSM4-ECB; the vendor bridge merely receives it and does the crypto. Nothing in the vendor contract validation (SmRemoteVendorContract, same file) constrains the SM4 mode — it validates contract version, adapter identity/kind,production=true, and simulator-shape, and it only checks that/statusadvertises SM2 (SmRemoteHttpClient.cs:410-414), never that the requested SM4 mode is advertised. - But the vendor bridge implementation absolutely dictates what it can actually do: a bridge fronting an SM HSM / SDF / PKCS#11 device only performs the SM4 mode(s) the device supports. Some GM/T HSMs and SDF services historically expose SM4 in ECB; others do CBC/CTR/GCM. Which modes real operator-contracted bridges support is the single fact that decides feasibility, and it is not knowable from this repo — it lives in the operator↔vendor relationship. This is the crux open question below.
- Changing the mode therefore requires: (a) new algorithm ids the service will accept; (b) a place on the wire for the IV (and, for GCM, the tag); © a contract-version bump so a bridge and the service agree on the new shape; and (d) the bridge implementing the new mode. Ciphertext produced under ECB is not interchangeable with CBC/GCM ciphertext, so any persisted ECB ciphertext would need migration — but per the materiality section nothing in-repo persists SmRemote SM4 output.
Options considered
Option 1 — Stay ECB, document the risk (status quo)
- Security: Poor default. No semantic security; structure leakage on multi-block plaintext. Only partial mitigation: an SM4 block is 16 bytes, so a single-block payload (e.g. a wrapped 16-byte DEK) leaks nothing beyond equality across identical wraps; the danger is real only for structured / repetitive multi-block plaintext.
- Compatibility: Perfect — zero change, no vendor coordination,
v1bridges keep working. - Verdict: Acceptable only as the current time-boxed interim (already documented in the SmRemote dossier as “accepted risk, tracked”). Not defensible as the permanent posture for a security/sovereignty product whose sibling GOST surface already demoted ECB.
Option 2 — Hard cutover to CBC/GCM with an IV
- Security: Good→best. CBC + random IV gives semantic security; GCM additionally gives authenticated encryption (integrity), matching what
SmPluginand the credential-store already use. - Compatibility: Breaking. Every vendor bridge must implement the new mode + IV handling in lockstep; an ECB-only
v1bridge stops working at the flip. Requires a contract-version bump and strands vendors that cannot do CBC/GCM. - Verdict: The correct security end-state, but a hard flip is only safe if we can confirm no ECB-only bridge is (or will be) in service — see open question 1.
Option 3 — Make the mode negotiable (RECOMMENDED, with a safe default)
Bump the contract to sm-remote-vendor-adapter.v2 that adds SM4-CBC (new default) and SM4-GCM, keeps SM4-ECB reachable only under an explicit interop opt-in (mirroring GOST’s ECB-Interop), and picks the strongest mutually advertised mode — failing closed rather than silently downgrading.
- Security: Good, provided negotiation is fail-closed: if the caller requests CBC/GCM and the vendor
/statusdoes not advertise it, reject withalgorithm_missing— never auto-fall-back to ECB (that would be a downgrade attack surface). ECB is available only when an operator deliberately turns it on for a legacy bridge, with a loud startup warning. - Compatibility: Best.
v1/ECB bridges keep working under the interop opt-in;v2bridges advertiseSM4-CBC/SM4-GCMin/status.supportedAlgorithmsand the service selects accordingly. Backward compatible viaSM_REMOTE_HSM_CONTRACT_VERSIONpinning. - Cost/Risk: More code than Option 2 (negotiation + a real
supportedAlgorithmsmode check, which today only checks SM2) and a slightly larger contract surface. The downgrade risk is fully mitigated by the fail-closed rule. - Verdict: Delivers the safe default of Option 2 without stranding ECB-only vendors, and turns the current implicit ECB lock-in into an explicit, discoverable choice.
Decision (Accepted 2026-07-20 — owner selected Option 3)
Adopt Option 3, concretely:
- New default
SM4-CBC; addSM4-GCM.TryGetSupportedSm4Algorithm(Program.cs:726-736) acceptsSM4-CBC(mapping bareSM4→SM4-CBC),SM4-GCM, and — only under opt-in —SM4-ECB. - ECB behind an explicit opt-in, mirroring GOST. Introduce
SMREMOTE_SM4_MODEwith valuesCBC(default),GCM,ECB-Interop, exactly parallelingGostSymmetricModes/ theGostOptions.SymmetricModedefault ofCBCand the loud startup warning on ECB (GostPlugin.cs:81-96,88-93,583-594; crypto architecture §4.4). No bareECBtoken — the-Interopsuffix makes the security waiver self-documenting. - Contract bump
…v1→…v2.v2adds IV/tag transport and the CBC/GCM algorithm ids;v1remains valid and means ECB-only. Operators pin viaSM_REMOTE_HSM_CONTRACT_VERSION. - IV/tag on the wire (explicit fields, not prepend). Add
IvBase64toSmRemoteEncrypt{Request,Response}andSmRemoteDecryptRequest, and a GCMTagBase64(or a documented ct‖tag convention). Prefer explicit fields over the SmPlugin/GostPlugin “IV‖ciphertext” envelope so the HSM can own IV generation and the wire stays self-describing. The in-process plugins already support a caller-owned IV (options.Iv), so this aligns the surfaces. - Fail-closed mode negotiation. Extend the
/statusvalidation (today only asserts SM2,SmRemoteHttpClient.cs:410-414) to require the requested SM4 mode be present insupportedAlgorithms; otherwise throwalgorithm_missing. Never downgrade to ECB implicitly. UpdateSupportsOperation(Program.cs:826-834) to match the negotiated mode’s key descriptor. - Collapse the divergent soft path. Replace the inline ECB
PaddedBufferedBlockCipher(Program.cs:379-385,:458-462) with CBC/GCM — ideally by delegating to the already-correctSmPluginrather than hand-rolling a second SM4 implementation (also resolves the PKCS7-vs-zero-pad inconsistency noted above). - Conformance vectors. Add frozen, deterministic GM/T 0002 SM4-CBC and SM4-GCM known-answer test vectors (offline, no network) covering encrypt→decrypt round-trips and cross-checking against
SmPlugin; keep an explicit ECB-Interop vector so the legacy path stays verifiable. (BL-5’s completion criterion “if changed, conformance vectors pass” applies here.)
Until this ADR is Accepted and implemented, the status quo (Option 1) stands as the documented, tracked interim — the SmRemote dossier already carries the confidentiality warning and marks /encrypt /decrypt as vendor-bridge interop primitives, not a general-purpose API.
Consequences
- If Accepted (Option 3): safe-by-default SM4 across the platform (SmRemote now matches SmPlugin and the GOST plugin); ECB survives only as an explicit, warned, legacy-interop choice; a
v2contract that external vendors adopt at their own pace; new IV/tag wire fields and av2bridge conformance suite. Existingv1(ECB) bridges keep working underECB-Interop. - If deferred (Option 1 stands): no interop breakage, but the weak default persists on a published contract; every new vendor bridge that implements
v1cements ECB and raises the future migration cost. Acceptable only as an explicitly time-boxed interim. - Regional-crypto consistency: this closes the last “ECB-by-default” symmetric surface; the Doctor
SmProviderCheckCBC expectation and the credential-store SM4-GCM path already assume the safe default everywhere else. - Determinism / offline: conformance vectors are frozen KATs; no external fetch. GCM IVs are random per-encrypt (non-deterministic ciphertext) — same posture as the existing GCM paths, and never folded into any content hash.
Decision owed to the operator (crypto + product call)
These cannot be resolved from the codebase and must be answered before implementation:
- Do the operator’s contracted/deployed vendor SM-HSM bridges support SM4-CBC and/or SM4-GCM, or are they ECB-only? This decides whether a clean
v2-only cutover (Option 2) is feasible or whetherECB-Interopmust remain indefinitely (Option 3). (Recommendation: assume Option 3 unless the operator confirms zero ECB-only bridges.) - Is SmRemote SM4 used by any real deployment today, or is all confidentiality handled by the in-process
SmPlugin(credential-store DEK wrap)? If SmRemote SM4 has zero real traffic, Option 2 becomes viable with negligible breakage. (In-repo evidence: no consumer — see materiality.) - Default mode: CBC or GCM? GCM (AEAD/integrity) is preferred where the vendor supports it; CBC as the interoperable floor. (Recommendation: negotiate GCM→CBC, fail-closed, ECB only on opt-in.)
- Is off-ECB a compliance gate (GM/T conformance) or a best-effort default? i.e. should
ECB-Interopbe permanently allowed or sunset on a date? - ADR or RFC? Because this alters an external wire contract, promote to
docs/rfcs/for vendor consultation first, or accept directly as this ADR? (Maintainer’s call.)
Rejected / non-goals
- Silently editing the mode (the tempting one-liner). Rejected: it would break
v1vendor bridges and change persisted-ciphertext semantics without a contract signal — exactly the class of change the vendor-contract-version mechanism exists to prevent. - Prepending the IV to the ciphertext to avoid new wire fields. Workable and matches the plugins, but rejected in the recommendation in favor of explicit
IvBase64/TagBase64for wire clarity and HSM-owned IV generation; left as a fallback if the vendor ecosystem prefers a single opaque blob. - Auto-downgrading to ECB when the vendor lacks CBC/GCM. Rejected outright — a downgrade path is a security hole; negotiation must fail closed.
