diff --git a/.github/workflows/BuildDeploy.yml b/.github/workflows/BuildDeploy.yml index ea55158..ecce812 100644 --- a/.github/workflows/BuildDeploy.yml +++ b/.github/workflows/BuildDeploy.yml @@ -230,18 +230,176 @@ jobs: path: packages/*.nupkg if-no-files-found: error - publish-nuget: + sign: needs: release runs-on: ubuntu-latest environment: name: release permissions: - id-token: write + contents: read + container: + image: ghcr.io/reactiveui/certum-signer:latest + env: + CERTUM_USER_ID: ${{ secrets.CERTUM_USER_ID }} + CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }} + CERTUM_CERT_FINGERPRINT: ${{ secrets.CERTUM_CERT_FINGERPRINT }} steps: - name: Download unsigned packages uses: actions/download-artifact@v8 with: name: unsigned-nuget + path: signed + + - name: Resolve prebuilt SimplySign paths + shell: bash + run: | + set -euo pipefail + : "${SS_DIST:?prebuilt image must set SS_DIST}" + { + echo "SS_DIST=$SS_DIST" + echo "SS_START=$(find "$SS_DIST/" -name 'SimplySignDesktop_start' -type f | head -1)" + echo "SS_EXE=$(find "$SS_DIST/" -name 'SimplySignDesktop' -type f | head -1)" + echo "SS_PKCS11=$(find "$SS_DIST/" -name 'SimplySignPKCS*.so' | head -1)" + } >> "$GITHUB_ENV" + + - name: Start headless display + shell: bash + run: | + set -euo pipefail + Xvfb :99 -screen 0 1280x1024x24 >"$RUNNER_TEMP/xvfb.log" 2>&1 & + sleep 3 + mkdir -p "$HOME/.fluxbox" + printf 'session.screen0.rootCommand: /bin/true\n' > "$HOME/.fluxbox/init" + DISPLAY=:99 fluxbox >"$RUNNER_TEMP/fluxbox.log" 2>&1 & + sleep 2 + DISPLAY=:99 xdpyinfo >/dev/null && echo "X display :99 up" + + - name: Connect SimplySign + shell: bash + env: + DISPLAY: ':99' + run: | + set -euo pipefail + : "${CERTUM_USER_ID:?map the CERTUM_USER_ID secret into the job env}" + : "${CERTUM_OTP_URI:?map the CERTUM_OTP_URI secret into the job env}" + + export USER="${USER:-root}" + cp "$SS_DIST/SimplySignDesktop.xml" "$HOME/" 2>/dev/null || true + mkdir -p "$HOME/.config" + { + echo "[General]" + echo "CacheUserIdAtLogon=Yes" + echo "ShowLogonDialogAfterApplicationStartup=Yes" + echo "ShowLogonDialogWhenAnyAppRequestsAccess=Yes" + } > "$HOME/.config/Unknown Organization.conf" + + export LD_LIBRARY_PATH="$SS_DIST:${LD_LIBRARY_PATH:-}" + if [ -n "${SS_START:-}" ]; then + "$SS_START" >"$RUNNER_TEMP/simplysign.log" 2>&1 & + else + "$SS_EXE" >"$RUNNER_TEMP/simplysign.log" 2>&1 & + fi + sleep 20 + + OTP="$(python3 -c 'import os,hmac,hashlib,base64,struct,time;from urllib.parse import urlparse,parse_qs;q=parse_qs(urlparse(os.environ["CERTUM_OTP_URI"]).query);s=q["secret"][0];digits=int(q.get("digits",["6"])[0]);period=int(q.get("period",["30"])[0]);algo={"SHA1":hashlib.sha1,"SHA256":hashlib.sha256,"SHA512":hashlib.sha512}[q.get("algorithm",["SHA1"])[0].upper()];key=base64.b32decode(s+"="*((8-len(s)%8)%8));h=hmac.new(key,struct.pack(">Q",int(time.time()//period)),algo).digest();o=h[-1]&0xF;print(str((int.from_bytes(h[o:o+4],"big")&0x7fffffff)%(10**digits)).zfill(digits))')" + echo "::add-mask::$OTP" + + WID=""; BX=0; BY=0; BW=0; BH=0 + for attempt in $(seq 1 30); do + best=0; WID="" + for w in $(xdotool search --name '' 2>/dev/null); do + nm="$(xdotool getwindowname "$w" 2>/dev/null || true)" + case "$nm" in *implySign*) ;; *) continue ;; esac + eval "$(xdotool getwindowgeometry --shell "$w" 2>/dev/null)" + area=$(( ${WIDTH:-0} * ${HEIGHT:-0} )) + if [ "$area" -gt "$best" ]; then best=$area; WID=$w; BX=${X:-0}; BY=${Y:-0}; BW=${WIDTH:-0}; BH=${HEIGHT:-0}; fi + done + [ "${BW:-0}" -ge 400 ] && [ "${BH:-0}" -ge 300 ] && { echo "login window $WID ${BW}x${BH} (attempt $attempt)"; break; } + WID=""; sleep 2 + done + + [ -z "$WID" ] && { echo "::error::SimplySign login window did not appear"; cat "$RUNNER_TEMP/simplysign.log" || true; exit 1; } + xdotool windowactivate --sync "$WID" || xdotool windowactivate "$WID" || true + xdotool windowraise "$WID" || true + sleep 1 + + xdotool mousemove $((BX + BW/2)) $((BY + BH*39/100)) click 1; sleep 1 + xdotool type --clearmodifiers --delay 50 "$CERTUM_USER_ID" + xdotool key Tab; sleep 1 + xdotool type --clearmodifiers --delay 50 "$OTP" + xdotool mousemove $((BX + BW/2)) $((BY + BH*76/100)) click 1 + sleep 8 + + best=0 + for w in $(xdotool search --name '' 2>/dev/null); do + nm="$(xdotool getwindowname "$w" 2>/dev/null || true)" + case "$nm" in *implySign*) ;; *) continue ;; esac + eval "$(xdotool getwindowgeometry --shell "$w" 2>/dev/null)" + area=$(( ${WIDTH:-0} * ${HEIGHT:-0} )) + if [ "$area" -gt "$best" ]; then best=$area; WID=$w; BX=${X:-0}; BY=${Y:-0}; BW=${WIDTH:-0}; BH=${HEIGHT:-0}; fi + done + xdotool windowactivate --sync "$WID" || true + xdotool mousemove $((BX + BW/2)) $((BY + BH*94/100)) click 1 + sleep 3 + + timeout 60 pkcs11-tool --module "$SS_PKCS11" -L + + - name: Sign packages with jsign + shell: bash + run: | + set -euo pipefail + : "${CERTUM_CERT_FINGERPRINT:?map the CERTUM_CERT_FINGERPRINT secret into the job env}" + + JAR="${JSIGN_JAR:-$RUNNER_TEMP/jsign.jar}" + if [ ! -s "$JAR" ]; then + curl -fsSL -o "$JAR" "https://repo1.maven.org/maven2/net/jsign/jsign/7.4/jsign-7.4.jar" + fi + + CONF="$RUNNER_TEMP/sunpkcs11.conf" + { + echo "name = SimplySign" + echo "library = $SS_PKCS11" + echo "slotListIndex = 0" + } > "$CONF" + + expected_fingerprint="$(printf '%s' "$CERTUM_CERT_FINGERPRINT" | tr -d '[:space:]:')" + if [[ ! "$expected_fingerprint" =~ ^[[:xdigit:]]{64}$ ]]; then + echo "::error::CERTUM_CERT_FINGERPRINT must be the signer certificate SHA256 fingerprint." + exit 1 + fi + + shopt -s nullglob + pkgs=( signed/*.nupkg ) + [ "${#pkgs[@]}" -gt 0 ] || { echo "::error::no packages matched: signed/*.nupkg"; exit 1; } + + for pkg in "${pkgs[@]}"; do + echo "::group::sign $pkg" + java -jar "$JAR" --storetype PKCS11 --keystore "$CONF" --storepass "" \ + --alg SHA-256 --tsmode RFC3161 --tsaurl "http://time.certum.pl" --replace "$pkg" + dotnet nuget verify "$pkg" --all --certificate-fingerprint "$expected_fingerprint" --verbosity detailed + echo "::endgroup::" + done + + - name: Upload signed packages + uses: actions/upload-artifact@v7 + with: + name: signed-nuget + path: signed/*.nupkg + if-no-files-found: error + + publish-nuget: + needs: [release, sign] + runs-on: ubuntu-latest + environment: + name: release + permissions: + id-token: write + steps: + - name: Download signed packages + uses: actions/download-artifact@v8 + with: + name: signed-nuget + path: signed - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -255,7 +413,7 @@ jobs: - name: Push to NuGet shell: bash run: | - for pkg in *.nupkg; do + for pkg in ./signed/*.nupkg; do dotnet nuget push "$pkg" --source https://api.nuget.org/v3/index.json --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" done @@ -267,4 +425,4 @@ jobs: tag: ${{ needs.release.outputs.tag }} target: ${{ needs.release.outputs.sourceSha }} prerelease: ${{ needs.release.outputs.prerelease == 'true' }} - artifactName: unsigned-nuget + artifactName: signed-nuget