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:

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:16 still reads using StellaOps.Cryptography.Plugin.SmRemote; even though the transport types now live in the StellaOps.Cryptography.Sm.Contracts project. Follow the .csproj references, not the using lines. StellaOps.SmRemote.Service.csproj references Sm.Contracts only, and carries an inline boundary comment plus a DoesNotContain boundary test asserting no provider-plug-in reference.

Data Flow

  1. An upstream service (Signer, AirGap) sends a cryptographic operation request (sign, verify, hash, encrypt, decrypt) to SM Remote.
  2. SM Remote selects the configured provider via the ICryptoProviderRegistry — preferring cn.sm.remote.http, then cn.sm.soft (ResolveProvider, Program.cs:599-612). The registry is composed at startup (Program.cs:63-135): cn.sm.remote.http is constructed directly by the host from Sm.Contracts (no in-process key material, so it is not packaged); cn.sm.soft is sourced from the mounted signed crypto pack via MountedSmCryptoRuntimePluginLoader, falling back to the reflective in-process provider only in Development/Testing (SmHostDevFallbackProviderFactory). If neither yields a provider, startup throws — Production has no fallback.
  3. For signing/verification in Development/Testing local harnesses, and only when SMREMOTE_ALLOW_EPHEMERAL_KEYS=true with a positive SMREMOTE_EPHEMERAL_KEY_TTL, the service may seed an ephemeral SM2 key pair on first use for the given keyId using the BouncyCastle SM2P256V1 named curve (GMNamedCurves.GetByName). Each seeded key carries TTL/expiresAt/seededBy metadata, is registered in the in-process EphemeralKeyRegistry, and emits a sm-remote.key.seeded audit event. Outside those harnesses (or when the flag is off) unknown keyId values fail with a 404 rather than creating signing authority. EphemeralKeyRotationService sweeps every 30s, evicting expired keys and emitting sm-remote.key.rotated.
  4. In production remote-provider mode, SM Remote first maps the request keyId to 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.
  5. The provider executes the operation and returns the result. Vendor-contract or transport failures surface as fail-closed Results.Problem responses (HTTP 502 for crypto operations, 503 for /health and /status) with a failureClass extension.
  6. 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/LibraryPurpose
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.ContractsThe 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.LocalizationLocalized endpoint descriptions and error messages
StellaOps.CryptographyCanonical crypto assembly (ICryptoProvider, CryptoProviderRegistry)

ADR-024 boundary (verified in StellaOps.SmRemote.Service.csproj): the service references StellaOps.Cryptography, StellaOps.Cryptography.Sm.Contracts, StellaOps.Plugin, StellaOps.Auth.ServerIntegration, and StellaOps.Localization — and neither StellaOps.Cryptography.Plugin.SmSoft nor StellaOps.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; pkcs11 is only one of the three valid adapter kind labels (sm-hsm, sdf, pkcs11) the bridge may declare in its contract metadata.

Endpoints

MethodPathDescriptionAuth
POST/hashCompute SM3 hash of input data; returns algorithmId, hashBase64, hashHex. Only SM3 is accepted.sm-remote:hash
POST/encryptSM4 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/decryptSM4 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/signSM2 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/verifySM2 signature verification; returns { "valid": bool }.sm-remote:verify
GET/statusProvider availability, name, supported algorithms (SM2); in remote mode also returns the vendor adapterId/adapterKind/contractVersion and discovered keys[].anonymous
GET/healthHealth 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:

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): Sm4SoftCipher uses PKCS7 for SM4-ECB/SM4-CBC (matching the original inline ECB path and the GOST CBC envelope). The in-process SmPlugin (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; SmPlugin behavior 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 /keys endpoint. /keys is a vendor-bridge endpoint that SM Remote consumes (see Vendor Adapter Contract) to discover and validate configured keys. Endpoint names (WithName) are SmRemoteHash, 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:

MethodPathContract requirement
GET/healthReturns status, availability, adapterId, supported adapterKind, exact contract version, and production evidence.
GET/statusReturns availability, provider name, supported algorithms including SM2, adapter identity, contract version, and production evidence.
GET/keysReturns discoverable key descriptors with local/remote identity, algorithm, and usages; configured keys must be present.
POST/signAccepts mapped remote key ID, algorithm, and base64 PAE payload; returns base64 signature plus adapter metadata.
POST/verifyAccepts mapped remote key ID, algorithm, payload, and signature; returns verification result plus adapter metadata.
POST/encryptAccepts mapped remote key ID, algorithm, and base64 payload; returns base64 ciphertext plus adapter metadata.
POST/decryptAccepts 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.

EndpointRequired scope
POST /hashsm-remote:hash
POST /encryptsm-remote:encrypt
POST /decryptsm-remote:decrypt
POST /signsm-remote:sign
POST /verifysm-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)PurposeDefault
SMREMOTE_PROVIDER / SM_REMOTE_PROVIDERActive 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_URLAbsolute HTTP(S) base address of the vendor bridge (required in remote mode).(empty)
SM_REMOTE_HSM_API_KEYBearer token sent to the vendor bridge.(empty)
SM_REMOTE_HSM_REQUIRE_API_KEYIf true, the API key is mandatory in remote mode.false
SM_REMOTE_HSM_TIMEOUTHTTP client timeout in milliseconds.30000
SM_REMOTE_HSM_ADAPTER_KIND / SMREMOTE_ADAPTER_KINDExpected adapter kind: sm-hsm, sdf, or pkcs11.sm-hsm
SM_REMOTE_HSM_CONTRACT_VERSION / SMREMOTE_ADAPTER_CONTRACT_VERSIONExpected vendor contract version.sm-remote-vendor-adapter.v2
SMREMOTE_SM4_MODE / SM_REMOTE_SM4_MODESM4 symmetric mode: CBC (default), GCM, or ECB-Interop (legacy waiver).CBC
SM_REMOTE_HSM_REQUIRE_VENDOR_CONTRACT / SMREMOTE_REQUIRE_VENDOR_CONTRACTEnforce strict vendor contract validation.true outside the local harness
SMREMOTE_KEYS / SM_REMOTE_KEYS / SM_REMOTE_HSM_KEYSExplicit local→remote key mapping, comma/semicolon-separated localId=remoteId tokens.(empty)
SMREMOTE_ALLOW_SOFTWARE_PROVIDER / SM_SOFT_ALLOWEDPermit 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_PRODADR-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_ALLOWEDPermit ephemeral SM2 key seeding (local harness only).false
SMREMOTE_EPHEMERAL_KEY_TTL / SM_EPHEMERAL_KEY_TTLRequired 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_IDStable runtime instance id stamped into ephemeral-key audit records (seededBy).fresh GUID per process
SMREMOTE_DURABLE_KEY_STOREOptional durable key-store path (bound but not yet used by the seeder).(unset)
SM_REMOTE_PORTHost port published by the compose overlay.56080

Mounted signed crypto-pack keys (SmRemote:RuntimePlugins section, MountedSmCryptoRuntimePluginOptions):

Config keyPurposeDefault
SmRemote:RuntimePlugins:RootPathContainer root for mounted SM crypto-provider bundles./app/plugins/crypto
SmRemote:RuntimePlugins:ProfileMounted profile selected when none is configured.crypto-sm
SmRemote:RuntimePlugins:TrustRootPathCosign public key that admits the signed pack./app/etc/certificates/trust-roots/plugins/crypto/cosign.pub
SmRemote:RuntimePlugins:AllowUnsignedSkip 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