Assemble a KMS-backed certificate¶
Emit a publishable OpenPGP certificate whose primary and encryption subkey are both KMS keys, with both signatures made by the service.
What you need¶
- A
SIGN_VERIFYKMS key for the certification primary. AKEY_AGREEMENTkey cannot sign its own binding signature, so a second signing-capable key is structurally required. - A
KEY_AGREEMENTkey on a NIST curve for the encryption subkey. - Credentials that can call
kms:Signon the primary andkms:GetPublicKeyon both.
Assemble¶
signer, err := kmsenc.NewSigner(ctx, client, client, "alias/your-certify-key")
if err != nil {
return err
}
der, err := certificate.Certificate{
UserID: "Security <security@example.com>",
Created: created,
Subkey: subkey,
}.Assemble(signer.WithContext(ctx))
There is no field for the primary's public key: it is read from the signer, so a certificate cannot advertise a key its signer does not hold.
Assemble makes two kms:Sign calls and returns binary packets. Armour them
yourself if you are publishing to a page rather than to WKD.
Why WithContext¶
Assemble takes a crypto.Signer, the standard library's interface for a
key held elsewhere. Its Sign method has nowhere to put a context, but the call
it makes here is a network request that must be cancellable — so the provider
carries one on a copy of the signer. Pass signer directly and it uses the
context it was constructed with.
Choose the signature hash¶
SHA-256 by default. WithHash(crypto.SHA384) or WithHash(crypto.SHA512) if
you would rather, subject to your key service supporting the matching signing
algorithm — KMS offers PKCS#1 v1.5 over all three.
Pick the creation time deliberately¶
Created is hashed into the fingerprint, and the fingerprint is bound into every
message addressed to the certificate. The same key material with a different
creation time is a different certificate, and nothing encrypted to the old one
will open.
Store the timestamp you used.
Assembly is not byte-reproducible by default¶
Each signature carries a random salt, so assembling the same inputs twice produces different octets. That is deliberate — see what a certificate claims.
It does not change who the certificate is. Fingerprints are hashed over the public-key packets alone, so a regenerated certificate accepts exactly the same messages as the one it replaces.
If you genuinely need identical octets — a reproducible build, or a published
file that must not appear to have changed — pass WithoutSignatureSalt(), and
read what it costs before you do.
Verify before publishing¶
Do not publish a certificate you have not had something else parse:
entities, err := openpgp.ReadKeyRing(bytes.NewReader(der))
if err != nil {
return fmt.Errorf("certificate is not readable: %w", err)
}
if len(entities[0].Identities) != 1 || len(entities[0].Subkeys) != 1 {
return errors.New("a signature did not verify")
}
An OpenPGP implementation drops an identity or subkey whose signature fails rather than erroring, so their survival is the check. A certificate that parses but has lost its subkey is one nobody can encrypt to.
Then prove it round-trips¶
The strongest check is to encrypt something to the certificate you just made and recover the session key with the decryption path. Assembly and decryption meet at the fingerprint, so a round trip exercises both — and neither is meaningfully tested without the other. See the fingerprint binding.