openpgpkey

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package openpgpkey mints an ASCII-armored OpenPGP public key from a crypto.Signer. It exists to bridge a KMS/HSM-held signing key (which exposes only a public key plus a remote Sign operation) into the OpenPGP form that the self-update verifier (pkg/setup) and Web Key Directory require.

Why this is needed: the verifier rejects a bare public-key packet ("v4 entity without any identities") — it needs a User ID and a self-signature. Producing that self-signature requires *signing* with the private key. go-crypto signs RSA keys through the crypto.Signer interface, so an opaque KMS-backed signer works without the private key ever leaving the KMS.

Supported key algorithm: **RSA only.**

signer.Public() must return *rsa.PublicKey. The package produces a v4 RSA OpenPGP public-key packet — used by the primary release-signing key path (AWS KMS exposes asymmetric SIGN_VERIFY keys only as RSA) and by `gtb keys generate --algorithm rsa` for the local-signing tutorial flow.

Other key types return ErrUnsupportedKeyType. Ed25519 key generation lives in `internal/cmd/keys/generate.go`, where it goes through `openpgp.NewEntity(... PubKeyAlgoEdDSA ...)` to produce a v4-EdDSA (algorithm 22) key that GnuPG 2.4 and older can import. Bringing Ed25519 into this package would require reaching go-crypto's `internal/ecc` package, which is unreachable from outside the go-crypto module — see docs/development/specs/2026-06-08-keys-mint-command.md D12.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupportedKeyType = errors.New("unsupported key type: only RSA is supported")

ErrUnsupportedKeyType is returned by ArmoredPublicKey when the signer's Public() returns a key type this package does not handle. Only *rsa.PublicKey is supported in v0.1 — see the package doc.

Functions

func ArmoredPublicKey

func ArmoredPublicKey(signer crypto.Signer, name, email string, creationTime time.Time) ([]byte, error)

ArmoredPublicKey builds a self-signed OpenPGP key from signer and returns its ASCII-armored *public* half — ready to embed (internal/trustkeys/keys) or publish via WKD. creationTime is baked into the key and its self-signature; keep it stable (rotations get a new key, not a new timestamp on the same key).

func DetachSign added in v0.11.0

func DetachSign(signer crypto.Signer, publicKey []byte, data io.Reader, sigCreationTime time.Time) ([]byte, error)

DetachSign produces an ASCII-armored OpenPGP detached signature over data using signer.

publicKey is the armored OpenPGP public-key block that identifies the signer (the .asc file previously produced by ArmoredPublicKey and embedded in the verifier's trust set / published via WKD). Its creation time and primary-UID drive the OpenPGP entity that DetachSign rebuilds around signer, so the signer-fingerprint that lands in the signature is identical to publicKey's fingerprint.

signer must be the private half of the same RSA key as publicKey — if it isn't, the resulting signature will name publicKey's fingerprint but the underlying signature bytes will be wrong, and every verifier will reject it.

sigCreationTime is the signature's own creation-time subpacket (RFC 4880 §5.2.3.4). It is independent of the key's creation time embedded in publicKey. Pinning it produces byte-identical signatures across re-runs over the same content.

Errors:

  • ErrUnsupportedKeyType if signer.Public() is not *rsa.PublicKey (matches ArmoredPublicKey's contract).
  • If publicKey cannot be parsed as a single-entity armored key ring.
  • If signer.Public() disagrees with the public half embedded in publicKey (we'd be producing a signature with a fingerprint that doesn't match the actual signing key).
  • Any error from signer.Sign() (KMS network/permissions/etc.).
  • Any error from go-crypto's packet framing.

API stability: Beta. Surface is additive to ArmoredPublicKey / Entity / WriteWKDTree.

func Entity

func Entity(signer crypto.Signer, name, email string, creationTime time.Time) (*openpgp.Entity, error)

Entity assembles a self-signed, signing-capable OpenPGP entity around signer: an RSA public-key packet, an RSA private-key packet driven by signer (so KMS-held keys work), a User ID, and a positive-cert self-signature.

Callers that need only the armored public half should prefer ArmoredPublicKey, which wraps Entity + armor.Encode + Entity.Serialize in one call. Direct callers of Entity get both halves and can drive Entity.Serialize / Entity.SerializePrivate themselves — useful when writing both halves to separate files, which is what `gtb keys generate` does for the RSA path.

Supports opaque signers (any crypto.Signer with *rsa.PublicKey from Public()) — go-crypto's RSA signing path dispatches via the crypto.Signer interface, so a KMS-backed signer works.

Non-RSA signers return ErrUnsupportedKeyType. Ed25519 generation is handled separately in `internal/cmd/keys/generate.go` via openpgp.NewEntity with PubKeyAlgoEdDSA — see the package doc for the architectural rationale.

func WKDHash

func WKDHash(email string) (string, error)

WKDHash returns the z-base-32 encoded SHA-1 of the lowercased local-part of an email — i.e. the filename used under hu/ in the WKD layout. Exposed so callers can pre-compute the expected URL or hash a UID without writing files.

func WriteArmoredPublicKey

func WriteArmoredPublicKey(w io.Writer, signer crypto.Signer, name, email string, creationTime time.Time) error

WriteArmoredPublicKey writes the same content as ArmoredPublicKey directly to w. The two-function shape exists so callers that want to stream into a file or socket can skip the intermediate buffer; it also lets the test suite inject a failing writer to exercise the error-wrapping branches around armor.Encode / Close.

func WriteWKDTree

func WriteWKDTree(outDir, domain string, opts Options, entries ...Entry) ([]string, error)

WriteWKDTree writes a complete WKD directory layout under outDir.

For each unique Email across entries, it computes the WKD hu/<hash> filename, concatenates that email's binary OpenPGP key packets (sorted by primary-key fingerprint for reproducible output), and writes the resulting file. Alongside hu/ it writes a zero-byte policy file (RFC-required) and, if opts.SubmissionAddress is non-empty, a submission-address file.

The full layout for advanced method (the default):

outDir/.well-known/openpgpkey/<domain>/
├── policy                  (zero bytes)
├── submission-address      (optional — only when opts.SubmissionAddress != "")
└── hu/<z-base-32-hash>     (one per unique email)

Direct method drops the <domain>/ level:

outDir/.well-known/openpgpkey/
├── policy
├── submission-address?
└── hu/<z-base-32-hash>

Returns the relative paths (under outDir) of every file written, in the order written, for callers that want to log or assert against the result.

WriteWKDTree refuses to operate outside outDir/.well-known/openpgpkey (path-traversal defence against pathological domain values).

Errors:

  • if domain is empty, contains a path separator, or is otherwise invalid as a hostname;
  • if an Entry's Email cannot be parsed by net/mail;
  • if a key cannot be parsed as armored or binary OpenPGP;
  • if no entries are supplied (an empty WKD tree is never useful).

Types

type Entry

type Entry struct {
	// Email must contain a parseable RFC 5322 address. Only the
	// local-part is used (lowercased, ASCII) for WKD hashing.
	Email string

	// Keys are the public-key bytes to publish under Email. Each
	// element may be either ASCII-armored or already-binary OpenPGP;
	// WriteWKDTree auto-detects and dearmors as needed before writing
	// the hu/ file (which must contain binary packets per RFC §3.1).
	Keys [][]byte
}

Entry binds one email address to the public keys to publish under it. The Email's local part is what gets hashed into the hu/<hash> filename; the Keys are concatenated (binary OpenPGP packets) into that file.

Multiple Entry values with the same Email merge: their Keys are pooled into a single hu/ file. Re-using Entry values with the same Email is therefore equivalent to passing a single Entry with all the keys.

type Method

type Method string

Method selects the WKD URL layout per draft-koch-openpgp-webkey-service §3.1.

const (
	// MethodAdvanced is the dedicated-subdomain layout served from
	// openpgpkey.<domain>; URLs include an extra <domain>/ path
	// segment (...openpgpkey/<domain>/hu/<hash>). This is the layout
	// used by the gtb release-signing endpoint at
	// openpgpkey.phpboyscout.uk.
	MethodAdvanced Method = "advanced"

	// MethodDirect is the same-domain layout served from <domain>
	// itself; URLs omit the extra <domain>/ segment
	// (...openpgpkey/hu/<hash>). GPG clients fall back to this when
	// the advanced URL is missing.
	MethodDirect Method = "direct"
)

type Options

type Options struct {
	// Method is the URL layout to emit. Zero value defaults to
	// MethodAdvanced (the dedicated-subdomain layout).
	Method Method

	// SubmissionAddress, if non-empty, is written to the domain
	// directory's submission-address file. WKS-aware clients fetch
	// this to discover where to send key-submission requests. Empty
	// means "do not emit submission-address" (still emits policy,
	// which is always required by RFC §3.1).
	SubmissionAddress string
}

Options configures non-per-entry WKD emission settings.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL