From c587799394963ccded231fed5717478a3ba9a5e8 Mon Sep 17 00:00:00 2001 From: Strobel Pierre Date: Wed, 13 May 2026 15:23:34 +0200 Subject: [PATCH] fix: sign app via occ integrity:sign-app instead of PHP openssl_sign PHP's openssl_sign() only emits PKCS#1 v1.5 signatures, but Nextcloud's integrity check requires RSA-PSS / SHA-512 / MGF1-SHA-512 / saltLen=0. The previous CI step therefore shipped every release with a signature that fails verification, causing a silent "Signature could not get verified" warning in the admin overview for any installed app. Switch to the canonical signing path: spin up nextcloud:32-apache in a container, run `occ integrity:sign-app` with the project key and cert, and pull the resulting signature.json back into the tarball before repackaging. Add a separate verify step that installs the freshly signed tarball into a clean Nextcloud and runs `occ integrity:check-app`. Any non-empty output fails the build, so we never silently ship a bad signature again. --- .github/workflows/appstore-build-publish.yml | 103 ++++++++++++------- 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/.github/workflows/appstore-build-publish.yml b/.github/workflows/appstore-build-publish.yml index 7db016e..d1776ee 100644 --- a/.github/workflows/appstore-build-publish.yml +++ b/.github/workflows/appstore-build-publish.yml @@ -55,53 +55,86 @@ jobs: - name: Build tarball run: make appstore - - name: Sign app + - name: Sign app (via occ integrity:sign-app) + # PHP's openssl_sign() only supports PKCS#1 v1.5 padding, but Nextcloud's + # integrity check requires RSA-PSS / SHA-512 / MGF1-SHA-512 / saltLen=0. + # We therefore drive `occ integrity:sign-app` on a real Nextcloud instance, + # which is the canonical signing path documented by Nextcloud. run: | - # Write key and fetch certificate echo "${{ secrets.APP_PRIVATE_KEY }}" > /tmp/app.key curl -sL "https://github.com/nextcloud/app-certificate-requests/raw/master/${APP_NAME}/${APP_NAME}.crt" \ -o /tmp/app.crt - # Extract, sign, repackage + docker run -d --name nc-signer \ + -e SQLITE_DATABASE=db \ + -e NEXTCLOUD_ADMIN_USER=a \ + -e NEXTCLOUD_ADMIN_PASSWORD=a \ + nextcloud:32-apache + + for i in $(seq 1 60); do + if docker exec -u www-data nc-signer php occ status 2>/dev/null | grep -q "installed: true"; then + break + fi + sleep 3 + done + cd build/artifacts tar xzf ${APP_NAME}.tar.gz - cd ${APP_NAME} - - # Generate signature.json using PHP + openssl - php << 'PHPSIGN' - isFile()) { - $relPath = ltrim(str_replace($appPath, '', $file->getPathname()), '/'); - if ($relPath === 'appinfo/signature.json') continue; - $hashes[$relPath] = ['sha512' => hash_file('sha512', $file->getPathname())]; - } - } - ksort($hashes); - $privateKey = file_get_contents('/tmp/app.key'); - openssl_sign(json_encode($hashes), $signature, $privateKey, OPENSSL_ALGO_SHA512); - $result = [ - 'hashes' => $hashes, - 'signature' => base64_encode($signature), - 'certificate' => file_get_contents('/tmp/app.crt'), - ]; - file_put_contents('appinfo/signature.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - echo "Signed " . count($hashes) . " files\n"; - PHPSIGN - - cd .. + + docker cp ${APP_NAME} nc-signer:/var/www/html/custom_apps/ + docker cp /tmp/app.key nc-signer:/tmp/app.key + docker cp /tmp/app.crt nc-signer:/tmp/app.crt + docker exec nc-signer chown -R www-data:www-data \ + /var/www/html/custom_apps/${APP_NAME} /tmp/app.key /tmp/app.crt + + docker exec -u www-data nc-signer php occ integrity:sign-app \ + --path=/var/www/html/custom_apps/${APP_NAME} \ + --privateKey=/tmp/app.key \ + --certificate=/tmp/app.crt + + docker cp nc-signer:/var/www/html/custom_apps/${APP_NAME}/appinfo/signature.json \ + ${APP_NAME}/appinfo/signature.json + tar czf ${APP_NAME}.tar.gz ${APP_NAME} rm -rf ${APP_NAME} - # Generate detached signature of the tarball for App Store API + # Detached tarball signature for the App Store REST API (separate + # mechanism, PKCS#1 here is what the API expects). openssl dgst -sha512 -sign /tmp/app.key ${APP_NAME}.tar.gz | openssl base64 -A > /tmp/tarball.sig + - name: Verify signed app passes integrity check + # Guard against the silent failure mode that shipped v1.0.0 and v1.1.0 with + # invalid signatures: spin up a fresh NC and confirm integrity:check-app + # returns no errors before publishing anything. + run: | + docker run -d --name nc-verify \ + -e SQLITE_DATABASE=db \ + -e NEXTCLOUD_ADMIN_USER=a \ + -e NEXTCLOUD_ADMIN_PASSWORD=a \ + nextcloud:32-apache + + for i in $(seq 1 60); do + if docker exec -u www-data nc-verify php occ status 2>/dev/null | grep -q "installed: true"; then + break + fi + sleep 3 + done + + cd build/artifacts + mkdir -p verify && tar xzf ${APP_NAME}.tar.gz -C verify + + docker cp verify/${APP_NAME} nc-verify:/var/www/html/custom_apps/ + docker exec nc-verify chown -R www-data:www-data /var/www/html/custom_apps/${APP_NAME} + + OUTPUT=$(docker exec -u www-data nc-verify php occ integrity:check-app ${APP_NAME} 2>&1) + rm -rf verify + if [ -n "$OUTPUT" ]; then + echo "Integrity check failed on the signed tarball:" + echo "$OUTPUT" + exit 1 + fi + echo "Integrity check passed." + - name: Attach signed tarball to release uses: softprops/action-gh-release@v2 with: