Using AWS KMS RSA keys with Golang!

Posted on Mar 8, 2023
tl;dr:

Getting golang to talk to AWS/KMS RSA Keys

In short, there’s a few things to get done. To start with we need the Amazon C++ SDK to be able to provide the first layer. Then, we need a pkcs11 provider to talk to that SDK. After that, we need a golang library to be able to talk to that pkcs11 provider. 😀

Amazon Linux

Because I’m a sadist, I decided on using AWS Linux to make things as complicated as possible. To build stuff later on, you’ll need to yum install cmake3 package, as well as yum groupinstall "Development Tools".

AWS SDK for C++

Download it from https://github.com/aws/aws-sdk-cpp. Note you’ll need an instance or machine with >=4GB ram, otherwise the OOM killerclown will run amok.

This is pretty straightforward, you can follow the build instructions on the page. If you’re in a rush and know you only want the KMS functionality, then you can add -DBUILD_ONLY="kms" to the cmake command. A nice side effect of this is that you can build it on smaller systems too.

One caveat, the default will install the libraries into /usr/local/lib64, which on an Amazon Linux instance wasn’t in the default ld library path. I just added it all in manually but we’ll check in the next stage.

Once built, move onto the next component.

aws-kms-pkcs11

Now we’ve got the SDK installed, it’s time for something to talk to it and expose the interfaces via standard pkcs11 stuff. Again, not too tricky, follow the instructions at https://github.com/JackOfMostTrades/aws-kms-pkcs11.

Do the usual make install dance, and this at least will put the libraries somewhere sensible. Another gotcha, this installed the library on my AWS instance into /usr/lib64/pkcs11/aws_kms_pkcs11.so but all the documentation on that page refers to (I think) a debian install location of /usr/lib/x86_64-linux-gnu/pkcs11/aws_kms_pkcs11.so. Amend your exports, etc, accordingly.

At this point it’s worth running ldd aws_kms_pkcs11.so and checking everthing is resolved ok, in essence make sure it can find these two:

libaws-cpp-sdk-core.so => /usr/local/lib64/libaws-cpp-sdk-core.so (0x00007fe94b5b0000)
libaws-cpp-sdk-kms.so => /usr/local/lib64/libaws-cpp-sdk-kms.so (0x00007fe94b2c9000)

Otherwise, prepare for a world of pain.

At this point, we need to do some testing of it.

Obviously you will need to have pre-created a key in AWS KMS of a suitable type (let’s assume RSA going forward), and given permissions to your instance or some access credentials to be able to use it

As your user, you’ll need to create a config file for this library, example:

~/.config/aws-kms-pkcs11/config.json
<-- contains -->

{
  "slots": [
    {
      "label": "some-friendly-name",
      "kms_key_id": "some-uuid-from-kms",
      "aws_region": "eu-west-1"
    }
  ]
}

With that in place, we should be able to see if we can ssh-agent to pull it back via the following command, as a quick test:

[ec2-user@x build-static]$ eval `ssh-agent`
Agent pid 6755

[ec2-user@x build-static]$ ssh-add -s /usr/lib64/pkcs11/aws_kms_pkcs11.so
Enter passphrase for PKCS#11: (hit enter)
Card added: /usr/lib64/pkcs11/aws_kms_pkcs11.so

[ec2-user@x build-static]$ ssh-add -L
ssh-rsa AAAAB--SOMEKEY-- /usr/lib64/pkcs11/aws_kms_pkcs11.so

If you’ve got this far, great. We’re (almost) done - we just need to be able to get golang to talk to the pkcs11 library, which will talk to the aws-sdk, which will talk to kms…

For this, there’s the Crypto11 library.

Thales Crypto11

Source for this is at https://github.com/ThalesIgnite/crypto11. The documentation on this page makes no reference to supporting AWSKMS but given it’s only talking to the others via pkcs11 I figured it was worth a shot, and was pretty please when it actually worked.

Here’s an example program. Before running it, create a testfile, e.g. dd if=/dev/urandom of=testfile bs=1k count=256.

package main

import (
        "crypto"
        "crypto/rand"
        "crypto/rsa"
        "crypto/sha256"
        "crypto/x509"
        "encoding/pem"
        "fmt"
        "log"
        "os"

        "github.com/ThalesIgnite/crypto11"
)

func main() {
        config := &crypto11.Config{
                Path:       "/usr/lib64/pkcs11/aws_kms_pkcs11.so",  // make sure matches.
                TokenLabel: "changeme-matches-label-in-above-config",
                Pin:        "",
        }
        //fmt.Printf("Config: %#v", config)
        c, err := crypto11.Configure(config)
        if err != nil {
                log.Fatalf("%v", err)
        }

        key, err := c.FindKeyPair(nil, []byte("changeme-matches-label-in-above-config"))
        if err != nil {
                log.Fatalf("%v", err)
        }
        data, err := os.ReadFile("testfile")
        if err != nil {
                log.Fatalf("err: %v", err)
        }
        h := sha256.New()
        _, err = h.Write(data)
        if err != nil {
                log.Fatalf("err: %v", err)
        }

        dataHash := h.Sum([]byte{})

        sig, err := key.Sign(rand.Reader, dataHash, crypto.SHA256)
        if err != nil {
                log.Fatalf("err: %v", err)
        }
        // write out the sig to disk
        err = os.WriteFile("testfile.sig", sig, 0644)
        if err != nil {
                log.Fatalf("err: %v", err)
        }

        rsaPubkey := key.Public().(crypto.PublicKey).(*rsa.PublicKey)
        err = rsa.VerifyPKCS1v15(rsaPubkey, crypto.SHA256, dataHash, sig)
        if err != nil {
                log.Fatalf("err: %v", err)
        }
        // pretty print the pubkey, while we have it around...
        fmt.Printf("\n\nif no errors above, then it appears to have worked and signed with with this public key:\n\n")
        publicKeyBytes, _ := x509.MarshalPKIXPublicKey(rsaPubkey)
        pem.Encode(os.Stdout, &pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes})
        fmt.Printf("\nsave the key to the file testkey.pub, then")
        fmt.Printf("\n\nverify with: \n\topenssl dgst -sha256 -verify testkey.pub -signature testfile.sig testfile\n")
}

After that, check the sig on disk matches via the openssl command:

openssl dgst -sha256 -verify testkey.pub -signature testfile.sig testfile

Further development left as an exercise to the reader….