Documentation
¶
Overview ¶
Package encryptionawskms provides AWS KMS backends for the key operations that gitlab.com/phpboyscout/go/encryption leaves to its caller.
The split exists because only one operation in the OpenPGP ECDH decryption path needs a private key at all: deriving the ECDH shared secret. Everything else — the KDF, the AES key unwrap, the message decryption — is arithmetic over public data and lives in the core module. Keeping the two apart means the core is testable against fixed vectors with no AWS involved, and this module stays small enough to audit.
Signing IS implemented here, in signer.go: a crypto.Signer over kms:Sign, which certificate assembly needs for the binding signature over the certification primary. This said the work was outstanding long after it landed.
Nothing here ever holds private key material: KMS performs the operation and returns only its result.
Index ¶
- Constants
- Variables
- func NewServiceDefault(name string) (encryption.KeyService, error)
- func NewServiceFromClient(name string, client kmsAPI) (encryption.KeyService, error)
- func NewServiceFromConfig(name string, cfg aws.Config) (encryption.KeyService, error)
- func NewServiceFromSource(name string, src configSource) (encryption.KeyService, error)
- type Deriver
- type PublicKeyAPI
- type SharedSecretAPI
- type Signer
- type SignerAPI
Constants ¶
const ServiceName = "aws-kms"
ServiceName is the identifier the zero-conf default registers under, and the one a caller selects this provider by with "--backend aws-kms".
Variables ¶
var ( // secret. The API does this when the caller asked for an attested response // (CiphertextForRecipient), which this package never does — so it indicates // a response shape we do not understand rather than a recoverable state. ErrNoSharedSecret = errors.New("kms returned no shared secret") // ErrInvalidPoint means the peer's public point could not be interpreted as // a point on the expected curve. ErrInvalidPoint = errors.New("invalid peer public point") // ErrCurveMismatch means the configured curve disagrees with the secret KMS // returned for the key — the key behind an alias has changed curve since // this Deriver was built. ErrCurveMismatch = errors.New("curve does not match the KMS key") // ErrNoCurve means a Deriver has no curve: it was built with a nil one, or // never built by a constructor at all. // // Distinct from ErrNotConfigured so a caller can tell "I could not work out // the curve" from "you did not give me a client", which want different // responses. ErrNoCurve = errors.New("no curve configured") // ErrNotConfigured means a dependency or identifier was missing at // construction — a nil client, an empty key id. A wiring fault, nothing to // do with curves. ErrNotConfigured = errors.New("deriver is not configured") // ErrInvalidResponse means KMS, or something standing in for it, returned a // response this package cannot read: a nil output alongside a nil error, or // a public key that is not a SubjectPublicKeyInfo. Usually transport, // permissions or a substituted implementation — not a property of the key. ErrInvalidResponse = errors.New("unreadable response from kms") // ErrKeyRotated means the alias resolved to a different key than it did on // a previous derivation. The secret may be perfectly valid — it is simply // for a different key, so anything encrypted to the old certificate will // not open, and the cause is the alias rather than the message. ErrKeyRotated = errors.New("kms key behind the alias has changed") // ErrKeyUnusable means the KMS key cannot perform ECDH key agreement: wrong // KeyUsage, no ECDH among its agreement algorithms, or not an // elliptic-curve key at all. ErrKeyUnusable = errors.New("kms key cannot perform ECDH key agreement") )
Errors reported by this package.
var ErrDigestLength = errors.New("digest length does not match the hash")
ErrDigestLength means the digest handed to the signer is not the length its hash produces. KMS rejects a mismatched digest, but it does so as a ValidationException that says nothing about which side is wrong.
var ErrUnsupported = encryption.ErrUnsupported
ErrUnsupported is reported for a hash this package cannot map to a KMS signing algorithm, or a signature form it does not produce.
It is the core encryption.ErrUnsupported, re-exported, so a caller matching the sentinel it already knows catches this provider's unsupported cases too. Two independently declared errors with identical text is how a caller comes to handle "unsupported" for one backend and silently miss it for another.
Functions ¶
func NewServiceDefault ¶ added in v0.2.0
func NewServiceDefault(name string) (encryption.KeyService, error)
NewServiceDefault builds a named service that resolves its own configuration from the ambient AWS chain — profile, SSO session or instance role — the first time an operation needs one, lazily and memoised, via go/awsclient. This is the rung the blank-import default uses. There is no default region: awsclient surfaces an empty one as an error, because guessing means calling a key in an account nobody named.
func NewServiceFromClient ¶ added in v0.2.0
func NewServiceFromClient(name string, client kmsAPI) (encryption.KeyService, error)
NewServiceFromClient builds a named service around a KMS client the caller already holds — a shared client, a custom endpoint, an assumed-role client. The client is used as-is.
func NewServiceFromConfig ¶ added in v0.2.0
func NewServiceFromConfig(name string, cfg aws.Config) (encryption.KeyService, error)
NewServiceFromConfig builds a named service around an aws.Config the caller resolved. The client is constructed here with kms.NewFromConfig, which does no I/O. The region is checked now rather than left to fail per call, because a config with no region names a key in an account nobody chose.
func NewServiceFromSource ¶ added in v0.3.0
func NewServiceFromSource(name string, src configSource) (encryption.KeyService, error)
NewServiceFromSource builds a named service that resolves its AWS configuration through the supplied source. It is the sharing seam (org 0002 L-5, 0003 P-5): a consumer resolves one chain with go/awsclient and hands the same source to the config, signing and encryption adapters, so one credential resolution serves all three.
src := awsclient.Ambient(awsclient.WithRegion("eu-west-2"))
svc, err := encryptionawskms.NewServiceFromSource("aws-kms", src)
Types ¶
type Deriver ¶
type Deriver struct {
// contains filtered or unexported fields
}
Deriver performs the ECDH agreement for an OpenPGP message inside KMS.
It holds the curve because the value it is given — the ephemeral point from a message's PKESK packet — carries no curve of its own; it is a bare uncompressed point, and KMS wants a DER SubjectPublicKeyInfo. Re-encoding is the only translation that happens here.
Prefer NewDeriverForKey, which reads the curve from the key itself and checks the key can perform ECDH at all. A curve supplied by hand is a constant that can drift from the key it describes, and the resulting failure names the peer's point — sending whoever debugs it after the sender's message rather than the local configuration.
func NewDeriver ¶
NewDeriver returns a Deriver for a KMS key whose curve the caller already knows.
keyID may be a key id, ARN or alias. curve must be the curve of that key — P-256, P-384 or P-521, the NIST curves KMS supports for KEY_AGREEMENT. A nil curve is rejected here rather than panicking later, because the realistic source of one is a lookup that returned nothing for an unrecognised key spec.
func NewDeriverForKey ¶
func NewDeriverForKey( ctx context.Context, api SharedSecretAPI, pub PublicKeyAPI, keyID string, ) (*Deriver, error)
NewDeriverForKey returns a Deriver whose curve is read from the KMS key itself, and which refuses a key that cannot perform ECDH.
This is the constructor to reach for. The alternative — passing the curve as a constant — is a one-line configuration that silently disagrees with a rotated or re-created key, and the mismatch surfaces as an error about the sender's point rather than about local wiring.
The curve is resolved once, here. The key id is not: it is re-sent on every derivation, so an alias repointed at another key of the SAME curve keeps working, which is the ordinary rotation. A rotation that changes curve is detected rather than tolerated — the first derivation returns ErrCurveMismatch naming the key, and the Deriver must be rebuilt. Those two behaviours cannot both be had from one cached value, so the choice here is to survive the common rotation and fail loudly on the rare one.
func (*Deriver) CoordinateBytes ¶
CoordinateBytes reports the curve's coordinate length, which is both the expected length of a derived secret and the value the core package needs in its KDF parameters. It reports 0 for a Deriver with no curve, rather than panicking: Curve() returns nil and DeriveSharedSecret returns an error in that state, and an accessor that crashes where its siblings report is a trap for a caller assembling KDF parameters before its first derivation.
func (*Deriver) DeriveSharedSecret ¶
DeriveSharedSecret returns the raw ECDH shared secret for peerPoint, which is the ephemeral public point from the message's PKESK packet in uncompressed form (0x04 || X || Y).
The result is the bare x-coordinate of the shared point, left-padded to the curve's byte length — exactly the input the OpenPGP KDF expects. That equivalence is the reason this whole approach works, and it was verified byte-for-byte against a live KMS key rather than inferred from the documentation.
KMS performs cofactor ECDH (SP 800-56A). NIST curves have cofactor h=1, so that is ordinary ECDH here.
func (*Deriver) PublicKey ¶ added in v0.2.0
PublicKey returns the encryption key's public half, which a certificate must carry as its subkey.
Available only on a Deriver built by NewDeriverForKey: NewDeriver is told the curve rather than reading the key, so it has nothing to return. That is reported rather than returning nil, because a nil public key would surface much later as an unreadable subkey packet.
type PublicKeyAPI ¶
type PublicKeyAPI interface {
GetPublicKey(
ctx context.Context,
params *kms.GetPublicKeyInput,
optFns ...func(*kms.Options),
) (*kms.GetPublicKeyOutput, error)
}
PublicKeyAPI reports a KMS key's public half. Kept separate from SharedSecretAPI so a caller who already knows the curve need not implement it.
type SharedSecretAPI ¶
type SharedSecretAPI interface {
ctx context.Context,
params *kms.DeriveSharedSecretInput,
optFns ...func(*kms.Options),
) (*kms.DeriveSharedSecretOutput, error)
}
SharedSecretAPI is the slice of the KMS API this package needs to derive a shared secret.
Declared here, narrowly, rather than accepting *kms.Client: it is the whole of what a caller must provide to substitute KMS in a test, and a one-method interface is one method's worth of fake.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer makes signatures with a KMS-held key, satisfying crypto.Signer.
The private half never leaves KMS: this type sends a digest and receives a signature, which is the whole of its job.
crypto.Signer rather than an interface of our own, because that is what the ecosystem already speaks — go-crypto assembles an OpenPGP certificate given one, and x509.CreateCertificate and tls.Certificate take one too. A bespoke signing interface would have obliged every consumer to write the assembly itself.
func NewSigner ¶
NewSigner returns a Signer for a KMS key, checking at construction that the key can actually sign.
The usage check matters for the same reason it does on the deriver: pointing at the wrong key of the right type wires up cleanly and fails later, per signature, inside whatever was assembling a certificate at the time.
func (*Signer) Public ¶ added in v0.2.0
Public returns the certification key's public half, so a certificate can carry it.
func (*Signer) Sign ¶ added in v0.2.0
Sign signs a precomputed digest in KMS, implementing crypto.Signer.
The digest is precomputed because that is the contract: crypto.Signer is handed a hash of the message, and KMS is asked to sign it with MessageType DIGEST. It suits OpenPGP, which hashes more than the material being signed — the signature's own preamble and a trailer go in too — so there is no message KMS could be given that would produce the right digest.
The io.Reader is ignored, as it is by every remote signer: KMS chooses its own randomness, and PKCS#1 v1.5 signing is deterministic in any case.
func (*Signer) WithContext ¶ added in v0.2.0
WithContext returns a copy of the signer whose Sign calls use ctx.
crypto.Signer.Sign takes no context, but the call it makes here is a network request that a caller must be able to cancel or bound. Carrying the context on the signer rather than in the method is how every remote signer resolves that; a copy rather than a mutation so one signer can be shared across concurrent requests with different deadlines.
type SignerAPI ¶
type SignerAPI interface {
Sign(
ctx context.Context,
params *kms.SignInput,
optFns ...func(*kms.Options),
) (*kms.SignOutput, error)
}
SignerAPI is the slice of the KMS API certificate assembly needs.
Separate from SharedSecretAPI because they are different keys with different roles: the certification primary signs, the encryption subkey agrees. A caller wiring only decryption should not have to supply a signer at all.