Skip to content

Commit 9a6ff73

Browse files
committed
docs: documented SBOM generation and release key lifecycle
Extended the release documentation to cover the CycloneDX SBOM files now produced by the release scripts, plus end-to-end guidance for adding, rotating, and revoking the release signing key.
1 parent ac07f2d commit 9a6ff73

1 file changed

Lines changed: 269 additions & 8 deletions

File tree

docs/release.md

Lines changed: 269 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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:
5457
build/release/4.2.0/phpMyFAQ-4.2.0.zip
5558
build/release/4.2.0/phpMyFAQ-4.2.0.tar.gz
5659
build/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
5763
build/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

6273
To 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

7592
The 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
111128
docs/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

119149
After 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
148181
gpg --verify phpMyFAQ-<version>.zip.asc phpMyFAQ-<version>.zip
149182
gpg --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

164227
Publish 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

172238
The 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

Comments
 (0)