@@ -39,6 +39,9 @@ The signing phase adds these files to the same directory:
3939- ` SHA256SUMS.asc `
4040- ` phpMyFAQ-<version>.zip.asc `
4141- ` phpMyFAQ-<version>.tar.gz.asc `
42+ - ` phpMyFAQ-<version>.php.sbom.json.asc `
43+ - ` phpMyFAQ-<version>.js.sbom.json.asc `
44+ - ` phpMyFAQ-<version>.sbom.json.asc `
4245
4346## 13.4 Build helper
4447
@@ -54,9 +57,17 @@ This creates:
5457build/release/4.2.0/phpMyFAQ-4.2.0.zip
5558build/release/4.2.0/phpMyFAQ-4.2.0.tar.gz
5659build/release/4.2.0/hashes-4.2.0.json
60+ build/release/4.2.0/phpMyFAQ-4.2.0.php.sbom.json
61+ build/release/4.2.0/phpMyFAQ-4.2.0.js.sbom.json
62+ build/release/4.2.0/phpMyFAQ-4.2.0.sbom.json
5763build/release/4.2.0/ARTIFACTS.txt
5864```
5965
66+ The release helper invokes ` scripts/generate-sbom.sh ` after packaging, so
67+ CycloneDX Software Bill of Materials files are emitted alongside the archives
68+ without an extra manual step. See section 13.13 for the standalone usage of
69+ the SBOM helper.
70+
6071## 13.5 Signing command
6172
6273To generate checksums and signatures:
@@ -71,6 +82,12 @@ The signing helper creates:
7182- ` SHA256SUMS.asc `
7283- ` phpMyFAQ-<version>.zip.asc `
7384- ` phpMyFAQ-<version>.tar.gz.asc `
85+ - ` phpMyFAQ-<version>.php.sbom.json.asc `
86+ - ` phpMyFAQ-<version>.js.sbom.json.asc `
87+ - ` phpMyFAQ-<version>.sbom.json.asc `
88+
89+ The ` SHA256SUMS ` manifest covers both archives and all three SBOM files, and
90+ detached signatures are produced for each of them.
7491
7592The helper also verifies the generated checksums and signatures before it exits.
7693
@@ -105,21 +122,37 @@ It does not create detached signatures.
105122
106123## 13.8 Public key location
107124
108- The public release-signing key should be published at:
125+ The public release-signing key is published at:
109126
110127``` text
111128docs/keys/phpmyfaq-release-public-key.asc
112129```
113130
114- The repository currently reserves that location but does not ship a fake placeholder key.
115- Add the real armored public key there once the dedicated release-signing key is created.
131+ The file is an ASCII-armored OpenPGP public key block
132+ (` -----BEGIN PGP PUBLIC KEY BLOCK----- ` ). Only the public key is committed to
133+ the repository; the primary private key and its revocation certificate are
134+ kept offline, and the signing subkey lives on the dedicated release signing
135+ machine (or a hardware token).
136+
137+ To export the public key from the release keyring, run:
138+
139+ ``` bash
140+ gpg --armor --export < FINGERPRINT> \
141+ > docs/keys/phpmyfaq-release-public-key.asc
142+ ```
143+
144+ Replace ` <FINGERPRINT> ` with the full 40-character fingerprint from
145+ section 13.10. Never export the file with ` --export-secret-keys ` .
116146
117147## 13.9 Verification
118148
119149After downloading a release, users should have these files:
120150
121151- ` phpMyFAQ-<version>.zip `
122152- ` phpMyFAQ-<version>.tar.gz `
153+ - ` phpMyFAQ-<version>.php.sbom.json `
154+ - ` phpMyFAQ-<version>.js.sbom.json `
155+ - ` phpMyFAQ-<version>.sbom.json `
123156- ` SHA256SUMS `
124157- ` SHA256SUMS.asc `
125158- ` phpmyfaq-release-public-key.asc `
@@ -147,37 +180,265 @@ Optional detached signature verification:
147180``` bash
148181gpg --verify phpMyFAQ-< version> .zip.asc phpMyFAQ-< version> .zip
149182gpg --verify phpMyFAQ-< version> .tar.gz.asc phpMyFAQ-< version> .tar.gz
183+ gpg --verify phpMyFAQ-< version> .php.sbom.json.asc phpMyFAQ-< version> .php.sbom.json
184+ gpg --verify phpMyFAQ-< version> .js.sbom.json.asc phpMyFAQ-< version> .js.sbom.json
185+ gpg --verify phpMyFAQ-< version> .sbom.json.asc phpMyFAQ-< version> .sbom.json
150186```
151187
152188## 13.10 Fingerprint publication
153189
154- When the real release key is available, publish the full fingerprint in:
190+ Release key fingerprint:
191+
192+ ``` text
193+ TODO FILL IN TODO FILL IN TODO FILL IN TODO FILL IN TODO FILL IN
194+ ```
195+
196+ <!--
197+ Replace the placeholder line above with the full 40-character fingerprint
198+ exactly as produced by:
199+
200+ gpg --fingerprint release@phpmyfaq.de
201+
202+ Keep the grouping used by GnuPG (ten groups of four hex characters,
203+ separated by double spaces between the fifth and sixth group).
204+ -->
205+
206+ Key metadata:
207+
208+ - UID: ` phpMyFAQ Release Signing Key <release@phpmyfaq.de> `
209+ - Long key ID: ` TODO FILL IN ` (16 hex characters)
210+ - Created: ` TODO FILL IN `
211+ - Expires: ` TODO FILL IN `
212+
213+ Publish this exact fingerprint in every one of the following locations:
155214
156215- this document
157- - the GitHub release notes
216+ - the GitHub release notes for each tagged release
158217- the project website release page
218+ - any third-party mirror that ships the phpMyFAQ archives
159219
160- Use one exact fingerprint value everywhere. Do not publish shortened or inconsistent variants.
220+ Use one exact fingerprint value everywhere. Do not publish shortened or
221+ inconsistent variants. If the key is ever rotated or revoked, update this
222+ section and announce the change in the release notes of the first release
223+ signed with the new key.
161224
162225## 13.11 Release publication checklist
163226
164227Publish these files with each release:
165228
166229- ` phpMyFAQ-<version>.zip `
167230- ` phpMyFAQ-<version>.tar.gz `
231+ - ` phpMyFAQ-<version>.php.sbom.json `
232+ - ` phpMyFAQ-<version>.js.sbom.json `
233+ - ` phpMyFAQ-<version>.sbom.json `
168234- ` SHA256SUMS `
169235- ` SHA256SUMS.asc `
170- - optional detached signatures for each archive
236+ - optional detached signatures for each archive and each SBOM file
171237
172238The release notes should also include:
173239
174240- the release key fingerprint
175241- a link to the verification instructions
176242- a link to the public key location
177243
178- ## 13.12 Notes
244+ ## 13.13 Software Bill of Materials (SBOM)
245+
246+ Each release ships a [ CycloneDX] ( https://cyclonedx.org/ ) 1.5 Software Bill of
247+ Materials that enumerates every pinned dependency of the packaged code. Three
248+ files are produced per release:
249+
250+ - ` phpMyFAQ-<version>.php.sbom.json ` — Composer (PHP) dependency graph only.
251+ - ` phpMyFAQ-<version>.js.sbom.json ` — pnpm (JavaScript/TypeScript) dependency graph only.
252+ - ` phpMyFAQ-<version>.sbom.json ` — combined PHP + JavaScript/TypeScript graph.
253+
254+ The SBOMs are included in ` SHA256SUMS ` and each one gets its own detached GPG
255+ signature during the signing phase, so downstream consumers can verify the
256+ provenance of the bill of materials using the same release key that signs the
257+ archives.
258+
259+ ### 13.13.1 Automatic generation
260+
261+ ` scripts/prepare-release-artifacts.sh ` calls the SBOM helper automatically, so
262+ the release directory already contains the three JSON files before signing.
263+ Nothing extra is required for a standard release.
264+
265+ ### 13.13.2 Standalone generation
266+
267+ To generate the SBOM files without running a full release build, use the
268+ helper directly:
269+
270+ ``` bash
271+ ./scripts/generate-sbom.sh
272+ ```
273+
274+ Both arguments are optional:
275+
276+ ``` bash
277+ ./scripts/generate-sbom.sh [source-dir] [output-dir]
278+ ```
279+
280+ - ` source-dir ` defaults to the repository root.
281+ - ` output-dir ` defaults to ` build/release/<version>/ ` , where ` <version> ` is
282+ resolved from ` scripts/get-version.php ` .
283+
284+ Environment variables:
285+
286+ - ` VERSION ` — override the release version used in filenames and the CycloneDX
287+ metadata.
288+ - ` CDXGEN_VERSION ` — pin a specific ` @cyclonedx/cdxgen ` version (defaults to
289+ ` latest ` ).
290+ - ` CDXGEN_SPEC_VERSION ` — override the CycloneDX spec version (defaults to
291+ ` 1.5 ` ).
292+ - ` PHP_BIN ` — override the PHP binary used to resolve the release version.
293+
294+ ### 13.13.3 Requirements
295+
296+ The helper needs:
297+
298+ - ` pnpm ` on the ` PATH ` (used via ` pnpm dlx ` to run ` @cyclonedx/cdxgen ` ).
299+ - A valid ` composer.lock ` in the source directory.
300+ - A valid ` pnpm-lock.yaml ` in the source directory.
301+ - ` php ` (or ` PHP_BIN ` ) to resolve the release version when ` VERSION ` is not set.
302+
303+ ## 13.14 Notes
179304
180305- The package payload is the ` phpmyfaq/ ` directory prepared from a clean git checkout.
181306- The helper installs production dependencies and runs the frontend production build before packaging.
182307- TCPDF fonts and examples are removed from the packaged checkout, matching the existing release process.
183308- Use ` ./scripts/sign-release-artifacts.sh ` to generate checksums and signatures.
309+ - Use ` ./scripts/generate-sbom.sh ` to regenerate the CycloneDX SBOM files outside of a full release build.
310+
311+ ## 13.15 Key rotation and revocation
312+
313+ The release key is long-lived but not permanent. Plan for three scenarios:
314+ routine expiry extension, planned rotation, and emergency revocation after a
315+ compromise.
316+
317+ ### 13.15.1 Extending the expiration date
318+
319+ When the key is approaching its expiry but is otherwise healthy, extend the
320+ validity instead of generating a new key. This keeps the fingerprint stable
321+ and avoids re-establishing trust.
322+
323+ ``` bash
324+ gpg --edit-key < FINGERPRINT>
325+ gpg> expire # extend the primary key
326+ gpg> key 1 # select the signing subkey
327+ gpg> expire # extend the subkey
328+ gpg> save
329+ ```
330+
331+ After extending, re-export the public key and update the repository:
332+
333+ ``` bash
334+ gpg --armor --export < FINGERPRINT> \
335+ > docs/keys/phpmyfaq-release-public-key.asc
336+ ```
337+
338+ Also push the updated key to any keyserver the project uses:
339+
340+ ``` bash
341+ gpg --keyserver keys.openpgp.org --send-keys < FINGERPRINT>
342+ ```
343+
344+ Commit the refreshed ` docs/keys/phpmyfaq-release-public-key.asc ` and update
345+ the ` Expires: ` line in section 13.10.
346+
347+ ### 13.15.2 Rotating the signing subkey only
348+
349+ Rotating the ** signing subkey** while keeping the ** primary key** preserves
350+ the fingerprint in section 13.10 and keeps all historic signatures valid.
351+ This is the preferred rotation path for routine key hygiene.
352+
353+ ``` bash
354+ gpg --edit-key < FINGERPRINT>
355+ gpg> addkey # follow the prompts: sign-only, Ed25519 or RSA 4096
356+ gpg> save
357+ ```
358+
359+ Then:
360+
361+ 1 . Re-export the public key to ` docs/keys/phpmyfaq-release-public-key.asc `
362+ so downstream users receive the new subkey.
363+ 2 . Revoke the previous signing subkey once the new one is in place:
364+ ` gpg --edit-key <FINGERPRINT> ` → ` key <N> ` → ` revkey ` → ` save ` .
365+ 3 . Re-export the public key again so the revocation is published.
366+ 4 . Update the signing machine / hardware token with the new subkey only.
367+ 5 . Note the rotation in the release notes of the first release signed with
368+ the new subkey.
369+
370+ ### 13.15.3 Full key rotation
371+
372+ If the primary key must change (for example, the algorithm needs upgrading
373+ or the old key is considered too weak), generate a new key following the
374+ steps in the "how to add a new release signing key" flow and then:
375+
376+ 1 . Sign the new key with the old key to create an explicit bridge of trust:
377+
378+ ``` bash
379+ gpg --default-key < OLD_FINGERPRINT> --sign-key < NEW_FINGERPRINT>
380+ ```
381+
382+ 2 . Publish a transition statement in the release notes and on the project
383+ website. The statement must be signed with the ** old** key and announce
384+ the ** new** fingerprint.
385+ 3 . Update ` docs/release.md ` section 13.10 with the new fingerprint, long
386+ key ID, creation date, and expiration.
387+ 4 . Replace ` docs/keys/phpmyfaq-release-public-key.asc ` with the export of
388+ the new key.
389+ 5 . Keep signing releases with the old key for one transition release, so
390+ users have time to import the new key before it becomes mandatory.
391+ 6 . Retire the old key after the transition release by importing its
392+ revocation certificate and publishing the revocation.
393+
394+ ### 13.15.4 Emergency revocation after compromise
395+
396+ If the signing key or its passphrase is suspected to be compromised, act
397+ immediately and assume every signature made after the suspected compromise
398+ date is untrustworthy.
399+
400+ 1 . Import the pre-generated revocation certificate from offline backup:
401+
402+ ``` bash
403+ gpg --import revocation-cert.asc
404+ ```
405+
406+ 2 . Push the revocation to keyservers:
407+
408+ ``` bash
409+ gpg --keyserver keys.openpgp.org --send-keys < FINGERPRINT>
410+ ```
411+
412+ 3 . Commit an updated ` docs/keys/phpmyfaq-release-public-key.asc ` that
413+ includes the revocation signature, and a clearly marked security advisory
414+ in the release notes and on the project website.
415+ 4 . Generate a brand-new release key (see the new-key flow) and publish the
416+ new fingerprint in section 13.10 with a note that all releases after the
417+ compromise date must be re-verified against the new key.
418+ 5 . Re-sign the currently published release archives and SBOMs with the new
419+ key. Update ` SHA256SUMS.asc ` and every ` <file>.asc ` artifact in place.
420+ Keep the archives themselves byte-identical — only the detached
421+ signatures change.
422+ 6 . File a security advisory in the repository's security advisories section
423+ so downstream package maintainers are notified through the normal GitHub
424+ security channel.
425+
426+ The revocation certificate can only be created while the private key is
427+ still available. Always generate it at key-creation time (see the new-key
428+ flow) and store it with the offline backup of the primary key.
429+
430+ ### 13.15.5 Post-rotation checklist
431+
432+ After any of the flows above, verify the following before cutting the next
433+ release:
434+
435+ - ` gpg --show-keys docs/keys/phpmyfaq-release-public-key.asc ` lists the
436+ expected primary key, active signing subkey, and (if applicable)
437+ revocation status.
438+ - Section 13.10 reflects the current fingerprint, long key ID, creation
439+ date, and expiration.
440+ - A dry-run ` ./scripts/sign-release-artifacts.sh 0.0.0-signtest ` succeeds
441+ end-to-end, including the internal ` gpg --verify ` pass on every
442+ ` .asc ` file.
443+ - CI secrets (` GPG_KEY_ID ` , ` GPG_PASSPHRASE ` , any imported signing subkey
444+ export) are updated to match the new key material.
0 commit comments