Documentation
¶
Index ¶
- Variables
- func Fingerprint(packetBody []byte) ([20]byte, error)
- func Parse(der []byte, opts ...ParseOption) (Recipient, Findings, error)
- func ParseAt(der []byte, now time.Time, opts ...ParseOption) (Recipient, Findings, error)
- func RecommendedKDF(curveOID []byte) (hashID, kekAlgID byte, err error)
- type AssembleOption
- type Certificate
- type ECDHPublicKey
- type Findings
- type ParseOption
- type RSAPublicKey
- type Recipient
Constants ¶
This section is empty.
Variables ¶
var ErrTooManySignatures = fmt.Errorf("%w: too many signatures over the primary key", encryption.ErrMalformed)
ErrTooManySignatures means a certificate carries more signatures over its primary key than this reads, so it refuses rather than risk a revocation hidden past the bound.
Exported because it is a DELIBERATE fail-closed, not damage: appending enough signature packets to a published certificate makes it unreadable, which is the availability cost of never returning a key a hidden revocation might have withdrawn. A caller — the appended-bytes fuzz especially — matches this to tell that documented refusal apart from a certificate genuinely lost. A well-formed certificate never reaches it: it carries one self-certification, not seventeen.
Functions ¶
func Fingerprint ¶
Fingerprint returns the version 4 fingerprint of a public-key packet body.
RFC 9580 §5.5.4: SHA-1 over 0x99, the body's two-octet length, and the body. SHA-1 is what the format specifies for v4 — it is not a security choice made here, and it is not used for any integrity claim.
func Parse ¶
func Parse(der []byte, opts ...ParseOption) (Recipient, Findings, error)
Parse reads a certificate against the current time. See ParseAt.
func ParseAt ¶
ParseAt reads a certificate as of the instant now, returning its encryption subkey's parameters, any findings, and an error.
now is injected rather than read from the clock inside so that expiry is a pure function of (certificate, now): a test can place a certificate before or after a boundary without waiting for it, and the metamorphic harness can drive time directly. Parse supplies time.Now for callers that do not care.
The subkey's binding signature is verified against the primary key, so a subkey appended by someone who does not hold the primary is rejected rather than returned. A subkey the primary has revoked is refused even though its binding still verifies; if that leaves no candidate, the error wraps encryption.ErrRevoked, so a caller can tell "withdrawn, fetch a current certificate" from "this certificate is broken".
A revoked or expired certificate is refused by default — the safe answer for a sender. AllowWithdrawn returns its recipient alongside the standing error instead, for a caller decrypting ciphertext already encrypted to it.
Identity self-certifications are not checked for what they claim about the identity: this reads a certificate to find out how to decrypt, and the user ID plays no part in that. The primary's own expiry, which rides on one, is.
Only ECDH subkeys are considered, since those are the only ones this module can derive against. A certificate with none is refused.
func RecommendedKDF ¶
RecommendedKDF returns the hash and key-wrapping algorithm RFC 6637 §12.1 pairs with a curve.
Each curve is matched with a hash and a KEK of comparable strength, so a P-521 subkey wraps under AES-256 rather than inheriting whatever the caller happened to hardcode. Advertising a weaker pair than the curve supports is not an error any implementation reports — every message simply arrives weaker than the key allows.
Types ¶
type AssembleOption ¶
type AssembleOption func(*assembleConfig)
AssembleOption adjusts how a certificate is assembled.
func WithHash ¶
func WithHash(h crypto.Hash) AssembleOption
WithHash sets the signature hash algorithm. SHA-256 by default.
The signer must be able to produce a signature over this hash; a key service that supports a fixed set will reject anything else.
func WithSignatureTime ¶
func WithSignatureTime(t time.Time) AssembleOption
WithSignatureTime stamps both signatures. Defaults to the current time.
Deliberately separate from Certificate.Created, which stamps the key packets. The two are different kinds of fact: Created is identity — it is hashed into the primary's fingerprint, so a rotation must hold it fixed or the certificate becomes a different certificate — whereas signing is an event that happens whenever it happens.
Stamping both from Created is what makes a rotation ambiguous. The ordinary way to rotate an encryption subkey is to publish one certificate carrying the retired key and its replacement, both validly bound, and a reader picks the newest binding. Two assemblies sharing one Created produce bindings with identical creation times, so "newest wins" has nothing to compare and falls through to packet order — and a reader that merges and re-exports the two, as GnuPG does, can hand back the retired subkey. Every message encrypted to the new one then fails an integrity check, pointing the maintainer at the sender's message rather than at their own certificate.
Pass it to pin the value; leave it alone and each assembly stamps now, which is what makes successive rotations orderable.
func WithoutSignatureSalt ¶
func WithoutSignatureSalt() AssembleOption
WithoutSignatureSalt turns off the randomised salt notation, removing the one source of randomness in assembly.
Reach for it only when byte-for-byte reproducibility is the requirement — a reproducible build, or a published certificate that must not appear to have changed when it is regenerated. It costs the two protections described on newSalt.
Not sufficient for reproducibility on its own: signatures are stamped with the current time unless WithSignatureTime pins it, so a byte-identical regeneration needs both.
It is not needed to keep a certificate addressable. Fingerprints are hashed over the public-key packets alone, so a salted and an unsalted assembly of the same keys accept exactly the same messages.
type Certificate ¶
type Certificate struct {
// UserID is the identity the primary certifies, in the conventional
// "Name <email>" form. Hashed verbatim, so it is carried exactly as given.
UserID string
// Created stamps the primary key packet, and is hashed into the primary's
// fingerprint — so it is part of the certificate's identity. The same key
// material with a different Created is a different certificate, and nothing
// encrypted to the old one will open.
//
// Never time.Now() for a certificate that will be republished, and held
// fixed across a rotation for the same reason. Record the value used.
//
// The subkey carries its own creation time. The signatures do not take
// theirs from here: see WithSignatureTime, which defaults to now, and why
// the two must be separable for a rotation to be orderable.
Created time.Time
// Subkey is the ECDH encryption subkey the primary binds.
Subkey ECDHPublicKey
}
Certificate describes the certificate to assemble.
There is no field for the primary's public key: it comes from the signer, so a certificate cannot advertise a key its signer does not hold.
func (Certificate) Assemble ¶
func (c Certificate) Assemble(signer crypto.Signer, opts ...AssembleOption) ([]byte, error)
Assemble produces the certificate as a sequence of OpenPGP packets.
Two signatures are made, both by the primary through signer: a positive certification over the user ID, and a binding over the encryption subkey. Neither can be made locally, which is the entire point — the primary's private half lives in a key service and this process never sees it.
The primary is taken from signer.Public, which must be an *rsa.PublicKey. That is not a limitation of OpenPGP but of the pairing this package exists to serve: an agreement-only key cannot sign its own binding signature, so a separate signing key is structurally required, and RSA is the one whose OpenPGP signature form is PKCS#1 v1.5 — which is what the format specifies and what a KMS will produce.
The result is binary packets. Callers wanting an armoured certificate wrap it themselves; this package does not carry an armour encoder for one caller.
type ECDHPublicKey ¶
type ECDHPublicKey struct {
// Created is the key's creation time, truncated to the second. It is part
// of the packet and therefore part of the fingerprint: the same key
// material with a different creation time is a different certificate.
Created time.Time
// CurveOID is the curve identifier with its leading length octet, the same
// encoding KDFParams takes.
CurveOID []byte
// Point is the public point in uncompressed form (0x04 || X || Y).
Point []byte
// HashID and KEKAlgID are the KDF parameters the certificate advertises,
// and which a decrypting party must use. They are part of the packet, so
// changing them changes the fingerprint.
HashID byte
KEKAlgID byte
}
ECDHPublicKey is the material an OpenPGP ECDH public-key packet carries.
The fields are the ones that end up hashed into the fingerprint, so a caller assembling a certificate around a key held elsewhere — in a KMS, say — supplies exactly what it can read back from that service.
func (ECDHPublicKey) PacketBody ¶
func (k ECDHPublicKey) PacketBody() ([]byte, error)
PacketBody returns the body of the version 4 public-key packet for this key — the packet without its framing header.
The body rather than a full packet because that is what the fingerprint is computed over, and what a caller embeds in a certificate. Callers that need a serialised packet add their own header.
type Findings ¶
type Findings []error
Findings are things Parse noticed that did not stop it returning a subkey but that a caller may want to act on — a revocation it could not evaluate, a certificate read only part-way, a subkey standing in for a revoked one.
A separate return rather than a field on Recipient, because a field rode the success value: on an error path the zero Recipient was returned and its notes went with it, so the one case where a finding matters most — no usable subkey, and a revocation nobody could check — was exactly the case that dropped it (finding #24). As a second value they come back on every path, error or not.
Inspect with errors.Is against encryption.ErrUnverifiableRevocation and encryption.ErrIncompleteCertificate. They are errors rather than a bespoke type so a caller matches, wraps and logs them the way it already handles everything else this module returns.
type ParseOption ¶
type ParseOption func(*parseConfig)
ParseOption adjusts how ParseAt treats a certificate whose standing is in question.
func AllowWithdrawn ¶
func AllowWithdrawn() ParseOption
AllowWithdrawn returns the certificate's recipient even when its holder has revoked the certificate or let its primary key expire, alongside the encryption.ErrRevoked or encryption.ErrExpired that says so.
The default refuses a withdrawn certificate outright, and that is the right answer for a SENDER: nothing new should be encrypted to a key its holder has retired. A caller DECRYPTING ciphertext already encrypted to the certificate needs the opposite — a revoked or expired key still opens messages sent before it was withdrawn — so it opts in here. It then receives the recipient together with the standing error and decides for itself, loudly, whether to proceed; the error is not suppressed, only made non-fatal at the caller's discretion.
type RSAPublicKey ¶
type RSAPublicKey struct {
// Created is the key's creation time. Hashed into the fingerprint.
Created time.Time
// Modulus and Exponent are big-endian, without leading zero octets.
Modulus []byte
Exponent []byte
}
RSAPublicKey is the material an OpenPGP RSA public-key packet carries.
The certification primary is RSA because that is what the estate's KMS key spec is: KEY_AGREEMENT keys cannot sign their own binding signature, so a second signing-capable key is structurally required, and RSA_4096 is the combination already driven from KMS here.
func (RSAPublicKey) PacketBody ¶
func (k RSAPublicKey) PacketBody() ([]byte, error)
PacketBody returns the body of the version 4 public-key packet.
type Recipient ¶
type Recipient struct {
// KeyID identifies the encryption subkey, so a message addressed elsewhere
// can be refused before any key-service call is made.
KeyID [8]byte
// Fingerprint is the subkey's full fingerprint. KeyID is its low octets,
// and only the fingerprint is bound into the derivation.
Fingerprint [20]byte
// KDF carries the derivation parameters the message's key was made with.
// CoordinateBytes is left unset: it is a property of the curve as the key
// service reports it, not something the certificate states.
KDF encryption.KDFParams
}
Recipient is a certificate reduced to what recovering a session key needs.