Sample procedure to encrypt AWS Access Secret Access Key using GCP Tink and a way to embed the the Key into an HSM device supporting PKCS #11, Trusted Platform Module and Hashicorp Vault
AWS secret key and ID can be thought of as a username/password and should be carefully managed, rotated, secured as described in Best practices for managing AWS access keys. However, if you need to invoke AWS from remote systems which do not provide ambient federation (eg on GCP using OIDC tokens), then you must either utilize an AWS credentials file or set them dynamically as an environment variable.
This repo provides four ways to protect the aws secret in various ways:
KMS
and access it via TINK
.HSM
an access it via PKCS11
TPM
an access it via go-tpm
Hashicorp Vault
an access it via Vault APIsusecases 2,3,4 are far more interesting (esp 3)
Basically for 2,3,4 above we will encode the key into hardware/sealed devices and then ‘ask’ the device to create HMAC signature compatible with AWS.
You maybe wondering: why can’t i embed an HMAC key into Google Cloud KMS
? Well, for some odd reason, HMAC-SHA256 keys on GCP KMS must have a length of 32 bytes…and our hmac key for aws is larger than that (on keysize for hmac, see hmac keysize testing). Why that weird limit is there; no clue
You can find the source here
In (1) you are using KMS to encrypt the Secret and save it in encrypted format. When you need to access the Secret to make it generate an AWS v4 signing request, the raw Secret is automatically decrypted by TINK using KMS and made to HMAC sign. The user will never need to “see” the secret but it is true that the key gets decoded in memory. You will also need to have access to KMS in the first place so this example is a bit convoluted. Use this if you already have access to another cloud providers KMS (eg GCP KMS), then use that decrypt the Key and then make it sign:
The encrypted Key would look like the following:
{
"encryptedKeyset": "CiUAmT+VVWAVKQZfFW6UheHPI1E3VmvTFlv2C4cspNaqpbxc8YvEEqUBACsKZVI1IW8U+86r2Yset0WOKwnggDitP0hi0oUapgOrF4W7Pklrbso93gfMoNDVw2QCWW4HwJwKzElQRi3zWHuL6NJP4t/t2VtIWORgWLz76zpH7+JWn6IrlqA/M4sammN0kAn+ZcgiG6kCvoMXzczUz3jzyk96Uz6U2LIuZb+bFaCasMYyka2fpSndMQ2SxpmHbVSe2AvhBVMLhM29LOcio41D",
"keysetInfo": {
"primaryKeyId": 2596996162,
"keyInfo": [
{
"typeUrl": "type.googleapis.com/google.crypto.tink.HmacKey",
"status": "ENABLED",
"keyId": 2596996162,
"outputPrefixType": "RAW"
}
]
}
}
In (2),(3),(4), you are embedding the HMAC key INTO an HSM. When you then need to access the secret, you ask the HSM to generate an HMAC for the AWS v4 signing process. At no time does the client ever see the secret after it is embedded: the actual HMAC is done within the HSM.
Once the key is embedded into an HSM, you can access it to sign, verify, etc but the key is ever exposed
$ pkcs11-tool --module /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --list-objects --pin mynewpin
Secret Key Object; unknown key algorithm 43
label: HMACKey
ID: 0100
Usage: verify
Access: sensitive
The big advantage of these modes is clear: the HSM owns the key and is not exportable: nobody will see the raw key once thats done but yet you can use it to create an AWS s4 sign.
The AWS S4 Signing protocol described shows something interesting: the raw AWS Secret is used in a chain to create any AWS compatible HMAC secret. On careful inspection, the only time the key is actually used is in the first step where a small prefix AWS4
is prefixed to the key and hashed with the current calendar date. The output of that key is chained through the rest of the signing.
This gives us an opportunity to seal the prefixed AWS Secret into hardware and then invoke the hardware’s HMAC system to kick off the processs.
Thats what we’ll do here
The implementations for the golang aws signer is here
This repo provides an AWS Credential and Signerv4 which is intended to be used standalone
Some references for TINK and PKCS11:
To use TINK, we will assume you are a GCP customer with access to GCP KMS and want to access AWS via v4 Signing
First create a KMS keychain,key for Symmetric Encryption:
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
export LOCATION=us-central1
export USER=`gcloud config get-value core/account`
# create keyring
gcloud kms keyrings create mykeyring --location $LOCATION
# create key
gcloud kms keys create key1 --keyring=mykeyring --purpose=encryption --location=$LOCATION
gcloud kms keys add-iam-policy-binding key1 \
--keyring mykeyring \
--location $LOCATION \
--member user:$USER \
--role roles/cloudkms.cryptoKeyDecrypter
Now create an EncryptedKeySet with Tink, then read in that KeySet and make Tink generate an HMAC signature:
export AWS_ACCESS_KEY_ID=AKIAUH3H6EGKERNFQLHJ
export AWS_SECRET_ACCESS_KEY=YRJ86SK5qTOZQzZTI1u-redacted
$ go run main.go --mode=tink \
--keyURI "projects/$PROJECT_ID/locations/$LOCATION/keyRings/mykeyring/cryptoKeys/key1" \
--awsRegion=us-east-2 -accessKeyID $AWS_ACCESS_KEY_ID \
-secretAccessKey $AWS_SECRET_ACCESS_KEY
2021/06/02 17:50:44 Create tink subtle.HMAC using secret
2021/06/02 17:50:44 Tink Keyset:
{
"encryptedKeyset": "CiUAmT+VVfqKvEJIXDR1j+kuMjx1fatYYcFmxmrPjtPMhD+p+/E8EqUBACsKZVKCnrihcBHGUoD2ql1CqLsMVzM2MnZkYrKalNdhxB7vUs3y3CScnnsdH+80cTSiVr8ybugaG7c4LKMDw4dB06ox8TS1YbB/hL5+W3IX2yOWPqyFN/t/RVe2QyjGQr7rPqQGM0gOJHDEyTdMX8a9YzG6D7sVM15QQmQtLwKkXNYWr2c0O8iRNRiBcV0ekFwouenMj7+VseyeN0m/dyHNM610",
"keysetInfo": {
"primaryKeyId": 2596996162,
"keyInfo": [
{
"typeUrl": "type.googleapis.com/google.crypto.tink.HmacKey",
"status": "ENABLED",
"keyId": 2596996162,
"outputPrefixType": "RAW"
}
]
}
}
2021/06/02 17:50:44 Constructing MAC with TINK
2021/06/02 17:50:44 Signed RequestURI:
2021/06/02 17:50:44 STS Response:
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::291738886548:user/svcacct1</Arn>
<UserId>AIDAUH3H6EGKDO36JYJH3</UserId>
<Account>291738886548</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>985a21aa-42b3-47af-ab12-9602de5f4a88</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
The relevant code that read in TINK is:
import (
"github.com/golang/protobuf/proto"
"github.com/google/tink/go/keyset"
"github.com/google/tink/go/mac/subtle"
common_go_proto "github.com/google/tink/go/proto/common_go_proto"
tinkpb "github.com/google/tink/go/proto/tink_go_proto"
"github.com/google/tink/go/tink"
hmacpb "github.com/google/tink/go/proto/hmac_go_proto"
"github.com/google/tink/go/core/registry"
"github.com/google/tink/go/integration/gcpkms"
hmacsigner "github.com/salrashid123/aws_hmac/aws"
hmaccred "github.com/salrashid123/aws_hmac/aws/credentials"
)
// register the backend KMS, in this case its GCP
gcpClient, err := gcpkms.NewClient("gcp-kms://")
registry.RegisterKMSClient(gcpClient)
backend, err := gcpClient.GetAEAD("gcp-kms://" + *keyURI)
// read the KeySetJSON as
var prettyJSON bytes.Buffer
error := json.Indent(&prettyJSON, buf.Bytes(), "", "\t")
// create credentials
cc, err = hmaccred.NewHMACCredential(&hmaccred.HMACCredentialConfig{
TinkConfig: hmaccred.TinkConfig{
KmsBackend: backend,
JSONBytes: prettyJSON.Bytes(),
},
AccessKeyID: *accessKeyID,
})
hs := hmacsigner.NewSigner()
body := strings.NewReader("")
sreq, err := http.NewRequest(http.MethodPost, "https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", body)
payloadHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
// Sign the request
hs.SignHTTP(ctx, *cc, sreq, payloadHash, "sts", "us-east-1", time.Now())
// use signedURL
sres, err := http.DefaultClient.Do(sreq)
For PKCS, we will use SoftHSM which supports the PKCS mechanism to use HMAC. You should be able to use other HSM like yubikey, etc but unfortunately, TPM’s CLi i used for PKCS does not support HMAC imports: see Support TPM HMAC Import.
Anyway, first install SoftHSM and confirm you have the library linked properly (i used linux)
$ ldd /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so
linux-vdso.so.1 (0x00007fff91bd7000)
libcrypto.so.1.1 => /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007f56ad1a1000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f56acfd4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f56ace0f000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f56acdf5000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f56acdef000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f56acdcd000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f56acc87000)
/lib64/ld-linux-x86-64.so.2 (0x00007f56ad589000)
Now initialize the SoftHSM device:
edit aws_hmac/softhsm/softhsm.conf
# set directories.tokendir = /path/to/aws_hmac/softhsm/tokens
export SOFTHSM2_CONF=`pwd`/softhsm/softhsm.conf
# initialize softHSM and list supported mechanisms
rm -rf /tmp/tokens
mkdir /tmp/tokens
pkcs11-tool --module /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot-index=0 --init-token --label="token1" --so-pin="123456"
pkcs11-tool --module /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --label="token1" --init-pin --so-pin "123456" --pin mynewpin
pkcs11-tool --module /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --list-objects --pin mynewpin
At this point, we are ready to run the sample application which will
note, after step2, the AWS secret is embedded inside the HSM and can only be used to make HMAC signatures.
export AWS_ACCESS_KEY_ID=AKIAUH3H6EGKERNFQLHJ
export AWS_SECRET_ACCESS_KEY=YRJ86SK5qTOZQzZTI1u-redacted
go run main.go --mode=pkcs \
--hsmLibrary /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so \
--awsRegion=us-east-2 -accessKeyID $AWS_ACCESS_KEY_ID \
-secretAccessKey $AWS_SECRET_ACCESS_KEY
The output of this will run STS.GetCallerIdentity
$ go run main.go --mode=pkcs --hsmLibrary /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so \
--awsRegion=us-east-2 -accessKeyID $AWS_ACCESS_KEY_ID \
-secretAccessKey $AWS_SECRET_ACCESS_KEY
Using slot with index 0 (0x0)
Token successfully initialized
Using slot 0 with a present token (0x42f8fd2b)
User PIN successfully initialized
bash: pcs11-tool: command not found
CryptokiVersion.Major 2
2021/06/02 20:19:01 Created HMAC Key: 2
2021/06/02 20:19:01 Signed RequestURI:
2021/06/02 20:19:01 STS Response:
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::291738886548:user/svcacct1</Arn>
<UserId>AIDAUH3H6EGKDO36JYJH3</UserId>
<Account>291738886548</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>b273ed3f-279f-43f8-af07-e97a04d0a1ef</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
The this code does is the magic:
// crate a credential which will use the HSM embedded AWS Secret:
cc, err = hmaccred.NewHMACCredential(&hmaccred.HMACCredentialConfig{
PKCSConfig: hmaccred.PKCSConfig{
Library: "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
Slot: 0,
Label: "HMACKey",
PIN: "mynewpin",
Id: id,
},
AccessKeyID: *accessKeyID,
})
// create a signer
ctx := context.Background()
hs := hmacsigner.NewSigner()
// create the request
body := strings.NewReader("")
sreq, err := http.NewRequest(http.MethodPost, "https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", body)
// sign the request
// $ touch empty.txt
// $ sha256sum empty.txt
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.txt
payloadHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
hs.SignHTTP(ctx, *cc, sreq, payloadHash, "sts", "us-east-1", time.Now())
// make the api call and print the result
sres, err := http.DefaultClient.Do(sreq)
sb, err := ioutil.ReadAll(sres.Body)
In this variation, you will embed the AWS HMAC key into a Trusted Platform Module (TPM). One embedded, the key will never leave the device can only be accessed to “sign” similar to the PKCS example above. To note, the TPM itself has a PKCS interface but at the moment, it does not support HMAC operations like import. See Issue #688. On the other end, go-tpm does not support pretty much any hmac operations: Issue 249
Usage:
One you installed go, just git clone this repo, export the env-vars and run
go run main.go -mode=tpm \
--awsRegion=us-east-2 -accessKeyID $AWS_ACCESS_KEY_ID \
-secretAccessKey $AWS_SECRET_ACCESS_KEY
What the script above does is
hmaccredentials
object in this repoNote, this example does an import of a key every time…you can ofcorse stop after step 6 and just run step 7,8 separately.
this bit bootstraps the tpm and the file handle:
// pHandle := tpmutil.Handle(0x81010002)
// err = tpm2.EvictControl(rwc, emptyPassword, tpm2.HandleOwner, newHandle, pHandle)
// if err != nil {
// fmt.Fprintf(os.Stderr,"Error persisting hash key %v\n", err)
// os.Exit(1)
// }
// defer tpm2.FlushContext(rwc, pHandle)
cc, err = hmaccred.NewHMACCredential(&hmaccred.HMACCredentialConfig{
TPMConfig: hmaccred.TPMConfig{
TpmDevice: *tpmPath,
TpmHandleFile: *hmacKeyHandle,
//TpmHandle: 0x81010002,
},
AccessKeyID: *accessKeyID,
})
$ go run main.go -mode=tpm --awsRegion=us-east-2 -accessKeyID $AWS_ACCESS_KEY_ID -secretAccessKey $AWS_SECRET_ACCESS_KEY
2021/06/10 13:55:51 Using Standard AWS v4Signer
2021/06/10 13:55:52 Response using AWS STS NewStaticCredentials and Standard v4.Singer
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::291738886548:user/svcacct1</Arn>
<UserId>AIDAUH3H6EGKDO36JYJH3</UserId>
<Account>291738886548</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>8bab0855-1536-4689-854a-10fe2fdd9500</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
Handle 0x80000000 flushed
======= ContextSave (newHandle) ========
2021/06/10 13:55:52 Signed RequestURI:
2021/06/10 13:55:52 STS Response:
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::291738886548:user/svcacct1</Arn>
<UserId>AIDAUH3H6EGKDO36JYJH3</UserId>
<Account>291738886548</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>fe67e9dd-1d58-4ace-9678-3021fd6e90eb</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
# tpm2_readpublic -c 0x81010002
name: 000b8fb81fa736a1cc6aae780f7ff5d32d6efc3b495ce1854af8aac9dc56dec287ee
qualified name: 000b23eba35005caaacdfa30defedc83a9c4986ed43be8728d544cbb93223d5b8045
name-alg:
value: sha256
raw: 0xb
attributes:
value: fixedtpm|fixedparent|userwithauth|sign
raw: 0x40052
type:
value: keyedhash
raw: 0x8
algorithm:
value: hmac
raw: 0x5
hash-alg:
value: sha256
raw: 0xb
keyedhash: 3d2733a76ef6723e5ddb7e1ab88eab0c5e9b2728606756334cc9639570b26cea
In this variation, you will embed the AWS HMAC
key into a Vault’s Transit Engine
Vault already has a secrets engine for AWS which returns temp AWS Access keys to you.
However, in this we are doing something different: we are going to embed an AWS Secret INTO vault and use Vault’s own transit hmac to sign the AWS request
Usage:
# add to /etc/hosts
# 127.0.0.1 vault.domain.com
# start vault
cd vault_resources
vault server -config=server.conf
# new window
export VAULT_ADDR='https://vault.domain.com:8200'
export VAULT_CACERT=/full/path/to/aws_hmac/vault_resources/ca.pem
vault operator init
# note down the UNSEAL_KEYS and the INITIAL_ROOT_TOKEN
## unseal using the various unsealing keys
vault operator unseal [$UNSEAL_KEYS]
export VAULT_TOKEN=$INITIAL_ROOT_TOKEN
vault secrets enable transit
## Create a temp transit key
export AWS_ACCESS_KEY_ID=AKIAUH3H6EGKF4ZY5GGQ
export AWS_SECRET_ACCESS_KEY=HMrL6cNwJCNQzRX8oN-redacted
## this is critical...prfix "AWS4" to the secret_access_key and use that as the hmac key to import
export IMPORT_AWS_SECRET_ACCESS_KEY=`echo -n "AWS4$AWS_SECRET_ACCESS_KEY" | base64`
echo $IMPORT_AWS_SECRET_ACCESS_KEY
Edit key_backup.json and specify the IMPORT_AWS_SECRET_ACCESS_KEY
as the hmac_key
value
"hmac_key": "QVdTNEhNckw2Y053SkNOUXpSWDhvTjNtbm8vamlLT-redacted",
Now import the key into vault as a backup (thats the only way to import a key, it seems)
export BACKUP_B64="$(cat key_backup.json | base64)"
vault write transit/restore/aws-key-1 backup="${BACKUP_B64}"
vault read transit/keys/aws-key-1
# check hmac works
echo -n "foo" | base64 | vault write transit/hmac/aws-key-1/sha2-256 input=-
Create a vault policy to test the signer (token_policy.hcl, secrets_policy.hcl)
vault policy write token-policy token_policy.hcl
vault policy write secrets-policy secrets_policy.hcl
# create a token with those policies (VAULT_TOKEN_FROM_POLICY)
vault token create -policy=token-policy -policy=secrets-policy
Now run the vault client application
# go run main.go -mode=vault --awsRegion=us-east-1 \
-accessKeyID $AWS_ACCESS_KEY_ID \
-secretAccessKey $AWS_SECRET_ACCESS_KEY \
-vaultToken=$VAULT_TOKEN_FROM_POLICY
2021/06/10 13:55:51 Using Standard AWS v4Signer
2021/06/10 13:55:52 Response using AWS STS NewStaticCredentials and Standard v4.Singer
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::291738886548:user/svcacct1</Arn>
<UserId>AKIAUH3H6EGKF4ZY5GGQ</UserId>
<Account>291738886548</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>8bab0855-1536-4689-854a-10fe2fdd9500</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
2021/06/10 13:55:52 Signed RequestURI:
2021/06/10 13:55:52 STS Response:
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::291738886548:user/svcacct1</Arn>
<UserId>AKIAUH3H6EGKF4ZY5GGQ</UserId>
<Account>291738886548</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>fe67e9dd-1d58-4ace-9678-3021fd6e90eb</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
What that just did is used Vaults transit engine to sign aws’s auentication request.
In this flow, the raw hmac was never exposed…only vault knew the key an used it for aws’s signature protocol…
THis is just a POC, caveat emptor
This site supports webmentions. Send me a mention via this form.