Skip to content

Provision an encryption identity

By the end of this you'll have a certification primary key, an ECDH encryption subkey, and the two IAM roles that use them — all in AWS KMS, with neither private half ever leaving it. You'll finish by downloading the public key you would eventually publish.

Allow about twenty minutes. Most of that is waiting on tofu apply.

What this does not cover

You will not end up with a publishable OpenPGP certificate, and you will not decrypt anything. Assembling the certificate and decrypting messages are code, they live in sigillum, and at the time of writing an end-to-end decrypt through DeriveSharedSecret has not yet been demonstrated. This tutorial stops at the infrastructure, which is the part that works today.

That is worth knowing before you start, because it changes whether this is useful to you right now.

Before you start

You'll need:

  • OpenTofu 1.12.5 or later. Terraform will mostly work, but this module drives prevent_destroy from a variable, which OpenTofu allows and Terraform does not.
  • AWS credentials for an account you don't mind creating keys in. A sandbox is ideal.
  • Two IAM role ARNs that already exist in that account — one to read, one to certify. They can be the same role while you're learning.

Two asymmetric KMS keys cost about $2/month while they exist, and you cannot delete them for at least 7 days once you schedule deletion. Use a sandbox.

Write the configuration

Make a directory and put this in main.tf. Replace the four ARNs with real ones from your account.

terraform {
  required_version = "~> 1.12.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "eu-west-2"
}

module "identity" {
  source  = "gitlab.com/phpboyscout/encryption-kms/aws"
  version = "~> 0.1"

  name        = "tutorial-identity-v1"
  description = "Learning the encryption-kms module"

  reader_principal_arns    = ["arn:aws:iam::111122223333:role/your-role"]
  certifier_principal_arns = ["arn:aws:iam::111122223333:role/your-role"]
  key_administrator_arns   = ["arn:aws:iam::111122223333:role/your-role"]
  automation_role_arn      = "arn:aws:iam::111122223333:role/your-role"
}

output "encrypt_alias" {
  value = module.identity.encryption_key_alias_name
}

The -v1 on the name is deliberate. An OpenPGP identity is superseded by minting a new one rather than editing this one, so the version belongs in the name from the start.

Apply it

tofu init
tofu apply

You'll see six resources planned: two aws_kms_key, two aws_kms_alias, and two aws_iam_role. Apply takes a minute or so — KMS key creation is not instant.

When it finishes:

Apply complete! Resources: 6 added, 0 changed, 0 destroyed.

Outputs:

encrypt_alias = "alias/tutorial-identity-v1-encrypt"

Confirm the keys are what you expect

Check the encryption subkey first. This is the one that has to be a key-agreement key — if it came out as anything else, nothing downstream will work:

aws kms describe-key --key-id alias/tutorial-identity-v1-encrypt \
  --query 'KeyMetadata.{Usage:KeyUsage,Spec:KeySpec,Algos:KeyAgreementAlgorithms}'
{
    "Usage": "KEY_AGREEMENT",
    "Spec": "ECC_NIST_P256",
    "Algos": [
        "ECDH"
    ]
}

KEY_AGREEMENT with ECDH available is the whole point. A key that says ENCRYPT_DECRYPT here cannot decrypt OpenPGP at all — see why RSA cannot work.

Now the certification primary:

aws kms describe-key --key-id alias/tutorial-identity-v1-certify \
  --query 'KeyMetadata.{Usage:KeyUsage,Spec:KeySpec}'
{
    "Usage": "SIGN_VERIFY",
    "Spec": "RSA_4096"
}

Two keys, two usages. KMS fixes key usage at creation and allows only one per key, which is why an OpenPGP identity needs both.

Download the public key

This is the half that eventually gets published:

aws kms get-public-key \
  --key-id alias/tutorial-identity-v1-encrypt \
  --output text --query PublicKey | base64 -d > subkey.der

You now have a DER-encoded SubjectPublicKeyInfo. Confirm it is the curve you asked for:

openssl pkey -pubin -inform DER -in subkey.der -text -noout | head -3
ED25519 Public-Key:

— is what you would see for the wrong key type. You should instead see:

Public-Key: (256 bit)

with ASN1 OID: prime256v1 further down. That is NIST P-256, the curve OpenPGP calls nistp256.

Clean up

The module sets prevent_destroy = true on both keys, so a plain destroy is refused — deliberately, because deleting an encryption key destroys the ability to read everything ever encrypted to it.

To tear down a tutorial identity, set the flag to false first:

module "identity" {
  # ...
  prevent_destroy = false
}

Then tofu apply to update the lifecycle rule, and tofu destroy. The keys enter a 30-day pending-deletion window rather than disappearing; you'll keep paying for them until it expires, and you can cancel with aws kms cancel-key-deletion if you change your mind.

Where to go next