Skip to content

Implement a key service

Use a key service other than AWS KMS — an HSM, a smartcard, Vault, or a software key held somewhere specific.

What the packages need

Two interfaces, and neither mentions a vendor.

For decryption, the caller supplies whatever satisfies its own needs; the core takes the raw shared secret as bytes. The provider modules exist to fetch that value, and the shape they follow is one method:

DeriveSharedSecret(ctx context.Context, peerPoint []byte) ([]byte, error)

For certificate assembly, the core takes the standard library's crypto.Signer — no interface of its own:

Public() crypto.PublicKey
Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)

This package once declared its own narrower signing interface, on the reasoning that a key service is handed a digest and chooses its own randomness. That was true and beside the point: crypto.Signer is what the rest of the ecosystem speaks, so an adapter that implements it also drives x509.CreateCertificate, tls.Certificate and anything else that takes one — for free, instead of once per consumer.

Two consequences worth knowing before you write one:

  • Public() cannot return an error. Read and validate the public half in your constructor, where you can still report a failure, and cache it. An asymmetric key's material does not change under you.
  • Sign() takes no context. If your service needs one — and a network-backed one does — carry it on the signer. The AWS provider offers WithContext(ctx), which returns a copy so a single signer can be shared across requests with different deadlines.

The rand argument is ignored by every remote signer, including ours.

Two contracts to honour

The shared secret is the bare x-coordinate, left-padded to the curve's coordinate length. Not a DER wrapper, not a KDF applied for you. If your service returns something else, convert it — the KDF hashes these bytes verbatim, and a secret that lost a leading zero produces a plausible but wrong key.

The signature is PKCS#1 v1.5, not PSS. An OpenPGP RSA signature is defined that way, and a PSS signature over the same digest is perfectly valid and rejected by every OpenPGP implementation. It will look like it works right up until someone tries to use the certificate.

Testing your adapter

There is no mock for the signer, because crypto.Signer is the standard library's and a fake is three lines:

type fakeSigner struct{ key *rsa.PrivateKey }

func (s fakeSigner) Public() crypto.PublicKey { return &s.key.PublicKey }

func (s fakeSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
    return rsa.SignPKCS1v15(rand.Reader, s.key, opts.HashFunc(), digest)
}

Expect two calls when assembling a certificate — the user ID self-certification and the subkey binding. A fake that counts them will catch an adapter that signs the same digest twice.

For the registry interfaces the core does declare, generated mocks ship as a subpackage:

import mocks "gitlab.com/phpboyscout/go/encryption/mocks"

deriver := mocks.NewMockDeriver(t)
deriver.EXPECT().CoordinateBytes().Return(32)

The AWS provider ships mocks for its own KMS API seams the same way, under gitlab.com/phpboyscout/go/encryption-aws-kms/mocks.