Skip to content

docs: specify the PEM encoding required for each native package signing key #65

Description

@jmgilman

Problem

docs/how-to/operate-a-native-package-repository.md
tells an adopter to create four key pairs and base64-encode the private halves,
but never states the PEM encoding each consumer requires. Two of the four
have a hard requirement that the obvious OpenSSL 3 command does not satisfy, and
neither failure surfaces until after a tag exists.

Producer APK key must be a legacy-encrypted traditional RSA PEM

sign-native-packages: true passes APK_SIGNING_KEY through to nFPM, which
signs with internal/sign/rsa.go (nfpm v2.47.0):

decryptedBlockData, err = x509.DecryptPEMBlock(block, []byte(passphrase))
...
priv, err = x509.ParsePKCS1PrivateKey(blockData)

x509.DecryptPEMBlock only understands the legacy Proc-Type: 4,ENCRYPTED /
DEK-Info: PEM headers. The natural command an adopter reaches for on OpenSSL 3

openssl genrsa -aes256 -out apk-signing-key.pem 4096

emits a PKCS#8 -----BEGIN ENCRYPTED PRIVATE KEY----- block, which fails. The
working form is:

openssl genrsa -out apk.plain 4096
openssl rsa -in apk.plain -traditional -aes256 -passout file:pass.txt -out apk-signing-key.pem

which yields -----BEGIN RSA PRIVATE KEY----- with Proc-Type: 4,ENCRYPTED.

Aggregate APK index key must be unencrypted

PACKAGE_REPOSITORY_APK_PRIVATE_KEY is materialized by
.github/actions/setup-package-repository and handed to
repogen.generateAPK, which mounts it into Alpine and runs:

abuild-sign -k "<mounted key>" APKINDEX.tar.gz

abuild-sign is non-interactive, so this key must be an unencrypted PEM.
The only current signal is the absence of a PACKAGE_REPOSITORY_APK_PASSPHRASE
environment secret in the table, which is easy to read as an oversight rather
than a requirement.

Why this matters

Neither encoding is validated early. go-pre-publish.yml checks only that the
four secrets are non-empty:

const required = ['RPM_SIGNING_KEY','RPM_SIGNING_PASSPHRASE','APK_SIGNING_KEY','APK_SIGNING_PASSPHRASE']
const missing = required.filter((name) => !process.env[name])

so a wrong producer encoding fails inside goreleaser release after the tag
and draft exist
, and a wrong aggregate encoding fails inside the central
receiver after the producer release is already public. Both land the adopter
in the recovery path documented in
operate-and-recover-releases.md
for a cause that is purely a key-format detail.

Suggested change

  1. In operate-a-native-package-repository.md, replace "one RSA private key for
    aggregate APK indexes" and the producer key bullet with exact generation
    commands and the resulting PEM header for each of the four keys.
  2. Add an encoding column to the secret tables in
    docs/reference/release-system.md: OpenPGP armored secret key (producer RPM
    and aggregate), traditional RSA PEM + legacy encryption (producer APK),
    unencrypted traditional RSA PEM (aggregate APK).
  3. Optionally, have go-pre-publish.yml and setup-package-repository
    pem.Decode each key and fail on the wrong block type, so the error arrives
    before the tag rather than mid-publication.

Context

Found while onboarding a second organization (componere) at v0.1.17. All four
keys were verified by decrypting them with the same primitives nFPM and
abuild-sign use before any release was attempted; the producer APK key had to
be regenerated in the traditional form to pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions