SM Remote Architecture
Stateless microservice providing Chinese national standard cryptographic operations (SM2/SM3/SM4) via HTTP endpoints.
SM Remote is the SM-series (GM/T) crypto plane of the Stella Ops release control plane. It gives other services (Signer, AirGap) a uniform HTTP surface for SM2/SM3/SM4 operations while keeping production key material behind a hardware/remote HSM boundary — part of Stella Ops’ regional-crypto (eIDAS/FIPS/GOST/SM) sovereignty story.
Audience: operators deploying SM Remote against a vendor HSM bridge, and engineers integrating SM crypto operations. This dossier covers the service runtime, the vendor adapter contract, configuration, and the production fail-closed posture.
Overview
SM Remote is a single-project ASP.NET Core minimal-API microservice (StellaOps.SmRemote.Service, target net10.0) that exposes SM-series cryptographic operations over HTTP. It is designed to be stateless: no database is required. The service binds to port 56080 (SM_REMOTE_PORT).
Per ADR-024 the service is contracts-only: it has no compile-time reference to any SM provider plug-in. It references StellaOps.Cryptography.Sm.Contracts (the host-owned HTTP transport: SmRemoteHttpClient, SmRemoteHttpProvider, SmRemoteSigner, SmRemoteVendorContract, provider name cn.sm.remote.http), and providers carrying real in-process key material arrive at runtime:
cn.sm.remote.httpis constructed directly by the host from the contracts library and delegates over HTTP to a vendor bridge in front of an HSM.cn.sm.soft(SmSoftCryptoProvider, real in-process SM2/SM3 material) is mounted as a signed crypto pack byMountedSmCryptoRuntimePluginLoader(see Components →Plugins/). In Development/Testing local harnesses only,SmHostDevFallbackProviderFactoryreflectively falls back to the in-process soft provider so local harnesses keep working; Production has no such fallback and an empty provider set faults startup (Program.cs:123-132).
Production configuration fails closed at startup (ValidateOnStart). See Security Considerations → Production fail closed for the exact accepted set — note that cn.sm.soft is not unconditionally rejected in Production: it is admitted when, and only when, an admitted signed pack supplies it and the operator sets SMREMOTE_ALLOW_SIGNED_SOFT_PACK_IN_PROD.
Components
src/SmRemote/
StellaOps.SmRemote.Service/ # ASP.NET Core minimal-API web service
Program.cs # Host + endpoint registration; also holds the
# request/response DTOs (HashRequest/Response,
# EncryptRequest/Response, SignRequest/Response, ...)
# and SmRemoteRuntimeOptions inline
Plugins/ # ADR-024 signed-pack provider resolution (security-critical:
# this is what decides which provider signs in Production)
MountedSmCryptoRuntimePluginLoader.cs # Loads + admits the mounted signed SM crypto pack
MountedSmCryptoRuntimePluginOptions.cs # Mount root / profile / trust-root / AllowUnsigned
SmHostDevFallbackProviderFactory.cs # Dev/Testing-ONLY reflective in-process fallback
SmSoftProviderActivation.cs # Disables the pack provider's SM_SOFT_ALLOWED env
# gate for an ADMITTED pack (admission is the
# authorization; reflection-only, no SmSoft ref)
Security/
SmRemotePolicies.cs # Named scope-policy constants (SmRemote.Hash, ...)
EphemeralKeyAuditLog.cs # In-process ephemeral-key audit log + registry
# (process-local; NOT durable across restart)
EphemeralKeyRotationService.cs # Background TTL-eviction hosted service
__Tests/StellaOps.SmRemote.Service.Tests/
# The host-owned HTTP transport the service DOES reference (ADR-024 contracts-only boundary):
src/__Libraries/
StellaOps.Cryptography.Sm.Contracts/ # <- the SmRemote service's only crypto reference
SmRemoteHttpClient.cs # vendor-bridge HTTP client + DTOs
# + SmRemoteVendorContract (contract validation
# and simulator detection) in the same file
SmRemoteHttpProvider.cs # SmRemoteHttpProvider -> cn.sm.remote.http
SmRemoteProviderOptions.cs
SmRemoteSigner.cs
# Provider plug-ins the service must NOT reference at compile time (they arrive as signed packs):
StellaOps.Cryptography.Plugin.SmSoft/ # SmSoftCryptoProvider -> cn.sm.soft (BouncyCastle)
StellaOps.Cryptography.Plugin.SmRemote/ # thin plug-in wrapper that references Sm.Contracts
Namespace vs. project (drift trap). The ADR-024 split preserved namespaces:
Program.cs:16still readsusing StellaOps.Cryptography.Plugin.SmRemote;even though the transport types now live in theStellaOps.Cryptography.Sm.Contractsproject. Follow the.csprojreferences, not theusinglines.StellaOps.SmRemote.Service.csprojreferencesSm.Contractsonly, and carries an inline boundary comment plus aDoesNotContainboundary test asserting no provider-plug-in reference.
Data Flow
- An upstream service (Signer, AirGap) sends a cryptographic operation request (sign, verify, hash, encrypt, decrypt) to SM Remote.
- SM Remote selects the configured provider via the
ICryptoProviderRegistry— preferringcn.sm.remote.http, thencn.sm.soft(ResolveProvider,Program.cs:599-612). The registry is composed at startup (Program.cs:63-135):cn.sm.remote.httpis constructed directly by the host fromSm.Contracts(no in-process key material, so it is not packaged);cn.sm.softis sourced from the mounted signed crypto pack viaMountedSmCryptoRuntimePluginLoader, falling back to the reflective in-process provider only in Development/Testing (SmHostDevFallbackProviderFactory). If neither yields a provider, startup throws — Production has no fallback. - For signing/verification in Development/Testing local harnesses, and only when
SMREMOTE_ALLOW_EPHEMERAL_KEYS=truewith a positiveSMREMOTE_EPHEMERAL_KEY_TTL, the service may seed an ephemeral SM2 key pair on first use for the givenkeyIdusing the BouncyCastleSM2P256V1named curve (GMNamedCurves.GetByName). Each seeded key carries TTL/expiresAt/seededBymetadata, is registered in the in-processEphemeralKeyRegistry, and emits asm-remote.key.seededaudit event. Outside those harnesses (or when the flag is off) unknownkeyIdvalues fail with a 404 rather than creating signing authority.EphemeralKeyRotationServicesweeps every 30s, evicting expired keys and emittingsm-remote.key.rotated. - In production remote-provider mode, SM Remote first maps the request
keyIdto a configured remote key (SMREMOTE_KEYS), then probes the configured vendor bridge (/status,/keys) and validates key usage before forwarding/sign,/verify,/encrypt, or/decrypt. It rejects responses that do not carry the expected adapter identity, supported adapter kind, exact contract version,production=true, and non-simulator metadata. - The provider executes the operation and returns the result. Vendor-contract or transport failures surface as fail-closed
Results.Problemresponses (HTTP 502 for crypto operations, 503 for/healthand/status) with afailureClassextension. - No state is persisted between requests; key material is ephemeral within the process lifecycle for local harnesses, and production key material remains behind the remote adapter boundary.
Database Schema
Not applicable. SM Remote is a stateless service with no persistent storage.
Dependencies
| Service/Library | Purpose |
|---|---|
BouncyCastle (Org.BouncyCastle) | SM2/SM3/SM4 algorithm implementations (SM3 hash and SM4 CBC/GCM/ECB ciphers are called from the service soft path via Sm4SoftCipher) |
StellaOps.Cryptography.Sm.Contracts | The only crypto reference the service holds (ADR-024). SmRemoteHttpProvider/SmRemoteHttpClient/SmRemoteSigner (cn.sm.remote.http) — HTTP delegation to the vendor bridge and SmRemoteVendorContract validation |
StellaOps.Plugin (+ .Host) | Runtime-bundle admission/loading for the mounted signed SM crypto pack (MountedSmCryptoRuntimePluginLoader) |
Router (StellaOps.Router.AspNet) | Service mesh routing/discovery (AddRouterMicroservice("smremote")) + UseIdentityEnvelopeAuthentication() for gateway-proxied calls |
Authority (StellaOps.Auth.ServerIntegration) | JWT/OAuth resource-server auth + scope policies for inbound requests |
StellaOps.Localization | Localized endpoint descriptions and error messages |
StellaOps.Cryptography | Canonical crypto assembly (ICryptoProvider, CryptoProviderRegistry) |
ADR-024 boundary (verified in
StellaOps.SmRemote.Service.csproj): the service referencesStellaOps.Cryptography,StellaOps.Cryptography.Sm.Contracts,StellaOps.Plugin,StellaOps.Auth.ServerIntegration, andStellaOps.Localization— and neitherStellaOps.Cryptography.Plugin.SmSoftnorStellaOps.Cryptography.Plugin.SmRemote. Providers with in-process key material must arrive as signed packs, never as compile-time references.
Note: there is no
Pkcs11Interop(or any direct PKCS#11) dependency in this service. The remote provider talks to a vendor bridge purely over HTTP;pkcs11is only one of the three valid adapter kind labels (sm-hsm,sdf,pkcs11) the bridge may declare in its contract metadata.
Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /hash | Compute SM3 hash of input data; returns algorithmId, hashBase64, hashHex. Only SM3 is accepted. | sm-remote:hash |
| POST | /encrypt | SM4 encrypt in the negotiated mode (SM4-CBC default, SM4-GCM; SM4-ECB only under SMREMOTE_SM4_MODE=ECB-Interop) — see SM4 mode below. Soft path: 16-byte keyBase64, returns ivBase64 (CBC/GCM) + tagBase64 (GCM). Remote path: maps keyId to the vendor bridge. Bare SM4 maps to the configured mode. | sm-remote:encrypt |
| POST | /decrypt | SM4 decrypt in the negotiated mode. CBC requires ivBase64; GCM requires ivBase64 + tagBase64. Soft path: 16-byte keyBase64. Remote path: maps keyId to the vendor bridge. | sm-remote:decrypt |
| POST | /sign | SM2 digital signature (SM2P256V1 curve), via the resolved provider’s signer. Unknown keyId returns 404 unless an ephemeral key can be seeded (local harness only). | sm-remote:sign |
| POST | /verify | SM2 signature verification; returns { "valid": bool }. | sm-remote:verify |
| GET | /status | Provider availability, name, supported algorithms (SM2); in remote mode also returns the vendor adapterId/adapterKind/contractVersion and discovered keys[]. | anonymous |
| GET | /health | Health check; in remote mode proxies the vendor /health and echoes adapter metadata. | anonymous |
SM4 mode: negotiable — CBC default, GCM available, ECB behind an opt-in (ADR-037)
TryGetSupportedSm4Algorithm (Program.cs) is mode-aware. SMREMOTE_SM4_MODE selects CBC (default), GCM, or the explicit legacy waiver ECB-Interop, paralleling the GOST plugin’s GostOptions.SymmetricMode:
SM4-CBC(default) andSM4-GCMare always accepted; a bareSM4maps to the configured mode’s default (SM4-CBCby default,SM4-GCMunderGCM,SM4-ECBunderECB-Interop).SM4-ECBis accepted only whenSMREMOTE_SM4_MODE=ECB-Interop— otherwise rejected with a typed 400 (failureClass=sm4_mode_not_allowed) naming the configured mode.ECB-Interopemits a loud startup warning that ECB is not semantically secure and is enabled only for legacy vendor-bridge interop. There is no bareECBtoken.
The soft path (Sm4SoftCipher) implements CBC (PKCS7, random 16-byte IV per op) and GCM (AEAD, random 12-byte IV, 128-bit tag); IV and tag are returned as explicit ivBase64/tagBase64 fields, never prepended to the ciphertext. SM4-ECB (interop) keeps PKCS7 with no IV.
Padding reconciliation (ADR-037):
Sm4SoftCipheruses PKCS7 for SM4-ECB/SM4-CBC (matching the original inline ECB path and the GOST CBC envelope). The in-processSmPlugin(SmPlugin.ProcessBlocks) uses zero-padding for its raw ECB/CBC block loop. The two surfaces never share a ciphertext, so the padding choice is documented per algorithm id rather than unified;SmPluginbehavior is left unchanged (no test proves a defect there).
The remote path forwards the negotiated SM4-CBC/SM4-GCM/SM4-ECB id and the IV/tag to the vendor bridge. Fail-closed negotiation: when the host is configured for CBC/GCM (or ECB-Interop), it requires the corresponding algorithm to be present in the bridge /status.supportedAlgorithms and rejects a mismatch at probe time with algorithm_missing — it never silently downgrades to ECB. This matches the platform’s other symmetric surfaces (GOST CBC-default + ECB-Interop; the in-process SM plugin’s SM4-CBC/GCM).
The wire contract is versioned sm-remote-vendor-adapter.v2 (adds IV/tag + CBC/GCM ids); v1 remains valid and means ECB-only — pin it via SM_REMOTE_HSM_CONTRACT_VERSION. See SM Remote Vendor Adapter Contract.
The service does not expose a
GET /keysendpoint./keysis a vendor-bridge endpoint that SM Remote consumes (see Vendor Adapter Contract) to discover and validate configured keys. Endpoint names (WithName) areSmRemoteHash,SmRemoteEncrypt,SmRemoteDecrypt,SmRemoteSign,SmRemoteVerify,SmRemoteStatus,SmRemoteHealth.
Vendor Adapter Contract
Production remote-provider mode uses SMREMOTE_PROVIDER=cn.sm.remote.http and requires SM_REMOTE_HSM_URL, SMREMOTE_KEYS, and vendor contract validation. SM_REMOTE_HSM_API_KEY is optional unless SM_REMOTE_HSM_REQUIRE_API_KEY=1; when present, it is sent as a bearer token to the vendor bridge. SM_REMOTE_HSM_ADAPTER_KIND must be one of sm-hsm, sdf, or pkcs11; SM_REMOTE_HSM_CONTRACT_VERSION defaults to sm-remote-vendor-adapter.v2 (pin …v1 for a legacy ECB-only bridge).
The vendor bridge must expose the SM Remote HTTP contract:
| Method | Path | Contract requirement |
|---|---|---|
| GET | /health | Returns status, availability, adapterId, supported adapterKind, exact contract version, and production evidence. |
| GET | /status | Returns availability, provider name, supported algorithms including SM2, adapter identity, contract version, and production evidence. |
| GET | /keys | Returns discoverable key descriptors with local/remote identity, algorithm, and usages; configured keys must be present. |
| POST | /sign | Accepts mapped remote key ID, algorithm, and base64 PAE payload; returns base64 signature plus adapter metadata. |
| POST | /verify | Accepts mapped remote key ID, algorithm, payload, and signature; returns verification result plus adapter metadata. |
| POST | /encrypt | Accepts mapped remote key ID, algorithm, and base64 payload; returns base64 ciphertext plus adapter metadata. |
| POST | /decrypt | Accepts mapped remote key ID, algorithm, and base64 ciphertext; returns base64 payload plus adapter metadata. |
Every strict response must include the expected contract version (compared with ordinal equality against the configured SM_REMOTE_HSM_CONTRACT_VERSION, default sm-remote-vendor-adapter.v2), an adapterId, a supported adapterKind, production=true, and must not set testOnly=true or use simulator-shaped identifiers (any value containing sim.crypto.remote, simulator, simulation, or starting with sim./ending .sim). Validation lives in SmRemoteVendorContract (in StellaOps.Cryptography.Plugin.SmRemote). Failures throw SmRemoteContractValidationException carrying a stable failureClass, surfaced on the wire as a Results.Problem with a failureClass extension. Observed failure classes include: empty_response, contract_version_missing, adapter_identity_missing, adapter_kind_unsupported, adapter_kind_mismatch, adapter_unavailable, algorithm_missing, keys_missing, key_incomplete, key_mapping_missing, key_usage_mismatch, simulator_response, field_missing, field_malformed, plus the transport-level adapter_http_error and adapter_timeout raised by the service host.
Authorization
Each crypto operation requires an explicit StellaOps scope in addition to tenant context. Tenant presence alone is not authorization.
| Endpoint | Required scope |
|---|---|
POST /hash | sm-remote:hash |
POST /encrypt | sm-remote:encrypt |
POST /decrypt | sm-remote:decrypt |
POST /sign | sm-remote:sign |
POST /verify | sm-remote:verify |
Configuration
Configuration binds the SmRemote section (AddOptions<SmRemoteRuntimeOptions>().Bind("SmRemote")) and is then overlaid by environment variables in SmRemoteRuntimeOptions.ApplyConfiguration. Runtime safety is enforced by ValidateRuntime with ValidateOnStart() (the host refuses to start on an unsafe production configuration).
| Variable (and alias) | Purpose | Default |
|---|---|---|
SMREMOTE_PROVIDER / SM_REMOTE_PROVIDER | Active provider: cn.sm.remote.http (remote) or cn.sm.soft (soft). | cn.sm.soft on the local harness, otherwise cn.sm.remote.http |
SM_REMOTE_HSM_URL | Absolute HTTP(S) base address of the vendor bridge (required in remote mode). | (empty) |
SM_REMOTE_HSM_API_KEY | Bearer token sent to the vendor bridge. | (empty) |
SM_REMOTE_HSM_REQUIRE_API_KEY | If true, the API key is mandatory in remote mode. | false |
SM_REMOTE_HSM_TIMEOUT | HTTP client timeout in milliseconds. | 30000 |
SM_REMOTE_HSM_ADAPTER_KIND / SMREMOTE_ADAPTER_KIND | Expected adapter kind: sm-hsm, sdf, or pkcs11. | sm-hsm |
SM_REMOTE_HSM_CONTRACT_VERSION / SMREMOTE_ADAPTER_CONTRACT_VERSION | Expected vendor contract version. | sm-remote-vendor-adapter.v2 |
SMREMOTE_SM4_MODE / SM_REMOTE_SM4_MODE | SM4 symmetric mode: CBC (default), GCM, or ECB-Interop (legacy waiver). | CBC |
SM_REMOTE_HSM_REQUIRE_VENDOR_CONTRACT / SMREMOTE_REQUIRE_VENDOR_CONTRACT | Enforce strict vendor contract validation. | true outside the local harness |
SMREMOTE_KEYS / SM_REMOTE_KEYS / SM_REMOTE_HSM_KEYS | Explicit local→remote key mapping, comma/semicolon-separated localId=remoteId tokens. | (empty) |
SMREMOTE_ALLOW_SOFTWARE_PROVIDER / SM_SOFT_ALLOWED | Permit the un-admitted in-process soft provider (rejected in Production regardless of the signed-pack opt-in). | local harness only |
SMREMOTE_ALLOW_SIGNED_SOFT_PACK_IN_PROD | ADR-024 Production opt-in for cn.sm.soft when it is supplied by an admitted signed crypto pack. Clears only the option-level gate; startup still fails closed if no admitted pack supplies the provider. Distinct from SM_SOFT_ALLOWED above. | false |
SMREMOTE_ALLOW_EPHEMERAL_KEYS / SM_EPHEMERAL_KEYS_ALLOWED | Permit ephemeral SM2 key seeding (local harness only). | false |
SMREMOTE_EPHEMERAL_KEY_TTL / SM_EPHEMERAL_KEY_TTL | Required positive TTL when ephemeral keys are enabled. Accepts ISO-8601 duration (PT1H), HH:MM:SS, or integer seconds. | (unset) |
SMREMOTE_RUNTIME_ID / SM_REMOTE_RUNTIME_ID | Stable runtime instance id stamped into ephemeral-key audit records (seededBy). | fresh GUID per process |
SMREMOTE_DURABLE_KEY_STORE | Optional durable key-store path (bound but not yet used by the seeder). | (unset) |
SM_REMOTE_PORT | Host port published by the compose overlay. | 56080 |
Mounted signed crypto-pack keys (SmRemote:RuntimePlugins section, MountedSmCryptoRuntimePluginOptions):
| Config key | Purpose | Default |
|---|---|---|
SmRemote:RuntimePlugins:RootPath | Container root for mounted SM crypto-provider bundles. | /app/plugins/crypto |
SmRemote:RuntimePlugins:Profile | Mounted profile selected when none is configured. | crypto-sm |
SmRemote:RuntimePlugins:TrustRootPath | Cosign public key that admits the signed pack. | /app/etc/certificates/trust-roots/plugins/crypto/cosign.pub |
SmRemote:RuntimePlugins:AllowUnsigned | Skip signature admission (test harness escape hatch). | false |
Bool parsing accepts true/false, 1/0, and yes/no. RequireVendorAdapterContract defaults to enabled outside the Development/Testing local harness. The provider, key store path, and ephemeral-key flags also accept nested SmRemote:* config keys via standard binding.
Security Considerations
Transport security & identity: SM Remote does not configure mTLS itself; inbound requests are authenticated via Authority JWT/OAuth (
AddStellaOpsResourceServerAuthentication) with per-endpoint scope policies and tenant middleware (UseStellaOpsTenantMiddleware). The service participates in the Stella Router mesh and callsUseIdentityEnvelopeAuthentication()beforeUseAuthentication()(Program.cs, afterUseStellaOpsLocalization()): the Router gateway strips the caller’sAuthorizationheader and substitutes a signed identity envelope, so gateway-proxied calls carry no bearer token and would otherwise 401 on every scoped endpoint. The envelope middleware no-ops when the principal is already authenticated or the envelope headers are absent, so direct internal-network callers (Signer, AirGap) using a bearer token are unaffected. Mutual TLS between services is a deployment/mesh-level concern, not something this service enforces in code.Ephemeral key lifecycle: SM2 key pairs may be generated per
keyIdonly in Development/Testing local harnesses, only whenSMREMOTE_ALLOW_EPHEMERAL_KEYS=truewith a positiveSMREMOTE_EPHEMERAL_KEY_TTL. Seeded keys carry ephemeral metadata + expiry, are audited (sm-remote.key.seeded), and are evicted byEphemeralKeyRotationServiceon TTL expiry (sm-remote.key.rotated). Production must not silently create keys for unknownkeyIdvalues; startup validation rejects ephemeral keys outright outside the local harness.HSM offloading: Production deployments must use a hardware/remote provider adapter to delegate key operations to a hardware security module, ensuring key material never leaves the HSM boundary. The checked-in service host supports vendor bridges through the stable SM Remote HTTP contract, while proprietary SDKs remain outside the repo until they pass the dependency license gate.
No key export: The service does not expose endpoints for exporting private key material.
Provider selection: The active provider is configured at startup; switching providers requires a service restart to prevent mixed-mode operation.
Production fail closed (
ValidateRuntime,Program.cs:1080-1179): outside Development/Testing the host refuses to start onSMREMOTE_ALLOW_SOFTWARE_PROVIDER=1(the un-admitted in-process soft provider),SMREMOTE_ALLOW_EPHEMERAL_KEYS=1, missing remote URL, missing explicit key mapping, unsupported adapter kind, disabled vendor contract validation, and simulator/test-only adapter responses. The production compose overlay defaults these flags to disabled.The accepted Production provider set is
cn.sm.remote.http, PLUScn.sm.softunder one narrow, explicit exception (ADR-024): the operator setsSMREMOTE_ALLOW_SIGNED_SOFT_PACK_IN_PROD=trueand an admitted signed crypto pack suppliescn.sm.soft(Program.cs:1110-1114). Both conditions are required — the opt-in flag alone clears only the option-level gate; the registry factory (Program.cs:98-135) still faults startup with an empty provider set when no admitted pack is mounted (there is no host fallback in Production).SMREMOTE_ALLOW_SIGNED_SOFT_PACK_IN_PRODandSM_SOFT_ALLOWEDare different gates: the former authorizes an admitted, signed soft pack, the latter (AllowSoftwareProvider) authorizes the un-admitted in-process provider and stays Production-forbidden regardless. This is fail-closed-by-default and, in the admitted-pack case, arguably stronger than a blanket rejection — but it is not an unconditional “cn.sm.softrejected in Production” posture; acn.sm.softpresence backed by an admitted pack + the flag is by design, not a defect. The cryptography dossier §4.3a/§4.3b documents the same opt-in.
