diff --git a/Makefile b/Makefile index 5225426..9cb395c 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ -VERSION=1.0.6 +VERSION=1.1.0 release: 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 +# Setting up SoftHSM for PKCS#11 tests. +# This portion is largely copied from https://gitlab.com/openconnect/openconnect/-/blob/v9.12/tests/Makefile.am#L363. +SHM2_UTIL=SOFTHSM2_CONF=tst/softhsm2.conf.tmp softhsm2-util +P11TOOL=SOFTHSM2_CONF=tst/softhsm2.conf.tmp p11tool + certsdir=tst/certs curdir=$(shell pwd) @@ -13,8 +18,40 @@ 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)) -test: test-certs - go test -v ./... +# It's hard to do 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) + rm -rf tst/softhsm/* + sed 's|@top_srcdir@|${curdir}|g' $< > $@.tmp + $(SHM2_UTIL) --show-slots + $(SHM2_UTIL) --init-token --free --label credential-helper-test \ + --so-pin 12345678 --pin 1234 + + $(SHM2_UTIL) --token credential-helper-test --pin 1234 \ + --import $(certsdir)/rsa-2048-key-pkcs8.pem --label rsa-2048 --id 01 + $(P11TOOL) --load-certificate $(certsdir)/rsa-2048-sha256-cert.pem \ + --no-mark-private --label rsa-2048 --id 01 --set-pin 1234 --login \ + --write "pkcs11:token=credential-helper-test;pin-value=1234" + + $(SHM2_UTIL) --token credential-helper-test --pin 1234 \ + --import $(certsdir)/ec-prime256v1-key-pkcs8.pem --label ec-prime256v1 --id 02 + $(P11TOOL) --load-certificate $(certsdir)/ec-prime256v1-sha256-cert.pem \ + --no-mark-private --label ec-prime256v1 --id 02 --set-pin 1234 --login \ + --write "pkcs11:token=credential-helper-test;pin-value=1234" + + $(P11TOOL) --load-privkey $(certsdir)/rsa-2048-key-pkcs8.pem \ + --label rsa-2048-always-auth --id 03 --set-pin 1234 --login \ + --write "pkcs11:token=credential-helper-test;pin-value=1234" \ + --mark-always-authenticate + + $(P11TOOL) --load-privkey $(certsdir)/ec-prime256v1-key-pkcs8.pem \ + --label ec-prime256v1-always-auth --id 04 --set-pin 1234 --login \ + --write "pkcs11:token=credential-helper-test;pin-value=1234" \ + --mark-always-authenticate + mv $@.tmp $@ + +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|'); \ @@ -43,6 +80,8 @@ test: test-certs -keypbe pbeWithSHA1And3-KeyTripleDES-CBC \ -inkey $${KEY} -out "$@" -in $${CERT} +# And once again, it's hard to do a file-based rule for the contents of the certificate store. +# So just populate it as a side-effect of creating the p12 file. %-pass.p12: %-cert.pem echo Creating $@... ls -l $< @@ -66,7 +105,13 @@ $(ECKEYS): $(certsdir)/cert-bundle.pem: $(RSACERTS) $(ECCERTS) cat $^ > $@ -test-certs: $(PKCS8KEYS) $(RSAKEYS) $(ECKEYS) $(RSACERTS) $(ECCERTS) $(PKCS12CERTS) $(certsdir)/cert-bundle.pem +$(certsdir)/cert-bundle-with-comments.pem: $(RSACERTS) $(ECCERTS) + for dep in $^; do \ + cat $$dep >> $@; \ + echo "Comment in bundle\n" >> $@; \ + done + +test-certs: $(PKCS8KEYS) $(RSAKEYS) $(ECKEYS) $(RSACERTS) $(ECCERTS) $(PKCS12CERTS) $(certsdir)/cert-bundle.pem $(certsdir)/cert-bundle-with-comments.pem tst/softhsm2.conf test-clean: rm -f $(RSAKEYS) $(ECKEYS) @@ -74,3 +119,6 @@ test-clean: rm -f $(RSACERTS) $(ECCERTS) rm -f $(PKCS12CERTS) rm -f $(certsdir)/cert-bundle.pem + rm -f $(certsdir)/cert-with-comments.pem + rm -f tst/softhsm2.conf + rm -rf tst/softhsm/* diff --git a/README.md b/README.md index 77105d1..ab7c196 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ The project also comes with two bash scripts at its root, called `generate-certs ### read-certificate-data -Reads a certificate that is on disk. Either the path to the certificate on disk is provided with the `--certificate` parameter, or the `--cert-selector` flag is provided to select a certificate within an OS certificate store. Further details about the flag are provided below. +Reads a certificate. Either the path to the certificate on disk or PKCS#11 URI to identify the certificate is provided with the `--certificate` parameter, or the `--cert-selector` flag is provided to select a certificate within an OS certificate store. Further details about the `--cert-selector` flag are provided below. -If there are multiple certificates that match a given `--cert-selector`, information about each of them is printed. +If there are multiple certificates that match a given `--cert-selector` or PKCS#11 URI (as specified through the `--certificate` parameter), information about each of them is printed. For PKCS#11, URIs for each matched certificate is also printed in the hopes that it will be useful in uniquely identifying a certificate. #### cert-selector flag @@ -79,7 +79,7 @@ Signs a fixed strings: `"AWS Roles Anywhere Credential Helper Signing Test" || S ### credential-process -Vends temporary credentials by sending a `CreateSession` request to the Roles Anywhere service. The request is signed by the private key whose path can be provided with the `--private-key` parameter. Other parameters include `--certificate` (the path to the end-entity certificate), `--role-arn` (the ARN of the role to obtain temporary credentials for), `--profile-arn` (the ARN of the profile that provides a mapping for the specified role), and `--trust-anchor-arn` (the ARN of the trust anchor used to authenticate). Optional parameters that can be used are `--debug` (to provide debugging output about the request sent), `--no-verify-ssl` (to skip verification of the SSL certificate on the endpoint called), `--intermediates` (the path to intermediate certificates), `--with-proxy` (to make the binary proxy aware), `--endpoint` (the endpoint to call), `--region` (the region to scope the request to), and `--session-duration` (the duration of the vended session). Instead of passing in paths to the plaintext private key on your file system, another option (depending on your OS) could be to use the `--cert-selector` flag. More details can be found below. +Vends temporary credentials by sending a `CreateSession` request to the Roles Anywhere service. The request is signed by the private key whose path can be provided with the `--private-key` parameter. Currently, only plaintext private keys are supported. Other parameters include `--certificate` (the path to the end-entity certificate), `--role-arn` (the ARN of the role to obtain temporary credentials for), `--profile-arn` (the ARN of the profile that provides a mapping for the specified role), and `--trust-anchor-arn` (the ARN of the trust anchor used to authenticate). Optional parameters that can be used are `--debug` (to provide debugging output about the request sent), `--no-verify-ssl` (to skip verification of the SSL certificate on the endpoint called), `--intermediates` (the path to intermediate certificates), `--with-proxy` (to make the binary proxy aware), `--endpoint` (the endpoint to call), `--region` (the region to scope the request to), and `--session-duration` (the duration of the vended session). Instead of passing in paths to the plaintext private key on your file system, another option (depending on your OS) could be to use the `--cert-selector` flag. More details can be found below. Note that if more than one certificate matches the `--cert-selector` parameter within the OS-specific secure store, the `credential-process` command will fail. To find the list of certificates that match a given `--cert-selector` parameter, you can use the same flag with the `read-certificate-data` command. @@ -133,6 +133,78 @@ The above command will import the PFX file into the user's "MY" certificate stor Also note that the above step can be done through a [Powershell cmdlet](https://learn.microsoft.com/en-us/powershell/module/pki/import-pfxcertificate?view=windowsserver2022-ps) or through [Windows CNG/Cryptography APIs](https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-pfximportcertstore). +#### PKCS#11 Integration + +As you should expect from all applications which use keys and certificates, you can simply give a +[PKCS#11 URI](https://datatracker.ietf.org/doc/html/rfc7512) in place of a filename in order to +use certificates and/or keys from hardware or software PKCS#11 tokens / HSMs. A hybrid mode +using a certificate from a file but only the key in the token is also supported. Some examples: + + * `--certificate 'pkcs11:manufacturer=piv_II;id=%01'` + * `--certificate 'pkcs11:object=My%20RA%20key'` + * `--certificate client-cert.pem --private-key 'pkcs11:model=SoftHSM%20v2;object=My%20RA%20key'` + +Some documentation which may assist with finding the correct URI for +your key can be found [here](https://www.infradead.org/openconnect/pkcs11.html). Otherwise, you +can also potentially scope down your PKCS#11 URI by using the `read-certificate-data` diagnostic +command. + +Most Linux and similar *nix systems use +[p11-kit](https://p11-glue.github.io/p11-glue/p11-kit/manual/config.html) +to provide consistent system-wide and per-user configuration of +available PKCS#11 providers. Any properly packaged provider module +will register itself with p11-kit and will be automatically visible +through the `p11-kit-proxy.{dylib, dll, so}` provider which is used by default. + +If you have a poorly packaged provider module from a vendor, then +after you have filed a bug, you can manually create a p11-kit [module +file](https://p11-glue.github.io/p11-glue/p11-kit/manual/pkcs11-conf.html) +for it. + +For systems or containers which lack p11-kit, a specific PKCS#11 +provider library can be specified using the `--pkcs11-lib` parameter. + +The other relevant parameter is `--reuse-pin`. This is a boolean parameter that can +be specified if the private key object you would like to use to sign data has the +`CKA_ALWAYS_AUTHENTICATE` attribute set and the `CKU_CONTEXT_SPECIFIC` PIN for the +object matches the `CKU_USER` PIN. If this parameter isn't set, you will be prompted +to provide the `CKU_CONTEXT_SPECIFIC` PIN for the object through the console. If this +parameter is set and the `CKU_USER` PIN doesn't match the `CKU_CONTEXT_SPECIFIC` PIN, +the credential helper application will fall back to prompting you. In an unattended +scenario, this flag is very helpful. There is currently no way in which to specify +the `CKU_CONTEXT_SPECIFIC` PIN without being prompted for it, so you are out of luck +for the time being when it comes to unattended workloads if the `CKU_CONTEXT_SPECIFIC` +PIN of the private key object you want to use is different from the `CKU_USER` PIN of +the token that it belongs to. + +The searching methodology used to find objects within PKCS#11 tokens can largely be found +[here](https://datatracker.ietf.org/doc/html/draft-woodhouse-cert-best-practice-01). Do note +that there are some slight differences in how objects are found in the credential helper +application. + +#### Other Notes + +##### YubiKey Attestation Certificates + +Note that if you're using a YubiKey device with PIV support, when a key pair +and certificate exist in slots 9a or 9c (PIV authentication and digital signature, +respectively), the YubiKey will automatically generate an attestation certificate +for the slot. Testing has shown that the attestation certificate can't be deleted. +In this case, if you attempt to use the `CKA_ID` (the `id` path attribute in a URI) +of your certificate to identify it in your supplied PKCS#11 URI, there will be +two certificates that match. One way in which you can disambiguate between the +two in your PKCS#11 URI can be through `CKA_LABEL` (the `object` path attribute +in a URI). Attestation certificates in either of these two slots can be +identified through the hard-coded labels, `X.509 Certificate for PIV Attestation +9a` or `X.509 Certificate for PIV Attestation 9c`. + +##### Implementation Note + +Due to this package's use of a dependency to integrate with PKCS#11 modules, we are unable +to guarantee that PINs are zeroized in memory after they are no longer needed. We will continue +to explore options to overcome this. Customers are encouraged to study the impact of this limitation +and determine whether compensating controls are warranted for their system and threat model. + ### update Updates temporary credentials in the [credential file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). Parameters for this command include those for the `credential-process` command, as well as `--profile`, which specifies the named profile for which credentials should be updated (if the profile doesn't already exist, it will be created), and `--once`, which specifies that credentials should be updated only once. Both arguments are optional. If `--profile` isn't specified, the default profile will have its credentials updated, and if `--once` isn't specified, credentials will be continuously updated. In this case, credentials will be updated through a call to `CreateSession` five minutes before the previous set of credentials are set to expire. Please note that running the `update` command multiple times, creating multiple processes, may not work as intended. There may be issues with concurrent writes to the credentials file. diff --git a/THIRD-PARTY-LICENSES.txt b/THIRD-PARTY-LICENSES.txt index 56b0638..4685d1b 100644 --- a/THIRD-PARTY-LICENSES.txt +++ b/THIRD-PARTY-LICENSES.txt @@ -1,6 +1,1123 @@ +** go-pkcs11uri; version v0.0.0-20230614165346-c1cad3d2f68c -- github.com/stefanberger/go-pkcs11uri + + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +* For go-pkcs11uri see also this required NOTICE: + (c) Copyright IBM Corporation, 2020 + +------ + +** Cobra; version v1.6.1 -- https://github.com/spf13/cobra + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. +* For Cobra see also this required NOTICE: + Copyright 2013-2023 The Cobra Authors + +------ + +** yaml; version v2.4.0 -- https://github.com/go-yaml/yaml/tree/v2.4.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +* For yaml see also this required NOTICE: + Copyright (c) 2011-2019 Canonical Ltd + Copyright (c) 2006-2010 Kirill Simonov + +------ + +** mousetrap; version v1.0.1 -- https://github.com/inconshreveable/mousetrap + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Alan Shreve (@inconshreveable) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +* For mousetrap see also this required NOTICE: + N/A + +------ + +** go-jmespath; version v0.4.0 -- https://github.com/jmespath/go-jmespath + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +* For go-jmespath see also this required NOTICE: + Copyright 2015 James Saryerwinnie + +------ + +** sys; version v0.10.0 -- https://cs.opensource.google/go/x/sys +Copyright (c) 2009 The Go Authors. All rights reserved. +** term; version v0.10.0 -- https://cs.opensource.google/go/x/term +Copyright (c) 2009 The Go Authors. All rights reserved. +** crypto; version v0.10.0 -- https://cs.opensource.google/go/x/crypto +Copyright (c) 2009 The Go Authors. All rights reserved. + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------ + +** pkcs11; version v1.1.1 -- github.com/miekg/pkcs11 +Copyright (c) 2013 Miek Gieben. All rights reserved. + +Copyright (c) 2013 Miek Gieben. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Miek Gieben nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------ + +** pflag; version v1.0.5 -- github.com/spf13/pflag +Copyright (c) 2012 Alex Ogier. All rights reserved. +Copyright (c) 2012 The Go Authors. All rights reserved. + +Copyright (c) 2012 Alex Ogier. All rights reserved. +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------ + +** go-spew; version v1.1.1 -- https://github.com/davecgh/go-spew +Copyright (c) 2012-2016 Dave Collins + +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------ + ** smimesign; version v0.2.0-rc1 -- https://github.com/github/smimesign Copyright (c) 2017 GitHub, Inc. - + MIT License Copyright (c) 2017 GitHub, Inc. @@ -22,4 +1139,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/TODO.pkcs11 b/TODO.pkcs11 new file mode 100644 index 0000000..68c81a3 --- /dev/null +++ b/TODO.pkcs11 @@ -0,0 +1,36 @@ + + + +TODO for PKCS#11 support + + • Clean up the support for entering PIN interactively, perhaps add URI + pin-source= support + + • Automated testing, perhaps by pulling in the prepopulated SoftHSM tokens + used for openconnect testing. I've manually tested those and Yubikey PIV + URIs including the following as --private-key arguments: + • pkcs11:token=openconnect-test;object=RSA;pin-value=1234 + • pkcs11:token=openconnect-test;object=EC;pin-value=1234 + Repeat those as --certificate in order to test cert to key matching. + Repeat (as both key and cert arguments) with openconnect-test[123] tokens + which are torture tests for various things seen in the wild (no pubkey, + having to log in to the token, and tokens lacking CKF_LOGIN_REQUIRED). + + My Yubikey is old and doesn't have EC support, but it's useful for testing + the CKA_ALWAYS_AUTHENTICATE support (on the Digital Signature key) and the + matching by CKA_ID when CKA_LABEL doesn't match. So (assuming you provision + the slot) this should work as both --private-key and --certificate: + • pkcs11:manufacturer=piv_II;id=%02;pin-value=123456 + And this should work as --certificate, correctly finding the key: + • pkcs11:manufacturer=piv_II;object=Certificate%20for%20Digital%20Signature?pin-value=123456 + + Check the output by using 'openssl dgst -verify', for example: + + $ build/bin/aws_signing_helper sign-string --certificate pkcs11:token=openconnect-test\;object=RSA?pin-value=1234 <<< Test | xxd -r -ps > testsig + $ openssl dgst -sha256 -engine pkcs11 -keyform engine -verify 'pkcs11:token=openconnect-test;object=RSA?pin-value=1234' -signature testsig <<< Test + + +References: + http://david.woodhou.se/draft-woodhouse-cert-best-practice.html#rfc.section.8 + https://datatracker.ietf.org/doc/html/rfc7512 + https://gitlab.com/openconnect/openconnect/-/blob/v9.12/openssl-pkcs11.c diff --git a/aws_signing_helper/credentials.go b/aws_signing_helper/credentials.go index d8929c7..c918b4e 100644 --- a/aws_signing_helper/credentials.go +++ b/aws_signing_helper/credentials.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "encoding/base64" "errors" + "log" "net/http" "runtime" @@ -30,11 +31,12 @@ type CredentialsOpts struct { Debug bool Version string LibPkcs11 string + ReusePin bool } // Function to create session and generate credentials func GenerateCredentials(opts *CredentialsOpts, signer Signer, signatureAlgorithm string) (CredentialProcessOutput, error) { - // assign values to region and endpoint if they haven't already been assigned + // Assign values to region and endpoint if they haven't already been assigned trustAnchorArn, err := arn.Parse(opts.TrustAnchorArnStr) if err != nil { return CredentialProcessOutput{}, err @@ -87,7 +89,10 @@ func GenerateCredentials(opts *CredentialsOpts, signer Signer, signatureAlgorith } certificateChain, err := signer.CertificateChain() if err != nil { - return CredentialProcessOutput{}, errors.New("unable to find certificate chain") + // If the chain couldn't be found, don't include it in the request + if Debug { + log.Println(err) + } } rolesAnywhereClient.Handlers.Sign.PushBackNamed(request.NamedHandler{Name: "v4x509.SignRequestHandler", Fn: CreateRequestSignFunction(signer, signatureAlgorithm, certificate, certificateChain)}) diff --git a/aws_signing_helper/darwin_cert_store_signer.go b/aws_signing_helper/darwin_cert_store_signer.go index a83735c..40be35d 100644 --- a/aws_signing_helper/darwin_cert_store_signer.go +++ b/aws_signing_helper/darwin_cert_store_signer.go @@ -22,7 +22,7 @@ import ( "errors" "fmt" "io" - "os" + "log" "unsafe" ) @@ -104,7 +104,7 @@ func GetMatchingCertsAndIdentity(certIdentifier CertIdentifier) (C.SecIdentityRe } if Debug { - fmt.Fprintf(os.Stderr, "found %d matching identities\n", len(certContainers)) + log.Printf("found %d matching identities\n", len(certContainers)) } // Only retain the SecIdentityRef if it should be used later on diff --git a/aws_signing_helper/file_system_signer.go b/aws_signing_helper/file_system_signer.go index e793543..ba0e365 100644 --- a/aws_signing_helper/file_system_signer.go +++ b/aws_signing_helper/file_system_signer.go @@ -8,10 +8,8 @@ import ( "crypto/sha512" "crypto/x509" "errors" - "golang.org/x/crypto/pkcs12" "io" "log" - "os" ) type FileSystemSigner struct { @@ -36,8 +34,7 @@ func (fileSystemSigner FileSystemSigner) Public() crypto.PublicKey { return nil } -func (fileSystemSigner FileSystemSigner) Close() { -} +func (fileSystemSigner FileSystemSigner) Close() {} func (fileSystemSigner FileSystemSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { var hash []byte @@ -83,8 +80,7 @@ func (fileSystemSigner FileSystemSigner) CertificateChain() ([]*x509.Certificate return fileSystemSigner.certChain, nil } -// Returns a FileSystemSigner, that signs a payload using the -// private key passed in +// Returns a FileSystemSigner, that signs a payload using the private key passed in func GetFileSystemSigner(privateKey crypto.PrivateKey, certificate *x509.Certificate, certificateChain []*x509.Certificate) (signer Signer, signingAlgorithm string, err error) { // Find the signing algorithm _, isRsaKey := privateKey.(rsa.PrivateKey) @@ -102,31 +98,3 @@ func GetFileSystemSigner(privateKey crypto.PrivateKey, certificate *x509.Certifi return FileSystemSigner{privateKey, certificate, certificateChain}, signingAlgorithm, nil } - -func GetPKCS12Signer(certificateId string) (signer Signer, signingAlgorithm string, err error) { - bytes, err := os.ReadFile(certificateId) - if err != nil { - return nil, "", err - } - privateKey, certificate, err := pkcs12.Decode(bytes, "") - if err != nil { - return nil, "", err - } - if privateKey == nil { - return nil, "", errors.New("PKCS#12 has no private key") - } - - rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) - if ok { - signingAlgorithm = aws4_x509_rsa_sha256 - return FileSystemSigner{*rsaPrivateKey, certificate, nil}, signingAlgorithm, nil - } - - ecPrivateKey, ok := privateKey.(*ecdsa.PrivateKey) - if ok { - signingAlgorithm = aws4_x509_ecdsa_sha256 - return FileSystemSigner{*ecPrivateKey, certificate, nil}, signingAlgorithm, nil - } - - return nil, "", errors.New("unsupported algorithm on PKCS#12 key") -} diff --git a/aws_signing_helper/pkcs11_signer.go b/aws_signing_helper/pkcs11_signer.go new file mode 100644 index 0000000..b07e74e --- /dev/null +++ b/aws_signing_helper/pkcs11_signer.go @@ -0,0 +1,1330 @@ +package aws_signing_helper + +// RFC7512 defines a standard URI format for referencing PKCS#11 objects. +// +// Decent applications should silently accept these in place of a file name, +// and Do The Right Thing. There should be no additional configuration or +// anything else to confuse the user. +// +// Users shouldn't even need to specify the PKCS#11 "provider" library, as +// most systems should use p11-kit for that. Properly packaged providers +// will ship with a p11-kit 'module' file which makes them discoverable. +// +// p11-kit has system-wide and per-user configuration for providers, and +// automatically makes all the discovered tokens available through the +// "p11-kit-proxy.so" provider module. We just use *that* by default. +// +// So all the user should ever have to do is something like +// --private-key pkcs11:manufacturer=piv_II;id=%01 +// or --certificate pkcs11:object=Certificate%20for%20Digital%20Signature?pin-value=123456 +// +// The PKCS#11 URI is a bit of a misnomer; it's not really a unique +// identifier — it's more of a search term; specifying the constraints +// which must match either the token or the object therein. Some rules +// for how you apply those search constraints, and in particular where +// you look for a matching private key after finding a certificate, are +// at http://david.woodhou.se/draft-woodhouse-cert-best-practice.html#rfc.section.8.2 +// +// This code is based on the C implementation at +// https://gitlab.com/openconnect/openconnect/-/blob/v9.12/openssl-pkcs11.c +// +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "errors" + "fmt" + "io" + "log" + "os" + "runtime" + "strconv" + "strings" + "unsafe" + + "github.com/miekg/pkcs11" + pkcs11uri "github.com/stefanberger/go-pkcs11uri" + "golang.org/x/term" +) + +var PKCS11_TEST_VERSION int16 = 1 +var MAX_OBJECT_LIMIT int = 1000 + +// In our list of certs, we want to remember the CKA_ID/CKA_LABEL too. +type CertObjInfo struct { + id []byte + label []byte + cert *x509.Certificate + certObject pkcs11.ObjectHandle +} + +// In our list of keys, we want to remember the CKA_ID/CKA_LABEL too. +type KeyObjInfo struct { + id []byte + label []byte + keyObject pkcs11.ObjectHandle +} + +// Used to enumerate slots with all token/slot info for matching. +type SlotIdInfo struct { + id uint + info pkcs11.SlotInfo + tokInfo pkcs11.TokenInfo +} + +type PKCS11Signer struct { + cert *x509.Certificate + certChain []*x509.Certificate + module *pkcs11.Ctx + userPin string + alwaysAuth uint + contextSpecificPin string + certUri *pkcs11uri.Pkcs11URI + keyUri *pkcs11uri.Pkcs11URI + reusePin bool +} + +// Initialize a PKCS#11 module. +func initializePKCS11Module(lib string) (module *pkcs11.Ctx, err error) { + // In a properly configured system, nobody should need to override this. + if lib == "" { + switch runtime.GOOS { + case "darwin": + lib = "p11-kit-proxy.dylib" + case "windows": + lib = "p11-kit-proxy.dll" + default: + lib = "p11-kit-proxy.so" + } + } + + module = pkcs11.New(lib) + if module == nil { + err = errors.New("Failed to load provider library " + lib) + goto fail + } + if err = module.Initialize(); err != nil { + goto fail + } + + return module, nil + +fail: + if module != nil { + module.Finalize() + module.Destroy() + } + return nil, err +} + +// Enumerate slots in the PKCS#11 module. This method assumes that the +// module isn't nil and has been initialized. +func enumerateSlotsInPKCS11Module(module *pkcs11.Ctx) (slots []SlotIdInfo, err error) { + var slotIds []uint + + slotIds, err = module.GetSlotList(true) + if err != nil { + return nil, err + } + + for _, slotId := range slotIds { + var slotIdInfo SlotIdInfo + var slotErr error + + slotIdInfo.id = slotId + slotIdInfo.info, slotErr = module.GetSlotInfo(slotId) + if slotErr != nil { + if Debug { + log.Printf("unable to get slot info for slot %d"+ + " (%s)\n", slotId, slotErr) + } + continue + } + slotIdInfo.tokInfo, slotErr = module.GetTokenInfo(slotId) + if slotErr != nil { + if Debug { + log.Printf("unable to get token info for slot %d"+ + " (%s)\n", slotId, slotErr) + } + continue + } + + slots = append(slots, slotIdInfo) + } + + return slots, nil +} + +// Return true if the URI specifies the attribute and it *doesn't* match. +func mismatchAttr(uri *pkcs11uri.Pkcs11URI, attr string, val string) bool { + var ( + uriVal string + ok bool + ) + + uriVal, ok = uri.GetPathAttribute(attr, false) + return ok && uriVal != val +} + +// Return the set of slots which match the given URI. +func matchSlots(slots []SlotIdInfo, uri *pkcs11uri.Pkcs11URI) (matches []SlotIdInfo) { + var ( + uriSlotNr uint64 + uriSlot string + ok bool + ) + + if uri == nil { + return slots + } + + uriSlot, ok = uri.GetPathAttribute("slot-id", false) + if ok { + uriSlotNr, _ = strconv.ParseUint(uriSlot, 0, 32) + } + + for _, slot := range slots { + if uriSlotNr != 0 && uriSlotNr != uint64(slot.id) { + continue + } + if mismatchAttr(uri, "token", slot.tokInfo.Label) || + mismatchAttr(uri, "model", slot.tokInfo.Model) || + mismatchAttr(uri, "manufacturer", slot.tokInfo.ManufacturerID) || + mismatchAttr(uri, "serial", slot.tokInfo.SerialNumber) || + mismatchAttr(uri, "slot-description", slot.info.SlotDescription) || + mismatchAttr(uri, "slot-manufacturer", slot.info.ManufacturerID) { + continue + } + matches = append(matches, slot) + } + + return matches +} + +// Convert the object-related fields in a URI to []*pkcs11.Attribute for FindObjectsInit(). +func getFindTemplate(uri *pkcs11uri.Pkcs11URI, class uint) (template []*pkcs11.Attribute) { + var ( + v string + ok bool + ) + + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_CLASS, class)) + + if uri == nil { + return template + } + + v, ok = uri.GetPathAttribute("object", false) + if ok { + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_LABEL, v)) + } + v, ok = uri.GetPathAttribute("id", false) + if ok { + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_ID, v)) + } + return template +} + +// Gets certificate(s) within the PKCS#11 session (i.e. a given token) that +// matches the given URI. +func getCertsInSession(module *pkcs11.Ctx, slotId uint, session pkcs11.SessionHandle, uri *pkcs11uri.Pkcs11URI) (certs []CertObjInfo, err error) { + var ( + sessionCertObjects []pkcs11.ObjectHandle + certObjects []pkcs11.ObjectHandle + templateCrt []*pkcs11.Attribute + ) + + // Convert the URI into a template for FindObjectsInit(). + templateCrt = getFindTemplate(uri, pkcs11.CKO_CERTIFICATE) + + if err = module.FindObjectsInit(session, templateCrt); err != nil { + return nil, err + } + + for true { + sessionCertObjects, _, err = module.FindObjects(session, MAX_OBJECT_LIMIT) + if err != nil { + return nil, err + } + if len(sessionCertObjects) == 0 { + break + } + certObjects = append(certObjects, sessionCertObjects...) + if len(sessionCertObjects) < MAX_OBJECT_LIMIT { + break + } + } + + err = module.FindObjectsFinal(session) + if err != nil { + return nil, err + } + + for _, certObject := range certObjects { + crtAttributes := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_VALUE, 0), + } + if crtAttributes, err = module.GetAttributeValue(session, certObject, crtAttributes); err != nil { + return nil, err + } + + rawCert := crtAttributes[0].Value + + var certObj CertObjInfo + + certObj.certObject = certObject + + certObj.cert, err = x509.ParseCertificate(rawCert) // nosemgrep + if err != nil { + return nil, errors.New("error parsing certificate") + } + + // Fetch the CKA_ID and CKA_LABEL of the matching cert(s), so + // that they can be used later when hunting for the matching + // key. + crtAttributes[0] = pkcs11.NewAttribute(pkcs11.CKA_ID, 0) + crtAttributes, err = module.GetAttributeValue(session, certObject, crtAttributes) + if err == nil { + certObj.id = crtAttributes[0].Value + } + + crtAttributes[0] = pkcs11.NewAttribute(pkcs11.CKA_LABEL, 0) + crtAttributes, err = module.GetAttributeValue(session, certObject, crtAttributes) + if err == nil { + certObj.label = crtAttributes[0].Value + } + + certs = append(certs, certObj) + } + + return certs, nil +} + +// Scan all matching slots to until we find certificates that match the URI. +// If there is at least one matching certificate found, the returned session +// will be left open and returned. The session may also be logged in to in the +// case that the certificate being searched for could only be found after +// logging in to the token. +// +// NB: It's generally only looking for *one* cert to use. If you want +// `p11tool --list-certificates`, use that instead. +func getMatchingCerts(module *pkcs11.Ctx, slots []SlotIdInfo, uri *pkcs11uri.Pkcs11URI, userPin string, single bool) (matchedSlot SlotIdInfo, session pkcs11.SessionHandle, loggedIn bool, matchingCerts []CertObjInfo, err error) { + var ( + errNoMatchingCerts error + ) + + errNoMatchingCerts = errors.New("no matching certificates") + + if uri != nil { + slots = matchSlots(slots, uri) + } + + // http://david.woodhou.se/draft-woodhouse-cert-best-practice.html#rfc.section.8.1 + // + // "For locating certificates, applications first iterate over the + // available tokens without logging in to them. In each token which + // matches the provided PKCS#11 URI, a search is performed for + // matching certificate objects." + for _, slot := range slots { + curSession, err := module.OpenSession(slot.id, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKS_RO_PUBLIC_SESSION) + if err != nil { + if Debug { + log.Printf("unable to open session in slot %d"+ + " (%s)\n", slot.id, err) + } + module.CloseSession(curSession) + continue + } + + curMatchingCerts, err := getCertsInSession(module, slot.id, curSession, uri) + if err == nil && len(curMatchingCerts) > 0 { + matchingCerts = append(matchingCerts, curMatchingCerts...) + // We only care about this value when there is a single matching + // certificate found. + if matchedSlot == (SlotIdInfo{}) { + matchedSlot = slot + session = curSession + goto skipCloseSession + } + } + module.CloseSession(curSession) + skipCloseSession: + } + + if len(matchingCerts) >= 1 { + goto foundCert + } + + // http://david.woodhou.se/draft-woodhouse-cert-best-practice.html#rfc.section.8.1 + // + // "If no match is found, and precisely one token was matched by the + // specified URI, then the application attempts to log in to that + // token using a PIN [...]. Another search is performed for matching + // objects, which this time will return even any certificate objects + // with the CKA_PRIVATE attribute. Is it important to note that the + // login should only be attempted if there is precisely one token + // which matches the URI, and not if there are multiple possible + // tokens in which the object could reside." + if len(slots) == 1 { + if userPin != "" { + curSession, err := module.OpenSession(slots[0].id, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKS_RO_PUBLIC_SESSION) + if err != nil { + err = errNoMatchingCerts + goto fail + } + + err = module.Login(curSession, pkcs11.CKU_USER, userPin) + if err != nil { + err = errNoMatchingCerts + goto fail + } + + curMatchingCerts, err := getCertsInSession(module, slots[0].id, curSession, uri) + if err == nil && len(curMatchingCerts) > 0 { + matchingCerts = append(matchingCerts, curMatchingCerts...) + // We only care about this value when there is a single matching + // certificate found. + if session == 0 { + loggedIn = true + matchedSlot = slots[0] + session = curSession + goto foundCert + } + } + } else { + err = errors.New("one matching slot, but no user PIN provided") + goto fail + } + } else if len(slots) == 0 { + err = errors.New("no matching slots") + goto fail + } else { + err = errors.New("multiple matching slots") + goto fail + } + +foundCert: + if single && len(matchingCerts) > 1 { + err = errors.New("multiple matching certificates") + goto fail + } + + // Exactly one matching certificate after logging in to the appropriate token + // iff single is true (otherwise there can be multiple matching certificates). + return matchedSlot, session, loggedIn, matchingCerts, nil + +fail: + if session != 0 { + module.Logout(session) + module.CloseSession(session) + } + return SlotIdInfo{}, session, false, nil, err +} + +// Used to implement a cut-down version of `p11tool --list-certificates`. +func GetMatchingPKCSCerts(uriStr string, lib string) (matchingCerts []CertificateContainer, err error) { + var ( + slots []SlotIdInfo + module *pkcs11.Ctx + uri *pkcs11uri.Pkcs11URI + userPin string + certObjs []CertObjInfo + session pkcs11.SessionHandle + loggedIn bool + slot SlotIdInfo + ) + + uri = pkcs11uri.New() + err = uri.Parse(uriStr) + if err != nil { + return nil, err + } + + userPin, _ = uri.GetQueryAttribute("pin-value", false) + + module, err = initializePKCS11Module(lib) + if err != nil { + goto cleanUp + } + + slots, err = enumerateSlotsInPKCS11Module(module) + if err != nil { + goto cleanUp + } + + slot, session, loggedIn, certObjs, err = getMatchingCerts(module, slots, uri, userPin, false) + if err != nil { + goto cleanUp + } + + for _, obj := range certObjs { + curUri := pkcs11uri.New() + curUri.AddPathAttribute("model", slot.tokInfo.Model) + curUri.AddPathAttribute("manufacturer", slot.tokInfo.ManufacturerID) + curUri.AddPathAttribute("serial", slot.tokInfo.SerialNumber) + curUri.AddPathAttribute("slot-description", slot.info.SlotDescription) + curUri.AddPathAttribute("slot-manufacturer", slot.info.ManufacturerID) + if obj.id != nil { + curUri.AddPathAttribute("id", string(obj.id[:])) + } + if obj.label != nil { + curUri.AddPathAttribute("object", string(obj.label[:])) + } + curUri.AddPathAttribute("type", "cert") + curUriStr, err := curUri.Format() // nosemgrep + if err != nil { + curUriStr = "" + } + matchingCerts = append(matchingCerts, CertificateContainer{obj.cert, curUriStr}) + } + + // Note that this clean up should happen regardless of failure. +cleanUp: + if module != nil { + if session != 0 { + if loggedIn { + module.Logout(session) + } + module.CloseSession(session) + } + module.Finalize() + module.Destroy() + } + + return matchingCerts, err +} + +// Returns the public key associated with this PKCS11Signer. +func (pkcs11Signer *PKCS11Signer) Public() crypto.PublicKey { + var ( + cert *x509.Certificate + err error + certUri *pkcs11uri.Pkcs11URI + ) + + certUri = pkcs11Signer.certUri + if certUri == nil { + return nil + } + + cert, err = pkcs11Signer.Certificate() + if err == nil { + return cert.PublicKey + } + + return nil +} + +// Closes this PKCS11Signer. +func (pkcs11Signer *PKCS11Signer) Close() { + var module *pkcs11.Ctx + + module = pkcs11Signer.module + + if module != nil { + module.Finalize() + module.Destroy() + } + + pkcs11Signer.module = nil +} + +// Does PIN prompting until the password has been received. +// This method is used both for prompting for the user PIN and the +// context-specific PIN. Note that finalAuthErrMsg should contain a +// `%s` so that the actual error message can be included. +func pkcs11PasswordPrompt(module *pkcs11.Ctx, session pkcs11.SessionHandle, userType uint, passwordName string, finalAuthErrMsg string) (pinValue string, err error) { + var ( + parseErrMsg string + pin string + prompt string + ttyReadPath string + ttyWritePath string + ttyReadFile *os.File + ttyWriteFile *os.File + ) + + parseErrMsg = fmt.Sprintf("unable to read PKCS#11 %s", passwordName) + prompt = fmt.Sprintf("Please enter your %s:", passwordName) + + 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 "", errors.New(parseErrMsg) + } + defer ttyReadFile.Close() + + ttyWriteFile, err = os.OpenFile(ttyWritePath, os.O_WRONLY, 0) + if err != nil { + return "", errors.New(parseErrMsg) + } + defer ttyWriteFile.Close() + + for true { + pin, err = GetPassword(ttyReadFile, ttyWriteFile, prompt, parseErrMsg) + if err != nil && err.Error() == parseErrMsg { + continue + } + + err = module.Login(session, userType, pin) + if err != nil { + // Loop on failure in case the user mistyped their PIN. + if strings.Contains(err.Error(), "CKR_PIN_INCORRECT") { + prompt = fmt.Sprintf("Incorrect %s. Please re-enter your %s:", passwordName, passwordName) + continue + } + return "", fmt.Errorf(finalAuthErrMsg, err.Error()) + } + return pin, nil + } + + // The code should never reach here. + 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 + // you'll want to use something other than SHA256 in many cases. + // For TLSv1.3 the hash needs to precisely match the bit size of the + // curve, IIRC. And you'll need RSA-PSS too. You might find that + // ThalesIgnite/crypto11 has some of that. + // e.g. https://github.com/ThalesIgnite/crypto11/blob/master/rsa.go#L230 + var ( + mechanism uint + keyUri *pkcs11uri.Pkcs11URI + keyUriStr string + ) + + if keyType == pkcs11.CKK_EC { + switch hashFunc { + case crypto.SHA256: + hash := sha256.Sum256(digest) + digest = hash[:] + case crypto.SHA384: + hash := sha512.Sum384(digest) + digest = hash[:] + case crypto.SHA512: + hash := sha512.Sum512(digest) + digest = hash[:] + default: + return "", nil, ErrUnsupportedHash + } + mechanism = pkcs11.CKM_ECDSA + } else { + switch hashFunc { + case crypto.SHA256: + mechanism = pkcs11.CKM_SHA256_RSA_PKCS + case crypto.SHA384: + mechanism = pkcs11.CKM_SHA384_RSA_PKCS + case crypto.SHA512: + mechanism = pkcs11.CKM_SHA512_RSA_PKCS + default: + return "", nil, ErrUnsupportedHash + } + } + + err = module.SignInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(mechanism, nil)}, privateKeyObj.keyObject) + if err != nil { + return "", nil, fmt.Errorf("signing initiation failed (%s)", err.Error()) + } + + if alwaysAuth != 0 { + // Set the value for the context-specific PIN used to do the signing + // operation with this key. If the context-specific PIN wasn't specified + // in the input, and the "reuse PIN" option was set, try to use the + // user PIN as the context-specific PIN. + if contextSpecificPin == "" && userPin != "" && reusePin { + contextSpecificPin = userPin + } + if contextSpecificPin != "" { + err = module.Login(session, pkcs11.CKU_CONTEXT_SPECIFIC, contextSpecificPin) + if err == nil { + goto afterContextSpecificLogin + } else { + if Debug { + log.Printf("user re-authentication attempt failed (%s)\n", err.Error()) + } + } + } + + // If the context-specific PIN couldn't be derived, prompt the user for + // the context-specific PIN for this object. + keyUri = pkcs11uri.New() + keyUri.AddPathAttribute("model", slot.tokInfo.Model) + keyUri.AddPathAttribute("manufacturer", slot.tokInfo.ManufacturerID) + keyUri.AddPathAttribute("serial", slot.tokInfo.SerialNumber) + keyUri.AddPathAttribute("slot-description", slot.info.SlotDescription) + keyUri.AddPathAttribute("slot-manufacturer", slot.info.ManufacturerID) + if privateKeyObj.id != nil { + keyUri.AddPathAttribute("id", string(privateKeyObj.id[:])) + } + if privateKeyObj.label != nil { + keyUri.AddPathAttribute("object", string(privateKeyObj.label[:])) + } + keyUri.AddPathAttribute("type", "private") + keyUriStr, err = keyUri.Format() // nosemgrep + if err != nil { + keyUriStr = "" + } + passwordName := "context-specific PIN" + if keyUriStr != "" { + passwordName = fmt.Sprintf("context-specific PIN for private key object (%s)", keyUriStr) + } + finalAuthErrMsg := "user re-authentication failed (%s)" + contextSpecificPin, err = pkcs11PasswordPrompt(module, session, pkcs11.CKU_CONTEXT_SPECIFIC, passwordName, finalAuthErrMsg) + if err != nil { + return "", nil, err + } + } + +afterContextSpecificLogin: + sig, err := module.Sign(session, digest) + if err != nil { + return contextSpecificPin, nil, fmt.Errorf("signing failed (%s)", err.Error()) + } + + // Yay, we have to do the ASN.1 encoding of the R, S values ourselves. + if mechanism == pkcs11.CKM_ECDSA { + sig, err = encodeEcdsaSigValue(sig) + if err != nil { + return contextSpecificPin, nil, err + } + } + + return contextSpecificPin, sig, nil +} + +// Gets a handle to the private key object (along with some other information +// that may need to be saved). +func getPKCS11Key(module *pkcs11.Ctx, session pkcs11.SessionHandle, loggedIn bool, certUri *pkcs11uri.Pkcs11URI, keyUri *pkcs11uri.Pkcs11URI, noKeyUri bool, certSlotNr uint, certObj CertObjInfo, userPin string, contextSpecificPin string, reusePin bool, slots []SlotIdInfo) (_session pkcs11.SessionHandle, _userPin string, _keyUri *pkcs11uri.Pkcs11URI, keyType uint, privateKeyObj KeyObjInfo, slot SlotIdInfo, alwaysAuth uint, _contextSpecificPin string, err error) { + var ( + keySlot SlotIdInfo + manufacturerId string + templatePrivateKey []*pkcs11.Attribute + privateKeyObjects []pkcs11.ObjectHandle + keyAttributes []*pkcs11.Attribute + ) + + if keyUri == nil { + keyUri = certUri + noKeyUri = true + } + + if userPin == "" { + userPin, _ = keyUri.GetQueryAttribute("pin-value", false) + } + + // This time we're looking for a *single* slot, as we (presumably) + // will have to log in to access the key. + slots = matchSlots(slots, keyUri) + if len(slots) == 1 { + if certSlotNr != slots[0].id { + keySlot = slots[0] + manufacturerId = slots[0].info.ManufacturerID + if session != 0 { + if loggedIn { + module.Logout(session) + module.CloseSession(session) + } + } + loggedIn = false + session = 0 + } + } else { + if Debug { + log.Printf("Found %d matching slots for the PKCS#11 key\n", len(slots)) + } + // If the URI matched multiple slots *but* one of them is the + // one (certSlotNr) that the certificate was found in, then use + // that. + for _, slot := range slots { + if certSlotNr == slot.id { + keySlot = slot + manufacturerId = slot.info.ManufacturerID + goto got_slot + } + } + err = errors.New("Could not identify unique slot for PKCS#11 key") + goto fail + } + +got_slot: + if session == 0 { + session, err = module.OpenSession(keySlot.id, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKS_RO_PUBLIC_SESSION) + } + if err != nil { + goto fail + } + + // And *now* we fall back to prompting the user for a PIN if necessary. + if !loggedIn { + if userPin == "" { + passwordName := "user PIN" + finalAuthErrMsg := "user authentication failed (%s)" + userPin, err = pkcs11PasswordPrompt(module, session, pkcs11.CKU_USER, passwordName, finalAuthErrMsg) + if err != nil { + goto fail + } + } else { + err = module.Login(session, pkcs11.CKU_USER, userPin) + if err != nil { + goto fail + } + } + } + +retry_search: + templatePrivateKey = getFindTemplate(keyUri, pkcs11.CKO_PRIVATE_KEY) + + if err = module.FindObjectsInit(session, templatePrivateKey); err != nil { + goto fail + } + for true { + sessionPrivateKeyObjects, _, err := module.FindObjects(session, MAX_OBJECT_LIMIT) + if err != nil { + goto fail + } + if len(sessionPrivateKeyObjects) == 0 { + break + } + privateKeyObjects = append(privateKeyObjects, sessionPrivateKeyObjects...) + if len(sessionPrivateKeyObjects) < MAX_OBJECT_LIMIT { + break + } + } + if err = module.FindObjectsFinal(session); err != nil { + goto fail + } + + // If we found multiple keys, try them until we find the one + // that actually matches the cert. More realistically, there + // will be only one. Sanity check that it matches the cert. + for _, curPrivateKeyHandle := range privateKeyObjects { + keyAttributes = []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, 0), + } + if keyAttributes, err = module.GetAttributeValue(session, curPrivateKeyHandle, keyAttributes); err != nil { + continue + } + keyType, err = bytesToUint(keyAttributes[0].Value) + if err != nil { + goto fail + } + + keyAttributes[0] = pkcs11.NewAttribute(pkcs11.CKA_ALWAYS_AUTHENTICATE, 0) + keyAttributes, err = module.GetAttributeValue(session, curPrivateKeyHandle, keyAttributes) + if err == nil { + alwaysAuth, err = bytesToUint(keyAttributes[0].Value) + if err != nil { + goto fail + } + } else { + alwaysAuth = 0 + } + + var curPrivateKeyObj KeyObjInfo + curPrivateKeyObj.keyObject = curPrivateKeyHandle + + // Fetch the CKA_ID and CKA_LABEL of the current private key object, so + // that more specific attributes can be used to identify the private key + // when prompting for a context-specifc PIN (assuming the CKA_ALWAYS_AUTHENTICATE + // attribute is set on the private key object). + keyAttributes[0] = pkcs11.NewAttribute(pkcs11.CKA_ID, 0) + keyAttributes, err = module.GetAttributeValue(session, curPrivateKeyHandle, keyAttributes) + if err == nil { + curPrivateKeyObj.id = keyAttributes[0].Value + } + + keyAttributes[0] = pkcs11.NewAttribute(pkcs11.CKA_LABEL, 0) + keyAttributes, err = module.GetAttributeValue(session, curPrivateKeyHandle, keyAttributes) + if err == nil { + curPrivateKeyObj.label = keyAttributes[0].Value + } + + if certObj.cert == nil { + if len(privateKeyObjects) == 1 { + privateKeyObj = curPrivateKeyObj + break + } else { + err = errors.New("multiple matching private keys, but" + + " no certificate provided to match with") + goto fail + } + } + + var curContextSpecificPin string + privateKeyMatchesCert := false + curContextSpecificPin, privateKeyMatchesCert = checkPrivateKeyMatchesCert(module, session, keyType, userPin, alwaysAuth, "", reusePin, curPrivateKeyObj, keySlot, certObj.cert, manufacturerId) + if privateKeyMatchesCert { + privateKeyObj = curPrivateKeyObj + contextSpecificPin = curContextSpecificPin + break + } + } + + if privateKeyObj.keyObject == 0 { + /* "If the key is not found and the original search was by + * CKA_LABEL of the certificate, then repeat the search using + * the CKA_ID of the certificate that was actually found, but + * not requiring a CKA_LABEL match." + * + * http://david.woodhou.se/draft-woodhouse-cert-best-practice.html#rfc.section.8.2 + */ + if certObj.cert != nil { + if noKeyUri { + _, keyHadLabel := keyUri.GetPathAttribute("object", false) + if keyHadLabel { + if Debug { + log.Println("unable to find private key with CKA_LABEL;" + + " repeating the search using CKA_ID of the certificate" + + " without requiring a CKA_LABEL match") + } + keyUri.RemovePathAttribute("object") + keyUri.SetPathAttribute("id", escapeAll(certObj.id)) + goto retry_search + } + } + } + + err = errors.New("unable to find matching private key") + goto fail + } + + // So that hunting for the key can be more efficient in the future, + // return a key URI that has CKA_ID and CKA_LABEL appropriately set. + if privateKeyObj.id != nil { + keyUri.SetPathAttribute("id", escapeAll(privateKeyObj.id)) + } + if privateKeyObj.label != nil { + keyUri.SetPathAttribute("object", escapeAll(privateKeyObj.label)) + } + + return session, userPin, keyUri, keyType, privateKeyObj, keySlot, alwaysAuth, contextSpecificPin, nil + +fail: + return 0, "", nil, 0, KeyObjInfo{}, SlotIdInfo{}, 0, "", err +} + +// Gets the certificate in a token, given the URI that identifies the +// certificate. This method also optionally takes in a user PIN, which is +// only used (and prompted for, if not given and needed) if the token has to be +// logged in to, in order to obtain the certificate. +func getCertificate(module *pkcs11.Ctx, certUri *pkcs11uri.Pkcs11URI, userPin string) (certSlot SlotIdInfo, slots []SlotIdInfo, session pkcs11.SessionHandle, loggedIn bool, certObj CertObjInfo, err error) { + var ( + matchingCerts []CertObjInfo + ) + + slots, err = enumerateSlotsInPKCS11Module(module) + if err != nil { + return SlotIdInfo{}, nil, 0, false, CertObjInfo{}, err + } + + certSlot, session, loggedIn, matchingCerts, err = getMatchingCerts(module, slots, certUri, userPin, true) + if err != nil { + return SlotIdInfo{}, nil, 0, false, CertObjInfo{}, err + } + + return certSlot, slots, session, loggedIn, matchingCerts[0], nil +} + +// Implements the crypto.Signer interface and signs the passed in digest +func (pkcs11Signer *PKCS11Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + var ( + module *pkcs11.Ctx + session pkcs11.SessionHandle + certUri *pkcs11uri.Pkcs11URI + keyUri *pkcs11uri.Pkcs11URI + userPin string + contextSpecificPin string + privateKeyObj KeyObjInfo + keySlot SlotIdInfo + keyType uint + certSlotNr uint + certObj CertObjInfo + slots []SlotIdInfo + loggedIn bool + reusePin bool + alwaysAuth uint + certSlot SlotIdInfo + ) + + hashFunc := opts.HashFunc() + + module = pkcs11Signer.module + userPin = pkcs11Signer.userPin + alwaysAuth = pkcs11Signer.alwaysAuth + contextSpecificPin = pkcs11Signer.contextSpecificPin + certUri = pkcs11Signer.certUri + keyUri = pkcs11Signer.keyUri + reusePin = pkcs11Signer.reusePin + + // If a PKCS#11 URI was provided for the certificate, use it. + if certUri != nil { + certSlot, slots, session, loggedIn, certObj, err = getCertificate(module, certUri, userPin) + if err != nil { + goto cleanUp + } + certSlotNr = certSlot.id + } + + // Otherwise, if the certificate's PKCS#11 URI wasn't provided, enumerate slots. + if certUri == nil { + slots, err = enumerateSlotsInPKCS11Module(module) + if err != nil { + goto cleanUp + } + } + + session, userPin, keyUri, keyType, privateKeyObj, keySlot, alwaysAuth, contextSpecificPin, err = getPKCS11Key(module, session, loggedIn, certUri, keyUri, false, certSlotNr, certObj, userPin, contextSpecificPin, reusePin, slots) + if err != nil { + goto cleanUp + } + + contextSpecificPin, signature, err = signHelper(module, session, privateKeyObj, keySlot, userPin, alwaysAuth, contextSpecificPin, reusePin, keyType, digest, hashFunc) + if err != nil { + goto cleanUp + } else { + pkcs11Signer.contextSpecificPin = contextSpecificPin + } + + // Note that the session should be logged out of and closed even if there + // are no errors after the signing operation. +cleanUp: + if session != 0 { + if loggedIn { + module.Logout(session) + } + module.CloseSession(session) + } + + return signature, err +} + +// Gets the *x509.Certificate associated with this PKCS11Signer. +func (pkcs11Signer *PKCS11Signer) Certificate() (cert *x509.Certificate, err error) { + // If there was a certificate chain associated with this Signer, it + // should've been saved before. + cert = pkcs11Signer.cert + + // If the certificate was saved, return it. + if cert != nil { + return cert, nil + } + + return nil, errors.New("no certificate associated with signer") +} + +// Checks whether the first certificate issues the second. +func certIssues(issuer *x509.Certificate, candidate *x509.Certificate) bool { + roots := x509.NewCertPool() + roots.AddCert(issuer) + + opts := x509.VerifyOptions{ + Roots: roots, + } + + _, err := candidate.Verify(opts) + return err != nil +} + +// Gets the certificate chain from the given session. Certificates in the +// chain are obtained through public key signature verification. +func getCertificateChain(module *pkcs11.Ctx, session pkcs11.SessionHandle, cert *x509.Certificate) (certChain []*x509.Certificate, err error) { + var ( + certsFound []CertObjInfo + ) + + // The certificate chain starts with the passed in end-entity certificate. + certChain = append(certChain, cert) + + certsFound, err = getCertsInSession(module, 0, session, nil) + if err != nil { + return nil, err + } + + for true { + nextInChainFound := false + for i, curCert := range certsFound { + lastCertChainCert := certChain[len(certChain)-1] + if certIssues(curCert.cert, lastCertChainCert) { + nextInChainFound = true + certChain = append(certChain, curCert.cert) + + // Remove current cert, so that it won't be iterated again. + lastIndex := len(certsFound) - 1 + certsFound[i] = certsFound[lastIndex] + certsFound = certsFound[:lastIndex] + + break + } + } + if !nextInChainFound { + break + } + } + + return certChain, err +} + +// Gets the certificate chain associated with this PKCS11Signer. +func (pkcs11Signer *PKCS11Signer) CertificateChain() (certChain []*x509.Certificate, err error) { + certChain = pkcs11Signer.certChain + + // If there was a certificate chain associated with this Signer, it + // should've been saved before. + if certChain != nil { + return certChain, nil + } + + return nil, errors.New("no certificate chain associated with signer") +} + +// Checks whether the private key and certificate are associated with each other. +func checkPrivateKeyMatchesCert(module *pkcs11.Ctx, session pkcs11.SessionHandle, keyType uint, userPin string, alwaysAuth uint, contextSpecificPin string, reusePin bool, privateKeyObj KeyObjInfo, keySlot SlotIdInfo, certificate *x509.Certificate, manufacturerId string) (string, bool) { + var digestSuffix []byte + publicKey := certificate.PublicKey + ecdsaPublicKey, isEcKey := publicKey.(*ecdsa.PublicKey) + if isEcKey { + digestSuffixArr := sha256.Sum256(append([]byte("IAM RA"), elliptic.Marshal(ecdsaPublicKey, ecdsaPublicKey.X, ecdsaPublicKey.Y)...)) + digestSuffix = digestSuffixArr[:] + if keyType != pkcs11.CKK_EC { + return "", false + } + } + + rsaPublicKey, isRsaKey := publicKey.(*rsa.PublicKey) + if isRsaKey { + digestSuffixArr := sha256.Sum256(append([]byte("IAM RA"), x509.MarshalPKCS1PublicKey(rsaPublicKey)...)) + digestSuffix = digestSuffixArr[:] + if keyType != pkcs11.CKK_RSA { + return "", false + } + } + // "AWS Roles Anywhere Credential Helper PKCS11 Test" || PKCS11_TEST_VERSION || + // MANUFACTURER_ID || SHA256("IAM RA" || PUBLIC_KEY_BYTE_ARRAY) + digest := "AWS Roles Anywhere Credential Helper PKCS11 Test" + + strconv.Itoa(int(PKCS11_TEST_VERSION)) + manufacturerId + string(digestSuffix) + digestBytes := []byte(digest) + hash := sha256.Sum256(digestBytes) + + contextSpecificPin, signature, err := signHelper(module, session, privateKeyObj, keySlot, userPin, alwaysAuth, "", reusePin, keyType, digestBytes, crypto.SHA256) + if err != nil { + return "", false + } + + if isEcKey { + valid := ecdsa.VerifyASN1(ecdsaPublicKey, hash[:], signature) + return contextSpecificPin, valid + } + + if isRsaKey { + err := rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hash[:], signature) + return contextSpecificPin, err == nil + } + + return "", false +} + +// This is not my proudest moment. But there's no binary.NativeEndian. +func bytesToUint(b []byte) (res uint, err error) { + if len(b) == 1 { + return uint(b[0]), nil + } + if len(b) == 2 { + var p16 *uint16 + p16 = (*uint16)(unsafe.Pointer(&b[0])) + return uint(*p16), nil + } + if len(b) == 4 { + var p32 *uint32 + p32 = (*uint32)(unsafe.Pointer(&b[0])) + return uint(*p32), nil + } + if len(b) == 8 { + var p64 *uint64 + p64 = (*uint64)(unsafe.Pointer(&b[0])) + return uint(*p64), nil + } + return 0, errors.New("Unsupported integer size in bytesToUint") +} + +/* + * Lifted from pkcs11uri.go because it doesn't let us set an attribute + * from a []byte; only a pct-encoded string. + * https://github.com/stefanberger/go-pkcs11uri/issues/11 + */ + +// Upper character hex digits needed for pct-encoding. +const hexchar = "0123456789ABCDEF" + +// escapeAll pct-escapes all characters in the string. +func escapeAll(s []byte) string { + res := make([]byte, len(s)*3) + j := 0 + for i := 0; i < len(s); i++ { + c := s[i] + res[j] = '%' + res[j+1] = hexchar[c>>4] + res[j+2] = hexchar[c&0xf] + j += 3 + } + return string(res) +} + +// Given an optional certificate either as *x509.Certificate (because it was +// already found in a file) or as a PKCS#11 URI, and an optional private key +// PKCS#11 URI, return a PKCS11Signer that can be used to sign a payload +// through a PKCS#11-compatible cryptographic device. +func GetPKCS11Signer(libPkcs11 string, cert *x509.Certificate, certChain []*x509.Certificate, privateKeyId string, certificateId string, reusePin bool) (signer Signer, signingAlgorithm string, err error) { + var ( + module *pkcs11.Ctx + certObj CertObjInfo + session pkcs11.SessionHandle + loggedIn bool + keyType uint + contextSpecificPin string + userPin string + alwaysAuth uint + certSlotNr uint + certUri *pkcs11uri.Pkcs11URI + keyUri *pkcs11uri.Pkcs11URI + slots []SlotIdInfo + certSlot SlotIdInfo + noKeyUri bool + ) + + module, err = initializePKCS11Module(libPkcs11) + if err != nil { + goto fail + } + + // If a PKCS#11 URI was provided for the certificate, find it. + if cert == nil && certificateId != "" { + certUri = pkcs11uri.New() + err = certUri.Parse(certificateId) + if err != nil { + goto fail + } + userPin, _ = certUri.GetQueryAttribute("pin-value", false) + certSlot, slots, session, loggedIn, certObj, err = getCertificate(module, certUri, userPin) + if err != nil { + goto fail + } + certSlotNr = certSlot.id + cert = certObj.cert + + // So that hunting for the certificate can be more efficient in the future, + // update the cert URI that has CKA_ID and CKA_VALUE appropriately set. + crtAttributes := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_ID, 0), + } + crtAttributes, err = module.GetAttributeValue(session, certObj.certObject, crtAttributes) + if err == nil { + certUri.SetPathAttribute("id", escapeAll(crtAttributes[0].Value)) + } + + crtAttributes[0] = pkcs11.NewAttribute(pkcs11.CKA_LABEL, 0) + crtAttributes, err = module.GetAttributeValue(session, certObj.certObject, crtAttributes) + if err == nil { + certUri.SetPathAttribute("object", escapeAll(crtAttributes[0].Value)) + } + + if certChain == nil { + certChain, err = getCertificateChain(module, session, cert) + if err != nil { + goto fail + } + } + } else if cert != nil { + // Populate certObj, so that it can be used to find the matching private key. + certObj = CertObjInfo{nil, nil, cert, 0} + } + + // If an explicit private-key option was given, use it. Otherwise + // we look in the same place as the certificate URI as directed by + // http://david.woodhou.se/draft-woodhouse-cert-best-practice.html#rfc.section.8.2 + if privateKeyId != "" { + keyUri = pkcs11uri.New() + err = keyUri.Parse(privateKeyId) + if err != nil { + goto fail + } + } else { + certUriStr, _ := certUri.Format() + keyUri = pkcs11uri.New() + keyUri.Parse(certUriStr) + noKeyUri = true + } + if _userPin, ok := keyUri.GetQueryAttribute("pin-value", false); ok { + userPin = _userPin + } + + // If the certificate's PKCS#11 URI wasn't provided, enumerate slots. + if certificateId == "" { + slots, err = enumerateSlotsInPKCS11Module(module) + if err != nil { + goto fail + } + } + + session, userPin, keyUri, keyType, _, _, alwaysAuth, contextSpecificPin, err = getPKCS11Key(module, session, loggedIn, certUri, keyUri, noKeyUri, certSlotNr, certObj, userPin, "", reusePin, slots) + if err != nil { + goto fail + } + + switch keyType { + case pkcs11.CKK_EC: + signingAlgorithm = aws4_x509_ecdsa_sha256 + case pkcs11.CKK_RSA: + signingAlgorithm = aws4_x509_rsa_sha256 + default: + return nil, "", errors.New("unsupported algorithm") + } + + if session != 0 { + if loggedIn { + module.Logout(session) + } + module.CloseSession(session) + } + + return &PKCS11Signer{cert, certChain, module, userPin, alwaysAuth, contextSpecificPin, certUri, keyUri, reusePin}, signingAlgorithm, nil + +fail: + if module != nil { + if session != 0 { + if loggedIn { + module.Logout(session) + } + module.CloseSession(session) + } + module.Finalize() + module.Destroy() + } + + return nil, "", err +} diff --git a/aws_signing_helper/signer.go b/aws_signing_helper/signer.go index dd6f30f..8d174e0 100644 --- a/aws_signing_helper/signer.go +++ b/aws_signing_helper/signer.go @@ -25,6 +25,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" + "golang.org/x/crypto/pkcs12" ) type SignerParams struct { @@ -156,21 +157,24 @@ func encodeEcdsaSigValue(signature []byte) (out []byte, err error) { // Gets the Signer based on the flags passed in by the user (from which the CredentialsOpts structure is derived) func GetSigner(opts *CredentialsOpts) (signer Signer, signatureAlgorithm string, err error) { - var certificate *x509.Certificate - var certificateChain []*x509.Certificate + var ( + certificate *x509.Certificate + certificateChain []*x509.Certificate + privateKey crypto.PrivateKey + ) privateKeyId := opts.PrivateKeyId if privateKeyId == "" { if opts.CertificateId == "" { if Debug { - fmt.Fprintln(os.Stderr, "attempting to use CertStoreSigner") + log.Println("attempting to use CertStoreSigner") } return GetCertStoreSigner(opts.CertIdentifier) } privateKeyId = opts.CertificateId } - if opts.CertificateId != "" { + if opts.CertificateId != "" && !strings.HasPrefix(opts.CertificateId, "pkcs11:") { certificateData, err := ReadCertificateData(opts.CertificateId) if err == nil { certificateDerData, err := base64.StdEncoding.DecodeString(certificateData.CertificateData) @@ -183,10 +187,30 @@ func GetSigner(opts *CredentialsOpts) (signer Signer, signatureAlgorithm string, } } else if opts.PrivateKeyId == "" { if Debug { - fmt.Fprintln(os.Stderr, "not a PEM certificate, so trying PKCS#12") + log.Println("not a PEM certificate, so trying PKCS#12") + } + if opts.CertificateBundleId != "" { + return nil, "", errors.New("can't specify certificate chain when" + + " using PKCS#12 files; certificate bundle should be provided" + + " within the PKCS#12 file") } // Not a PEM certificate? Try PKCS#12 - return GetPKCS12Signer(opts.CertificateId) + certificateChain, privateKey, err = ReadPKCS12Data(opts.CertificateId) + if err != nil { + return nil, "", err + } + if privateKey != nil { + ecPrivateKeyPtr, isEcKey := privateKey.(*ecdsa.PrivateKey) + if isEcKey { + privateKey = *ecPrivateKeyPtr + } + + rsaPrivateKeyPtr, isRsaKey := privateKey.(*rsa.PrivateKey) + if isRsaKey { + privateKey = *rsaPrivateKeyPtr + } + } + return GetFileSystemSigner(privateKey, certificateChain[0], certificateChain) } else { return nil, "", err } @@ -202,15 +226,25 @@ func GetSigner(opts *CredentialsOpts) (signer Signer, signatureAlgorithm string, } } - privateKey, err := ReadPrivateKeyData(privateKeyId) - if err != nil { - return nil, "", err - } + if strings.HasPrefix(privateKeyId, "pkcs11:") { + if Debug { + log.Println("attempting to use PKCS11Signer") + } + if certificate != nil { + opts.CertificateId = "" + } + return GetPKCS11Signer(opts.LibPkcs11, certificate, certificateChain, opts.PrivateKeyId, opts.CertificateId, opts.ReusePin) + } else { + privateKey, err = ReadPrivateKeyData(privateKeyId) + if err != nil { + return nil, "", err + } - if Debug { - fmt.Fprintln(os.Stderr, "attempting to use FileSystemSigner") + if Debug { + log.Println("attempting to use FileSystemSigner") + } + return GetFileSystemSigner(privateKey, certificate, certificateChain) } - return GetFileSystemSigner(privateKey, certificate, certificateChain) } // Obtain the date-time, formatted as specified by SigV4 @@ -494,7 +528,7 @@ func ReadCertificateBundleData(certificateBundleId string) ([]*x509.Certificate, for len(bytes) > 0 { block, bytes = pem.Decode(bytes) if block == nil { - return nil, errors.New("unable to parse PEM data") + break } if block.Type != "CERTIFICATE" { return nil, errors.New("invalid certificate chain") @@ -555,7 +589,82 @@ func readPKCS8PrivateKey(privateKeyId string) (crypto.PrivateKey, error) { return *ecPrivateKey, nil } - return nil, errors.New("could not parse PKCS8 private key") + return nil, errors.New("could not parse PKCS#8 private key") +} + +// Reads and parses a PKCS#12 file (which should contain an end-entity +// certificate, (optional) certificate chain, and the key associated with the +// end-entity certificate). The end-entity certificate will be the first +// certificate in the returned chain. This method assumes that there is +// exactly one certificate that doesn't issue any others within the container +// and treats that as the end-entity certificate. Also, the order of the other +// certificates in the chain aren't guaranteed (it's also not guaranteed that +// those certificates form a chain with the end-entity certificat either). +func ReadPKCS12Data(certificateId string) (certChain []*x509.Certificate, privateKey crypto.PrivateKey, err error) { + var ( + bytes []byte + pemBlocks []*pem.Block + parsedCerts []*x509.Certificate + certMap map[string]*x509.Certificate + endEntityFoundIndex int + ) + + bytes, err = os.ReadFile(certificateId) + if err != nil { + return nil, nil, nil + } + + pemBlocks, err = pkcs12.ToPEM(bytes, "") + if err != nil { + return nil, "", err + } + + for _, block := range pemBlocks { + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + parsedCerts = append(parsedCerts, cert) + continue + } + privateKeyTmp, err := ReadPrivateKeyDataFromPEMBlock(block) + if err == nil { + privateKey = privateKeyTmp + continue + } + // If neither a certificate nor a private key could be parsed from the + // Block, ignore it and continue. + if Debug { + log.Println("unable to parse PEM block in PKCS#12 file - skipping") + } + } + + certMap = make(map[string]*x509.Certificate) + for _, cert := range parsedCerts { + // pkix.Name.String() roughly following the RFC 2253 Distinguished Names + // syntax, so we assume that it's canonical. + issuer := cert.Issuer.String() + certMap[issuer] = cert + } + + endEntityFoundIndex = -1 + for i, cert := range parsedCerts { + subject := cert.Subject.String() + if _, ok := certMap[subject]; !ok { + certChain = append(certChain, cert) + endEntityFoundIndex = i + break + } + } + if endEntityFoundIndex == -1 { + return nil, "", errors.New("no end-entity certificate found in PKCS#12 file") + } + + for i, cert := range parsedCerts { + if i != endEntityFoundIndex { + certChain = append(certChain, cert) + } + } + + return certChain, privateKey, nil } // Load the private key referenced by `privateKeyId`. @@ -575,6 +684,21 @@ func ReadPrivateKeyData(privateKeyId string) (crypto.PrivateKey, error) { return nil, errors.New("unable to parse private key") } +// Reads private key data from a *pem.Block. +func ReadPrivateKeyDataFromPEMBlock(block *pem.Block) (key crypto.PrivateKey, err error) { + key, err = x509.ParseECPrivateKey(block.Bytes) + if err == nil { + return key, nil + } + + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err == nil { + return key, nil + } + + return nil, errors.New("unable to parse private key") +} + // Load the certificate referenced by `certificateId` and extract // details required by the SDK to construct the StringToSign. func ReadCertificateData(certificateId string) (CertificateData, error) { diff --git a/aws_signing_helper/signer_test.go b/aws_signing_helper/signer_test.go index 97189ce..fbb3d78 100644 --- a/aws_signing_helper/signer_test.go +++ b/aws_signing_helper/signer_test.go @@ -79,10 +79,17 @@ func TestReadInvalidCertificateData(t *testing.T) { } func TestReadCertificateBundleData(t *testing.T) { - _, err := ReadCertificateBundleData("../tst/certs/cert-bundle.pem") - if err != nil { - t.Log("Failed to read certificate bundle data") - t.Fail() + fixtures := []string{ + "../tst/certs/cert-bundle.pem", + "../tst/certs/cert-bundle-with-comments.pem", + } + + for _, fixture := range fixtures { + _, err := ReadCertificateBundleData(fixture) + if err != nil { + t.Log("Failed to read certificate bundle data") + t.Fail() + } } } @@ -187,6 +194,8 @@ func TestSign(t *testing.T) { msg := "test message" testTable := []CredentialsOpts{} + // TODO: Include tests for PKCS#12 containers, once fixtures are created + // with end-entity certificates. ec_digests := []string{"sha1", "sha256", "sha384", "sha512"} ec_curves := []string{"prime256v1", "secp384r1"} @@ -205,12 +214,6 @@ 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, - }) } } @@ -232,14 +235,50 @@ 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, - }) + pkcs11_objects := []string{"rsa-2048", "ec-prime256v1"} - } + for _, object := range pkcs11_objects { + base_pkcs11_uri := "pkcs11:token=credential-helper-test?pin-value=1234" + basic_pkcs11_uri := fmt.Sprintf("pkcs11:token=credential-helper-test;object=%s?pin-value=1234", object) + always_auth_pkcs11_uri := fmt.Sprintf("pkcs11:token=credential-helper-test;object=%s-always-auth?pin-value=1234", object) + cert_file := fmt.Sprintf("../tst/certs/%s-sha256-cert.pem", object) + + testTable = append(testTable, CredentialsOpts{ + CertificateId: basic_pkcs11_uri, + }) + testTable = append(testTable, CredentialsOpts{ + PrivateKeyId: basic_pkcs11_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: basic_pkcs11_uri, + PrivateKeyId: basic_pkcs11_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert_file, + PrivateKeyId: basic_pkcs11_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: basic_pkcs11_uri, + PrivateKeyId: always_auth_pkcs11_uri, + ReusePin: true, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert_file, + PrivateKeyId: always_auth_pkcs11_uri, + ReusePin: true, + }) + // Note that for the below test case, there are two matching keys. + // Both keys will validate with the certificate, and one will be chosen + // (it doesn't matter which, since both are the exact same key - it's + // just that one has the CKA_ALWAYS_AUTHENTICATE attribute set). + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert_file, + PrivateKeyId: base_pkcs11_uri, + ReusePin: true, + }) } digestList := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} @@ -259,7 +298,6 @@ func TestSign(t *testing.T) { t.Fail() return } - defer signer.Close() pubKey := signer.Public() if credOpts.CertificateId != "" && pubKey == nil { @@ -271,11 +309,21 @@ func TestSign(t *testing.T) { 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) @@ -381,6 +429,103 @@ func TestCertStoreSignerCreationFails(t *testing.T) { } } +func TestSignerCreationFails(t *testing.T) { + var cert string + testTable := []CredentialsOpts{} + + ec_digests := []string{"sha1", "sha256", "sha384", "sha512"} + ec_curves := []string{"prime256v1", "secp384r1"} + + for _, digest := range ec_digests { + for _, curve := range ec_curves { + cert = fmt.Sprintf("../tst/certs/ec-%s-%s.p12", + curve, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) + } + } + + rsa_digests := []string{"md5", "sha1", "sha256", "sha384", "sha512"} + rsa_key_lengths := []string{"1024", "2048", "4096"} + + for _, digest := range rsa_digests { + for _, keylen := range rsa_key_lengths { + cert = fmt.Sprintf("../tst/certs/rsa-%s-%s.p12", + keylen, digest) + testTable = append(testTable, CredentialsOpts{ + CertificateId: cert, + }) + } + } + + for _, credOpts := range testTable { + _, _, err := GetSigner(&credOpts) + // We expect a failure since the certificates in these .p12 files are + // self-signed. When creating a signer, we expect there to be an + // end-entity certificate within the container. + if err == nil { + t.Log("Expected failure when creating PKCS#12 signer, but received none") + t.Fail() + } + } +} + +func TestPKCS11SignerCreationFails(t *testing.T) { + testTable := []CredentialsOpts{} + + template_uri := "pkcs11:token=credential-helper-test;object=%s?pin-value=1234" + rsa_generic_uri := fmt.Sprintf(template_uri, "rsa-2048") + ec_generic_uri := fmt.Sprintf(template_uri, "ec-prime256v1") + always_auth_rsa_uri := fmt.Sprintf(template_uri, "rsa-2048-always-auth") + always_auth_ec_uri := fmt.Sprintf(template_uri, "ec-prime256v1-always-auth") + + testTable = append(testTable, CredentialsOpts{ + CertificateId: rsa_generic_uri, + PrivateKeyId: ec_generic_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: ec_generic_uri, + PrivateKeyId: rsa_generic_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: "../tst/certs/ec-prime256v1-sha256-cert.pem", + PrivateKeyId: rsa_generic_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: "../tst/certs/rsa-2048-sha256-cert.pem", + PrivateKeyId: ec_generic_uri, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: rsa_generic_uri, + PrivateKeyId: always_auth_ec_uri, + ReusePin: true, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: ec_generic_uri, + PrivateKeyId: always_auth_rsa_uri, + ReusePin: true, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: "../tst/certs/ec-prime256v1-sha256-cert.pem", + PrivateKeyId: always_auth_rsa_uri, + ReusePin: true, + }) + testTable = append(testTable, CredentialsOpts{ + CertificateId: "../tst/certs/rsa-2048-sha256-cert.pem", + PrivateKeyId: always_auth_ec_uri, + ReusePin: true, + }) + + for _, credOpts := range testTable { + _, _, err := GetSigner(&credOpts) + if err == nil { + t.Log("Expected failure when creating PKCS#11 signer, but received none") + t.Fail() + } + } +} + func TestUpdate(t *testing.T) { testTable := []struct { name string diff --git a/aws_signing_helper/windows_cert_store_signer.go b/aws_signing_helper/windows_cert_store_signer.go index b0c5850..ddd0e87 100644 --- a/aws_signing_helper/windows_cert_store_signer.go +++ b/aws_signing_helper/windows_cert_store_signer.go @@ -40,7 +40,7 @@ import ( "fmt" "golang.org/x/sys/windows" "io" - "os" + "log" "strconv" "strings" "unsafe" @@ -184,7 +184,7 @@ func GetMatchingCertsAndChain(certIdentifier CertIdentifier) (store windows.Hand } if Debug { - fmt.Fprintf(os.Stderr, "found %d matching identities\n", len(certContainers)) + log.Printf("found %d matching identities\n", len(certContainers)) } return store, certCtx, certChain, certContainers, nil diff --git a/cmd/credentials.go b/cmd/credentials.go index 99749ca..b44b2b0 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -21,16 +21,14 @@ var ( noVerifySSL bool withProxy bool debug bool + reusePin bool certificateId string privateKeyId string certificateBundleId string certSelector string - libPkcs11 string - pinPkcs11 string - slotPkcs11 uint - checkPkcs11 bool + libPkcs11 string credentialsOptions helper.CredentialsOpts @@ -67,8 +65,14 @@ func initCredentialsSubCommand(subCmd *cobra.Command) { subCmd.PersistentFlags().StringVar(&certificateBundleId, "intermediates", "", "Path to intermediate certificate bundle file") subCmd.PersistentFlags().StringVar(&certSelector, "cert-selector", "", "JSON structure to identify a certificate from a certificate store. "+ "Can be passed in either as string or a file name (prefixed by \"file://\")") + subCmd.PersistentFlags().StringVar(&libPkcs11, "pkcs11-lib", "", "Library for smart card / cryptographic device (OpenSC or vendor specific)") + 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.MarkFlagsMutuallyExclusive("private-key", "cert-selector") + subCmd.MarkFlagsMutuallyExclusive("cert-selector", "intermediates") + subCmd.MarkFlagsMutuallyExclusive("cert-selector", "reuse-pin") } // Parses a cert selector string to a map @@ -216,6 +220,7 @@ func PopulateCredentialsOptions() error { Debug: debug, Version: Version, LibPkcs11: libPkcs11, + ReusePin: reusePin, } return nil diff --git a/cmd/read_certificate_data.go b/cmd/read_certificate_data.go index f76e286..b995d02 100644 --- a/cmd/read_certificate_data.go +++ b/cmd/read_certificate_data.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "os" + "strings" helper "github.com/aws/rolesanywhere-credential-helper/aws_signing_helper" "github.com/spf13/cobra" @@ -17,6 +18,7 @@ func init() { readCertificateDataCmd.PersistentFlags().StringVar(&certificateId, "certificate", "", "Path to certificate file") readCertificateDataCmd.PersistentFlags().StringVar(&certSelector, "cert-selector", "", "JSON structure to identify a certificate from a certificate store."+ " Can be passed in either as string or a file name (prefixed by \"file://\")") + readCertificateDataCmd.PersistentFlags().StringVar(&libPkcs11, "pkcs11-lib", "", "Library for smart card / cryptographic device (OpenSC or vendor specific)") readCertificateDataCmd.PersistentFlags().BoolVar(&debug, "debug", false, "To print debug output") } @@ -54,7 +56,13 @@ var readCertificateDataCmd = &cobra.Command{ // PrintCertificate interface can be assigned to this variable. var printFunction PrintCertificate = DefaultPrintCertificate - if certificateId != "" && certIdentifier == (helper.CertIdentifier{}) { + if strings.HasPrefix(certificateId, "pkcs11:") { + certContainers, err = helper.GetMatchingPKCSCerts(certificateId, libPkcs11) + if err != nil { + log.Println(err) + os.Exit(1) + } + } else if certificateId != "" && certIdentifier == (helper.CertIdentifier{}) { data, err := helper.ReadCertificateData(certificateId) if err != nil { os.Exit(1) diff --git a/cmd/sign_string.go b/cmd/sign_string.go index 00401c9..c74a551 100644 --- a/cmd/sign_string.go +++ b/cmd/sign_string.go @@ -73,11 +73,15 @@ func init() { rootCmd.AddCommand(signStringCmd) format = newEnum([]string{"json", "text", "bin"}, "json") digestArg = newEnum([]string{"SHA256", "SHA384", "SHA512"}, "SHA256") - signStringCmd.PersistentFlags().StringVar(&certificateId, "certificate", "", "Path to certificate file") - signStringCmd.PersistentFlags().StringVar(&privateKeyId, "private-key", "", "Path to private key file") + signStringCmd.PersistentFlags().StringVar(&certificateId, "certificate", "", "Path to certificate file or PKCS#11 URI to identify the certificate") + signStringCmd.PersistentFlags().StringVar(&privateKeyId, "private-key", "", "Path to private key file or PKCS#11 URI to identify the private key") signStringCmd.PersistentFlags().BoolVar(&debug, "debug", false, "To print debug output") signStringCmd.PersistentFlags().StringVar(&certSelector, "cert-selector", "", "JSON structure to identify a certificate from a certificate store. "+ "Can be passed in either as string or a file name (prefixed by \"file://\")") + signStringCmd.PersistentFlags().StringVar(&libPkcs11, "pkcs11-lib", "", "Library for smart card / cryptographic device (default: p11-kit-proxy.{so, dll, dylib})") + 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().Var(format, "format", "Output format. One of json, text, and bin") signStringCmd.PersistentFlags().Var(digestArg, "digest", "One of SHA256, SHA384, and SHA512") } @@ -141,7 +145,7 @@ var signStringCmd = &cobra.Command{ stringToSignBytes = []byte(stringToSign) if credentialsOptions.Debug { - fmt.Fprintln(os.Stderr, "Signing fixed string of the form: \"AWS Roles Anywhere "+ + log.Println("Signing fixed string of the form: \"AWS Roles Anywhere " + "Credential Helper Signing Test\" || SIGN_STRING_TEST_VERSION || SHA256(\"IAM RA\" || PUBLIC_KEY_BYTE_ARRAY)\"") } } else { diff --git a/go.mod b/go.mod index f1e738c..10ba30a 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,16 @@ require ( github.com/aws/aws-sdk-go v1.44.57 github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.10.0 - golang.org/x/sys v0.9.0 + golang.org/x/sys v0.10.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stefanberger/go-pkcs11uri v0.0.0-20230614165346-c1cad3d2f68c // indirect + golang.org/x/term v0.10.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 885c299..19c118e 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y 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/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/pkg/errors v0.9.1/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= @@ -18,6 +20,8 @@ github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 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/stefanberger/go-pkcs11uri v0.0.0-20230614165346-c1cad3d2f68c h1:HxmodsFg2lqbspDblBhyR6fXOYwilB6Esnw3PJSaSCA= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230614165346-c1cad3d2f68c/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= @@ -26,7 +30,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=