Skip to content

Decrypt with AWS KMS

Recover a session key where the private half lives in KMS and never leaves it.

What you need

  • An ECC_NIST_P256, ECC_NIST_P384 or ECC_NIST_P521 KMS key whose KeyUsage is KEY_AGREEMENT. Those are the only specs KMS offers for key agreement, and RSA cannot be substituted — see why ECDH, not RSA.
  • Credentials that can call kms:GetPublicKey and kms:DeriveSharedSecret on it.
  • The certificate the message was addressed to, for its KDF parameters.

Build a deriver

import (
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/kms"

    kmsenc "gitlab.com/phpboyscout/go/encryption-aws-kms"
)

cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("eu-west-2"))
if err != nil {
    return err
}

client := kms.NewFromConfig(cfg)

deriver, err := kmsenc.NewDeriverForKey(ctx, client, client, "alias/your-key")
if err != nil {
    return err
}

Prefer NewDeriverForKey. It reads the curve from the key and refuses a key that cannot perform ECDH, so a misconfiguration fails at start-up rather than per message. NewDeriver takes the curve as an argument for callers who already know it, and a constant that disagrees with the key produces an error naming the sender's point — which sends whoever debugs it in the wrong direction.

Recover the session key

secret, err := deriver.DeriveSharedSecret(ctx, pkesk.EphemeralPoint)
if err != nil {
    return err
}

cipherID, sessionKey, err := encryption.SessionKey(encryption.Message{
    Version:      pkesk.Version,
    SharedSecret: secret,
    WrappedKey:   pkesk.WrappedKey,
}, encryption.KDFParams{
    CurveOID:        curveOID,     // from the certificate, with its length octet
    Fingerprint:     fingerprint,  // of the encryption subkey
    HashID:          hashID,       // from the certificate
    KEKAlgID:        kekAlgID,     // from the certificate
    CoordinateBytes: deriver.CoordinateBytes(),
})

Take the KDF parameters from the certificate the sender used, not from constants. The derivation binds the subkey's fingerprint, so parameters that do not come from that certificate cannot recover anything.

What the errors mean

The distinction matters when you are the one being paged:

Error Meaning
ErrIntegrity The message is not addressed to this key.
ErrChecksum The key was right; the payload under it is wrong.
ErrCurveMismatch The key behind the alias is not the curve configured.
ErrKeyRotated The alias resolved to a different key than last time.
ErrNotConfigured A wiring fault — a missing client, an empty key id.

ErrKeyRotated is the one worth wiring an alert to. It means messages addressed to the old certificate will stop opening, and no amount of retrying will help.

Cost and permissions

One DeriveSharedSecret call per message. NewDeriverForKey adds one GetPublicKey call at construction, so build the deriver once and reuse it rather than per message.