Receive your first encrypted report¶
By the end of this you will have published an OpenPGP certificate backed by two
KMS keys, had gpg encrypt a report to it, and read that report back — with no
private key ever existing in your process.
This is the whole capability, end to end, in about twenty minutes.
Before you start¶
You need:
- an AWS account you are willing to create two KMS keys in, and credentials for
it (
aws sts get-caller-identityshould work); - Go 1.26 or later;
gpg, to play the part of the researcher sending you something.
Two KMS keys cost about $2/month combined. Delete them at the end if this is only an experiment — the last section says how.
Step 1 — create the keys¶
Two keys, because an agreement key cannot sign its own binding signature. If that sounds arbitrary, why two keys explains it.
ENCRYPT_KEY=$(aws kms create-key \
--key-usage KEY_AGREEMENT --key-spec ECC_NIST_P256 \
--description 'tutorial: OpenPGP encryption subkey' \
--query KeyMetadata.KeyId --output text)
CERTIFY_KEY=$(aws kms create-key \
--key-usage SIGN_VERIFY --key-spec RSA_4096 \
--description 'tutorial: OpenPGP certification primary' \
--query KeyMetadata.KeyId --output text)
echo "encrypt=$ENCRYPT_KEY certify=$CERTIFY_KEY"
Write those down; you will need them in a moment.
The RSA key takes a few seconds to become available. If step 3 fails with a key state error, wait and try again.
Step 2 — set up a module¶
mkdir first-report && cd first-report
go mod init first-report
go get gitlab.com/phpboyscout/go/encryption
go get gitlab.com/phpboyscout/go/encryption-aws-kms
go get github.com/aws/aws-sdk-go-v2/config
Step 3 — assemble and publish the certificate¶
Create main.go:
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"fmt"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/kms"
"gitlab.com/phpboyscout/go/encryption/certificate"
kmsenc "gitlab.com/phpboyscout/go/encryption-aws-kms"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "failed:", err)
os.Exit(1)
}
}
func run() error {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return err
}
client := kms.NewFromConfig(cfg)
encryptKey, certifyKey := os.Getenv("ENCRYPT_KEY"), os.Getenv("CERTIFY_KEY")
// Fixed, not time.Now(). It is hashed into the fingerprint, and the
// fingerprint is bound into every message sent to this certificate.
created := time.Unix(1_700_000_000, 0).UTC()
signer, err := kmsenc.NewSigner(ctx, client, client, certifyKey)
if err != nil {
return err
}
deriver, err := kmsenc.NewDeriverForKey(ctx, client, client, encryptKey)
if err != nil {
return err
}
// The subkey's public point, in the uncompressed form a packet carries.
out, err := client.GetPublicKey(ctx, &kms.GetPublicKeyInput{KeyId: &encryptKey})
if err != nil {
return err
}
parsed, err := x509.ParsePKIXPublicKey(out.PublicKey)
if err != nil {
return err
}
ec := parsed.(*ecdsa.PublicKey)
point := elliptic.Marshal(deriver.Curve(), ec.X, ec.Y)
der, err := certificate.Certificate{
UserID: "Tutorial Security <security@example.invalid>",
Created: created,
Subkey: certificate.ECDHPublicKey{
Created: created,
CurveOID: []byte{0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07},
Point: point,
HashID: 8, // SHA-256
KEKAlgID: 7, // AES-128
},
}.Assemble(signer.WithContext(ctx))
if err != nil {
return err
}
fmt.Printf("assembled %d octets, two kms:Sign calls, no private key here\n", len(der))
return os.WriteFile("certificate.pgp", der, 0o600)
}
Run it:
You now have certificate.pgp — a publishable OpenPGP certificate whose private
halves are both inside KMS.
Step 4 — be the researcher¶
Use a throwaway keyring so nothing touches your real one:
That gpg accepted it is already meaningful: gpg validates self-signatures on import and silently drops what does not verify.
Now send yourself a report:
echo 'XSS in the profile editor, PoC attached' > report.txt
gpg --batch --yes --trust-model always --encrypt \
--recipient security@example.invalid \
--output report.pgp report.txt
Check what your certificate told gpg to use:
9 is AES-256, and gpg picks the first algorithm it supports — so that is what
your report was encrypted with. Without those preferences gpg would have used
AES-128, silently. See what a certificate claims.
The message itself will not tell you: the algorithm is inside the encrypted
session key, so --list-packets report.pgp reports No secret key. You will
see the algorithm in step 5, when the key service opens it.
Step 5 — read it back¶
Opening the encrypted body is a full OpenPGP concern — symmetric modes,
integrity protection, compression — which this module deliberately leaves to a
mature implementation. sigillum wires the two together, so use it:
go install gitlab.com/phpboyscout/sigillum/cmd/sigillum@latest
sigillum decrypt \
--certificate certificate.pgp \
--key "$ENCRYPT_KEY" \
report.pgp
The report prints, and the algorithm it was encrypted under is the one your certificate asked for.
If you would rather stay in Go, the pieces are
encryption.ParsePacket, certificate.Parse and encryption.SessionKey — the
deriver you built in step 3 supplies the one secret operation. That is exactly
what the command does.
What just happened¶
Three KMS calls did all the secret work: two kms:Sign to build the
certificate, one kms:DeriveSharedSecret to open the message. Everything
else — the key derivation, the AES key unwrap, the packet framing, the body
decryption — happened locally on data anyone is allowed to see.
At no point did a private key exist in your process. There is no code path that could have exported one, because KMS offers no API that would allow it.
Clean up¶
gpgconf --kill all && rm -rf "$GNUPGHOME"
aws kms schedule-key-deletion --key-id "$ENCRYPT_KEY" --pending-window-in-days 7
aws kms schedule-key-deletion --key-id "$CERTIFY_KEY" --pending-window-in-days 7
Seven days is the minimum window. Cancel with cancel-key-deletion if you
change your mind.
Next¶
- Provision the keys properly, with the reader and certifier roles split.
- The role split — why the credential that reads reports must not be able to issue certificates.