From 5a0429d46b485d9cb6e4f15a3e3a53d328cc2739 Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Mon, 14 Sep 2026 11:45:53 +0200 Subject: [PATCH 1/2] fix(ci): add SBOM+sign release chain and gate Coolify redeploy on it 0pt had no SBOM/sign/digest chain at all (re-confirmed live before this change: no cosign/syft/sbom hit anywhere under .gitea/workflows/, only doc mentions in CHANGELOG.md/docs/architecture/ARCHITECTURE.md). Applies the same two-part pattern already proven on sax3l/cl0ud and sax3l/st0re (fix/wire-signed-digest-to-deploy, 2026-09-14): 1. .gitea/workflows/release.yml (new): builds the image, pushes it to git.cloud.siax.io/sax3l/0pt:sha-<12>, SBOMs it (syft), signs it (cosign, key-based), verifies the signature in the same run, and commits source-bundle/production-evidence records back to the repo. 2. .gitea/workflows/cd.yml: the `deploy` job now polls the registry for this exact commit's release-chain image and cosign-verifies it (against the committed cosign.pub) before calling Coolify's deploy endpoint -- fails closed (10 min timeout) if release.yml never produced a verifiable artifact for this sha. Added AFTER the existing guard step, which still fails closed first on 0pt's real, pre-existing blocker: no Coolify Application is provisioned for 0pt yet (COOLIFY_APP_ID is a literal placeholder). This gate is wired ahead of that so it's already in place the day the app is provisioned, instead of a second follow-up change. Honest limitation carried over from cl0ud/st0re/h0st: once provisioned, 0pt's Coolify Application is expected to use the `dockerfile` build pack (per siax.config.json), so Coolify would still perform its own independent docker build rather than pulling the signed image, and /api/v1/deploy has no digest parameter to pin against -- true digest-pinning needs a production Coolify resource-type decision, out of CI-YAML scope. This gate proves a signed, verified build of the exact commit exists before authorizing any deploy call; it does not prove byte-identical output from Coolify's own rebuild. cosign.pub is a fresh, 0pt-specific keypair generated offline for this change. NOT YET SET on sax3l/0pt (release.yml/cd.yml's gate will fail closed until present): REGISTRY_USER, REGISTRY_TOKEN, COSIGN_PRIVATE_KEY, COSIGN_PASSWORD, SIAX_CI_TOKEN repo secrets. Validated without live Coolify/registry access: YAML-parsed all 2 files, bash -n syntax-checked all 19 embedded run: blocks, and dry-ran the actual verification logic -- the RepoDigests filter and bounded poll-retry loop against mocked scenarios, plus a real cosign sign+verify round-trip (offline signing-config, local throwaway registry container) proving the gate's cosign verify command genuinely passes on the correct key/digest and fails closed on a wrong key and on an unsigned image. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/cd.yml | 112 ++++++++++++++ .gitea/workflows/release.yml | 275 +++++++++++++++++++++++++++++++++++ cosign.pub | 4 + docs/runbook/deploy.md | 28 +++- 4 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 cosign.pub diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml index 6ff9a8c..11cea66 100644 --- a/.gitea/workflows/cd.yml +++ b/.gitea/workflows/cd.yml @@ -23,7 +23,38 @@ jobs: runs-on: ubuntu-latest env: COOLIFY_APP_ID: ${{ vars.COOLIFY_APP_ID || 'TODO-no-coolify-app-provisioned-yet' }} + IMAGE_REPO: git.cloud.siax.io/sax3l/0pt steps: + # ----------------------------------------------------------------- + # SIGNED-ARTIFACT GATE (added 2026-09-14, same pattern as + # sax3l/cl0ud + sax3l/st0re's fix/wire-signed-digest-to-deploy): once + # the guard step below stops failing (i.e. once a real Coolify app + # exists for 0pt and COOLIFY_APP_ID is filled in), this job will + # REFUSE to call Coolify at all unless a cosign-verified image exists + # in the registry for the EXACT commit being deployed. + # `.gitea/workflows/release.yml` (added in this same change) is the + # piece that builds, SBOMs, signs, and pushes that image. + # + # HONEST LIMITATION (identical to cl0ud/st0re/h0st's writeup): + # `siax.config.json`'s `deploy.target: "coolify"` and this file's own + # `/api/v1/deploy` call assume a `dockerfile` build-pack Application + # once provisioned -- same as cl0ud/st0re/h0st, Coolify would clone + # this repo and run its OWN independent `docker build`, never pulling + # the image release.yml pushed and signed, and `/api/v1/deploy` has no + # digest parameter. True digest-pinning would need 0pt's eventual + # Coolify app migrated to the separate "Docker Image" resource type -- + # a production infrastructure decision that hasn't been made yet + # (0pt isn't even provisioned on Coolify at all as of this writing; + # see the guard step below and docs/runbook/deploy.md §1), not + # something this CI-YAML change can or should decide. + # + # So: what this DOES guarantee, from the day 0pt's Coolify app exists + # -- Coolify is only ever told to rebuild+redeploy a commit whose + # release-chain artifact was built, SBOM'd, signed, and had its + # signature RE-verified in THIS job. What it does NOT guarantee -- + # bit-identical bytes between the signed image and whatever Coolify's + # own rebuild produces from the same source. + # ----------------------------------------------------------------- - name: Guard against unset token / placeholder app id run: | if [ -z "${{ secrets.COOLIFY_TOKEN }}" ]; then @@ -34,10 +65,91 @@ jobs: echo "::error::No Coolify app has been provisioned for 0pt yet. Create the app in the Coolify UI (cloud.siax.io), copy its UUID into COOLIFY_APP_ID in this file, and set the COOLIFY_TOKEN repo secret before this workflow can deploy anything." exit 1 fi + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve this commit's release-chain tag (same scheme as release.yml) + id: sha + run: | + set -euo pipefail + short="$(git rev-parse --short=12 HEAD)" + echo "short=$short" >> "$GITHUB_OUTPUT" + echo "image_ref=${IMAGE_REPO}:sha-$short" >> "$GITHUB_OUTPUT" + + - name: Docker login (git.cloud.siax.io) — read access to check release.yml's output + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + registry_host="$(echo "$IMAGE_REPO" | cut -d/ -f1)" + echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin + + - name: Install cosign v3.1.3 (pinned, checksum-verified — never :latest) + run: | + set -euo pipefail + cd /tmp + curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64 + curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign_checksums.txt + grep 'cosign-linux-amd64$' cosign_checksums.txt | sha256sum -c - + install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign + cosign version + + - name: Wait for release.yml's signed artifact for THIS commit, then verify (fail closed) + id: verify + env: + IMAGE_REF: ${{ steps.sha.outputs.image_ref }} + SHORT: ${{ steps.sha.outputs.short }} + run: | + set -euo pipefail + test -f cosign.pub || { echo "::error::cosign.pub not found at repo root — cannot verify, refusing to deploy"; exit 1; } + + # release.yml runs as a SEPARATE workflow triggered by the same push + # (Gitea/GitHub Actions has no cross-workflow-file `needs:`), so this + # job cannot depend on that job directly — it polls the registry for + # the artifact release.yml produces instead. Bounded: a genuinely + # broken/skipped release run fails this job loudly rather than + # hanging forever. + attempt=0 + max_attempts=40 + until docker pull "$IMAGE_REF" >/tmp/pull.log 2>&1; do + attempt=$((attempt + 1)) + if [ "$attempt" -ge "$max_attempts" ]; then + echo "::error::no signed image found at $IMAGE_REF after $max_attempts attempts (10 min) — release.yml has not produced an artifact for sha-$SHORT. Refusing to deploy an unverified rebuild." + cat /tmp/pull.log + exit 1 + fi + echo "image not yet available at $IMAGE_REF (attempt $attempt/$max_attempts) — waiting for release.yml..." + sleep 15 + done + + # Read back the digest the registry actually stored for this tag — + # same filter release.yml itself uses (RepoDigests can hold multiple + # entries; never trust index 0) — and verify THAT digest, never the + # mutable tag alone, so a tag reassigned after signing can't slip past. + repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \ + | node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const m=rd.find(r=>r.startsWith(prefix)); if(!m){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(m);")" + digest="${repo_digest##*@}" + echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; } + + echo "verifying cosign signature on ${IMAGE_REPO}@${digest} ..." + cosign verify --key cosign.pub "${IMAGE_REPO}@${digest}" + + echo "digest=$digest" >> "$GITHUB_OUTPUT" + echo "SIGNED ARTIFACT VERIFIED: ${IMAGE_REPO}@${digest} — authorizing Coolify redeploy of sha-$SHORT" + - name: Trigger Coolify redeploy env: COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} + VERIFIED_DIGEST: ${{ steps.verify.outputs.digest }} run: | + # See the SIGNED-ARTIFACT GATE comment above: Coolify's /deploy has + # no digest/image param to pass, so $VERIFIED_DIGEST is not (and + # cannot be) sent to Coolify — it is recorded here as the + # provenance proof that authorized this specific redeploy call. + echo "authorized by verified digest: $VERIFIED_DIGEST" curl -fsS -X POST \ "https://cloud.siax.io/api/v1/deploy?uuid=${COOLIFY_APP_ID}&force=false" \ -H "Authorization: Bearer $COOLIFY_TOKEN" \ diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..f3bf22b --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,275 @@ +name: Release chain (P007 pattern) + +# ADDED 2026-09-14: 0pt had NO SBOM/sign/digest chain at all before this file +# (re-confirmed live: no cosign/syft/sbom hit anywhere under .gitea/workflows/, +# only doc mentions in CHANGELOG.md/docs/architecture/ARCHITECTURE.md). This +# is the proven sax3l/cl0ud + sax3l/st0re pattern (originally inlined +# 2026-09-10 from sax3l/siax-standard's release-chain.yml, then hardened +# 2026-09-14), authored directly for 0pt because cross-repo `uses:` clones +# anonymously on this Gitea instance and fails with "repository not found" +# against private repos (the same documented trap that forced cl0ud/st0re to +# inline rather than call out to a shared workflow). +# +# Cosign keypair is 0pt-specific (generated offline 2026-09-14, never +# transmitted anywhere); cosign.pub in the repo root is the corresponding +# public key. Neither this Dockerfile nor h0st's declares +# `ARG GIT_SHA`/`ARG BUILD_TIME`, so this file does not pass them as +# --build-arg (would just produce a harmless-but-noisy "not consumed" +# warning) -- GIT_SHA/DIGEST still go into the evidence JSON below either way. +# +# STATUS NOTE (see .gitea/workflows/cd.yml's own header): 0pt has NO Coolify +# Application provisioned yet -- cd.yml's guard step already fails closed on +# the placeholder COOLIFY_APP_ID for that reason, independent of this file. +# This release chain is deliberately built ahead of that: once the app +# exists and cd.yml's placeholder is replaced with a real UUID, the signed- +# artifact gate added to cd.yml in this same change is already wired and +# ready, instead of being a second follow-up piece of work. +# +# Krävda repo-secrets (NOT YET SET on sax3l/0pt as of this writing -- this +# file will fail closed on its first run until Simon sets them, same +# prerequisite cl0ud/st0re needed before their release.yml went green +# 2026-09-10): REGISTRY_USER/REGISTRY_TOKEN (git.cloud.siax.io push), +# COSIGN_PRIVATE_KEY/COSIGN_PASSWORD (the private half of this repo's +# cosign.pub), SIAX_CI_TOKEN (scoped write:repository token for the +# evidence-commit push -- the runner's ephemeral actions token gets a +# pre-receive 500, GetUserByID(uid=-2), same fleet-wide gotcha as cl0ud/st0re). +# Verification: cosign verify --key cosign.pub git.cloud.siax.io/sax3l/0pt@ + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + release: + name: Build, push, SBOM, sign, record evidence + runs-on: ubuntu-latest + permissions: + contents: write + env: + IMAGE_REPO: git.cloud.siax.io/sax3l/0pt + DOCKERFILE: Dockerfile + EVIDENCE_DIR: siax + SERVICE_NAME: "@siax/0pt" + REF_NAME: ${{ github.ref_name }} + RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + steps: + - uses: https://git.siax.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4, mirrored + SHA-pinned (SUP-06, same pin n0d/cl0ud/st0re already use) + with: + fetch-depth: 0 + + - name: Resolve SHA + tag (never :latest) + id: sha + run: | + set -euo pipefail + full="$(git rev-parse HEAD)" + short="$(git rev-parse --short=12 HEAD)" + built_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "full=$full" >> "$GITHUB_OUTPUT" + echo "short=$short" >> "$GITHUB_OUTPUT" + echo "built_at=$built_at" >> "$GITHUB_OUTPUT" + echo "image_ref=${IMAGE_REPO}:sha-$short" >> "$GITHUB_OUTPUT" + + - name: Docker login (git.cloud.siax.io) + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + registry_host="$(echo "$IMAGE_REPO" | cut -d/ -f1)" + echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin + + - name: Build + env: + IMAGE_REF: ${{ steps.sha.outputs.image_ref }} + run: | + set -euo pipefail + docker build \ + -f "$DOCKERFILE" \ + -t "$IMAGE_REF" \ + . + + - name: Push (tag sha-<12>, never :latest) + env: + IMAGE_REF: ${{ steps.sha.outputs.image_ref }} + run: docker push "$IMAGE_REF" + + - name: Read back the digest the registry actually stored (never assumed) + id: digest + env: + IMAGE_REF: ${{ steps.sha.outputs.image_ref }} + run: | + set -euo pipefail + # `docker inspect`'s RepoDigests can carry MULTIPLE entries (e.g. one for + # whatever this image was originally pulled/built FROM, plus one per registry + # it has since been pushed to) — index 0 is not reliably "the one we just + # pushed". Filter for the entry whose repo matches IMAGE_REPO instead (same + # filter proven live on cl0ud's P007 build-sandbox: a probe push showed + # RepoDigests holding both an "alpine@sha256:..." entry AND the + # "git.cloud.siax.io/...@sha256:..." entry side by side). + repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \ + | node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const match=rd.find(r=>r.startsWith(prefix)); if(!match){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(match);")" + digest="${repo_digest##*@}" + echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; } + echo "digest=$digest" >> "$GITHUB_OUTPUT" + echo "pushed and verified: $IMAGE_REF @ $digest" + + # KNOWN RISK, NOT YET FIXED (same open issue as cl0ud's/st0re's/h0st's + # copy of this file, flagged 2026-09-03/09-14): this fleet's runner + # egress is confirmed to reach raw.githubusercontent.com but NOT + # github.com/*/releases/download URLs (curl -sfL -> silent failure). + # The two `curl -fsSL -O https://github.com/.../releases/download/...` + # steps below (syft, cosign) use exactly that broken URL shape, and + # neither tool ships a raw.githubusercontent.com-hosted installer that + # avoids a github.com/releases fetch for the actual binary. Left as + # pinned, checksum-verified curl installs, same as cl0ud/st0re/h0st; + # the first real run of this file on 0pt's own runners is the first + # live evidence for OR against this risk actually manifesting here + # (not simulated in this dry-run pass — see PR description). + - name: Install syft v1.51.1 (pinned, checksum-verified — never :latest) + run: | + set -euo pipefail + cd /tmp + curl -fsSL -O https://github.com/anchore/syft/releases/download/v1.51.1/syft_1.51.1_linux_amd64.tar.gz + curl -fsSL -O https://github.com/anchore/syft/releases/download/v1.51.1/syft_1.51.1_checksums.txt + grep 'syft_1.51.1_linux_amd64.tar.gz' syft_1.51.1_checksums.txt | sha256sum -c - + tar -xzf syft_1.51.1_linux_amd64.tar.gz syft + install -m 0755 syft /usr/local/bin/syft + syft version + + - name: Install cosign v3.1.3 (pinned, checksum-verified — never :latest) + run: | + set -euo pipefail + cd /tmp + curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64 + curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign_checksums.txt + grep 'cosign-linux-amd64$' cosign_checksums.txt | sha256sum -c - + install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign + cosign version + + - name: SBOM (syft, spdx-json, against the DIGEST not the tag) + env: + DIGEST: ${{ steps.digest.outputs.digest }} + SHORT: ${{ steps.sha.outputs.short }} + run: | + set -euo pipefail + mkdir -p "$EVIDENCE_DIR/_release-artifacts" + syft "${IMAGE_REPO}@${DIGEST}" -o "spdx-json=${EVIDENCE_DIR}/_release-artifacts/sbom-${SHORT}.spdx.json" + + - name: Sign (cosign, key-based — see cosign.pub at repo root) + env: + COSIGN_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + DIGEST: ${{ steps.digest.outputs.digest }} + run: | + set -euo pipefail + echo "$COSIGN_KEY" > /tmp/cosign.key + cosign sign --yes --key /tmp/cosign.key "${IMAGE_REPO}@${DIGEST}" + rm -f /tmp/cosign.key + + - name: Verify the signature just written (fail closed if it does not verify) + env: + DIGEST: ${{ steps.digest.outputs.digest }} + run: | + set -euo pipefail + test -f cosign.pub || { echo "::error::cosign.pub not found at repo root — commit the PUBLIC key (not a secret) so anyone can verify without CI access"; exit 1; } + cosign verify --key cosign.pub "${IMAGE_REPO}@${DIGEST}" + + - name: Write siax.source-bundle + siax.production-evidence records + env: + SHA_FULL: ${{ steps.sha.outputs.full }} + SHA_SHORT: ${{ steps.sha.outputs.short }} + DIGEST: ${{ steps.digest.outputs.digest }} + IMAGE_REF: ${{ steps.sha.outputs.image_ref }} + run: | + set -euo pipefail + mkdir -p "$EVIDENCE_DIR/source-bundles" "$EVIDENCE_DIR/production-evidence" + now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + repo_name="$(basename "$(git rev-parse --show-toplevel)")" + + cat > "$EVIDENCE_DIR/source-bundles/release-${SHA_SHORT}.json" < "$EVIDENCE_DIR/production-evidence/release-${SHA_SHORT}-artifact.json" < "$EVIDENCE_DIR/production-evidence/release-${SHA_SHORT}-sbom.json" < "$EVIDENCE_DIR/production-evidence/release-${SHA_SHORT}-signature.json" <`. `cd.yml`s `deploy`-jobb har nu ett +extra steg (efter guard-steget i punkt 1 ovan) som kräver att exakt denna +commits avbildning finns i registret och att dess cosign-signatur +verifierar (mot committad `cosign.pub`) innan Coolify överhuvudtaget +anropas. + +Detta är byggt FÖRE Coolify-appen finns (se blockerarna i punkt 1) — så att +grinden redan är på plats den dag appen provisioneras, i stället för att bli +ett separat uppföljningsarbete. Samma ärliga begränsning som cl0ud/st0re/ +h0st: när appen väl provisioneras som `dockerfile` build pack (så som +`siax.config.json` föreskriver) kör Coolify sin EGEN oberoende +`docker build` från git-källan, inte den signerade avbildningen — +`/api/v1/deploy` saknar digest-parameter helt. Sann digest-pinning skulle +kräva att appen provisioneras som Coolifys separata `dockerimage`-resurstyp +i stället, ett beslut som inte är fattat än. Se `.gitea/workflows/cd.yml`s +"SIGNED-ARTIFACT GATE"-kommentar för den fullständiga skrivningen. + +**Inte satt på `sax3l/0pt` ännu:** repo-secrets `REGISTRY_USER`, +`REGISTRY_TOKEN`, `COSIGN_PRIVATE_KEY`, `COSIGN_PASSWORD`, `SIAX_CI_TOKEN` — +`release.yml` fail-closed:ar på sin första körning tills dessa finns. + ## 2. Deploy (när appen finns) 1. Push till `main` → `.gitea/workflows/cd.yml` POSTar till - `https://cloud.siax.io/api/v1/deploy?uuid=$COOLIFY_APP_ID&force=false`. + `https://cloud.siax.io/api/v1/deploy?uuid=$COOLIFY_APP_ID&force=false`, + efter att den signerade-artefakt-grinden i punkt 1b gått grön. 2. Följ run i Gitea Actions; verifiera att den slutar grön. 3. Verifiera hälsa: `curl https://0pt.app/api/health` → `ok:true` och `db: configured` (när Postgres kopplats in). -- 2.54.0 From a403424f479d524e80e7712f90e1c8ba4fd958e1 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 15 Sep 2026 03:54:41 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(release-chain):=20cosign.pub=20=3D=20ny?= =?UTF-8?q?ckelparet=20som=20s=C3=A4tts=20i=20repo-secrets=20(genererat=20?= =?UTF-8?q?2026-09-15;=20det=20offline-genererade=20paret=20fr=C3=A5n=2009?= =?UTF-8?q?-14=20saknas=20i=20den=20h=C3=A4r=20sessionen=20=E2=80=94=20nyt?= =?UTF-8?q?t=20par,=20private-del=20g=C3=A5r=20endast=20till=20repo-secret?= =?UTF-8?q?=20COSIGN=5FPRIVATE=5FKEY=20+=20Infisical)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosign.pub | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cosign.pub b/cosign.pub index 87973c0..9959c27 100644 --- a/cosign.pub +++ b/cosign.pub @@ -1,4 +1,4 @@ -----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3AMWlyuwKcdgaDje4yhc6iz5dAnO -2hxZAO9VcJqBXZpyLTFK84oubcPqouZaHIK+ZQzSbjM8QnyHqFPaYhU9Yg== +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3EIrsPWBhSnH6HL7yD9US1RtXDg4 +kHaLp8mDefBhepQRFsFPedyWwNaaqEwA3Pe1G2qBdm/FgAMAFkYjZyvgCg== -----END PUBLIC KEY----- -- 2.54.0