Skip to content

Sign with the local backend

Sign a checksums manifest using a private key held in a PEM file on disk, via the reference local backend. This is the library path; in production, release signing is normally driven by a CLI such as gtb sign, which wraps these same calls.

The local backend is the lightweight default, meant for onboarding, local development and reproducible examples. A private key on disk is a weaker posture than a KMS or HSM, so for production use AWS KMS or implement a custom backend for your own KMS or HSM.

Activate the backend

Backends register themselves on import. Blank-import the local package so its init() registers the name "local":

import (
    "context"

    "gitlab.com/phpboyscout/go/signing"
    _ "gitlab.com/phpboyscout/go/signing/local" // registers "local"
    "gitlab.com/phpboyscout/go/signing/openpgpkey"
)

Point at a PEM key

Generate (or reuse) an unencrypted RSA private key of at least 3072 bits:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out release.pem

The keyID you pass to the local backend is simply the path to this PEM file.

Supported formats:

  • PKCS#1, RSA PRIVATE KEY armour (RSA)
  • PKCS#8, PRIVATE KEY armour (RSA, ECDSA P-256 or Ed25519)
  • SEC 1, EC PRIVATE KEY armour (ECDSA P-256)

Three key types, matching the signing paths:

Key type Path
RSA OpenPGP signing of checksum manifests
Ed25519 minisign artefact signing, which is what the Rust consumers verify
ECDSA P-256 container image signing in the cosign formats

Ed25519 support is what makes the artefact path usable without an HSM, so you can try it, test it, or run it in a project that has no cloud KMS at all.

Limitations (all surface as typed sentinels from the local package):

  • Encrypted PEM is unsupported (local.ErrEncryptedPEMUnsupported). Decrypt out of band first, or use a KMS or HSM backend.
  • Only RSA, ECDSA P-256 and Ed25519 are supported. Anything else (ECDSA on P-384, for instance) returns local.ErrUnsupportedKeyType. P-256 is read from PKCS#8 or from a SEC 1 EC PRIVATE KEY block.
  • An armored OpenPGP secret key is refused (local.ErrOpenPGPSecretKey). keys generate --algorithm ed25519 writes one by default; add --private-format pem to get a private half this backend reads.
  • A file with no PEM block, or with a PEM block of an unrecognised type, yields local.ErrMissingPEMBlock.
  • The path is read as given: symlinks are followed and nothing is canonicalised. Fine for an operator-supplied path, worth knowing if it could come from anywhere less trusted.

A key on disk is a key that can be copied. This backend is for tutorials, tests, and projects without an HSM; where custody matters, use aws-kms.

Resolve, sign, publish

Resolve the backend through the registry, build a crypto.Signer from the PEM path, then mint the public key and sign the manifest:

backend, err := signing.Get("local") // requires the blank import above
if err != nil {
    return err // e.g. signing.ErrUnknownBackend if not imported
}

signer, err := backend.NewSigner(context.Background(), "release.pem") // keyID = PEM path
if err != nil {
    return err // e.g. local.ErrEncryptedPEMUnsupported
}

now := time.Now()
pub, err := openpgpkey.ArmoredPublicKey(signer, "Release", "release@example.test", now)
if err != nil {
    return err
}

manifest := []byte("sha256  ffmpeg.wasm  0xc0ffee\n")
sig, err := openpgpkey.DetachSign(signer, pub, bytes.NewReader(manifest), now)
if err != nil {
    return err
}
// publish: manifest, sig (ASCII-armoured detached signature), and pub

Ship manifest, sig, and pub with your release assets. Consumers embed pub and run the verify recipe.

ArmoredPublicKey and DetachSign take any crypto.Signer, so the same code signs whether the key lives in a PEM file or a remote KMS. Only the backend differs.

Run a named instance

The local package also registers a factory, so signing.NewInstance("local", "staging") returns a backend named local:staging. It reads keys exactly as the default does; local carries no per-instance settings, and the factory exists so a CLI can treat every backend type the same way. NewInstance does not register what it returns. See the backend registry.

See also