Prepare release engine
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
/vendor/
|
/vendor/
|
||||||
/vendor.lit-bak/
|
/vendor.lit-bak/
|
||||||
/dist/
|
/dist/
|
||||||
|
/.gitea-token
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
|||||||
+10
-13
@@ -3,9 +3,9 @@
|
|||||||
#
|
#
|
||||||
# bin/release-check.sh [version] default: Plugin::VERSION
|
# bin/release-check.sh [version] default: Plugin::VERSION
|
||||||
#
|
#
|
||||||
# Fails unless releases/latest is that version, ZIP and .sig are public downloads below the repository's release
|
# Fails unless releases/latest is that version, the ZIP is a public download below the repository's release URL (the
|
||||||
# URL (the only place GiteaUpdater accepts a package from), and the downloaded ZIP passes the signature check against
|
# only place GiteaUpdater accepts a package from), and the downloaded ZIP is the plugin in that version with its
|
||||||
# the key built into the plugin.
|
# autoloader.
|
||||||
#
|
#
|
||||||
# Environment: LIT_MCP_GITEA (default https://git.loosen-it.de), only to try the script against another server.
|
# Environment: LIT_MCP_GITEA (default https://git.loosen-it.de), only to try the script against another server.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -25,18 +25,15 @@ latest="$(curl -fsS "$gitea/api/v1/repos/$repo/releases/latest")" \
|
|||||||
tag="$(jq -r .tag_name <<<"$latest")"
|
tag="$(jq -r .tag_name <<<"$latest")"
|
||||||
[[ "$tag" == "v$version" ]] || fail "releases/latest liefert '$tag', erwartet v$version"
|
[[ "$tag" == "v$version" ]] || fail "releases/latest liefert '$tag', erwartet v$version"
|
||||||
|
|
||||||
asset_url() { jq -r --arg n "$1" '.assets[] | select(.name == $n) | .browser_download_url' <<<"$latest"; }
|
zip_url="$(jq -r --arg n "$name" '.assets[] | select(.name == $n) | .browser_download_url' <<<"$latest")"
|
||||||
zip_url="$(asset_url "$name")"
|
[[ "$zip_url" == "$gitea/$repo/releases/download/"* ]] || fail "Asset-URL fehlt oder liegt woanders: '$zip_url'"
|
||||||
sig_url="$(asset_url "$name.sig")"
|
|
||||||
for url in "$zip_url" "$sig_url"; do
|
|
||||||
[[ "$url" == "$gitea/$repo/releases/download/"* ]] || fail "Asset-URL fehlt oder liegt woanders: '$url'"
|
|
||||||
done
|
|
||||||
|
|
||||||
work="$(mktemp -d)"
|
work="$(mktemp -d)"
|
||||||
trap 'rm -rf "$work"' EXIT
|
trap 'rm -rf "$work"' EXIT
|
||||||
curl -fsSL "$zip_url" -o "$work/$name" || fail "ZIP nicht ohne Anmeldung ladbar: $zip_url"
|
curl -fsSL "$zip_url" -o "$work/$name" || fail "ZIP nicht ohne Anmeldung ladbar: $zip_url"
|
||||||
curl -fsSL "$sig_url" -o "$work/$name.sig" || fail "Signatur nicht ohne Anmeldung ladbar: $sig_url"
|
entries="$(bsdtar -tf "$work/$name")" || fail "Das heruntergeladene ZIP ist kein gültiges ZIP"
|
||||||
php bin/release-sign.php verify "$work/$name" "$work/$name.sig" >/dev/null \
|
grep -qx 'wp-lit-mcp/vendor/autoload_packages.php' <<<"$entries" || fail "Im ZIP fehlt der Autoloader (vendor/)"
|
||||||
|| fail "Das heruntergeladene Release besteht die Signaturprüfung nicht"
|
[[ "$(bsdtar -xOf "$work/$name" wp-lit-mcp/wp-lit-mcp.php | sed -n 's/^ \* Version: //p')" == "$version" ]] \
|
||||||
|
|| fail "Das ZIP enthält nicht Version $version"
|
||||||
|
|
||||||
echo "Release v$version in Ordnung: öffentlich ladbar, Signatur gültig"
|
echo "Release v$version in Ordnung: öffentlich ladbar, Plugin und Autoloader im ZIP"
|
||||||
|
|||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Publishes a release built by bin/release.sh: draft -> ZIP attached and checked -> published -> checked from outside
|
||||||
|
# (bin/release-check.sh).
|
||||||
|
#
|
||||||
|
# bin/release-publish.sh [version] default: Plugin::VERSION
|
||||||
|
#
|
||||||
|
# The release only turns public once the ZIP hangs on it, so no customer site ever sees a release without its
|
||||||
|
# package. An existing draft for the tag (created by hand or by an earlier run) is reused; a published one is refused.
|
||||||
|
# An existing tag fixes the commit (a release recreated after deleting it), a new tag points at HEAD.
|
||||||
|
#
|
||||||
|
# Needs: curl, jq, bsdtar and a Gitea token with write access to the repository (see bin/token.sh).
|
||||||
|
# Environment: LIT_MCP_GITEA (default https://git.loosen-it.de), only to try the script against another server.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
gitea="${LIT_MCP_GITEA:-https://git.loosen-it.de}"
|
||||||
|
api="$gitea/api/v1/repos/janniclas.loosen/WP-LIT-MCP"
|
||||||
|
version="${1:-$(sed -n "s/.*const VERSION = '\(.*\)';/\1/p" src/Plugin.php)}"
|
||||||
|
name="wp-lit-mcp-$version.zip"
|
||||||
|
zip="dist/$name"
|
||||||
|
|
||||||
|
fail() { echo "ABBRUCH: $*" >&2; exit 1; }
|
||||||
|
step() { echo "==> $*"; }
|
||||||
|
|
||||||
|
source "$root/bin/token.sh"
|
||||||
|
load_token
|
||||||
|
[[ -r "$zip" ]] || fail "dist/ hat nicht $name: erst bin/release.sh"
|
||||||
|
|
||||||
|
# A stale file in dist/ is the cheapest way to ship the wrong release.
|
||||||
|
step "ZIP prüfen"
|
||||||
|
[[ "$(bsdtar -xOf "$zip" wp-lit-mcp/wp-lit-mcp.php | sed -n 's/^ \* Version: //p')" == "$version" ]] \
|
||||||
|
|| fail "Das ZIP enthält nicht Version $version"
|
||||||
|
|
||||||
|
auth=(-H "Authorization: token $GITEA_TOKEN")
|
||||||
|
json=(-H 'Content-Type: application/json')
|
||||||
|
api_get() { curl -fsS "${auth[@]}" "$api/$1"; }
|
||||||
|
|
||||||
|
step "Entwurf v$version suchen oder anlegen"
|
||||||
|
release="$(api_get 'releases?per_page=50' | jq -c --arg t "v$version" '[.[] | select(.tag_name == $t)][0] // empty')"
|
||||||
|
if [[ -z "$release" ]]; then
|
||||||
|
notes="$(awk -v header="= $version =" '$0 == header { on = 1; next } /^= / { on = 0 } on' readme.txt)"
|
||||||
|
target="$(curl -sS "${auth[@]}" "$api/tags/v$version" | jq -r '.commit.sha // empty')"
|
||||||
|
[[ -n "$target" ]] || target="$(git rev-parse HEAD)"
|
||||||
|
request="$(jq -n --arg tag "v$version" --arg sha "$target" --arg body "$notes" \
|
||||||
|
'{tag_name: $tag, target_commitish: $sha, name: $tag, body: $body, draft: true}')"
|
||||||
|
release="$(curl -fsS "${auth[@]}" "${json[@]}" -d "$request" "$api/releases")" || fail "Release ließ sich nicht anlegen"
|
||||||
|
fi
|
||||||
|
[[ "$(jq -r .draft <<<"$release")" == "true" ]] || fail "v$version ist schon veröffentlicht"
|
||||||
|
id="$(jq -r .id <<<"$release")"
|
||||||
|
|
||||||
|
step "ZIP anhängen"
|
||||||
|
old="$(jq -r --arg n "$name" '.assets[]? | select(.name == $n) | .id' <<<"$release")"
|
||||||
|
[[ -z "$old" ]] || curl -fsS -X DELETE "${auth[@]}" "$api/releases/$id/assets/$old" >/dev/null
|
||||||
|
curl -fsS "${auth[@]}" -F "attachment=@$zip" "$api/releases/$id/assets?name=$name" >/dev/null \
|
||||||
|
|| fail "Upload von $name fehlgeschlagen"
|
||||||
|
|
||||||
|
# Read it back instead of trusting the upload: only a draft that really carries the ZIP goes public.
|
||||||
|
step "Anhang nachprüfen"
|
||||||
|
size="$(api_get "releases/$id" | jq -r --arg n "$name" '.assets[]? | select(.name == $n) | .size')"
|
||||||
|
[[ "$size" == "$(stat -c %s "$zip")" ]] || fail "$name hängt nicht oder mit falscher Größe am Entwurf (Gitea: '$size')"
|
||||||
|
|
||||||
|
step "Veröffentlichen"
|
||||||
|
curl -fsS -X PATCH "${auth[@]}" "${json[@]}" -d '{"draft": false}' "$api/releases/$id" >/dev/null \
|
||||||
|
|| fail "Veröffentlichen fehlgeschlagen"
|
||||||
|
|
||||||
|
step "Ergebnis von außen prüfen"
|
||||||
|
exec bash bin/release-check.sh "$version"
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release signing for WP-LIT-MCP (called by bin/release.sh, usable by hand).
|
|
||||||
*
|
|
||||||
* php bin/release-sign.php keygen <key-file> new key pair; prints the public key for PackageSignature
|
|
||||||
* php bin/release-sign.php sign <zip> <key-file> prints the signature (the .sig asset)
|
|
||||||
* php bin/release-sign.php verify <zip> <sig-file> checks against the key built into the plugin
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
// The class guards against direct web access; this is the command line.
|
|
||||||
define('ABSPATH', __DIR__ . '/');
|
|
||||||
require_once __DIR__ . '/../src/Updater/PackageSignature.php';
|
|
||||||
|
|
||||||
use LIT\MCP\Updater\PackageSignature;
|
|
||||||
|
|
||||||
function fail(string $message): never
|
|
||||||
{
|
|
||||||
fwrite(STDERR, $message . "\n");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[, $command, $first, $second] = $argv + [null, '', '', ''];
|
|
||||||
|
|
||||||
switch ($command) {
|
|
||||||
case 'keygen':
|
|
||||||
if ($first === '' || file_exists($first)) {
|
|
||||||
fail('usage: keygen <key-file> (an existing file is never overwritten)');
|
|
||||||
}
|
|
||||||
$pair = sodium_crypto_sign_keypair();
|
|
||||||
if (! is_dir(dirname($first)) && ! mkdir(dirname($first), 0700, true)) {
|
|
||||||
fail('cannot create ' . dirname($first));
|
|
||||||
}
|
|
||||||
// 0600 before the secret is written, so it is never readable by others.
|
|
||||||
touch($first);
|
|
||||||
chmod($first, 0600);
|
|
||||||
file_put_contents($first, base64_encode(sodium_crypto_sign_secretkey($pair)));
|
|
||||||
echo base64_encode(sodium_crypto_sign_publickey($pair)), "\n";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'sign':
|
|
||||||
$secret = is_readable($second) ? base64_decode(trim((string) file_get_contents($second)), true) : false;
|
|
||||||
if (! is_readable($first) || $secret === false || strlen($secret) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) {
|
|
||||||
fail('usage: sign <zip> <key-file> (readable ZIP, key file from keygen)');
|
|
||||||
}
|
|
||||||
$message = PackageSignature::message(basename($first), $first);
|
|
||||||
echo base64_encode(sodium_crypto_sign_detached($message, $secret)), "\n";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'verify':
|
|
||||||
if (! is_readable($first) || ! is_readable($second)) {
|
|
||||||
fail('usage: verify <zip> <sig-file>');
|
|
||||||
}
|
|
||||||
if (! (new PackageSignature())->verifies(basename($first), $first, (string) file_get_contents($second))) {
|
|
||||||
fail('signature does not verify against the key built into the plugin');
|
|
||||||
}
|
|
||||||
echo "ok\n";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
fail('usage: keygen <key-file> | sign <zip> <key-file> | verify <zip> <sig-file>');
|
|
||||||
}
|
|
||||||
+9
-20
@@ -1,20 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Builds and signs a WP-LIT-MCP release from the committed state. It publishes nothing: tag, Gitea release and the
|
# Builds a WP-LIT-MCP release ZIP from the committed state into dist/. It publishes nothing: bin/ship.sh does the
|
||||||
# upload of the two files in dist/ are done afterwards, then bin/release-check.sh looks at the result.
|
# whole way (this script, push, release, upload, check from outside).
|
||||||
#
|
#
|
||||||
# bin/release.sh
|
# bin/release.sh
|
||||||
#
|
#
|
||||||
# Needs: bsdtar, composer, php (sodium). The signing key (php bin/release-sign.php keygen) lives outside the
|
# Needs: bsdtar, composer.
|
||||||
# repository.
|
# Environment: LIT_MCP_DIVI_CHECK (command that must succeed before a release, default:
|
||||||
#
|
# ddev wp --user=janniclas lit-mcp divi-check).
|
||||||
# Environment: LIT_MCP_RELEASE_KEY (default ~/.config/wp-lit-mcp/release.key), LIT_MCP_DIVI_CHECK (command that must
|
|
||||||
# succeed before a release, default: ddev wp --user=janniclas lit-mcp divi-check).
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$root"
|
cd "$root"
|
||||||
|
|
||||||
key="${LIT_MCP_RELEASE_KEY:-$HOME/.config/wp-lit-mcp/release.key}"
|
|
||||||
divi_check="${LIT_MCP_DIVI_CHECK:-ddev wp --user=janniclas lit-mcp divi-check}"
|
divi_check="${LIT_MCP_DIVI_CHECK:-ddev wp --user=janniclas lit-mcp divi-check}"
|
||||||
|
|
||||||
fail() { echo "ABBRUCH: $*" >&2; exit 1; }
|
fail() { echo "ABBRUCH: $*" >&2; exit 1; }
|
||||||
@@ -29,7 +26,6 @@ version="$(sed -n "s/.*const VERSION = '\(.*\)';/\1/p" src/Plugin.php)"
|
|||||||
grep -q "^= $version =\$" readme.txt || fail "readme.txt hat keinen Changelog-Eintrag für $version"
|
grep -q "^= $version =\$" readme.txt || fail "readme.txt hat keinen Changelog-Eintrag für $version"
|
||||||
[[ -z "$(git status --porcelain)" ]] || fail "Arbeitsbaum nicht sauber: erst committen, das ZIP entsteht aus HEAD"
|
[[ -z "$(git status --porcelain)" ]] || fail "Arbeitsbaum nicht sauber: erst committen, das ZIP entsteht aus HEAD"
|
||||||
! git rev-parse -q --verify "refs/tags/v$version" >/dev/null || fail "Tag v$version existiert schon"
|
! git rev-parse -q --verify "refs/tags/v$version" >/dev/null || fail "Tag v$version existiert schon"
|
||||||
[[ -r "$key" ]] || fail "Signierschlüssel fehlt: $key (php bin/release-sign.php keygen <Datei>)"
|
|
||||||
|
|
||||||
# --- 2. Tests and the Divi check must be green ------------------------------------------------------------------------
|
# --- 2. Tests and the Divi check must be green ------------------------------------------------------------------------
|
||||||
step "Tests"
|
step "Tests"
|
||||||
@@ -46,8 +42,9 @@ git archive HEAD | tar -x -C "$work/wp-lit-mcp"
|
|||||||
(cd "$work/wp-lit-mcp" && composer install --no-dev --optimize-autoloader --no-interaction --quiet)
|
(cd "$work/wp-lit-mcp" && composer install --no-dev --optimize-autoloader --no-interaction --quiet)
|
||||||
name="wp-lit-mcp-$version.zip"
|
name="wp-lit-mcp-$version.zip"
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
|
# Files of earlier releases would be one wrong click away from the upload.
|
||||||
|
rm -f dist/wp-lit-mcp-*
|
||||||
zip="$root/dist/$name"
|
zip="$root/dist/$name"
|
||||||
rm -f "$zip" "$zip.sig"
|
|
||||||
(cd "$work" && bsdtar --format zip -cf "$zip" wp-lit-mcp)
|
(cd "$work" && bsdtar --format zip -cf "$zip" wp-lit-mcp)
|
||||||
|
|
||||||
# --- 4. Look into the ZIP like WordPress will: one folder, the plugin, its autoloader, the right version --------------
|
# --- 4. Look into the ZIP like WordPress will: one folder, the plugin, its autoloader, the right version --------------
|
||||||
@@ -57,18 +54,10 @@ if grep -qv '^wp-lit-mcp/' <<<"$entries"; then fail "ZIP enthält Einträge auß
|
|||||||
for need in wp-lit-mcp/wp-lit-mcp.php wp-lit-mcp/vendor/autoload_packages.php; do
|
for need in wp-lit-mcp/wp-lit-mcp.php wp-lit-mcp/vendor/autoload_packages.php; do
|
||||||
grep -qx "$need" <<<"$entries" || fail "Im ZIP fehlt $need"
|
grep -qx "$need" <<<"$entries" || fail "Im ZIP fehlt $need"
|
||||||
done
|
done
|
||||||
if grep -qE '^wp-lit-mcp/(tests|bin|docs/superpowers|\.git)(/|$)' <<<"$entries"; then
|
if grep -qE '^wp-lit-mcp/(tests|bin|docs/superpowers|\.git|\.gitea-token)(/|$)' <<<"$entries"; then
|
||||||
fail "Entwicklungsdateien im ZIP"
|
fail "Entwicklungsdateien im ZIP"
|
||||||
fi
|
fi
|
||||||
[[ "$(bsdtar -xOf "$zip" wp-lit-mcp/wp-lit-mcp.php | sed -n 's/^ \* Version: //p')" == "$version" ]] \
|
[[ "$(bsdtar -xOf "$zip" wp-lit-mcp/wp-lit-mcp.php | sed -n 's/^ \* Version: //p')" == "$version" ]] \
|
||||||
|| fail "Version im ZIP stimmt nicht mit $version überein"
|
|| fail "Version im ZIP stimmt nicht mit $version überein"
|
||||||
|
|
||||||
# --- 5. Sign, and prove the signature against the key built into the plugin -------------------------------------------
|
echo "In Ordnung: dist/$name"
|
||||||
step "Signieren"
|
|
||||||
php bin/release-sign.php sign "$zip" "$key" > "$zip.sig"
|
|
||||||
php bin/release-sign.php verify "$zip" "$zip.sig" >/dev/null \
|
|
||||||
|| fail "Signatur passt nicht zum Schlüssel im Plugin (PackageSignature::RELEASE_KEY)"
|
|
||||||
|
|
||||||
echo "In Ordnung: dist/$name und dist/$name.sig"
|
|
||||||
echo "Weiter: Tag v$version und Release auf Gitea anlegen, beide Dateien hochladen, veröffentlichen."
|
|
||||||
echo "Danach: bin/release-check.sh $version"
|
|
||||||
|
|||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One command for a release: build (tests, Divi check, ZIP), push main, publish, check from outside.
|
||||||
|
#
|
||||||
|
# bin/ship.sh
|
||||||
|
#
|
||||||
|
# Run it when Plugin::VERSION is bumped and committed (release.sh refuses a dirty tree or an existing tag). The token
|
||||||
|
# comes from $GITEA_TOKEN or a file (see bin/token.sh).
|
||||||
|
#
|
||||||
|
# Environment: LIT_MCP_GITEA (default https://git.loosen-it.de), only to try the script against another server.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
gitea="${LIT_MCP_GITEA:-https://git.loosen-it.de}"
|
||||||
|
repo="janniclas.loosen/WP-LIT-MCP"
|
||||||
|
|
||||||
|
fail() { echo "ABBRUCH: $*" >&2; exit 1; }
|
||||||
|
step() { echo "==> $*"; }
|
||||||
|
|
||||||
|
source "$root/bin/token.sh"
|
||||||
|
load_token
|
||||||
|
export GITEA_TOKEN
|
||||||
|
[[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]] || fail "Releases gehen nur von main aus"
|
||||||
|
|
||||||
|
bash bin/release.sh
|
||||||
|
|
||||||
|
# Push only if Gitea does not have HEAD yet. The token travels in a header of this one call, via the environment: not
|
||||||
|
# in the URL, not in the git configuration, not in the process list.
|
||||||
|
remote_head="$(curl -fsS "$gitea/api/v1/repos/$repo/branches/main" | jq -r .commit.id)"
|
||||||
|
if [[ "$remote_head" != "$(git rev-parse HEAD)" ]]; then
|
||||||
|
step "main pushen"
|
||||||
|
basic="$(printf 'janniclas.loosen:%s' "$GITEA_TOKEN" | base64 -w0)"
|
||||||
|
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraHeader GIT_CONFIG_VALUE_0="Authorization: Basic $basic" \
|
||||||
|
git push "$gitea/$repo.git" HEAD:main || fail "Push fehlgeschlagen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
bash bin/release-publish.sh
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Loads the Gitea token into GITEA_TOKEN: the environment first, then .gitea-token in the repository (gitignored),
|
||||||
|
# then ~/.config/wp-lit-mcp/gitea-token. Sourced by bin/ship.sh and bin/release-publish.sh, which define $root and fail().
|
||||||
|
|
||||||
|
load_token() {
|
||||||
|
local file
|
||||||
|
for file in "$root/.gitea-token" "$HOME/.config/wp-lit-mcp/gitea-token"; do
|
||||||
|
[[ -z "${GITEA_TOKEN:-}" && -r "$file" ]] || continue
|
||||||
|
# A token file the repository would commit, or that others can read, is refused before it is used.
|
||||||
|
if [[ "$file" == "$root/.gitea-token" ]]; then
|
||||||
|
git -C "$root" check-ignore -q .gitea-token || fail ".gitea-token steht nicht in .gitignore"
|
||||||
|
fi
|
||||||
|
[[ "$(stat -c %a "$file")" =~ ^[0-7]00$ ]] || fail "$file ist für andere lesbar: chmod 600"
|
||||||
|
GITEA_TOKEN="$(<"$file")"
|
||||||
|
done
|
||||||
|
[[ -n "${GITEA_TOKEN:-}" ]] \
|
||||||
|
|| fail "Kein Token: GITEA_TOKEN setzen oder .gitea-token im Repo (alternativ ~/.config/wp-lit-mcp/gitea-token) anlegen"
|
||||||
|
}
|
||||||
+2
-6
@@ -200,8 +200,7 @@ Bibliothekseintrags.
|
|||||||
Neue Versionen erscheinen als Release auf git.loosen-it.de; WordPress zeigt sie unter Plugins und Dashboard →
|
Neue Versionen erscheinen als Release auf git.loosen-it.de; WordPress zeigt sie unter Plugins und Dashboard →
|
||||||
Aktualisierungen an wie jedes andere Plugin. Automatisch einspielen lässt sich ein Update mit dem Schalter
|
Aktualisierungen an wie jedes andere Plugin. Automatisch einspielen lässt sich ein Update mit dem Schalter
|
||||||
„Automatische Updates aktivieren“ in der Plugin-Liste. Ein Update ersetzt den Plugin-Ordner; ein Entwicklungs-
|
„Automatische Updates aktivieren“ in der Plugin-Liste. Ein Update ersetzt den Plugin-Ordner; ein Entwicklungs-
|
||||||
Checkout mit .git-Ordner bekommt deshalb kein Update angeboten. Jedes Paket ist signiert (Ed25519); WordPress
|
Checkout mit .git-Ordner bekommt deshalb kein Update angeboten. Versionen vor 1.0.1 kennen diesen
|
||||||
installiert nur, was zur Signatur im Plugin passt. Versionen vor 1.0.1 kennen diesen
|
|
||||||
Weg nicht: der Wechsel auf 1.0.1 ist einmalig von Hand (ZIP hochladen), danach läuft es über WordPress.
|
Weg nicht: der Wechsel auf 1.0.1 ist einmalig von Hand (ZIP hochladen), danach läuft es über WordPress.
|
||||||
|
|
||||||
== Divi-Freigabe ==
|
== Divi-Freigabe ==
|
||||||
@@ -211,10 +210,7 @@ LIT MCP schreibt nur mit Divi-Versionen, die für diese Plugin-Version geprüft
|
|||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
= 1.0.1 =
|
= 1.0.1 =
|
||||||
* Erstes vollständig ausgeliefertes Release: 1.0.0 wurde ohne Paketdatei veröffentlicht. Inhalt unverändert.
|
* Updates über WordPress: Das neueste Release von git.loosen-it.de erscheint als Plugin-Update (Update URI); automatisches Einspielen über den Schalter in der Plugin-Liste. Der Wechsel auf 1.0.1 ist einmalig von Hand. (Version 1.0.0 wurde nie ausgeliefert.)
|
||||||
|
|
||||||
= 1.0.0 =
|
|
||||||
* Updates über WordPress: Das neueste Release von git.loosen-it.de erscheint als Plugin-Update (Update URI); automatisches Einspielen über den Schalter in der Plugin-Liste. Der Wechsel auf 1.0.0 ist einmalig von Hand.
|
|
||||||
|
|
||||||
= 0.7.0 =
|
= 0.7.0 =
|
||||||
* Designsystem schreiben (Etappe 4): Presets lesen, anlegen, ändern, löschen, einzelnen Modulen und ganzen Seiten zuweisen und Verweise überall entfernen (Probelauf, Bestätigung, Snapshot, Wiederherstellen).
|
* Designsystem schreiben (Etappe 4): Presets lesen, anlegen, ändern, löschen, einzelnen Modulen und ganzen Seiten zuweisen und Verweise überall entfernen (Probelauf, Bestätigung, Snapshot, Wiederherstellen).
|
||||||
|
|||||||
+1
-2
@@ -38,7 +38,6 @@ use LIT\MCP\Server\RequestContext;
|
|||||||
use LIT\MCP\Server\ServerRegistrar;
|
use LIT\MCP\Server\ServerRegistrar;
|
||||||
use LIT\MCP\Server\VisibilityFilter;
|
use LIT\MCP\Server\VisibilityFilter;
|
||||||
use LIT\MCP\Updater\GiteaUpdater;
|
use LIT\MCP\Updater\GiteaUpdater;
|
||||||
use LIT\MCP\Updater\PackageSignature;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ final class Plugin
|
|||||||
self::$pluginFile = $pluginFile;
|
self::$pluginFile = $pluginFile;
|
||||||
|
|
||||||
// Before the dependency check: a plugin that is inactive for a missing dependency must still be updatable.
|
// Before the dependency check: a plugin that is inactive for a missing dependency must still be updatable.
|
||||||
(new GiteaUpdater(plugin_basename($pluginFile), dirname($pluginFile), new PackageSignature()))->register();
|
(new GiteaUpdater(plugin_basename($pluginFile), dirname($pluginFile)))->register();
|
||||||
|
|
||||||
$missing = (new Dependencies())->missing();
|
$missing = (new Dependencies())->missing();
|
||||||
if ($missing !== []) {
|
if ($missing !== []) {
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ defined('ABSPATH') || exit;
|
|||||||
* Gitea host, so WordPress asks the filter update_plugins_<host> (about every 12 hours, which is also the only
|
* Gitea host, so WordPress asks the filter update_plugins_<host> (about every 12 hours, which is also the only
|
||||||
* caching needed). The release carries the ZIP as asset wp-lit-mcp-<version>.zip.
|
* caching needed). The release carries the ZIP as asset wp-lit-mcp-<version>.zip.
|
||||||
*
|
*
|
||||||
* The ZIP is only installed with a valid signature (PackageSignature, asset <zip>.sig); a hijacked Gitea account
|
* shortcut: no signature – a package is trusted as far as the repository's release downloads and TLS are, so whoever
|
||||||
* alone cannot ship code.
|
* can publish a release there ships code to every site that updates. Public release assets only (WordPress downloads
|
||||||
*
|
* without credentials). Add a signature checked before unpacking (key outside Gitea) before the plugin runs on sites
|
||||||
* shortcut: public release assets only (WordPress downloads without credentials), a private repository would
|
* you do not control.
|
||||||
* need a token in the downloads.
|
|
||||||
*/
|
*/
|
||||||
final class GiteaUpdater
|
final class GiteaUpdater
|
||||||
{
|
{
|
||||||
@@ -29,55 +28,18 @@ final class GiteaUpdater
|
|||||||
private const SLUG = 'wp-lit-mcp';
|
private const SLUG = 'wp-lit-mcp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $pluginBasename plugin_basename() of the main file, the only plugin answered for.
|
* @param string $pluginBasename plugin_basename() of the main file, the only plugin answered for.
|
||||||
* @param string $pluginDir The plugin's folder; an update replaces it as a whole.
|
* @param string $pluginDir The plugin's folder; an update replaces it as a whole.
|
||||||
* @param PackageSignature $signature Checks the downloaded ZIP before WordPress unpacks it.
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $pluginBasename,
|
private string $pluginBasename,
|
||||||
private string $pluginDir,
|
private string $pluginDir
|
||||||
private PackageSignature $signature
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
add_filter('update_plugins_' . self::HOST, [$this, 'offer'], 10, 3);
|
add_filter('update_plugins_' . self::HOST, [$this, 'offer'], 10, 3);
|
||||||
add_filter('upgrader_pre_download', [$this, 'verifyDownload'], 10, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads our package itself to check its signature before WordPress unpacks a byte. Fails closed: a missing
|
|
||||||
* or wrong signature file stops the update.
|
|
||||||
*
|
|
||||||
* @param mixed $reply What an earlier filter decided; false = WordPress downloads as usual.
|
|
||||||
* @return mixed The downloaded file's path, a WP_Error, or $reply for any package that is not ours.
|
|
||||||
*/
|
|
||||||
public function verifyDownload(mixed $reply, string $package): mixed
|
|
||||||
{
|
|
||||||
if ($reply !== false || ! str_starts_with($package, self::RELEASE_DOWNLOADS)) {
|
|
||||||
return $reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
$file = download_url($package);
|
|
||||||
if (is_wp_error($file)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The signature is 89 bytes; never read more of an answer than that file can be.
|
|
||||||
$response = wp_safe_remote_get($package . '.sig', ['timeout' => 10, 'limit_response_size' => 1024]);
|
|
||||||
$signature = wp_remote_retrieve_body($response);
|
|
||||||
if (! $this->signature->verifies(basename($package), $file, $signature)) {
|
|
||||||
wp_delete_file($file);
|
|
||||||
Log::failure('update', 'signature of ' . basename($package) . ' is missing or does not verify');
|
|
||||||
|
|
||||||
return new \WP_Error(
|
|
||||||
'lit_mcp_bad_signature',
|
|
||||||
__('Die Signatur des Plugin-Pakets stimmt nicht. Das Update wurde nicht installiert.', 'wp-lit-mcp')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Updater;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the Ed25519 signature of a release ZIP. The private key never leaves the maintainer's machine, so a
|
|
||||||
* compromised Gitea account alone cannot ship code to the sites. bin/release-sign.php signs with the same message.
|
|
||||||
*/
|
|
||||||
final class PackageSignature
|
|
||||||
{
|
|
||||||
/** Base64 of the 32-byte public key; the matching private key signs the releases (bin/release-sign.php keygen). */
|
|
||||||
public const RELEASE_KEY = '8iPADRjYwxrnKK3bZ0nUnP2iXlIKpC1dJ0nZxCxOG2E=';
|
|
||||||
|
|
||||||
public function __construct(private string $publicKey = self::RELEASE_KEY)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signed text: asset name and SHA-256 of the file. The name binds the signature to one version – a validly
|
|
||||||
* signed older ZIP does not pass as a newer release.
|
|
||||||
*/
|
|
||||||
public static function message(string $assetName, string $zipPath): string
|
|
||||||
{
|
|
||||||
return $assetName . "\n" . (string) hash_file('sha256', $zipPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function verifies(string $assetName, string $zipPath, string $signatureBase64): bool
|
|
||||||
{
|
|
||||||
$signature = base64_decode(trim($signatureBase64), true);
|
|
||||||
$key = base64_decode($this->publicKey, true);
|
|
||||||
if (
|
|
||||||
$signature === false || strlen($signature) !== SODIUM_CRYPTO_SIGN_BYTES
|
|
||||||
|| $key === false || strlen($key) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES
|
|
||||||
|| ! is_readable($zipPath)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sodium_crypto_sign_verify_detached($signature, self::message($assetName, $zipPath), $key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ use Brain\Monkey\Filters;
|
|||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use LIT\MCP\Updater\GiteaUpdater;
|
use LIT\MCP\Updater\GiteaUpdater;
|
||||||
use LIT\MCP\Updater\PackageSignature;
|
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
|
|
||||||
@@ -20,40 +19,26 @@ final class GiteaUpdaterTest extends TestCase
|
|||||||
|
|
||||||
private string $logFile = '';
|
private string $logFile = '';
|
||||||
private string|false $previousLog = '';
|
private string|false $previousLog = '';
|
||||||
private string $secretKey = '';
|
|
||||||
private string $publicKey = '';
|
|
||||||
/** @var list<string> */
|
|
||||||
private array $tempFiles = [];
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
Functions\stubTranslationFunctions();
|
|
||||||
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
||||||
$this->logFile = $this->tempFile();
|
$this->logFile = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||||
$this->previousLog = ini_set('error_log', $this->logFile);
|
$this->previousLog = ini_set('error_log', $this->logFile);
|
||||||
|
|
||||||
$pair = sodium_crypto_sign_keypair();
|
|
||||||
$this->secretKey = sodium_crypto_sign_secretkey($pair);
|
|
||||||
$this->publicKey = base64_encode(sodium_crypto_sign_publickey($pair));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
ini_set('error_log', (string) $this->previousLog);
|
ini_set('error_log', (string) $this->previousLog);
|
||||||
foreach ($this->tempFiles as $file) {
|
unlink($this->logFile);
|
||||||
if (file_exists($file)) {
|
|
||||||
unlink($file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRegistersItsFilters(): void
|
public function testRegistersItselfForTheGiteaHost(): void
|
||||||
{
|
{
|
||||||
$updater = $this->updater();
|
$updater = $this->updater();
|
||||||
Filters\expectAdded('update_plugins_' . GiteaUpdater::HOST)->once()->with([$updater, 'offer'], 10, 3);
|
Filters\expectAdded('update_plugins_' . GiteaUpdater::HOST)->once()->with([$updater, 'offer'], 10, 3);
|
||||||
Filters\expectAdded('upgrader_pre_download')->once()->with([$updater, 'verifyDownload'], 10, 2);
|
|
||||||
|
|
||||||
$updater->register();
|
$updater->register();
|
||||||
}
|
}
|
||||||
@@ -84,8 +69,7 @@ final class GiteaUpdaterTest extends TestCase
|
|||||||
Functions\expect('wp_remote_get')->never();
|
Functions\expect('wp_remote_get')->never();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$updater = new GiteaUpdater(self::BASENAME, $dir, new PackageSignature($this->publicKey));
|
self::assertFalse((new GiteaUpdater(self::BASENAME, $dir))->offer(false, [], self::BASENAME));
|
||||||
self::assertFalse($updater->offer(false, [], self::BASENAME));
|
|
||||||
} finally {
|
} finally {
|
||||||
rmdir($dir . '/.git');
|
rmdir($dir . '/.git');
|
||||||
rmdir($dir);
|
rmdir($dir);
|
||||||
@@ -136,81 +120,10 @@ final class GiteaUpdaterTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDownloadWithAValidSignatureIsHandedToWordPress(): void
|
|
||||||
{
|
|
||||||
$zip = $this->tempFile('release bytes');
|
|
||||||
$this->givenDownload($zip, $this->sign(self::ZIP_NAME, $zip));
|
|
||||||
|
|
||||||
self::assertSame($zip, $this->updater()->verifyDownload(false, self::ZIP));
|
|
||||||
self::assertFileExists($zip);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDownloadWithAForeignSignatureIsDeletedAndRefused(): void
|
|
||||||
{
|
|
||||||
$zip = $this->tempFile('release bytes');
|
|
||||||
$other = $this->tempFile('other bytes');
|
|
||||||
$this->givenDownload($zip, $this->sign(self::ZIP_NAME, $other));
|
|
||||||
|
|
||||||
$result = $this->updater()->verifyDownload(false, self::ZIP);
|
|
||||||
|
|
||||||
self::assertInstanceOf(WP_Error::class, $result);
|
|
||||||
self::assertFileDoesNotExist($zip);
|
|
||||||
self::assertStringContainsString('update failed: signature of ' . self::ZIP_NAME, $this->log());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSignatureOfAnotherVersionDoesNotPassAsThisOne(): void
|
|
||||||
{
|
|
||||||
$zip = $this->tempFile('old release bytes');
|
|
||||||
$this->givenDownload($zip, $this->sign('wp-lit-mcp-0.9.0.zip', $zip));
|
|
||||||
|
|
||||||
self::assertInstanceOf(WP_Error::class, $this->updater()->verifyDownload(false, self::ZIP));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDownloadWithoutSignatureFileIsRefused(): void
|
|
||||||
{
|
|
||||||
$zip = $this->tempFile('release bytes');
|
|
||||||
$this->givenDownload($zip, '');
|
|
||||||
|
|
||||||
self::assertInstanceOf(WP_Error::class, $this->updater()->verifyDownload(false, self::ZIP));
|
|
||||||
self::assertFileDoesNotExist($zip);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFailedDownloadIsReturnedAsIs(): void
|
|
||||||
{
|
|
||||||
$error = new WP_Error('download_failed', 'cURL error 28');
|
|
||||||
Functions\expect('download_url')->once()->with(self::ZIP)->andReturn($error);
|
|
||||||
|
|
||||||
self::assertSame($error, $this->updater()->verifyDownload(false, self::ZIP));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testOtherPackagesAndEarlierDecisionsAreNotTouched(): void
|
|
||||||
{
|
|
||||||
Functions\expect('download_url')->never();
|
|
||||||
|
|
||||||
self::assertFalse($this->updater()->verifyDownload(false, 'https://downloads.wordpress.org/plugin/x.zip'));
|
|
||||||
self::assertSame('/tmp/earlier.zip', $this->updater()->verifyDownload('/tmp/earlier.zip', self::ZIP));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updater(): GiteaUpdater
|
private function updater(): GiteaUpdater
|
||||||
{
|
{
|
||||||
// __DIR__ has no .git folder.
|
// __DIR__ has no .git folder.
|
||||||
return new GiteaUpdater(self::BASENAME, __DIR__, new PackageSignature($this->publicKey));
|
return new GiteaUpdater(self::BASENAME, __DIR__);
|
||||||
}
|
|
||||||
|
|
||||||
private function tempFile(string $contents = ''): string
|
|
||||||
{
|
|
||||||
$file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-test');
|
|
||||||
file_put_contents($file, $contents);
|
|
||||||
$this->tempFiles[] = $file;
|
|
||||||
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sign(string $assetName, string $zipPath): string
|
|
||||||
{
|
|
||||||
$message = PackageSignature::message($assetName, $zipPath);
|
|
||||||
|
|
||||||
return base64_encode(sodium_crypto_sign_detached($message, $this->secretKey));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function log(): string
|
private function log(): string
|
||||||
@@ -218,18 +131,6 @@ final class GiteaUpdaterTest extends TestCase
|
|||||||
return (string) file_get_contents($this->logFile);
|
return (string) file_get_contents($this->logFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function givenDownload(string $zip, string $signatureBody): void
|
|
||||||
{
|
|
||||||
Functions\expect('download_url')->once()->with(self::ZIP)->andReturn($zip);
|
|
||||||
Functions\expect('wp_safe_remote_get')->once()
|
|
||||||
->with(self::ZIP . '.sig', Mockery::type('array'))
|
|
||||||
->andReturn(['body' => $signatureBody]);
|
|
||||||
Functions\when('wp_remote_retrieve_body')->justReturn($signatureBody);
|
|
||||||
Functions\when('wp_delete_file')->alias(static function (string $file): void {
|
|
||||||
unlink($file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function givenRelease(int $status, string $body): void
|
private function givenRelease(int $status, string $body): void
|
||||||
{
|
{
|
||||||
$latestRelease = Mockery::pattern('#^https://git\.loosen-it\.de/api/v1/repos/.+/releases/latest$#');
|
$latestRelease = Mockery::pattern('#^https://git\.loosen-it\.de/api/v1/repos/.+/releases/latest$#');
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Updater;
|
|
||||||
|
|
||||||
use LIT\MCP\Updater\PackageSignature;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
final class PackageSignatureTest extends TestCase
|
|
||||||
{
|
|
||||||
private string $zip = '';
|
|
||||||
private string $secretKey = '';
|
|
||||||
private PackageSignature $signature;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
$pair = sodium_crypto_sign_keypair();
|
|
||||||
$this->secretKey = sodium_crypto_sign_secretkey($pair);
|
|
||||||
$this->signature = new PackageSignature(base64_encode(sodium_crypto_sign_publickey($pair)));
|
|
||||||
$this->zip = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-sig');
|
|
||||||
file_put_contents($this->zip, 'zip bytes');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
unlink($this->zip);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testASignatureOverNameAndContentVerifies(): void
|
|
||||||
{
|
|
||||||
$signature = $this->sign('wp-lit-mcp-1.0.0.zip');
|
|
||||||
|
|
||||||
self::assertTrue($this->signature->verifies('wp-lit-mcp-1.0.0.zip', $this->zip, $signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAnotherAssetNameDoesNotVerify(): void
|
|
||||||
{
|
|
||||||
$signature = $this->sign('wp-lit-mcp-1.0.0.zip');
|
|
||||||
|
|
||||||
self::assertFalse($this->signature->verifies('wp-lit-mcp-1.0.1.zip', $this->zip, $signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testChangedContentDoesNotVerify(): void
|
|
||||||
{
|
|
||||||
$signature = $this->sign('x.zip');
|
|
||||||
file_put_contents($this->zip, 'changed bytes');
|
|
||||||
|
|
||||||
self::assertFalse($this->signature->verifies('x.zip', $this->zip, $signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAnotherKeyDoesNotVerify(): void
|
|
||||||
{
|
|
||||||
$foreign = new PackageSignature(base64_encode(sodium_crypto_sign_publickey(sodium_crypto_sign_keypair())));
|
|
||||||
|
|
||||||
self::assertFalse($foreign->verifies('x.zip', $this->zip, $this->sign('x.zip')));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider malformedInput */
|
|
||||||
public function testMalformedInputFailsClosed(string $signature, string $publicKey, string $zipPath): void
|
|
||||||
{
|
|
||||||
$subject = new PackageSignature($publicKey === 'real' ? $this->realKey() : $publicKey);
|
|
||||||
|
|
||||||
self::assertFalse($subject->verifies('x.zip', $zipPath === 'zip' ? $this->zip : $zipPath, $signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, array{string, string, string}> */
|
|
||||||
public static function malformedInput(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'signature is no base64' => ['not base64 !!', 'real', 'zip'],
|
|
||||||
'signature is empty' => ['', 'real', 'zip'],
|
|
||||||
'signature has the wrong length' => [base64_encode('short'), 'real', 'zip'],
|
|
||||||
'no key configured' => ['AAAA', '', 'zip'],
|
|
||||||
'key has the wrong length' => ['AAAA', base64_encode('short'), 'zip'],
|
|
||||||
'file is missing' => ['AAAA', 'real', '/nonexistent/x.zip'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sign(string $assetName): string
|
|
||||||
{
|
|
||||||
$message = PackageSignature::message($assetName, $this->zip);
|
|
||||||
|
|
||||||
return base64_encode(sodium_crypto_sign_detached($message, $this->secretKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function realKey(): string
|
|
||||||
{
|
|
||||||
return base64_encode(sodium_crypto_sign_publickey(sodium_crypto_sign_keypair()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user