up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
cryptopro-linux-csp / build-and-test (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-09 09:38:09 +02:00
parent bc0762e97d
commit 108d1c64b3
193 changed files with 7265 additions and 13029 deletions

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<InvariantGlobalization>true</InvariantGlobalization>
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
</PropertyGroup>
</Project>

View File

@@ -1,31 +1,36 @@
# syntax=docker/dockerfile:1.7
FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0 AS build
WORKDIR /src
COPY ops/cryptopro/linux-csp-service/CryptoProLinuxApi.csproj .
RUN dotnet restore CryptoProLinuxApi.csproj
COPY ops/cryptopro/linux-csp-service/ .
RUN dotnet publish CryptoProLinuxApi.csproj -c Release -r linux-x64 --self-contained true \
/p:PublishSingleFile=true /p:DebugType=none /p:DebugSymbols=false -o /app/publish
FROM ubuntu:22.04
ARG CRYPTOPRO_ACCEPT_EULA=0
ENV DEBIAN_FRONTEND=noninteractive \
CRYPTOPRO_ACCEPT_EULA=1 \
CRYPTOPRO_ACCEPT_EULA=${CRYPTOPRO_ACCEPT_EULA} \
CRYPTOPRO_MINIMAL=1
WORKDIR /app
# System deps
# System deps for CryptoPro installer
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip tar xz-utils && \
apt-get install -y --no-install-recommends tar xz-utils ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Copy CryptoPro packages (provided in repo) and installer
# CryptoPro packages (provided in repo) and installer
COPY opt/cryptopro/downloads/*.tgz /opt/cryptopro/downloads/
COPY ops/cryptopro/install-linux-csp.sh /usr/local/bin/install-linux-csp.sh
RUN chmod +x /usr/local/bin/install-linux-csp.sh
# Install CryptoPro CSP
RUN /usr/local/bin/install-linux-csp.sh
# Install CryptoPro CSP (requires CRYPTOPRO_ACCEPT_EULA=1 at build/runtime)
RUN CRYPTOPRO_ACCEPT_EULA=${CRYPTOPRO_ACCEPT_EULA} /usr/local/bin/install-linux-csp.sh
# Python deps
COPY ops/cryptopro/linux-csp-service/requirements.txt /app/requirements.txt
RUN pip3 install --no-cache-dir -r /app/requirements.txt
# App
COPY ops/cryptopro/linux-csp-service/app.py /app/app.py
# Copy published .NET app
COPY --from=build /app/publish/ /app/
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
ENTRYPOINT ["/app/CryptoProLinuxApi"]

View File

@@ -0,0 +1,118 @@
using System.Diagnostics;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(opts =>
{
opts.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
var app = builder.Build();
const string CsptestPath = "/opt/cprocsp/bin/amd64/csptest";
app.MapGet("/health", () =>
{
if (!File.Exists(CsptestPath))
{
return Results.Problem(statusCode: 500, detail: "csptest not found; ensure CryptoPro CSP is installed");
}
return Results.Ok(new { status = "ok", csptest = CsptestPath });
});
app.MapGet("/license", () =>
{
var result = RunProcess([CsptestPath, "-keyset", "-info"], allowFailure: true);
return Results.Json(result);
});
app.MapPost("/hash", async (HashRequest request) =>
{
byte[] data;
try
{
data = Convert.FromBase64String(request.DataBase64);
}
catch (FormatException)
{
return Results.BadRequest(new { error = "Invalid base64" });
}
var inputPath = Path.GetTempFileName();
var outputPath = Path.GetTempFileName();
await File.WriteAllBytesAsync(inputPath, data);
var result = RunProcess([CsptestPath, "-hash", "-alg", "GOST12_256", "-in", inputPath, "-out", outputPath], allowFailure: true);
string? digestBase64 = null;
if (File.Exists(outputPath))
{
var digestBytes = await File.ReadAllBytesAsync(outputPath);
digestBase64 = Convert.ToBase64String(digestBytes);
}
TryDelete(inputPath);
TryDelete(outputPath);
return Results.Json(new
{
result.ExitCode,
result.Output,
digest_b64 = digestBase64
});
});
app.MapPost("/keyset/init", (KeysetRequest request) =>
{
var name = string.IsNullOrWhiteSpace(request.Name) ? "default" : request.Name!;
var result = RunProcess([CsptestPath, "-keyset", "-newkeyset", "-container", name, "-keytype", "none"], allowFailure: true);
return Results.Json(result);
});
app.Run("http://0.0.0.0:8080");
static void TryDelete(string path)
{
try { File.Delete(path); } catch { /* ignore */ }
}
static ProcessResult RunProcess(string[] args, bool allowFailure = false)
{
try
{
var psi = new ProcessStartInfo
{
FileName = args[0],
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
ArgumentList = { }
};
for (var i = 1; i < args.Length; i++)
{
psi.ArgumentList.Add(args[i]);
}
using var proc = Process.Start(psi)!;
var output = proc.StandardOutput.ReadToEnd();
output += proc.StandardError.ReadToEnd();
proc.WaitForExit();
if (proc.ExitCode != 0 && !allowFailure)
{
throw new InvalidOperationException($"Command failed with exit {proc.ExitCode}: {output}");
}
return new ProcessResult(proc.ExitCode, output);
}
catch (Exception ex)
{
if (!allowFailure)
{
throw;
}
return new ProcessResult(-1, ex.ToString());
}
}
sealed record HashRequest([property: JsonPropertyName("data_b64")] string DataBase64);
sealed record KeysetRequest([property: JsonPropertyName("name")] string? Name);
sealed record ProcessResult(int ExitCode, string Output);

View File

@@ -1,6 +1,6 @@
# CryptoPro Linux CSP Service (experimental)
# CryptoPro Linux CSP Service (.NET minimal API)
Minimal FastAPI wrapper around the Linux CryptoPro CSP binaries to prove installation and expose simple operations.
Minimal HTTP wrapper around the Linux CryptoPro CSP binaries to prove installation and hash operations.
## Build
@@ -8,18 +8,26 @@ Minimal FastAPI wrapper around the Linux CryptoPro CSP binaries to prove install
docker build -t cryptopro-linux-csp -f ops/cryptopro/linux-csp-service/Dockerfile .
```
`CRYPTOPRO_ACCEPT_EULA` defaults to `0` (build will fail); set to `1` only if you hold a valid CryptoPro license and accept the vendor EULA:
```bash
docker build -t cryptopro-linux-csp \
--build-arg CRYPTOPRO_ACCEPT_EULA=1 \
-f ops/cryptopro/linux-csp-service/Dockerfile .
```
## Run
```bash
docker run --rm -p 8080:8080 cryptopro-linux-csp
docker run --rm -p 18080:8080 --name cryptopro-linux-csp-test cryptopro-linux-csp
```
Endpoints:
- `GET /health` — checks `csptest` presence.
- `GET /license` — runs `csptest -license`.
- `POST /hash` with `{ "data_b64": "<base64>" }`runs `csptest -hash -hash_alg gost12_256`.
- `GET /license` — runs `csptest -keyset -info` (reports errors if no keyset/token present).
- `POST /hash` with `{"data_b64":"<base64>"}`hashes using `csptest -hash -alg GOST12_256`.
- `POST /keyset/init` with optional `{"name":"<container>"}` — creates an empty keyset (`-keytype none`) to silence missing-container warnings.
## Notes
- Uses the provided CryptoPro `.tgz` bundles under `opt/cryptopro/downloads`. Ensure you have rights to these binaries; the image builds with `CRYPTOPRO_ACCEPT_EULA=1`.
- Default install is minimal (no browser/plugin). Set `CRYPTOPRO_INCLUDE_PLUGIN=1` if you need plugin packages.
- This is not a production service; intended for validation only.
Notes:
- Uses the provided CryptoPro `.tgz` bundles under `opt/cryptopro/downloads`. Do not set `CRYPTOPRO_ACCEPT_EULA=1` unless you are licensed to use these binaries.
- Minimal, headless install; browser/plugin packages are not included.

View File

@@ -1,57 +0,0 @@
import base64
import subprocess
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="CryptoPro Linux CSP Service", version="0.1.0")
CSPTEST = Path("/opt/cprocsp/bin/amd64/csptest")
def run_cmd(cmd: list[str], input_bytes: Optional[bytes] = None, allow_fail: bool = False) -> str:
try:
proc = subprocess.run(
cmd,
input=input_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=True,
)
return proc.stdout.decode("utf-8", errors="replace")
except subprocess.CalledProcessError as exc:
output = exc.stdout.decode("utf-8", errors="replace") if exc.stdout else ""
if allow_fail:
return output
raise HTTPException(status_code=500, detail={"cmd": cmd, "output": output})
@app.get("/health")
def health():
if not CSPTEST.exists():
raise HTTPException(status_code=500, detail="csptest binary not found; ensure CryptoPro CSP is installed")
return {"status": "ok", "csptest": str(CSPTEST)}
@app.get("/license")
def license_info():
output = run_cmd([str(CSPTEST), "-keyset", "-info"], allow_fail=True)
return {"output": output}
class HashRequest(BaseModel):
data_b64: str
@app.post("/hash")
def hash_data(body: HashRequest):
try:
data = base64.b64decode(body.data_b64)
except Exception:
raise HTTPException(status_code=400, detail="Invalid base64")
cmd = [str(CSPTEST), "-hash", "-in", "-", "-hash_alg", "gost12_256"]
output = run_cmd(cmd, input_bytes=data)
return {"output": output}

View File

@@ -1,2 +0,0 @@
fastapi==0.111.0
uvicorn[standard]==0.30.1

View File

@@ -1,193 +0,0 @@
# syntax=docker/dockerfile:1.7
# Wine CSP Service - GOST cryptographic operations via Wine-hosted CryptoPro CSP
#
# WARNING: For TEST VECTOR GENERATION ONLY - not for production signing
#
# Build:
# docker buildx build -f ops/wine-csp/Dockerfile -t wine-csp:latest .
#
# Run:
# docker run -p 5099:5099 -e WINE_CSP_MODE=limited wine-csp:latest
# ==============================================================================
# Stage 1: Build .NET application for Windows x64
# ==============================================================================
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0-preview-bookworm-slim
FROM ${SDK_IMAGE} AS build
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
DOTNET_NOLOGO=1 \
DOTNET_ROLL_FORWARD=LatestMajor \
SOURCE_DATE_EPOCH=1704067200
WORKDIR /src
# Copy solution files and NuGet configuration
COPY Directory.Build.props Directory.Build.rsp NuGet.config ./
# Copy local NuGet packages if available
COPY local-nugets/ ./local-nugets/
# Copy Wine CSP Service source
COPY src/__Tools/WineCspService/ ./src/__Tools/WineCspService/
# Copy GostCryptography fork dependency
COPY third_party/forks/AlexMAS.GostCryptography/ ./third_party/forks/AlexMAS.GostCryptography/
# Restore and publish for Windows x64 (runs under Wine)
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore src/__Tools/WineCspService/WineCspService.csproj && \
dotnet publish src/__Tools/WineCspService/WineCspService.csproj \
-c Release \
-r win-x64 \
--self-contained true \
-o /app/publish \
/p:PublishSingleFile=true \
/p:EnableCompressionInSingleFile=true \
/p:DebugType=none \
/p:DebugSymbols=false
# ==============================================================================
# Stage 2: Runtime with Wine and CryptoPro CSP support
# ==============================================================================
FROM ubuntu:22.04 AS runtime
# OCI Image Labels
LABEL org.opencontainers.image.title="StellaOps Wine CSP Service" \
org.opencontainers.image.description="GOST cryptographic test vector generation via Wine-hosted CryptoPro CSP" \
org.opencontainers.image.vendor="StellaOps" \
org.opencontainers.image.source="https://git.stella-ops.org/stellaops/router" \
com.stellaops.component="wine-csp" \
com.stellaops.security.production-signing="false" \
com.stellaops.security.test-vectors-only="true"
# Wine CSP service configuration
ARG WINE_CSP_PORT=5099
ARG APP_USER=winecsp
ARG APP_UID=10001
ARG APP_GID=10001
ENV DEBIAN_FRONTEND=noninteractive \
# Wine configuration
WINEDEBUG=-all \
WINEPREFIX=/home/${APP_USER}/.wine \
WINEARCH=win64 \
# Service configuration
WINE_CSP_PORT=${WINE_CSP_PORT} \
ASPNETCORE_URLS=http://+:${WINE_CSP_PORT} \
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
# CSP configuration
WINE_CSP_MODE=limited \
WINE_CSP_INSTALLER_PATH=/opt/cryptopro/csp-installer.msi \
WINE_CSP_LOG_LEVEL=Information \
NODE_PATH=/usr/local/lib/node_modules \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
# Display for Wine (headless)
DISPLAY=:99
# Install Wine and dependencies
# Using WineHQ stable repository for consistent Wine version
RUN set -eux; \
dpkg --add-architecture i386; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg2 \
software-properties-common \
wget \
xvfb \
cabextract \
p7zip-full \
procps; \
# Add WineHQ repository key
mkdir -pm755 /etc/apt/keyrings; \
wget -O /etc/apt/keyrings/winehq-archive.key \
https://dl.winehq.org/wine-builds/winehq.key; \
# Add WineHQ repository
wget -NP /etc/apt/sources.list.d/ \
https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources; \
apt-get update; \
# Install Wine stable
apt-get install -y --no-install-recommends \
winehq-stable; \
# Install winetricks for runtime dependencies
wget -O /usr/local/bin/winetricks \
https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks; \
chmod +x /usr/local/bin/winetricks; \
# Cleanup
apt-get clean; \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install Node.js + Playwright (headless Chromium) for CryptoPro downloader
RUN set -eux; \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
apt-get update; \
apt-get install -y --no-install-recommends \
nodejs \
rpm2cpio \
cpio; \
npm install -g --no-progress playwright-chromium@1.48.2; \
npx playwright install-deps chromium; \
npx playwright install chromium; \
chown -R ${APP_UID}:${APP_GID} /ms-playwright || true; \
apt-get clean; \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Create non-root user for Wine service
# Note: Wine requires writable home directory for prefix
RUN groupadd -r -g ${APP_GID} ${APP_USER} && \
useradd -r -u ${APP_UID} -g ${APP_GID} -m -d /home/${APP_USER} -s /bin/bash ${APP_USER} && \
mkdir -p /app /opt/cryptopro /var/log/wine-csp /var/run/wine-csp && \
chown -R ${APP_UID}:${APP_GID} /app /home/${APP_USER} /opt/cryptopro /var/log/wine-csp /var/run/wine-csp
WORKDIR /app
# Copy application from build stage
COPY --from=build --chown=${APP_UID}:${APP_GID} /app/publish/ ./
# Copy supporting scripts
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/healthcheck.sh /usr/local/bin/healthcheck.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/install-csp.sh /usr/local/bin/install-csp.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/fetch-cryptopro.py /usr/local/bin/fetch-cryptopro.py
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/download-cryptopro.sh /usr/local/bin/download-cryptopro.sh
COPY --chown=${APP_UID}:${APP_GID} scripts/crypto/download-cryptopro-playwright.cjs /usr/local/bin/download-cryptopro-playwright.cjs
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh /usr/local/bin/install-csp.sh /usr/local/bin/fetch-cryptopro.py /usr/local/bin/download-cryptopro.sh /usr/local/bin/download-cryptopro-playwright.cjs
# Switch to non-root user for Wine prefix initialization
USER ${APP_UID}:${APP_GID}
# Initialize Wine prefix (creates .wine directory with Windows environment)
# This must run as the app user to set correct ownership
# Using xvfb-run for headless Wine initialization
RUN set -eux; \
# Start virtual framebuffer and initialize Wine
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" \
wine64 wineboot --init; \
wineserver --wait; \
# Install Visual C++ 2019 runtime via winetricks (required for .NET)
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" \
winetricks -q vcrun2019 || true; \
wineserver --wait; \
# Set Windows version to Windows 10 for compatibility
wine64 reg add "HKCU\\Software\\Wine\\Version" /v Windows /d "win10" /f || true; \
wineserver --wait; \
# Cleanup Wine temp files
rm -rf /home/${APP_USER}/.cache/winetricks /tmp/.X* /tmp/winetricks* || true
EXPOSE ${WINE_CSP_PORT}
# Health check using custom script that probes /health endpoint
# Extended start_period due to Wine initialization time
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD /usr/local/bin/healthcheck.sh
# Volumes for persistence and CSP installer
# - Wine prefix: stores CSP installation, certificates, keys
# - CSP installer: mount customer-provided CryptoPro MSI here
# - Logs: service logs
VOLUME ["/home/${APP_USER}/.wine", "/opt/cryptopro", "/var/log/wine-csp"]
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["wine64", "/app/WineCspService.exe"]

View File

@@ -1,62 +0,0 @@
#!/bin/bash
# CryptoPro Linux package fetcher (Playwright-driven)
# Uses the Node-based Playwright crawler to authenticate (if required) and
# download Linux CSP installers. Intended to run once per container startup.
set -euo pipefail
OUTPUT_DIR="${CRYPTOPRO_OUTPUT_DIR:-/opt/cryptopro/downloads}"
MARKER="${CRYPTOPRO_DOWNLOAD_MARKER:-${OUTPUT_DIR}/.downloaded}"
FORCE="${CRYPTOPRO_FORCE_DOWNLOAD:-0}"
UNPACK="${CRYPTOPRO_UNPACK:-1}"
DRY_RUN="${CRYPTOPRO_DRY_RUN:-1}"
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [crypto-fetch] $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [crypto-fetch] [ERROR] $*" >&2
}
if [[ -f "${MARKER}" && "${FORCE}" != "1" ]]; then
log "Download marker present at ${MARKER}; skipping (set CRYPTOPRO_FORCE_DOWNLOAD=1 to refresh)."
exit 0
fi
log "Ensuring CryptoPro Linux packages are available (dry-run unless CRYPTOPRO_DRY_RUN=0)"
log " Output dir: ${OUTPUT_DIR}"
log " Unpack: ${UNPACK}"
mkdir -p "${OUTPUT_DIR}"
# Export defaults for the Playwright downloader
export CRYPTOPRO_OUTPUT_DIR="${OUTPUT_DIR}"
export CRYPTOPRO_UNPACK="${UNPACK}"
export CRYPTOPRO_DRY_RUN="${DRY_RUN}"
export CRYPTOPRO_URL="${CRYPTOPRO_URL:-https://cryptopro.ru/products/csp/downloads#latest_csp50r3_linux}"
export CRYPTOPRO_EMAIL="${CRYPTOPRO_EMAIL:-contact@stella-ops.org}"
export CRYPTOPRO_PASSWORD="${CRYPTOPRO_PASSWORD:-Hoko33JD3nj3aJD.}"
if ! node /usr/local/bin/download-cryptopro-playwright.cjs; then
rc=$?
if [[ "${rc}" == "2" ]]; then
log "Playwright downloader blocked by auth/captcha; skipping download (set CRYPTOPRO_DEBUG=1 for details)."
exit 0
fi
log_error "Playwright downloader failed (exit=${rc})"
exit "${rc}"
fi
if [[ "${DRY_RUN}" == "0" ]]; then
touch "${MARKER}"
log "Download complete; marker written to ${MARKER}"
else
log "Dry-run mode; marker not written. Set CRYPTOPRO_DRY_RUN=0 to fetch binaries."
fi
# List latest artifacts (best-effort)
if compgen -G "${OUTPUT_DIR}/*" > /dev/null; then
log "Artifacts in ${OUTPUT_DIR}:"
find "${OUTPUT_DIR}" -maxdepth 1 -type f -printf " %f (%s bytes)\n" | head -20
fi

View File

@@ -1,272 +0,0 @@
#!/bin/bash
# Wine CSP Service Entrypoint
#
# Initializes Wine environment and starts the WineCspService under Wine.
# For TEST VECTOR GENERATION ONLY - not for production signing.
set -euo pipefail
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
WINE_CSP_PORT="${WINE_CSP_PORT:-5099}"
WINE_CSP_MODE="${WINE_CSP_MODE:-limited}"
WINE_CSP_INSTALLER_PATH="${WINE_CSP_INSTALLER_PATH:-/opt/cryptopro/csp-installer.msi}"
WINE_CSP_LOG_LEVEL="${WINE_CSP_LOG_LEVEL:-Information}"
WINE_PREFIX="${WINEPREFIX:-$HOME/.wine}"
DISPLAY="${DISPLAY:-:99}"
CSP_DOWNLOAD_MARKER="${WINE_CSP_INSTALLER_PATH}.downloaded"
CRYPTOPRO_DOWNLOAD_DIR="${CRYPTOPRO_DOWNLOAD_DIR:-/opt/cryptopro/downloads}"
CRYPTOPRO_DOWNLOAD_MARKER="${CRYPTOPRO_DOWNLOAD_MARKER:-${CRYPTOPRO_DOWNLOAD_DIR}/.downloaded}"
CRYPTOPRO_FETCH_ON_START="${CRYPTOPRO_FETCH_ON_START:-1}"
# Marker files
CSP_INSTALLED_MARKER="${WINE_PREFIX}/.csp_installed"
WINE_INITIALIZED_MARKER="${WINE_PREFIX}/.wine_initialized"
# Log prefix for structured logging
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [entrypoint] $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [entrypoint] [ERROR] $*" >&2
}
# ------------------------------------------------------------------------------
# Virtual Framebuffer Management
# ------------------------------------------------------------------------------
start_xvfb() {
if ! pgrep -x Xvfb > /dev/null; then
log "Starting Xvfb virtual framebuffer on display ${DISPLAY}"
Xvfb "${DISPLAY}" -screen 0 1024x768x24 &
sleep 2
fi
}
stop_xvfb() {
if pgrep -x Xvfb > /dev/null; then
log "Stopping Xvfb"
pkill -x Xvfb || true
fi
}
# ------------------------------------------------------------------------------
# Wine Initialization
# ------------------------------------------------------------------------------
initialize_wine() {
if [[ -f "${WINE_INITIALIZED_MARKER}" ]]; then
log "Wine prefix already initialized"
return 0
fi
log "Initializing Wine prefix at ${WINE_PREFIX}"
start_xvfb
# Initialize Wine prefix
wine64 wineboot --init 2>/dev/null || true
wineserver --wait
# Set Windows version for CryptoPro compatibility
wine64 reg add "HKCU\\Software\\Wine\\Version" /v Windows /d "win10" /f 2>/dev/null || true
wineserver --wait
# Create marker
touch "${WINE_INITIALIZED_MARKER}"
log "Wine prefix initialized successfully"
}
# ------------------------------------------------------------------------------
# CryptoPro Linux Downloads (Playwright-driven)
# ------------------------------------------------------------------------------
download_linux_packages() {
if [[ "${CRYPTOPRO_FETCH_ON_START}" == "0" ]]; then
log "Skipping CryptoPro Linux fetch (CRYPTOPRO_FETCH_ON_START=0)"
return 0
fi
if [[ -f "${CRYPTOPRO_DOWNLOAD_MARKER}" && "${CRYPTOPRO_FORCE_DOWNLOAD:-0}" != "1" ]]; then
log "CryptoPro download marker present at ${CRYPTOPRO_DOWNLOAD_MARKER}; skipping fetch"
return 0
fi
log "Ensuring CryptoPro Linux packages via Playwright (dry-run unless CRYPTOPRO_DRY_RUN=0)"
export CRYPTOPRO_DOWNLOAD_MARKER
export CRYPTOPRO_OUTPUT_DIR="${CRYPTOPRO_DOWNLOAD_DIR}"
export CRYPTOPRO_UNPACK="${CRYPTOPRO_UNPACK:-1}"
if /usr/local/bin/download-cryptopro.sh; then
if [[ "${CRYPTOPRO_DRY_RUN:-1}" != "0" ]]; then
log "CryptoPro downloader ran in dry-run mode; set CRYPTOPRO_DRY_RUN=0 to fetch binaries"
else
[[ -f "${CRYPTOPRO_DOWNLOAD_MARKER}" ]] || touch "${CRYPTOPRO_DOWNLOAD_MARKER}"
log "CryptoPro Linux artifacts staged in ${CRYPTOPRO_DOWNLOAD_DIR}"
fi
else
log_error "CryptoPro Playwright download failed"
fi
}
# ------------------------------------------------------------------------------
# CryptoPro CSP Installation
# ------------------------------------------------------------------------------
install_cryptopro() {
# Check if already installed
if [[ -f "${CSP_INSTALLED_MARKER}" ]]; then
log "CryptoPro CSP already installed"
return 0
fi
# Attempt to download installer if missing (dry-run by default)
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log "CryptoPro CSP installer not found at ${WINE_CSP_INSTALLER_PATH}; attempting crawl/download (dry-run unless CRYPTOPRO_DRY_RUN=0)."
if ! CRYPTOPRO_OUTPUT="${WINE_CSP_INSTALLER_PATH}" /usr/local/bin/fetch-cryptopro.py; then
log_error "CryptoPro CSP download failed; continuing without CSP (limited mode)"
return 0
fi
fi
# Check if installer is available
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log "CryptoPro CSP installer not found at ${WINE_CSP_INSTALLER_PATH}"
log "Service will run in limited mode without CSP"
return 0
fi
log "Installing CryptoPro CSP from ${WINE_CSP_INSTALLER_PATH}"
start_xvfb
# Run the CSP installation script
if /usr/local/bin/install-csp.sh; then
touch "${CSP_INSTALLED_MARKER}"
log "CryptoPro CSP installed successfully"
else
log_error "CryptoPro CSP installation failed"
return 1
fi
}
# ------------------------------------------------------------------------------
# Service Configuration
# ------------------------------------------------------------------------------
configure_service() {
log "Configuring Wine CSP service"
log " Mode: ${WINE_CSP_MODE}"
log " Port: ${WINE_CSP_PORT}"
log " Log Level: ${WINE_CSP_LOG_LEVEL}"
# Configure Wine debug output based on log level
case "${WINE_CSP_LOG_LEVEL}" in
Trace|Debug)
export WINEDEBUG="warn+all"
;;
Information)
export WINEDEBUG="-all"
;;
Warning|Error|Critical)
export WINEDEBUG="-all"
;;
*)
export WINEDEBUG="-all"
;;
esac
# Set ASP.NET Core environment
export ASPNETCORE_URLS="http://+:${WINE_CSP_PORT}"
export ASPNETCORE_ENVIRONMENT="${ASPNETCORE_ENVIRONMENT:-Production}"
export Logging__LogLevel__Default="${WINE_CSP_LOG_LEVEL}"
# Check if CSP is available
if [[ -f "${CSP_INSTALLED_MARKER}" ]]; then
export WINE_CSP_CSP_AVAILABLE="true"
log "CryptoPro CSP is available"
else
export WINE_CSP_CSP_AVAILABLE="false"
log "Running without CryptoPro CSP (limited mode)"
fi
}
# ------------------------------------------------------------------------------
# Startup Validation
# ------------------------------------------------------------------------------
validate_environment() {
log "Validating environment"
# Check Wine is available
if ! command -v wine64 &> /dev/null; then
log_error "wine64 not found in PATH"
exit 1
fi
# Check application exists
if [[ ! -f "/app/WineCspService.exe" ]]; then
log_error "WineCspService.exe not found at /app/"
exit 1
fi
# Verify Wine prefix is writable
if [[ ! -w "${WINE_PREFIX}" ]]; then
log_error "Wine prefix ${WINE_PREFIX} is not writable"
exit 1
fi
log "Environment validation passed"
}
# ------------------------------------------------------------------------------
# Signal Handlers
# ------------------------------------------------------------------------------
cleanup() {
log "Received shutdown signal, cleaning up..."
# Stop Wine server gracefully
wineserver -k 15 2>/dev/null || true
sleep 2
wineserver -k 9 2>/dev/null || true
stop_xvfb
log "Cleanup complete"
exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
# ------------------------------------------------------------------------------
# Main Entry Point
# ------------------------------------------------------------------------------
main() {
log "=========================================="
log "Wine CSP Service Entrypoint"
log "=========================================="
log "WARNING: For TEST VECTOR GENERATION ONLY"
log "=========================================="
validate_environment
download_linux_packages
initialize_wine
# Only attempt CSP installation in full mode
if [[ "${WINE_CSP_MODE}" == "full" ]]; then
install_cryptopro
fi
configure_service
# Start Xvfb for the main process
start_xvfb
log "Starting WineCspService..."
log "Listening on port ${WINE_CSP_PORT}"
# Execute the command passed to the container (or default)
if [[ $# -gt 0 ]]; then
exec "$@"
else
exec wine64 /app/WineCspService.exe
fi
}
main "$@"

View File

@@ -1,164 +0,0 @@
#!/usr/bin/env python3
"""
CryptoPro crawler (metadata only by default).
Fetches https://cryptopro.ru/downloads (or override) with basic auth, recurses linked pages,
and selects candidate Linux packages (.deb/.rpm/.tar.gz/.tgz/.run) or MSI as fallback.
Environment:
CRYPTOPRO_DOWNLOAD_URL: start URL (default: https://cryptopro.ru/downloads)
CRYPTOPRO_USERNAME / CRYPTOPRO_PASSWORD: credentials
CRYPTOPRO_MAX_PAGES: max pages to crawl (default: 20)
CRYPTOPRO_MAX_DEPTH: max link depth (default: 2)
CRYPTOPRO_DRY_RUN: 1 (default) to list only, 0 to enable download
CRYPTOPRO_OUTPUT: output path (default: /opt/cryptopro/csp-installer.bin)
"""
import os
import sys
import re
import html.parser
import urllib.parse
import urllib.request
from collections import deque
SESSION_HEADERS = {
"User-Agent": "StellaOps-CryptoPro-Crawler/1.0 (+https://stella-ops.org)",
}
LINUX_PATTERNS = re.compile(r"\.(deb|rpm|tar\.gz|tgz|run)(?:$|\?)", re.IGNORECASE)
MSI_PATTERN = re.compile(r"\.msi(?:$|\?)", re.IGNORECASE)
def log(msg: str) -> None:
sys.stdout.write(msg + "\n")
sys.stdout.flush()
def warn(msg: str) -> None:
sys.stderr.write("[WARN] " + msg + "\n")
sys.stderr.flush()
class LinkParser(html.parser.HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
if tag != "a":
return
href = dict(attrs).get("href")
if href:
self.links.append(href)
def fetch(url: str, auth_handler) -> tuple[str, list[str]]:
opener = urllib.request.build_opener(auth_handler)
req = urllib.request.Request(url, headers=SESSION_HEADERS)
with opener.open(req, timeout=30) as resp:
data = resp.read()
parser = LinkParser()
parser.feed(data.decode("utf-8", errors="ignore"))
return data, parser.links
def resolve_links(base: str, links: list[str]) -> list[str]:
resolved = []
for href in links:
if href.startswith("#") or href.startswith("mailto:"):
continue
resolved.append(urllib.parse.urljoin(base, href))
return resolved
def choose_candidates(urls: list[str]) -> tuple[list[str], list[str]]:
linux = []
msi = []
for u in urls:
if LINUX_PATTERNS.search(u):
linux.append(u)
elif MSI_PATTERN.search(u):
msi.append(u)
# stable ordering
linux = sorted(set(linux))
msi = sorted(set(msi))
return linux, msi
def download(url: str, output_path: str, auth_handler) -> int:
opener = urllib.request.build_opener(auth_handler)
req = urllib.request.Request(url, headers=SESSION_HEADERS)
with opener.open(req, timeout=60) as resp:
with open(output_path, "wb") as f:
f.write(resp.read())
return os.path.getsize(output_path)
def main() -> int:
start_url = os.environ.get("CRYPTOPRO_DOWNLOAD_URL", "https://cryptopro.ru/downloads")
username = os.environ.get("CRYPTOPRO_USERNAME", "contact@stella-ops.org")
password = os.environ.get("CRYPTOPRO_PASSWORD", "Hoko33JD3nj3aJD.")
max_pages = int(os.environ.get("CRYPTOPRO_MAX_PAGES", "20"))
max_depth = int(os.environ.get("CRYPTOPRO_MAX_DEPTH", "2"))
dry_run = os.environ.get("CRYPTOPRO_DRY_RUN", "1") != "0"
output_path = os.environ.get("CRYPTOPRO_OUTPUT", "/opt/cryptopro/csp-installer.bin")
if username == "contact@stella-ops.org" and password == "Hoko33JD3nj3aJD.":
warn("Using default demo credentials; set CRYPTOPRO_USERNAME/CRYPTOPRO_PASSWORD to real customer creds.")
passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
passman.add_password(None, start_url, username, password)
auth_handler = urllib.request.HTTPBasicAuthHandler(passman)
seen = set()
queue = deque([(start_url, 0)])
crawled = 0
all_links = []
while queue and crawled < max_pages:
url, depth = queue.popleft()
if url in seen or depth > max_depth:
continue
seen.add(url)
try:
data, links = fetch(url, auth_handler)
crawled += 1
log(f"[crawl] {url} ({len(data)} bytes, depth={depth}, links={len(links)})")
except Exception as ex: # noqa: BLE001
warn(f"[crawl] failed {url}: {ex}")
continue
resolved = resolve_links(url, links)
all_links.extend(resolved)
for child in resolved:
if child not in seen and depth + 1 <= max_depth:
queue.append((child, depth + 1))
linux, msi = choose_candidates(all_links)
log(f"[crawl] Linux candidates: {len(linux)}; MSI candidates: {len(msi)}")
if dry_run:
log("[crawl] Dry-run mode: not downloading. Set CRYPTOPRO_DRY_RUN=0 and CRYPTOPRO_OUTPUT to enable download.")
for idx, link in enumerate(linux[:10], 1):
log(f" [linux {idx}] {link}")
for idx, link in enumerate(msi[:5], 1):
log(f" [msi {idx}] {link}")
return 0
os.makedirs(os.path.dirname(output_path), exist_ok=True)
target = None
if linux:
target = linux[0]
elif msi:
target = msi[0]
else:
warn("No candidate downloads found.")
return 1
log(f"[download] Fetching {target} -> {output_path}")
size = download(target, output_path, auth_handler)
log(f"[download] Complete, size={size} bytes")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,24 +0,0 @@
#!/bin/bash
# Wine CSP Service Health Check
#
# Probes the /health endpoint to determine if the service is healthy.
# Returns 0 (healthy) or 1 (unhealthy).
set -euo pipefail
WINE_CSP_PORT="${WINE_CSP_PORT:-5099}"
HEALTH_ENDPOINT="http://127.0.0.1:${WINE_CSP_PORT}/health"
TIMEOUT_SECONDS=8
# Perform health check
response=$(wget -q -O - --timeout="${TIMEOUT_SECONDS}" "${HEALTH_ENDPOINT}" 2>/dev/null) || exit 1
# Verify response contains expected status
if echo "${response}" | grep -q '"status":"Healthy"'; then
exit 0
elif echo "${response}" | grep -q '"status":"Degraded"'; then
# Degraded is acceptable (e.g., CSP not installed but service running)
exit 0
else
exit 1
fi

View File

@@ -1,215 +0,0 @@
#!/bin/bash
# CryptoPro CSP Installation Script for Wine
#
# Installs customer-provided CryptoPro CSP MSI under Wine environment.
# This script is called by entrypoint.sh when CSP installer is available.
#
# IMPORTANT: CryptoPro CSP is commercial software. The installer MSI must be
# provided by the customer with appropriate licensing. StellaOps does not
# distribute CryptoPro CSP.
set -euo pipefail
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
WINE_CSP_INSTALLER_PATH="${WINE_CSP_INSTALLER_PATH:-/opt/cryptopro/csp-installer.msi}"
WINE_PREFIX="${WINEPREFIX:-$HOME/.wine}"
DISPLAY="${DISPLAY:-:99}"
# Expected CSP installation paths (under Wine prefix)
CSP_PROGRAM_FILES="${WINE_PREFIX}/drive_c/Program Files/Crypto Pro"
CSP_MARKER="${WINE_PREFIX}/.csp_installed"
CSP_VERSION_FILE="${WINE_PREFIX}/.csp_version"
# Installation timeout (5 minutes)
INSTALL_TIMEOUT=300
# Log prefix
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [install-csp] $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [install-csp] [ERROR] $*" >&2
}
# ------------------------------------------------------------------------------
# Pre-Installation Checks
# ------------------------------------------------------------------------------
check_prerequisites() {
log "Checking installation prerequisites"
# Check installer exists
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log_error "CSP installer not found: ${WINE_CSP_INSTALLER_PATH}"
return 1
fi
# Verify file is an MSI
if ! file "${WINE_CSP_INSTALLER_PATH}" | grep -qi "microsoft installer"; then
log_error "File does not appear to be an MSI installer"
return 1
fi
# Check Wine is available
if ! command -v wine64 &> /dev/null; then
log_error "wine64 not found"
return 1
fi
# Check Wine prefix exists
if [[ ! -d "${WINE_PREFIX}" ]]; then
log_error "Wine prefix not initialized: ${WINE_PREFIX}"
return 1
fi
log "Prerequisites check passed"
return 0
}
# ------------------------------------------------------------------------------
# Installation
# ------------------------------------------------------------------------------
install_csp() {
log "Starting CryptoPro CSP installation"
log "Installer: ${WINE_CSP_INSTALLER_PATH}"
# Create installation log directory
local log_dir="${WINE_PREFIX}/csp_install_logs"
mkdir -p "${log_dir}"
local install_log="${log_dir}/install_$(date -u '+%Y%m%d_%H%M%S').log"
# Run MSI installer silently
# /qn = silent mode, /norestart = don't restart, /l*v = verbose logging
log "Running msiexec installer (this may take several minutes)..."
timeout "${INSTALL_TIMEOUT}" wine64 msiexec /i "${WINE_CSP_INSTALLER_PATH}" \
/qn /norestart /l*v "${install_log}" \
AGREETOLICENSE=Yes \
2>&1 | tee -a "${install_log}" || {
local exit_code=$?
log_error "MSI installation failed with exit code: ${exit_code}"
log_error "Check installation log: ${install_log}"
return 1
}
# Wait for Wine to finish
wineserver --wait
log "MSI installation completed"
return 0
}
# ------------------------------------------------------------------------------
# Post-Installation Verification
# ------------------------------------------------------------------------------
verify_installation() {
log "Verifying CryptoPro CSP installation"
# Check for CSP program files
if [[ -d "${CSP_PROGRAM_FILES}" ]]; then
log "Found CSP directory: ${CSP_PROGRAM_FILES}"
else
log_error "CSP program directory not found"
return 1
fi
# Check for key CSP DLLs
local csp_dll="${WINE_PREFIX}/drive_c/windows/system32/cpcspi.dll"
if [[ -f "${csp_dll}" ]]; then
log "Found CSP DLL: ${csp_dll}"
else
log "Warning: CSP DLL not found at expected location"
# This might be OK depending on CSP version
fi
# Try to query CSP registry entries
local csp_registry
csp_registry=$(wine64 reg query "HKLM\\SOFTWARE\\Crypto Pro" 2>/dev/null || true)
if [[ -n "${csp_registry}" ]]; then
log "CSP registry entries found"
else
log "Warning: CSP registry entries not found"
fi
# Extract version if possible
local version="unknown"
if [[ -f "${CSP_PROGRAM_FILES}/CSP/version.txt" ]]; then
version=$(cat "${CSP_PROGRAM_FILES}/CSP/version.txt" 2>/dev/null || echo "unknown")
fi
echo "${version}" > "${CSP_VERSION_FILE}"
log "CSP version: ${version}"
log "Installation verification completed"
return 0
}
# ------------------------------------------------------------------------------
# Cleanup on Failure
# ------------------------------------------------------------------------------
cleanup_failed_install() {
log "Cleaning up failed installation"
# Try to uninstall via msiexec
wine64 msiexec /x "${WINE_CSP_INSTALLER_PATH}" /qn 2>/dev/null || true
wineserver --wait
# Remove any partial installation directories
rm -rf "${CSP_PROGRAM_FILES}" 2>/dev/null || true
# Remove marker files
rm -f "${CSP_MARKER}" "${CSP_VERSION_FILE}" 2>/dev/null || true
log "Cleanup completed"
}
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
main() {
log "=========================================="
log "CryptoPro CSP Installation Script"
log "=========================================="
# Check if already installed
if [[ -f "${CSP_MARKER}" ]]; then
log "CryptoPro CSP is already installed"
if [[ -f "${CSP_VERSION_FILE}" ]]; then
log "Installed version: $(cat "${CSP_VERSION_FILE}")"
fi
return 0
fi
# Run prerequisite checks
if ! check_prerequisites; then
log_error "Prerequisites check failed"
return 1
fi
# Perform installation
if ! install_csp; then
log_error "Installation failed"
cleanup_failed_install
return 1
fi
# Verify installation
if ! verify_installation; then
log_error "Installation verification failed"
cleanup_failed_install
return 1
fi
# Create installation marker
touch "${CSP_MARKER}"
log "=========================================="
log "CryptoPro CSP installation successful"
log "=========================================="
return 0
}
main "$@"

View File

@@ -1,114 +0,0 @@
#!/bin/bash
# Wine CSP Docker Build and Test
#
# Builds the Wine CSP Docker image and runs the full test suite.
# This script is designed for local development and CI/CD pipelines.
#
# Usage:
# ./docker-test.sh # Build and test
# ./docker-test.sh --no-build # Test existing image
# ./docker-test.sh --push # Build, test, and push if tests pass
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# Configuration
IMAGE_NAME="${WINE_CSP_IMAGE:-wine-csp}"
IMAGE_TAG="${WINE_CSP_TAG:-test}"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
DOCKERFILE="${PROJECT_ROOT}/ops/wine-csp/Dockerfile"
DO_BUILD=true
DO_PUSH=false
VERBOSE=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--no-build)
DO_BUILD=false
shift
;;
--push)
DO_PUSH=true
shift
;;
--verbose|-v)
VERBOSE=true
shift
;;
--image)
IMAGE_NAME="$2"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
shift 2
;;
--tag)
IMAGE_TAG="$2"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"
}
# Build image
if [[ "${DO_BUILD}" == "true" ]]; then
log "Building Wine CSP Docker image: ${FULL_IMAGE}"
log "Dockerfile: ${DOCKERFILE}"
log "Context: ${PROJECT_ROOT}"
build_args=""
if [[ "${VERBOSE}" == "true" ]]; then
build_args="--progress=plain"
fi
docker build \
${build_args} \
-f "${DOCKERFILE}" \
-t "${FULL_IMAGE}" \
"${PROJECT_ROOT}"
log "Build completed successfully"
fi
# Verify image exists
if ! docker image inspect "${FULL_IMAGE}" > /dev/null 2>&1; then
echo "Error: Image ${FULL_IMAGE} not found"
exit 1
fi
# Run tests
log "Running integration tests..."
test_args=""
if [[ "${VERBOSE}" == "true" ]]; then
test_args="--verbose"
fi
"${SCRIPT_DIR}/run-tests.sh" --image "${FULL_IMAGE}" ${test_args} --ci
# Check test results
if [[ $? -ne 0 ]]; then
log "Tests failed!"
exit 1
fi
log "All tests passed!"
# Push if requested
if [[ "${DO_PUSH}" == "true" ]]; then
log "Pushing image: ${FULL_IMAGE}"
docker push "${FULL_IMAGE}"
log "Push completed"
fi
log "Done!"

View File

@@ -1,144 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "GOST cryptographic test vectors for Wine CSP validation",
"version": "1.0.0",
"generated": "2025-12-07T00:00:00Z",
"warning": "FOR TEST VECTOR VALIDATION ONLY - NOT FOR PRODUCTION USE",
"hashVectors": {
"streebog256": [
{
"id": "streebog256-empty",
"description": "GOST R 34.11-2012 (256-bit) hash of empty message",
"input": "",
"inputBase64": "",
"expectedHash": "3f539a213e97c802cc229d474c6aa32a825a360b2a933a949fd925208d9ce1bb",
"reference": "GOST R 34.11-2012 specification"
},
{
"id": "streebog256-m1",
"description": "GOST R 34.11-2012 (256-bit) test message M1",
"input": "012345678901234567890123456789012345678901234567890123456789012",
"inputBase64": "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEy",
"expectedHash": "9d151eefd8590b89daa6ba6cb74af9275dd051026bb149a452fd84e5e57b5500",
"reference": "GOST R 34.11-2012 specification Appendix A.1"
},
{
"id": "streebog256-hello",
"description": "GOST R 34.11-2012 (256-bit) hash of 'Hello'",
"input": "Hello",
"inputBase64": "SGVsbG8=",
"note": "Common test case for implementation validation"
},
{
"id": "streebog256-abc",
"description": "GOST R 34.11-2012 (256-bit) hash of 'abc'",
"input": "abc",
"inputBase64": "YWJj",
"note": "Standard test vector"
}
],
"streebog512": [
{
"id": "streebog512-empty",
"description": "GOST R 34.11-2012 (512-bit) hash of empty message",
"input": "",
"inputBase64": "",
"expectedHash": "8e945da209aa869f0455928529bcae4679e9873ab707b55315f56ceb98bef0a7362f715528356ee83cda5f2aac4c6ad2ba3a715c1bcd81cb8e9f90bf4c1c1a8a",
"reference": "GOST R 34.11-2012 specification"
},
{
"id": "streebog512-m1",
"description": "GOST R 34.11-2012 (512-bit) test message M1",
"input": "012345678901234567890123456789012345678901234567890123456789012",
"inputBase64": "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEy",
"expectedHash": "1b54d01a4af5b9d5cc3d86d68d285462b19abc2475222f35c085122be4ba1ffa00ad30f8767b3a82384c6574f024c311e2a481332b08ef7f41797891c1646f48",
"reference": "GOST R 34.11-2012 specification Appendix A.2"
},
{
"id": "streebog512-hello",
"description": "GOST R 34.11-2012 (512-bit) hash of 'Hello'",
"input": "Hello",
"inputBase64": "SGVsbG8=",
"note": "Common test case for implementation validation"
}
]
},
"signatureVectors": {
"gost2012_256": [
{
"id": "gost2012-256-test1",
"description": "GOST R 34.10-2012 (256-bit) signature test",
"algorithm": "GOST12-256",
"message": "Test message for signing",
"messageBase64": "VGVzdCBtZXNzYWdlIGZvciBzaWduaW5n",
"note": "Signature will vary due to random k parameter; verify deterministic hash first"
}
],
"gost2012_512": [
{
"id": "gost2012-512-test1",
"description": "GOST R 34.10-2012 (512-bit) signature test",
"algorithm": "GOST12-512",
"message": "Test message for signing",
"messageBase64": "VGVzdCBtZXNzYWdlIGZvciBzaWduaW5n",
"note": "Signature will vary due to random k parameter; verify deterministic hash first"
}
]
},
"determinismVectors": [
{
"id": "determinism-1",
"description": "Determinism test - same input should produce same hash",
"algorithm": "STREEBOG-256",
"input": "Determinism test data 12345",
"inputBase64": "RGV0ZXJtaW5pc20gdGVzdCBkYXRhIDEyMzQ1",
"iterations": 10,
"expectation": "All iterations should produce identical hash"
},
{
"id": "determinism-2",
"description": "Determinism test with binary data",
"algorithm": "STREEBOG-512",
"inputBase64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
"iterations": 10,
"expectation": "All iterations should produce identical hash"
}
],
"errorVectors": [
{
"id": "error-invalid-algo",
"description": "Invalid algorithm should return 400",
"endpoint": "/hash",
"request": {"algorithm": "INVALID-ALGO", "data": "SGVsbG8="},
"expectedStatus": 400
},
{
"id": "error-missing-data",
"description": "Missing data field should return 400",
"endpoint": "/hash",
"request": {"algorithm": "STREEBOG-256"},
"expectedStatus": 400
},
{
"id": "error-invalid-base64",
"description": "Invalid base64 should return 400",
"endpoint": "/hash",
"request": {"algorithm": "STREEBOG-256", "data": "not-valid-base64!!!"},
"expectedStatus": 400
}
],
"performanceBenchmarks": {
"hashThroughput": {
"description": "Hash operation throughput benchmark",
"algorithm": "STREEBOG-256",
"inputSize": 1024,
"iterations": 100,
"expectedMinOpsPerSecond": 10
}
}
}

View File

@@ -1,4 +0,0 @@
# Wine CSP Integration Test Dependencies
pytest>=7.4.0
pytest-timeout>=2.2.0
requests>=2.31.0

View File

@@ -1,590 +0,0 @@
#!/bin/bash
# Wine CSP Container Integration Tests
#
# This script runs comprehensive tests against the Wine CSP container.
# It can test a running container or start one for testing.
#
# Usage:
# ./run-tests.sh # Start container and run tests
# ./run-tests.sh --url http://host:port # Test existing endpoint
# ./run-tests.sh --image wine-csp:tag # Use specific image
# ./run-tests.sh --verbose # Verbose output
# ./run-tests.sh --ci # CI mode (JUnit XML output)
set -euo pipefail
# ==============================================================================
# Configuration
# ==============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
WINE_CSP_IMAGE="${WINE_CSP_IMAGE:-wine-csp:test}"
WINE_CSP_PORT="${WINE_CSP_PORT:-5099}"
WINE_CSP_URL="${WINE_CSP_URL:-}"
CONTAINER_NAME="wine-csp-test-$$"
STARTUP_TIMEOUT=120
TEST_TIMEOUT=30
VERBOSE=false
CI_MODE=false
CLEANUP_CONTAINER=true
TEST_RESULTS_DIR="${SCRIPT_DIR}/results"
JUNIT_OUTPUT="${TEST_RESULTS_DIR}/junit.xml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
TESTS_SKIPPED=0
TEST_RESULTS=()
# ==============================================================================
# Utility Functions
# ==============================================================================
log() {
echo -e "${BLUE}[$(date -u '+%Y-%m-%dT%H:%M:%SZ')]${NC} $*"
}
log_success() {
echo -e "${GREEN}[PASS]${NC} $*"
}
log_fail() {
echo -e "${RED}[FAIL]${NC} $*"
}
log_skip() {
echo -e "${YELLOW}[SKIP]${NC} $*"
}
log_verbose() {
if [[ "${VERBOSE}" == "true" ]]; then
echo -e "${YELLOW}[DEBUG]${NC} $*"
fi
}
die() {
echo -e "${RED}[ERROR]${NC} $*" >&2
exit 1
}
# ==============================================================================
# Argument Parsing
# ==============================================================================
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--url)
WINE_CSP_URL="$2"
CLEANUP_CONTAINER=false
shift 2
;;
--image)
WINE_CSP_IMAGE="$2"
shift 2
;;
--port)
WINE_CSP_PORT="$2"
shift 2
;;
--verbose|-v)
VERBOSE=true
shift
;;
--ci)
CI_MODE=true
shift
;;
--help|-h)
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " --url URL Test existing endpoint (skip container start)"
echo " --image IMAGE Docker image to test (default: wine-csp:test)"
echo " --port PORT Port to expose (default: 5099)"
echo " --verbose, -v Verbose output"
echo " --ci CI mode (JUnit XML output)"
echo " --help, -h Show this help"
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
# Set URL if not provided
if [[ -z "${WINE_CSP_URL}" ]]; then
WINE_CSP_URL="http://127.0.0.1:${WINE_CSP_PORT}"
fi
}
# ==============================================================================
# Container Management
# ==============================================================================
start_container() {
log "Starting Wine CSP container: ${WINE_CSP_IMAGE}"
docker run -d \
--name "${CONTAINER_NAME}" \
-p "${WINE_CSP_PORT}:5099" \
-e WINE_CSP_MODE=limited \
-e WINE_CSP_LOG_LEVEL=Debug \
"${WINE_CSP_IMAGE}"
log "Container started: ${CONTAINER_NAME}"
log "Waiting for service to be ready (up to ${STARTUP_TIMEOUT}s)..."
local elapsed=0
while [[ $elapsed -lt $STARTUP_TIMEOUT ]]; do
if curl -sf "${WINE_CSP_URL}/health" > /dev/null 2>&1; then
log "Service is ready after ${elapsed}s"
return 0
fi
sleep 5
elapsed=$((elapsed + 5))
log_verbose "Waiting... ${elapsed}s elapsed"
done
log_fail "Service failed to start within ${STARTUP_TIMEOUT}s"
docker logs "${CONTAINER_NAME}" || true
return 1
}
stop_container() {
if [[ "${CLEANUP_CONTAINER}" == "true" ]] && docker ps -q -f name="${CONTAINER_NAME}" | grep -q .; then
log "Stopping container: ${CONTAINER_NAME}"
docker stop "${CONTAINER_NAME}" > /dev/null 2>&1 || true
docker rm "${CONTAINER_NAME}" > /dev/null 2>&1 || true
fi
}
# ==============================================================================
# Test Framework
# ==============================================================================
record_test() {
local name="$1"
local status="$2"
local duration="$3"
local message="${4:-}"
TESTS_RUN=$((TESTS_RUN + 1))
case $status in
pass)
TESTS_PASSED=$((TESTS_PASSED + 1))
log_success "${name} (${duration}ms)"
;;
fail)
TESTS_FAILED=$((TESTS_FAILED + 1))
log_fail "${name}: ${message}"
;;
skip)
TESTS_SKIPPED=$((TESTS_SKIPPED + 1))
log_skip "${name}: ${message}"
;;
esac
TEST_RESULTS+=("${name}|${status}|${duration}|${message}")
}
run_test() {
local name="$1"
shift
local start_time=$(date +%s%3N)
log_verbose "Running test: ${name}"
if "$@"; then
local end_time=$(date +%s%3N)
local duration=$((end_time - start_time))
record_test "${name}" "pass" "${duration}"
return 0
else
local end_time=$(date +%s%3N)
local duration=$((end_time - start_time))
record_test "${name}" "fail" "${duration}" "Test assertion failed"
return 1
fi
}
# ==============================================================================
# HTTP Helper Functions
# ==============================================================================
http_get() {
local endpoint="$1"
curl -sf --max-time "${TEST_TIMEOUT}" "${WINE_CSP_URL}${endpoint}"
}
http_post() {
local endpoint="$1"
local data="$2"
curl -sf --max-time "${TEST_TIMEOUT}" \
-X POST \
-H "Content-Type: application/json" \
-d "${data}" \
"${WINE_CSP_URL}${endpoint}"
}
# ==============================================================================
# Test Cases
# ==============================================================================
# Health endpoint tests
test_health_endpoint() {
local response
response=$(http_get "/health") || return 1
echo "${response}" | grep -q '"status"' || return 1
}
test_health_liveness() {
local response
response=$(http_get "/health/liveness") || return 1
echo "${response}" | grep -qi 'healthy\|alive' || return 1
}
test_health_readiness() {
local response
response=$(http_get "/health/readiness") || return 1
echo "${response}" | grep -qi 'healthy\|ready' || return 1
}
# Status endpoint tests
test_status_endpoint() {
local response
response=$(http_get "/status") || return 1
echo "${response}" | grep -q '"serviceName"' || return 1
echo "${response}" | grep -q '"mode"' || return 1
}
test_status_mode_limited() {
local response
response=$(http_get "/status") || return 1
echo "${response}" | grep -q '"mode":"limited"' || \
echo "${response}" | grep -q '"mode": "limited"' || return 1
}
# Keys endpoint tests
test_keys_endpoint() {
local response
response=$(http_get "/keys") || return 1
# Should return an array (possibly empty in limited mode)
echo "${response}" | grep -qE '^\[' || return 1
}
# Hash endpoint tests
test_hash_streebog256() {
# Test vector: "Hello" -> known Streebog-256 hash
local data='{"algorithm":"STREEBOG-256","data":"SGVsbG8="}'
local response
response=$(http_post "/hash" "${data}") || return 1
echo "${response}" | grep -q '"hash"' || return 1
echo "${response}" | grep -q '"algorithm"' || return 1
}
test_hash_streebog512() {
# Test vector: "Hello" -> known Streebog-512 hash
local data='{"algorithm":"STREEBOG-512","data":"SGVsbG8="}'
local response
response=$(http_post "/hash" "${data}") || return 1
echo "${response}" | grep -q '"hash"' || return 1
}
test_hash_invalid_algorithm() {
local data='{"algorithm":"INVALID","data":"SGVsbG8="}'
# Should fail with 400
if http_post "/hash" "${data}" > /dev/null 2>&1; then
return 1 # Should have failed
fi
return 0 # Correctly rejected
}
test_hash_empty_data() {
# Empty string base64 encoded
local data='{"algorithm":"STREEBOG-256","data":""}'
local response
response=$(http_post "/hash" "${data}") || return 1
echo "${response}" | grep -q '"hash"' || return 1
}
# Test vectors endpoint
test_vectors_endpoint() {
local response
response=$(http_get "/test-vectors") || return 1
# Should return test vectors array
echo "${response}" | grep -q '"vectors"' || \
echo "${response}" | grep -qE '^\[' || return 1
}
# Sign endpoint tests (limited mode may not support all operations)
test_sign_basic() {
local data='{"keyId":"test-key","algorithm":"GOST12-256","data":"SGVsbG8gV29ybGQ="}'
local response
# In limited mode, this may fail or return a mock signature
if response=$(http_post "/sign" "${data}" 2>/dev/null); then
echo "${response}" | grep -q '"signature"' || return 1
else
# Expected to fail in limited mode without keys
log_verbose "Sign failed (expected in limited mode)"
return 0
fi
}
# Verify endpoint tests
test_verify_basic() {
local data='{"keyId":"test-key","algorithm":"GOST12-256","data":"SGVsbG8gV29ybGQ=","signature":"AAAA"}'
# In limited mode, this may fail
if http_post "/verify" "${data}" > /dev/null 2>&1; then
return 0 # Verification endpoint works
else
log_verbose "Verify failed (expected in limited mode)"
return 0 # Expected in limited mode
fi
}
# Determinism tests
test_hash_determinism() {
local data='{"algorithm":"STREEBOG-256","data":"VGVzdCBkYXRhIGZvciBkZXRlcm1pbmlzbQ=="}'
local hash1 hash2
hash1=$(http_post "/hash" "${data}" | grep -o '"hash":"[^"]*"' | head -1) || return 1
hash2=$(http_post "/hash" "${data}" | grep -o '"hash":"[^"]*"' | head -1) || return 1
[[ "${hash1}" == "${hash2}" ]] || return 1
}
# Known test vector validation
test_known_vector_streebog256() {
# GOST R 34.11-2012 (Streebog-256) test vector
# Input: "012345678901234567890123456789012345678901234567890123456789012" (63 bytes)
# Expected hash: 9d151eefd8590b89daa6ba6cb74af9275dd051026bb149a452fd84e5e57b5500
local input_b64="MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEy"
local expected_hash="9d151eefd8590b89daa6ba6cb74af9275dd051026bb149a452fd84e5e57b5500"
local data="{\"algorithm\":\"STREEBOG-256\",\"data\":\"${input_b64}\"}"
local response
response=$(http_post "/hash" "${data}") || return 1
# Check if hash matches expected value
if echo "${response}" | grep -qi "${expected_hash}"; then
return 0
else
log_verbose "Hash mismatch. Response: ${response}"
log_verbose "Expected hash containing: ${expected_hash}"
# In limited mode, hash implementation may differ
return 0 # Skip strict validation for now
fi
}
# Error handling tests
test_malformed_json() {
# Send malformed JSON
local response_code
response_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time "${TEST_TIMEOUT}" \
-X POST \
-H "Content-Type: application/json" \
-d "not valid json" \
"${WINE_CSP_URL}/hash")
[[ "${response_code}" == "400" ]] || return 1
}
test_missing_required_fields() {
# Missing 'data' field
local data='{"algorithm":"STREEBOG-256"}'
local response_code
response_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time "${TEST_TIMEOUT}" \
-X POST \
-H "Content-Type: application/json" \
-d "${data}" \
"${WINE_CSP_URL}/hash")
[[ "${response_code}" == "400" ]] || return 1
}
# Performance tests
test_hash_performance() {
local data='{"algorithm":"STREEBOG-256","data":"SGVsbG8gV29ybGQ="}'
local start_time end_time duration
start_time=$(date +%s%3N)
for i in {1..10}; do
http_post "/hash" "${data}" > /dev/null || return 1
done
end_time=$(date +%s%3N)
duration=$((end_time - start_time))
log_verbose "10 hash operations completed in ${duration}ms (avg: $((duration / 10))ms)"
# Should complete 10 hashes in under 10 seconds
[[ $duration -lt 10000 ]] || return 1
}
# CryptoPro downloader dry-run (Playwright)
test_downloader_dry_run() {
docker exec "${CONTAINER_NAME}" \
env CRYPTOPRO_DRY_RUN=1 CRYPTOPRO_UNPACK=0 CRYPTOPRO_FETCH_ON_START=1 \
/usr/local/bin/download-cryptopro.sh
}
# ==============================================================================
# Test Runner
# ==============================================================================
run_all_tests() {
log "=========================================="
log "Wine CSP Integration Tests"
log "=========================================="
log "Target: ${WINE_CSP_URL}"
log ""
# Downloader dry-run (only when we control the container)
if [[ "${CLEANUP_CONTAINER}" == "true" ]]; then
run_test "cryptopro_downloader_dry_run" test_downloader_dry_run
else
record_test "cryptopro_downloader_dry_run" "skip" "0" "External endpoint; downloader test skipped"
fi
# Health tests
log "--- Health Endpoints ---"
run_test "health_endpoint" test_health_endpoint
run_test "health_liveness" test_health_liveness
run_test "health_readiness" test_health_readiness
# Status tests
log "--- Status Endpoint ---"
run_test "status_endpoint" test_status_endpoint
run_test "status_mode_limited" test_status_mode_limited
# Keys tests
log "--- Keys Endpoint ---"
run_test "keys_endpoint" test_keys_endpoint
# Hash tests
log "--- Hash Operations ---"
run_test "hash_streebog256" test_hash_streebog256
run_test "hash_streebog512" test_hash_streebog512
run_test "hash_invalid_algorithm" test_hash_invalid_algorithm
run_test "hash_empty_data" test_hash_empty_data
run_test "hash_determinism" test_hash_determinism
run_test "known_vector_streebog256" test_known_vector_streebog256
# Test vectors
log "--- Test Vectors ---"
run_test "test_vectors_endpoint" test_vectors_endpoint
# Sign/Verify tests (may skip in limited mode)
log "--- Sign/Verify Operations ---"
run_test "sign_basic" test_sign_basic
run_test "verify_basic" test_verify_basic
# Error handling tests
log "--- Error Handling ---"
run_test "malformed_json" test_malformed_json
run_test "missing_required_fields" test_missing_required_fields
# Performance tests
log "--- Performance ---"
run_test "hash_performance" test_hash_performance
log ""
log "=========================================="
}
# ==============================================================================
# Results Output
# ==============================================================================
print_summary() {
log "=========================================="
log "Test Results Summary"
log "=========================================="
echo ""
echo -e "Total: ${TESTS_RUN}"
echo -e "${GREEN}Passed: ${TESTS_PASSED}${NC}"
echo -e "${RED}Failed: ${TESTS_FAILED}${NC}"
echo -e "${YELLOW}Skipped: ${TESTS_SKIPPED}${NC}"
echo ""
if [[ ${TESTS_FAILED} -gt 0 ]]; then
echo -e "${RED}TESTS FAILED${NC}"
return 1
else
echo -e "${GREEN}ALL TESTS PASSED${NC}"
return 0
fi
}
generate_junit_xml() {
mkdir -p "${TEST_RESULTS_DIR}"
local timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
local total_time=0
cat > "${JUNIT_OUTPUT}" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="Wine CSP Integration Tests" tests="${TESTS_RUN}" failures="${TESTS_FAILED}" skipped="${TESTS_SKIPPED}" timestamp="${timestamp}">
<testsuite name="wine-csp" tests="${TESTS_RUN}" failures="${TESTS_FAILED}" skipped="${TESTS_SKIPPED}">
EOF
for result in "${TEST_RESULTS[@]}"; do
IFS='|' read -r name status duration message <<< "${result}"
local time_sec=$(echo "scale=3; ${duration} / 1000" | bc)
total_time=$((total_time + duration))
echo " <testcase name=\"${name}\" classname=\"wine-csp\" time=\"${time_sec}\">" >> "${JUNIT_OUTPUT}"
case $status in
fail)
echo " <failure message=\"${message}\"/>" >> "${JUNIT_OUTPUT}"
;;
skip)
echo " <skipped message=\"${message}\"/>" >> "${JUNIT_OUTPUT}"
;;
esac
echo " </testcase>" >> "${JUNIT_OUTPUT}"
done
cat >> "${JUNIT_OUTPUT}" << EOF
</testsuite>
</testsuites>
EOF
log "JUnit XML output: ${JUNIT_OUTPUT}"
}
# ==============================================================================
# Main
# ==============================================================================
main() {
parse_args "$@"
# Setup results directory
mkdir -p "${TEST_RESULTS_DIR}"
# Start container if needed
if [[ "${CLEANUP_CONTAINER}" == "true" ]]; then
trap stop_container EXIT
start_container || die "Failed to start container"
fi
# Run tests
run_all_tests
# Generate outputs
if [[ "${CI_MODE}" == "true" ]]; then
generate_junit_xml
fi
# Print summary and exit with appropriate code
print_summary
}
main "$@"

View File

@@ -1,463 +0,0 @@
#!/usr/bin/env python3
"""
Wine CSP Integration Tests
Comprehensive test suite for the Wine CSP HTTP service.
Designed for pytest with JUnit XML output for CI integration.
Usage:
pytest test_wine_csp.py -v --junitxml=results/junit.xml
pytest test_wine_csp.py -v -k "test_health"
pytest test_wine_csp.py -v --wine-csp-url=http://localhost:5099
"""
import base64
import json
import os
import time
from typing import Any, Dict, Optional
import pytest
import requests
# ==============================================================================
# Configuration
# ==============================================================================
WINE_CSP_URL = os.environ.get("WINE_CSP_URL", "http://127.0.0.1:5099")
REQUEST_TIMEOUT = 30
STARTUP_TIMEOUT = 120
def pytest_addoption(parser):
"""Add custom pytest options."""
parser.addoption(
"--wine-csp-url",
action="store",
default=WINE_CSP_URL,
help="Wine CSP service URL",
)
@pytest.fixture(scope="session")
def wine_csp_url(request):
"""Get Wine CSP URL from command line or environment."""
return request.config.getoption("--wine-csp-url") or WINE_CSP_URL
@pytest.fixture(scope="session")
def wine_csp_client(wine_csp_url):
"""Create a requests session for Wine CSP API calls."""
session = requests.Session()
session.headers.update({"Content-Type": "application/json", "Accept": "application/json"})
# Wait for service to be ready
start_time = time.time()
while time.time() - start_time < STARTUP_TIMEOUT:
try:
response = session.get(f"{wine_csp_url}/health", timeout=5)
if response.status_code == 200:
break
except requests.exceptions.RequestException:
pass
time.sleep(5)
else:
pytest.fail(f"Wine CSP service not ready after {STARTUP_TIMEOUT}s")
return {"session": session, "base_url": wine_csp_url}
# ==============================================================================
# Helper Functions
# ==============================================================================
def get(client: Dict, endpoint: str) -> requests.Response:
"""Perform GET request."""
return client["session"].get(
f"{client['base_url']}{endpoint}", timeout=REQUEST_TIMEOUT
)
def post(client: Dict, endpoint: str, data: Dict[str, Any]) -> requests.Response:
"""Perform POST request with JSON body."""
return client["session"].post(
f"{client['base_url']}{endpoint}", json=data, timeout=REQUEST_TIMEOUT
)
def encode_b64(text: str) -> str:
"""Encode string to base64."""
return base64.b64encode(text.encode("utf-8")).decode("utf-8")
def decode_b64(b64: str) -> bytes:
"""Decode base64 string."""
return base64.b64decode(b64)
# ==============================================================================
# Health Endpoint Tests
# ==============================================================================
class TestHealthEndpoints:
"""Tests for health check endpoints."""
def test_health_returns_200(self, wine_csp_client):
"""Health endpoint should return 200 OK."""
response = get(wine_csp_client, "/health")
assert response.status_code == 200
def test_health_returns_status(self, wine_csp_client):
"""Health endpoint should return status field."""
response = get(wine_csp_client, "/health")
data = response.json()
assert "status" in data
def test_health_status_is_healthy_or_degraded(self, wine_csp_client):
"""Health status should be Healthy or Degraded."""
response = get(wine_csp_client, "/health")
data = response.json()
assert data["status"] in ["Healthy", "Degraded"]
def test_health_liveness(self, wine_csp_client):
"""Liveness probe should return 200."""
response = get(wine_csp_client, "/health/liveness")
assert response.status_code == 200
def test_health_readiness(self, wine_csp_client):
"""Readiness probe should return 200."""
response = get(wine_csp_client, "/health/readiness")
assert response.status_code == 200
# ==============================================================================
# Status Endpoint Tests
# ==============================================================================
class TestStatusEndpoint:
"""Tests for status endpoint."""
def test_status_returns_200(self, wine_csp_client):
"""Status endpoint should return 200 OK."""
response = get(wine_csp_client, "/status")
assert response.status_code == 200
def test_status_contains_service_name(self, wine_csp_client):
"""Status should contain serviceName."""
response = get(wine_csp_client, "/status")
data = response.json()
assert "serviceName" in data
def test_status_contains_mode(self, wine_csp_client):
"""Status should contain mode."""
response = get(wine_csp_client, "/status")
data = response.json()
assert "mode" in data
assert data["mode"] in ["limited", "full"]
def test_status_contains_version(self, wine_csp_client):
"""Status should contain version."""
response = get(wine_csp_client, "/status")
data = response.json()
assert "version" in data or "serviceVersion" in data
# ==============================================================================
# Keys Endpoint Tests
# ==============================================================================
class TestKeysEndpoint:
"""Tests for keys endpoint."""
def test_keys_returns_200(self, wine_csp_client):
"""Keys endpoint should return 200 OK."""
response = get(wine_csp_client, "/keys")
assert response.status_code == 200
def test_keys_returns_array(self, wine_csp_client):
"""Keys endpoint should return an array."""
response = get(wine_csp_client, "/keys")
data = response.json()
assert isinstance(data, list)
# ==============================================================================
# Hash Endpoint Tests
# ==============================================================================
class TestHashEndpoint:
"""Tests for hash operations."""
@pytest.mark.parametrize(
"algorithm",
["STREEBOG-256", "STREEBOG-512", "GOST3411-256", "GOST3411-512"],
)
def test_hash_algorithms(self, wine_csp_client, algorithm):
"""Test supported hash algorithms."""
data = {"algorithm": algorithm, "data": encode_b64("Hello World")}
response = post(wine_csp_client, "/hash", data)
# May return 200 or 400 depending on algorithm support
assert response.status_code in [200, 400]
def test_hash_streebog256_returns_hash(self, wine_csp_client):
"""Streebog-256 should return a hash."""
data = {"algorithm": "STREEBOG-256", "data": encode_b64("Hello")}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 200
result = response.json()
assert "hash" in result
assert len(result["hash"]) == 64 # 256 bits = 64 hex chars
def test_hash_streebog512_returns_hash(self, wine_csp_client):
"""Streebog-512 should return a hash."""
data = {"algorithm": "STREEBOG-512", "data": encode_b64("Hello")}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 200
result = response.json()
assert "hash" in result
assert len(result["hash"]) == 128 # 512 bits = 128 hex chars
def test_hash_empty_input(self, wine_csp_client):
"""Hash of empty input should work."""
data = {"algorithm": "STREEBOG-256", "data": ""}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 200
def test_hash_invalid_algorithm(self, wine_csp_client):
"""Invalid algorithm should return 400."""
data = {"algorithm": "INVALID-ALGO", "data": encode_b64("Hello")}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 400
def test_hash_missing_data(self, wine_csp_client):
"""Missing data field should return 400."""
data = {"algorithm": "STREEBOG-256"}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 400
def test_hash_missing_algorithm(self, wine_csp_client):
"""Missing algorithm field should return 400."""
data = {"data": encode_b64("Hello")}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 400
# ==============================================================================
# Determinism Tests
# ==============================================================================
class TestDeterminism:
"""Tests for deterministic behavior."""
def test_hash_determinism_same_input(self, wine_csp_client):
"""Same input should produce same hash."""
data = {"algorithm": "STREEBOG-256", "data": encode_b64("Test data for determinism")}
hashes = []
for _ in range(5):
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 200
hashes.append(response.json()["hash"])
# All hashes should be identical
assert len(set(hashes)) == 1, f"Non-deterministic hashes: {hashes}"
def test_hash_determinism_binary_data(self, wine_csp_client):
"""Binary input should produce deterministic hash."""
binary_data = bytes(range(256))
data = {"algorithm": "STREEBOG-512", "data": base64.b64encode(binary_data).decode()}
hashes = []
for _ in range(5):
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 200
hashes.append(response.json()["hash"])
assert len(set(hashes)) == 1
# ==============================================================================
# Known Test Vector Validation
# ==============================================================================
class TestKnownVectors:
"""Tests using known GOST test vectors."""
def test_streebog256_m1_vector(self, wine_csp_client):
"""Validate Streebog-256 against GOST R 34.11-2012 M1 test vector."""
# M1 = "012345678901234567890123456789012345678901234567890123456789012"
m1 = "012345678901234567890123456789012345678901234567890123456789012"
expected_hash = "9d151eefd8590b89daa6ba6cb74af9275dd051026bb149a452fd84e5e57b5500"
data = {"algorithm": "STREEBOG-256", "data": encode_b64(m1)}
response = post(wine_csp_client, "/hash", data)
if response.status_code == 200:
result = response.json()
# Note: Implementation may use different encoding
actual_hash = result["hash"].lower()
# Check if hash matches (may need to reverse bytes for some implementations)
assert len(actual_hash) == 64, f"Invalid hash length: {len(actual_hash)}"
# Log for debugging
print(f"Expected: {expected_hash}")
print(f"Actual: {actual_hash}")
def test_streebog512_m1_vector(self, wine_csp_client):
"""Validate Streebog-512 against GOST R 34.11-2012 M1 test vector."""
m1 = "012345678901234567890123456789012345678901234567890123456789012"
expected_hash = "1b54d01a4af5b9d5cc3d86d68d285462b19abc2475222f35c085122be4ba1ffa00ad30f8767b3a82384c6574f024c311e2a481332b08ef7f41797891c1646f48"
data = {"algorithm": "STREEBOG-512", "data": encode_b64(m1)}
response = post(wine_csp_client, "/hash", data)
if response.status_code == 200:
result = response.json()
actual_hash = result["hash"].lower()
assert len(actual_hash) == 128, f"Invalid hash length: {len(actual_hash)}"
print(f"Expected: {expected_hash}")
print(f"Actual: {actual_hash}")
# ==============================================================================
# Test Vectors Endpoint
# ==============================================================================
class TestTestVectorsEndpoint:
"""Tests for test vectors endpoint."""
def test_vectors_returns_200(self, wine_csp_client):
"""Test vectors endpoint should return 200."""
response = get(wine_csp_client, "/test-vectors")
assert response.status_code == 200
def test_vectors_returns_array_or_object(self, wine_csp_client):
"""Test vectors should return valid JSON."""
response = get(wine_csp_client, "/test-vectors")
data = response.json()
assert isinstance(data, (list, dict))
# ==============================================================================
# Sign/Verify Endpoint Tests
# ==============================================================================
class TestSignVerifyEndpoints:
"""Tests for sign and verify operations."""
def test_sign_without_key_returns_error(self, wine_csp_client):
"""Sign without valid key should return error in limited mode."""
data = {
"keyId": "nonexistent-key",
"algorithm": "GOST12-256",
"data": encode_b64("Test message"),
}
response = post(wine_csp_client, "/sign", data)
# Should return error (400 or 404) in limited mode
assert response.status_code in [200, 400, 404, 500]
def test_verify_invalid_signature(self, wine_csp_client):
"""Verify with invalid signature should fail."""
data = {
"keyId": "test-key",
"algorithm": "GOST12-256",
"data": encode_b64("Test message"),
"signature": "aW52YWxpZA==", # "invalid" in base64
}
response = post(wine_csp_client, "/verify", data)
# Should return error or false verification
assert response.status_code in [200, 400, 404, 500]
# ==============================================================================
# Error Handling Tests
# ==============================================================================
class TestErrorHandling:
"""Tests for error handling."""
def test_malformed_json(self, wine_csp_client):
"""Malformed JSON should return 400."""
response = wine_csp_client["session"].post(
f"{wine_csp_client['base_url']}/hash",
data="not valid json",
headers={"Content-Type": "application/json"},
timeout=REQUEST_TIMEOUT,
)
assert response.status_code == 400
def test_invalid_base64(self, wine_csp_client):
"""Invalid base64 should return 400."""
data = {"algorithm": "STREEBOG-256", "data": "not-valid-base64!!!"}
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 400
def test_unknown_endpoint(self, wine_csp_client):
"""Unknown endpoint should return 404."""
response = get(wine_csp_client, "/unknown-endpoint")
assert response.status_code == 404
# ==============================================================================
# Performance Tests
# ==============================================================================
class TestPerformance:
"""Performance benchmark tests."""
@pytest.mark.slow
def test_hash_throughput(self, wine_csp_client):
"""Hash operations should meet minimum throughput."""
data = {"algorithm": "STREEBOG-256", "data": encode_b64("X" * 1024)}
iterations = 50
start_time = time.time()
for _ in range(iterations):
response = post(wine_csp_client, "/hash", data)
assert response.status_code == 200
elapsed = time.time() - start_time
ops_per_second = iterations / elapsed
print(f"Hash throughput: {ops_per_second:.2f} ops/sec")
print(f"Average latency: {(elapsed / iterations) * 1000:.2f} ms")
# Should achieve at least 5 ops/sec
assert ops_per_second >= 5, f"Throughput too low: {ops_per_second:.2f} ops/sec"
@pytest.mark.slow
def test_concurrent_requests(self, wine_csp_client):
"""Service should handle concurrent requests."""
import concurrent.futures
data = {"algorithm": "STREEBOG-256", "data": encode_b64("Concurrent test")}
def make_request():
return post(wine_csp_client, "/hash", data)
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(make_request) for _ in range(20)]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
success_count = sum(1 for r in results if r.status_code == 200)
assert success_count >= 18, f"Too many failures: {20 - success_count}/20"
# ==============================================================================
# Main
# ==============================================================================
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])