diff --git a/Makefile b/Makefile index 0a1ccab..e18be2f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ build/bin/aws_signing_helper: go build -buildmode=pie -ldflags "-X 'github.com/aws/rolesanywhere-credential-helper/cmd.Version=${VERSION}' -linkmode=external -w -s" -trimpath -o build/bin/aws_signing_helper main.go .PHONY: clean -clean: +clean: test-clean rm -rf build # Setting up SoftHSM for PKCS#11 tests. @@ -25,9 +25,156 @@ ECCERTS := $(foreach digest, sha1 sha256 sha384 sha512, $(patsubst %-key.pem, %- RSACERTS := $(foreach digest, md5 sha1 sha256 sha384 sha512, $(patsubst %-key.pem, %-$(digest)-cert.pem, $(RSAKEYS))) PKCS12CERTS := $(patsubst %-cert.pem, %.p12, $(RSACERTS) $(ECCERTS)) -# It's hard to do a file-based rule for the contents of the SoftHSM token. +# Software TPM. For generating keys/certs, we run the swtpm in TCP mode, +# because that's what the tools and the OpenSSL ENGINE require. Each of +# the rules which might need the swtpm will ensure that it's running by +# invoking $(START_SWTPM_TCP). The 'test-certs:; rule will then *stop* it +# by running $(STOP_SWTPM_TCP), after all the certs and keys have been +# created. +# +# For the actual test, we need it to run in UNIX socket mode, since +# *that* is all that go-tpm can cope with. So we start it in that mode +# in the 'test:' (or 'test-tpm-signer:') recipe(s), and stop it again +# afterwards. +SWTPM_STATEDIR := $(curdir)/tst/swtpm +SWTPM_CTRLSOCK := $(curdir)/tst/swtpm-ctrl +SWTPM_SERVSOCK := $(curdir)/tst/swtpm-serv +SWTPM := swtpm socket --tpm2 --tpmstate dir=$(SWTPM_STATEDIR) +TABRMD_NAME := com.intel.tss2.Tabrmd2321 + +# Annoyingly, while we only support UNIX socket, the ENGINE only supports TCP. +SWTPM_UNIX := --server type=unixio,path=$(SWTPM_SERVSOCK) --ctrl type=unixio,path=$(SWTPM_CTRLSOCK) +SWTPM_NET := --server type=tcp,port=2321 --ctrl type=tcp,port=2322 + +SWTPM_PREFIX := TPM2TOOLS_TCTI=tabrmd:bus_name="$(TABRMD_NAME)",bus_type=session \ + TPM2OPENSSL_TCTI=tabrmd:bus_name="$(TABRMD_NAME)",bus_type=session + +# Check that the swtpm is running for TCP connections. This isn't a normal +# phony rule because we don't want it running unless there's actually some +# work to be done over the TCP socket (creating keys, certs, etc.). +START_SWTPM_TCP := \ + if ! swtpm_ioctl --tcp 127.0.0.1:2322 -g >/dev/null 2>/dev/null; then \ + mkdir -p $(SWTPM_STATEDIR); \ + $(SWTPM) $(SWTPM_NET) --flags not-need-init,startup-clear -d; \ + ( tpm2-abrmd --session --dbus-name="$(TABRMD_NAME)" --tcti "swtpm:host=localhost,port=2321" & ); \ + $(SWTPM_PREFIX) tpm2_dictionarylockout -s -t 0; \ + fi +STOP_SWTPM_TCP := swtpm_ioctl --tcp 127.0.0.1:2322 -s && kill $$(pgrep tpm2-abrmd) + +# This one is used for the actual test run +START_SWTPM_UNIX := \ + if ! swtpm_ioctl --unix $(SWTPM_CTRLSOCK) -g >/dev/null 2>/dev/null; then \ + $(SWTPM) $(SWTPM_UNIX) --flags not-need-init,startup-clear -d; \ + fi +STOP_SWTPM_UNIX := swtpm_ioctl --unix $(SWTPM_CTRLSOCK) -s + +$(certsdir)/tpm-sw-rsa-key.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) ./create_tpm2_key.sh -r $@ + +$(certsdir)/tpm-sw-rsa-key-with-pw.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) ./create_tpm2_key.sh -r -k 1234 $@ + +$(certsdir)/tpm-sw-ec-prime256-key.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) ./create_tpm2_key.sh -e prime256v1 $@ + +$(certsdir)/tpm-sw-ec-prime256-key-with-pw.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) ./create_tpm2_key.sh -e prime256v1 -k 1234 $@ + +$(certsdir)/tpm-sw-ec-secp384r1-key.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) ./create_tpm2_key.sh -e secp384r1 $@ + +$(certsdir)/tpm-sw-ec-secp384r1-key-with-pw.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) ./create_tpm2_key.sh -e secp384r1 -k 1234 $@ + +$(certsdir)/tpm-sw-loaded-81000101-ec-secp384r1-key.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) tpm2_createprimary -c parent.ctx + $(SWTPM_PREFIX) tpm2_create -C parent.ctx -u child.pub -r child.priv + $(SWTPM_PREFIX) tpm2_load -C parent.ctx -u child.pub -r child.priv -c child.ctx + $(SWTPM_PREFIX) tpm2_evictcontrol -c child.ctx 0x81000101 + rm parent.ctx child.pub child.priv child.ctx + +$(certsdir)/tpm-sw-loaded-81000102-ec-secp384r1-key-with-pw.pem: + $(START_SWTPM_TCP) + $(SWTPM_PREFIX) tpm2_createprimary -c parent.ctx + $(SWTPM_PREFIX) tpm2_create -C parent.ctx -u child.pub -r child.priv -p 1234 + $(SWTPM_PREFIX) tpm2_load -C parent.ctx -u child.pub -r child.priv -c child.ctx + $(SWTPM_PREFIX) tpm2_evictcontrol -c child.ctx 0x81000102 + rm parent.ctx child.pub child.priv child.ctx + +# Create a persistent key at 0x81000001 in the owner hierarchy, if it +# doesn't already exist. And a PEM key with that as its parent. +$(certsdir)/tpm-sw-ec-81000001-key.pem: + $(START_SWTPM_TCP) + if ! $(SWTPM_PREFIX) tpm2_readpublic -c 0x81000001; then \ + $(SWTPM_PREFIX) tpm2_createprimary -G rsa -c parent.ctx && \ + $(SWTPM_PREFIX) tpm2_evictcontrol -c parent.ctx 0x81000001; \ + fi + $(SWTPM_PREFIX) openssl genpkey -provider tpm2 -algorithm EC -pkeyopt group:prime256v1 -pkeyopt parent:0x81000001 -out $@ + +$(certsdir)/tpm-sw-ec-81000001-key-with-pw.pem: + $(START_SWTPM_TCP) + if ! $(SWTPM_PREFIX) tpm2_readpublic -c 0x81000001; then \ + $(SWTPM_PREFIX) tpm2_createprimary -G rsa -c parent.ctx && \ + $(SWTPM_PREFIX) tpm2_evictcontrol -c parent.ctx 0x81000001; \ + fi + $(SWTPM_PREFIX) openssl genpkey -provider tpm2 -algorithm EC -pkeyopt group:prime256v1 -pkeyopt parent:0x81000001 -pkeyopt user-auth:1234 -out $@ + +# Create RSA keys with the Sign capability +$(certsdir)/tpm-sw-rsa-81000001-sign-key.pem: + if ! $(SWTPM_PREFIX) tpm2_readpublic -c 0x81000001; then \ + $(SWTPM_PREFIX) tpm2_createprimary -G rsa -c parent.ctx && \ + $(SWTPM_PREFIX) tpm2_evictcontrol -c parent.ctx 0x81000001; \ + fi + $(SWTPM_PREFIX) openssl genpkey -provider tpm2 -algorithm RSA -pkeyopt parent:0x81000001 -out $@ + +$(certsdir)/tpm-sw-rsa-81000001-sign-key-with-pw.pem: + if ! $(SWTPM_PREFIX) tpm2_readpublic -c 0x81000001; then \ + $(SWTPM_PREFIX) tpm2_createprimary -G rsa -c parent.ctx && \ + $(SWTPM_PREFIX) tpm2_evictcontrol -c parent.ctx 0x81000001; \ + fi + $(SWTPM_PREFIX) openssl genpkey -provider tpm2 -algorithm RSA -pkeyopt parent:0x81000001 -pkeyopt user-auth:1234 -out $@ + +SWTPM_LOADED_KEYS_WO_PW := $(certsdir)/tpm-sw-loaded-81000101-ec-secp384r1-key.pem +SWTPM_LOADED_KEYS_W_PW := $(certsdir)/tpm-sw-loaded-81000102-ec-secp384r1-key-with-pw.pem +SWTPMKEYS_WO_PW_WO_SIGN_CAP := $(certsdir)/tpm-sw-rsa-key.pem +SWTPMKEYS_WO_PW := $(certsdir)/tpm-sw-ec-secp384r1-key.pem $(certsdir)/tpm-sw-ec-prime256-key.pem $(certsdir)/tpm-sw-rsa-81000001-sign-key.pem +SWTPMKEYS_W_PW := $(patsubst %.pem, %-with-pw.pem, $(SWTPMKEYS_WO_PW)) $(certsdir)/tpm-sw-ec-81000001-key.pem $(certsdir)/tpm-sw-ec-81000001-key-with-pw.pem $(certsdir)/tpm-sw-rsa-81000001-sign-key-with-pw.pem +SWTPMKEYS := $(SWTPMKEYS_WO_PW) $(SWTPMKEYS_W_PW) $(SWTPMKEYS_WO_PW_WO_SIGN_CAP) +SWTPM_LOADED_KEY_CERTS := $(foreach digest, sha1 sha256 sha384 sha512, $(patsubst %-key.pem, %-$(digest)-cert.pem, $(SWTPM_LOADED_KEYS_WO_PW))) +SWTPMCERTS := $(foreach digest, sha1 sha256 sha384 sha512, $(patsubst %-key.pem, %-$(digest)-cert.pem, $(SWTPMKEYS_WO_PW))) + +HWTPMKEYS_WO_PW := $(certsdir)/tpm-hw-rsa-key.pem $(certsdir)/tpm-hw-ec-key.pem $(certsdir)/tpm-hw-ec-81000001-key.pem +HWTPMKEYS_W_PW := $(patsubst %.pem, %-with-pw.pem, $(HWTPMKEYS_WO_PW)) +HWTPMKEYS := $(HWTPMKEYS_WO_PW) $(HWTPMKEYS_W_PW) +HWTPMCERTS := $(foreach digest, sha1 sha256 sha384 sha512, $(patsubst %-key.pem, %-$(digest)-cert.pem, $(HWTPMKEYS_WO_PW))) + +# User can test on hardware TPM with `make TPM_DEVICE=/dev/tpmrm0 test` +ifeq ($(TPM_DEVICE),) +TPM_DEVICE := $(SWTPM_SERVSOCK) +TPMKEYS := $(SWTPMKEYS) +TPMCERTS := $(SWTPMCERTS) +TPMLOADEDKEY_CERTS := $(SWTPM_LOADED_KEY_CERTS) +START_SWTPM := $(START_SWTPM_UNIX) +STOP_SWTPM := $(STOP_SWTPM_UNIX) +else +TPMKEYS := $(HWTPMKEYS) +TPMCERTS := $(HWTPMCERTS) +START_SWTPM := true +STOP_SWTPM := true +endif + +export TPM_DEVICE + +# It's hard to ao a file-based rule for the contents of the SoftHSM token. # So just populate it as a side-effect of creating the softhsm2.conf file. -tst/softhsm2.conf: tst/softhsm2.conf.template $(PKCS8KEYS) $(RSACERTS) $(ECCERTS) tst/certs/rsa-2048-2-sha256-cert.pem +tst/softhsm2.conf: tst/softhsm2.conf.template $(PKCS8KEYS) $(RSACERTS) $(ECCERTS) rm -rf tst/softhsm/* sed 's|@top_srcdir@|${curdir}|g' $< > $@.tmp $(SHM2_UTIL) --show-slots @@ -59,31 +206,40 @@ tst/softhsm2.conf: tst/softhsm2.conf.template $(PKCS8KEYS) $(RSACERTS) $(ECCERTS .PHONY: test test: test-certs tst/softhsm2.conf - SOFTHSM2_CONF=$(curdir)/tst/softhsm2.conf go test -v ./... - -%-md5-cert.pem: %-key.pem - SUBJ=$$(echo "$@" | sed -r 's|.*/([^/]+)-cert.pem|\1|'); \ - openssl req -x509 -new -key $< -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -md5 -%-sha1-cert.pem: %-key.pem - SUBJ=$$(echo "$@" | sed -r 's|.*/([^/]+)-cert.pem|\1|'); \ - openssl req -x509 -new -key $< -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -sha1 -%-sha256-cert.pem: %-key.pem - SUBJ=$$(echo "$@" | sed -r 's|.*/([^/]+)-cert.pem|\1|'); \ - openssl req -x509 -new -key $< -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -sha256 -%-2-sha256-cert.pem: %-key.pem - SUBJ=$$(echo "$@" | sed -r 's|.*/([^/]+)-cert.pem|\1|'); \ - openssl req -x509 -new -key $< -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -sha256 -%-sha384-cert.pem: %-key.pem - SUBJ=$$(echo "$@" | sed -r 's|.*/([^/]+)-cert.pem|\1|'); \ - openssl req -x509 -new -key $< -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -sha384 -%-sha512-cert.pem: %-key.pem - SUBJ=$$(echo "$@" | sed -r 's|.*/([^/]+)-cert.pem|\1|'); \ - openssl req -x509 -new -key $< -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -sha512 + $(START_SWTPM) + SOFTHSM2_CONF=$(curdir)/tst/softhsm2.conf go test -v ./... || : + $(STOP_SWTPM) + +TPMCOMBOS := $(patsubst %-cert.pem, %-combo.pem, $(TPMCERTS)) + +.PHONY: test-tpm-signer +test-tpm-signer: $(certsdir)/cert-bundle.pem $(TPMKEYS) $(TPMCERTS) $(TPMLOADEDKEY_CERTS) $(TPMCOMBOS) + $(STOP_SWTPM_TCP) 2>/dev/null || : + $(START_SWTPM) + go test ./... -run "TPM" + $(STOP_SWTPM) + +define CERT_RECIPE + @SUBJ=$$(echo "$@" | sed 's^\(.*/\)\?\([^/]*\)-cert.pem^\2^'); \ + [ "$${SUBJ#tpm-}" != "$${SUBJ}" ] && ENG="-provider tpm2 -provider default -propquery '?provider=tpm2'"; \ + if [ "$${SUBJ#tpm-sw-}" != "$${SUBJ}" ]; then $(START_SWTPM_TCP); TPM_PREFIX="$(SWTPM_PREFIX)"; fi; \ + if echo $< | grep -q "loaded"; then KEY=handle:0x$(word 4, $(subst -, , $<)); else KEY=$<; fi; \ + echo $${TPM_PREFIX} openssl req -x509 -new $${ENG} -key $${KEY} -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -$${SUBJ##*-}; \ + eval $${TPM_PREFIX} openssl req -x509 -new $${ENG} -key $${KEY} -out $@ -days 10000 -subj "/CN=roles-anywhere-$${SUBJ}" -$${SUBJ##*-}; +endef + +%-md5-cert.pem: %-key.pem; $(CERT_RECIPE) +%-sha256-cert.pem: %-key.pem; $(CERT_RECIPE) +%-sha1-cert.pem: %-key.pem; $(CERT_RECIPE) +%-sha384-cert.pem: %-key.pem; $(CERT_RECIPE) +%-sha512-cert.pem: %-key.pem; $(CERT_RECIPE) + +%-combo.pem: %-cert.pem + KEY=$$(echo "$@" | sed 's/-[^-]*-combo.pem/-key.pem/'); \ + cat $${KEY} $< > $@.tmp && mv $@.tmp $@ # Go PKCS#12 only supports SHA1 and 3DES!! -%.p12: %-pass.p12 - echo Creating $@... - ls -l $< +%.p12: %-cert.pem KEY=$$(echo "$@" | sed 's/-[^-]*\.p12/-key.pem/'); \ CERT=$$(echo "$@" | sed 's/.p12/-cert.pem/'); \ openssl pkcs12 -export -passout pass: -macalg SHA1 \ @@ -105,6 +261,32 @@ test: test-certs tst/softhsm2.conf %-pkcs8.pem: %.pem openssl pkcs8 -topk8 -inform PEM -outform PEM -in $< -out $@ -nocrypt +$(certsdir)/tpm-hw-rsa-key.pem: + ./create_tpm2_key.sh -r $@ + +$(certsdir)/tpm-hw-rsa-key-with-pw.pem: + ./create_tpm2_key.sh -r -k 1234 $@ + +$(certsdir)/tpm-hw-ec-key.pem: + ./create_tpm2_key.sh -e prime256v1 $@ + +$(certsdir)/tpm-hw-ec-key-with-pw.pem: + ./create_tpm2_key.sh -e prime256v1 -k 1234 $@ + +$(certsdir)/tpm-hw-ec-81000001-key.pem: + if ! tpm2_readpublic -c 0x81000001; then \ + tpm2_createprimary -G rsa -c parent.ctx && \ + tpm2_evictcontrol -c parent.ctx 0x81000001; \ + fi + openssl genpkey -provider tpm2 -algorithm EC -pkeyopt group:prime256v1 -pkeyopt parent:0x81000001 -out $@ + +$(certsdir)/tpm-hw-ec-81000001-key.pem: + if ! tpm2_readpublic -c 0x81000001; then \ + tpm2_createprimary -G rsa -c parent.ctx && \ + tpm2_evictcontrol -c parent.ctx 0x81000001; \ + fi + openssl genpkey -provider tpm2 -algorithm EC -pkeyopt group:prime256v1 -pkeyopt parent:0x81000001 -pkeyopt user-auth:1234 -out $@ + $(RSAKEYS): KEYLEN=$$(echo "$@" | sed 's/.*rsa-\([0-9]*\)-key.pem/\1/'); \ openssl genrsa -out $@ $${KEYLEN} @@ -122,16 +304,25 @@ $(certsdir)/cert-bundle-with-comments.pem: $(RSACERTS) $(ECCERTS) echo "Comment in bundle\n" >> $@; \ done +KEYS := $(RSAKEYS) $(ECKEYS) $(TPMKEYS) $(PKCS8KEYS) +CERTS := $(RSACERTS) $(ECCERTS) $(TPMCERTS) +COMBOS := $(patsubst %-cert.pem, %-combo.pem, $(CERTS)) + .PHONY: test-certs -test-certs: $(PKCS8KEYS) $(RSAKEYS) $(ECKEYS) $(RSACERTS) $(ECCERTS) $(PKCS12CERTS) $(certsdir)/cert-bundle.pem $(certsdir)/cert-bundle-with-comments.pem tst/softhsm2.conf +test-certs: $(KEYS) $(CERTS) $(COMBOS) $(PKCS12CERTS) $(certsdir)/cert-bundle.pem $(certsdir)/cert-bundle-with-comments.pem tst/softhsm2.conf + $(STOP_SWTPM_TCP) 2>/dev/null || : .PHONY: test-clean test-clean: - rm -f $(RSAKEYS) $(ECKEYS) + rm -f $(RSAKEYS) $(ECKEYS) $(HWTPMKEYS) rm -f $(PKCS8KEYS) - rm -f $(RSACERTS) $(ECCERTS) - rm -f $(PKCS12CERTS) + rm -f $(RSACERTS) $(ECCERTS) $(HWTPMCERTS) + rm -f $(PKCS12CERTS) $(COMBOS) rm -f $(certsdir)/cert-bundle.pem rm -f $(certsdir)/cert-with-comments.pem rm -f tst/softhsm2.conf rm -rf tst/softhsm/* + $(STOP_SWTPM_TCP) || : + $(STOP_SWTPM_UNIX) || : + rm -rf $(SWTPMKEYS) $(SWTPMCERTS) $(SWTPM_TMPKEYS) $(SWTPM_STATEDIR) + diff --git a/README.md b/README.md index 334caf0..43779f9 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,6 @@ make release After building, you should see the `aws_signing_helper` binary built for your system at `build/bin/aws_signing_helper`. Usage can be found in [AWS's documentation](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/credential-helper.html). A later section also goes into how you can use the scripts provided in this repository to test out the credential helper binary. -### Scripts - -The project also comes with two bash scripts at its root, called `generate-certs.sh` and `generate-credential-process-data.sh`. The former script is used strictly for unit testing, and it generates certificate and private key data with different parameters that are supported by IAM Roles Anywhere. You can run the bash script using `/bin/bash generate-certs.sh`, and you will see the generated certificates and keys under the `tst/certs` directory. The latter script is used both for unit testing and can also be used for testing the `credential-process` command after having built the binary. It will create a CA certificate/private key as well as a leaf certificate/private key. When testing IAM Roles Anywhere, you will have to upload the CA certificate a trust anchor and create a profile within Roles Anywhere before using the binary along with the leaf certificate/private key to call `credential-process` (more instructions can be found in the next section). You can run the bash script using `/bin/bash generate-credential-process-data.sh`, and you will see the generated certificate hierarchy (and corresponding keys) under the `credential-process-data` directory. Note that the unit tests that require these fixtures to exist will run the bash script themselves, before executing those tests that depend on the fixtures existing. Please note that these scripts currently only work on Unix-based systems and require `openssl` to be installed. - ## Diagnostic Command Tools ### read-certificate-data @@ -184,6 +180,133 @@ The searching methodology used to find objects within PKCS#11 tokens can largely that there are some slight differences in how objects are found in the credential helper application. +#### TPMv2 Integration + +Private key files containing a TPM wrapped key in the `-----BEGIN TSS2 PRIVATE KEY-----` +form as described [here](https://www.hansenpartnership.com/draft-bottomley-tpm2-keys.html) +are transparently supported. You can just use such a file as you would any plain key +file and expect it to work, just as you should expect with any well-behaved application. + +These files are supported, and can be created by, both TPMv2 OpenSSL engines/providers, and GnuTLS. + +Note that some features of the TSS private key format are not yet supported. Some or all +of these may be implemented in future versions. In some semblance of the order in which +they're likely to be added: + * Password authentication on parent keys (and hierarchies) + as a parent + * Importable keys + * TPM Policy / AuthPolicy + * Sealed keys + +Note that it is possible to get around the parent key password authentication limit by loading +the signing key (the loading process will have to be done with other tools and will require +you to provivde your parent key password) into the TPM and referencing its handle in the command +you want to call with the credential helper. + +##### Testing +Currently, unit tests for testing TPM support are written in such a way that TPM keys that are used +for testing are either bound to a hardware TPM, or are bound to a software TPM. For software TPM +testing, `swtpm` is used. You can find the repository [here](https://github.com/stefanberger/swtpm). +Also, to create the keys and certificates that are required for unit testing, you will need to install +the [Intel TSS](https://github.com/tpm2-software/tpm2-tss), +[Intel OpenSSL provider](https://github.com/tpm2-software/tpm2-openssl), [`tpm2-tools`](https://github.com/tpm2-software/tpm2-tools), + and the [`tpm2-tabrmd`](https://github.com/tpm2-software/tpm2-abrmd) (resource manager). + +Once you've installed all the dependencies (which should be available on many Linux distributions +through standard package managers), you can run just the unit tests related to TPM support +through `make test-tpm-signer`. Note that `swtpm` will have to be run in UNIX socket mode (it can't +be run in TCP socket mode) for the tests since that is all `go-tpm` can cope with. But key and +certificate fixtures will be created when `swtpm` is running in TCP socket mode (as a part of the +appropriate `Makefile` targets). Afterwards, right before the unit tests are run, we switch `swtpm` +over to run in UNIX socket mode. + +Also, for the sake of testing, a small script is included that emulates a subset of the functionality +that can be achieved with `create_tpm2_key`, a utility program that comes with the +[IBM OpenSSL ENGINE](https://git.kernel.org/pub/scm/linux/kernel/git/jejb/openssl_tpm2_engine.git/). It +is used to test TPM key files that include a permanent handle as their parent. + +##### Notes on Tooling Used + +`tpm2-tools` and `tpm2-openssl` will by default create RSA keys that have the sign attribute, but that may +not be the case for other tools that you may find. As an example, the tools that come with the IBM OpenSSL +ENGINE will create RSA keys with the decrypt attribute but not the sign attribute by default. In order to +be able to use an RSA key with the credential helper it must have the sign attribute set. The credential +helper will delegate the signing operation to the TPM as opposed to using a raw RSA decrypt and deriving +the signature by implementing PKCS#1 v1.5 padding. + +##### Guidance +If you haven't already initialized your TPM's owner hierarchy yet, it is recommended that you configure +it with a password that has high entropy, as there are no dictionary attack protections for it. + +Once you have initialized the TPM's owner hierarchy, you can create a primary key in it. You can do so +using one of the utility programs that comes with `tpm2-tools`: + +``` +tpm2_createprimary -G rsa -g sha256 -p ${TPM_PRIMARY_KEY_PASSWORD} -c parent.ctx -P ${OWNER_HIERARCHY_PASSWORD} +``` + +This will create a primary key in the TPM owner hierarchy, with a key password of +`${TPM_PRIMARY_KEY_PASSWORD}`. If the owner hierarchy in your TPM doesn't have a password (not recommended) +you can omit the `-P` option in the above command. + +Next, you can create a child key with the primary you just created as its parent: +``` +tpm2_create -C parent.ctx -u child.pub -r child.priv -P ${TPM_PRIMARY_KEY_PASSWORD} -p ${TPM_CHILD_KEY_PASSWORD} +``` + +Next, load the child key that was just created into the TPM as a transient object: +``` +tpm2_load -C parent.ctx -u child.pub -r child.priv -c child.ctx -P ${TPM_PRIMARY_KEY_PASSWORD} +``` + +Afterwards, make the transient object that is the child key into a persistent one and save its handle: +``` +CHILD_HANDLE=$(tpm2_evictcontrol -c child.ctx | cut -d ' ' -f 2 | head -n 1) +``` + +Then, you can create a CSR, using the [`tpm2-openssl`](https://github.com/tpm2-software/tpm2-openssl) OpenSSL +provider. +``` +openssl req -provider tpm2 -provider default -propquery '?provider=tpm2' \ + -new -key handle:${CHILD_HANDLE} \ + -out client-csr.pem +``` + +Note that the above will prompt you for your password (`TPM_CHILD_KEY_PASSWORD`). + +Lastly, once you have your CSR, you can provide it to a CA so that it can issue a client certificate for +you. The client certificate and TPM key can then be used with the credential helper application as follows: +``` +/path/to/aws_signing_helper credential-process \ + --certificate /path/to/certificate/file \ + --private-key handle:${CHILD_HANDLE} \ + --role-arn ${ROLE_ARN} \ + --trust-anchor-arn ${TA_ARN} \ + --profile-arn ${PROFILE_ARN} +``` + +Please note that with this approach, it is your responsibility for clearing out the persistent and +temporary objects from the TPM after you no longer need them, so that they can't be used by others +on the same machine to escalate their privilege. + +The alternative is to use a TPM key PEM file in the format described +[here](https://www.hansenpartnership.com/draft-bottomley-tpm2-keys.html), for use with the credential +helper. If a TPM key file is used, the wrapped private key within the key file will be loaded into the +TPM as a transient object and automatically flushed from the TPM after use by the credential helper (so +after signing). If signing needs to occur multiple times, the key will be loaded into the TPM each +time. The limitation with this approach is that the parent of the signing key can't be password-protected, +as there is no way currently for you to pass this password to the credential helper. + +Below is an example of how you can use the credential helper with a TPM key file: +``` +/path/to/aws_signing_helper credential-process \ + --certificate /path/to/certificate/file \ + --private-key /path/to/tpm/key/file \ + --role-arn ${ROLE_ARN} \ + --trust-anchor-arn ${TA_ARN} \ + --profile-arn ${PROFILE_ARN} +``` + #### Other Notes ##### YubiKey Attestation Certificates @@ -224,11 +347,7 @@ When using `serve` it is important to understand that processes running on a sys ### Scripts -The project also comes with two bash scripts at its root, called `generate-certs.sh` and `generate-credential-process-data.sh`. Note that these scripts currently only work on Unix-based systems and require `openssl` to be installed. - -#### generate-certs.sh - -Used by unit tests to generate test certificates and private keys supported by IAM Roles Anywhere. The test data is stored in the tst/certs directory. +The project also comes with two bash scripts at its root, called `generate-credential-process-data.sh` and `create_tpm2_key.sh`. Please note that these scripts currently only work on Unix-based systems and require additional dependencies to be installed (further documented below). #### generate-credential-process-data.sh @@ -258,6 +377,10 @@ PROFILE_ARN=$(aws rolesanywhere create-profile \ In the above example, you will have to create a role with a trust policy as documented [here](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/trust-model.html). After having done so, record the role ARN and use it both when creating a profile and when obtaining temporary security credentials through `credential-process`. +#### create_tpm2_key.sh + +Used in the Makefile to emulate the `create_tpm2_key` utility that comes with the IBM OpenSSL TPM 2.0 ENGINE. Note that this script only supports a limited subset of the functionality that's available with the utility that comes with the OpenSSL ENGINE. The purpose is so that keys can be created with the appropriate attributes for the sake of testing, and error handling may not bbe very good. It is not recommended to use this script for other purposes. If you have a need to use the script, it is recommended that you install the OpenSSL ENGINE and use the utility that comes with it instead. + ## Security See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. diff --git a/aws_signing_helper/credentials.go b/aws_signing_helper/credentials.go index c918b4e..b1467ea 100644 --- a/aws_signing_helper/credentials.go +++ b/aws_signing_helper/credentials.go @@ -32,6 +32,8 @@ type CredentialsOpts struct { Version string LibPkcs11 string ReusePin bool + TpmKeyPassword string + NoTpmKeyPassword bool } // Function to create session and generate credentials diff --git a/aws_signing_helper/pkcs11_signer.go b/aws_signing_helper/pkcs11_signer.go index a45108a..5c1eb12 100644 --- a/aws_signing_helper/pkcs11_signer.go +++ b/aws_signing_helper/pkcs11_signer.go @@ -48,7 +48,6 @@ import ( "github.com/miekg/pkcs11" pkcs11uri "github.com/stefanberger/go-pkcs11uri" - "golang.org/x/term" ) var PKCS11_TEST_VERSION int16 = 1 @@ -596,19 +595,6 @@ func pkcs11PasswordPrompt(module *pkcs11.Ctx, session pkcs11.SessionHandle, user return "", fmt.Errorf("unexpected error when prompting for %s", passwordName) } -// Prompts the user for their password -func GetPassword(ttyReadFile *os.File, ttyWriteFile *os.File, prompt string, parseErrMsg string) (string, error) { - fmt.Fprintln(ttyWriteFile, prompt) - passwordBytes, err := term.ReadPassword(int(ttyReadFile.Fd())) - if err != nil { - return "", errors.New(parseErrMsg) - } - - password := string(passwordBytes[:]) - strings.Replace(password, "\r", "", -1) // Remove CR - return password, nil -} - // Helper function to sign a digest using a PKCS#11 private key handle. func signHelper(module *pkcs11.Ctx, session pkcs11.SessionHandle, privateKeyObj KeyObjInfo, slot SlotIdInfo, userPin string, alwaysAuth uint, contextSpecificPin string, reusePin bool, keyType uint, digest []byte, hashFunc crypto.Hash) (_contextSpecificPin string, signature []byte, err error) { // XXX: If you use this outside the context of IAM RA, be aware that diff --git a/aws_signing_helper/signer.go b/aws_signing_helper/signer.go index 0b6829d..b06c0a4 100644 --- a/aws_signing_helper/signer.go +++ b/aws_signing_helper/signer.go @@ -19,6 +19,7 @@ import ( "math/big" "net/http" "os" + "runtime" "sort" "strings" "time" @@ -26,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "golang.org/x/crypto/pkcs12" + "golang.org/x/term" ) type SignerParams struct { @@ -127,6 +129,120 @@ var ignoredHeaderKeys = map[string]bool{ var Debug bool = false +// Prompts the user for their password +func GetPassword(ttyReadFile *os.File, ttyWriteFile *os.File, prompt string, parseErrMsg string) (string, error) { + fmt.Fprintln(ttyWriteFile, prompt) + passwordBytes, err := term.ReadPassword(int(ttyReadFile.Fd())) + if err != nil { + return "", errors.New(parseErrMsg) + } + + password := string(passwordBytes[:]) + strings.Replace(password, "\r", "", -1) // Remove CR + return password, nil +} + +type PasswordPromptProps struct { + InitialPassword string + NoPassword bool + CheckPassword func(string) (interface{}, error) + IncorrectPasswordMsg string + Prompt string + Reprompt string + ParseErrMsg string + CheckPasswordAuthorizationErrorMsg string +} + +func PasswordPrompt(passwordPromptInput PasswordPromptProps) (string, interface{}, error) { + var ( + err error + ttyReadPath string + ttyWritePath string + ttyReadFile *os.File + ttyWriteFile *os.File + parseErrMsg string + prompt string + reprompt string + password string + incorrectPasswordMsg string + checkPasswordAuthorizationErrorMsg string + checkPassword func(string) (interface{}, error) + checkPasswordResult interface{} + noPassword bool + ) + + password = passwordPromptInput.InitialPassword + noPassword = passwordPromptInput.NoPassword + incorrectPasswordMsg = passwordPromptInput.IncorrectPasswordMsg + prompt = passwordPromptInput.Prompt + reprompt = passwordPromptInput.Reprompt + parseErrMsg = passwordPromptInput.ParseErrMsg + checkPassword = passwordPromptInput.CheckPassword + checkPasswordAuthorizationErrorMsg = passwordPromptInput.CheckPasswordAuthorizationErrorMsg + + ttyReadPath = "/dev/tty" + ttyWritePath = ttyReadPath + if runtime.GOOS == "windows" { + ttyReadPath = "CONIN$" + ttyWritePath = "CONOUT$" + } + + ttyReadFile, err = os.OpenFile(ttyReadPath, os.O_RDWR, 0) + if err != nil { + return "", nil, errors.New(parseErrMsg) + } + defer ttyReadFile.Close() + + ttyWriteFile, err = os.OpenFile(ttyWritePath, os.O_WRONLY, 0) + if err != nil { + return "", nil, errors.New(parseErrMsg) + } + defer ttyWriteFile.Close() + + // If no password is required + if noPassword { + checkPasswordResult, err = checkPassword("") + if err != nil { + return "", nil, err + } + return "", checkPasswordResult, nil + } + + // If the password was provided explicitly, beforehand + if password != "" { + checkPasswordResult, err = checkPassword(password) + if err != nil { + return "", nil, errors.New(incorrectPasswordMsg) + } + return password, checkPasswordResult, nil + } + + // The key has a password, so prompt for it + password, err = GetPassword(ttyReadFile, ttyWriteFile, prompt, parseErrMsg) + if err != nil { + return "", nil, err + } + checkPasswordResult, err = checkPassword(password) + for true { + // If we've found the right password, return both it and the result of `checkPassword` + if err == nil { + return password, checkPasswordResult, nil + } + // Otherwise, if the password was incorrect, prompt for it again + if strings.Contains(err.Error(), checkPasswordAuthorizationErrorMsg) { + password, err = GetPassword(ttyReadFile, ttyWriteFile, reprompt, parseErrMsg) + if err != nil { + return "", nil, err + } + checkPasswordResult, err = checkPassword(password) + continue + } + return "", nil, err + } + + return "", nil, err +} + // Find whether the current certificate matches the CertIdentifier func certMatches(certIdentifier CertIdentifier, cert x509.Certificate) bool { if certIdentifier.Subject != "" && certIdentifier.Subject != cert.Subject.String() { @@ -222,7 +338,38 @@ func GetSigner(opts *CredentialsOpts) (signer Signer, signatureAlgorithm string, opts.CertificateId = "" } return GetPKCS11Signer(opts.LibPkcs11, certificate, certificateChain, opts.PrivateKeyId, opts.CertificateId, opts.ReusePin) + } else if strings.HasPrefix(privateKeyId, "handle:") { + if Debug { + log.Println("attempting to use TPMv2Signer") + } + return GetTPMv2Signer( + GetTPMv2SignerOpts{ + certificate, + certificateChain, + nil, + opts.TpmKeyPassword, + opts.NoTpmKeyPassword, + opts.PrivateKeyId, + }, + ) } else { + tpmKey, err := parseDERFromPEM(privateKeyId, "TSS2 PRIVATE KEY") + if err == nil { + if Debug { + log.Println("attempting to use TPMv2Signer") + } + return GetTPMv2Signer( + GetTPMv2SignerOpts{ + certificate, + certificateChain, + tpmKey, + opts.TpmKeyPassword, + opts.NoTpmKeyPassword, + "", + }, + ) + } + _, err = ReadPrivateKeyData(privateKeyId) if err != nil { return nil, "", err diff --git a/aws_signing_helper/signer_test.go b/aws_signing_helper/signer_test.go index 3476eac..9386294 100644 --- a/aws_signing_helper/signer_test.go +++ b/aws_signing_helper/signer_test.go @@ -2,13 +2,6 @@ package aws_signing_helper import ( "bytes" - "crypto" - "crypto/ecdsa" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/sha512" - "errors" "fmt" "io/ioutil" "log" @@ -193,46 +186,7 @@ func TestBuildAuthorizationHeader(t *testing.T) { requestSignFunction2(&awsRequest) } -// Verify that the provided payload was signed correctly with the provided options. -// This function is specifically used for unit testing. -func Verify(payload []byte, publicKey crypto.PublicKey, digest crypto.Hash, sig []byte) (bool, error) { - var hash []byte - switch digest { - case crypto.SHA256: - sum := sha256.Sum256(payload) - hash = sum[:] - case crypto.SHA384: - sum := sha512.Sum384(payload) - hash = sum[:] - case crypto.SHA512: - sum := sha512.Sum512(payload) - hash = sum[:] - default: - log.Fatal("unsupported digest") - return false, errors.New("unsupported digest") - } - - { - publicKey, ok := publicKey.(*ecdsa.PublicKey) - if ok { - valid := ecdsa.VerifyASN1(publicKey, hash, sig) - return valid, nil - } - } - - { - publicKey, ok := publicKey.(*rsa.PublicKey) - if ok { - err := rsa.VerifyPKCS1v15(publicKey, digest, hash, sig) - return err == nil, nil - } - } - - return false, nil -} - func TestSign(t *testing.T) { - msg := "test message" testTable := []CredentialsOpts{} // TODO: Include tests for PKCS#12 containers, once fixtures are created @@ -255,6 +209,18 @@ func TestSign(t *testing.T) { CertificateId: cert, PrivateKeyId: key, }) + + cert = fmt.Sprintf("../tst/certs/ec-%s-%s.p12", + curve, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) + + cert = fmt.Sprintf("../tst/certs/ec-%s-%s-combo.pem", + curve, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) } } @@ -276,6 +242,46 @@ func TestSign(t *testing.T) { CertificateId: cert, PrivateKeyId: key, }) + + cert = fmt.Sprintf("../tst/certs/rsa-%s-%s.p12", + keylen, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) + + cert = fmt.Sprintf("../tst/certs/rsa-%s-%s-combo.pem", + keylen, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) + } + } + + tpm_digests := []string{"sha1", "sha256", "sha384", "sha512"} + var tpm_keys []string + + tpmdev := os.Getenv("TPM_DEVICE") + if strings.HasPrefix(tpmdev, "/dev/") { + tpm_keys = []string{"hw-rsa", "hw-ec", "hw-ec-81000001"} + } else { + tpm_keys = []string{"sw-rsa", "sw-ec-prime256", "sw-ec-secp384r1", "sw-ec-81000001"} + } + + for _, digest := range tpm_digests { + for _, keyname := range tpm_keys { + cert := fmt.Sprintf("../tst/certs/tpm-%s-%s-cert.pem", + keyname, digest) + key := fmt.Sprintf("../tst/certs/tpm-%s-key.pem", keyname) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + PrivateKeyId: key, + }) + + cert = fmt.Sprintf("../tst/certs/tpm-%s-%s-combo.pem", + keyname, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) } } @@ -322,63 +328,7 @@ func TestSign(t *testing.T) { }) } - digestList := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} - - for _, credOpts := range testTable { - signer, _, err := GetSigner(&credOpts) - if err != nil { - var logMsg string - if credOpts.CertificateId != "" || credOpts.PrivateKeyId != "" { - logMsg = fmt.Sprintf("Failed to get signer for '%s'/'%s'", - credOpts.CertificateId, credOpts.PrivateKeyId) - } else { - logMsg = fmt.Sprintf("Failed to get signer for '%s'", - credOpts.CertIdentifier.Subject) - } - t.Log(logMsg) - t.Fail() - return - } - - pubKey := signer.Public() - if credOpts.CertificateId != "" && pubKey == nil { - t.Log(fmt.Sprintf("Signer didn't provide public key for '%s'/'%s'", - credOpts.CertificateId, credOpts.PrivateKeyId)) - t.Fail() - return - } - - for _, digest := range digestList { - signatureBytes, err := signer.Sign(rand.Reader, []byte(msg), digest) - // Try signing again to make sure that there aren't any issues - // with reopening sessions. Also, in some test cases, signing again - // makes sure that the context-specific PIN was saved. - signer.Sign(rand.Reader, []byte(msg), digest) - if err != nil { - t.Log("Failed to sign the input message") - t.Fail() - return - } - _, err = signer.Sign(rand.Reader, []byte(msg), digest) - if err != nil { - t.Log("Failed second signature on the input message") - t.Fail() - return - } - - if pubKey != nil { - valid, _ := Verify([]byte(msg), pubKey, digest, signatureBytes) - if !valid { - t.Log(fmt.Sprintf("Failed to verify the signature for '%s'/'%s'", - credOpts.CertificateId, credOpts.PrivateKeyId)) - t.Fail() - return - } - } - } - - signer.Close() - } + RunSignTestWithTestTable(t, testTable) } func TestCredentialProcess(t *testing.T) { diff --git a/aws_signing_helper/test_utils.go b/aws_signing_helper/test_utils.go new file mode 100644 index 0000000..d690bdf --- /dev/null +++ b/aws_signing_helper/test_utils.go @@ -0,0 +1,157 @@ +package aws_signing_helper + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/sha512" + "errors" + "fmt" + "log" + "testing" +) + +func RunSignTestWithTestTable(t *testing.T, testTable []CredentialsOpts) { + msg := "test message" + digestList := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} + + for _, credOpts := range testTable { + signer, _, err := GetSigner(&credOpts) + if err != nil { + var logMsg string + if credOpts.CertificateId != "" || credOpts.PrivateKeyId != "" { + logMsg = fmt.Sprintf("Failed to get signer for '%s'/'%s'", + credOpts.CertificateId, credOpts.PrivateKeyId) + } else { + logMsg = fmt.Sprintf("Failed to get signer for '%s'", + credOpts.CertIdentifier.Subject) + } + t.Log(logMsg) + t.Fail() + return + } + + pubKey := signer.Public() + if credOpts.CertificateId != "" && pubKey == nil { + t.Log(fmt.Sprintf("Signer didn't provide public key for '%s'/'%s'", + credOpts.CertificateId, credOpts.PrivateKeyId)) + t.Fail() + return + } + + for _, digest := range digestList { + signatureBytes, err := signer.Sign(rand.Reader, []byte(msg), digest) + // Try signing again to make sure that there aren't any issues + // with reopening sessions. Also, in some test cases, signing again + // makes sure that the context-specific PIN was saved. + signer.Sign(rand.Reader, []byte(msg), digest) + if err != nil { + t.Log(fmt.Sprintf("Failed to %s sign the input message for '%s'/'%s': %s", + digest, credOpts.CertificateId, credOpts.PrivateKeyId, err)) + t.Fail() + return + } + _, err = signer.Sign(rand.Reader, []byte(msg), digest) + if err != nil { + t.Log("Failed second signature on the input message") + t.Fail() + return + } + + if pubKey != nil { + valid, _ := Verify([]byte(msg), pubKey, digest, signatureBytes) + if !valid { + t.Log(fmt.Sprintf("Failed to verify %s signature for '%s'/'%s'", + digest, credOpts.CertificateId, credOpts.PrivateKeyId)) + t.Fail() + return + } + } + } + + signer.Close() + } +} + +func RunNegativeSignTestWithTestTable(t *testing.T, testTable []CredentialsOpts) { + msg := "test message" + digestList := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} + + for _, credOpts := range testTable { + signer, _, err := GetSigner(&credOpts) + if err != nil { + var logMsg string + if credOpts.CertificateId != "" || credOpts.PrivateKeyId != "" { + logMsg = fmt.Sprintf("Failed to get signer for '%s'/'%s'", + credOpts.CertificateId, credOpts.PrivateKeyId) + } else { + logMsg = fmt.Sprintf("Failed to get signer for '%s'", + credOpts.CertIdentifier.Subject) + } + t.Log(logMsg) + t.Fail() + return + } + + pubKey := signer.Public() + if credOpts.CertificateId != "" && pubKey == nil { + t.Log(fmt.Sprintf("Signer didn't provide public key for '%s'/'%s'", + credOpts.CertificateId, credOpts.PrivateKeyId)) + t.Fail() + return + } + + for _, digest := range digestList { + _, err := signer.Sign(rand.Reader, []byte(msg), digest) + signer.Sign(rand.Reader, []byte(msg), digest) + if err == nil { + t.Log(fmt.Sprintf("Expected %s sign on the input message to fail for '%s'/'%s': %s, but it succeeded", + digest, credOpts.CertificateId, credOpts.PrivateKeyId, err)) + t.Fail() + return + } + } + + signer.Close() + } +} + +// Verify that the provided payload was signed correctly with the provided options. +// This function is specifically used for unit testing. +func Verify(payload []byte, publicKey crypto.PublicKey, digest crypto.Hash, sig []byte) (bool, error) { + var hash []byte + switch digest { + case crypto.SHA256: + sum := sha256.Sum256(payload) + hash = sum[:] + case crypto.SHA384: + sum := sha512.Sum384(payload) + hash = sum[:] + case crypto.SHA512: + sum := sha512.Sum512(payload) + hash = sum[:] + default: + log.Fatal("unsupported digest") + return false, errors.New("unsupported digest") + } + + { + publicKey, ok := publicKey.(*ecdsa.PublicKey) + if ok { + valid := ecdsa.VerifyASN1(publicKey, hash, sig) + return valid, nil + } + } + + { + publicKey, ok := publicKey.(*rsa.PublicKey) + if ok { + err := rsa.VerifyPKCS1v15(publicKey, digest, hash, sig) + return err == nil, nil + } + } + + return false, nil +} diff --git a/aws_signing_helper/tpm_signer.go b/aws_signing_helper/tpm_signer.go new file mode 100644 index 0000000..c34538e --- /dev/null +++ b/aws_signing_helper/tpm_signer.go @@ -0,0 +1,444 @@ +package aws_signing_helper + +import ( + "crypto" + "crypto/ecdsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "io" + "math/big" + "strconv" + "strings" + + tpm2 "github.com/google/go-tpm/tpm2" + tpmutil "github.com/google/go-tpm/tpmutil" +) + +type tpm2_TPMPolicy struct { + CommandCode int `asn1:"explicit,tag:0"` + CommandPolicy []byte `asn1:"explicit,tag:1"` +} + +type tpm2_TPMAuthPolicy struct { + Name string `asn1:"utf8,optional,explicit,tag:0"` + Policy []tpm2_TPMPolicy `asn1:"explicit,tag:1"` +} + +type tpm2_TPMKey struct { + Oid asn1.ObjectIdentifier + EmptyAuth bool `asn1:"optional,explicit,tag:0"` + Policy []tpm2_TPMPolicy `asn1:"optional,explicit,tag:1"` + Secret []byte `asn1:"optional,explicit,tag:2"` + AuthPolicy []tpm2_TPMAuthPolicy `asn1:"optional,explicit,tag:3"` + Parent int + Pubkey []byte + Privkey []byte +} + +var oidLoadableKey = asn1.ObjectIdentifier{2, 23, 133, 10, 1, 3} +var TPM_RC_AUTH_FAIL = "0x22" + +type TPMv2Signer struct { + cert *x509.Certificate + certChain []*x509.Certificate + tpmData tpm2_TPMKey + public tpm2.Public + private []byte + password string + emptyAuth bool + handle tpmutil.Handle +} + +func handleIsPersistent(h int) bool { + return (h >> 24) == int(tpm2.HandleTypePersistent) +} + +var primaryParams = tpm2.Public{ + Type: tpm2.AlgECC, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagUserWithAuth | tpm2.FlagRestricted | tpm2.FlagDecrypt | tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagNoDA | tpm2.FlagSensitiveDataOrigin, + ECCParameters: &tpm2.ECCParams{ + Symmetric: &tpm2.SymScheme{ + Alg: tpm2.AlgAES, + KeyBits: 128, + Mode: tpm2.AlgCFB, + }, + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgNull, + }, + CurveID: tpm2.CurveNISTP256, + KDF: &tpm2.KDFScheme{ + Alg: tpm2.AlgNull, + }, + }, +} + +// Returns the public key associated with this TPMv2Signer +func (tpmv2Signer *TPMv2Signer) Public() crypto.PublicKey { + ret, _ := tpmv2Signer.public.Key() + return ret +} + +// Closes this TPMv2Signer +func (tpmv2Signer *TPMv2Signer) Close() { + tpmv2Signer.password = "" +} + +func checkCapability(rw io.ReadWriter, algo tpm2.Algorithm) error { + descs, _, err := tpm2.GetCapability(rw, tpm2.CapabilityAlgs, 1, uint32(algo)) + if err != nil { + errMsg := fmt.Sprintf("error trying to get capability from TPM for the algorithm (%s)", algo) + return errors.New(errMsg) + } + if tpm2.Algorithm(descs[0].(tpm2.AlgorithmDescription).ID) != algo { + errMsg := fmt.Sprintf("unsupported algorithm (%s) for TPM", algo) + return errors.New(errMsg) + } + + return nil +} + +// Implements the crypto.Signer interface and signs the passed in digest +func (tpmv2Signer *TPMv2Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + var ( + keyHandle tpmutil.Handle + ) + + rw, err := openTPM() + if err != nil { + return nil, err + } + defer rw.Close() + + if tpmv2Signer.handle != 0 { + keyHandle = tpmv2Signer.handle + } else { + parentHandle := tpmutil.Handle(tpmv2Signer.tpmData.Parent) + if !handleIsPersistent(tpmv2Signer.tpmData.Parent) { + // Parent and owner passwords aren't supported currently when creating a primary given a persistent handle for the parent + parentHandle, _, err = tpm2.CreatePrimary(rw, tpmutil.Handle(tpmv2Signer.tpmData.Parent), tpm2.PCRSelection{}, "", "", primaryParams) + if err != nil { + return nil, err + } + defer tpm2.FlushContext(rw, parentHandle) + } + + keyHandle, _, err = tpm2.Load(rw, parentHandle, "", tpmv2Signer.tpmData.Pubkey[2:], tpmv2Signer.tpmData.Privkey[2:]) + if err != nil { + return nil, err + } + defer tpm2.FlushContext(rw, keyHandle) + } + + var algo tpm2.Algorithm + var shadigest []byte + + switch opts.HashFunc() { + case crypto.SHA256: + sha256digest := sha256.Sum256(digest) + shadigest = sha256digest[:] + algo = tpm2.AlgSHA256 + case crypto.SHA384: + sha384digest := sha512.Sum384(digest) + shadigest = sha384digest[:] + algo = tpm2.AlgSHA384 + case crypto.SHA512: + sha512digest := sha512.Sum512(digest) + shadigest = sha512digest[:] + algo = tpm2.AlgSHA512 + } + + if tpmv2Signer.public.Type == tpm2.AlgECC { + // Check to see that ECDSA is supported for signing + err = checkCapability(rw, tpm2.AlgECC) + if err != nil { + return nil, err + } + + // For an EC key we lie to the TPM about what the hash is. + // It doesn't actually matter what the original digest was; + // the algo we feed to the TPM is *purely* based on the + // size of the curve itself. We truncate the actual digest, + // or pad with zeroes, to the byte size of the key. + pubKey, err := tpmv2Signer.public.Key() + if err != nil { + return nil, err + } + ecPubKey, ok := pubKey.(*ecdsa.PublicKey) + if !ok { + return nil, errors.New("failed to obtain ecdsa.PublicKey") + } + bitSize := ecPubKey.Curve.Params().BitSize + byteSize := (bitSize + 7) / 8 + if byteSize > sha512.Size { + byteSize = sha512.Size + } + switch byteSize { + case sha512.Size: + algo = tpm2.AlgSHA512 + case sha512.Size384: + algo = tpm2.AlgSHA384 + case sha512.Size256: + algo = tpm2.AlgSHA256 + case sha1.Size: + algo = tpm2.AlgSHA1 + default: + return nil, errors.New("unsupported curve") + } + + if len(shadigest) > byteSize { + shadigest = shadigest[:byteSize] + } + + for len(shadigest) < byteSize { + shadigest = append([]byte{0}, shadigest...) + } + + sig, err := tpmv2Signer.signHelper(rw, keyHandle, shadigest, + &tpm2.SigScheme{Alg: tpm2.AlgECDSA, Hash: algo}) + if err != nil { + return nil, err + } + signature, err = asn1.Marshal(struct { + R *big.Int + S *big.Int + }{sig.ECC.R, sig.ECC.S}) + if err != nil { + return nil, err + } + } else { + // Check to see that the requested hash function is supported + err = checkCapability(rw, algo) + if err != nil { + return nil, err + } + + // Check to see that RSASSA is supported for signing + err = checkCapability(rw, tpm2.AlgRSASSA) + if err != nil { + return nil, err + } + + sig, err := tpmv2Signer.signHelper(rw, keyHandle, shadigest, + &tpm2.SigScheme{Alg: tpm2.AlgRSASSA, Hash: algo}) + if err != nil { + return nil, err + } + signature = sig.RSA.Signature + } + return signature, nil +} + +func (tpmv2Signer *TPMv2Signer) signHelper(rw io.ReadWriter, keyHandle tpmutil.Handle, digest tpmutil.U16Bytes, sigScheme *tpm2.SigScheme) (*tpm2.Signature, error) { + passwordPromptInput := PasswordPromptProps{ + InitialPassword: tpmv2Signer.password, + NoPassword: tpmv2Signer.emptyAuth, + CheckPassword: func(password string) (interface{}, error) { + return tpm2.Sign(rw, keyHandle, password, digest, nil, sigScheme) + }, + IncorrectPasswordMsg: "incorrect TPM key password", + Prompt: "Please enter your TPM key password:", + Reprompt: "Incorrect TPM key password. Please try again:", + ParseErrMsg: "unable to read your TPM key password", + CheckPasswordAuthorizationErrorMsg: TPM_RC_AUTH_FAIL, + } + + password, sig, err := PasswordPrompt(passwordPromptInput) + if err != nil { + return nil, err + } + + tpmv2Signer.password = password + return sig.(*tpm2.Signature), err +} + +// Gets the x509.Certificate associated with this TPMv2Signer +func (tpmv2Signer *TPMv2Signer) Certificate() (*x509.Certificate, error) { + return tpmv2Signer.cert, nil +} + +// Gets the certificate chain associated with this TPMv2Signer +func (tpmv2Signer *TPMv2Signer) CertificateChain() (chain []*x509.Certificate, err error) { + return tpmv2Signer.certChain, nil +} + +/* + * DER forbids storing a BOOLEAN as anything but 0x00 or 0xFF, + * 0x01, and the Go asn1 parser cannot be relaxed. But both + * OpenSSL ENGINEs which produce these keys have at least in + * the past emitted 0x01 as the value, leading to an Unmarshal + * failure with 'asn1: syntax error: invalid boolean'. So... + */ +func fixupEmptyAuth(tpmData *[]byte) { + var pos int = 0 + + // Skip the SEQUENCE tag and length + if len(*tpmData) < 2 || (*tpmData)[0] != 0x30 { + return + } + + // Don't care what the SEQUENCE length is, just skip it + pos = 1 + lenByte := (*tpmData)[pos] + if lenByte < 0x80 { + pos = pos + 1 + } else if lenByte < 0x85 { + pos = pos + 1 + int(lenByte) - 0x80 + } else { + return + } + + if len(*tpmData) <= pos { + return + } + + // Use asn1.Unmarshal to eat the OID; we care about 'rest' + var oid asn1.ObjectIdentifier + rest, err := asn1.Unmarshal((*tpmData)[pos:], &oid) + if err != nil || rest == nil || !oid.Equal(oidLoadableKey) || len(rest) < 5 { + return + } + + // If the OPTIONAL EXPLICIT BOOLEAN [0] exists, it'll be here + pos = len(*tpmData) - len(rest) + + if (*tpmData)[pos] == 0xa0 && // Tag + (*tpmData)[pos+1] == 0x03 && // length + (*tpmData)[pos+2] == 0x01 && + (*tpmData)[pos+3] == 0x01 && + (*tpmData)[pos+4] == 0x01 { + (*tpmData)[pos+4] = 0xff + } +} + +type GetTPMv2SignerOpts struct { + certificate *x509.Certificate + certificateChain []*x509.Certificate + keyPem *pem.Block + password string + emptyAuth bool + handle string +} + +// Returns a TPMv2Signer, that can be used to sign a payload through a TPMv2-compatible +// cryptographic device +func GetTPMv2Signer(opts GetTPMv2SignerOpts) (signer Signer, signingAlgorithm string, err error) { + var ( + certificate *x509.Certificate + certificateChain []*x509.Certificate + keyPem *pem.Block + password string + emptyAuth bool + tpmData tpm2_TPMKey + handle tpmutil.Handle + public tpm2.Public + private []byte + ) + + certificate = opts.certificate + certificateChain = opts.certificateChain + keyPem = opts.keyPem + password = opts.password + emptyAuth = opts.emptyAuth + + // If a handle is provided instead of a TPM key file + if opts.handle != "" { + handleParts := strings.Split(opts.handle, ":") + if len(handleParts) != 2 { + return nil, "", errors.New("invalid TPM handle format") + } + hexHandleStr := handleParts[1] + if strings.HasPrefix(hexHandleStr, "0x") { + hexHandleStr = hexHandleStr[2:] + } + handleValue, err := strconv.ParseUint(hexHandleStr, 16, 32) + if err != nil { + return nil, "", errors.New("invalid hex TPM handle value") + } + handle = tpmutil.Handle(handleValue) + + // Read the public key from the loaded key within the TPM + rw, err := openTPM() + if err != nil { + return nil, "", err + } + defer rw.Close() + + public, _, _, err = tpm2.ReadPublic(rw, handle) + if err != nil { + return nil, "", err + } + } else { + fixupEmptyAuth(&keyPem.Bytes) + _, err = asn1.Unmarshal(keyPem.Bytes, &tpmData) + if err != nil { + return nil, "", err + } + + emptyAuth = tpmData.EmptyAuth + if emptyAuth && password != "" { + return nil, "", errors.New("password is provided but TPM key file indicates that one isn't required") + } + + if !tpmData.Oid.Equal(oidLoadableKey) { + return nil, "", errors.New("invalid OID for TPMv2 key:" + tpmData.Oid.String()) + } + + if tpmData.Policy != nil || tpmData.AuthPolicy != nil { + return nil, "", errors.New("TPMv2 policy not implemented yet") + } + if tpmData.Secret != nil { + return nil, "", errors.New("TPMv2 key has 'secret' field which should not be set") + } + + if !handleIsPersistent(tpmData.Parent) && + tpmData.Parent != int(tpm2.HandleOwner) && + tpmData.Parent != int(tpm2.HandleNull) && + tpmData.Parent != int(tpm2.HandleEndorsement) && + tpmData.Parent != int(tpm2.HandlePlatform) { + return nil, "", errors.New("invalid parent for TPMv2 key") + } + if len(tpmData.Pubkey) < 2 || + len(tpmData.Pubkey)-2 != (int(tpmData.Pubkey[0])<<8)+int(tpmData.Pubkey[1]) { + return nil, "", errors.New("invalid length for TPMv2 PUBLIC blob") + } + + public, err = tpm2.DecodePublic(tpmData.Pubkey[2:]) + if err != nil { + return nil, "", err + } + + if len(tpmData.Privkey) < 2 || + len(tpmData.Privkey)-2 != (int(tpmData.Privkey[0])<<8)+int(tpmData.Privkey[1]) { + return nil, "", errors.New("invalid length for TPMv2 PRIVATE blob") + } + private = tpmData.Privkey[2:] + } + + switch public.Type { + case tpm2.AlgRSA: + signingAlgorithm = aws4_x509_rsa_sha256 + case tpm2.AlgECC: + signingAlgorithm = aws4_x509_ecdsa_sha256 + default: + return nil, "", errors.New("unsupported TPMv2 key type") + } + + return &TPMv2Signer{ + certificate, + certificateChain, + tpmData, + public, + private, + password, + emptyAuth, + handle, + }, + signingAlgorithm, nil +} diff --git a/aws_signing_helper/tpm_signer_helper_other.go b/aws_signing_helper/tpm_signer_helper_other.go new file mode 100644 index 0000000..e161403 --- /dev/null +++ b/aws_signing_helper/tpm_signer_helper_other.go @@ -0,0 +1,19 @@ +//go:build !windows + +package aws_signing_helper + +import ( + "io" + "os" + + tpm2 "github.com/google/go-tpm/tpm2" +) + +func openTPM() (io.ReadWriteCloser, error) { + var paths []string + tpmdev := os.Getenv("TPM_DEVICE") + if tpmdev != "" { + paths = append(paths, tpmdev) + } + return tpm2.OpenTPM(paths...) +} diff --git a/aws_signing_helper/tpm_signer_helper_windows.go b/aws_signing_helper/tpm_signer_helper_windows.go new file mode 100644 index 0000000..be1206b --- /dev/null +++ b/aws_signing_helper/tpm_signer_helper_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package aws_signing_helper + +import ( + "io" + + tpm2 "github.com/google/go-tpm/tpm2" +) + +func openTPM() (io.ReadWriteCloser, error) { + return tpm2.OpenTPM() +} diff --git a/aws_signing_helper/tpm_signer_test.go b/aws_signing_helper/tpm_signer_test.go new file mode 100644 index 0000000..2fcbee4 --- /dev/null +++ b/aws_signing_helper/tpm_signer_test.go @@ -0,0 +1,178 @@ +package aws_signing_helper + +import ( + "fmt" + "os" + "strings" + "testing" +) + +func TestTPMSigner(t *testing.T) { + testTable := []CredentialsOpts{} + + tpm_digests := []string{"sha1", "sha256", "sha384", "sha512"} + tpm_key_handles := []string{"0x81000101", "81000101"} + var tpm_keys []string + + tpmdev := os.Getenv("TPM_DEVICE") + if strings.HasPrefix(tpmdev, "/dev/") { + tpm_keys = []string{"hw-rsa", "hw-ec", "hw-ec-81000001"} + } else { + tpm_keys = []string{"sw-rsa-81000001-sign", "sw-ec-prime256", "sw-ec-secp384r1"} + } + + // For each of these keys, the parent key doesn't have a password + for _, digest := range tpm_digests { + for _, keyname := range tpm_keys { + cert := fmt.Sprintf("../tst/certs/tpm-%s-%s-cert.pem", + keyname, digest) + key := fmt.Sprintf("../tst/certs/tpm-%s-key.pem", keyname) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + PrivateKeyId: key, + NoTpmKeyPassword: true, + }) + keyWithPw := fmt.Sprintf("../tst/certs/tpm-%s-key-with-pw.pem", keyname) + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: keyWithPw, + TpmKeyPassword: "1234", + }) + + cert = fmt.Sprintf("../tst/certs/tpm-%s-%s-combo.pem", + keyname, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + NoTpmKeyPassword: true, + }) + + cert = fmt.Sprintf("../tst/certs/tpm-%s-%s-cert.pem", + keyname, digest) + key = fmt.Sprintf("../tst/certs/tpm-%s-key.pem", keyname) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + PrivateKeyId: key, + }) + + cert = fmt.Sprintf("../tst/certs/tpm-%s-%s-combo.pem", + keyname, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) + } + + // Using a loaded key + for _, handle := range tpm_key_handles { + keyHandle := fmt.Sprintf("handle:%s", handle) + cert := fmt.Sprintf("../tst/certs/tpm-sw-loaded-81000101-ec-secp384r1-%s-cert.pem", digest) + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: keyHandle, + NoTpmKeyPassword: true, + CertificateId: cert, + }) + } + } + + // Some positive tests, without a certificate + key := "../tst/certs/tpm-sw-rsa-81000001-sign-key.pem" + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: key, + NoTpmKeyPassword: true, + }) + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: key, + }) + keyWithPw := "../tst/certs/tpm-sw-rsa-81000001-sign-key-with-pw.pem" + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: keyWithPw, + TpmKeyPassword: "1234", + }) + + RunSignTestWithTestTable(t, testTable) +} + +func TestTPMSignerWithCertificateBundle(t *testing.T) { + keyWithPw := "../tst/certs/tpm-sw-rsa-81000001-sign-key-with-pw.pem" + certBundle := "../tst/certs/cert-bundle.pem" + credOpts := CredentialsOpts{ + PrivateKeyId: keyWithPw, + TpmKeyPassword: "1234", + CertificateBundleId: certBundle, + } + + signer, _, err := GetSigner(&credOpts) + if err != nil { + var logMsg string + if credOpts.CertificateId != "" || credOpts.PrivateKeyId != "" { + logMsg = fmt.Sprintf("Failed to get signer for '%s'/'%s'", + credOpts.CertificateId, credOpts.PrivateKeyId) + } else { + logMsg = fmt.Sprintf("Failed to get signer for '%s'", + credOpts.CertIdentifier.Subject) + } + t.Log(logMsg) + t.Fail() + return + } + + certChain, err := signer.CertificateChain() + if err != nil { + t.Log("Error when retrieving certificate chain") + t.Fail() + } + if certChain == nil { + t.Log("Expecting certificate chain but found none") + t.Fail() + } +} + +func TestTPMSignerFails(t *testing.T) { + testTable := []CredentialsOpts{} + + var tpm_keys []string + + tpmdev := os.Getenv("TPM_DEVICE") + if strings.HasPrefix(tpmdev, "/dev/") { + return // Skip this test in the case of hardware TPM, so as to not cause DA lockout + } else { + tpm_keys = []string{"sw-rsa", "sw-rsa-81000001-sign", "sw-ec-prime256", "sw-ec-secp384r1", "sw-ec-81000001"} + } + + // Test that RSA keys that don't have the Sign capability aren't able to + // sign (even in the case that they have the raw Decrypt capability) + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: "../tst/certs/tpm-sw-rsa-key.pem", + NoTpmKeyPassword: true, + }) + + // Test that signing fails when an incorrect password is provided + for _, keyname := range tpm_keys { + keyWithPw := fmt.Sprintf("../tst/certs/tpm-%s-key-with-pw.pem", keyname) + // Wrong child key password + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: keyWithPw, + TpmKeyPassword: "incorrect-password", + }) + } + + RunNegativeSignTestWithTestTable(t, testTable) +} + +func TestTPMSignerInstantiationFails(t *testing.T) { + var key string + + tpmdev := os.Getenv("TPM_DEVICE") + if strings.HasPrefix(tpmdev, "/dev/") { + key = "../tst/certs/tpm-hw-ec-key.pem" + } else { + key = "../tst/certs/tpm-sw-ec-prime256-key.pem" + } + + _, _, err := GetSigner(&CredentialsOpts{ + PrivateKeyId: key, + TpmKeyPassword: "unneeded-password", + }) + if err == nil { + t.Log("expected error when instantiating signer but received none") + t.Fail() + } +} diff --git a/cmd/credentials.go b/cmd/credentials.go index 5c76b97..923b8ee 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -31,6 +31,9 @@ var ( libPkcs11 string + tpmKeyPassword string + noTpmKeyPassword bool + credentialsOptions helper.CredentialsOpts X509_SUBJECT_KEY = "x509Subject" @@ -72,6 +75,9 @@ func initCredentialsSubCommand(subCmd *cobra.Command) { subCmd.PersistentFlags().BoolVar(&reusePin, "reuse-pin", false, "Use the CKU_USER PIN as the CKU_CONTEXT_SPECIFIC PIN for "+ "private key objects, when they are first used to sign. If the CKU_USER PIN doesn't work as the CKU_CONTEXT_SPECIFIC PIN "+ "for a given private key object, fall back to prompting the user") + subCmd.PersistentFlags().StringVar(&tpmKeyPassword, "tpm-key-password", "", "Password for TPM key, if applicable") + subCmd.PersistentFlags().BoolVar(&noTpmKeyPassword, "no-tpm-key-password", false, "Required if the TPM key has no password and"+ + "a handle is used to refer to the key") subCmd.MarkFlagsMutuallyExclusive("certificate", "cert-selector") subCmd.MarkFlagsMutuallyExclusive("certificate", "system-store-name") @@ -80,6 +86,10 @@ func initCredentialsSubCommand(subCmd *cobra.Command) { subCmd.MarkFlagsMutuallyExclusive("cert-selector", "intermediates") subCmd.MarkFlagsMutuallyExclusive("cert-selector", "reuse-pin") subCmd.MarkFlagsMutuallyExclusive("system-store-name", "reuse-pin") + subCmd.MarkFlagsMutuallyExclusive("tpm-key-password", "cert-selector") + subCmd.MarkFlagsMutuallyExclusive("tpm-key-password", "reuse-pin") + subCmd.MarkFlagsMutuallyExclusive("no-tpm-key-password", "cert-selector") + subCmd.MarkFlagsMutuallyExclusive("no-tpm-key-password", "tpm-key-password") } // Parses a cert selector string to a map @@ -242,6 +252,8 @@ func PopulateCredentialsOptions() error { Version: Version, LibPkcs11: libPkcs11, ReusePin: reusePin, + TpmKeyPassword: tpmKeyPassword, + NoTpmKeyPassword: noTpmKeyPassword, } return nil diff --git a/cmd/sign_string.go b/cmd/sign_string.go index 943bd07..b645f6d 100644 --- a/cmd/sign_string.go +++ b/cmd/sign_string.go @@ -84,6 +84,9 @@ func init() { signStringCmd.PersistentFlags().BoolVar(&reusePin, "reuse-pin", false, "Use the CKU_USER PIN as the CKU_CONTEXT_SPECIFIC PIN for "+ "private key objects, when they are first used to sign. If the CKU_USER PIN doesn't work as the CKU_CONTEXT_SPECIFIC PIN "+ "for a given private key object, fall back to prompting the user") + signStringCmd.PersistentFlags().StringVar(&tpmKeyPassword, "tpm-key-password", "", "Password for TPM key, if applicable") + signStringCmd.PersistentFlags().BoolVar(&noTpmKeyPassword, "no-tpm-key-password", false, "Required if the TPM key has no password and"+ + "a handle is used to refer to the key") signStringCmd.PersistentFlags().Var(format, "format", "Output format. One of json, text, and bin") signStringCmd.PersistentFlags().Var(digestArg, "digest", "One of SHA256, SHA384, and SHA512") @@ -93,6 +96,11 @@ func init() { signStringCmd.MarkFlagsMutuallyExclusive("private-key", "system-store-name") signStringCmd.MarkFlagsMutuallyExclusive("cert-selector", "reuse-pin") signStringCmd.MarkFlagsMutuallyExclusive("system-store-name", "reuse-pin") + signStringCmd.MarkFlagsMutuallyExclusive("tpm-key-password", "cert-selector") + signStringCmd.MarkFlagsMutuallyExclusive("tpm-key-password", "reuse-pin") + signStringCmd.MarkFlagsMutuallyExclusive("no-tpm-key-password", "cert-selector") + signStringCmd.MarkFlagsMutuallyExclusive("no-tpm-key-password", "reuse-pin") + signStringCmd.MarkFlagsMutuallyExclusive("no-tpm-key-password", "tpm-key-password") } func getFixedStringToSign(publicKey crypto.PublicKey) string { @@ -163,7 +171,7 @@ var signStringCmd = &cobra.Command{ sigBytes, err := signer.Sign(rand.Reader, stringToSignBytes, digest) if err != nil { - log.Println("unable to sign the digest") + log.Println("unable to sign the digest:", err) os.Exit(1) } sigStr := hex.EncodeToString(sigBytes) diff --git a/create_tpm2_key.sh b/create_tpm2_key.sh new file mode 100755 index 0000000..98fcc96 --- /dev/null +++ b/create_tpm2_key.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +set -exuo pipefail + +# This script will emulate the utility of the same name provided by James Bottomley +# with his OpenSSL ENGINE, using utilities within tpm2-tools. Note that the options +# in this script will be far more restricted than what Bottomley provides and may not +# handle errors gracefully. While you may use this script to generate your own keys, +# it isn't recommended (you may want to consider installing Bottomley's OpenSSL ENGINE +# and using the utility tools that come with it instead, because, if nothing else, they +# are better documented). The primary purpose for this script in this codebase is for +# unit testing. + +ecc_curve="" +rsa_key=false +password="" +filename="" + +# Parse command-line options +while getopts "e:rk:" opt; do + case $opt in + e) + ecc_curve=$OPTARG + ;; + r) + rsa_key=true + ;; + k) + password=$OPTARG + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument" >&2 + exit 1 + ;; + esac +done + +# Check for mutually exclusive options +if [ -n "$ecc_curve" ] && "$rsa_key"; then + echo "Error: -e and -r options are mutually exclusive" >&2 + exit 1 +fi + +# Check for presence of at least one of -e and -r +if [ -z "$ecc_curve" ] && [ ! "$rsa_key" ]; then + echo "Error: exactly one of -e and -r options should be provided" >&2 + exit 1 +fi + +# Shift to remove the processed options from the arguments +shift $((OPTIND-1)) + +# Check if the filename is provided and capture it if so +if [ $# -eq 0 ]; then + echo "Error: A filename is required" >&2 + echo "Usage: $0 [options] " >&2 + exit 1 +fi +filename=$1 + +# Create the primary key under the Owner hierarchy +tpm2_createprimary \ + -C o \ + -G ecc256:aes128cfb \ + -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda|restricted|decrypt" \ + -c primary.ctx + +# Make the primary available at a persistent handle +PRIMARY_HANDLE=$(tpm2_evictcontrol -c primary.ctx | cut -d ' ' -f 2 | head -n 1) + +if [ -z "$ecc_curve" ]; then # RSA key + # Create child key with the appropriate attributes (no sign attribute) + cmd="tpm2_create -C primary.ctx -a 'decrypt|fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda' -c child.ctx -u child.pub -r child.priv" + [ -n "$password" ] && cmd="$cmd -p $password" + eval $cmd + + # Serialize the ASN.1 DER file + noPassword=$( [ -z "$password" ] && echo TRUE || echo FALSE ) + hexChildPubData=$(xxd -p child.pub | tr -d '\n') + hexChildPrivData=$(xxd -p child.priv | tr -d '\n') + cat > tpm-key-asn1.conf < $filename + base64 -w 64 < child.der >> $filename + echo "-----END TSS2 PRIVATE KEY-----" >> $filename + + # Remove files that are no longer needed + rm child.priv child.pub child.ctx child.der tpm-key-asn1.conf +else # EC key + # Create the child key, using the above primary as its parent + cmd="openssl genpkey -provider tpm2 -pkeyopt parent:${PRIMARY_HANDLE} -out $filename" + [ -n "$ecc_curve" ] && cmd+=("-algorithm EC -pkeyopt group:$ecc_curve") + [ -n "$password" ] && cmd+=("-pkeyopt user-auth:$password") + ${cmd[@]} + + # Convert the PEM file to DER + openssl asn1parse -inform PEM -in $filename -out temp.der + + # Find ASN.1 sequence length and decrement it by one (the permanent handle takes up one less byte) + case $ecc_curve in + prime256v1) + seq_replacement="s/3081f1/3081f0/" + ;; + secp384r1) + seq_replacement="s/30820121/30820120/" + ;; + esac + + # Change the handle to the permanent one corresponding to the Owner hierarchy + xxd -p temp.der | tr -d '\n' | sed "s/020500$(echo ${PRIMARY_HANDLE} | awk '{print substr($0, 3)}')/020440000001/" | sed "$seq_replacement" | xxd -r -p > modified.der + + # Create the new PEM file with modified contents + echo "-----BEGIN TSS2 PRIVATE KEY-----" > $filename + base64 -w 64 < modified.der >> $filename + echo "-----END TSS2 PRIVATE KEY-----" >> $filename + + # Remove the temporary DER files + rm temp.der modified.der +fi + +# Remove the persistent handle for the primary (parent) +tpm2_evictcontrol -c ${PRIMARY_HANDLE} + +# Remove files related to the primary (parent) +rm primary.ctx diff --git a/go.mod b/go.mod index 1f96a07..6a7f854 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/aws/rolesanywhere-credential-helper -go 1.21 +go 1.22 require ( github.com/aws/aws-sdk-go v1.50.30 + github.com/google/go-tpm v0.3.3 github.com/miekg/pkcs11 v1.1.1 github.com/spf13/cobra v1.8.0 github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 diff --git a/go.sum b/go.sum index b8350b8..d5d3434 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,208 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.50.30 h1:2OelKH1eayeaH7OuL1Y9Ombfw4HK+/k0fEnJNWjyLts= github.com/aws/aws-sdk-go v1.50.30/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI= +github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw= +github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo= +github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4= +github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0= +github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=