From 93da1b7b6fe95d85478c4cfea3177b592300f60e Mon Sep 17 00:00:00 2001 From: Chris Newman Date: Sun, 28 Feb 2021 22:52:49 +0000 Subject: [PATCH] github release --- LICENSE | 363 +++++++++++++++++ README.md | 27 ++ SETUP.md | 329 +++++++++++++++ TESTING.md | 43 ++ cmd/casigner11/main.go | 117 ++++++ conf/pkcs11-softhsm.cnf | 8 + go.mod | 9 + go.sum | 26 ++ internal/app/config.go | 33 ++ internal/app/logger.go | 12 + pkg/pkcs11client/hsmconfig.go | 71 ++++ pkg/pkcs11client/hsmconfig_test.go | 11 + pkg/pkcs11client/hsmsigner.go | 111 +++++ pkg/pkcs11client/keyconfig.go | 30 ++ pkg/pkcs11client/keymech.go | 115 ++++++ pkg/pkcs11client/pkcs11client.go | 561 ++++++++++++++++++++++++++ pkg/pkcs11client/pkcs11client_test.go | 178 ++++++++ pkg/pkcs11client/pkcs11const.go | 30 ++ pkg/pkcs11client/utils.go | 138 +++++++ 19 files changed, 2212 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SETUP.md create mode 100644 TESTING.md create mode 100644 cmd/casigner11/main.go create mode 100644 conf/pkcs11-softhsm.cnf create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/config.go create mode 100644 internal/app/logger.go create mode 100644 pkg/pkcs11client/hsmconfig.go create mode 100644 pkg/pkcs11client/hsmconfig_test.go create mode 100644 pkg/pkcs11client/hsmsigner.go create mode 100644 pkg/pkcs11client/keyconfig.go create mode 100644 pkg/pkcs11client/keymech.go create mode 100644 pkg/pkcs11client/pkcs11client.go create mode 100644 pkg/pkcs11client/pkcs11client_test.go create mode 100644 pkg/pkcs11client/pkcs11const.go create mode 100644 pkg/pkcs11client/utils.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e87a115 --- /dev/null +++ b/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4ead7f --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# pkcs11helper + +Go [PKCS#11](http://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/os/pkcs11-base-v2.40-os.html) helper module for certificate signing using HSMs. + + +## Setup + +The [Setup](SETUP.md) instructions help get an HSM up and running with a usable signed Intermediate CA. + +SoftHSM2, Thales SafeNet DPoD and Entrust nShield HSMs are currently documented, though any PKCS#11 compliant HSM should work. + +## Test + +The casigner11 command line client is work in progress, as is this documentation. + +Once the signed Intermediate issuing CA cert has been produced, use [TestCASigner](./pkg/pkcs11client/pkcs11_client_test.go) to try out the HSM signer. + +Check [TESTING](TESTING.md) for more instructions. + +A [Vault plugin](https://github.com/mode51software/vaultplugin-hsmpki) is also available which uses this pkcs11helper +module to add support for HSM backed PKI. + +## License + +HSM PKI for Vault was sponsored by [BT UK](https://www.globalservices.bt.com/en/aboutus/our-services/security) and developed by [mode51 Software](https://mode51.software) under the Mozilla Public License v2. + +By [Chris Newman](https://mode51.software) \ No newline at end of file diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..cf92649 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,329 @@ +# pkcs11helper Setup HSMs + +Go [PKCS#11](http://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/os/pkcs11-base-v2.40-os.html) helper module for certificate signing using HSMs + +## Dependencies + +### OpenSSL + +[OpenSSL](https://www.openssl.org/) includes a conf file commonly found under Linux in /etc/ssl/openssl.cnf that is used to configure the HSM engines + +On Ubuntu install the follwing packages: + +[OpenSSL](https://packages.ubuntu.com/focal/openssl) + +[OpenSSL PKCS#11 engine](https://packages.ubuntu.com/focal/libengine-pkcs11-openssl) + +### pkcs11-tool + +Part of the [OpenSC](https://github.com/OpenSC/OpenSC) project. + +Install on [Ubuntu](http://manpages.ubuntu.com/manpages/focal/man1/pkcs11-tool.1.html). + +## End Entity + +The End Entity may be eg. a web server. + +### Create Private Key for End Entity + +`openssl genrsa -out localhost.key 2048` + + +### Generate CSR for End Entity Certificate + +`openssl req -sha512 -key ./localhost.key -new -out localhost512.csr.pem` + +## HSMs + +### SoftHSM + +[SoftHSM](https://github.com/opendnssec/SoftHSMv2) is a software based HSM developed as part of the [OpenDNSSec](https://www.opendnssec.org/) project. + +#### Dependencies + +On Ubuntu, install the following packages: + +[softhsm2](https://packages.ubuntu.com/focal/softhsm2) + +[softhsm2-common](https://packages.ubuntu.com/focal/softhsm2-common) + +[libsofthsm2](https://packages.ubuntu.com/focal/libsofthsm2) + + +#### Configuration + +##### SoftHSM2 Conf + +On installation the default token can be seen, though may need to be run as root: + +`softhsm2-util --show-slots` + +```Available slots: +Slot 0 +Slot info: +Description: SoftHSM slot ID 0x0 +Manufacturer ID: SoftHSM project +Hardware version: 2.6 +Firmware version: 2.6 +Token present: yes +Token info: +Manufacturer ID: SoftHSM project +Model: SoftHSM v2 +Hardware version: 2.6 +Firmware version: 2.6 +Serial number: +Initialized: no +User PIN init.: no +Label: +``` + +Initialise the token: + +`softhsm2-util --init-token --slot 0 --label "token0" --pin 1234 --so-pin 1234` + +A slot number will be generated: + +`The token has been initialized and is reassigned to slot 1601805484` + +show-slots will now provide the new slot's details: + +`softhsm2-util --show-slots` + +``` +Available slots: +Slot 1601805484 +Slot info: +Description: SoftHSM slot ID 0x5f799cac +Manufacturer ID: SoftHSM project +Hardware version: 2.6 +Firmware version: 2.6 +Token present: yes +Token info: +Manufacturer ID: SoftHSM project +Model: SoftHSM v2 +Hardware version: 2.6 +Firmware version: 2.6 +Serial number: 58bc41dc5f799cac +Initialized: yes +User PIN init.: yes +Label: token0 +``` + + +##### OpenSSL Engine Configuration + +/etc/ssl/openssl.cnf needs this section at the top: + +``` +openssl_conf = openssl_init + +[openssl_init] +engines=engine_section + +[engine_section] +pkcs11 = pkcs11_section + +[pkcs11_section] +engine_id = pkcs11 +dynamic_path = /usr/lib/x86_64-linux-gnu/engines-1.1/libpkcs11.so +MODULE_PATH = /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so +init = 1601805484 # this is the SoftHSM slot ID +``` + + +#### Commands + +##### Show Slots + +`softhsm2-util --show-slots` + +or + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so -L` + +##### List Keys + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot 1601805484 --login -O` + +##### Signing + +###### Gen Root and Intermediate CA RSA Keys + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot 1601805484 --login --keypairgen --key-type rsa:4096 --label "RSATestCARootKey0001" --id "0001"` + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot 1601805484 --login --keypairgen --key-type rsa:2048 --label "RSATestCAInterKey0002" --id "0002"` + +###### Gen Root CA Cert + +`openssl req -new -x509 -days 3560 -sha512 -extensions v3_ca -engine pkcs11 -keyform engine -key 1601805484:0001 -out softhsm-root-0001.ca.cert.pem -set_serial 5000 + +###### Gen Intermediate CA CSR + +`openssl req -new -sha512 -engine pkcs11 -keyform engine -key "1601805484:0002" -out softhsm-inter-0002.ca.csr.pem` + +###### Create OpenSSL demoCA directory + +``` +mkdir -p /etc/ssl/private/demoCA/newcerts +touch demaCA/index.txt +echo "1000" > demoCA/serial +``` + +###### Sign Intermediate CA CSR + +`openssl ca -days 3650 -md sha512 -notext -extensions v3_intermediate_ca -engine pkcs11 -keyform engine -keyfile 1601805484:0001 -in softhsm-inter-0002.ca.csr.pem -out softhsm-inter-0002.ca.cert.pem -cert softhsm-root-0001.ca.cert.pem -noemailDN` + +###### Extract the CA's public key + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot 1601805484 --login -r -y pubkey -a "RSATestCAInterKey0002" -o softhsm-inter-0002.ca.pub.der` + +The signed Intermediate CA is now ready for use with [TESTING](TESTING.md) + + +##### Encryption + +###### Create RSA Key + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot 1601805484 --login --keypairgen --key-type rsa:4096 --label "RSATestKey0020" --id "0020"` + +###### Create EC Key + +`pkcs11-tool --module=/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so --slot 1601805484 --login --keypairgen --key-type EC:prime384v1 --label "ECDSATestKey0021" --id "0021"` + +###### Encryption test + +`openssl pkeyutl -encrypt -engine pkcs11 -keyform engine -inkey 1601805484:0020 -in ./test.txt -out ./test.enc` + +###### Decryption test + +`openssl pkeyutl -decrypt -engine pkcs11 -keyform engine -inkey 1601805484:0020 -in ./test.enc -out ./testdec.txt` + + +### SafeNet DPoD + +#### Configuration + +##### SafeNet Configuration + +Using a SafeNet DPoD account, download the +source yoursafenetdpodpath/setenv needs to be run first. +If you are using an IDE then source this script in a terminal and then start the IDE from the terminal. +The unit tests should then work with debug enabled within the IDE. + +##### OpenSSL Engine Configuration + +``` +openssl_conf = openssl_init + +[openssl_init] +engines=engine_section + +[engine_section] +pkcs11 = pkcs11_section + +[pkcs11_section] +engine_id = pkcs11 +dynamic_path = /usr/lib/x86_64-linux-gnu/engines-1.1/libpkcs11.so +MODULE_PATH = /yoursafenetpath/libs/64/libCryptoki2.so +``` + +#### Commands + +##### List Keys +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 -O` + +##### Signing + +###### Gen Root and Intermediate CA RSA Keys + +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --keypairgen --key-type rsa:4096 --label RSATestCARootKey0001 --id "0001"` + +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --keypairgen --key-type rsa:2048 --label RSATestCAInterKey0002 --id "0002"` + +###### Gen Root CA Cert + +`openssl req -new -x509 -days 7300 -sha512 -extensions v3_ca -engine pkcs11 -keyform engine -key "pkcs11:id=%00%01" -out safenet-root-0001.ca.cert.pem -set_serial 5000` + +###### Gen Intermediate CA CSR + +`openssl req -new -sha512 -engine pkcs11 -keyform engine -key "pkcs11:id=%00%02" -out safenet-inter-0002.ca.csr.pem` + +###### Sign Intermediate CA CSR + +`openssl ca -days 3650 -md sha512 -notext -extensions v3_intermediate_ca -engine pkcs11 -keyform engine -keyfile "pkcs11:id=%00%09" -in safenet-inter-0009.ca.csr.pem -out safenet-inter-0009.ca.cert.pem -cert safenet-root-0009.ca.cert.pem -noemailDN` + +###### Extract the Intermediate CA's public key + +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --id "0010" --type pubkey -r -o /tmp/safenet-inter.ca.pub.der` + + +###### Gen Root and Intermediate CA ECDSA Keys + +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --keypairgen --key-type EC:secp521r1 --label ECTestCARootKey0015 --id "0015"` + +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --keypairgen --key-type EC:secp384r1 --label ECTestCAInterKey0016 --id "0016"` + +###### Gen Root CA Cert +`openssl req -new -x509 -days 7300 -sha512 -extensions v3_ca -engine pkcs11 -keyform engine -key "pkcs11:id=%00%15" -out safenet-root-0015.ca.cert.pem -set_serial 5010` + +###### Gen Intermediate CA CSR +`openssl req -new -sha512 -engine pkcs11 -keyform engine -key "pkcs11:id=%00%16" -out safenet-inter-0016.ca.csr.pem` + +###### Sign Intermediate CA CSR +`openssl ca -days 3650 -md sha512 -notext -extensions v3_intermediate_ca -engine pkcs11 -keyform engine -keyfile "pkcs11:id=%00%15" -in safenet-inter-0016.ca.csr.pem -out safenet-inter-0016.ca.cert.pem -cert safenet-root-0015.ca.cert.pem -noemailDN` + +###### Extract the Intermediate CA's public key +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --id "0016" --type pubkey -r -o safenet-inter-0016.ca.pub.der` + + +##### Encryption + +###### Create RSA key +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --keypairgen --key-type rsa:2048 --label RSATestKey0020 --id "0020"` + +###### Create EC key +`pkcs11-tool --module=/opt/apps/safenet/dpod/current/libs/64/libCryptoki2.so --login --login-type user --slot 3 --keypairgen --key-type EC:secp384r1 --label ECTestKey0014 --id 30303134` + +###### Encryption test +`openssl pkeyutl -encrypt -engine pkcs11 -keyform engine -inkey "pkcs11:id=0007;type=public;" -in ./test.txt -out ./testsafe.enc` + +###### Decryption test +`openssl pkeyutl -decrypt -engine pkcs11 -keyform engine -inkey "pkcs11:id=0007;type=private;" -in ./testsafe.enc -out ./testsafe.dec` + + +## Entrust nShield + +``` +openssl_conf = openssl_init + +[openssl_init] +engines=engine_section + +[engine_section] +pkcs11 = pkcs11_section + +[pkcs11_section] +engine_id = pkcs11 +dynamic_path = /usr/lib/x86_64-linux-gnu/engines-1.1/libpkcs11.so +MODULE_PATH = /opt/apps/nfast/20201219/bin/libcknfast.so +``` + +### Commands + +#### nCipher Encryption Test +`openssl pkeyutl -encrypt -engine pkcs11 -keyform engine -inkey "pkcs11:id=%61%02%1f%1f%ed%1e%fc%39%f9%d6%0f%28%9b%d5%5f%e9%78%91%6c%e9;type=public;" -in ./test.txt -out ./testncipher.enc` + +#### nCipher Decryption Test +`openssl pkeyutl -decrypt -engine pkcs11 -keyform engine -inkey "pkcs11:id=%61%02%1f%1f%ed%1e%fc%39%f9%d6%0f%28%9b%d5%5f%e9%78%91%6c%e9;type=public;" -in ./testncipher.enc -out ./testncipher.dec` + +#### OpenSSL Gen Root CA Cert +`openssl req -new -x509 -days 7300 -sha512 -extensions v3_ca -engine pkcs11 -keyform engine -key "pkcs11:id=%61%02%1f%1f%ed%1e%fc%39%f9%d6%0f%28%9b%d5%5f%e9%78%91%6c%e9;type=public;" -out ncipher-root-0005.ca.cert.pem -set_serial 5001` + +#### OpenSSL Gen Intermediate CA CSR +`openssl req -new -sha512 -engine pkcs11 -keyform engine -key "pkcs11:id=%88%d8%42%c8%6f%7a%49%ae%92%be%d6%0f%3b%e7%41%51%94%27%69%86" -out ncipher-inter-0006.ca.csr.pem` + +#### OpenSSL Sign Intermediate CA CSR +`openssl ca -days 3650 -md sha512 -notext -extensions v3_intermediate_ca -engine pkcs11 -keyform engine -keyfile "pkcs11:id=%61%02%1f%1f%ed%1e%fc%39%f9%d6%0f%28%9b%d5%5f%e9%78%91%6c%e9" -in ncipher-inter-0006.ca.csr.pem -out ncipher-inter-0006.ca.cert.pem -cert ncipher-root-0005.ca.cert.pem -noemailDN` + +#### Extract the Intermediate CA's public key +`pkcs11-tool --module=/opt/apps/nfast/20201219/bin/libcknfast.so --id "61021f1fed1efc39f9d60f289bd55fe978916ce9" --type pubkey -r -o /tmp/ncipher-inter.ca.pub.der` diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..2279941 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,43 @@ +# Tests + +## Signing + +### TestCASigner + +TODO: conf file + +Configure the HSM in pkg/pkcs11client/pkcs11client_test.go + +``` + pkcs11Client.HsmConfig = &HsmConfig{ + Lib: "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2", + SlotId: 1, + Pin: "1234", + ConnectTimeoutS: 10, + ReadTimeoutS: 30, + } +``` + +Convert the signed Intermediate CA to DER format and place in data/ + +`openssl x509 -in softhsm-inter-0002.ca.cert.pem -outform DER -out softhsm-inter-0002.ca.cert.der` + +Also convert the CSR to DER format and place in data/ + +`openssl req -in localhost512.csr.pem` -outform DER -out localhost512.csr.der` + +Copy the signed Intermediate CA's DER public key into data/ and convert to PEM: + +`openssl rsa -pubin -inform DER -in softhsm-inter-0002.ca.pub.der -out softhsm-inter-0002.ca.pub.pem` + + +``` +var caFiles = CASigningRequest { + csrFile: "../../data/localhost512.csr.der", + caPubkeyFile: "../../data/softhsm-inter-0002.ca.pub.der", + caCertFile: "../../data/softhsm-inter-0002.ca.cert.der", +} +``` + + +go test -v -run TestCASigner ./pkg/pkcs11client diff --git a/cmd/casigner11/main.go b/cmd/casigner11/main.go new file mode 100644 index 0000000..455f77b --- /dev/null +++ b/cmd/casigner11/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "flag" + "fmt" + "github.com/mode51software/pkcs11helper/internal/app" + "github.com/mode51software/pkcs11helper/pkg/pkcs11client" + "github.com/rs/zerolog/log" + "math/rand" + "os" +) + +func init() { + app.InitLogger() +} + +const ( + // the JSON config file + FLAG_HSMCONFIG = "hsmconfig" + + // the Certificate Signing Request + FLAG_CSRFILE = "csr" + + // the validity period for the signed certificate + FLAG_DAYS = "days" + + // the CA cert + FLAG_CAFILE = "ca" + + // help + FLAG_HELP = "help" + + USAGE string = "casigner\n" + + "\t-" + FLAG_CAFILE + "=\n" + + "\t-" + FLAG_CSRFILE + "=\n" + + "\t-" + FLAG_DAYS + "=\n" + + "\t-" + FLAG_HELP + "=help\n" + + "\t-" + FLAG_HSMCONFIG + "=\n" +) + +// Supports either a conf file or args +func main() { + + // TODO: this is incomplete, use the test cases for now + caSignerConfig, err := parseFlags() + if err != nil { + log.Error().Err(err).Msg("Error parsing command line args") + exitUsage() + } else { + // csr, err := pkcs11client.LoadCertRequestFromFile("./data/localhost.csr.der") + csr, err := pkcs11client.LoadCertRequestFromFile(caSignerConfig.CSRFile) + if err != nil { + log.Error().Err(err) + exitUsage() + } else { + log.Info().Msg("Loaded CSR with CN=" + csr.Subject.CommonName) + + // "./data/cacert.der" + if caCert, err := pkcs11client.LoadCertFromFile(caSignerConfig.CAFile); err != nil { + log.Error().Msg(err.Error()) + } else { + log.Info().Msg("Loaded CA cert with CN=" + caCert.Subject.CommonName) + + if caPubKey, err := pkcs11client.LoadPubkeyFromFile("./data/capubkey.pem"); err != nil { + log.Error().Msg(err.Error() + " check RSA in -----BEGIN RSA PUBLIC KEY-----") + } else { + log.Info().Msgf("Loaded CA pubkey with E=%d", caPubKey.E) + + _, err := pkcs11client.ParseHSMConfig(caSignerConfig.HSMConfigFile) + if err != nil { + panic(err) + } + + hsmSigner := pkcs11client.HsmSigner{ + Pkcs11Client: pkcs11client.Pkcs11Client{}, + PublicKey: caPubKey, + KeyConfig: pkcs11client.KeyConfig{ + Label: "SSL Root CA 02", + }, + Serial: rand.Int(), + } + + if signedCsr, err := pkcs11client.GenSignedCert(csr, caCert, &hsmSigner); err != nil { + log.Error().Msg(err.Error()) + } else { + log.Info().Msg("Signed CSR with CN=" + signedCsr.Subject.CommonName) + if err = pkcs11client.SaveCertToFile("./data/signedcert.der", signedCsr); err != nil { + log.Error().Msg(err.Error()) + } else { + log.Info().Msg("Saved signed cert") + + } + } + } + } + } + } +} + +func parseFlags() (caSignerConfig app.CASignerConfig, err error) { + + flag.StringVar(&(caSignerConfig.HSMConfigFile), FLAG_HSMCONFIG, "", "JSON HSM config file") + flag.StringVar(&(caSignerConfig.CAFile), FLAG_CAFILE, "", "CA file") + flag.StringVar(&(caSignerConfig.CSRFile), FLAG_CSRFILE, "", "Certificate Signing Request file DER encoded") + flag.UintVar(&(caSignerConfig.Days), FLAG_DAYS, 365, "Validity period for signed certificate, defaults to 365 days") + + flag.Parse() + + // validate flags + err = caSignerConfig.ValidateCASignerConfig() + return +} + +func exitUsage() { + fmt.Println(USAGE) + os.Exit(1) +} diff --git a/conf/pkcs11-softhsm.cnf b/conf/pkcs11-softhsm.cnf new file mode 100644 index 0000000..a5736d6 --- /dev/null +++ b/conf/pkcs11-softhsm.cnf @@ -0,0 +1,8 @@ +{ + "lib" : "/opt/server/softhsm/current/lib/softhsm/libsofthsm2.so", + "slot_id" : 288648064, + "pin" : "1234", + "key_label" : "SSL Root CA 02", + "connect_timeout_s" : 30, + "read_timeout_s" : 30 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7bd6ad --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/mode51software/pkcs11helper + +go 1.15 + +require ( + github.com/rs/zerolog v1.20.0 + github.com/stretchr/testify v1.7.0 + github.com/miekg/pkcs11 v1.0.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..84e0459 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.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= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/config.go b/internal/app/config.go new file mode 100644 index 0000000..19f5ff4 --- /dev/null +++ b/internal/app/config.go @@ -0,0 +1,33 @@ +package app + +import "errors" + +type CASignerConfig struct { + + // JSON config file containing HSM access details + HSMConfigFile string + + // Certificate Signing Request (CSR) file DER encoded + CSRFile string + + // Signed Certificate validity period + Days uint + + // CA cert + CAFile string +} + +func (c *CASignerConfig) ValidateCASignerConfig() error { + if len(c.HSMConfigFile) > 0 && + len(c.CSRFile) > 0 && + len(c.CAFile) > 0 { + return nil + } + return errors.New("Please set HSM Config file, CSR file and CA Cert file") +} + +// confFile, err := os.Open(filename) +// if err != nil { return err } +// bufConfFile := bufio.NewReader(confFile) +// hsmPkiConfig := HsmPkiConfig{} +// if err = jsonutil.DecodeJSONFromReader(bufConfFile, hsmPkiConfig); err != nil { return err } diff --git a/internal/app/logger.go b/internal/app/logger.go new file mode 100644 index 0000000..00df161 --- /dev/null +++ b/internal/app/logger.go @@ -0,0 +1,12 @@ +package app + +import ( + "github.com/rs/zerolog" +) + +func InitLogger() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + zerolog.TimestampFieldName = "t" + zerolog.LevelFieldName = "l" + zerolog.MessageFieldName = "m" +} diff --git a/pkg/pkcs11client/hsmconfig.go b/pkg/pkcs11client/hsmconfig.go new file mode 100644 index 0000000..c90e21e --- /dev/null +++ b/pkg/pkcs11client/hsmconfig.go @@ -0,0 +1,71 @@ +package pkcs11client + +import ( + "encoding/json" + "errors" + "os" +) + +type HsmConfig struct { + // the HSM's client PKCS#11 library + Lib string + + // the HSM slot ID + SlotId uint `json:"slot_id"` + + // the slot pin + Pin string + + // a key label + KeyLabel string `json:"key_label"` + + // connection timeout seconds + ConnectTimeoutS uint `json:"connect_timeout_s"` + + // function timeout seconds + ReadTimeoutS uint `json:"read_timeout_s"` +} + +const ( + DEFAULT_CONNECTTIMEOUTS = 30 + DEFAULT_READTIMEOUTS = 30 +) + +func ParseHsmConfig(filename string) (*HsmConfig, error) { + file, err := os.Open(filename) + if err != nil { + return nil, errors.New("Unable to open conf file ") + } + defer file.Close() + decoder := json.NewDecoder(file) + config := HsmConfig{} + err = decoder.Decode(&config) + if err != nil { + return nil, errors.New("Unable to decode conf file: " + err.Error()) + } + err = config.ValidateConfig() + if err != nil { + return nil, err + } + return &config, nil +} + +// Only check the presence of the client lib +// the slot could be 0, the pin could be blank and the key label could be set dynamically +func (h *HsmConfig) ValidateConfig() error { + if len((*h).Lib) == 0 { + return errors.New("Please specify the path of the PKCS#11 client library") + } else { + return nil + } +} + +func (h *HsmConfig) CheckSetDefaultTimeouts() { + if h.ConnectTimeoutS == 0 { + h.ConnectTimeoutS = DEFAULT_CONNECTTIMEOUTS + } + if h.ReadTimeoutS == 0 { + h.ReadTimeoutS = DEFAULT_READTIMEOUTS + } + return +} diff --git a/pkg/pkcs11client/hsmconfig_test.go b/pkg/pkcs11client/hsmconfig_test.go new file mode 100644 index 0000000..f87a55b --- /dev/null +++ b/pkg/pkcs11client/hsmconfig_test.go @@ -0,0 +1,11 @@ +package pkcs11client + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParseConfig(t *testing.T) { + _, err := ParseHsmConfig("../../conf/pkcs11-softhsm.cnf") + assert.NoError(t, err) +} diff --git a/pkg/pkcs11client/hsmsigner.go b/pkg/pkcs11client/hsmsigner.go new file mode 100644 index 0000000..deee7c3 --- /dev/null +++ b/pkg/pkcs11client/hsmsigner.go @@ -0,0 +1,111 @@ +package pkcs11client + +import ( + "crypto" + "crypto/dsa" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "errors" + "github.com/rs/zerolog/log" + "io" + "reflect" + "sync" + "time" +) + +type HsmSigner struct { + CryptoSigner crypto.Signer + refreshMutex sync.Mutex + Pkcs11Client *Pkcs11Client + PublicKey crypto.PublicKey + KeyConfig KeyConfig + SignerOpts crypto.SignerOpts + Serial int64 + SignatureAlgo x509.SignatureAlgorithm +} + +type CASigningRequest struct { + csrFile string + caPubkeyFile string + caCertFile string +} + +// In PKCS#1 padding if the message digest is not set then the supplied data is signed or verified directly +// instead of using a DigestInfo structure. If a digest is set then the a +// DigestInfo structure is used and its the length must correspond to the digest type. +// https://www.openssl.org/docs/man1.1.1/man1/openssl-pkeyutl.html +// RFC 8017 Specifies the DigestInfo structures +// https://github.com/letsencrypt/pkcs11key/blob/master/key.go#L85 +var digestInfos = map[crypto.Hash][]byte{ + crypto.MD5: {0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, 0x00, 0x04, 0x10}, + crypto.SHA1: {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14}, + crypto.SHA224: {0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c}, + crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}, + crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}, + crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}, + crypto.MD5SHA1: {}, // A special TLS case which doesn't use an ASN1 prefix. + crypto.RIPEMD160: {0x30, 0x20, 0x30, 0x08, 0x06, 0x06, 0x28, 0xcf, 0x06, 0x03, 0x00, 0x31, 0x04, 0x14}, +} + +func (t HsmSigner) Public() crypto.PublicKey { + return t.PublicKey +} + +func (t HsmSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signedCsr []byte, err error) { + + t.refreshMutex.Lock() + + defer t.refreshMutex.Unlock() + + log.Debug().Msgf("hashiFunc=%s id=%d len=%d type=%s", + opts.HashFunc().String(), opts.HashFunc(), len(digest), reflect.TypeOf(opts)) + + t.Pkcs11Client.LastErrCode = 0 + + chan1 := make(chan error, 1) + go func() { + + switch t.PublicKey.(type) { + case *rsa.PublicKey: + if pssOpts, ok := opts.(*rsa.PSSOptions); ok { + t.SignerOpts = pssOpts + signedCsr, err = t.Pkcs11Client.SignCertRSAPSS(digest, &t) + } else { + digestWithHeader := append(digestInfos[opts.HashFunc()], digest...) + signedCsr, err = t.Pkcs11Client.SignCertRSA(digestWithHeader, &t) + } + case *dsa.PublicKey: + signedCsr, err = t.Pkcs11Client.SignCertDSA(digest, &t) + case *ecdsa.PublicKey: + signedCsr, err = t.Pkcs11Client.SignCertECDSA(digest, &t) + case *ed25519.PublicKey: + signedCsr, err = t.Pkcs11Client.SignCertEDDSA(digest, &t) + default: + err = errors.New("Unsupported PublicKey type") + } + chan1 <- err + + }() + + select { + case res := <-chan1: + if res != nil { + log.Debug().Msg("generic error here") + t.Pkcs11Client.LastErrCode = PKCS11ERR_GENERICERROR + return nil, res + } + case <-time.After(time.Duration(t.Pkcs11Client.HsmConfig.ReadTimeoutS) * time.Second): + log.Debug().Msg("read timeout error here") + t.Pkcs11Client.LastErrCode = PKCS11ERR_READTIMEOUT + return nil, errors.New("PKCS#11 connection timeout") + } + + if err != nil { + t.Pkcs11Client.LastErrCode = PKCS11ERR_GENERICERROR + return nil, err + } else { + return signedCsr, nil + } +} diff --git a/pkg/pkcs11client/keyconfig.go b/pkg/pkcs11client/keyconfig.go new file mode 100644 index 0000000..1f57139 --- /dev/null +++ b/pkg/pkcs11client/keyconfig.go @@ -0,0 +1,30 @@ +package pkcs11client + +import ( + "errors" + "github.com/miekg/pkcs11" +) + +type KeyConfig struct { + Label string + + // CKA_ID doesn't appear to work with SoftHSM + Id string + + // for CKA_KEY_TYPE + Type uint + + // The mechanism will be auto populated but it can be manually set + Mechanism []*pkcs11.Mechanism +} + +func (k *KeyConfig) appendKeyIdentity(attribs []*pkcs11.Attribute) (fullAttribs []*pkcs11.Attribute, err error) { + if len(k.Id) > 0 { + fullAttribs = append(attribs, pkcs11.NewAttribute(pkcs11.CKA_ID, k.Id)) + } else if len(k.Label) > 0 { + fullAttribs = append(attribs, pkcs11.NewAttribute(pkcs11.CKA_LABEL, k.Label)) + } else { + return nil, errors.New("Provide a key id or label") + } + return +} diff --git a/pkg/pkcs11client/keymech.go b/pkg/pkcs11client/keymech.go new file mode 100644 index 0000000..2699d46 --- /dev/null +++ b/pkg/pkcs11client/keymech.go @@ -0,0 +1,115 @@ +package pkcs11client + +import ( + "crypto" + "crypto/rsa" + "errors" + "github.com/miekg/pkcs11" + "github.com/rs/zerolog/log" +) + +// https://github.com/letsencrypt/pkcs11key/blob/v4.0.0/v4/key.go#L35 +// https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/csprd01/pkcs11-curr-v3.0-csprd01.html#_Toc10560827 +type pssParams struct { + ckmHash uint // CKM constant for hash function + ckgMGF uint // CKG constant for mask generation function +} + +var hashPSSParams = map[crypto.Hash]pssParams{ + crypto.SHA1: {pkcs11.CKM_SHA_1, pkcs11.CKG_MGF1_SHA1}, + crypto.SHA224: {pkcs11.CKM_SHA224, pkcs11.CKG_MGF1_SHA224}, + crypto.SHA256: {pkcs11.CKM_SHA256, pkcs11.CKG_MGF1_SHA256}, + crypto.SHA384: {pkcs11.CKM_SHA384, pkcs11.CKG_MGF1_SHA384}, + crypto.SHA512: {pkcs11.CKM_SHA512, pkcs11.CKG_MGF1_SHA512}, +} + +// For mechanisms that don't need additional params +//case pkcs11.CKM_RSA_PKCS: // PKCS#1 RSASSA v1.5 sign +//case pkcs11.CKM_RSA_X_509: // not in FIPS mode +func GenMechanismById(mechanismId uint) (mechanism []*pkcs11.Mechanism, err error) { + mechanism = []*pkcs11.Mechanism{pkcs11.NewMechanism(mechanismId, nil)} + return +} + +func GenSignerMechanismById(mechanismId uint, opts crypto.SignerOpts) ([]*pkcs11.Mechanism, error) { + switch mechanismId { + case pkcs11.CKM_RSA_PKCS_PSS: + pssOpts := opts.(*rsa.PSSOptions) + pssParams, err := genPSSParamsForMechanism(pssOpts) + if err != nil { + return nil, err + } + return []*pkcs11.Mechanism{pkcs11.NewMechanism(mechanismId, pssParams)}, nil + case pkcs11.CKM_RSA_PKCS: // PKCS#1 RSASSA v1.5 sign + fallthrough + case pkcs11.CKM_RSA_X_509: // not in FIPS mode + fallthrough + case pkcs11.CKM_DSA: + fallthrough + case pkcs11.CKM_ECDSA: + fallthrough + case CKM_EDDSA: + return []*pkcs11.Mechanism{pkcs11.NewMechanism(mechanismId, nil)}, nil + } + + return nil, nil +} + +// mechanisms vs functions: http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/os/pkcs11-curr-v2.40-os.html#_Toc416959967 +func genMechanismByIdWithOaepParams(mechanismId uint, hashAlg crypto.Hash) (mechanism []*pkcs11.Mechanism, err error) { + switch mechanismId { + case pkcs11.CKM_RSA_PKCS_OAEP: // PKCS OAEP enc/dec + if hashAlg == 0 { + return nil, errors.New("OAEP requires a SHA algo") + } + oaepParams, err := genOaepParamsForMechanism(hashAlg, []byte{}) + if err != nil { + return nil, err + } + mechanism = []*pkcs11.Mechanism{pkcs11.NewMechanism(mechanismId, oaepParams)} + default: + mechanism = nil + } + return +} + +// https://go.googlesource.com/go/+/refs/tags/go1.16beta1/src/crypto/rsa/rsa.go#393 +func genOaepParamsForMechanism(hashAlg crypto.Hash, label []byte) (*pkcs11.OAEPParams, error) { + pkcsHashAlg, pkcsMgfAlg, err := genHashParamsForMechanism(hashAlg) + if err != nil { + return nil, err + } + return pkcs11.NewOAEPParams(pkcsHashAlg, pkcsMgfAlg, pkcs11.CKZ_DATA_SPECIFIED, label), nil +} + +func genHashParamsForMechanism(hashAlg crypto.Hash) (pkcs11HashAlg uint, pkcs11MgfAlg uint, err error) { + switch hashAlg { + case crypto.SHA1: + return pkcs11.CKM_SHA_1, pkcs11.CKG_MGF1_SHA1, nil // 20 + case crypto.SHA224: + return pkcs11.CKM_SHA224, pkcs11.CKG_MGF1_SHA224, nil // 28 + case crypto.SHA256: + return pkcs11.CKM_SHA256, pkcs11.CKG_MGF1_SHA256, nil // 32 + case crypto.SHA384: + return pkcs11.CKM_SHA384, pkcs11.CKG_MGF1_SHA384, nil // 48 + case crypto.SHA512: + return pkcs11.CKM_SHA512, pkcs11.CKG_MGF1_SHA512, nil // 64 + default: + return 0, 0, errors.New("Unknown hash algo") + } +} + +func genPSSParamsForMechanism(opts *rsa.PSSOptions) (pssParams []byte, err error) { + params, ok := hashPSSParams[opts.Hash] + if !ok { + err = errors.New("pkcs11key: unknown hash function") + return + } + + if opts.SaltLength == rsa.PSSSaltLengthAuto || opts.SaltLength == rsa.PSSSaltLengthEqualsHash { + opts.SaltLength = opts.Hash.Size() + } + pssParams = pkcs11.NewPSSParams(params.ckmHash, params.ckgMGF, uint(opts.SaltLength)) + log.Info().Msgf("pssParams hash=%d mgf=%d saltlen=%d", params.ckmHash, params.ckgMGF, opts.SaltLength) + return +} diff --git a/pkg/pkcs11client/pkcs11client.go b/pkg/pkcs11client/pkcs11client.go new file mode 100644 index 0000000..31b8eff --- /dev/null +++ b/pkg/pkcs11client/pkcs11client.go @@ -0,0 +1,561 @@ +// Helpers for PKCS#11 including instructions for configuring: +// - SoftHSM +// - Thales SafeNet DPoD +// - Entrust nShield +package pkcs11client + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/asn1" + "errors" + "fmt" + "github.com/miekg/pkcs11" + "github.com/rs/zerolog/log" + "sync" + "time" +) + +type Pkcs11ErrorCode int + +type Pkcs11ConnectionState int + +const ( + PKCS11ERR_NONE Pkcs11ErrorCode = iota + PKCS11ERR_GENERICERROR + PKCS11ERR_CONNECTIONTIMEOUT + PKCS11ERR_READTIMEOUT + + PKCS11CONNECTION_NONE = iota + PKCS11CONNECTION_INPROGRESS + PKCS11CONNECTION_FAILED + PKCS11CONNECTION_SUCCEEDED +) + +type Pkcs11Client struct { + context *pkcs11.Ctx + session pkcs11.SessionHandle + HsmConfig *HsmConfig + Pkcs11Mutex sync.Mutex + // the most recent error and code should only be used whilst holding the mutex lock + ConnectionState Pkcs11ConnectionState + LastErrCode Pkcs11ErrorCode + LastErr error +} + +func (p *Pkcs11Client) Init() (err error) { + p.context = pkcs11.New(p.HsmConfig.Lib) + err = p.context.Initialize() + return +} + +// this includes the PKCS#11 Initialize as part of the overall timeout +func (p *Pkcs11Client) InitAndLoginWithTimeout() (err error) { + + p.Pkcs11Mutex.Lock() + defer p.Pkcs11Mutex.Unlock() + + p.ConnectionState = PKCS11CONNECTION_INPROGRESS + + chan1 := make(chan error, 1) + go func() { + + if err = p.Init(); err != nil { + chan1 <- err + } + if err = p.Login(); err != nil { + chan1 <- err + } + chan1 <- nil + }() + + select { + case res := <-chan1: + if res != nil { + p.ConnectionState = PKCS11CONNECTION_FAILED + return res + } + case <-time.After(time.Duration(p.HsmConfig.ConnectTimeoutS) * time.Second): + p.ConnectionState = PKCS11CONNECTION_FAILED + return errors.New("PKCS#11 connection timeout") + } + p.ConnectionState = PKCS11CONNECTION_SUCCEEDED + return +} + +func (p *Pkcs11Client) FlushSession() { + if p.context != nil && p.session > 0 { + p.context.CloseSession(p.session) + p.Cleanup() + } +} + +// for module handling of connection timeout without the PKCS#11 Initialize as part of the timeout +// alternatively the Login function can be called directly so that timeouts can be handled externally +func (p *Pkcs11Client) LoginWithTimeout() error { + + chan1 := make(chan error, 1) + go func() { + + if err := p.Login(); err != nil { + chan1 <- err + } + chan1 <- nil + }() + + select { + case res := <-chan1: + if res != nil { + return errors.Unwrap(res) + } + case <-time.After(time.Duration(p.HsmConfig.ConnectTimeoutS) * time.Second): + return errors.New("PKCS#11 connection timeout") + } + + return nil +} + +func (p *Pkcs11Client) Login() (err error) { + p.session, err = p.context.OpenSession(p.HsmConfig.SlotId, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) + if err != nil { + return + } + if len(p.HsmConfig.Pin) > 0 { + err = p.context.Login(p.session, pkcs11.CKU_USER, p.HsmConfig.Pin) + } else { + err = errors.New("Please set the HSM slot PIN") + } + return +} + +func (p *Pkcs11Client) Logout() (err error) { + err = p.context.Logout(p.session) + if err != nil { + err = p.context.CloseSession(p.session) + } + return +} + +func (p *Pkcs11Client) Cleanup() { + p.context.Destroy() + p.context.Finalize() +} + +func (p *Pkcs11Client) SignCertRSA(csrData []byte, signer *HsmSigner) (cert []byte, err error) { + cert, err = p.signCert(csrData, signer, pkcs11.CKK_RSA, pkcs11.CKM_RSA_PKCS) + return +} + +func (p *Pkcs11Client) SignCertRSAPSS(csrData []byte, signer *HsmSigner) (cert []byte, err error) { + cert, err = p.signCert(csrData, signer, pkcs11.CKK_RSA, pkcs11.CKM_RSA_PKCS_PSS) + return +} + +func (p *Pkcs11Client) SignCertDSA(csrData []byte, signer *HsmSigner) (cert []byte, err error) { + cert, err = p.signCert(csrData, signer, pkcs11.CKK_DSA, pkcs11.CKM_DSA) + return +} + +func (p *Pkcs11Client) SignCertECDSA(csrData []byte, signer *HsmSigner) (cert []byte, err error) { + // CKK_ECDSA is deprecated in v2.11, use CKK_EC + cert, err = p.signCert(csrData, signer, pkcs11.CKK_EC, pkcs11.CKM_ECDSA) + if err == nil { + // match RFC 5480 output + cert, err = ecdsaPKCS11ToRFC5480(cert) + } + return +} + +// EDDSA uses the Edwards Ed25519 elliptic curve in FIPS 186-5 +// https://csrc.nist.gov/publications/detail/fips/186/5/draft +func (p *Pkcs11Client) SignCertEDDSA(csrData []byte, signer *HsmSigner) (cert []byte, err error) { + // CKK_ECDSA is deprecated in v2.11, use CKK_EC + cert, err = p.signCert(csrData, signer, CKK_EC_EDWARDS, CKM_EDDSA) + if err == nil { + // match RFC 5480 output + cert, err = ecdsaPKCS11ToRFC5480(cert) + } + return +} + +func (p *Pkcs11Client) signCert(csrData []byte, signer *HsmSigner, privKeyType int, mechanismId uint) (signedCsr []byte, err error) { + + //err = SaveDataToFile("./data/outdigest.der", &csrData) + + attribs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, privKeyType), + pkcs11.NewAttribute(pkcs11.CKA_SIGN, true), + } + var fullAttribs []*pkcs11.Attribute + if fullAttribs, err = (*signer).KeyConfig.appendKeyIdentity(attribs); err != nil { + return nil, err + } + + err = p.context.FindObjectsInit(p.session, fullAttribs) + if err != nil { + return nil, err + } + + objHandles, _, err := p.context.FindObjects(p.session, 1) + if err != nil { + return nil, err + } + if objHandles == nil || len(objHandles) == 0 { + return nil, errors.New("PKCS11 FindObjects empty") + } else { + if err = p.context.FindObjectsFinal(p.session); err != nil { + return nil, err + } + var mechanism []*pkcs11.Mechanism + if mechanism, err = GenSignerMechanismById(mechanismId, signer.SignerOpts); err != nil { + return nil, err + } + + if err = p.context.SignInit(p.session, mechanism, objHandles[0]); err != nil { + return nil, err + } + signedCsr, err = p.context.Sign(p.session, csrData) + if err != nil { + return nil, err + } + + /* err = SaveDataToFile("./data/out.der", &signedCsr) + if err != nil { + return nil, err + }*/ + return signedCsr, nil + + } + return nil, nil +} + +// PKCS v1_15 supports Encrypt/Decrypt, Sign/Verify, SR/VR, Wrap/Unwrap only +// insecure PKCSv1_15 not supported by FIPS enabled SafeNet HSM but works with SoftHSM +func (p *Pkcs11Client) EncryptRsaPkcs1v15(plainData *[]byte, encryptedData *[]byte, keyConfig KeyConfig) (err error) { + keyConfig.Mechanism, err = GenMechanismById(pkcs11.CKM_RSA_PKCS) + return p.encrypt(plainData, encryptedData, keyConfig) +} + +func (p *Pkcs11Client) EncryptRsaPkcsX509(plainData *[]byte, encryptedData *[]byte, keyConfig KeyConfig) (err error) { + keyConfig.Mechanism, err = GenMechanismById(pkcs11.CKM_RSA_X_509) + return p.encrypt(plainData, encryptedData, keyConfig) +} + +// RSA OAEP supports Encrypt/Decrypt and Wrap/Unwrap only +// requires additional params +// keyConfig.Mechanism will be auto populated based on the hashAlg unless already set, ie. it can be overridden +// hashAlg is eg. crypto.SHA256 +// check RSA mechanisms vs functions: http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/os/pkcs11-curr-v2.40-os.html#_Toc416959967 +func (p *Pkcs11Client) EncryptRsaPkcsOaep(plainData *[]byte, encryptedData *[]byte, keyConfig KeyConfig, hashAlg crypto.Hash) (err error) { + if hashAlg == 0 { + return errors.New("Must supply a hash algorithm, eg. crypto.SHA256") + } + if len(keyConfig.Mechanism) == 0 { + keyConfig.Mechanism, err = genMechanismByIdWithOaepParams(pkcs11.CKM_RSA_PKCS_OAEP, hashAlg) + if err != nil { + return err + } + } + return p.encrypt(plainData, encryptedData, keyConfig) +} + +func (p *Pkcs11Client) encrypt(plainData *[]byte, encryptedData *[]byte, keyConfig KeyConfig) (err error) { + attribs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, keyConfig.Type), + pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true), + } + var fullAttribs []*pkcs11.Attribute + if fullAttribs, err = keyConfig.appendKeyIdentity(attribs); err != nil { + return err + } + + var objHandles []pkcs11.ObjectHandle + if objHandles, err = p.FindObjects(fullAttribs, 1); err != nil { + return err + } else if len(objHandles) == 0 { + return errors.New("No key found") + } else { + //mechanism := []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_OAEP, OAEPSha256Params)} + err = p.EncryptWithHandle(plainData, encryptedData, keyConfig.Mechanism, objHandles[0]) + log.Info().Msgf("cipheredText addr=%p", encryptedData) + return err + } +} + +// insecure PKCSv1_15 not supported by FIPS enabled SafeNet HSM but works with SoftHSM +func (p *Pkcs11Client) DecryptRsaPkcs1v15(encryptedData *[]byte, plainData *[]byte, keyConfig KeyConfig) (err error) { + keyConfig.Mechanism, err = GenMechanismById(pkcs11.CKM_RSA_PKCS) + return p.decrypt(encryptedData, plainData, keyConfig) +} + +func (p *Pkcs11Client) DecryptRsaPkcsX509(encryptedData *[]byte, plainData *[]byte, keyConfig KeyConfig) (err error) { + keyConfig.Mechanism, err = GenMechanismById(pkcs11.CKM_RSA_X_509) + return p.decrypt(encryptedData, plainData, keyConfig) +} + +// RSA OAEP requires additional params +// keyConfig.Mechanism will be auto populated based on the hashAlg unless already set, ie. it can be overridden +// hashAlg is eg. crypto.SHA256 +func (p *Pkcs11Client) DecryptRsaPkcsOaep(encryptedData *[]byte, plainData *[]byte, keyConfig KeyConfig, hashAlg crypto.Hash) (err error) { + if hashAlg == 0 { + return errors.New("Must supply a hash algorithm, eg. crypto.SHA256") + } + if len(keyConfig.Mechanism) == 0 { + keyConfig.Mechanism, err = genMechanismByIdWithOaepParams(pkcs11.CKM_RSA_PKCS_OAEP, hashAlg) + if err != nil { + return err + } + } + return p.decrypt(encryptedData, plainData, keyConfig) +} + +func (p *Pkcs11Client) decrypt(encryptedData *[]byte, plainData *[]byte, keyConfig KeyConfig) (err error) { + + attribs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, keyConfig.Type), + pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true), + } + var fullAttribs []*pkcs11.Attribute + if fullAttribs, err = keyConfig.appendKeyIdentity(attribs); err != nil { + return err + } + var objHandles []pkcs11.ObjectHandle + if objHandles, err = p.FindObjects(fullAttribs, 1); err != nil { + return err + } else { + //mechanism := []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_OAEP, OAEPSha256Params)} + err = p.DecryptWithHandle(encryptedData, plainData, keyConfig.Mechanism, objHandles[0]) + return err + } +} + +func (p *Pkcs11Client) EncryptWithHandle(plainData *[]byte, + encryptedData *[]byte, + mechanism []*pkcs11.Mechanism, + objHandle pkcs11.ObjectHandle) (err error) { + + if err = p.context.EncryptInit(p.session, mechanism, objHandle); err != nil { + return err + } + + // single part call - C_EncryptFinal is only needed if C_EncryptUpdate is used + if *encryptedData, err = p.context.Encrypt(p.session, *plainData); err != nil { + return err + } else { + return + } +} + +func (p *Pkcs11Client) DecryptWithHandle(encryptedData *[]byte, + plainText *[]byte, + mechanism []*pkcs11.Mechanism, + objHandle pkcs11.ObjectHandle) (err error) { + + if err = p.context.DecryptInit(p.session, mechanism, objHandle); err != nil { + return err + } + + // single part call, C_DecryptFinal is only needed if C_DecryptUpdate is used + if *plainText, err = p.context.Decrypt(p.session, *encryptedData); err != nil { + return err + } else { + return + } +} + +func (p *Pkcs11Client) FindObjects(attribs []*pkcs11.Attribute, max int) (objHandles []pkcs11.ObjectHandle, err error) { + + if err = p.context.FindObjectsInit(p.session, attribs); err != nil { + return + } + + if objHandles, _, err = p.context.FindObjects(p.session, max); err != nil { + return + } else { + if err = p.context.FindObjectsFinal(p.session); err != nil { + return + } + if objHandles == nil || len(objHandles) == 0 { + err = errors.New("PKCS11 FindObjects empty") + } + return + } +} + +// https://stackoverflow.com/a/25181584/2002211 + +func (p *Pkcs11Client) ReadRSAPublicKey(keyConfig *KeyConfig) (pubKey interface{}, err error) { + return p.ReadPublicKey(keyConfig, pkcs11.CKK_RSA) +} + +func (p *Pkcs11Client) ReadECPublicKey(keyConfig *KeyConfig) (pubKey interface{}, err error) { + return p.ReadPublicKey(keyConfig, pkcs11.CKK_EC) +} + +func (p *Pkcs11Client) ReadPublicKey(keyConfig *KeyConfig, pubKeyType uint) (pubKey interface{}, err error) { + + attribs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + } + + var fullAttribs []*pkcs11.Attribute + if fullAttribs, err = (keyConfig).appendKeyIdentity(attribs); err != nil { + return + } + var objHandles []pkcs11.ObjectHandle + if objHandles, err = p.FindObjects(fullAttribs, 1); err != nil { + return + } + switch pubKeyType { + //case pkcs11.CKK_RSA: + case pkcs11.CKK_ECDSA: + return p.GetECDSAPublicKey(objHandles[0]) + } + return nil, nil +} + +// https://github.com/letsencrypt/boulder/blob/release-2021-02-08/pkcs11helpers/helpers.go#L208 +func (p *Pkcs11Client) GetECDSAPublicKey(object pkcs11.ObjectHandle) (*ecdsa.PublicKey, error) { + // Retrieve the curve and public point for the generated public key + attrs, err := p.context.GetAttributeValue(p.session, object, []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, nil), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, nil), + }) + if err != nil { + return nil, fmt.Errorf("Failed to retrieve key attributes: %s", err) + } + + pubKey := &ecdsa.PublicKey{} + var pointBytes []byte + for _, a := range attrs { + switch a.Type { + case pkcs11.CKA_EC_PARAMS: + rCurve, present := oidDERToCurve[fmt.Sprintf("%X", a.Value)] + if !present { + return nil, errors.New("Unknown curve OID value returned") + } + pubKey.Curve = rCurve + case pkcs11.CKA_EC_POINT: + pointBytes = a.Value + } + } + if pointBytes == nil || pubKey.Curve == nil { + return nil, errors.New("Couldn't retrieve EC point and EC parameters") + } + + x, y := elliptic.Unmarshal(pubKey.Curve, pointBytes) + if x == nil { + // http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/os/pkcs11-curr-v2.40-os.html#_ftn1 + // PKCS#11 v2.20 specified that the CKA_EC_POINT was to be stored in a DER-encoded + // OCTET STRING. + var point asn1.RawValue + _, err = asn1.Unmarshal(pointBytes, &point) + if err != nil { + return nil, fmt.Errorf("Failed to unmarshal returned CKA_EC_POINT: %s", err) + } + if len(point.Bytes) == 0 { + return nil, errors.New("Invalid CKA_EC_POINT value returned, OCTET string is empty") + } + x, y = elliptic.Unmarshal(pubKey.Curve, point.Bytes) + if x == nil { + return nil, errors.New("Invalid CKA_EC_POINT value returned, point is malformed") + } + } + pubKey.X, pubKey.Y = x, y + + return pubKey, nil +} + +func (p *Pkcs11Client) ReadExistsPublicKey(keyConfig *KeyConfig) (publicKey []byte, err error) { + + attribs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + } + + var fullAttribs []*pkcs11.Attribute + if fullAttribs, err = (*keyConfig).appendKeyIdentity(attribs); err != nil { + return + } + var objHandles []pkcs11.ObjectHandle + if objHandles, err = p.FindObjects(fullAttribs, 1); err != nil { + return + } + + attrs, err := p.context.GetAttributeValue(p.session, objHandles[0], []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_ID, nil), + }) + if err != nil { + return + } + if len(attrs) == 1 && attrs[0].Type == pkcs11.CKA_VALUE { + return attrs[0].Value, nil + } + return nil, errors.New("GetAttributeValue error") + + //pKey := rsa.PublicKey{N: n, E: int(e)} + + //return nil +} + +// getPublicKeyID looks up the given public key in the PKCS#11 token, and +// returns its ID as a []byte, for use in looking up the corresponding private +// key. +/*func (p *Pkcs11Client) GetPublicKey(label string, publicKey crypto.PublicKey) ([]byte, error) { + + var template []*pkcs11.Attribute + switch key := publicKey.(type) { + case *rsa.PublicKey: + template = []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_RSA), + pkcs11.NewAttribute(pkcs11.CKA_MODULUS, key.N.Bytes()), + pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(key.E)).Bytes()), + } + case *ecdsa.PublicKey: + // http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/os/pkcs11-curr-v2.40-os.html#_ftn1 + // PKCS#11 v2.20 specified that the CKA_EC_POINT was to be store in a DER-encoded + // OCTET STRING. + rawValue := asn1.RawValue{ + Tag: 4, // in Go 1.6+ this is asn1.TagOctetString + Bytes: elliptic.Marshal(key.Curve, key.X, key.Y), + } + marshalledPoint, err := asn1.Marshal(rawValue) + if err != nil { + return nil, err + } + curveOID, err := asn1.Marshal(curveOIDs[key.Curve.Params().Name]) + if err != nil { + return nil, err + } + template = []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, curveOID), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, marshalledPoint), + } + default: + return nil, fmt.Errorf("unsupported public key of type %T", publicKey) + } + + publicKeyHandle, err := s.FindObject(template) + if err != nil { + return nil, err + } + + attrs, err := s.Module.GetAttributeValue(p.session, publicKeyHandle, []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_ID, nil), + }) + if err != nil { + return nil, err + } + if len(attrs) == 1 && attrs[0].Type == pkcs11.CKA_ID { + return attrs[0].Value, nil + } + return nil, fmt.Errorf("invalid result from GetAttributeValue") +}*/ diff --git a/pkg/pkcs11client/pkcs11client_test.go b/pkg/pkcs11client/pkcs11client_test.go new file mode 100644 index 0000000..27ac0c1 --- /dev/null +++ b/pkg/pkcs11client/pkcs11client_test.go @@ -0,0 +1,178 @@ +package pkcs11client + +import ( + "crypto" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" + "github.com/miekg/pkcs11" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "testing" +) + +var pkcs11Client Pkcs11Client + +// test signing +var caFiles = CASigningRequest{ + csrFile: "../../data/localhost512.csr.der", + caPubkeyFile: "../../data/softhsm-inter-0002.ca.pub.pem", + caCertFile: "../../data/softhsm-inter-0002.ca.cert.der", +} + +// test encryption +var keyConfig = KeyConfig{Label: "RSATestKey0020", Type: pkcs11.CKK_RSA} + +func init() { + + pkcs11Client.HsmConfig = &HsmConfig{ + Lib: "/opt/server/softhsm/current/lib/softhsm/libsofthsm2.so", + SlotId: 288648064, + Pin: "1234", + ConnectTimeoutS: 10, + ReadTimeoutS: 30, + } + +} + +func registerTest(t *testing.T) { + registerCleanup(t) + registerConnection(t) +} + +func registerConnection(t *testing.T) { + if err := pkcs11Client.InitAndLoginWithTimeout(); err != nil { + t.Fatal(err) + } +} + +func registerCleanup(t *testing.T) { + t.Cleanup(func() { + //pkcs11Client.Logout() + pkcs11Client.Cleanup() + log.Info().Msg("Cleaned up!") + }) +} + +func TestCASigner(t *testing.T) { + registerTest(t) + + csr, err := LoadCertRequestFromFile(caFiles.csrFile) + if err != nil { + t.Fatal(err) + } else { + log.Info().Msg("Loaded CSR with CN=" + csr.Subject.CommonName) + + if caCert, err := LoadCertFromFile(caFiles.caCertFile); err != nil { + t.Fatal(err) + } else { + log.Info().Msg("Loaded CA cert with CN=" + caCert.Subject.CommonName) + + if caPubKey, err := LoadPubkeyFromFile(caFiles.caPubkeyFile); err != nil { + t.Fatalf("Err %s %s", err, " check -----BEGIN RSA PUBLIC KEY-----") + } else { + log.Info().Msgf("Loaded CA pubkey") // with E=%d", caPubKey.E) + + var caSigner HsmSigner + caSigner.Serial = 4128 + caSigner.PublicKey = caPubKey + caSigner.KeyConfig.Label = "RSATestCAInterKey0002" + caSigner.Pkcs11Client = &pkcs11Client + caSigner.SignatureAlgo = x509.SHA512WithRSA //ECDSAWithSHA512 + + if signedCsr, err := GenSignedCert(csr, caCert, &caSigner); err != nil { + t.Fatal(err) + } else { + log.Info().Msg("Signed CSR with CN=" + signedCsr.Subject.CommonName) + if err = SaveCertToFile("../../data/signedcert.der", signedCsr); err != nil { + t.Fatal(err) + } else { + log.Info().Msg("Saved signed cert") + } + } + } + } + } +} + +func TestReadExistsPubKey(t *testing.T) { + registerTest(t) + keyConfig := &KeyConfig{ + Label: "ECTestCAInterKey0016", + } + data, err := pkcs11Client.ReadExistsPublicKey(keyConfig) + + pubPem, _ := pem.Decode(data) + + if pubPem == nil { + t.Fatal(errors.New("no public key in file")) + } + + var _ interface{} + _, err = x509.ParsePKIXPublicKey(pubPem.Bytes) + + if err != nil { + t.Fatal(err) + } + + log.Info().Msgf("Found Public Key") + +} + +func TestReadECPubKey(t *testing.T) { + registerCleanup(t) + keyConfig := &KeyConfig{ + Label: "ECTestCAInterKey0016", + } + pubKey, err := pkcs11Client.ReadECPublicKey(keyConfig) + + if err != nil { + t.Fatal(err) + } else { + log.Info().Msgf("Pubkey data=%s", (pubKey.(*ecdsa.PublicKey)).Params().Name) + } + + log.Info().Msgf("Found Public Key") + +} + +func TestEncrypt(t *testing.T) { + registerTest(t) + plainText := []byte("Test") + var encryptedText []byte + //var cipherText []byte // = make([]byte, 16) + testOverflow := "overflow" + log.Info().Msgf("encryptedText addr=%p overflow=%p", &encryptedText, &testOverflow) + err := pkcs11Client.EncryptRsaPkcsOaep(&plainText, &encryptedText, keyConfig, crypto.SHA256) + if err != nil { + assert.NoError(t, err) + } else { + log.Info().Msgf("cipherText testoverflow=%s testoverflow addr=%p sz=%d dat=%s", + testOverflow, &testOverflow, len(encryptedText), encryptedText) + log.Info().Msgf("cipheredText sz=%d", len(encryptedText)) + } +} + +func TestEncryptThenDecrypt(t *testing.T) { + registerTest(t) + plainText := []byte("Test") + var encryptedText []byte + log.Info().Msgf("encryptedText addr=%p", &encryptedText) + err := pkcs11Client.EncryptRsaPkcsOaep(&plainText, &encryptedText, keyConfig, crypto.SHA512) + if err != nil { + t.Fatal(err) + } else { + log.Info().Msgf("encryptedText addr=%p", &encryptedText) + log.Info().Msgf("encryptedText sz=%d", len(encryptedText)) + //SaveDataToFile("/tmp/out.bin", &encryptedText) + var decryptedText []byte + err = pkcs11Client.DecryptRsaPkcsOaep(&encryptedText, &decryptedText, keyConfig, crypto.SHA512) + if err != nil { + t.Fatal(err) + } else { + log.Info().Msgf("decrypted text %s", decryptedText) + assert.Equal(t, plainText, decryptedText) + } + } +} diff --git a/pkg/pkcs11client/pkcs11const.go b/pkg/pkcs11client/pkcs11const.go new file mode 100644 index 0000000..2ff7e45 --- /dev/null +++ b/pkg/pkcs11client/pkcs11const.go @@ -0,0 +1,30 @@ +package pkcs11client + +import ( + "crypto/elliptic" + "encoding/asn1" + "github.com/miekg/pkcs11" +) + +const ( + CKM_EDDSA_NACL = (pkcs11.CKM_VENDOR_DEFINED + 0xC02) // ed25519 sign/verify - NaCl compatible + CKM_EDDSA = (pkcs11.CKM_VENDOR_DEFINED + 0xC03) // ed25519 sign/verify + + CKK_EC_EDWARDS = (pkcs11.CKK_VENDOR_DEFINED + 0x12) +) + +// https://tools.ietf.org/html/rfc5759#section-3.2 +var curveOIDs = map[string]asn1.ObjectIdentifier{ + "P-256": {1, 2, 840, 10045, 3, 1, 7}, + "P-384": {1, 3, 132, 0, 34}, +} + +// https://github.com/letsencrypt/boulder/blob/release-2021-02-08/pkcs11helpers/helpers.go#L208 +// oidDERToCurve maps the hex of the DER encoding of the various curve OIDs to +// the relevant curve parameters +var oidDERToCurve = map[string]elliptic.Curve{ + "06052B81040021": elliptic.P224(), + "06082A8648CE3D030107": elliptic.P256(), + "06052B81040022": elliptic.P384(), + "06052B81040023": elliptic.P521(), +} diff --git a/pkg/pkcs11client/utils.go b/pkg/pkcs11client/utils.go new file mode 100644 index 0000000..e74cade --- /dev/null +++ b/pkg/pkcs11client/utils.go @@ -0,0 +1,138 @@ +package pkcs11client + +import ( + "crypto/rand" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "errors" + "github.com/rs/zerolog/log" + "io/ioutil" + "math/big" + "time" +) + +type rfc5480ECDSASignature struct { + R, S *big.Int +} + +func LoadCertRequestFromFile(filename string) (*x509.CertificateRequest, error) { + + fileData, err := ioutil.ReadFile(filename) + + if err != nil { + return nil, err + } + + return x509.ParseCertificateRequest(fileData) +} + +func LoadCertFromFile(filename string) (*x509.Certificate, error) { + + fileData, err := ioutil.ReadFile(filename) + + if err != nil { + return nil, err + } + + return x509.ParseCertificate(fileData) +} + +func SaveCertToFile(filename string, cert *x509.Certificate) error { + + err := ioutil.WriteFile(filename, cert.Raw, 0644) + + return err +} + +func LoadPubkeyFromFile(filename string) (interface{}, error) { + + fileData, err := ioutil.ReadFile(filename) + + if err != nil { + return nil, err + } + + pubPem, _ := pem.Decode(fileData) + + if pubPem == nil { + return nil, errors.New("no public key in file") + } + + var parsedKey interface{} + + if parsedKey, err = x509.ParsePKIXPublicKey(pubPem.Bytes); err != nil { + return nil, errors.New("Unable to parse pubkey") + } + + return parsedKey, err +} + +func SaveDataToFile(filename string, fileData *[]byte) (err error) { + + err = ioutil.WriteFile(filename, *fileData, 0x644) + + if err != nil { + return err + } + return nil +} + +func GenSignedCert(csr *x509.CertificateRequest, + caCert *x509.Certificate, + caSigner *HsmSigner) (signedCert *x509.Certificate, err error) { + + // log.Info().Msgf("CA exp=%d", caPubKey.E) + + if caSigner.SignatureAlgo == x509.UnknownSignatureAlgorithm { + return nil, errors.New("Please specify a signature algorith, eg. x509.ECDSAwithSHA512") + } + + // https://pkg.go.dev/crypto/x509#Certificate + template := &x509.Certificate{ + Subject: csr.Subject, + SerialNumber: big.NewInt(caSigner.Serial), + DNSNames: csr.DNSNames, //[]string { "localhost2", "mode51.software" }, + EmailAddresses: csr.EmailAddresses, + IPAddresses: csr.IPAddresses, + URIs: csr.URIs, + //ExtraExtensions: csr.ExtraExtensions, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + IsCA: false, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, //x509.KeyUsageCertSign, + BasicConstraintsValid: true, + SignatureAlgorithm: caSigner.SignatureAlgo, + } + + log.Info().Msgf("pubkey algo=%s", csr.PublicKeyAlgorithm) + + // Chrome rejects a cert that only has a common name and no SubjectAlternativeName + // The X.509 module automatically adds a SAN entry when any of DNSNames or IPAddresses are populated + //if len(csr.DNSNames) == 0 && len(csr.IPAddresses) == 0 { //} && len(csr.EmailAddresses) == 0 { + // template.DNSNames = []string{csr.Subject.CommonName} + //} + + certBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, csr.PublicKey, *caSigner) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(certBytes) + + return cert, err +} + +// https://github.com/letsencrypt/pkcs11key/blob/c9e453037c675bb913cde3388b43a9828b2b6a1d/v4/key.go#L508 +func ecdsaPKCS11ToRFC5480(pkcs11Signature []byte) (rfc5480Signature []byte, err error) { + mid := len(pkcs11Signature) / 2 + + r := &big.Int{} + s := &big.Int{} + + return asn1.Marshal(rfc5480ECDSASignature{ + R: r.SetBytes(pkcs11Signature[:mid]), + S: s.SetBytes(pkcs11Signature[mid:]), + }) +}