Repository navigation
Integrity signature
An ncmake app carries two signatures, and they answer different questions. The one from the App Store module (make sign) proves to the store where the tarball came from. The one on this page, appinfo/signature.json, is what a Nextcloud server reads: it lists a hash per shipped file, so the server can tell an installed app apart from a modified one. Without the file an app is not integrity-checked at all, and silently so.
The targets for it come from the integrity developer module.
Tip
TL;DR — make dev-init once per machine, make integrity-enable once per app (it puts the public certificate in the repository — commit it), and from then on make tag offers to produce the signature and the release workflow refuses to attach a tarball without it. The signing key never leaves your machine and never becomes a CI secret.
- The developer module
- What signature.json is
- Marking an app as signed
- The signature is made at tag time
- The runner verifies, then packs
- The gist is dropped after publishing
- Signing by hand
- Target reference
- FAQ
Like the App Store and workflow targets, these are not part of the core Makefile. Someone who clones your app to build and install it never needs them, so they stay out of their make help. Fetch the modules once per machine with make dev-init (details on App Store); make dev-clean removes them again.
The module needs nothing beyond what ncmake already requires: openssl and python3. No Nextcloud installation, which is the reason it exists — the documented route to this file is occ integrity:sign-app, and that needs a server with the app already in place.
The format is fully determined by the server's own lib/private/IntegrityCheck/Checker.php, so ncmake writes the file directly instead of reaching for occ:
- a sha512 per file, keyed by the path relative to the app root
- what the signature covers is PHP's
json_encodeover that map afterksort— the verifier re-sorts and re-encodes, so the order in the file is free but the escaping is not -
RSA-PSS with digest sha1, MGF1 sha512 and salt length 0. At salt length 0 the scheme is deterministic, so
opensslandoccproduce identical bytes for the same file set - the document holds
hashes,signature(base64) andcertificate(the PEM), in that order, pretty-printed with four spaces
Excluded from the hashing are appinfo/signature.json itself and the names the server's own ExcludeFileByNameFilterIterator drops — .DS_Store, .directory, .rnd, .webapp, Thumbs.db, nextcloud-init-sync.lock and the .webapp-nextcloud-* pattern — the files a desktop or an admin can leave in an app directory without the app knowing about it.
What gets hashed is the staged tree, never the checkout: make stage has already applied the keep model and .nextcloudignore, so that file set is the shipped file set, and no build artefact the checkout happens to carry can slip in. See Building and packaging for the keep model.
Note
Nothing anywhere records that an app once signed has to stay signed. Measured against a running server, occ integrity:check-app answers App signature not found, skipping app integrity check for an app without the file, and the instance-wide verification skips it just as quietly. Dropping the signature therefore costs you the integrity check and nothing else — which is also why ncmake keeps its own record of whether your app is supposed to have one.
make integrity-enablecopies your App Store certificate to .ncmake/certificate.crt and leaves it for you to commit. That committed file is the whole switch, and it does two things:
- it marks the app as signed —
make tagoffers the signature from then on, and the release workflow fails a release that carries none - it pins the certificate every verification is held against, so a
signature.jsonmade with some other key is caught asCERTIFICATE_MISMATCH
It is the public certificate; there is no key material in it. It lies outside the keep model, so it is not part of the tarball — it is ncmake's record, not something the server or the store ever sees.
To stop signing, remove the file from the repository. The next release then ships unsigned.
flowchart LR
A["make tag<br>clean tree, confirm"] -- "Generate signature? [Y/n]" --> B["stage + sign<br>(the key stays here)"]
B --> C["secret gist"]
C -- "ncmake-signature: <id><br>inside the signed tag message" --> D["tag + push"]
D --> E["GitHub release"]
E --> F["runner: fetch, verify, pack, attach"]
F --> G["make publish<br>+ drop the gist"]
make tag (Releasing) gained two things for this. It refuses a working tree that is not clean, untracked files included, because stage copies from the working tree while the runner copies from the checkout — anything only your machine has would be hashed here and be missing there. And after your confirmation, before the tag exists, it runs the signature step:
Generate appinfo/signature.json for this release? [Y/n]
Answer y and ncmake stages, signs, holds the result against the committed certificate, uploads the file as a secret gist and writes one line into the tag message:
ncmake-signature: <gist id>
The tag is signed with your GPG key, so that line is covered by the tag's own signature. Answer n and the release ships unsigned, with no further question.
The whole point of the detour through a gist is that the signing key stays where it belongs. A secret gist is readable by anyone holding its id and findable by nobody, so the runner fetches the file without any token — nothing about signing is added to the workflow's permissions, and your key never becomes a repository secret. The documented recipe for this file in the Nextcloud release automation guide does put the key into CI; ncmake deliberately does not.
The release workflow (Workflows) reads the id out of the tag message and then:
-
no id while
.ncmake/certificate.crtis committed → the job fails. An unsigned tarball for an app that is supposed to be signed is never attached - otherwise it fetches the gist into
appinfo/signature.jsonand runsmake integrity-checkbeforemake dist, so nothing is packed that did not verify - it also prints the age of the tag into the job summary
integrity-check recomputes the hashes over the staged tree, holds them against the file, and verifies the signature against the pinned certificate:
| Reported as | Means |
|---|---|
EXTRA_FILE |
the tree has a file the signature does not cover |
FILE_MISSING |
the signature covers a file the tree does not have |
INVALID_HASH |
the file is there and its content differs |
CERTIFICATE_CN |
the certificate's CN is not the app id — the server rejects that pairing |
CERTIFICATE_MISMATCH |
signed with a certificate other than the committed one |
INVALID_SIGNATURE |
the signature does not match the hashes in the file |
The first three are the classes Checker::verify() itself reports. Any of them exits non-zero.
This makes build reproducibility a release gate: the hashes were taken over a tree built on your machine, so a runner build that does not reproduce it byte for byte fails here instead of shipping. Which is exactly the question make build-verify (Checkout audit) asks ahead of time, and why the tag's age is worth printing — lockfiles, .nvmrc and pinned images keep a build reproducible, and a tag that sat around for months is the plainest sign that it may no longer be.
What integrity-check does not check is the certificate chain. That the certificate was issued by Nextcloud is verified by the server against its own resources/codesigning/root.crt, and that root is not ncmake's to carry.
make publish (App Store) deletes the gist at the end — and only after the store has answered 200 or 201:
- not in the workflow, because
GITHUB_TOKENis scoped to the repository and can neither read nor delete gists (which is the same reason the runner can fetch the file without it) - not earlier, because a release job that failed for an unrelated reason has to stay re-runnable, and a re-run needs the file to still be there
make integrity-gist-drop is that step on its own. Called by hand it is harmless: without an id in the tag message of the current version it does nothing.
make dist-signedstages, signs into the staged tree, runs the same integrity-check against what was just written, and packs. The result is the tarball the runner would have produced, for you to attach by hand — the way out when the runner build cannot be repaired quickly. The three steps are ordered by their position in the prerequisite list, so run it without -j, like make release.
The two halves are also targets of their own. make integrity-sign writes the file into the staged tree; DIR=<path> signs a tree that already exists instead and skips staging — it refuses the checkout itself. make integrity-check takes the same DIR=, or TARBALL=<file> to check a packed release from an extraction under build/cache/.
Signing an app that is already deployed is the quickest way to see the server agree:
make integrity-sign DIR=/var/www/nextcloud/apps/<app_id>
chown www-data: /var/www/nextcloud/apps/<app_id>/appinfo/signature.json
occ integrity:check-app -vvv -- <app_id>occ then does the full verification — hashes, the chain against its own root certificate, the CN, the PSS signature. Note that the next make rsync removes the file again: it is not part of the deploy, it is part of the release.
| Target | What it does | |
|---|---|---|
make integrity‑enable |
[m] |
copies the certificate to .ncmake/certificate.crt for you to commit — marks the app as signed |
make integrity‑sign |
[m] |
writes appinfo/signature.json into the staged tree; DIR=<path> signs an existing tree |
make integrity‑check |
holds a tree against its signature.json; DIR=<path> or TARBALL=<file>; needs no key, runs in CI |
|
make dist‑signed |
[m] |
stage + sign + check + tarball, in that order |
make integrity‑tag |
[m] |
the step make tag runs; not called by hand |
make integrity‑gist‑drop |
[m] |
the step make publish runs; not called by hand |
make help shows whether certificate, key and .ncmake/certificate.crt are in place, and make help-<target> explains any of them in depth.
Do I have to sign my app? No. Signing is opt-in per app, and an unsigned app installs and runs exactly as before — it is simply not integrity-checked.
Does the App Store need this file? No. The store verifies your signature over the tarball and knows nothing about signature.json; the server is the only consumer. The two do share the certificate, which is deliberate: one app, one certificate, and the server finds inside signature.json the very certificate the store verified the tarball against.
Why not just sign in CI? Because that needs the key as a repository secret, and a secret a workflow can read is one merged pull request away from being read by someone else. The signature travels instead of the key.
The runner reports INVALID_HASH for files I did not touch. Then the runner's build output differs from the one the signature was taken over — the usual suspects are vendor/, bundle metadata and anything not pinned by a lockfile or .nvmrc. make build-verify locates it; make dist-signed gets the release out in the meantime.
Can I check an app that is already installed? Yes: make integrity-check DIR=/var/www/nextcloud/apps/<app_id>, which is the same verification the server does minus the certificate chain.
Is the secret gist a hole? Its content is a list of hashes and a public certificate — nothing that is not in the tarball a minute later. It exists to move that file to the runner without giving the runner a key, and it is deleted once the release is published.
© 2026 [ernolf] Raphael Gradenwitz · MIT-licensed · Report an issue
For app developers
- Getting started
- How ncmake understands your app
- Building and packaging
- Bundle analysis
- Build tools
- Checkout audit
- Releasing
- Integrity signature
- Per-app tuning
- Target reference
- AI assistant
CI workflows
App Store
For app users
Git
Background
Reference