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
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:
12
ops/cryptopro/linux-csp-service/CryptoProLinuxApi.csproj
Normal file
12
ops/cryptopro/linux-csp-service/CryptoProLinuxApi.csproj
Normal 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>
|
||||
@@ -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"]
|
||||
|
||||
118
ops/cryptopro/linux-csp-service/Program.cs
Normal file
118
ops/cryptopro/linux-csp-service/Program.cs
Normal 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);
|
||||
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
@@ -1,2 +0,0 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.1
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
@@ -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!"
|
||||
144
ops/wine-csp/tests/fixtures/test-vectors.json
vendored
144
ops/wine-csp/tests/fixtures/test-vectors.json
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Wine CSP Integration Test Dependencies
|
||||
pytest>=7.4.0
|
||||
pytest-timeout>=2.2.0
|
||||
requests>=2.31.0
|
||||
@@ -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 "$@"
|
||||
@@ -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"])
|
||||
Reference in New Issue
Block a user