Remote OpenSSL GOST Signer (OSS) · 2025-12-11
A portable, open-source recipe for signing with GOST R 34.10/34.11 using the rnix/openssl-gost image, for operators who need GOST signing where CryptoPro CSP is unavailable. It pairs with the in-process local GOST provider (ru.openssl.gost), which is the shipping component; the remote HTTP/TCP gateway here is a draft operator recipe, not a platform-integrated service — read the status banner before relying on it.
For GOST key configuration and the RootPack validation flow, see rootpack_ru_validation.md and crypto-compliance.md.
Status (verified against
src/on 2026-05-30). The local OpenSSL/Bouncy GOST provider (ru.openssl.gost) is fully implemented — see Implemented today: local provider. The remote HTTP gateway described in the Quickstart/Client sections and theSTELLAOPS_RU_OPENSSL_REMOTE_URLtoggle are a Draft / roadmap operator recipe only: no code undersrc/__Libraries/StellaOps.Cryptography.Plugin.OpenSslGostreadsSTELLAOPS_RU_OPENSSL_REMOTE_URLor routesru.openssl.gostsigning over the network. The shell snippets below are a manual, copy-paste illustration, not a shipped component. Do not assume the platform will dispatch signing to a remote host automatically.
Goals
- Remote, OSS-only signer for the
ru.openssl.gostprofile (draft recipe; not wired into the platform). - Deterministic digest harness (fixed message) for smoke checks. The committed, supported harness is
devops/tools/validate-openssl-gost.sh(usesrnix/openssl-gost:latest,md_gost12_256). - A future configurable endpoint so hosts could toggle between local and remote (see Configuration toggle).
Quickstart (remote host) · Draft recipe
Manual operator recipe — not a shipped service. The signature wire protocol below is raw TCP carrying a single JSON line via
netcat, not HTTP, despite thecurlin the client section (a quirk of this illustration). Note also that the gateway listens on 9090 (nc -l -p 9090); the-p 8088:8080published port on the container is unrelated and unused by the gateway.
# 1) Run the OpenSSL GOST container on the remote host
docker run --rm -p 8088:8080 --name gost-remote rnix/openssl-gost:latest sleep 365d
# 2) Start the lightweight gateway (one-liner, no deps; raw TCP + JSON, not HTTP)
cat > /tmp/gost-remote.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
msg_file="$(mktemp)"
sig_file="$(mktemp)"
pub_file="$(mktemp)"
trap 'rm -f "$msg_file" "$sig_file" "$pub_file"' EXIT
while true; do
# Simple netcat JSON protocol: {"message_b64":"..."}
nc -l -p 9090 -q 1 | {
read payload
msg_b64="$(echo "$payload" | jq -r .message_b64)"
echo "$msg_b64" | base64 -d > "$msg_file"
# Generate key once per container (persist by volume if desired)
if [ ! -f /tmp/gost.key.pem ]; then
openssl genpkey -engine gost -algorithm gost2012_256 -pkeyopt paramset:A -out /tmp/gost.key.pem >/dev/null
openssl pkey -engine gost -in /tmp/gost.key.pem -pubout -out /tmp/gost.pub.pem >/dev/null
fi
# Sign (nonce-driven, signatures differ each call)
openssl dgst -engine gost -md_gost12_256 -sign /tmp/gost.key.pem -out "$sig_file" "$msg_file"
# Respond with signature/public key (base64)
jq -n --arg sig_b64 "$(base64 -w0 "$sig_file")" \
--arg pub_pem "$(base64 -w0 /tmp/gost.pub.pem)" \
'{signature_b64:$sig_b64, public_key_pem_b64:$pub_pem}'
}
done
EOF
chmod +x /tmp/gost-remote.sh
/tmp/gost-remote.sh
Client invocation (any host) · Draft recipe
The gateway speaks raw TCP, so
curlworks only because it sends the request line/body and the gateway ignores HTTP framing and reads the first line as JSON. Treat this as a smoke-test convenience, not a stable protocol.
MESSAGE="stellaops-remote-gost-smoke"
curl -s -X POST http://REMOTE_HOST:9090 \
-d "{\"message_b64\":\"$(printf '%s' \"$MESSAGE\" | base64 -w0)\"}" \
| tee /tmp/gost-remote-response.json
sig_b64=$(jq -r .signature_b64 /tmp/gost-remote-response.json)
pub_pem_b64=$(jq -r .public_key_pem_b64 /tmp/gost-remote-response.json)
printf '%s' "$pub_pem_b64" | base64 -d > /tmp/gost-remote.pub.pem
printf '%s' "$MESSAGE" > /tmp/gost-remote.msg
printf '%s' "$sig_b64" | base64 -d > /tmp/gost-remote.sig
# Verify locally
openssl dgst -engine gost -md_gost12_256 \
-verify /tmp/gost-remote.pub.pem \
-signature /tmp/gost-remote.sig /tmp/gost-remote.msg
Configuration toggle (Draft / NOT IMPLEMENTED)
STELLAOPS_RU_OPENSSL_REMOTE_URLis not consumed by any code. As of 2026-05-30 there is no reader for this variable undersrc/and no path that routesru.openssl.gostsigning through an HTTP/TCP gateway.OpenSslGostProvideralways signs in-process using the locally loaded PEM key (BouncyCastleECGost3410Signer). Setting the variable has no effect.- The only comparable shipped pattern is
STELLAOPS_SMREMOTE_URLfor the separateSmRemotemodule (src/SmRemote), not for GOST. If a remote GOST signer is built later, this section should be updated to match the implementing symbol. - What does work today: keep the local Linux provider enabled with
STELLAOPS_CRYPTO_ENABLE_RU_OPENSSL=1(default-on for Linux). This flag is read byCryptoProviderRegistryValidator.EnforceRuLinuxDefaults(inStellaOps.Cryptography.DependencyInjection), which fails closed on Linux: if the flag is on andru.openssl.gostis not registered, startup throws (crypto.di.ru_openssl_required).
Implemented today: local provider
The shipping component is the in-process provider OpenSslGostProvider (Name = "ru.openssl.gost") in src/__Libraries/StellaOps.Cryptography.Plugin.OpenSslGost. Register it with services.AddOpenSslGostProvider(...) (OpenSslCryptoServiceCollectionExtensions).
- Capabilities:
SigningandVerificationonly — no hashing or password hashing (those throwNotSupportedException). Algorithms:GOST12-256(SignatureAlgorithms.GostR3410_2012_256) andGOST12-512(SignatureAlgorithms.GostR3410_2012_512). Streebog digests come fromGostDigestUtilities(Gost3411_2012_256Digest/Gost3411_2012_512Digest). - Keys are external PEM material.
UpsertSigningKey/RemoveSigningKeyare intentionally unsupported; keys are loaded at startup from disk viaOpenSslPemLoaderand bound from configuration (StellaOps:Crypto:OpenSsl:Keys, seedocs/security/rootpack_ru_validation.md§2.1). EachOpenSslGostKeyOptionsentry has:KeyId(required),Algorithm(required; defaults toGOST12-256),PrivateKeyPath(required, must exist or startup throws).PrivateKeyPassphraseEnvVar— optional env var name holding the PEM passphrase (avoids inline secrets).CertificatePath+CertificatePasswordEnvVar— optional PEM/DER/PKCS#12 cert used to populate the JWKx5cchain.SignatureFormat—Der(default) orRaw(concatenateds || r, perGostSignatureFormat).
- Output:
SignAsyncreturns DER or raw bytes perSignatureFormat;ExportPublicJsonWebKeyemits aKty=ECJWK withCrvofGOST3410-2012-256/-512,use=sig, and key-opssign/verify. - Validation harness: the supported, committed smoke test is
devops/tools/validate-openssl-gost.sh— it pullsrnix/openssl-gost:latest, computes anmd_gost12_256digest over a fixed message, signs twice, verifies, and writessummary.json(signatures_deterministicis expected to befalse).
Determinism
- Digest is deterministic (
md_gost12_256over caller-supplied message). - Signatures vary per request (nonce) but verify deterministically; capture
signature_b64andpublic_key_pem_b64for evidence.
Operational notes
- The draft remote-host recipe needs Docker + the
rnix/openssl-gostimage (no vendor binaries). - The draft gateway listens on raw TCP 9090 with no auth or transport security; the
nc-based protocol is unauthenticated. Front it with mTLS or an SSH tunnel before using it anywhere but a throwaway smoke test — and remember it is not platform-integrated (see status banner). - Persist
/tmp/gost.key.pemvia a volume if you need a stable key; otherwise accept ephemeral keys for testing. For the local provider, key identity (KeyId) comes from configuration, not from the PEM file.
Attach to sprint evidence
- Store
gost-remote-response.json,gost-remote.pub.pem, and verification output with the sprint log. - Record the remote endpoint and run timestamp in the sprint Execution Log.
